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 接口
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,可以安全子类化)适配器职责对比
| 职责 | HostSubclassAdapter | MessageWindowAdapter |
|---|---|---|
| 宿主窗口子类化 | 直接子类化 | 不子类化(创建消息窗口) |
| 消息窗口 | 不需要 | 创建 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 → 转发给 CoreAccess 特殊处理
子窗口可见性强制
Access 的绘制引擎可能在 WV2 子窗口上方绘制,导致内容被遮挡。EnsureChildVisible() 强制设置子窗口为 TOP + VISIBLE:
' 内部实现
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 定时器轮询:
' 定时器回调
GetClientRect hostHWnd, rc
If rc.Width <> lastWidth Or rc.Height <> lastHeight Then
Controller.Bounds = rc ' 更新 WV2 控件大小
SyncChildSize ' 同步子窗口
End If各宿主使用指南
VB6 标准窗体
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 SubVB6 MDI 子窗体
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 SubExcel UserForm
' 在 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 SubAccess Form
' 在 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 SubVB6 Frame 控件内嵌
WebView2 也可以嵌入 Frame 控件而非整个 Form:
Dim wv As New cWebView2Host
Private Sub Form_Load()
wv.Initialize Me.Frame1.hWnd, "https://example.com"
End Sub常见问题
❓ Q1: 如何判断当前使用的适配器?
Debug.Print wv.HostAdapterName
' 输出 "HostSubclassAdapter" 或 "MessageWindowAdapter"❓ Q2: Access 中宿主鼠标事件不触发?
原因: MessageWindowAdapter 不子类化宿主窗口,因此无法拦截宿主区域的鼠标/键盘消息。
解决方案: 使用 WebView2 内容区域的事件(UserMouse 系列)替代,或使用 BindUI 绑定 DOM 事件到 VB6 方法。
❓ Q3: Access 中 WebView2 偶尔被遮挡?
原因: Access 绘制引擎会在 WV2 子窗口上方绘制,特别是切换窗口后。
解决方案: MessageWindowAdapter 内部已处理此问题(EnsureChildVisible)。如果仍有遮挡,可在代码中手动调用:
wv.Resize ' 触发子窗口刷新❓ Q4: VB6 中 Resize 后 WebView2 大小不同步?
原因: HostSubclassAdapter 在 WM_SIZE 中自动处理尺寸同步,但某些场景(如手动修改窗口大小)可能需要额外触发。
解决方案:
Private Sub Form_Resize()
If Not wv Is Nothing Then
wv.Resize
End If
End Sub❓ Q5: 多个 WebView2 实例是否支持?
支持。每个 cWebView2Host 实例独立管理自己的 WebView2 控件。在 MDI 应用中,每个子窗体可以有自己的 WebView2 实例:
' 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