mpelec使用一个透明窗体覆盖在libmpv渲染的窗体上。最初实现绑定两个窗体位置的方法是基于Electron上窗口的两个事件:
osc.on("move", () => {
win.setBounds(osc.getBounds());
});
osc.on("resize", () => {
win.setBounds(osc.getBounds());
});
这样处理确实OK,但是有性能上的问题:拖动的时候窗口绘制卡顿,然后风扇开始发力...这张是使用Electron事件处理的方法,移动的时候窗口错位、帧率较低。

为了提高体验,我考虑到使用更底层的方法来绑定窗体:Hook原本的WindowProc函数,手动处理WM_SIZE和WM_MOVE两个消息。
使用到的关键函数是 SetWindowLongPtr
和 GetWindowLongPtr
,前者用来注入我们自定义的消息处理函数,后者用来保存原本的函数以供处理其他消息。
获取窗体指针并绑定自定义函数
js端的调用:
let win = new electron.BrowserWindow({
...WindowConfig.PWIN
});
let hwnd_pwin = win.getNativeWindowHandle().readInt32LE();
let osc = new electron.BrowserWindow({
...WindowConfig.OSC,
parent: win
});
let hwnd_osc = osc.getNativeWindowHandle().readInt32LE();
// bind window movement
addon.bind_window(hwnd_osc, hwnd_pwin);
在C++ Addon的被调用函数中:
Napi::Value BindMove(const Napi::CallbackInfo &info)
{
Napi::Env env = info.Env();
if (info.Length() < 2)
{
Napi::TypeError::New(env, "Need more args.").ThrowAsJavaScriptException();
return env.Null();
}
HWND osc_hwnd = (HWND)(info[0].As<Napi::Number>().Int32Value());
HWND pwin_hwnd = (HWND)(info[1].As<Napi::Number>().Int32Value());
osc = osc_hwnd;
pwin = pwin_hwnd;
old_proc = (WNDPROC)GetWindowLongPtr(pwin, GWLP_WNDPROC);
SetWindowLongPtr(osc_hwnd, GWLP_WNDPROC, (LONG_PTR)_HookWindowProc);
return env.Null();
}
实际处理消息的_HookWindowProc
函数:
LRESULT CALLBACK _HookWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (uMsg == WM_MOVE || uMsg == WM_SIZE)
{
RECT osc_rect;
GetWindowRect(osc, &osc_rect);
SetWindowPos(pwin,
HWND_TOPMOST,
osc_rect.left,
osc_rect.top,
osc_rect.right - osc_rect.left,
osc_rect.bottom - osc_rect.top,
SWP_NOZORDER);
}
return CallWindowProc(old_proc, hwnd, uMsg, wParam, lParam);
}
效果对比
折腾这么多,实际效果如何? 努力没白费,使用底层方法处理后,窗体移动丝滑流畅。两段比较的视频不知道怎么上传,转成GIF对比起来又不明显,日后有机会再上视频比较吧。