```
feat(gameplay): 更新单位属性和材质配置 - 调整多个单位的基础生命值和攻击力数值平衡 - 修改CoreBattleGround材质的纹理缩放比例 - 更新地砖材质颜色为白色 - 优化纹理资源设置,调整最大纹理尺寸并添加PlayableAds平台支持 - 修复弓箭手技能目标查找逻辑,改用统一的敌人检测方法 - 为水晶矿添加中立阵营设置和光球视觉效果同步 - 调整预制体中的全局缩放参数 - 为狼人和石巨人添加NavMeshAgent组件支持 - 优化水晶矿血条显示位置计算方式 ```
This commit is contained in:
1 parent
0e7de5f885
commit
53d95802e5
448 files changed
+133333
-472
No files matched your search
+400
@@ -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);
|
||||
}
|
||||
+43
@@ -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:
|
||||
+110
@@ -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;
|
||||
};
|
||||
+43
@@ -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:
|
||||
+168
@@ -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;
|
||||
}
|
||||
+43
@@ -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:
|
||||
+154
@@ -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 (...) {}
|
||||
}
|
||||
}
|
||||
+43
@@ -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:
|
||||
+48
@@ -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 };
|
||||
};
|
||||
+43
@@ -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:
|
||||
+52
@@ -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);
|
||||
}
|
||||
+43
@@ -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:
|
||||
+30
@@ -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;
|
||||
};
|
||||
+43
@@ -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:
|
||||
Reference in new issue
Block a user