Skip to content

AIGC: ContentProducer: '001191110102MAD55U9H0F10002' ContentPropagator: '001191110102MAD55U9H0F10002' Label: '1' ProduceID: 'c1c917d9-c154-42e1-93f7-0004958fe88a' PropagateID: 'c1c917d9-c154-42e1-93f7-0004958fe88a' ReservedCode1: 'be1f7072-069e-40ef-b933-82e209678b6d' ReservedCode2: 'be1f7072-069e-40ef-b933-82e209678b6d'


AIGC: ContentProducer: '001191110102MAD55U9H0F10002' ContentPropagator: '001191110102MAD55U9H0F10002' Label: '1' ProduceID: '474f524a-3620-4d6d-944b-7d6d5e495014' PropagateID: '474f524a-3620-4d6d-944b-7d6d5e495014' ReservedCode1: 'bd9be2ba-6425-4398-904b-dcd897ea0b65' ReservedCode2: 'bd9be2ba-6425-4398-904b-dcd897ea0b65'


AIGC: ContentProducer: '001191110102MAD55U9H0F10002' ContentPropagator: '001191110102MAD55U9H0F10002' Label: '1' ProduceID: 'bf8064d8-888f-4b85-96ca-ecbd8cefa9b6' PropagateID: 'bf8064d8-888f-4b85-96ca-ecbd8cefa9b6' ReservedCode1: '2b38c4c7-b621-452e-afc7-7d727dfcb23d' ReservedCode2: '2b38c4c7-b621-452e-afc7-7d727dfcb23d'

宿主适配专题 - VB6/Excel/Access 多宿主集成

📖 目录


概述

cWebView2Host 需要将 WebView2 子窗口嵌入到宿主应用程序的窗口中,并通过消息拦截实现事件桥接。不同宿主环境(VB6、Excel UserForm、Access Form)的窗口机制差异较大,因此设计了适配器层来屏蔽差异。

✨ 核心特点

  • 🔄 自动检测 - 根据 Windows 类名自动选择适配器
  • 🖥️ 无缝适配 - VB6/Excel/Access 使用完全相同的 API
  • 🛡️ Access 安全 - 消息窗口适配器避免 Access 窗口子类化崩溃
  • 📡 事件桥接 - 统一的 HostMouse/HostKey 事件模型

适配器架构

IHostAdapter 接口

vb
Interface IHostAdapter
    Sub Attach(hostHWnd As LongPtr, core As WebView2Core)
    Sub Detach()
    Sub ScheduleOnMainThread(core As WebView2Core)
    Sub EnsureChildVisible(childHWnd As LongPtr, width As Long, height As Long)
    Sub SyncChildSize(childHWnd As LongPtr, width As Long, height As Long)
    Function FindAndSubclassWv2Child() As LongPtr
    Sub CleanupChildSubclass()
    Property Get Wv2ChildHWnd() As LongPtr
    Property Get AdapterName() As String
End Interface

自动选择逻辑

Initialize(HostOrHwnd, HttpOrDir)

    ├── 获取宿主窗口 hWnd

    ├── GetClassName(hWnd) == "OForm" ?
    │   ├── Yes → 创建 MessageWindowAdapter
    │   │         (Access 窗口,不能安全子类化)
    │   │
    │   └── No  → 创建 HostSubclassAdapter
                (VB6/Excel/UserForm,可以安全子类化)

适配器职责对比

职责HostSubclassAdapterMessageWindowAdapter
宿主窗口子类化直接子类化不子类化(创建消息窗口)
消息窗口不需要创建 HWND_MESSAGE 消息窗口
尺寸同步WM_SIZE 拦截200ms 定时器轮询
焦点管理WM_SETFOCUS/KILLFOCUS定时器检查 + 焦点守护
宿主鼠标事件完整支持不支持
宿主键盘事件完整支持不支持
WV2 子窗口右键捕获子类化 Chrome_WidgetWin_0子类化 Chrome_WidgetWin_0
子窗口可见性自动强制 TOP+VISIBLE(Access 遮挡问题)

子类化适配器 (VB6/Excel)

工作原理

VB6 Form (hWnd)
  │ [SetWindowSubclass] → SubclassProc

  ├── WM_SIZE → 调整 WV2 Controller Bounds + SyncChildSize
  ├── WM_SETFOCUS → 触发 HostFocus 事件
  ├── WM_KILLFOCUS → 触发 HostBlur 事件
  ├── WM_KEYDOWN/UP → 触发 HostKeyDown/Up 事件
  ├── WM_CHAR → 触发 HostKeyPress 事件
  ├── WM_xBUTTONDOWN/UP/DBLCLK → 触发 HostMouseDown/Up/DblClick
  ├── WM_MOUSEMOVE → 触发 HostMouseMove(需 EnableMouseMoveEvents)
  ├── WM_MOUSEWHEEL → 触发 HostMouseWheel
  ├── WM_CONTEXTMENU → 触发 HostContextMenu
  ├── WM_DESTROY → AdapterTriggerCleanup
  └── WM_WV2_DEFERRED_CALLBACK → ProcessDeferredCallbacks

Chrome_WidgetWin_0 (WV2 子窗口)
  │ [SetWindowSubclass] → ChildSubclassProc

  ├── WM_RBUTTONDOWN → 转发给 Core
  ├── WM_RBUTTONUP → 转发给 Core
  ├── WM_CONTEXTMENU → 触发 HostContextMenu
  └── WM_DESTROY → 清理子类化

使用限制

  • 子类化期间不能使用其他第三方窗口子类化工具(可能冲突)
  • AddressOf 在 VB6 中可能返回不同 thunk,但适配器在 Attach 时保存一次

消息窗口适配器 (Access)

工作原理

Access 的 OForm 窗口由 Access 运行时管理,直接子类化会导致崩溃。MessageWindowAdapter 创建独立的隐藏消息窗口来规避这个问题。

Access OForm (不能子类化)

  ├── 创建 Message-Only Window (HWND_MESSAGE 父窗口)
  │   │ [SetWindowSubclass] → SubclassProc(安全:我们拥有此窗口)
  │   │
  │   ├── WM_WV2_DEFERRED_CALLBACK → ProcessDeferredCallbacks
  │   ├── WM_TIMER →
  │   │   ├── 200ms 轮询:检查宿主尺寸变化 → SyncChildSize
  │   │   └── 5 拍焦点守护:检查 GetFocus()==0 → 恢复焦点
  │   └── WM_DESTROY → 清理

  └── Chrome_WidgetWin_0 (WV2 子窗口)
      │ [SetWindowSubclass] → ChildSubclassProc

      ├── WM_RBUTTONDOWN/UP → 转发给 Core
      └── WM_CONTEXTMENU → 转发给 Core

Access 特殊处理

子窗口可见性强制

Access 的绘制引擎可能在 WV2 子窗口上方绘制,导致内容被遮挡。EnsureChildVisible() 强制设置子窗口为 TOP + VISIBLE:

vb
' 内部实现
SetWindowPos childHWnd, HWND_TOP, 0, 0, width, height, _
    SWP_NOMOVE Or SWP_NOZORDER Or SWP_SHOWWINDOW

焦点守护

Access 可能意外夺取焦点,导致 WebView2 无法接收键盘输入。适配器每秒检查一次焦点状态:

每 5 个定时器周期(约 1 秒):
    If GetFocus() == 0  ' 无窗口拥有焦点
        SetFocus(hostHWnd)  ' 恢复焦点到宿主窗口
        冷却 3 秒避免焦点争夺

尺寸同步

由于无法拦截 OForm 的 WM_SIZE,使用 200ms 定时器轮询:

vb
' 定时器回调
GetClientRect hostHWnd, rc
If rc.Width <> lastWidth Or rc.Height <> lastHeight Then
    Controller.Bounds = rc  ' 更新 WV2 控件大小
    SyncChildSize            ' 同步子窗口
End If

各宿主使用指南

VB6 标准窗体

vb
Dim WithEvents wv As cWebView2Host

Private Sub Form_Load()
    Set wv = New cWebView2Host
    wv.Initialize Me.hWnd, "https://vb6.pro"
    ' 自动使用 HostSubclassAdapter
End Sub

Private Sub Form_Resize()
    ' 子类化适配器已自动处理 WM_SIZE
    ' 通常不需要手动调用 Resize
End Sub

Private Sub Form_Unload(Cancel As Integer)
    Set wv = Nothing
End Sub

VB6 MDI 子窗体

vb
Dim WithEvents wv As cWebView2Host
Dim ThisUrl As String

Public Sub Init(Optional ByVal Url As String)
    Me.Show
    If Url = "" Then Url = "https://vb6.pro"
    ThisUrl = Url
    Set wv = New cWebView2Host
    wv.Initialize Me       ' 自动获取 Me.hWnd
End Sub

Private Sub wv_Ready()
    wv.Navigate ThisUrl
End Sub

Excel UserForm

vb
' 在 UserForm 代码模块中
Dim WithEvents wv As cWebView2Host

Private Sub UserForm_Initialize()
    Set wv = New cWebView2Host
    wv.Initialize Me.hWnd, "https://example.com"
End Sub

Private Sub UserForm_Terminate()
    Set wv = Nothing
End Sub

Access Form

vb
' 在 Access 窗体代码模块中
Dim WithEvents wv As cWebView2Host

Private Sub Form_Load()
    Set wv = New cWebView2Host
    wv.Initialize Me.hWnd, "https://example.com"
    ' 自动检测到 OForm → 使用 MessageWindowAdapter
    ' 宿主鼠标/键盘事件不可用
End Sub

Private Sub Form_Close()
    Set wv = Nothing
End Sub

VB6 Frame 控件内嵌

WebView2 也可以嵌入 Frame 控件而非整个 Form:

vb
Dim wv As New cWebView2Host

Private Sub Form_Load()
    wv.Initialize Me.Frame1.hWnd, "https://example.com"
End Sub

常见问题

❓ Q1: 如何判断当前使用的适配器?

vb
Debug.Print wv.HostAdapterName
' 输出 "HostSubclassAdapter" 或 "MessageWindowAdapter"

❓ Q2: Access 中宿主鼠标事件不触发?

原因: MessageWindowAdapter 不子类化宿主窗口,因此无法拦截宿主区域的鼠标/键盘消息。

解决方案: 使用 WebView2 内容区域的事件(UserMouse 系列)替代,或使用 BindUI 绑定 DOM 事件到 VB6 方法。


❓ Q3: Access 中 WebView2 偶尔被遮挡?

原因: Access 绘制引擎会在 WV2 子窗口上方绘制,特别是切换窗口后。

解决方案: MessageWindowAdapter 内部已处理此问题(EnsureChildVisible)。如果仍有遮挡,可在代码中手动调用:

vb
wv.Resize  ' 触发子窗口刷新

❓ Q4: VB6 中 Resize 后 WebView2 大小不同步?

原因: HostSubclassAdapter 在 WM_SIZE 中自动处理尺寸同步,但某些场景(如手动修改窗口大小)可能需要额外触发。

解决方案:

vb
Private Sub Form_Resize()
    If Not wv Is Nothing Then
        wv.Resize
    End If
End Sub

❓ Q5: 多个 WebView2 实例是否支持?

支持。每个 cWebView2Host 实例独立管理自己的 WebView2 控件。在 MDI 应用中,每个子窗体可以有自己的 WebView2 实例:

vb
' MDI 子窗体
Dim WithEvents wv As cWebView2Host

Private Sub Init(ByVal Url As String)
    Set wv = New cWebView2Host
    wv.Initialize Me.hWnd, Url
End Sub

注意:多实例应使用不同的 UserDataFolder 以避免数据冲突。


最后更新: 2026-06-24

VB6及其LOGO版权为微软公司所有