feat(gameplay): 更新单位属性和材质配置

- 调整多个单位的基础生命值和攻击力数值平衡
- 修改CoreBattleGround材质的纹理缩放比例
- 更新地砖材质颜色为白色
- 优化纹理资源设置,调整最大纹理尺寸并添加PlayableAds平台支持
- 修复弓箭手技能目标查找逻辑,改用统一的敌人检测方法
- 为水晶矿添加中立阵营设置和光球视觉效果同步
- 调整预制体中的全局缩放参数
- 为狼人和石巨人添加NavMeshAgent组件支持
- 优化水晶矿血条显示位置计算方式
```
This commit is contained in:
huadexuan committed 2026-09-20 21:52:31 +08:00
1 parent 0e7de5f885
commit 53d95802e5
448 files changed
+133333 -472

No files matched your search

@@ -0,0 +1,63 @@
fileFormatVersion: 2
guid: 0e1e756d3d2df9149ba70839ab1b645f
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
:
second:
enabled: 0
settings: {}
- first:
: Any
second:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: x86
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: x86_64
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7fa78ea1264744445ab64746a03f4344
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,69 @@
cmake_minimum_required(VERSION 3.20)
project(NativeBrowser CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Statically link the MSVC runtime so users' VC++ Redistributable version
# does not affect us. An outdated MSVCP140.dll on the host (e.g. VS 2017's
# 14.11.x) ships an older _Mtx_internal_imp_t layout than the one this DLL
# was compiled against, and std::mutex::lock SIGSEGVs on first call.
# Requires CMP0091 NEW (default since CMake 3.15).
if(MSVC)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
# ── WebView2 SDK ──────────────────────────────────────────────────────────────
set(WEBVIEW2_DIR "${CMAKE_SOURCE_DIR}/vendor/webview2")
if(NOT EXISTS "${WEBVIEW2_DIR}/include/WebView2.h")
message(FATAL_ERROR "WebView2 SDK not found at ${WEBVIEW2_DIR}. "
"Run: nuget install Microsoft.Web.WebView2 -OutputDirectory vendor/nuget "
"and copy include/ and lib/x64/ to vendor/webview2/")
endif()
# ── 目标 DLL ─────────────────────────────────────────────────────────────────
add_library(NativeBrowser SHARED
src/NativeBrowser.cpp
src/BrowserInstance.cpp
src/STAThread.cpp
src/WndProcHook.cpp
)
target_include_directories(NativeBrowser PRIVATE
include/
${WEBVIEW2_DIR}/include/
)
target_compile_definitions(NativeBrowser PRIVATE
NATIVEBROWSER_EXPORTS
UNICODE
_UNICODE
WIN32_LEAN_AND_MEAN
NOMINMAX
)
target_compile_options(NativeBrowser PRIVATE
/W3
/WX- # 不把警告当错误(方便迭代)
/MP # 并行编译
)
target_link_libraries(NativeBrowser PRIVATE
${WEBVIEW2_DIR}/lib/x64/WebView2LoaderStatic.lib
shlwapi.lib # WebView2Loader 依赖
version.lib # WebView2Loader 依赖
comctl32.lib # SetWindowSubclass
ole32.lib # CoInitializeEx
oleaut32.lib
)
# ── 构建后复制到 Plugins/Win/ 目录
set(PLUGIN_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/..")
add_custom_command(TARGET NativeBrowser POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${PLUGIN_OUTPUT_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:NativeBrowser>"
"${PLUGIN_OUTPUT_DIR}/NativeBrowser.dll"
COMMENT "Copying NativeBrowser.dll → Plugins/Win/"
)
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2939fbf3c29694a4dad2a67467b71df4
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,138 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Build NativeBrowser.dll and copy to Plugins/Win/
.PARAMETER Config
Build configuration: Release (default) or Debug
.PARAMETER Clean
Remove build directory before configuring
.EXAMPLE
.\build.ps1
.\build.ps1 -Config Debug
.\build.ps1 -Clean
#>
param(
[ValidateSet("Release", "Debug")]
[string]$Config = "Release",
[switch]$Clean
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$ScriptDir = $PSScriptRoot
$BuildDir = Join-Path $ScriptDir "build"
# ── Tool detection ─────────────────────────────────────────────────────────────
function Find-CMake {
$cmake = Get-Command cmake -ErrorAction SilentlyContinue
if ($cmake) { return $cmake.Source }
$candidates = @(
"$env:ProgramFiles\CMake\bin\cmake.exe",
"${env:ProgramFiles(x86)}\CMake\bin\cmake.exe"
)
foreach ($c in $candidates) {
if (Test-Path $c) { return $c }
}
throw "cmake not found. Install CMake 3.20+ and add it to PATH.`nhttps://cmake.org/download/"
}
function Find-VSGenerator {
$generators = @(
@{ Name = "Visual Studio 17 2022"; MinVer = "17" },
@{ Name = "Visual Studio 16 2019"; MinVer = "16" }
)
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vswhere) {
foreach ($g in $generators) {
$found = & $vswhere -version "[$($g.MinVer),)" -products * `
-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 `
-property installationPath 2>$null
if ($found) { return $g.Name }
}
}
return "Visual Studio 17 2022"
}
# ── Main ───────────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "=======================================" -ForegroundColor Cyan
Write-Host " NativeBrowser Build [$Config]" -ForegroundColor Cyan
Write-Host "=======================================" -ForegroundColor Cyan
Write-Host ""
# 1. Check CMake
Write-Host "[1/5] Checking CMake..." -ForegroundColor Yellow
$cmake = Find-CMake
$cmakeVer = (& $cmake --version 2>&1 | Select-Object -First 1) -replace "cmake version ", ""
Write-Host " $cmake (v$cmakeVer)"
# 2. Check WebView2 SDK
Write-Host "[2/5] Checking WebView2 SDK..." -ForegroundColor Yellow
$webview2Header = Join-Path $ScriptDir "vendor\webview2\include\WebView2.h"
if (-not (Test-Path $webview2Header)) {
Write-Host ""
Write-Host "ERROR: WebView2 SDK not found at:" -ForegroundColor Red
Write-Host " $webview2Header" -ForegroundColor Red
Write-Host ""
Write-Host "Run the following to fetch it from NuGet cache:" -ForegroundColor Yellow
Write-Host ' $src = (Get-ChildItem "$env:USERPROFILE\.nuget\packages\microsoft.web.webview2" | Sort-Object Name -Desc | Select -First 1).FullName'
Write-Host ' Copy-Item "$src\build\native\include\WebView2.h" "vendor\webview2\include\" -Force'
Write-Host ' Copy-Item "$src\build\native\x64\WebView2LoaderStatic.lib" "vendor\webview2\lib\x64\" -Force'
exit 1
}
Write-Host " OK ($webview2Header)"
# 3. Clean (optional)
if ($Clean -and (Test-Path $BuildDir)) {
Write-Host "[3/5] Cleaning build directory..." -ForegroundColor Yellow
Remove-Item $BuildDir -Recurse -Force
Write-Host " Removed: $BuildDir"
} else {
Write-Host "[3/5] Clean skipped (use -Clean to force)" -ForegroundColor DarkGray
}
# 4. CMake configure
$generator = Find-VSGenerator
Write-Host "[4/5] CMake configure [Generator: $generator]..." -ForegroundColor Yellow
& $cmake -B $BuildDir -G $generator -A x64 -S $ScriptDir
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: CMake configure failed (exit $LASTEXITCODE)" -ForegroundColor Red
exit $LASTEXITCODE
}
# 5. Build
Write-Host ""
Write-Host "[5/5] Building [$Config]..." -ForegroundColor Yellow
& $cmake --build $BuildDir --config $Config --parallel
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Build failed (exit $LASTEXITCODE)" -ForegroundColor Red
exit $LASTEXITCODE
}
# Verify output
$outputDll = [IO.Path]::GetFullPath((Join-Path $ScriptDir "..\NativeBrowser.dll"))
Write-Host ""
if (Test-Path $outputDll) {
$sizeKB = [math]::Round((Get-Item $outputDll).Length / 1KB, 1)
Write-Host "Build succeeded!" -ForegroundColor Green
Write-Host " Output : $outputDll"
Write-Host " Size : $sizeKB KB"
} else {
Write-Host "ERROR: Build finished but output DLL not found: $outputDll" -ForegroundColor Red
exit 1
}
Write-Host ""
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 014f48bf90c089e45851051fdeb3d0a4
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: abf11ba4a72d5e64cb5d590ab381802a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,89 @@
#pragma once
#include <windows.h>
#include <stdint.h>
#include <stdbool.h>
#ifdef NATIVEBROWSER_EXPORTS
# define NB_API extern "C" __declspec(dllexport)
#else
# define NB_API extern "C" __declspec(dllimport)
#endif
// ── 回调函数类型 ──────────────────────────────────────────────────────────────
// 所有回调均从 STA 线程通过 PostMessage 投递到 Unity 主线程执行。
typedef void (__stdcall* NB_NavigationCallback) (const wchar_t* url, void* ctx);
typedef void (__stdcall* NB_LoadCompletedCallback)(bool success, void* ctx);
typedef void (__stdcall* NB_TitleChangedCallback) (const wchar_t* title, void* ctx);
typedef void (__stdcall* NB_MessageCallback) (const wchar_t* json, void* ctx);
typedef void (__stdcall* NB_DestroyedCallback) (void* ctx);
#pragma pack(push, 8)
struct NB_Callbacks {
NB_NavigationCallback onNavigationStarting; // 可为 null
NB_LoadCompletedCallback onLoadCompleted; // 可为 null
NB_TitleChangedCallback onTitleChanged; // 可为 null
NB_MessageCallback onWebMessageReceived; // 可为 null
NB_DestroyedCallback onParentDestroyed; // 父 HWND 销毁时通知 C#
void* ctx; // 透传给所有回调
};
#pragma pack(pop)
// ── 生命周期 ──────────────────────────────────────────────────────────────────
// NB_Create:启动独立 STA 线程,异步初始化 WebView2,完成后自动导航 initialUrl。
// 返回不透明句柄;失败(参数无效、Runtime 未安装等)返回 null。
NB_API void* NB_Create(
HWND parentHwnd,
const wchar_t* userDataFolder,
const wchar_t* initialUrl,
const NB_Callbacks* callbacks // 可为 null
);
NB_API void NB_Destroy(void* handle);
// NB_ShutdownAll:同步关闭所有活跃 BrowserInstance(总耗时最长 timeoutMs)。
// 在调用线程上 pump 消息以驱动 WebView2 跨 apartment marshaling,避免父 HWND
// 所属线程被阻塞导致 Close 死锁。仅 PrepareShutdown + FinalizeShutdown,不释放
// 内存(进程即将退出,OS 回收即可,避免与 NB_Destroy 后台线程的 delete 竞争)。
// 必须在 Unity Editor 退出时(EditorApplication.quitting)调用一次。
NB_API void NB_ShutdownAll(uint32_t timeoutMs);
// NB_SetCallbacks:替换托管回调函数指针。
// Unity Domain Reload 后旧指针失效,必须在 OnEnable 中调用此函数注册新委托。
NB_API void NB_SetCallbacks(void* handle, const NB_Callbacks* callbacks);
// NB_IsHandleValid:判断 handle 是否仍指向一个活跃的 BrowserInstance。
// 不解引用指针,安全用于 Domain Reload 后的句柄验证。
NB_API bool NB_IsHandleValid(void* handle);
// ── 导航(线程安全,内部转发到 STA 线程)──────────────────────────────────────
NB_API void NB_Navigate (void* handle, const wchar_t* url);
NB_API void NB_GoBack (void* handle);
NB_API void NB_GoForward (void* handle);
NB_API void NB_Reload (void* handle);
NB_API void NB_Stop (void* handle);
// ── 布局(线程安全)──────────────────────────────────────────────────────────
// NB_Create 内部已 hook 父 HWND 的 WM_SIZE,通常无需手动调用 NB_Resize。
NB_API void NB_Resize (void* handle, int x, int y, int w, int h);
NB_API void NB_SetVisible (void* handle, bool visible);
NB_API void NB_SetTopInset (void* handle, int topPx);
// ── 脚本 ─────────────────────────────────────────────────────────────────────
NB_API void NB_ExecuteScript (void* handle, const wchar_t* js);
NB_API void NB_PostWebMessage (void* handle, const wchar_t* json);
// ── Focus ──
// NB_MoveFocus: move keyboard focus into the WebView2 content (equivalent to
// ICoreWebView2Controller::MoveFocus with the PROGRAMMATIC reason). When the
// WebView2 is embedded as a child of Unity's GUIView, the parent keeps Win32
// focus after navigation, so HTML autofocus / element.focus() neither show a
// caret nor receive keystrokes; the host must call this to hand focus to the
// content. Thread-safe; forwarded to the STA thread internally.
NB_API void NB_MoveFocus (void* handle);
// ── 调试 ─────────────────────────────────────────────────────────────────────
NB_API void NB_OpenDevTools(void* handle);
// ── Runtime 检测(同步,可在任意线程调用)───────────────────────────────────
NB_API bool NB_IsRuntimeAvailable();
NB_API const wchar_t* NB_GetRuntimeVersion(); // 返回内部静态缓冲区
@@ -0,0 +1,47 @@
fileFormatVersion: 2
guid: 952eec9c9781d7c478a1057764a499ea
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
OSXUniversal:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ecfd8da23365a1346a0bef7733afff7b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,400 @@
#include "BrowserInstance.h"
#include <wrl/event.h> // Microsoft::WRL::Callback
#include <shlwapi.h>
// ── 静态成员定义 ──────────────────────────────────────────────────────────────
std::mutex BrowserInstance::s_mapMutex;
std::unordered_map<HWND, BrowserInstance*> BrowserInstance::s_hwndMap;
std::unordered_set<BrowserInstance*> BrowserInstance::s_liveInstances;
std::mutex BrowserInstance::s_liveSetMutex;
// ── WinEvent 回调(WINEVENT_INCONTEXT → 在 Unity 主线程同步调用)────────────
void CALLBACK BrowserInstance::WinEventProc(
HWINEVENTHOOK /*hook*/, DWORD /*event*/,
HWND hwnd, LONG objId, LONG childId,
DWORD /*idEventThread*/, DWORD /*dwmsEventTime*/)
{
if (objId != 0 /*OBJID_WINDOW*/ || childId != 0 /*CHILDID_SELF*/) return;
BrowserInstance* inst = nullptr;
{
std::lock_guard<std::mutex> lk(s_mapMutex);
auto it = s_hwndMap.find(hwnd);
if (it == s_hwndMap.end()) return;
inst = it->second;
}
if (!inst || inst->m_shutdown.load()) return;
RECT rc = {};
if (!GetClientRect(hwnd, &rc)) return;
inst->PostResizeBounds(rc.right, rc.bottom);
}
// ── Constructor / Destructor ──────────────────────────────────────────────────
BrowserInstance::BrowserInstance(HWND parentHwnd,
std::wstring userDataFolder,
std::wstring initialUrl,
NB_Callbacks callbacks)
: m_parentHwnd(parentHwnd)
, m_userDataFolder(std::move(userDataFolder))
, m_initialUrl(std::move(initialUrl))
, m_callbacks(callbacks)
{
{
std::lock_guard<std::mutex> lk(s_liveSetMutex);
s_liveInstances.insert(this);
}
m_staThread = std::make_unique<STAThread>(L"NativeBrowser-STA");
m_wndProcHook = std::make_unique<WndProcHook>(
parentHwnd,
[this](int w, int h) { PostResizeBounds(w, h); },
[this] { FireParentDestroyed(); }
);
m_staThread->Post([this] { InitOnSTAThread(); });
}
BrowserInstance::~BrowserInstance()
{
{
std::lock_guard<std::mutex> lk(s_liveSetMutex);
s_liveInstances.erase(this);
}
// 析构时确保两阶段都完成(正常情况下 NB_Destroy 已经完成了 Phase 1;
// 若析构由 NB_Destroy 的后台线程 delete 触发,Phase 2 也已完成,均幂等)。
PrepareShutdown();
FinalizeShutdown();
}
// ── 两阶段关闭 ────────────────────────────────────────────────────────────────
bool BrowserInstance::PrepareShutdown()
{
// m_shutdown 保证幂等:只有第一次调用执行实际逻辑
if (m_shutdown.exchange(true)) return false;
// 1. 从全局映射删除 → WinEvent/WM_SIZE 回调立即找不到此实例(线程安全)
{
std::lock_guard<std::mutex> lk(s_mapMutex);
s_hwndMap.erase(m_parentHwnd);
}
// 2. 卸载 WndProcHook(调用 RemoveWindowSubclass,必须在 Unity 主线程)
m_wndProcHook.reset();
// WinEvent 的 UnhookWinEvent 需在 STA 线程(与 SetWinEventHook 同线程),
// 留给 FinalizeShutdown 通过 STA Post 完成。
return true;
}
void BrowserInstance::PostFinalizeShutdown()
{
// m_finalized 保证幂等
if (m_finalized.exchange(true)) return;
// 确保 PrepareShutdown 已完成(若直接调用 FinalizeShutdown 而未先 Prepare)
PrepareShutdown();
// 捕获 WinEvent hook 供 STA 线程卸载(原子交换,避免与 RegisterWinEventHook 竞争)
HWINEVENTHOOK hook = m_winEventHook.exchange(nullptr);
// 向 STA 线程投递:卸载 WinEvent hook + 关闭 WebView2 COM 对象
m_staThread->Post([this, hook] {
if (hook) UnhookWinEvent(hook); // 与 SetWinEventHook 同线程(STA)
if (m_webview)
{
m_webview->remove_NavigationStarting(m_navigationToken);
m_webview->remove_DocumentTitleChanged(m_titleToken);
m_webview->remove_WebMessageReceived(m_messageToken);
m_webview->remove_NavigationCompleted(m_loadToken);
}
if (m_controller) m_controller->Close();
m_webview.Reset();
m_controller.Reset();
m_env.Reset();
});
// 投递 WM_QUIT,让 STA 线程在处理完上述 close 工作后退出。
// 不在此函数内等待——由调用方负责(NB_ShutdownAll 用 pumped wait 避免死锁)。
m_staThread->RequestQuit();
}
HANDLE BrowserInstance::GetStaThreadHandle() const
{
return m_staThread ? m_staThread->ThreadHandle() : nullptr;
}
void BrowserInstance::FinalizeShutdown(DWORD timeoutMs)
{
PostFinalizeShutdown(); // 幂等
if (m_staThread)
m_staThread->Shutdown(timeoutMs); // RequestQuit 已发;这里只是 WaitForSingleObject
}
// ── 跨 Domain Reload 恢复 ─────────────────────────────────────────────────────
bool BrowserInstance::IsLive(void* handle)
{
if (!handle) return false;
std::lock_guard<std::mutex> lk(s_liveSetMutex);
return s_liveInstances.count(static_cast<BrowserInstance*>(handle)) > 0;
}
void BrowserInstance::SetCallbacks(const NB_Callbacks& callbacks)
{
std::lock_guard<std::mutex> lk(m_callbacksMutex);
m_callbacks = callbacks;
}
// ── Public API ────────────────────────────────────────────────────────────────
void BrowserInstance::Navigate(const wchar_t* url)
{
std::wstring u(url ? url : L"");
m_staThread->Post([this, u = std::move(u)] {
if (m_webview) m_webview->Navigate(u.c_str());
});
}
void BrowserInstance::GoBack() { m_staThread->Post([this] { if (m_webview) m_webview->GoBack(); }); }
void BrowserInstance::GoForward() { m_staThread->Post([this] { if (m_webview) m_webview->GoForward(); }); }
void BrowserInstance::Reload() { m_staThread->Post([this] { if (m_webview) m_webview->Reload(); }); }
void BrowserInstance::Stop() { m_staThread->Post([this] { if (m_webview) m_webview->Stop(); }); }
void BrowserInstance::Resize(int x, int y, int w, int h)
{
m_staThread->Post([this, x, y, w, h] {
if (m_controller) m_controller->put_Bounds({ x, y, x + w, y + h });
});
}
void BrowserInstance::SetVisible(bool v)
{
m_staThread->Post([this, v] {
if (m_controller) m_controller->put_IsVisible(v ? TRUE : FALSE);
});
}
void BrowserInstance::ExecuteScript(const wchar_t* js)
{
std::wstring s(js ? js : L"");
m_staThread->Post([this, s = std::move(s)] {
if (m_webview) m_webview->ExecuteScript(s.c_str(), nullptr);
});
}
void BrowserInstance::PostWebMessage(const wchar_t* json)
{
std::wstring s(json ? json : L"");
m_staThread->Post([this, s = std::move(s)] {
if (m_webview) m_webview->PostWebMessageAsJson(s.c_str());
});
}
void BrowserInstance::OpenDevTools()
{
m_staThread->Post([this] {
if (m_webview) m_webview->OpenDevToolsWindow();
});
}
void BrowserInstance::MoveFocus()
{
// 必须在拥有 controller 的 STA 线程上调用。PROGRAMMATIC 让 WebView2 取得 Win32 焦点,
// 内容文档随之获得系统焦点,挂起的 autofocus / 已聚焦元素的光标得以显示并接收键盘输入。
m_staThread->Post([this] {
if (m_controller && !m_shutdown.load())
m_controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC);
});
}
void BrowserInstance::SetTopInset(int px)
{
m_topInset.store(px < 0 ? 0 : px);
m_staThread->Post([this] { SyncBounds(); });
}
// ── Resize 合并投递(主线程调用)─────────────────────────────────────────────
void BrowserInstance::PostResizeBounds(int w, int h)
{
m_pendingW.store((LONG)w);
m_pendingH.store((LONG)h);
bool expected = false;
if (m_resizePending.compare_exchange_strong(expected, true))
{
m_staThread->Post([this] {
m_resizePending.store(false);
if (!m_controller || m_shutdown.load()) return;
LONG w = m_pendingW.load();
LONG h = m_pendingH.load();
int inset = m_topInset.load();
m_controller->put_Bounds({ 0, inset, (int)w, (int)h });
});
}
}
// ── WinEvent Hook ─────────────────────────────────────────────────────────────
void BrowserInstance::RegisterWinEventHook()
{
HMODULE hMod = nullptr;
GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(&BrowserInstance::WinEventProc),
&hMod);
DWORD parentThreadId = GetWindowThreadProcessId(m_parentHwnd, nullptr);
m_winEventHook = SetWinEventHook(
EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE,
hMod,
WinEventProc,
0,
parentThreadId,
WINEVENT_INCONTEXT
);
}
// ── WebView2 Initialization (runs on STA thread) ──────────────────────────────
void BrowserInstance::InitOnSTAThread()
{
using namespace Microsoft::WRL;
HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(
nullptr,
m_userDataFolder.c_str(),
nullptr,
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
[this](HRESULT result, ICoreWebView2Environment* env) -> HRESULT
{
if (FAILED(result) || !env) { FireLoadCompleted(false); return result; }
m_env = env;
return env->CreateCoreWebView2Controller(
m_parentHwnd,
Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
[this](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT
{
if (FAILED(result) || !controller) { FireLoadCompleted(false); return result; }
m_controller = controller;
m_controller->get_CoreWebView2(&m_webview);
SyncBounds();
ComPtr<ICoreWebView2Settings> settings;
if (SUCCEEDED(m_webview->get_Settings(&settings)))
{
settings->put_IsStatusBarEnabled(FALSE);
settings->put_AreDevToolsEnabled(TRUE);
}
m_webview->add_NavigationStarting(
Callback<ICoreWebView2NavigationStartingEventHandler>(
[this](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT
{
LPWSTR uri = nullptr;
args->get_Uri(&uri);
if (uri) { FireNavigationStarting(uri); CoTaskMemFree(uri); }
return S_OK;
}).Get(), &m_navigationToken);
m_webview->add_DocumentTitleChanged(
Callback<ICoreWebView2DocumentTitleChangedEventHandler>(
[this](ICoreWebView2* wv, IUnknown*) -> HRESULT
{
LPWSTR title = nullptr;
wv->get_DocumentTitle(&title);
if (title) { FireTitleChanged(title); CoTaskMemFree(title); }
return S_OK;
}).Get(), &m_titleToken);
m_webview->add_WebMessageReceived(
Callback<ICoreWebView2WebMessageReceivedEventHandler>(
[this](ICoreWebView2*, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT
{
LPWSTR json = nullptr;
args->get_WebMessageAsJson(&json);
if (json) { FireWebMessageReceived(json); CoTaskMemFree(json); }
return S_OK;
}).Get(), &m_messageToken);
m_webview->add_NavigationCompleted(
Callback<ICoreWebView2NavigationCompletedEventHandler>(
[this](ICoreWebView2*, ICoreWebView2NavigationCompletedEventArgs* args) -> HRESULT
{
BOOL success = FALSE;
args->get_IsSuccess(&success);
FireLoadCompleted(success == TRUE);
return S_OK;
}).Get(), &m_loadToken);
m_ready = true;
{
std::lock_guard<std::mutex> lk(s_mapMutex);
s_hwndMap[m_parentHwnd] = this;
}
RegisterWinEventHook();
m_webview->Navigate(m_initialUrl.c_str());
return S_OK;
}).Get());
}).Get());
if (FAILED(hr))
FireLoadCompleted(false);
}
void BrowserInstance::SyncBounds()
{
if (!m_controller || !m_parentHwnd) return;
RECT rc = {};
GetClientRect(m_parentHwnd, &rc);
int inset = m_topInset.load();
m_controller->put_Bounds({ 0, inset, rc.right, rc.bottom });
}
// ── Callbacks ─────────────────────────────────────────────────────────────────
void BrowserInstance::FireNavigationStarting(const std::wstring& url)
{
NB_NavigationCallback cb; void* ctx;
{ std::lock_guard<std::mutex> lk(m_callbacksMutex); cb = m_callbacks.onNavigationStarting; ctx = m_callbacks.ctx; }
if (cb) cb(url.c_str(), ctx);
}
void BrowserInstance::FireLoadCompleted(bool success)
{
NB_LoadCompletedCallback cb; void* ctx;
{ std::lock_guard<std::mutex> lk(m_callbacksMutex); cb = m_callbacks.onLoadCompleted; ctx = m_callbacks.ctx; }
if (cb) cb(success, ctx);
}
void BrowserInstance::FireTitleChanged(const std::wstring& title)
{
NB_TitleChangedCallback cb; void* ctx;
{ std::lock_guard<std::mutex> lk(m_callbacksMutex); cb = m_callbacks.onTitleChanged; ctx = m_callbacks.ctx; }
if (cb) cb(title.c_str(), ctx);
}
void BrowserInstance::FireWebMessageReceived(const std::wstring& json)
{
NB_MessageCallback cb; void* ctx;
{ std::lock_guard<std::mutex> lk(m_callbacksMutex); cb = m_callbacks.onWebMessageReceived; ctx = m_callbacks.ctx; }
if (cb) cb(json.c_str(), ctx);
}
void BrowserInstance::FireParentDestroyed()
{
NB_DestroyedCallback cb; void* ctx;
{ std::lock_guard<std::mutex> lk(m_callbacksMutex); cb = m_callbacks.onParentDestroyed; ctx = m_callbacks.ctx; }
if (cb) cb(ctx);
}
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: 2fc66ef2416d9064f9d3a0e22f94fc97
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,110 @@
#pragma once
#include <windows.h>
#include <wrl/client.h>
#include <WebView2.h>
#include <string>
#include <memory>
#include <atomic>
#include <mutex>
#include <unordered_map>
#include <unordered_set>
#include "../include/NativeBrowser.h"
#include "STAThread.h"
#include "WndProcHook.h"
class BrowserInstance {
public:
BrowserInstance(HWND parentHwnd,
std::wstring userDataFolder,
std::wstring initialUrl,
NB_Callbacks callbacks);
~BrowserInstance();
// ── 跨 Domain Reload 恢复支持 ─────────────────────────────────────────
// 用于验证一个 void* 是否仍指向活跃的 BrowserInstance,避免野指针崩溃
static bool IsLive(void* handle);
// 替换托管回调函数指针(Domain Reload 后旧指针已失效,必须在 OnEnable 中调用)
void SetCallbacks(const NB_Callbacks& callbacks);
// 活跃实例注册表(供 NativeBrowser.cpp 的 NB_IsHandleValid 使用)
static std::unordered_set<BrowserInstance*> s_liveInstances;
static std::mutex s_liveSetMutex;
BrowserInstance(const BrowserInstance&) = delete;
BrowserInstance& operator=(const BrowserInstance&) = delete;
void Navigate (const wchar_t* url);
void GoBack ();
void GoForward ();
void Reload ();
void Stop ();
void Resize (int x, int y, int w, int h);
void SetVisible (bool v);
void SetTopInset (int px);
void ExecuteScript (const wchar_t* js);
void PostWebMessage (const wchar_t* json);
void OpenDevTools ();
void MoveFocus ();
bool PrepareShutdown();
void FinalizeShutdown(DWORD timeoutMs = 5000);
// 拆分版 FinalizeShutdown:仅投递 close 工作和 WM_QUIT 到 STA 线程,不等待。
// 调用方需通过 GetStaThreadHandle() 自行等待——通常用 MsgWaitForMultipleObjects
// pump 消息,避免 WebView2 的 cross-apartment marshaling 在父 HWND 所属线程上死锁。
void PostFinalizeShutdown();
HANDLE GetStaThreadHandle() const;
void PostResizeBounds(int w, int h);
private:
void InitOnSTAThread();
void SyncBounds();
void RegisterWinEventHook();
static void CALLBACK WinEventProc(HWINEVENTHOOK hook, DWORD event,
HWND hwnd, LONG objId, LONG childId,
DWORD idEventThread, DWORD dwmsEventTime);
static std::mutex s_mapMutex;
static std::unordered_map<HWND, BrowserInstance*> s_hwndMap;
void FireNavigationStarting (const std::wstring& url);
void FireLoadCompleted (bool success);
void FireTitleChanged (const std::wstring& title);
void FireWebMessageReceived (const std::wstring& json);
void FireParentDestroyed ();
HWND m_parentHwnd;
std::wstring m_userDataFolder;
std::wstring m_initialUrl;
NB_Callbacks m_callbacks;
std::unique_ptr<STAThread> m_staThread;
std::unique_ptr<WndProcHook> m_wndProcHook;
Microsoft::WRL::ComPtr<ICoreWebView2Environment> m_env;
Microsoft::WRL::ComPtr<ICoreWebView2Controller> m_controller;
Microsoft::WRL::ComPtr<ICoreWebView2> m_webview;
EventRegistrationToken m_navigationToken = {};
EventRegistrationToken m_titleToken = {};
EventRegistrationToken m_messageToken = {};
EventRegistrationToken m_loadToken = {};
std::atomic<HWINEVENTHOOK> m_winEventHook { nullptr };
std::atomic<LONG> m_pendingW { 0 };
std::atomic<LONG> m_pendingH { 0 };
std::atomic<bool> m_resizePending { false };
std::atomic<bool> m_ready { false };
std::atomic<bool> m_shutdown { false };
std::atomic<bool> m_finalized { false };
std::atomic<int> m_topInset { 0 };
// 保护 m_callbacks 的读写,SetCallbacks 在 Unity 主线程调用,
// Fire* 系列在 STA 线程调用,需要互斥
mutable std::mutex m_callbacksMutex;
};
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: 4b99dbf7103b9ae43a458a59e5b176b9
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,168 @@
#include "../include/NativeBrowser.h"
#include "BrowserInstance.h"
#include <WebView2.h>
#include <string>
#include <thread>
#include <vector>
// ── 生命周期 ──────────────────────────────────────────────────────────────
NB_API void* NB_Create(HWND parentHwnd, const wchar_t* userDataFolder,
const wchar_t* initialUrl, const NB_Callbacks* callbacks)
{
if (!parentHwnd || !IsWindow(parentHwnd)) return nullptr;
try
{
auto* inst = new BrowserInstance(
parentHwnd,
userDataFolder ? userDataFolder : L"",
initialUrl ? initialUrl : L"about:blank",
callbacks ? *callbacks : NB_Callbacks{}
);
return inst;
}
catch (...) { return nullptr; }
}
NB_API void NB_Destroy(void* handle)
{
if (!handle) return;
auto* inst = static_cast<BrowserInstance*>(handle);
inst->PrepareShutdown();
std::thread([inst] {
inst->FinalizeShutdown();
delete inst;
}).detach();
}
NB_API void NB_ShutdownAll(uint32_t timeoutMs)
{
// 复制一份活跃实例列表,避免在等待期间与 NB_Destroy 的后台 delete 线程竞争。
std::vector<BrowserInstance*> insts;
{
std::lock_guard<std::mutex> lk(BrowserInstance::s_liveSetMutex);
insts.assign(BrowserInstance::s_liveInstances.begin(),
BrowserInstance::s_liveInstances.end());
}
if (insts.empty()) return;
// Phase 1: PrepareShutdown 必须在调用线程(父 HWND 所属线程)上执行,
// 因为 ~WndProcHook 调用 RemoveWindowSubclass 要求窗口所属线程。
for (auto* inst : insts)
try { inst->PrepareShutdown(); } catch (...) {}
// Phase 2: 并行投递 close + WM_QUIT 到所有 STA 线程,收集线程句柄等待。
std::vector<HANDLE> threads;
threads.reserve(insts.size());
for (auto* inst : insts)
{
try
{
inst->PostFinalizeShutdown();
if (HANDLE h = inst->GetStaThreadHandle()) threads.push_back(h);
}
catch (...) {}
}
// Phase 3: 在调用线程 pump 消息等待 STA 线程退出。
// 不能 WaitForSingleObject:ICoreWebView2Controller::Close 会跨 apartment 把
// 工作 marshal 回父 HWND 所属线程;若调用线程阻塞不 pump 消息,STA 线程将
// 永久卡在 Close 内部,msedgewebview2.exe 不会被通知退出,并且后续父 HWND
// 销毁时无法整理归 STA 所有的子窗口 → 主线程挂起。
const DWORD start = GetTickCount();
while (!threads.empty())
{
const DWORD elapsed = GetTickCount() - start;
if (elapsed >= timeoutMs) break;
DWORD result = MsgWaitForMultipleObjectsEx(
(DWORD)threads.size(), threads.data(),
timeoutMs - elapsed,
QS_ALLINPUT, MWMO_INPUTAVAILABLE);
if (result == WAIT_TIMEOUT || result == WAIT_FAILED) break;
if (result < WAIT_OBJECT_0 + threads.size())
{
threads.erase(threads.begin() + (result - WAIT_OBJECT_0));
continue;
}
// 输入队列有消息:pump 以让 cross-apartment marshaling 完成。
MSG msg;
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
{
// 调用方的主消息循环需要这条;重新投递并立即返回。
PostQuitMessage((int)msg.wParam);
return;
}
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
NB_API void NB_SetCallbacks(void* handle, const NB_Callbacks* callbacks)
{
if (!handle || !callbacks) return;
if (!BrowserInstance::IsLive(handle)) return;
static_cast<BrowserInstance*>(handle)->SetCallbacks(*callbacks);
}
NB_API bool NB_IsHandleValid(void* handle)
{
return BrowserInstance::IsLive(handle);
}
// ── 导航 ──────────────────────────────────────────────────────────────────
NB_API void NB_Navigate (void* h, const wchar_t* url) { if (h) static_cast<BrowserInstance*>(h)->Navigate(url); }
NB_API void NB_GoBack (void* h) { if (h) static_cast<BrowserInstance*>(h)->GoBack(); }
NB_API void NB_GoForward (void* h) { if (h) static_cast<BrowserInstance*>(h)->GoForward(); }
NB_API void NB_Reload (void* h) { if (h) static_cast<BrowserInstance*>(h)->Reload(); }
NB_API void NB_Stop (void* h) { if (h) static_cast<BrowserInstance*>(h)->Stop(); }
// ── 布局 ──────────────────────────────────────────────────────────────────
NB_API void NB_Resize (void* h, int x, int y, int w, int h2) { if (h) static_cast<BrowserInstance*>(h)->Resize(x, y, w, h2); }
NB_API void NB_SetVisible (void* h, bool v) { if (h) static_cast<BrowserInstance*>(h)->SetVisible(v); }
NB_API void NB_SetTopInset(void* h, int px) { if (h) static_cast<BrowserInstance*>(h)->SetTopInset(px); }
// ── 脚本 ──────────────────────────────────────────────────────────────────
NB_API void NB_ExecuteScript (void* h, const wchar_t* js) { if (h) static_cast<BrowserInstance*>(h)->ExecuteScript(js); }
NB_API void NB_PostWebMessage (void* h, const wchar_t* json) { if (h) static_cast<BrowserInstance*>(h)->PostWebMessage(json); }
// ── 焦点 ──────────────────────────────────────────────────────────────────
NB_API void NB_MoveFocus(void* h) { if (h) static_cast<BrowserInstance*>(h)->MoveFocus(); }
// ── 调试 ──────────────────────────────────────────────────────────────────
NB_API void NB_OpenDevTools(void* h) { if (h) static_cast<BrowserInstance*>(h)->OpenDevTools(); }
// ── Runtime 检测 ──────────────────────────────────────────────────────────
NB_API bool NB_IsRuntimeAvailable()
{
LPWSTR version = nullptr;
HRESULT hr = GetAvailableCoreWebView2BrowserVersionString(nullptr, &version);
if (version) CoTaskMemFree(version);
return SUCCEEDED(hr);
}
NB_API const wchar_t* NB_GetRuntimeVersion()
{
static wchar_t s_buf[128] = {};
LPWSTR version = nullptr;
if (SUCCEEDED(GetAvailableCoreWebView2BrowserVersionString(nullptr, &version)) && version)
{
wcsncpy_s(s_buf, version, _TRUNCATE);
CoTaskMemFree(version);
}
return s_buf;
}
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: ffaeb9254924fd540b5d715b37113e41
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,154 @@
#include "STAThread.h"
#include <objbase.h>
#include <stdexcept>
static constexpr wchar_t kWndClass[] = L"NativeBrowserSTAPump";
static LRESULT CALLBACK PumpWndProc(HWND hwnd, UINT msg, WPARAM w, LPARAM l)
{
return DefWindowProcW(hwnd, msg, w, l);
}
// ── 构造/析构 ──────────────────────────────────────────────────────────────
STAThread::STAThread(std::wstring name)
: m_name(std::move(name))
{
m_readyEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (!m_readyEvent)
throw std::runtime_error("STAThread: CreateEvent failed");
m_thread = CreateThread(nullptr, 0, ThreadProc, this, 0, nullptr);
if (!m_thread)
{
CloseHandle(m_readyEvent);
throw std::runtime_error("STAThread: CreateThread failed");
}
// 等待线程就绪(m_pumpHwnd 已创建,CoInitialize 已完成)
WaitForSingleObject(m_readyEvent, INFINITE);
CloseHandle(m_readyEvent);
m_readyEvent = nullptr;
}
STAThread::~STAThread()
{
Shutdown(5000);
if (m_thread)
{
CloseHandle(m_thread);
m_thread = nullptr;
}
}
// ── 公开 API ──────────────────────────────────────────────────────────────
void STAThread::Post(std::function<void()> work)
{
{
std::lock_guard<std::mutex> lk(m_queueMutex);
m_queue.push(std::move(work));
}
if (m_pumpHwnd)
PostMessageW(m_pumpHwnd, WM_EXECUTE, 0, 0);
}
void STAThread::RequestQuit()
{
// 幂等:仅第一次调用真正发送 WM_QUIT
if (m_shutdown.exchange(true)) return;
if (m_pumpHwnd)
PostMessageW(m_pumpHwnd, WM_QUIT, 0, 0);
}
void STAThread::Shutdown(DWORD timeoutMs)
{
RequestQuit();
if (m_thread)
WaitForSingleObject(m_thread, timeoutMs);
}
// ── 线程实现 ──────────────────────────────────────────────────────────────
DWORD WINAPI STAThread::ThreadProc(LPVOID param)
{
reinterpret_cast<STAThread*>(param)->Run();
return 0;
}
void STAThread::Run()
{
// STA COM 初始化(必须在 GetMessage 循环前完成)
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr) && hr != S_FALSE)
{
SetEvent(m_readyEvent);
return;
}
// 注册 message-only 窗口类(忽略"已注册"错误,重用已有注册即可)
HINSTANCE inst = GetModuleHandleW(nullptr);
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = PumpWndProc;
wc.hInstance = inst;
wc.lpszClassName = kWndClass;
RegisterClassExW(&wc); // 忽略返回值;若已注册直接复用
// 创建 message-only 窗口(HWND_MESSAGE = (HWND)-3)
m_pumpHwnd = CreateWindowExW(
0, kWndClass, L"", 0,
0, 0, 0, 0,
HWND_MESSAGE, nullptr, inst, nullptr);
// 通知构造函数:线程已就绪
SetEvent(m_readyEvent);
if (!m_pumpHwnd)
{
CoUninitialize();
return;
}
// 处理构造函数返回前就入队的工作项
DrainQueue();
// Win32 消息泵——同时服务用户工作队列和 WebView2 内部 COM 回调
MSG msg;
while (GetMessageW(&msg, nullptr, 0, 0) > 0)
{
if (msg.message == WM_EXECUTE)
{
DrainQueue();
continue; // 线程消息无需 Translate/Dispatch
}
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
// 最后清理一次队列(处理 Shutdown 前投递的工作)
DrainQueue();
DestroyWindow(m_pumpHwnd);
m_pumpHwnd = nullptr;
UnregisterClassW(kWndClass, inst);
CoUninitialize();
}
void STAThread::DrainQueue()
{
for (;;)
{
std::function<void()> work;
{
std::lock_guard<std::mutex> lk(m_queueMutex);
if (m_queue.empty()) break;
work = std::move(m_queue.front());
m_queue.pop();
}
// 在 STA 线程上执行,异常不向上传播(避免崩溃消息泵)
try { work(); }
catch (...) {}
}
}
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: 2ac276fea4b65e041940e59b8ee03a84
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,48 @@
#pragma once
#include <windows.h>
#include <atomic>
#include <functional>
#include <queue>
#include <mutex>
#include <string>
/// STA 线程 + Win32 消息泵 + 工作队列。
/// 所有 WebView2 COM 对象必须在同一 STA 线程上创建和访问。
class STAThread {
public:
static constexpr UINT WM_EXECUTE = WM_APP + 1;
/// 启动 STA 线程,阻塞直到线程就绪(message-only 窗口已创建)。
explicit STAThread(std::wstring name = L"WebView2-STA");
~STAThread();
STAThread(const STAThread&) = delete;
STAThread& operator=(const STAThread&) = delete;
/// 在 STA 线程异步执行 work(线程安全,可从任意线程调用)。
void Post(std::function<void()> work);
/// 仅向 STA 线程投递 WM_QUIT,不等待退出(线程安全,幂等)。
/// 调用方负责后续等待 ThreadHandle()。
void RequestQuit();
/// 停止 STA 线程(发送 WM_QUIT)并等待退出(最多 timeoutMs ms)。
/// 等待使用 WaitForSingleObject——若调用方需要在等待期间 pump 消息,请使用 RequestQuit + ThreadHandle 自行 MsgWaitForMultipleObjects。
void Shutdown(DWORD timeoutMs = 5000);
HWND PumpHWND() const { return m_pumpHwnd; }
HANDLE ThreadHandle() const { return m_thread; }
private:
static DWORD WINAPI ThreadProc(LPVOID param);
void Run();
void DrainQueue();
std::wstring m_name;
HANDLE m_thread = nullptr;
HANDLE m_readyEvent = nullptr; // 线程就绪信号
HWND m_pumpHwnd = nullptr; // message-only 窗口
std::mutex m_queueMutex;
std::queue<std::function<void()>> m_queue;
std::atomic<bool> m_shutdown { false };
};
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: e265ceebc7185524f80432008c1508e8
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,52 @@
#include "WndProcHook.h"
WndProcHook::WndProcHook(HWND hwnd,
std::function<void(int w, int h)> onResize,
std::function<void()> onDestroy)
: m_hwnd(hwnd)
, m_onResize(std::move(onResize))
, m_onDestroy(std::move(onDestroy))
{
if (!hwnd || !IsWindow(hwnd)) return;
// data 指向 this,SubclassProc 通过 data 拿回实例指针
m_installed = SetWindowSubclass(hwnd, SubclassProc, kSubclassId,
reinterpret_cast<DWORD_PTR>(this));
}
WndProcHook::~WndProcHook()
{
if (m_installed && m_hwnd && IsWindow(m_hwnd))
RemoveWindowSubclass(m_hwnd, SubclassProc, kSubclassId);
}
LRESULT CALLBACK WndProcHook::SubclassProc(
HWND hwnd, UINT msg, WPARAM w, LPARAM l,
UINT_PTR subclassId, DWORD_PTR data)
{
auto* self = reinterpret_cast<WndProcHook*>(data);
switch (msg)
{
case WM_SIZE:
{
int width = LOWORD(l);
int height = HIWORD(l);
if (self && self->m_onResize && (width > 0 || height > 0))
self->m_onResize(width, height);
break;
}
case WM_DESTROY:
// 先通知上层,再自动卸载 hook(窗口即将销毁,不需要再保留)
if (self)
{
self->m_installed = false; // 防止析构函数重复 Remove
if (self->m_onDestroy)
self->m_onDestroy();
}
RemoveWindowSubclass(hwnd, SubclassProc, subclassId);
break;
}
return DefSubclassProc(hwnd, msg, w, l);
}
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: e747acbe12284b945801da61a4cf8a02
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
#pragma once
#include <windows.h>
#include <commctrl.h>
#include <functional>
/// 使用 SetWindowSubclass 对父 HWND 进行子类化,
/// 拦截 WM_SIZE(同步 WebView2 大小)和 WM_DESTROY(通知 C# 重建)。
/// SetWindowSubclass 比 SetWindowLongPtr 更安全,支持多层 hook 共存。
class WndProcHook {
public:
WndProcHook(HWND hwnd,
std::function<void(int w, int h)> onResize,
std::function<void()> onDestroy);
~WndProcHook(); // 自动 Unhook
WndProcHook(const WndProcHook&) = delete;
WndProcHook& operator=(const WndProcHook&) = delete;
private:
static LRESULT CALLBACK SubclassProc(
HWND hwnd, UINT msg, WPARAM w, LPARAM l,
UINT_PTR subclassId, DWORD_PTR data);
HWND m_hwnd;
bool m_installed = false;
std::function<void(int w, int h)> m_onResize;
std::function<void()> m_onDestroy;
static constexpr UINT_PTR kSubclassId = 0xC0DE1234;
};
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: ebcae0884dd4a174ba4583a1f6fb25bd
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b049108f6bef8e043a09a31eda129d5a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7f4f14217db9afd4db6275362c570019
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c50d484c6dc510340bdeb39b5d8d9fd8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: 62f77e9053e91ce458f4ae5435066105
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: db85966d76b8a9343ab2a40ba1dd12ab
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0acaf66b3e60e5b46b80de339531f445
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,43 @@
fileFormatVersion: 2
guid: 3df12db069448324ba9f45ea9785f546
PluginImporter:
externalObjects: {}
serializedVersion: 3
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
Any:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
Editor:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
Linux64:
enabled: 0
settings:
CPU: None
Win:
enabled: 0
settings:
CPU: None
Win64:
enabled: 0
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,63 @@
fileFormatVersion: 2
guid: 2cb1ee4cdb45fae41a791bcdb59188ac
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
:
second:
enabled: 0
settings: {}
- first:
: Any
second:
enabled: 0
settings:
Exclude Editor: 0
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: x86
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: x86_64
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,89 @@
fileFormatVersion: 2
guid: c0f1e4f2a8f94f8f8fd7b6a90d7b6e3c
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude ArmLinux: 1
Exclude ArmLinuxServer: 1
Exclude Editor: 0
Exclude EmbeddedLinux: 1
Exclude HMIAndroid: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude QNX: 1
Exclude WeixinMiniGame: 1
Exclude Win: 1
Exclude Win64: 1
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: x86_64
DefaultValueInitialized: true
OS: Windows
- first:
EmbeddedLinux: EmbeddedLinux
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
HMIAndroid: HMIAndroid
second:
enabled: 0
settings:
AndroidSharedLibraryType: Executable
CPU: ARMv7
- first:
QNX: QNX
second:
enabled: 0
settings:
CPU: AnyCPU
SDK: AnySDK
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: x86
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: x86_64
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,35 @@
fileFormatVersion: 2
guid: f8d3ec2fa14b4f5ea0a60d3116f7588f
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: x86_64
OS: Windows
- first:
Standalone: Standalone
second:
enabled: 1
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,35 @@
fileFormatVersion: 2
guid: 3dbbf1ff959f4e598f22563801107b6b
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: x86_64
OS: Windows
- first:
Standalone: Standalone
second:
enabled: 1
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,35 @@
fileFormatVersion: 2
guid: 8bb37c264a8d43949451e588006fb08f
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: x86_64
OS: Windows
- first:
Standalone: Standalone
second:
enabled: 1
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,35 @@
fileFormatVersion: 2
guid: eb05c77866434789a167d013f4b30a97
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 1
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
CPU: x86_64
OS: Windows
- first:
Standalone: Standalone
second:
enabled: 1
settings:
CPU: x86_64
OS: Windows
userData:
assetBundleName:
assetBundleVariant: