C#のWPFでソースコード表示機能付きWebブラウザを作る。

コンピュータ

WebView2 は、WPF アプリケーションに最新の Web 表示機能を組み込むためのブラウザエンジンです。
本記事では WebView2 を使って簡易的な Web ブラウザを実装し、
実際に画面に表示されているページの HTML(JavaScript 実行後の DOM)を取得・解析する方法を紹介します。
Web 表示だけでなく、リンクや画像 URL の抽出など、検証用途にも使える実装例をまとめています。

ソースコード

ファイル名:WebView2Browser.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
  </ItemGroup>

</Project>

ファイル名:MainWindow.xaml.cs

using Microsoft.Web.WebView2.Core;
using System;
using System.IO;
using System.Windows;
using System.Windows.Input;

namespace WebView2Browser;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Loaded += MainWindow_Loaded;
    }

    private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        var userDataFolder = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "WebView2Browser",
            "UserData"
        );

        Directory.CreateDirectory(userDataFolder);

        var env = await CoreWebView2Environment.CreateAsync(
            null,
            userDataFolder
        );

        await WebView.EnsureCoreWebView2Async(env);

        WebView.CoreWebView2.NavigationStarting += CoreWebView2_NavigationStarting;
        WebView.CoreWebView2.NavigationCompleted += CoreWebView2_NavigationCompleted;
        WebView.CoreWebView2.NewWindowRequested += CoreWebView2_NewWindowRequested;

        Navigate("https://www.bing.com");
    }

    private void CoreWebView2_NewWindowRequested(
        object? sender,
        Microsoft.Web.WebView2.Core.CoreWebView2NewWindowRequestedEventArgs e)
    {
        // 新規ウィンドウを抑止
        e.Handled = true;

        // 同じWebViewで開く
        WebView.CoreWebView2.Navigate(e.Uri);

        Log($"NewWindow suppressed: {e.Uri}");
    }


    private void OnNavigateClick(object sender, RoutedEventArgs e)
    {
        Navigate(AddressBar.Text);
    }

    private void AddressBar_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Enter)
        {
            Navigate(AddressBar.Text);
        }
    }

    private void Navigate(string url)
    {
        if (string.IsNullOrWhiteSpace(url))
            return;

        if (!url.StartsWith("http"))
        {
            url = "https://" + url;
        }

        try
        {
            WebView.CoreWebView2.Navigate(url);
        }
        catch (Exception ex)
        {
            Log($"Navigate error: {ex.Message}");
        }
    }

    private void CoreWebView2_NavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs e)
    {
        AddressBar.Text = e.Uri;
        Log($"Navigating: {e.Uri}");
    }

    // ページ移動完了
    private async void CoreWebView2_NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
    {
        if (WebView.Source != null)
        {
            AddressBar.Text = WebView.Source.ToString();
        }

        Log($"Navigation completed (Success={e.IsSuccess})");

        // ★ タイトル取得
        try
        {
            string? title = await GetPageTitleAsync();
            if (!string.IsNullOrWhiteSpace(title))
            {
                Log($"Title: {title}");
            }
        }
        catch (Exception ex)
        {
            Log($"Title fetch error: {ex.Message}");
        }
    }
    private async Task<string?> GetPageTitleAsync()
    {
        if (WebView.CoreWebView2 == null)
            return null;

        // ExecuteScriptAsync は JSON 文字列を返す
        var result = await WebView.CoreWebView2.ExecuteScriptAsync(
            "document.title"
        );

        // "文字列" → 文字列 に戻す
        return System.Text.Json.JsonSerializer.Deserialize<string>(result);
    }
    // ログ出力
    private void Log(string message)
    {
        LogBox.AppendText(message);
        LogBox.AppendText(Environment.NewLine);
        LogBox.ScrollToEnd();
    }

    // ログクリア
    private void OnClearLogClick(object sender, RoutedEventArgs e)
    {
        LogBox.Clear();
    }

    // ログコピー
    private void OnCopyLogClick(object sender, RoutedEventArgs e)
    {
        if (!string.IsNullOrEmpty(LogBox.Text))
        {
            Clipboard.SetText(LogBox.Text);
        }
    }
    // ページソース取得
    private async void OnGetSourceClick(object sender, RoutedEventArgs e)
    {
        if (WebView.CoreWebView2 == null)
            return;

        // ★ 取得前に必ずログをクリア
        LogBox.Clear();

        Log("Fetching page source...");

        try
        {
            string? html = await GetPageSourceAsync();
            if (!string.IsNullOrEmpty(html))
            {
                LogBox.Clear();
                Log(html);
            }
            else
            {
                Log("Source is empty.");
            }
        }
        catch (Exception ex)
        {
            Log($"Source fetch error: {ex.Message}");
        }
    }
    // ページソース取得の非同期メソッド
    private async Task<string?> GetPageSourceAsync()
    {
        if (WebView.CoreWebView2 == null)
            return null;

        var result = await WebView.CoreWebView2.ExecuteScriptAsync(
            "document.documentElement.outerHTML"
        );

        // ExecuteScriptAsync は JSON 文字列で返るためデシリアライズ
        return System.Text.Json.JsonSerializer.Deserialize<string>(result);
    }    
    // リンク一覧取得
    private async void OnGetLinksClick(object sender, RoutedEventArgs e)
    {
        if (WebView.CoreWebView2 == null)
            return;

        // ★ 取得前にログをクリア
        LogBox.Clear();

        try
        {
            var links = await GetPageLinksAsync();
            if (links != null && links.Count > 0)
            {
                foreach (var link in links)
                {
                    Log(link);
                }
            }
        }
        catch (Exception ex)
        {
            Log(ex.Message);
        }
    }

    // ページ内のリンク一覧を取得する非同期メソッド
    private async Task<List<string>> GetPageLinksAsync()
    {
        var result = await WebView.CoreWebView2.ExecuteScriptAsync(@"
            Array.from(document.querySelectorAll('a[href]'))
                .map(a => a.href);
        ");

        return System.Text.Json.JsonSerializer.Deserialize<List<string>>(result)
            ?? new List<string>();
    }
    // 画像一覧取得
    private async void OnGetImagesClick(object sender, RoutedEventArgs e)
    {
        if (WebView.CoreWebView2 == null)
            return;

        // ★ 取得前にログをクリア
        LogBox.Clear();

        try
        {
            var images = await GetPageImagesAsync();
            if (images != null && images.Count > 0)
            {
                foreach (var src in images)
                {
                    Log(src);
                }
            }
        }
        catch (Exception ex)
        {
            Log(ex.Message);
        }
    }
    // ページ内の画像一覧を取得する非同期メソッド
    private async Task<List<string>> GetPageImagesAsync()
    {
        var result = await WebView.CoreWebView2.ExecuteScriptAsync(@"
            Array.from(document.querySelectorAll('img[src]'))
                .map(img => img.src);
        ");

        return System.Text.Json.JsonSerializer.Deserialize<List<string>>(result)
            ?? new List<string>();
    }

}

ファイル名:MainWindow.xaml

<Window x:Class="WebView2Browser.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
        xmlns:local="clr-namespace:WebView2Browser"
        mc:Ignorable="d"
        Title="WebView2 Browser" Height="700" Width="1000">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- アドレスバー -->
        <DockPanel Grid.Row="0" Margin="4">
            <Button Content="Go"
                    Width="32"
                    Margin="4,0"
                    DockPanel.Dock="Right"
                    Click="OnNavigateClick"/>
            <TextBox x:Name="AddressBar"
                     Height="26"
                     VerticalContentAlignment="Center"
                     KeyDown="AddressBar_KeyDown"/>
        </DockPanel>


        <!-- メイン領域 -->
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="300"/>
                <ColumnDefinition Width="5"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <!-- ログエリア -->
            <DockPanel Grid.Column="0" Margin="4">
                <StackPanel Orientation="Horizontal"
                            DockPanel.Dock="Top"
                            Margin="0,0,0,4">
                    <!-- ログクリアボタン -->
                    <Button Content="ログクリア"
                            Height="28"
                            Click="OnClearLogClick"/>
                    <!-- ログコピー -->
                    <Button Content="ログコピー"
                            Height="28"
                            Click="OnCopyLogClick"/>
                    <!-- ソース取得 -->
                    <Button Content="ソース取得"
                            Height="28"
                            Click="OnGetSourceClick"/>
                    <!-- リンク一覧取得 -->
                    <Button Content="リンク一覧"
                            Height="28"
                            Click="OnGetLinksClick"/>
                    <!-- 画像一覧取得 -->
                    <Button Content="画像一覧"
                            Height="28"
                            Click="OnGetImagesClick"/>
                </StackPanel>
                <!-- ログ表示 -->
                <TextBox x:Name="LogBox"
                        IsReadOnly="True"
                        TextWrapping="Wrap"
                        VerticalScrollBarVisibility="Auto"/>
            </DockPanel>
            
            <!-- 左右の仕切り ここから-->
            <GridSplitter
                Grid.Column="1"
                HorizontalAlignment="Stretch" />  
            <!-- 左右の仕切り ここまで-->

            <!-- WebView2 -->
            <wv2:WebView2 x:Name="WebView"
                          Grid.Column="2"
                          Margin="4"/>
        </Grid>
    </Grid>
</Window>

使い方

  1. 起動すると初期ページのbingが表示される。
  2. アドレスバーにURLを入力し、「Go」ボタンでページの遷移
  3. ページの遷移情報がログエリアに出力
  4. 「ログクリア」ボタンで、ログを消去します
  5. 「ログコピー」ボタンで、ログの内容をクリップボードへコピー
  6. 「ソース取得」ボタンで、表示中のページのソースコード(HTML)を取得
  7. 「リンク取得」ボタンで、a hrefを取得
  8. 「画像取得」ボタンで、img srcを取得

コメント