1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【VBA × WebView2】脱IE! 「Edge内蔵PDFビューア」からPDFをダウンロードする

1
Last updated at Posted at 2026-07-18

はじめに
私の汎用RPAエンジンの機能追加等は終了(余りコード需要も無いようですが)としていましたが、追加機能が必要になり、私の作業メモでごめんなさい。

今回は、厄介!?な「ブラウザ内蔵のPDFビューア」です、業務システムで帳票を出力すると、EdgeネイティブのPDFビューアが立ち上がることがよくあります。この画面から「名前を付けて保存」を自動化しようと何度も試行錯誤を重ねましたが、どうしても安定(サイトによる)しませんでした。
今回も、AIさんの力を借りてようやく「安定するダウンロード手法(の一つ)」に辿り着いたので、その開発のポイントとアプローチをメモします。

PDF画面から「保存」ボタンを押す。人間なら一瞬の操作ですが、RPA(コード)からやろうとすると以下の壁にぶつかりました。

●プラグイン空間の罠: Edge内蔵のPDFビューアは、通常のHTML DOMとは完全に隔離されたネイティブ空間で描画されています。自作のShadow DOM貫通アルゴリズムを使っても、保存ボタン(#download)にはJavaScriptから一切干渉できませんでした。
●UIA(UI Automation)要素探索
DOMがダメならOS側からアプローチしようと、画面中央を物理クリック(Invoke-DesktopCenterClick)してフォーカスを奪った後、以下のようにWindows標準のUIA技術を使ってボタンを探すコードを試しました。

VBAのWin32 APIでの「画面中央を物理クリック」をPowerShellエンジン側へ

試行関数:Invoke-DesktopCenterClick (Lib-DesktopUIA_v103に追加しました。)
function Invoke-DesktopCenterClick {
    param ([int]$WaitMs = 500)

    $func = $MyInvocation.MyCommand.Name

    try {
        # 画面のフォーカスが安定するまで少し待機
        Start-Sleep -Milliseconds $WaitMs
        
        # 最前面のウィンドウハンドルを取得
        $hwnd = [Win32Api.Win32Utils]::GetForegroundWindow()
        if ($hwnd -eq [IntPtr]::Zero) {
            throw "最前面のウィンドウが取得できません"
        }
        
        # ウィンドウの矩形領域を取得
        $rect = New-Object Win32Api.RECT
        $res = [Win32Api.Win32Utils]::GetWindowRect($hwnd, [ref]$rect)
        if (-not $res) {
            throw "ウィンドウ領域の取得に失敗しました"
        }
        
        # 画面中央の座標を計算
        $centerX = [int]($rect.Left + (($rect.Right - $rect.Left) / 2))
        $centerY = [int]($rect.Top + (($rect.Bottom - $rect.Top) / 2))
        
        # マウスカーソルを中央へ移動
        [Win32Api.Win32Utils]::SetCursorPos($centerX, $centerY) | Out-Null
        
        # 物理クリックの発火 (LeftDown: 0x0002, LeftUp: 0x0004)
        [Win32Api.Win32Utils]::mouse_event(0x0002, 0, 0, 0, 0)
        [Win32Api.Win32Utils]::mouse_event(0x0004, 0, 0, 0, 0)
        
        return "Clicked Center: X=$centerX, Y=$centerY"

    } catch {
        throw (New-EngineException -Func $func -Type "UIAエラー" -Message "画面中央の物理クリックに失敗しました" -Details $_.Exception.Message)
    }
}
 なぜ「画面中央を物理クリック」する? ●PDFビューアは「Webページ」ではない
●「ウィンドウが前にある」=「キー入力ができる」ではない
●JavaScriptが無視される以上、人と同じようにOS(Windows)のレベルから直接「ここをクリックしたよ」という信号(マウスイベント)を送るしかない。
●PDFが表示されている画面の端の方をクリックしてしまうと、たまたま「スクロールバー」や「黒いツールバーの隙間」などに当たってしまい、フォーカスが正しくPDF本体に移らないリスクがある。
ウィンドウの「ど真ん中(縦の半分の位置、横の半分の位置)」であれば、どんなレイアウトやPDFのサイズであっても、確実にPDFの文書領域にヒットする。
' パターンA:「保存」ボタンを探してクリック
rpaEngine.RunAction "Invoke-UiaAction", CreateParams("Action", "Click", "Name", "保存")
' パターンB:「名前を付けて保存」を探してクリック
If Err.Number <> 0 Then
     Err.Clear
     rpaEngine.RunAction "Invoke-UiaAction", CreateParams("Action", "Click", "Name", "名前を付けて保存")
End If

これもダメでした。理由は「OSとブラウザの壁」です。EdgeのPDFビューアのツールバーは、標準的なWindowsのボタンではなく、ブラウザ内部で独自に描画された特殊なUI(PDFium)。OSのアクセシビリティ機能(UIA)からは「保存」という名前のボタンとして認識されないためタイムアウトでした。

●物理操作(SendKeys)とダイアログ捕捉
UIAでのボタン探索も諦め、 Ctrl + S のキーボード送信に頼りましたが、裏で別の画面が立ち上がったり、画面が最小化されているとキー送信が空振りします。

「Fetch API」: 画面のボタンを押すこと(物理的なアプローチ)を完全に諦める。

「現在ブラウザが開いているPDFのURL(セッション付き)を、JavaScriptのFetch APIで再取得し、強制的にダウンロードトリガーを引く」という手法を新たに追加しました。
AI(GEMINI)曰く、Fetch APIは、JavaScriptからネットワーク通信を行うための標準機能です。これを使うと大きなメリットがあるよ。

ブラウザに「気を利かせない」
通常URLをクリックすると、ブラウザは「PDFだからビューアで開こう」と画面(UI)を切り替えてしまうがFetch APIを使うと、画面UIを完全に無視して、裏側のネットワーク層で直接「生のデータ(バイナリ)」だけを奪い取れる。
ログイン状態(セッション)を完全共有
業務システム等は「ログインしていないとファイルをダウンロードできない」仕様になっているが、Fetch APIの最大の強みは、「現在ブラウザが開いている画面のログイン状態(Cookieやセッション)を自動的に引き継いで通信してくれる」。(VBA側で面倒な認証処理を作り直す必要がありません。)

以下が、Fetch APIを利用して裏側からPDFを取得し、自作自演でクリックを発火させるJavaScriptのコアロジックです。これをPowerShellエンジンに組み込みました。

// ① 対象のURLへ裏側でリクエストを送り、データを取得する
fetch(target)
    
    // ② 取得したデータを「Blob(生のバイナリデータ)」としてメモリに保持する
    .then(r => r.blob())
    .then(b => {
        // ③ メモリ上のデータにアクセスするための「一時的な内部URL」を作る
        var url = window.URL.createObjectURL(b);
        
        // ④ 画面には見えない「透明なダウンロードリンク」を仮想的に作り、自作自演でクリックする
        var a = document.createElement('a');
        a.href = url;
        a.download = 'auto_download_temp'; 
        document.body.appendChild(a);
        a.click();
        
        // (数秒後に一時URLや透明なリンクをメモリからお掃除して完了)
    });
Invoke-WebFetchDownload を追加しました。(Lib-WebAction_v202)
# --- Fetch APIを利用した裏側でのサイレントダウンロード発火 ---
function Invoke-WebFetchDownload {
    param ([string]$TargetUrl = "")

    $func = $MyInvocation.MyCommand.Name
    $targetUrlEscaped = $TargetUrl.Replace("'", "\'")

    $js = @"
        try {
            // URLの指定がない場合は現在のページURLを使用
            var target = '$targetUrlEscaped';
            if (!target) target = window.location.href;

            fetch(target)
                .then(r => r.blob())
                .then(b => {
                    var a = document.createElement('a');
                    var url = window.URL.createObjectURL(b);
                    a.href = url;
                    a.download = 'auto_download_temp'; // 実際のファイル名はエンジン側で強制上書きされる
                    document.body.appendChild(a);
                    a.click();
                    
                    // メモリ解放とDOMのお掃除
                    setTimeout(function() {
                        window.URL.revokeObjectURL(url);
                        if (a.parentNode) a.parentNode.removeChild(a);
                    }, 1000);
                });
            return true;
        } catch(e) {
            return false;
        }
"@

    $res = Invoke-WebScript -Js $js
    
    if (-not $res) {
        throw (New-EngineException -Func $func -Type "JSエラー" -Message "Fetch APIを利用したダウンロードトリガーの発火に失敗しました" -Details $TargetUrl)
    }

#● Write-DebugLog -Message "[$func] 情報: Fetch APIによる自己ダウンロードイベントを発火しました" -Level Info
    return "Fetch Download Triggered"
}
VBA側の実行コード例

シンプルな命令を出すだけで、安全・確実なダウンロードが完了します。

    Dim savePath As String
    savePath = "C:\Desktop\テストデータ\Dummy_20260718.pdf"
    ' 1. ダイアログを出さずに保存先とファイル名を強制指定(事前予約)
    rpaEngine.RunAction "Enable-SilentDownload", CreateParams("DownloadDirectory", "C:\Desktop\テストデータ", "FileName", "Dummy_20260718.pdf")
    ' 2. 業務システムのPDF画面へ遷移
    rpaEngine.RunAction "Invoke-WebNavigation", CreateParams("Url", "https://.../dummy.pdf")
    ' 3. PDFの実体(embed要素)の出現待機
    rpaEngine.RunAction "Wait-WebElement", CreateParams("Selector", "embed[type='application/pdf']", "TimeoutSec", 15)
    ' 4. Fetch APIで裏側でのサイレントダウンロード発火
    rpaEngine.RunAction "Invoke-WebFetchDownload"
    ' 5. ダウンロード一時ファイル(.crdownload)の消失とロック解除を監視して待機
    rpaEngine.RunAction "Wait-FileDownload", CreateParams("FilePath", savePath, "TimeoutSec", 30)
    MsgBox "PDFのサイレント保存が完了しました!"

このアプローチのメリット
この3ステップ(事前予約 → Fetch APIで発火 → 完了待機)で、「画面が裏に隠れていても、最小化されていても成功する」ダウンロードが実現しました。
(マウスカーソルの位置や解像度に依存するUI Automationの苦手を克服し、RPA化の汎用性を高める。)

続く予定 、、 Invoke-WebFetchDownload

おわりに
これだけAIさんが身近で応援してくれて、無料で簡単な操作コードが作成出来る時代はありがたい。
今回のPDFダウンロード対応コードも、私のガラクタ倉庫(GITHUBさん)に放り込んでおきます。バグ等を教えていただけたら幸いです。
(Lib-DesktopUIA_v103 / Lib-WebAction_v202 を追加しています。もしも、利用される時はPs_Engine_Core_v202 の # 常時ロード対象モジュールの定義 $alwaysLoadLibs = @(、、、 を修正してください。)
Invoke-UiaSafeSaveAs も一部修正しています。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?