604 lines
24 KiB
C++
604 lines
24 KiB
C++
#include "flutter_window.h"
|
||
|
||
#include <optional>
|
||
#include <commctrl.h>
|
||
|
||
#include <chrono>
|
||
#include <fstream>
|
||
#include <mutex>
|
||
#include <sstream>
|
||
#include <string>
|
||
|
||
#include <flutter/method_channel.h>
|
||
#include <flutter/standard_method_codec.h>
|
||
|
||
#include "flutter/generated_plugin_registrant.h"
|
||
|
||
// GET_X_LPARAM / GET_Y_LPARAM from windowsx.h
|
||
// Defined inline to avoid potential macro conflicts with plugin code
|
||
#ifndef GET_X_LPARAM
|
||
#define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp))
|
||
#endif
|
||
#ifndef GET_Y_LPARAM
|
||
#define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp))
|
||
#endif
|
||
|
||
// ============================================================
|
||
// debug instrumentation for window-drag-lag (flutter_window side)
|
||
// ============================================================
|
||
namespace {
|
||
std::mutex g_debug_log_mutex2;
|
||
std::wstring g_debug_log_path2;
|
||
|
||
void DebugLog(const std::string& tag, const std::string& detail) {
|
||
// 已禁用文件日志以排除 I/O 对消息循环的干扰。
|
||
(void)tag;
|
||
(void)detail;
|
||
#if 0
|
||
auto now = std::chrono::system_clock::now();
|
||
auto us = std::chrono::duration_cast<std::chrono::microseconds>(
|
||
now.time_since_epoch())
|
||
.count();
|
||
std::lock_guard<std::mutex> lock(g_debug_log_mutex2);
|
||
if (g_debug_log_path2.empty()) {
|
||
g_debug_log_path2 = L"E:\\project\\flutter\\f\\xianyan\\xianyan_drag_debug.log";
|
||
}
|
||
std::wofstream ofs(g_debug_log_path2, std::ios::app);
|
||
if (!ofs.is_open()) return;
|
||
ofs << us << L" [" << std::wstring(tag.begin(), tag.end()) << L"] "
|
||
<< std::wstring(detail.begin(), detail.end()) << L"\n";
|
||
#endif
|
||
}
|
||
|
||
std::string MsgName(UINT msg) {
|
||
switch (msg) {
|
||
case WM_NCHITTEST: return "WM_NCHITTEST";
|
||
case WM_NCLBUTTONDOWN: return "WM_NCLBUTTONDOWN";
|
||
case WM_NCLBUTTONUP: return "WM_NCLBUTTONUP";
|
||
case WM_NCMOUSEMOVE: return "WM_NCMOUSEMOVE";
|
||
case WM_LBUTTONDOWN: return "WM_LBUTTONDOWN";
|
||
case WM_LBUTTONUP: return "WM_LBUTTONUP";
|
||
case WM_MOUSEMOVE: return "WM_MOUSEMOVE";
|
||
case WM_MOVE: return "WM_MOVE";
|
||
case WM_MOVING: return "WM_MOVING";
|
||
case WM_SIZE: return "WM_SIZE";
|
||
case WM_WINDOWPOSCHANGING: return "WM_WINDOWPOSCHANGING";
|
||
case WM_WINDOWPOSCHANGED: return "WM_WINDOWPOSCHANGED";
|
||
case WM_SYSCOMMAND: return "WM_SYSCOMMAND";
|
||
case WM_ENTERSIZEMOVE: return "WM_ENTERSIZEMOVE";
|
||
case WM_EXITSIZEMOVE: return "WM_EXITSIZEMOVE";
|
||
case WM_PAINT: return "WM_PAINT";
|
||
case WM_ERASEBKGND: return "WM_ERASEBKGND";
|
||
default: return "MSG_" + std::to_string(msg);
|
||
}
|
||
}
|
||
} // namespace
|
||
|
||
// ============================================================
|
||
// 静态成员初始化
|
||
// ============================================================
|
||
bool FlutterWindow::is_in_native_drag_ = false;
|
||
|
||
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||
: project_(project) {}
|
||
|
||
FlutterWindow::~FlutterWindow() {}
|
||
|
||
bool FlutterWindow::OnCreate() {
|
||
if (!Win32Window::OnCreate()) {
|
||
return false;
|
||
}
|
||
|
||
RECT frame = GetClientArea();
|
||
|
||
// The size here must match the window dimensions to avoid unnecessary surface
|
||
// creation / destruction in the startup path.
|
||
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||
frame.right - frame.left, frame.bottom - frame.top, project_);
|
||
// Ensure that basic setup of the controller was successful.
|
||
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
||
return false;
|
||
}
|
||
RegisterPlugins(flutter_controller_->engine());
|
||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||
|
||
// ============================================================
|
||
// 子类化 Flutter 子窗口:标题栏命中测试穿透
|
||
// Flutter 子窗口覆盖整个客户区,拦截了所有鼠标消息。
|
||
// 通过子类化,在标题栏可拖拽区域返回 HTTRANSPARENT,让父窗口
|
||
// 的 WM_NCHITTEST 有机会返回 HTCAPTION,从而触发 Windows 原生
|
||
// 模态拖拽循环。原生拖拽由 DWM 直接处理,延迟最低。
|
||
// 标题栏右侧控制按钮区域返回 HTCLIENT,保证按钮可点击。
|
||
// ============================================================
|
||
HWND child_hwnd = flutter_controller_->view()->GetNativeWindow();
|
||
SetWindowSubclass(child_hwnd, ChildWndProc, kChildSubclassId,
|
||
reinterpret_cast<DWORD_PTR>(GetHandle()));
|
||
|
||
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||
this->Show();
|
||
});
|
||
|
||
// Flutter can complete the first frame before the "show window" callback is
|
||
// registered. The following call ensures a frame is pending to ensure the
|
||
// window is shown. It is a no-op if the first frame hasn't completed yet.
|
||
flutter_controller_->ForceRedraw();
|
||
|
||
// Register Windows platform MethodChannel for theme control
|
||
const static std::string channel_name("apps.xy.xianyan/windows");
|
||
|
||
platform_channel_ = std::make_unique<flutter::MethodChannel<>>(
|
||
flutter_controller_->engine()->messenger(), channel_name,
|
||
&flutter::StandardMethodCodec::GetInstance());
|
||
|
||
platform_channel_->SetMethodCallHandler(
|
||
[this](const flutter::MethodCall<>& call,
|
||
std::unique_ptr<flutter::MethodResult<>> result) {
|
||
const std::string& method = call.method_name();
|
||
|
||
if (method == "setDarkMode") {
|
||
bool is_dark = false;
|
||
if (const auto* args = std::get_if<flutter::EncodableMap>(call.arguments())) {
|
||
auto it = args->find(flutter::EncodableValue("isDark"));
|
||
if (it != args->end() && std::holds_alternative<bool>(it->second)) {
|
||
is_dark = std::get<bool>(it->second);
|
||
}
|
||
}
|
||
Win32Window::SetDarkMode(GetHandle(), is_dark);
|
||
result->Success();
|
||
} else if (method == "setWindowTitle") {
|
||
std::string title;
|
||
if (const auto* args = std::get_if<flutter::EncodableMap>(call.arguments())) {
|
||
auto it = args->find(flutter::EncodableValue("title"));
|
||
if (it != args->end() && std::holds_alternative<std::string>(it->second)) {
|
||
title = std::get<std::string>(it->second);
|
||
}
|
||
}
|
||
std::wstring wide_title(title.begin(), title.end());
|
||
Win32Window::SetWindowTitle(GetHandle(), wide_title);
|
||
result->Success();
|
||
} else if (method == "setFullscreen") {
|
||
bool fullscreen = false;
|
||
if (const auto* args = std::get_if<flutter::EncodableMap>(call.arguments())) {
|
||
auto it = args->find(flutter::EncodableValue("fullscreen"));
|
||
if (it != args->end() && std::holds_alternative<bool>(it->second)) {
|
||
fullscreen = std::get<bool>(it->second);
|
||
}
|
||
}
|
||
Win32Window::SetFullscreen(GetHandle(), fullscreen);
|
||
result->Success();
|
||
} else if (method == "isFullscreen") {
|
||
bool is_fullscreen = Win32Window::IsFullscreen(GetHandle());
|
||
result->Success(flutter::EncodableValue(is_fullscreen));
|
||
} else if (method == "setMinSize") {
|
||
unsigned int width = 0;
|
||
unsigned int height = 0;
|
||
if (const auto* args = std::get_if<flutter::EncodableMap>(call.arguments())) {
|
||
auto it_w = args->find(flutter::EncodableValue("width"));
|
||
auto it_h = args->find(flutter::EncodableValue("height"));
|
||
if (it_w != args->end() && std::holds_alternative<int>(it_w->second)) {
|
||
width = static_cast<unsigned int>(std::get<int>(it_w->second));
|
||
}
|
||
if (it_h != args->end() && std::holds_alternative<int>(it_h->second)) {
|
||
height = static_cast<unsigned int>(std::get<int>(it_h->second));
|
||
}
|
||
}
|
||
Win32Window::SetMinSize(GetHandle(), width, height);
|
||
result->Success();
|
||
} else if (method == "performHapticFeedback") {
|
||
int feedback_type = 0;
|
||
if (const auto* args = std::get_if<flutter::EncodableMap>(call.arguments())) {
|
||
auto it = args->find(flutter::EncodableValue("type"));
|
||
if (it != args->end() && std::holds_alternative<int>(it->second)) {
|
||
feedback_type = std::get<int>(it->second);
|
||
}
|
||
}
|
||
Win32Window::PerformHapticFeedback(GetHandle(), feedback_type);
|
||
result->Success();
|
||
} else if (method == "getSystemAppearance") {
|
||
std::string appearance = Win32Window::GetSystemAppearance();
|
||
result->Success(flutter::EncodableValue(appearance));
|
||
} else {
|
||
result->NotImplemented();
|
||
}
|
||
});
|
||
|
||
// ============================================================
|
||
// 窗口控制 MethodChannel(窗口大小预设菜单等)
|
||
// ============================================================
|
||
window_control_channel_ = std::make_unique<flutter::MethodChannel<>>(
|
||
flutter_controller_->engine()->messenger(), "xianyan/window_control",
|
||
&flutter::StandardMethodCodec::GetInstance());
|
||
|
||
window_control_channel_->SetMethodCallHandler(
|
||
[this](const flutter::MethodCall<>& call,
|
||
std::unique_ptr<flutter::MethodResult<>> result) {
|
||
const std::string& method = call.method_name();
|
||
|
||
if (method == "showWindowSizeMenu") {
|
||
// 使用 PostMessage 异步弹出菜单,避免 TrackPopupMenuEx
|
||
// 阻塞 MethodChannel 回调线程(Flutter UI 线程)
|
||
PostMessage(GetHandle(), WM_USER + 1000, 0, 0);
|
||
result->Success(flutter::EncodableValue(true));
|
||
} else {
|
||
result->NotImplemented();
|
||
}
|
||
});
|
||
|
||
return true;
|
||
}
|
||
|
||
void FlutterWindow::OnDestroy() {
|
||
if (flutter_controller_) {
|
||
HWND child_hwnd = flutter_controller_->view()->GetNativeWindow();
|
||
RemoveWindowSubclass(child_hwnd, ChildWndProc, kChildSubclassId);
|
||
flutter_controller_ = nullptr;
|
||
}
|
||
|
||
Win32Window::OnDestroy();
|
||
}
|
||
|
||
// ============================================================
|
||
// 原生窗口大小预设菜单
|
||
//
|
||
// 由于 Mica/Acrylic 导致 DefWindowProc 边框命中测试失效,
|
||
// 手动实现的边框命中测试在某些环境下仍无法调整窗口大小。
|
||
// 这里提供点击"口"按钮弹出原生菜单选择窗口大小的替代方案。
|
||
//
|
||
// 使用 Win32 TrackPopupMenuEx 弹出原生菜单,符合 Windows 原生风格。
|
||
// 菜单项包括:最大化/还原、几种预设尺寸。
|
||
// ============================================================
|
||
bool FlutterWindow::ShowWindowSizeMenu(HWND hwnd) {
|
||
if (!hwnd) return false;
|
||
|
||
// 创建弹出菜单
|
||
HMENU hMenu = CreatePopupMenu();
|
||
if (!hMenu) return false;
|
||
|
||
// 检查当前是否最大化
|
||
bool is_maximized = (::IsZoomed(hwnd) != 0);
|
||
|
||
// 菜单项 ID(从 1001 开始,避免与系统命令冲突)
|
||
enum MenuId {
|
||
ID_TOGGLE_MAXIMIZE = 1001,
|
||
ID_SIZE_800x600,
|
||
ID_SIZE_1024x768,
|
||
ID_SIZE_1280x720,
|
||
ID_SIZE_1440x900,
|
||
ID_SIZE_1920x1080,
|
||
};
|
||
|
||
// 添加菜单项
|
||
AppendMenuW(hMenu, MF_STRING,
|
||
ID_TOGGLE_MAXIMIZE,
|
||
is_maximized ? L"🔽 还原窗口" : L"⬜ 最大化");
|
||
AppendMenuW(hMenu, MF_SEPARATOR, 0, nullptr);
|
||
AppendMenuW(hMenu, MF_STRING, ID_SIZE_800x600, L"📱 小窗 800 × 600");
|
||
AppendMenuW(hMenu, MF_STRING, ID_SIZE_1024x768, L"💻 标准 1024 × 768");
|
||
AppendMenuW(hMenu, MF_STRING, ID_SIZE_1280x720, L"🖥️ 宽屏 1280 × 720");
|
||
AppendMenuW(hMenu, MF_STRING, ID_SIZE_1440x900, L"🖥️ 大屏 1440 × 900");
|
||
AppendMenuW(hMenu, MF_STRING, ID_SIZE_1920x1080, L"📺 全高清 1920 × 1080");
|
||
|
||
// 获取鼠标位置(菜单显示在鼠标位置)
|
||
POINT cursor_pos;
|
||
GetCursorPos(&cursor_pos);
|
||
|
||
// 设置菜单为右对齐、右键选择
|
||
UINT flags = TPM_LEFTALIGN | TPM_TOPALIGN | TPM_LEFTBUTTON |
|
||
TPM_RIGHTBUTTON | TPM_RETURNCMD;
|
||
|
||
// 弹出菜单(同步阻塞,返回选择的菜单项 ID,0 表示取消)
|
||
int cmd = TrackPopupMenuEx(hMenu, flags, cursor_pos.x, cursor_pos.y, hwnd,
|
||
nullptr);
|
||
DestroyMenu(hMenu);
|
||
|
||
if (cmd == 0) return false; // 用户取消
|
||
|
||
// 处理用户选择
|
||
int width = 0, height = 0;
|
||
switch (cmd) {
|
||
case ID_TOGGLE_MAXIMIZE:
|
||
if (is_maximized) {
|
||
PostMessage(hwnd, WM_SYSCOMMAND, SC_RESTORE, 0);
|
||
} else {
|
||
PostMessage(hwnd, WM_SYSCOMMAND, SC_MAXIMIZE, 0);
|
||
}
|
||
return true;
|
||
case ID_SIZE_800x600: width = 800; height = 600; break;
|
||
case ID_SIZE_1024x768: width = 1024; height = 768; break;
|
||
case ID_SIZE_1280x720: width = 1280; height = 720; break;
|
||
case ID_SIZE_1440x900: width = 1440; height = 900; break;
|
||
case ID_SIZE_1920x1080: width = 1920; height = 1080; break;
|
||
default: return false;
|
||
}
|
||
|
||
// 取消最大化(如果当前是最大化状态)
|
||
if (is_maximized) {
|
||
ShowWindow(hwnd, SW_RESTORE);
|
||
}
|
||
|
||
// 获取窗口当前位置,保持左上角不变
|
||
RECT wr;
|
||
GetWindowRect(hwnd, &wr);
|
||
|
||
// 调整为指定大小(考虑 DPI 缩放)
|
||
UINT dpi = GetDpiForWindow(hwnd);
|
||
int physical_width = static_cast<int>(width * dpi / 96.0);
|
||
int physical_height = static_cast<int>(height * dpi / 96.0);
|
||
|
||
SetWindowPos(hwnd, nullptr, wr.left, wr.top,
|
||
physical_width, physical_height,
|
||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||
|
||
return true;
|
||
}
|
||
|
||
// ============================================================
|
||
// Flutter 子窗口子类化回调
|
||
//
|
||
// 核心思路:
|
||
// 1. 标题栏可拖拽区域返回 HTTRANSPARENT,让父窗口的 WM_NCHITTEST
|
||
// 返回 HTCAPTION,Windows 随后进入原生模态拖拽循环。
|
||
// 2. 标题栏右侧控制按钮区域返回 HTCLIENT,保证 Flutter 按钮可点击。
|
||
// 3. 其他区域返回 DefSubclassProc,由 Flutter 正常处理。
|
||
//
|
||
// 注意:窗口边框缩放由父窗口原生非客户区处理,不需要子窗口干预。
|
||
// ============================================================
|
||
LRESULT CALLBACK FlutterWindow::ChildWndProc(
|
||
HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam,
|
||
UINT_PTR subclass_id, DWORD_PTR ref_data) {
|
||
HWND top_hwnd = reinterpret_cast<HWND>(ref_data);
|
||
|
||
// ============================================================
|
||
// 拖拽期间:拦截 Flutter 子窗口的所有消息(除了 WM_NCHITTEST)
|
||
//
|
||
// Flutter 子窗口覆盖整个客户区,在拖拽期间会持续接收消息
|
||
// (WM_PAINT/WM_MOVE/WM_SIZE/WM_WINDOWPOSCHANGED 等)并触发
|
||
// Flutter 引擎重绘,阻塞 Win32 消息循环(= UI 线程),导致
|
||
// DWM 无法应用 live-drag 优化。
|
||
//
|
||
// 拖拽期间所有消息直接交给 DefWindowProc(不经过 Flutter 引擎的
|
||
// WndProc),阻止 Flutter 重绘。WM_NCHITTEST 仍需正常处理,
|
||
// 否则鼠标在标题栏的命中测试会失败。
|
||
// ============================================================
|
||
if (is_in_native_drag_ && message != WM_NCHITTEST) {
|
||
return DefWindowProc(hwnd, message, wparam, lparam);
|
||
}
|
||
|
||
// region debug-point child-msg
|
||
if (message == WM_NCHITTEST || message == WM_NCLBUTTONDOWN ||
|
||
message == WM_NCLBUTTONUP || message == WM_NCMOUSEMOVE ||
|
||
message == WM_LBUTTONDOWN || message == WM_LBUTTONUP ||
|
||
message == WM_MOUSEMOVE || message == WM_MOVE || message == WM_MOVING ||
|
||
message == WM_SIZE || message == WM_WINDOWPOSCHANGING ||
|
||
message == WM_WINDOWPOSCHANGED || message == WM_SYSCOMMAND ||
|
||
message == WM_ENTERSIZEMOVE || message == WM_EXITSIZEMOVE ||
|
||
message == WM_PAINT || message == WM_ERASEBKGND) {
|
||
std::ostringstream oss;
|
||
oss << "hwnd=" << hwnd << " msg=" << MsgName(message)
|
||
<< " wp=" << wparam << " lp=" << lparam;
|
||
DebugLog("child", oss.str());
|
||
}
|
||
// endregion debug-point child-msg
|
||
|
||
switch (message) {
|
||
case WM_NCHITTEST: {
|
||
POINT pt = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)};
|
||
ScreenToClient(hwnd, &pt);
|
||
|
||
UINT dpi = GetDpiForWindow(top_hwnd);
|
||
double scale = static_cast<double>(dpi) / 96.0;
|
||
int title_h = static_cast<int>(36 * scale);
|
||
int btn_w = static_cast<int>(138 * scale);
|
||
RECT cr;
|
||
GetClientRect(top_hwnd, &cr);
|
||
|
||
{
|
||
std::ostringstream oss;
|
||
oss << "WM_NCHITTEST raw_screen=(" << GET_X_LPARAM(lparam) << ","
|
||
<< GET_Y_LPARAM(lparam) << ") client=(" << pt.x << "," << pt.y
|
||
<< ") dpi=" << dpi << " scale=" << scale
|
||
<< " title_h=" << title_h << " btn_w=" << btn_w
|
||
<< " cr=(" << cr.right << "," << cr.bottom << ")";
|
||
DebugLog("child", oss.str());
|
||
}
|
||
|
||
if (cr.right <= 0 || cr.bottom <= 0) {
|
||
DebugLog("child", "WM_NCHITTEST fallback: empty client rect");
|
||
return DefSubclassProc(hwnd, message, wparam, lparam);
|
||
}
|
||
|
||
// 标题栏可拖拽区域:穿透到父窗口,父窗口会返回 HTCAPTION
|
||
if (pt.y >= 0 && pt.y < title_h &&
|
||
pt.x >= 0 && pt.x < cr.right - btn_w) {
|
||
std::ostringstream oss;
|
||
oss << "WM_NCHITTEST -> HTTRANSPARENT pt=(" << pt.x << "," << pt.y
|
||
<< ")";
|
||
DebugLog("child", oss.str());
|
||
return HTTRANSPARENT;
|
||
}
|
||
|
||
// 标题栏右侧控制按钮区域:保持 HTCLIENT,让 Flutter 处理点击
|
||
if (pt.y >= 0 && pt.y < title_h &&
|
||
pt.x >= cr.right - btn_w && pt.x <= cr.right) {
|
||
DebugLog("child", "WM_NCHITTEST -> HTCLIENT (buttons)");
|
||
return HTCLIENT;
|
||
}
|
||
|
||
DebugLog("child", "WM_NCHITTEST -> DefSubclassProc");
|
||
return DefSubclassProc(hwnd, message, wparam, lparam);
|
||
}
|
||
}
|
||
|
||
return DefSubclassProc(hwnd, message, wparam, lparam);
|
||
}
|
||
|
||
LRESULT
|
||
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
||
WPARAM const wparam,
|
||
LPARAM const lparam) noexcept {
|
||
// ============================================================
|
||
// 异步弹出窗口大小预设菜单
|
||
// MethodChannel 回调中通过 PostMessage 触发,避免 TrackPopupMenuEx
|
||
// 阻塞 Flutter UI 线程
|
||
// ============================================================
|
||
if (message == WM_USER + 1000) {
|
||
ShowWindowSizeMenu(hwnd);
|
||
return 0;
|
||
}
|
||
|
||
// ============================================================
|
||
// 拖拽卡顿修复 v9:WM_NCLBUTTONDOWN 时禁用 Mica + SetWindowCompositionAttribute
|
||
//
|
||
// 根因链(经 v1~v8 验证):
|
||
// 1. 手动 SetWindowPos 拖拽不触发 DWM "live drag" 优化
|
||
// 2. 完全禁用 Mica 能解决卡顿(v8 验证),但临时禁用 Mica(v4~v7)失败
|
||
// 3. v4~v7 失败的原因:EnterSizeMove 遗漏了 SetWindowCompositionAttribute
|
||
// (ACCENT_DISABLED) 调用——这是 flutter_acrylic 的 setEffect(disabled)
|
||
// 能立即禁用 Mica 的关键。仅靠 DwmSetWindowAttribute 是异步的,不生效。
|
||
//
|
||
// v9 修复:
|
||
// - WM_NCLBUTTONDOWN(HTCAPTION) 时(拖拽还没开始)调用 EnterSizeMove 禁用 Mica
|
||
// (现在包含 SetWindowCompositionAttribute(ACCENT_DISABLED),能立即生效)
|
||
// - SWP_FRAMECHANGED 强制 DWM 同步应用变更
|
||
// - DefWindowProc 进入 SC_MOVE 模态循环
|
||
// - DefWindowProc 返回后(拖拽已结束)调用 ExitSizeMove 恢复 Mica
|
||
// - 模态循环期间绕过插件链 + 子窗口消息拦截(v8)
|
||
// ============================================================
|
||
|
||
// --- 标题栏 + 边框命中测试:直接返回,绕过插件 ---
|
||
// flutter_acrylic 的 DwmExtendFrameIntoClientArea({-1,-1,-1,-1}) 和
|
||
// window_manager 的 TitleBarStyle.hidden 会导致 DefWindowProc 的边框
|
||
// 命中测试失效。这里手动实现边框命中测试,恢复窗口边缘调整大小功能。
|
||
if (message == WM_NCHITTEST) {
|
||
POINT pt = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)};
|
||
|
||
UINT dpi = GetDpiForWindow(hwnd);
|
||
double scale = static_cast<double>(dpi) / 96.0;
|
||
int title_h = static_cast<int>(36.0 * scale);
|
||
int btn_w = static_cast<int>(138.0 * scale);
|
||
// 边框命中测试宽度(稍大于实际边框,方便用户操作)
|
||
int border_w = static_cast<int>(6.0 * scale);
|
||
|
||
// 使用窗口坐标(而非客户区坐标),避免客户区边距导致的负坐标问题
|
||
RECT wr;
|
||
GetWindowRect(hwnd, &wr);
|
||
|
||
bool maximized = (::IsZoomed(hwnd) != 0);
|
||
|
||
if (!maximized) {
|
||
bool on_left = pt.x >= wr.left && pt.x < wr.left + border_w;
|
||
bool on_right = pt.x >= wr.right - border_w && pt.x < wr.right;
|
||
bool on_top = pt.y >= wr.top && pt.y < wr.top + border_w;
|
||
bool on_bottom = pt.y >= wr.bottom - border_w && pt.y < wr.bottom;
|
||
|
||
if (on_top && on_left) return HTTOPLEFT;
|
||
if (on_top && on_right) return HTTOPRIGHT;
|
||
if (on_bottom && on_left) return HTBOTTOMLEFT;
|
||
if (on_bottom && on_right) return HTBOTTOMRIGHT;
|
||
if (on_left) return HTLEFT;
|
||
if (on_right) return HTRIGHT;
|
||
if (on_top) return HTTOP;
|
||
if (on_bottom) return HTBOTTOM;
|
||
}
|
||
|
||
// 标题栏拖拽区域(使用客户区坐标判断)
|
||
POINT client_pt = pt;
|
||
ScreenToClient(hwnd, &client_pt);
|
||
RECT cr;
|
||
GetClientRect(hwnd, &cr);
|
||
|
||
if (cr.right > 0 && cr.bottom > 0) {
|
||
if (client_pt.y >= 0 && client_pt.y < title_h &&
|
||
client_pt.x >= 0 && client_pt.x < cr.right - btn_w) {
|
||
return HTCAPTION;
|
||
}
|
||
}
|
||
// 非标题栏/边框区域:继续交给插件处理(返回 HTCLIENT)
|
||
}
|
||
|
||
// --- 标题栏拖拽启动:提前禁用 Mica,再进入原生 SC_MOVE 模态循环 ---
|
||
// 关键:在 WM_NCLBUTTONDOWN 时(拖拽还没开始)禁用 Mica。
|
||
// EnterSizeMove 现在包含 SetWindowCompositionAttribute(ACCENT_DISABLED),
|
||
// 能立即禁用 Mica(与 flutter_acrylic 的 setEffect(disabled) 一致)。
|
||
if (message == WM_NCLBUTTONDOWN && wparam == HTCAPTION) {
|
||
// 1. 拖拽开始前禁用 Mica/Acrylic backdrop(立即生效)
|
||
EnterSizeMove(hwnd);
|
||
// 2. SWP_FRAMECHANGED 触发 WM_NCCALCSIZE,强制 DWM 同步应用变更
|
||
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
|
||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||
// 3. 进入原生 SC_MOVE 模态循环(同步阻塞,返回时拖拽已结束)
|
||
LRESULT drag_result = DefWindowProc(hwnd, message, wparam, lparam);
|
||
// 4. 拖拽结束后恢复 Mica/Acrylic backdrop
|
||
ExitSizeMove(hwnd);
|
||
// 5. SWP_FRAMECHANGED 强制 DWM 同步应用 Mica 恢复
|
||
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
|
||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||
return drag_result;
|
||
}
|
||
|
||
// --- 原生拖拽/调整大小模态循环开始 ---
|
||
// 无论是拖拽标题栏(已在 WM_NCLBUTTONDOWN 提前禁用 Mica)还是从边框调整大小,
|
||
// 都会触发 WM_ENTERSIZEMOVE。对于边框调整大小的情况,这里禁用 Mica。
|
||
if (message == WM_ENTERSIZEMOVE) {
|
||
is_in_native_drag_ = true;
|
||
// 如果还没有禁用 Mica(边框调整大小的情况),现在禁用
|
||
if (!is_in_size_move_loop_) {
|
||
EnterSizeMove(hwnd);
|
||
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
|
||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||
}
|
||
return DefWindowProc(hwnd, message, wparam, lparam);
|
||
}
|
||
|
||
// --- 原生拖拽/调整大小模态循环结束 ---
|
||
if (message == WM_EXITSIZEMOVE) {
|
||
is_in_native_drag_ = false;
|
||
// 如果之前禁用了 Mica(边框调整大小的情况),现在恢复
|
||
if (is_in_size_move_loop_) {
|
||
ExitSizeMove(hwnd);
|
||
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0,
|
||
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
|
||
SWP_NOZORDER | SWP_NOACTIVATE);
|
||
}
|
||
return DefWindowProc(hwnd, message, wparam, lparam);
|
||
}
|
||
|
||
// ============================================================
|
||
// 原生拖拽期间:完全绕过插件链
|
||
//
|
||
// window_manager 插件在 WM_MOVING 中调用 _EmitEvent("move"),
|
||
// 通过 MethodChannel 通知 Dart 侧。Dart 侧的 listener 可能触发
|
||
// setState → Flutter 重建 → 阻塞 UI 线程(= Win32 消息循环线程)。
|
||
// 22 个插件的 delegate 分发也会累积延迟。
|
||
//
|
||
// 拖拽期间所有消息直接交给 DefWindowProc,让 DWM live-drag 优化
|
||
// 不受任何干扰。同时 ChildWndProc 也会拦截 Flutter 子窗口的消息,
|
||
// 阻止 Flutter 引擎重绘。此时 Mica 已被 EnterSizeMove 禁用。
|
||
// ============================================================
|
||
if (is_in_native_drag_) {
|
||
return DefWindowProc(hwnd, message, wparam, lparam);
|
||
}
|
||
|
||
// --- 非拖拽期间:正常消息流转 ---
|
||
// Give Flutter, including plugins, an opportunity to handle window messages.
|
||
if (flutter_controller_) {
|
||
std::optional<LRESULT> result =
|
||
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
||
lparam);
|
||
if (result) {
|
||
return *result;
|
||
}
|
||
}
|
||
|
||
switch (message) {
|
||
case WM_FONTCHANGE:
|
||
flutter_controller_->engine()->ReloadSystemFonts();
|
||
break;
|
||
}
|
||
|
||
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
||
}
|