Blazor Hybrid を用いた WPF アプリを試すと、Ctrl+マウスホイールでページ全体が拡大・縮小してしまいます。業務画面では思わぬ倍率変更が事故の原因になりがち。本記事では WebView2 の正しい設定から BlazorWebView の回避策、JavaScript での抑止まで、実案件で安全にズームを無効化する手順を網羅します。
前提とゴール
本記事のゴールは、WPF Blazor Hybrid アプリで発生する Ctrl + マウスホイールによるページズームを確実に無効化することです。読了後には、次の3つの方法を状況に応じて選択・実装できるようになります。
- (推奨)WebView2 のズーム制御を無効化 …
IsZoomControlEnabled = falseを正しく設定 - (BlazorWebView使用時の回避)内部 WebView2 を取得して設定 … ビジュアルツリーから WebView2 を安全に見つけて設定
- (最小限の代替)JavaScript での抑止 …
wheelイベントでctrlKeyを検知してpreventDefault()
記事中のコードは .NET 8/WPF/WebView2(WPF コントロール)を前提としています。WinForms の場合も考え方は同様です。
なぜズームが起きるのか:仕組みの整理
WPF の BlazorWebView は内部で Microsoft Edge(Chromium)ベースの WebView2 をホストしています。Ctrl + ホイールはブラウザの標準ショートカットとして実装され、WebView2 のデフォルト設定ではこの操作が有効です。したがって、WebView2 側のズーム制御を無効化することが最短・最確実の対処法になります。
最短の解決策:WebView2 のズーム制御を無効化する
WPF で WebView2 を直接使っている場合(最もシンプル)
WPF で標準の WebView2 コントロール(Microsoft.Web.WebView2.Wpf.WebView2)を配置しているアプリでは、初期化完了後に以下の1行を設定するだけで Ctrl + ホイールによるズームが無効化されます。
webView2.CoreWebView2.Settings.IsZoomControlEnabled = false;
実際の初期化コード例を示します(EnsureCoreWebView2Async を正しく待機することが重要)。
// using Microsoft.Web.WebView2.Wpf;
// XAML 側で <wpf:WebView2 x:Name="webView2" /> を配置している前提
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
}
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
// CoreWebView2 を確実に生成
await webView2.EnsureCoreWebView2Async();
// 1) Ctrl+ホイールなどのズームを無効化
webView2.CoreWebView2.Settings.IsZoomControlEnabled = false;
// 2) 念のため現在の倍率を 100% に戻す(任意)
webView2.CoreWebView2.ZoomFactor = 1.0;
// 3) つまみズーム(トラックパッド等)も禁止したい場合(任意)
// ※必要に応じて
// webView2.CoreWebView2.Settings.IsPinchZoomEnabled = false;
// 任意:初期ページへ遷移
webView2.Source = new Uri("https://example/");
}
}
ポイント:
IsZoomControlEnabledは WebView2 ランタイムおよび SDK の対応バージョンが必要です(一般的な開発環境ではすでに満たされます)。EnsureCoreWebView2Async()の前にCoreWebView2に触ろうとするとnull参照になります。必ず初期化完了を待機してください。- ユーザーが以前のセッションで倍率を変更していた場合に備えて、
ZoomFactor = 1.0を明示するのが実務では安全です。
イベントで確実に適用するパターン
初期化イベントで設定するパターンも確実です。
webView2.CoreWebView2InitializationCompleted += (_, e) =>
{
if (e.IsSuccess)
{
var s = webView2.CoreWebView2.Settings;
s.IsZoomControlEnabled = false;
// s.IsPinchZoomEnabled = false; // 必要なら
webView2.CoreWebView2.ZoomFactor = 1.0;
}
};
await webView2.EnsureCoreWebView2Async();
BlazorWebView を使っている場合:2つの現実解
BlazorWebView(WPF 用の Microsoft.AspNetCore.Components.WebView.Wpf.BlazorWebView)は、内部に WebView2 を保持しますが、現行の公開 API ではそのインスタンスを直接取得できません。そのため、以下のいずれかで対処します。
| アプローチ | 概要 | メリット | 注意点 |
|---|---|---|---|
| A. ビジュアルツリーから内部 WebView2 を取得 | BlazorWebView の子要素として生成される WebView2 を探索して設定 | 工数最小。既存構成をほぼ維持 | 内部構造依存。将来の破壊的変更に注意 |
| B. 標準 WebView2 に切り替え(自己ホスト) | Blazor を自己ホスト・配信し、WebView2 で閲覧 | WebView2 を完全制御。将来的な変更の影響が少ない | 初期構築コストがやや高い |
A. ビジュアルツリーから内部 WebView2 を取得(最小工数の回避策)
BlazorWebView のロード完了後、VisualTreeHelper で子孫から WebView2 を探し出し、IsZoomControlEnabled = false を適用します。非公開 API に依存するため将来の変更に備えた例外処理とオプトアウトを入れておくのが実務的です。
// XAML(例)
// <blazor:BlazorWebView x:Name="blazorWebView">
// <blazor:BlazorWebView.HostPage>wwwroot/index.html</blazor:BlazorWebView.HostPage>
// <blazor:RootComponents>
// <blazor:RootComponent Selector="#app" ComponentType="{x:Type local:App}" />
// </blazor:RootComponents>
// </blazor:BlazorWebView>
// ヘルパー:ビジュアルツリーから型 T の最初の子孫を探す
public static class VisualTreeExtensions
{
public static T? FindDescendant<T>(DependencyObject root) where T : DependencyObject
{
var count = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < count; i++)
{
var child = VisualTreeHelper.GetChild(root, i);
if (child is T t)
return t;
var nested = FindDescendant<T>(child);
if (nested is not null)
return nested;
}
return null;
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
}
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
// BlazorWebView が内部要素を生成し終えてから探索
await Dispatcher.InvokeAsync(async () =>
{
var inner = VisualTreeExtensions.FindDescendant<Microsoft.Web.WebView2.Wpf.WebView2>(blazorWebView);
if (inner is null)
{
// 生成タイミングの違いに備えて遅延リトライしてもよい
return;
}
await inner.EnsureCoreWebView2Async();
// ★ ズーム抑止の肝
inner.CoreWebView2.Settings.IsZoomControlEnabled = false;
inner.CoreWebView2.ZoomFactor = 1.0;
// つまみズームも抑止したい場合(任意)
// inner.CoreWebView2.Settings.IsPinchZoomEnabled = false;
});
}
}
実務メモ:
- この方法は 内部構造に依存します。更新で壊れる可能性があるため、例外時は機能を自動的に無効化するなどフェイルセーフを設けましょう。
- UI テスト(Smoke/Regression)に「Ctrl + ホイールで倍率が変わらないこと」を必ず追加してください。
B. 標準 WebView2 に切り替え(Blazor を自己ホスト)
アプリの設計方針として BlazorWebView への密結合を避けたい場合は、Blazor(Server/WASM)を自己ホストし、通常の WebView2 に遷移する構成へ移行します。これにより CoreWebView2.Settings を常時制御できるようになります。
最小構成(Kestrel 自己ホスト + WebView2)
- WPF 側で Kestrel を起動(ポートは 0 指定で動的割り当て)
- WebView2 に
http://localhost:{port}/を設定 - 初期化後に
IsZoomControlEnabled = falseを適用
// WPF 側(Program.cs/App.xaml.cs など)
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting.Server.Features;
public static class LocalServer
{
public static async Task<string> StartAsync(CancellationToken ct = default)
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseUrls("http://127.0.0.1:0"); // エフェメラルポート
// 例:静的ファイル + Razor Components or Blazor WASM を配信
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor(); // or builder.Services.AddRazorComponents();
var app = builder.Build();
app.UseStaticFiles();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
await app.StartAsync(ct);
var addresses = app.Urls; // または IServer から ServerAddressesFeature
return addresses.First(); // 例: "http://127.0.0.1:50093"
}
}
// MainWindow での利用例
public partial class MainWindow : Window
{
private string? _baseUrl;
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
}
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
_baseUrl = await LocalServer.StartAsync();
await webView2.EnsureCoreWebView2Async();
// ★ ズーム無効化
webView2.CoreWebView2.Settings.IsZoomControlEnabled = false;
webView2.CoreWebView2.ZoomFactor = 1.0;
webView2.Source = new Uri(_baseUrl);
}
}
この方式なら将来的に BlazorWebView の内部構造が変わっても影響を受けにくく、WebView2 の機能をフルに活用できます。初期工数は上がりますが、長期保守の観点では十分に検討価値があります。
JavaScript での回避策(フロント側のみで抑止)
フロント側だけでシンプルに抑止したい場合は、wheel イベントで ctrlKey を検知し、既定動作をキャンセルします。WebView2 の設定に触れないため、権限や構成上 コントロールを触れない場面でも適用できます。
window.addEventListener('wheel', (e) => {
if (e.ctrlKey) {
e.preventDefault();
}
}, { passive: false });
補足:
- キャプチャ段階で抑止したい場合は
addEventListenerの第3引数にcapture: trueを加えることを検討してください。 Ctrl + '+'やCtrl + '-'、Ctrl + '0'といったキーボードショートカットも抑止するなら、keydownでの検知が必要です。
window.addEventListener('keydown', (e) => {
if (e.ctrlKey) {
const k = e.key.toLowerCase();
if (k === '+' || k === '-' || k === '0') {
e.preventDefault();
}
}
}, { passive: false });
JavaScript 方式は「簡単・導入しやすい」一方、ブラウザ(エンジン)側のズームショートカットを完全に無効化できないケースや、予期しないアクセシビリティ影響が残ることがあります。可能であれば WebView2 設定での抑止を第一に選択してください。
実装比較と選び方(早見表)
| 方法 | 工数 | 堅牢性 | UX影響 | 推奨度 |
|---|---|---|---|---|
| WebView2 設定(IsZoomControlEnabled=false) | 低 | 高 | 最小 | ★★★★★ |
| BlazorWebView 内部 WebView2 を取得 | 低〜中 | 中(内部構造依存) | 最小 | ★★★★☆ |
| 標準 WebView2+自己ホスト | 中〜高 | 高 | 最小 | ★★★★☆ |
| JavaScript 抑止 | 低 | 中〜低 | 場合により影響 | ★★★☆☆ |
バージョン互換性とチェックリスト
ズーム抑止が効かないときは、バージョンや初期化手順の漏れを疑います。以下を順に確認してください。
| 確認項目 | 詳細 | 対処 |
|---|---|---|
| CoreWebView2 が生成済みか | EnsureCoreWebView2Async() 前は CoreWebView2==null | 初期化完了後に設定する(イベント or await) |
| 設定の適用順序 | ナビゲーション前に設定しておくのが無難 | 初期化後ただちに IsZoomControlEnabled=false |
| ランタイム/SDK の整合 | 古い WebView2 ランタイムではプロパティ未実装の可能性 | ランタイムを更新し、NuGet のバージョンも合わせる |
| ピンチズームの禁止 | トラックパッド等のピンチ操作は別設定 | IsPinchZoomEnabled=false(必要時のみ) |
| キーボードショートカット | AreBrowserAcceleratorKeysEnabled=false で広く抑止可能 | ただし F5/Back 等も無効化されるため注意 |
実務で役立つユーティリティ:ズーム無効化の拡張メソッド
複数画面で再利用できるよう、WebView2 向けの簡易ヘルパーを用意しておくと便利です。
public static class WebView2ZoomGuard
{
public static async Task ApplyAsync(Microsoft.Web.WebView2.Wpf.WebView2 view,
bool disablePinch = false)
{
await view.EnsureCoreWebView2Async();
var s = view.CoreWebView2.Settings;
s.IsZoomControlEnabled = false;
if (disablePinch)
{
// 必要に応じてピンチも禁止
// s.IsPinchZoomEnabled = false;
}
// 念のため倍率を初期化
view.CoreWebView2.ZoomFactor = 1.0;
}
}
// 利用側
await WebView2ZoomGuard.ApplyAsync(webView2, disablePinch: true);
アクセシビリティと UX の配慮
- ズーム禁止は利便性を下げる可能性があります。代替として、アプリ内に「フォントサイズ変更」「表の倍率切替」などの UI を用意すると良好な体験を保てます。
- OS の拡大鏡やハイコントラスト設定を利用するユーザーへの案内やヘルプ記載もおすすめです。
- 入力支援デバイス(トラックパッド・タッチ)での拡大縮小は
IsPinchZoomEnabledとの絡みで個別に検討してください。
トラブルシューティング(よくある落とし穴)
- アプリ起動直後は効くが、再ナビゲーションすると効かない
⇒ 新しい CoreWebView2 セッションでは設定が初期化されます。ナビゲーションのフックや再初期化箇所で再適用してください。 - BlazorWebView で内部 WebView2 が見つからない
⇒ 生成タイミングのズレが原因のことがあります。Loaded後にDispatcher.InvokeAsyncで遅延させる、または短いリトライを実装してください。 - Ctrl + ホイール以外(Ctrl + ‘+’等)も無効化したい
⇒AreBrowserAcceleratorKeysEnabled=falseは強力ですが副作用が大きいです。必要なショートカットのみ JS で抑止する方が現実的です。 - 一部 PC でだけズームが効いてしまう
⇒ ランタイムの差異やグループポリシー、仮想環境(RemoteApp 等)での入力合成が影響することがあります。ランタイム更新とポリシー確認を行いましょう。
サンプル構成(WPF + WebView2 直使用)
XAML
<Window x:Class="Sample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
Title="Sample" Height="600" Width="900">
<Grid>
<wpf:WebView2 x:Name="webView2" />
</Grid>
</Window>
コードビハインド
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
}
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
await webView2.EnsureCoreWebView2Async();
webView2.CoreWebView2.Settings.IsZoomControlEnabled = false;
webView2.CoreWebView2.ZoomFactor = 1.0;
webView2.Source = new Uri("https://example/");
}
}
サンプル構成(WPF + BlazorWebView 回避策)
XAML
<Window x:Class="Hybrid.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Wpf;assembly=Microsoft.AspNetCore.Components.WebView.Wpf"
Title="Hybrid" Height="600" Width="900">
<Grid>
<blazor:BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
<blazor:BlazorWebView.RootComponents>
<blazor:RootComponent Selector="#app" ComponentType="{x:Type local:App}" />
</blazor:BlazorWebView.RootComponents>
</blazor:BlazorWebView>
</Grid>
</Window>
コードビハインド(内部 WebView2 へ適用)
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += OnLoaded;
}
private async void OnLoaded(object? sender, RoutedEventArgs e)
{
var inner = VisualTreeExtensions.FindDescendant<Microsoft.Web.WebView2.Wpf.WebView2>(blazorWebView);
if (inner is null) return;
await inner.EnsureCoreWebView2Async();
inner.CoreWebView2.Settings.IsZoomControlEnabled = false;
inner.CoreWebView2.ZoomFactor = 1.0;
}
}
セキュリティ・保守の観点
- 依存関係の更新:WebView2 ランタイム/SDK、Blazor 関連パッケージは定期的に更新し、ズーム制御の既定動作に変更がないかをリリースノートで確認しましょう。
- フェイルセーフ:設定適用に失敗した場合でも致命的な UI 崩れを起こさない設計(フォントサイズの上限/下限、スクロール領域の確保)を取り入れます。
- テスト自動化:CI で「Ctrl + ホイールによる倍率変化なし」をチェックする Playwright/WinAppDriver の簡易スクリプトを用意しておくと回 regressions を防げます。
まとめ
WPF Blazor Hybrid での Ctrl + ホイール拡大縮小は、WebView2 のズーム制御を無効化するのが最短・確実な解です。BlazorWebView の公開 API が追いついていない場面では、内部 WebView2 を探索して適用するか、標準 WebView2 に切り替える構成で堅牢に運用できます。最小工数なら JavaScript での抑止も有効ですが、長期保守・確実性の観点ではエンジン側の設定で封じ込めるのが王道です。実務では「初期化を正しく待つ」「バージョン整合」「再ナビゲーション時の再適用」を押さえ、UI テストにズーム抑止の検証を組み込みましょう。

コメント