PowerShell と WPF を組み合わせて、マウスカーソル下のリンク先 URL をポップアップ表示するスクリプトを作成しました。
対象ブラウザを限定したり、ブロックリストで危険なサイトを赤色表示するなど、実用性の高い機能を盛り込んでいます。
主な機能
- マウスカーソル下の要素からリンク(URL)を検出して表示
- Chrome / Edge / Firefox など対象ブラウザを限定可能
- ブロックリストに一致する URL は背景を赤色で警告
- PowerShell を STA モード で自動再起動する仕組み付き
- 起動時に自己テスト用のポップアップを表示可能
事前準備
- PowerShell 5.1 以上が必要です。
(Windows 10 標準搭載の PowerShell でOKです) - ブロックリストファイルを作成します。
例:C:\myTool\AccessDeniedURL\URL_LIST.txt
# コメントは # から始まる example.com https://malicious-site.example
スクリプト本体
以下を .ps1 ファイルとして保存してください。
例: Show-UrlPopup.ps1
#requires -Version 5.1
Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase,UIAutomationClient
# ========= 設定(ここだけ好みに応じて変更) =========
# ① 起動時前面ロック ← 無効化
$LockToForegroundOnStart = $false
# ② プロセス名で限定(必要なものだけ列挙)
$TargetProcessNames = @('chrome','msedge','firefox') # 例。大文字小文字は無視されます
# ③ タイトル正規表現 ← 無効化
$TargetWindowTitleRegex = $null
# 起動直後の自己テストポップアップを出すか(動作確認用)
$EnableStartupSelfTest = $true
# ブロックリスト(1行1ドメイン or URL。例: blogspot.com / https://example.com)
$BlocklistPath = 'C:\myTool\AccessDeniedURL\URL_LIST.txt'
# ================================================
# ==== STA 自動再起動(非 STA なら自分を STA で再起動して即終了)====
if ([System.Threading.Thread]::CurrentThread.GetApartmentState() -ne 'STA') {
Write-Host '[INFO] STA で再起動します…' -ForegroundColor Yellow
try {
$psExe = (Get-Process -Id $PID).Path
} catch {
$psExe = "$env:WINDIR\System32\WindowsPowerShell\v1.0\powershell.exe"
}
$scriptPath = $PSCommandPath
if (-not $scriptPath) { $scriptPath = $MyInvocation.MyCommand.Path }
if ($scriptPath) {
function Quote-Arg([string]$s) {
if ($null -eq $s) { return $null }
if ($s -match '\s|"') { return '"' + ($s -replace '"','`"') + '"' }
return $s
}
$argList = @('-sta','-NoProfile','-ExecutionPolicy','Bypass','-File', $scriptPath) + $args
$argStr = ($argList | ForEach-Object { Quote-Arg $_ } | Where-Object { $_ -ne $null }) -join ' '
Start-Process -FilePath $psExe -ArgumentList $argStr -WorkingDirectory (Get-Location)
exit
} else {
Write-Warning 'STA が必要ですがスクリプトパスを特定できませんでした。手動で -sta 指定で起動してください。'
}
}
# ==== Win32: カーソル座標 / 前面ウィンドウ ====
Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class Native {
[DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT lpPoint);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
public struct POINT { public int X; public int Y; }
}
"@
# ==== URLポップアップ(WPF)====
$popupWindow = New-Object System.Windows.Window
$popupWindow.WindowStyle = 'None'
$popupWindow.AllowsTransparency = $true
$popupWindow.Topmost = $true
$popupWindow.ShowInTaskbar = $false
$popupWindow.SizeToContent = 'WidthAndHeight'
$popupWindow.Padding = [System.Windows.Thickness]::new(10)
$popupWindow.Opacity = 0.96
$popupWindow.IsHitTestVisible = $false # クリック透過
# 背景ブラシ(通常/ブロック)
$bgNormal = [System.Windows.Media.SolidColorBrush]([System.Windows.Media.Color]::FromArgb(200, 32, 32, 32))
$bgBlocked = [System.Windows.Media.SolidColorBrush]([System.Windows.Media.Color]::FromArgb(220, 200, 32, 32))
$popupWindow.Background = $bgNormal
$border = New-Object System.Windows.Controls.Border
$border.CornerRadius = '8'
$border.BorderBrush = [System.Windows.Media.SolidColorBrush]([System.Windows.Media.Color]::FromArgb(160,200,200,200))
$border.BorderThickness = '1'
$border.Padding = '6'
$textBlock = New-Object System.Windows.Controls.TextBlock
$textBlock.TextWrapping = 'Wrap'
$textBlock.MaxWidth = 800
$textBlock.FontFamily = 'Consolas, Meiryo, Segoe UI'
$textBlock.FontSize = 13
$textBlock.Foreground = [System.Windows.Media.Brushes]::White
$border.Child = $textBlock
$popupWindow.Content = $border
# 初期は非表示 & 一度だけハンドルを起こす(prime)
$null = $popupWindow.Hide()
$popupWindow.Opacity = 0.0
$popupWindow.Left = -10000
$popupWindow.Top = -10000
$null = $popupWindow.Show()
$popupWindow.UpdateLayout()
$null = $popupWindow.Hide()
$popupWindow.Opacity = 0.96
# ==== ターゲットウィンドウの特定 ====
[int]$LockedTargetHwnd = 0
if ($LockToForegroundOnStart) {
$h = [Native]::GetForegroundWindow()
if ($h -ne [IntPtr]::Zero) { $LockedTargetHwnd = $h.ToInt32() }
}
# ==== MSAA 役割定数(LegacyIAccessible.Role)====
# ROLE_SYSTEM_LINK = 0x1E (= 30)
$ROLE_SYSTEM_LINK = 0x1E
# ==== ユーティリティ ====
function Get-ElementUnderCursor {
$pt = New-Object Native+POINT
[Native]::GetCursorPos([ref]$pt) | Out-Null
$sysPt = [System.Windows.Point]::new($pt.X, $pt.Y)
[System.Windows.Automation.AutomationElement]::FromPoint($sysPt)
}
function Get-HyperlinkAncestor([System.Windows.Automation.AutomationElement]$el, [int]$maxDepth = 8) {
if (-not $el) { return $null }
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
$cur = $el
$depth = 0
while ($cur -and $depth -le $maxDepth) {
try {
# UIAのControlTypeでハイパーリンクか?
if ($cur.Current.ControlType -eq [System.Windows.Automation.ControlType]::Hyperlink) { return $cur }
} catch {}
# MSAAのロールで「リンク」か?(LegacyIAccessible)
try {
$legacy = $cur.GetCurrentPattern([System.Windows.Automation.LegacyIAccessiblePatternIdentifiers]::Pattern)
if ($legacy) {
$role = $legacy.Current.Role
if ($role -eq $script:ROLE_SYSTEM_LINK) { return $cur }
}
} catch {}
$cur = $walker.GetParent($cur)
$depth++
}
return $null
}
function Get-TopLevelWindowElement([System.Windows.Automation.AutomationElement]$el) {
if (-not $el) { return $null }
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
$top = $el
$max = 64
while ($max -gt 0) {
$p = $walker.GetParent($top)
if (-not $p) { break }
$top = $p; $max--
}
return $top
}
function Is-InTargetWindow([System.Windows.Automation.AutomationElement]$topWinEl) {
if (-not $topWinEl) { return $false }
# ① ハンドル固定があれば最優先
try {
$hwnd = $topWinEl.Current.NativeWindowHandle
if ($LockedTargetHwnd -ne 0) {
if ($hwnd -ne $LockedTargetHwnd) { return $false }
}
} catch {}
# ② プロセス名チェック
if ($TargetProcessNames -and $TargetProcessNames.Count -gt 0) {
try {
$pid = $topWinEl.Current.ProcessId
$proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
if ($proc) {
$name = $proc.ProcessName.ToLowerInvariant()
if (-not ($TargetProcessNames | ForEach-Object { $_.ToLowerInvariant() } | Where-Object { $_ -eq $name })) {
return $false
}
}
} catch {}
}
# ③ タイトル正規表現
if ($TargetWindowTitleRegex) {
try {
$title = [string]$topWinEl.Current.Name
if (-not ($title -match $TargetWindowTitleRegex)) { return $false }
} catch {}
}
return $true
}
# ==== ブロックリスト読み込み ====
$BlockedDomains = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::InvariantCultureIgnoreCase)
if (Test-Path -LiteralPath $BlocklistPath) {
Get-Content -LiteralPath $BlocklistPath -ErrorAction SilentlyContinue | ForEach-Object {
$t = $_.Trim()
if (-not $t -or $t.StartsWith('#')) { return }
# URL形式ならホスト抽出、そうでなければドメインとして扱う
$hostName = $null
if ($t -match '^\w+://') {
try { $u = [Uri]$t } catch {}
if ($u -and $u.Host) { $hostName = $u.Host }
}
if (-not $hostName) { $hostName = $t }
# 正規化
$hostName = $hostName.Trim().Trim('.')
if ($hostName.StartsWith('www.')) { $hostName = $hostName.Substring(4) }
if ($hostName) { [void]$BlockedDomains.Add($hostName) }
}
}
function Get-HostFromUrl([string]$url) {
if ([string]::IsNullOrWhiteSpace($url)) { return $null }
try {
# スキームが無い場合は補う
if ($url -notmatch '^[a-z]+://') { $url = 'https://' + $url }
$u = [Uri]$url
return $u.Host
} catch { return $null }
}
function Is-UrlBlocked([string]$url) {
$h = Get-HostFromUrl $url
if (-not $h) { return $false }
foreach ($d in $BlockedDomains) {
if ($h.Equals($d, [System.StringComparison]::InvariantCultureIgnoreCase)) { return $true }
if ($h.EndsWith('.' + $d, [System.StringComparison]::InvariantCultureIgnoreCase)) { return $true }
}
return $false
}
# ==== ここからリダイレクト復元ロジックを追加 ====
function Try-Decode-Base64Url {
param([string]$s)
if (-not $s) { return $null }
# URLセーフ Base64 -> 標準 Base64 に変換
$b = $s -replace '-','+' -replace '_','/'
switch ($b.Length % 4) {
2 { $b += '==' }
3 { $b += '=' }
1 { return $null }
default {}
}
try {
$bytes = [Convert]::FromBase64String($b)
return [System.Text.Encoding]::UTF8.GetString($bytes)
} catch { return $null }
}
function Resolve-RedirectUrl {
param([string]$redirUrl)
if ([string]::IsNullOrWhiteSpace($redirUrl)) { return $null }
try { $uri = [uri]$redirUrl } catch { return $null }
# クエリパラをハッシュに
$qs = @{}
if ($uri.Query -and $uri.Query.Length -gt 1) {
$pairs = $uri.Query.TrimStart('?').Split('&') | Where-Object { $_ -ne '' }
foreach ($p in $pairs) {
$kv = $p.Split('=',2)
$k = $kv[0]
$v = if ($kv.Count -ge 2) { $kv[1] } else { '' }
$qs[$k] = $v
}
}
# 1) 'u' param (Bing の ck/a 等) を優先
if ($qs.ContainsKey('u')) {
$raw = [System.Uri]::UnescapeDataString($qs['u'])
# u が plain URL の場合もあるし base64 の場合もある。base64 っぽい断片を抽出してデコード試行
$m = [regex]::Match($raw, '[A-Za-z0-9\-_]+={0,2}')
if ($m.Success) {
$dec = Try-Decode-Base64Url $m.Value
if ($dec -and $dec.StartsWith('http', [System.StringComparison]::InvariantCultureIgnoreCase)) {
return $dec
}
}
if ($raw.StartsWith('http')) { return $raw }
}
# 2) q, url, target, to, redirect などの一般的パラ
foreach ($k in @('q','url','target','to','redirect')) {
if ($qs.ContainsKey($k)) {
$val = [System.Uri]::UnescapeDataString($qs[$k])
if ($val -and $val.StartsWith('http')) { return $val }
$m2 = [regex]::Match($val, '[A-Za-z0-9\-_]+={0,2}')
if ($m2.Success) {
$d2 = Try-Decode-Base64Url $m2.Value
if ($d2 -and $d2.StartsWith('http')) { return $d2 }
}
}
}
# 3) パスに base64 が埋まっているケース(簡易探索)
$m3 = [regex]::Match($uri.AbsoluteUri, '[A-Za-z0-9\-_]{16,}={0,2}')
if ($m3.Success) {
$d3 = Try-Decode-Base64Url $m3.Value
if ($d3 -and $d3.StartsWith('http')) { return $d3 }
}
# 4) クエリの全値を unescape して http で始まるものがあれば返す
foreach ($v in $qs.Values) {
$uv = [System.Uri]::UnescapeDataString($v)
if ($uv -and $uv.StartsWith('http')) { return $uv }
}
return $null
}
# ==== URL抽出用のRegex ====
$UrlRegexFull = '(https?://[^\s"''<>]+)'
$UrlRegexDomain = '((?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,})(?:/[^\s"''<>]*)?'
function Add-Candidate([System.Collections.Generic.List[string]]$list, [string]$s) {
if ($s -and $s.Trim().Length -gt 0) { $list.Add([string]$s) }
}
function Collect-CandidatesFromElement([System.Windows.Automation.AutomationElement]$el) {
$list = [System.Collections.Generic.List[string]]::new()
if (-not $el) { return $list }
try {
$c = $el.Current
Add-Candidate $list $c.Name
$help = $el.GetCurrentPropertyValue([System.Windows.Automation.AutomationElement]::HelpTextProperty)
Add-Candidate $list ([string]$help)
} catch {}
try {
$legacy = $el.GetCurrentPattern([System.Windows.Automation.LegacyIAccessiblePatternIdentifiers]::Pattern)
if ($legacy -and $legacy.Current.Value) { Add-Candidate $list ([string]$legacy.Current.Value) }
} catch {}
try {
$valPat = $el.GetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern)
if ($valPat -and $valPat.Current.Value) { Add-Candidate $list ([string]$valPat.Current.Value) }
} catch {}
try {
$textPat = $el.GetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern)
if ($textPat) {
$rng = $textPat.DocumentRange
$txt = $rng.GetText(8192)
Add-Candidate $list $txt
}
} catch {}
return $list
}
function Extract-UrlFromElement([System.Windows.Automation.AutomationElement]$el) {
if (-not $el) { return $null }
# 0) まず「リンク祖先」を特定して、そこからURLを最優先で拾う
$linkEl = Get-HyperlinkAncestor $el 8
if ($linkEl) {
# 祖先リンク要素の候補を優先的に集める
$priorityCands = [System.Collections.Generic.List[string]]::new()
# a) LegacyIAccessible.Value は href 相当率が高い
try {
$legacy = $linkEl.GetCurrentPattern([System.Windows.Automation.LegacyIAccessiblePatternIdentifiers]::Pattern)
if ($legacy) {
if ($legacy.Current.Value) { $priorityCands.Add([string]$legacy.Current.Value) }
# 念のため Name/Description/Help も候補へ
if ($legacy.Current.Name) { $priorityCands.Add([string]$legacy.Current.Name) }
if ($legacy.Current.Description) { $priorityCands.Add([string]$legacy.Current.Description) }
if ($legacy.Current.Help) { $priorityCands.Add([string]$legacy.Current.Help) }
}
} catch {}
# b) UIA の Name / HelpText
try {
$priorityCands.Add([string]$linkEl.Current.Name)
$help = $linkEl.GetCurrentPropertyValue([System.Windows.Automation.AutomationElement]::HelpTextProperty)
if ($help) { $priorityCands.Add([string]$help) }
} catch {}
# c) TextPattern(リンクテキスト全体)
try {
$tp = $linkEl.GetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern)
if ($tp) {
$txt = $tp.DocumentRange.GetText(8192)
if ($txt) { $priorityCands.Add([string]$txt) }
}
} catch {}
# 優先候補から URL を探す(見つかったら復元を試す)
foreach ($s in $priorityCands) {
if ($s) {
$m = [regex]::Match([string]$s, $UrlRegexFull)
if ($m.Success) {
$raw = $m.Value
$resolved = Resolve-RedirectUrl $raw
if ($resolved) { return $resolved } else { return $raw }
}
}
}
# 優先候補からドメイン風文字列を探す(URLが無い場合)
foreach ($s in $priorityCands) {
if ($s) {
$m2 = [regex]::Match([string]$s, $UrlRegexDomain)
if ($m2.Success) {
$val = $m2.Value
if ($val -notmatch '^https?://') { $val = 'https://' + $val }
return $val
}
}
}
# 祖先リンクから取れなかった場合は、通常フローへフォールバック
}
# 1) 自要素から候補収集(既存ロジック)
$cands = Collect-CandidatesFromElement $el
# 2) 親(最大6階層に拡張)
try {
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
$p = $walker.GetParent($el)
$depth = 0
while ($p -and $depth -lt 6) {
foreach ($s in (Collect-CandidatesFromElement $p)) { Add-Candidate $cands $s }
$p = $walker.GetParent($p); $depth++
}
} catch {}
# 3) 子(直下)
try {
$children = $el.FindAll([System.Windows.Automation.TreeScope]::Children,
[System.Windows.Automation.Condition]::TrueCondition)
foreach ($i in 0..($children.Count-1)) {
$child = $children.Item($i)
foreach ($s in (Collect-CandidatesFromElement $child)) { Add-Candidate $cands $s }
}
} catch {}
# 4) 判定(完全URL優先→ドメイン風)
foreach ($s in $cands) {
$m = [regex]::Match([string]$s, $UrlRegexFull)
if ($m.Success) {
$raw = $m.Value
$resolved = Resolve-RedirectUrl $raw
if ($resolved) { return $resolved } else { return $raw }
}
}
foreach ($s in $cands) {
$m2 = [regex]::Match([string]$s, $UrlRegexDomain)
if ($m2.Success) {
$val = $m2.Value
if ($val -notmatch '^https?://') { $val = 'https://' + $val }
return $val
}
}
return $null
}
function Show-UrlPopup([string]$url, [int]$x, [int]$y) {
if ([string]::IsNullOrWhiteSpace($url)) { Hide-UrlPopup; return }
$display = if ($url.Length -gt 280) { $url.Substring(0,277) + '...' } else { $url }
$textBlock.Text = $display
# 背景色の切替(ブロック一致なら赤、そうでなければ通常)
if (Is-UrlBlocked $url) {
if ($popupWindow.Background -ne $bgBlocked) { $popupWindow.Background = $bgBlocked }
} else {
if ($popupWindow.Background -ne $bgNormal) { $popupWindow.Background = $bgNormal }
}
$offsetX = 16; $offsetY = 24
$left = $x + $offsetX
$top = $y + $offsetY
$vsLeft = [System.Windows.SystemParameters]::VirtualScreenLeft
$vsTop = [System.Windows.SystemParameters]::VirtualScreenTop
$vsWidth = [System.Windows.SystemParameters]::VirtualScreenWidth
$vsHeight = [System.Windows.SystemParameters]::VirtualScreenHeight
$popupWindow.Measure([System.Windows.Size]::new([double]::PositiveInfinity,[double]::PositiveInfinity))
$sz = $popupWindow.DesiredSize
if ($left + $sz.Width -gt $vsLeft + $vsWidth) { $left = [math]::Max($vsLeft, ($vsLeft + $vsWidth) - $sz.Width - 8) }
if ($top + $sz.Height -gt $vsTop + $vsHeight) { $top = [math]::Max($vsTop, ($vsTop + $vsHeight) - $sz.Height - 8) }
if ($left -lt $vsLeft) { $left = $vsLeft }
if ($top -lt $vsTop) { $top = $vsTop }
$popupWindow.Left = $left
$popupWindow.Top = $top
if (-not $popupWindow.IsVisible) { $null = $popupWindow.Show() }
}
function Hide-UrlPopup {
if ($popupWindow.IsVisible) { $null = $popupWindow.Hide() }
}
function Close-UrlPopup {
try {
if ($popupWindow) {
if ($popupWindow.Dispatcher.HasShutdownStarted -or $popupWindow.Dispatcher.HasShutdownFinished) { return }
$popupWindow.Dispatcher.Invoke({
try {
if ($this.IsVisible) { $this.Hide() }
$this.Close()
} catch {}
})
}
} catch {}
}
# ==== DispatcherTimer ====
$timer = [System.Windows.Threading.DispatcherTimer]::new()
$timer.Interval = [TimeSpan]::FromMilliseconds(140)
$lastRuntimeId = $null
$lastShownUrl = $null
$lastVisibleTick = [Environment]::TickCount
$hideDelayMs = 250
$timer.Add_Tick({
try {
# カーソル位置
$pt = New-Object Native+POINT
[Native]::GetCursorPos([ref]$pt) | Out-Null
$el = Get-ElementUnderCursor
if ($el) {
# 最上位ウィンドウを特定し、対象か判定
$topWin = Get-TopLevelWindowElement $el
if (-not (Is-InTargetWindow $topWin)) {
Hide-UrlPopup
return
}
$id = ($el.GetRuntimeId() -join '.')
$url = Extract-UrlFromElement $el
if ($url) {
Show-UrlPopup -url $url -x $pt.X -y $pt.Y
$lastShownUrl = $url
$lastVisibleTick = [Environment]::TickCount
} else {
$elapsed = [Environment]::TickCount - $lastVisibleTick
if ($elapsed -ge $hideDelayMs) {
Hide-UrlPopup
$lastShownUrl = $null
}
}
if ($id -ne $lastRuntimeId) {
$lastRuntimeId = $id
$ct = $el.Current.ControlType.ProgrammaticName
$nm = $el.Current.Name
$ht = $el.GetCurrentPropertyValue([System.Windows.Automation.AutomationElement]::HelpTextProperty)
$legacyValue = $null
try {
$legacy = $el.GetCurrentPattern([System.Windows.Automation.LegacyIAccessiblePatternIdentifiers]::Pattern)
if ($legacy) { $legacyValue = $legacy.Current.Value }
} catch {}
#Write-Host "Type=$ct`nName=$nm`nHelpText=$ht`nLegacyValue=$legacyValue`nURL=$url`nTopWin='$($topWin.Current.Name)'`n----"
}
} else {
Hide-UrlPopup
$lastShownUrl = $null
}
} catch {
# 握りつぶし
}
})
$timer.Start()
# ==== 起動自己テスト(任意)====
if ($EnableStartupSelfTest) {
try {
$textBlock.Text = 'TEST: https://example.com'
$popupWindow.Background = $bgNormal
$popupWindow.Left = [System.Windows.SystemParameters]::WorkArea.Left + 60
$popupWindow.Top = [System.Windows.SystemParameters]::WorkArea.Top + 60
$null = $popupWindow.Show()
Start-Sleep -Milliseconds 1200
$null = $popupWindow.Hide()
$textBlock.Text = ''
} catch {}
}
# ==== 終了ハンドリング ====
try {
$global:__UiAuto_ccHandler = [ConsoleCancelEventHandler]{ param($s,$e)
try { $timer.Stop() } catch {}
try { Close-UrlPopup } catch {}
}
[Console]::add_CancelKeyPress($global:__UiAuto_ccHandler) | Out-Null
} catch {}
$global:__UiAuto_exitSub = Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
try {
$t = $ExecutionContext.SessionState.PSVariable.GetValue('timer')
if ($t) { $t.Stop() }
} catch {}
try {
$w = $ExecutionContext.SessionState.PSVariable.GetValue('popupWindow')
if ($w -and -not $w.Dispatcher.HasShutdownStarted) {
$w.Dispatcher.Invoke({ try { if ($this.IsVisible) { $this.Hide() }; $this.Close() } catch {} })
}
} catch {}
}
# ConsoleHost では Dispatcher を自前で回す(ISE は既に回っている)
if ($Host.Name -ne 'Windows PowerShell ISE Host') {
try {
[System.Windows.Threading.Dispatcher]::Run()
} finally {
try { $timer.Stop() } catch {}
try { Close-UrlPopup } catch {}
try {
if ($global:__UiAuto_ccHandler) { [Console]::remove_CancelKeyPress($global:__UiAuto_ccHandler) }
} catch {}
try {
if ($global:__UiAuto_exitSub) {
Unregister-Event -SourceIdentifier PowerShell.Exiting -ErrorAction SilentlyContinue
if ($global:__UiAuto_exitSub.Action -and $global:__UiAuto_exitSub.Action.Job) {
Remove-Job $global:__UiAuto_exitSub.Action.Job -Force -ErrorAction SilentlyContinue
}
}
} catch {}
try { Get-EventSubscriber | Unregister-Event -ErrorAction SilentlyContinue } catch {}
try { Get-Job | Remove-Job -Force -ErrorAction SilentlyContinue } catch {}
}
}
動作イメージ
- 通常サイトのリンク → 灰色背景 のポップアップ
- ブロック対象サイトのリンク → 赤色背景 のポップアップ
これにより、リンク先を確認しながら安全にブラウジングできます。
使い方
- PowerShell で以下を実行:
powershell -sta -ExecutionPolicy Bypass -File .\Show-UrlPopup.ps1
※ -sta は自動で付与されるので、通常の実行でも動作します。
対象ブラウザを操作して、リンクにカーソルを合わせるとポップアップが表示されます。
検証用コード
# Requires -Version 5.1
param([int]$IntervalMs = 120)
# ===== STA でなければ自動再起動 =====
if ([Threading.Thread]::CurrentThread.ApartmentState -ne 'STA') {
$argsList = @('-sta','-NoProfile','-ExecutionPolicy','Bypass','-File', $PSCommandPath)
if ($PSBoundParameters.Count) { $PSBoundParameters.GetEnumerator() | % { $argsList += @("-$($_.Key)","$($_.Value)") } }
Start-Process -FilePath (Get-Command powershell).Source -ArgumentList $argsList | Out-Null
exit
}
# ===== 依存アセンブリ =====
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase
Add-Type @"
using System;
using System.Runtime.InteropServices;
public struct POINT { public int X; public int Y; }
public static class Native {
[DllImport("user32.dll")] public static extern bool GetCursorPos(out POINT pt);
[DllImport("user32.dll")] public static extern short GetAsyncKeyState(int vKey);
}
"@ | Out-Null
# ===== 可用性チェック: LegacyIAccessible* が無い環境に対応 =====
$LegacyIdsType = [Type]::GetType(
"System.Windows.Automation.LegacyIAccessiblePatternIdentifiers, UIAutomationTypes",
$false
)
# ===== 設定 =====
$BlocklistPath = 'C:\myTool\AccessDeniedURL\URL_LIST.txt'
$hideDelayMs = 1200
$lastVisibleTick = 0
# ===== ブロックリスト読み込み =====
$BlockedDomains = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::InvariantCultureIgnoreCase)
if (Test-Path -LiteralPath $BlocklistPath) {
Get-Content -LiteralPath $BlocklistPath -ErrorAction SilentlyContinue | %{
$t = $_.Trim(); if (-not $t -or $t.StartsWith('#')) { return }
# !!! ここを $host → $hostName に変更($Host 自動変数との衝突回避) !!!
$hostName = $null
if ($t -match '^\w+://') {
try { $u = [Uri]$t } catch {}
if ($u -and $u.Host){ $hostName = $u.Host }
}
if (-not $hostName){ $hostName = $t }
$hostName = $hostName.Trim().Trim('.')
if ($hostName.StartsWith('www.')){ $hostName = $hostName.Substring(4) }
if ($hostName){ [void]$BlockedDomains.Add($hostName) }
}
}
# ===== ユーティリティ =====
function Test-EscapePressed { (([Native]::GetAsyncKeyState(0x1B) -band 0x8000) -ne 0) }
function Get-CursorPoint { $pt = New-Object POINT; [Native]::GetCursorPos([ref]$pt) | Out-Null; [Windows.Point]::new($pt.X,$pt.Y) }
function Get-HostFromUrl([string]$url){ if([string]::IsNullOrWhiteSpace($url)){return $null}; try{ if($url -notmatch '^[a-z]+://'){ $url='https://'+$url }; ([Uri]$url).Host }catch{ $null } }
function Is-UrlBlocked([string]$url){
$h=Get-HostFromUrl $url; if(-not $h){ return $false }
foreach($d in $BlockedDomains){
if ($h.Equals($d,[StringComparison]::InvariantCultureIgnoreCase)) {return $true}
if ($h.EndsWith('.'+$d,[StringComparison]::InvariantCultureIgnoreCase)) {return $true}
}; $false
}
function Get-BingRealUrlFromString {
param([string]$Url)
if ([string]::IsNullOrWhiteSpace($Url)) { return $null }
try { $uri=[Uri]$Url } catch { return $Url }
Add-Type -AssemblyName System.Web | Out-Null
$qs=[System.Web.HttpUtility]::ParseQueryString($uri.Query)
$v=$qs["u"]
if (-not $v) { return $Url }
$v = $v -replace '^a1',''
try{
if ($v -match '^[A-Za-z0-9\-_]+=*$') {
$tmp=$v.Replace('-','+').Replace('_','/')
switch ($tmp.Length % 4) { 2{$tmp+='=='} 3{$tmp+='='} }
$bytes=[Convert]::FromBase64String($tmp)
$decoded=[Text.Encoding]::UTF8.GetString($bytes)
if ($decoded -match '^\w+://') { return $decoded }
}
}catch{}
return $v
}
# ===== 文字列(=URLっぽい値)を引き出す =====
function Get-ElementTextLikeValue {
param([System.Windows.Automation.AutomationElement]$El)
if (-not $El){
write-host "-not El"
return $null
}
# 1) ValuePattern
$vp=$null
if ($El.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$vp)) {
$v=$vp.Current.Value;
write-host "ValuePattern: $v"
if ($v) { return $v }
}
$v=$El.GetCurrentPropertyValue([System.Windows.Automation.ValuePattern]::ValueProperty)
if ($v -and $v -ne [System.Windows.Automation.Automation]::NotSupported) {
write-host "NotSupported: $v"
return $v
}
# 2) LegacyIAccessible (存在すれば)
if ($LegacyIdsType) {
$valProp = $LegacyIdsType::ValueProperty
$v = $El.GetCurrentPropertyValue($valProp)
if ($v -and $v -ne [System.Windows.Automation.Automation]::NotSupported) {
write-host "LegacyIAccessible: $v"
return $v
}
}
# 3) Name / HelpText
if ($El.Current.Name) { return $El.Current.Name }
$help=$El.GetCurrentPropertyValue([System.Windows.Automation.AutomationElement]::HelpTextProperty)
if ($help -and $help -ne [System.Windows.Automation.Automation]::NotSupported) {
write-host "Name / HelpText: $help"
return $help
}
$null
}
# ===== WPF ポップアップ =====
$popupWindow = New-Object System.Windows.Window
$popupWindow.WindowStyle='None'
$popupWindow.AllowsTransparency=$true
$popupWindow.Topmost=$true
$popupWindow.ShowInTaskbar=$false
$popupWindow.SizeToContent='WidthAndHeight'
$popupWindow.Padding=[Windows.Thickness]::new(10)
$popupWindow.Opacity=0.96
$popupWindow.IsHitTestVisible=$false
$bgNormal = [Windows.Media.SolidColorBrush]([Windows.Media.Color]::FromArgb(200, 32, 32, 32))
$bgBlocked = [Windows.Media.SolidColorBrush]([Windows.Media.Color]::FromArgb(220,200, 32, 32))
$popupWindow.Background=$bgNormal
$border = New-Object Windows.Controls.Border
$border.CornerRadius='8'
$border.BorderBrush=[Windows.Media.SolidColorBrush]([Windows.Media.Color]::FromArgb(160,200,200,200))
$border.BorderThickness='1'
$border.Padding='6'
$textBlock = New-Object Windows.Controls.TextBlock
$textBlock.TextWrapping='Wrap'
$textBlock.MaxWidth=800
$textBlock.FontFamily='Consolas, Meiryo, Segoe UI'
$textBlock.FontSize=13
$textBlock.Foreground=[Windows.Media.Brushes]::White
$border.Child=$textBlock
$popupWindow.Content=$border
function Show-UrlPopup([string]$url,[int]$x,[int]$y){
if([string]::IsNullOrWhiteSpace($url)){ Hide-UrlPopup; return }
$display = if($url.Length -gt 280){ $url.Substring(0,277) + '...' } else { $url }
$textBlock.Text = $display
$popupWindow.Background = (if (Is-UrlBlocked $url) { $bgBlocked } else { $bgNormal })
$offsetX=16; $offsetY=24
$left=$x+$offsetX; $top=$y+$offsetY
$vsLeft=[Windows.SystemParameters]::VirtualScreenLeft
$vsTop =[Windows.SystemParameters]::VirtualScreenTop
$vsW =[Windows.SystemParameters]::VirtualScreenWidth
$vsH =[Windows.SystemParameters]::VirtualScreenHeight
$popupWindow.Measure([Windows.Size]::new([double]::PositiveInfinity,[double]::PositiveInfinity))
$sz=$popupWindow.DesiredSize
if($left+$sz.Width -gt $vsLeft+$vsW){ $left=[math]::Max($vsLeft,($vsLeft+$vsW)-$sz.Width-8) }
if($top +$sz.Height -gt $vsTop +$vsH){ $top =[math]::Max($vsTop ,($vsTop +$vsH)-$sz.Height-8) }
if($left -lt $vsLeft){ $left=$vsLeft }
if($top -lt $vsTop ){ $top =$vsTop }
$popupWindow.Left=$left; $popupWindow.Top=$top
if(-not $popupWindow.IsVisible){ $null=$popupWindow.Show() }
}
function Hide-UrlPopup { if($popupWindow.IsVisible){ $null=$popupWindow.Hide() } }
# ===== メインループ =====
Write-Host "Move your mouse... (Esc to exit) Polling: ${IntervalMs}ms" -ForegroundColor Cyan
while ($true) {
if (Test-EscapePressed) { break }
$url = $null
try {
$pt = Get-CursorPoint
$el = [System.Windows.Automation.AutomationElement]::FromPoint($pt)
# まず ValuePattern / Legacy / Name から文字列を引く
$s = Get-ElementTextLikeValue -El $el
# 文字列が URL らしければ URL として採用
if ($s -and ($s -match '^[a-z]+://|^[\w\.\-]+\.[A-Za-z]{2,}(/|$)')) {
$url = $s
}
# Bing リダイレクトっぽいものは生 URL へ復号
if ($s -and $s -match '(^https?://www\.bing\.com/)|([?&]u=)') {
$decoded = Get-BingRealUrlFromString $s
if ($decoded) { $s = $decoded }
return $s
}
if ($url) {
Show-UrlPopup -url $url -x $pt.X -y $pt.Y
$lastVisibleTick = [Environment]::TickCount
} else {
if([Environment]::TickCount - $lastVisibleTick -ge $hideDelayMs) { Hide-UrlPopup }
}
}
catch {
# 要素消失等の瞬間例外は握りつぶし
}
Start-Sleep -Milliseconds $IntervalMs
}
Hide-UrlPopup
Write-Host "Stopped." -ForegroundColor Yellow
# Requires -Version 5.1
param(
[switch]$Watch, # 付けると ESC まで監視ループ
[int]$PollMs = 120 # 監視時のポーリング間隔
)
# ===== STA でなければ自動再起動 =====
if ([Threading.Thread]::CurrentThread.ApartmentState -ne 'STA') {
$argsList = @('-sta','-NoProfile','-ExecutionPolicy','Bypass','-File', $PSCommandPath)
if ($PSBoundParameters.Count) {
$PSBoundParameters.GetEnumerator() | ForEach-Object {
if ($_.Value -is [switch]) {
if ($_.Value.IsPresent) { $argsList += "-$($_.Key)" }
} else {
$argsList += @("-$($_.Key)","$($_.Value)")
}
}
}
Start-Process -FilePath (Get-Command powershell).Source -ArgumentList $argsList | Out-Null
exit
}
# ===== 依存アセンブリ =====
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase
# ===== 定数・補助 =====
$ct = [System.Windows.Automation.ControlType]
$ae = [System.Windows.Automation.AutomationElement]
$condTrue = [System.Windows.Automation.Condition]::TrueCondition
function Get-CursorPoint {
Add-Type -Namespace Ux -Name Native -MemberDefinition @"
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct POINT { public int X; public int Y; }
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern bool GetCursorPos(out POINT pt);
"@
$pt = New-Object Ux.Native+POINT
[void][Ux.Native]::GetCursorPos([ref]$pt)
# FromPoint は画面座標(ピクセル)
New-Object System.Windows.Point($pt.X, $pt.Y)
}
function Get-ElementFromCursor {
$p = Get-CursorPoint
try { return [System.Windows.Automation.AutomationElement]::FromPoint($p) }
catch { return $null }
}
function Find-Ancestor {
param(
[Parameter(Mandatory)] [System.Windows.Automation.AutomationElement]$Element,
[ScriptBlock]$Predicate
)
$cur = $Element
while ($cur) {
if ($Predicate -and (& $Predicate $cur)) { return $cur }
$cur = $cur.GetCachedParent()
if (-not $cur) {
$cur = $cur.GetCurrentPropertyValue([System.Windows.Automation.AutomationElement]::NativeWindowHandleProperty) | Out-Null
# 親の取り方を安全に:TreeWalker.RawViewWalker
$cur = [System.Windows.Automation.TreeWalker]::RawViewWalker.GetParent($Element)
}
$Element = $cur
}
return $null
}
function Test-LooksLikeUrl {
param([string]$s)
if (-not $s) { return $false }
return ($s -match '^(https?|file|ftp)://') -or ($s -match '^[\w-]+\.[\w\.-]+(/|$)')
}
function TryGet-UAValue {
param([System.Windows.Automation.AutomationElement]$el)
try {
$p = $el.GetCurrentPattern([System.Windows.Automation.LegacyIAccessiblePattern]::Pattern)
if ($p) {
$val = $p.Current.Value
if (Test-LooksLikeUrl $val) { return $val }
$desc = $p.Current.Description
if (Test-LooksLikeUrl $desc) { return $desc }
}
} catch {}
return $null
}
function TryGet-UAProps {
param([System.Windows.Automation.AutomationElement]$el)
$cands = @(
$el.Current.HelpText,
$el.Current.ItemStatus,
$el.Current.Name
) | Where-Object { $_ }
foreach ($c in $cands) {
# 複数語の中に URL が混ざっている場合を抽出
if ($c -match '(https?://\S+)' ) { return $Matches[1].TrimEnd('…', '.', ',', '、', '。') }
if (Test-LooksLikeUrl $c) { return $c }
}
return $null
}
function Get-TopLevelWindow($el) {
$walker = [System.Windows.Automation.TreeWalker]::RawViewWalker
$cur = $el
while ($cur) {
$parent = $walker.GetParent($cur)
if (-not $parent) { break }
$cur = $parent
}
return $cur
}
function TryGet-StatusBarUrl {
param([System.Windows.Automation.AutomationElement]$anyElementInWindow)
$win = Get-TopLevelWindow $anyElementInWindow
if (-not $win) { return $null }
# 下部パネル/ステータスバーっぽい要素を走査して URL らしきテキストを拾う
$walker = [System.Windows.Automation.TreeWalker]::RawViewWalker
$queue = New-Object System.Collections.Generic.Queue[System.Windows.Automation.AutomationElement]
$queue.Enqueue($win)
while ($queue.Count) {
$n = $queue.Dequeue()
try {
$name = $n.Current.Name
$lct = $n.Current.LocalizedControlType
if ($name -and ($lct -in @('ステータスバー','パネル','ペイン','テキスト','テーブル','リスト'))) {
$hit = TryGet-UAProps $n
if ($hit) { return $hit }
}
} catch {}
# 子ノード列挙
try {
$child = $walker.GetFirstChild($n)
while ($child) {
$queue.Enqueue($child)
$child = $walker.GetNextSibling($child)
}
} catch {}
}
return $null
}
function Decode-BingRedirect {
param([string]$url)
if (-not $url) { return $null }
# 典型: https://www.bing.com/ck/a?!...&u=https%3a%2f%2fexample.com%2fpath%3Fa%3d1&...
try {
$uri = [Uri]$url
$qs = [System.Web.HttpUtility]::ParseQueryString($uri.Query)
foreach ($k in @('u','url','r','RU')) {
$v = $qs[$k]
if ($v) {
$decoded = [System.Uri]::UnescapeDataString($v)
# さらに多重エンコードされている場合の軽い救済
while ($decoded -match '%[0-9A-Fa-f]{2}') {
$next = [System.Uri]::UnescapeDataString($decoded)
if ($next -eq $decoded) { break }
$decoded = $next
}
if (Test-LooksLikeUrl $decoded) { return $decoded }
}
}
} catch {}
return $url
}
function Get-HrefFromCursor {
# 1) カーソル下の要素
$el = Get-ElementFromCursor
if (-not $el) { return [pscustomobject]@{ Source='none'; Url=$null; Element=$null } }
# 2) 自身→祖先へ:リンクらしい要素を探す
$walker = [System.Windows.Automation.TreeWalker]::RawViewWalker
$cur = $el
$linkEl = $null
for ($i=0; $i -lt 8 -and $cur; $i++) {
try {
if ($cur.Current.ControlType -eq $ct::Hyperlink) { $linkEl = $cur; break }
# Chromium は Hyperlink でないことも多いので、Name に "http" などが見えたら候補に
if (Test-LooksLikeUrl $cur.Current.Name) { $linkEl = $cur; break }
} catch {}
$cur = $walker.GetParent($cur)
}
if (-not $linkEl) { $linkEl = $el }
# 3) 直接パターン/プロパティから URL 抽出
$url = TryGet-UAValue $linkEl
if (-not $url) { $url = TryGet-UAProps $linkEl }
# 4) だめならウィンドウのステータスバー等から拾う
if (-not $url) { $url = TryGet-StatusBarUrl $linkEl }
# 5) Bing リダイレクト復元
$final = Decode-BingRedirect $url
[pscustomobject]@{
Source = if ($url) { if ($url -eq $final) { 'uia' } else { 'bing-decoded' } } else { 'none' }
Url = $final
Element = $linkEl
}
}
function Test-BlockedUrl {
param([string]$url, [string[]]$BlockedHosts = @('example.org','bad.site'))
if (-not $url) { return $false }
try {
$u = [Uri]$url
return $BlockedHosts -contains $u.Host
} catch { return $false }
}
# ===== 使い方 =====
if ($Watch) {
Write-Host "Move your mouse over a link... (ESC to exit). Poll: $PollMs ms"
$esc = $false
$last = $null
while (-not $esc) {
Start-Sleep -Milliseconds $PollMs
if ([Console]::KeyAvailable -and [Console]::ReadKey($true).Key -eq 'Escape') { $esc = $true; break }
$res = Get-HrefFromCursor
if ($res.Url -and $res.Url -ne $last) {
$last = $res.Url
$blocked = Test-BlockedUrl $res.Url
Write-Host ("[{0}] {1}{2}" -f $res.Source, $res.Url, $(if($blocked){' (BLOCKED)'}))
}
}
} else {
$res = Get-HrefFromCursor
if ($res.Url) {
$blocked = Test-BlockedUrl $res.Url
"{0}`n{1}" -f $res.Source, $res.Url
if ($blocked) { Write-Warning "This URL is blocked by policy." }
} else {
Write-Warning "URL を見つけられませんでした(要素がリンクでない / ブラウザが href を UIA に公開していない可能性)。"
}
}
まとめ
今回のスクリプトは、PowerShell で UIAutomation を駆使した「リンク先確認ツール」です。
ブラウジング時に「このリンク、怪しくない?」と思ったときに役立つでしょう。
セキュリティ対策や URL 確認の補助ツールとして、ぜひ活用してみてください。