#include "Window.h" #include "Dialog.h" #include"SxLog.h" #include #include // 可能频繁出现且对调试信息干扰较大的消息(例如鼠标移动), // 可以在日志输出时特殊处理以减少干扰。 static bool SxIsNoisyMsg(UINT m) { return m == WM_MOUSEMOVE; } static const char* SxMsgName(UINT m) { switch (m) { case WM_MOUSEMOVE: return "WM_MOUSEMOVE"; case WM_LBUTTONDOWN: return "WM_LBUTTONDOWN"; case WM_LBUTTONUP: return "WM_LBUTTONUP"; case WM_RBUTTONDOWN: return "WM_RBUTTONDOWN"; case WM_RBUTTONUP: return "WM_RBUTTONUP"; case WM_KEYDOWN: return "WM_KEYDOWN"; case WM_KEYUP: return "WM_KEYUP"; case WM_CHAR: return "WM_CHAR"; case WM_SIZE: return "WM_SIZE"; default: return "WM_?"; } } static bool SxRectsIntersect(const RECT& a, const RECT& b) { return a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top; } static void collectManagedControlOverlays(const std::vector>& controls, Control* repaintRoot, const RECT& coverage, std::vector& overlays); bool Window::isManagedDispatchActive() const { return managedDispatchActive; } /** * requestManagedRepaint(source) * 作用:在“事件分发期”登记一笔托管重绘请求,而不是立即绘制。 * 关键点: * - source 是真正发生视觉变化的控件; * - root 是后续真正安全重绘的最小层级(通常是顶层控件/容器,或 Dialog 自身); * - coverage 记录这次变化影响的范围,用于判断哪些上层 Dialog 需要补画。 */ void Window::requestManagedRepaint(Control* source) { if (!source) return; managedSceneDirty = true; Control* root = source->getManagedRepaintRoot(); if (!root) return; RECT coverage = root->getManagedRepaintCoverageRect(); if (root->canCommitManagedPartialRepaint()) { // 对支持局部提交的 root,coverage 不能再盯着最深处的 source; // 否则像“三层 Canvas 里的按钮变色”这种情况,只会登记成一个很小的叶子矩形, // 顶层 root 提交时既容易漏掉那条直接脏分支,也会低估后续 overlay 补画范围。 Control* branch = source->getManagedRepaintDirectBranch(root); coverage = branch ? branch->getManagedRepaintCoverageRect() : source->getManagedRepaintCoverageRect(); const RECT sourceCoverage = source->getManagedRepaintCoverageRect(); coverage.left = (std::min)(coverage.left, sourceCoverage.left); coverage.top = (std::min)(coverage.top, sourceCoverage.top); coverage.right = (std::max)(coverage.right, sourceCoverage.right); coverage.bottom = (std::max)(coverage.bottom, sourceCoverage.bottom); } for (auto& item : managedRepaintItems) { if (item.root == root) { item.coverage.left = (std::min)(item.coverage.left, coverage.left); item.coverage.top = (std::min)(item.coverage.top, coverage.top); item.coverage.right = (std::max)(item.coverage.right, coverage.right); item.coverage.bottom = (std::max)(item.coverage.bottom, coverage.bottom); return; } } ManagedRepaintItem item; item.root = root; item.coverage = coverage; managedRepaintItems.push_back(item); } // 清空本轮托管重绘状态;通常在 flush/全场景重绘/resize 收口后调用 void Window::clearManagedRepaintState() { managedSceneDirty = false; managedRepaintItems.clear(); } void Window::drawWindowBackground() { if (!bkImageFile.empty()) { if (!background || background->getwidth() != width || background->getheight() != height) { background = std::make_unique(); loadimage(background.get(), bkImageFile.c_str(), width, height, true); } putimage(0, 0, background.get()); } else { setbkcolor(wBkcolor); cleardevice(); } } void Window::redrawScene(bool forceControlsDirty, bool forceDialogsDirty) { drawWindowBackground(); for (auto& c : controls) { if (forceControlsDirty) c->setDirty(true); c->draw(); } for (auto& d : dialogs) { if (forceDialogsDirty && d->IsVisible()) d->setDirty(true); d->draw(); } } /** * flushManagedRepaint() * 作用:提交当前事件分发阶段累计的托管重绘请求。 * 提交顺序: * 1)先根据 coverage 找出需要补画的非模态 Dialog; * 2)再按 Window::controls 的层级顺序提交受影响的普通 root; * 3)最后把相交的 Dialog 补画回最上层。 * 说明: * - 这里不做整场景重画,而是只画本轮登记的 root; * - 之所以按 controls 顺序提交,而不是按登记顺序,是为了保持顶层控件原有的 z-order。 */ void Window::flushManagedRepaint() { if (!managedSceneDirty || !hWnd) return; BeginBatchDraw(); auto unionCoverage = [](RECT& lhs, const RECT& rhs) { lhs.left = (std::min)(lhs.left, rhs.left); lhs.top = (std::min)(lhs.top, rhs.top); lhs.right = (std::max)(lhs.right, rhs.right); lhs.bottom = (std::max)(lhs.bottom, rhs.bottom); }; auto redrawOverlayUnit = [](Control* unit) { if (!unit || !unit->IsVisible()) return; // overlay 补画不是“沿用旧快照再贴回去”,而是重新站到当前顶层场景上合成一遍。 unit->invalidateBackgroundSnapshot(); unit->setDirty(true); unit->draw(); }; auto processManagedRoot = [&](Control* root, const RECT& initialCoverage) { if (!root || !root->IsVisible()) return; root->commitManagedRepaint(); RECT workingCoverage = initialCoverage; size_t controlStartIdx = controls.size(); for (size_t i = 0; i < controls.size(); ++i) { if (controls[i].get() == root) { controlStartIdx = i + 1; break; } } // 顶层普通控件的 overlay 补画必须是“传递式”的: // 如果 A 重画后把上层按钮 B 也重画出来,而 B 自己又伸进更上层的 C, // 那么 C 也必须继续补画回来,而不能只看最初 root 的 coverage 一跳收集。 if (controlStartIdx < controls.size()) { for (size_t i = controlStartIdx; i < controls.size(); ++i) { Control* current = controls[i].get(); if (!current || !current->IsVisible()) continue; const RECT currentCoverage = current->getManagedRepaintCoverageRect(); if (!SxRectsIntersect(currentCoverage, workingCoverage)) continue; redrawOverlayUnit(current); unionCoverage(workingCoverage, current->getManagedRepaintCoverageRect()); } } size_t dialogStartIdx = 0; for (size_t i = 0; i < dialogs.size(); ++i) { if (dialogs[i].get() == root) { dialogStartIdx = i + 1; break; } } // Dialog 永远位于普通顶层控件之上,也要使用扩张后的 coverage 做传递式补画。 for (size_t i = dialogStartIdx; i < dialogs.size(); ++i) { Control* dialog = dialogs[i].get(); if (!dialog || !dialog->IsVisible()) continue; const RECT dialogCoverage = dialog->getManagedRepaintCoverageRect(); if (!SxRectsIntersect(dialogCoverage, workingCoverage)) continue; redrawOverlayUnit(dialog); unionCoverage(workingCoverage, dialog->getManagedRepaintCoverageRect()); } }; for (auto& control : controls) { for (auto& item : managedRepaintItems) { if (item.root == control.get()) { processManagedRoot(item.root, item.coverage); break; } } } for (auto& dialog : dialogs) { for (auto& item : managedRepaintItems) { if (item.root == dialog.get()) { processManagedRoot(item.root, item.coverage); break; } } } EndBatchDraw(); clearManagedRepaintState(); } // 合成一条 WM_MOUSEMOVE 并直接分发给 Window 顶层控件;常用于同步 hover 状态 void Window::dispatchSyntheticMouseMoveToControls(short x, short y) { ExMessage mm{}; mm.message = WM_MOUSEMOVE; mm.x = x; mm.y = y; for (auto it = controls.rbegin(); it != controls.rend(); ++it) (*it)->handleEvent(mm); } /** * collectManagedDialogOverlays(repaintRoot, coverage, overlays) * 作用:找出在本轮提交后需要重新盖到最上层的非模态 Dialog。 * 规则: * - 如果 repaintRoot 本身就是 Dialog,则从它自己开始往上层 Dialog 收集; * - 如果 repaintRoot 是普通控件,则收集所有与 coverage 相交的可见 Dialog。 */ void Window::collectManagedDialogOverlays(Control* repaintRoot, const RECT& coverage, std::vector& overlays) { size_t startIdx = 0; if (auto* dialogRoot = dynamic_cast(repaintRoot)) { for (size_t i = 0; i < dialogs.size(); ++i) { if (dialogs[i].get() == dialogRoot) { startIdx = i; break; } } } for (size_t i = startIdx; i < dialogs.size(); ++i) { Control* dialog = dialogs[i].get(); if (!dialog || !dialog->IsVisible()) continue; if (dialog == repaintRoot || SxRectsIntersect(dialog->getBoundsRect(), coverage)) { if (std::find(overlays.begin(), overlays.end(), dialog) == overlays.end()) overlays.push_back(dialog); } } } /** * collectManagedControlOverlays(repaintRoot, coverage, overlays) * 作用:找出在本轮 root 提交后,需要重新补画回上层的普通顶层控件。 * 规则: * - 只处理 Window::controls 这一层的直接兄弟; * - 从 repaintRoot 在 controls 中的位置之后开始收集; * - 仅收集可见且与 coverage 相交的控件。 */ static void collectManagedControlOverlays(const std::vector>& controls, Control* repaintRoot, const RECT& coverage, std::vector& overlays) { size_t startIdx = controls.size(); for (size_t i = 0; i < controls.size(); ++i) { if (controls[i].get() == repaintRoot) { startIdx = i + 1; break; } } if (startIdx > controls.size()) return; for (size_t i = startIdx; i < controls.size(); ++i) { Control* control = controls[i].get(); if (!control || !control->IsVisible()) continue; if (SxRectsIntersect(control->getBoundsRect(), coverage)) { if (std::find(overlays.begin(), overlays.end(), control) == overlays.end()) overlays.push_back(control); } } } /** * ApplyResizableStyle * 作用:统一设置可拉伸/裁剪样式,并按开关使用 WS_EX_COMPOSITED(合成双缓冲)。 * 关键点: * - WS_THICKFRAME:允许从四边/四角拖动改变尺寸。 * - WS_CLIPCHILDREN / WS_CLIPSIBLINGS:避免子控件互相覆盖时闪烁。 * - WS_EX_COMPOSITED:在一些环境更平滑,但个别显卡/驱动可能带来一帧延迟感。 * - SWP_FRAMECHANGED:通知窗口样式已变更,强制系统重算非客户区(标题栏/边框)。 */ static void ApplyResizableStyle(HWND h, bool useComposited) { LONG style = GetWindowLong(h, GWL_STYLE); style |= WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_CLIPCHILDREN | WS_CLIPSIBLINGS; SetWindowLong(h, GWL_STYLE, style); LONG ex = GetWindowLong(h, GWL_EXSTYLE); if (useComposited) { ex |= WS_EX_COMPOSITED; } else { ex &= ~WS_EX_COMPOSITED; } SetWindowLong(h, GWL_EXSTYLE, ex); SetWindowPos(h, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED); } /** * ApplyMinSizeOnSizing * 作用:在 WM_SIZING 阶段执行“最小尺寸夹紧”。 * 规则:只回推“被拖动的那一侧”,另一侧当锚点(避免几何回弹/位置漂移)。 * 步骤: * 1)将“最小客户区尺寸”通过 AdjustWindowRectEx 换算为“最小窗口矩形”(含非客户区)。 * 2)若当前矩形比最小还小,则根据 edge(哪条边/角在被拖)调整对应边,另一侧保持不动。 * 说明:仅保证不小于最小值;不做对齐/回滚等操作,把其余交给系统尺寸逻辑。 */ static void ApplyMinSizeOnSizing(RECT* prc, WPARAM edge, HWND hWnd, int minClientW, int minClientH) { RECT rcFrame{ 0, 0, minClientW, minClientH }; DWORD style = GetWindowLong(hWnd, GWL_STYLE); DWORD ex = GetWindowLong(hWnd, GWL_EXSTYLE); AdjustWindowRectEx(&rcFrame, style, FALSE, ex); const int minW = rcFrame.right - rcFrame.left; const int minH = rcFrame.bottom - rcFrame.top; const int curW = prc->right - prc->left; const int curH = prc->bottom - prc->top; if (curW < minW) { switch (edge) { case WMSZ_LEFT: case WMSZ_TOPLEFT: case WMSZ_BOTTOMLEFT: prc->left = prc->right - minW; // 锚定右侧,回推左侧(左边被拖) break; default: prc->right = prc->left + minW; // 锚定左侧,回推右侧(右边被拖) break; } } if (curH < minH) { switch (edge) { case WMSZ_TOP: case WMSZ_TOPLEFT: case WMSZ_TOPRIGHT: prc->top = prc->bottom - minH; // 锚定下侧,回推上侧(上边被拖) break; default: prc->bottom = prc->top + minH; // 锚定上侧,回推下侧(下边被拖) break; } } } // ---------------- 构造 / 析构 ---------------- /** * 构造:初始化当前尺寸、待应用尺寸、最小客户区尺寸与 EasyX 模式。 * 注意:样式设置与子类化放在 draw() 内第一次绘制时完成。 */ Window::Window(int w, int h, int mode) { localwidth = minClientW = pendingW = width = w; localheight = minClientH = pendingH = height = h; windowMode = mode; } Window::Window(int w, int h, int mode, COLORREF bk) { localwidth = minClientW = pendingW = width = w; localheight = minClientH = pendingH = height = h; windowMode = mode; wBkcolor = bk; } Window::Window(int w, int h, int mode, COLORREF bk, std::string title) { localwidth = minClientW = pendingW = width = w; localheight = minClientH = pendingH = height = h; windowMode = mode; wBkcolor = bk; headline = std::move(title); } Window::~Window() { // 先销毁控件树,再关闭图形环境,避免控件析构时访问已关闭的 EasyX 上下文。 dialogs.clear(); controls.clear(); background.reset(); if (hWnd && procHooked && oldWndProc) { SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldWndProc); SetWindowLongPtr(hWnd, GWLP_USERDATA, 0); } if (hWnd) closegraph(); } // ---------------- 原生消息钩子---------------- /** * WndProcThun * 作用:替换 EasyX 的窗口过程,接管关键消息。 * 关键处理: * - WM_ERASEBKGND:返回 1,交由自绘清屏,避免系统擦背景造成闪烁。 * - WM_ENTERSIZEMOVE:开始拉伸 → isSizing=true 且 WM_SETREDRAW(FALSE) 冻结重绘。 * - WM_SIZING:拉伸中 → 仅做“最小尺寸夹紧”(按被拖边回推),不回滚、不绘制。 * - WM_EXITSIZEMOVE:结束拉伸 → 读取最终客户区尺寸 → 标记 needResizeDirty,解冻并刷新。 * - WM_GETMINMAXINFO:提供系统最小轨迹限制(四边一致)。 */ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l) { auto* self = reinterpret_cast(GetWindowLongPtr(h, GWLP_USERDATA)); if (!self) { return DefWindowProc(h, m, w, l); } // 关键点①:禁止系统擦背景,避免和我们自己的清屏/双缓冲打架造成闪烁 if (m == WM_ERASEBKGND) { return 1; } // 关键点②:拉伸开始 → 冻结重绘(系统调整窗口矩形时不触发即时重绘,防止抖) if (m == WM_ENTERSIZEMOVE) { SX_LOGI("Resize") << SX_T("WM_ENTERSIZEMOVE: 开始测量尺寸","WM_ENTERSIZEMOVE: begin sizing"); self->isSizing = true; SendMessage(h, WM_SETREDRAW, FALSE, 0); return 0; } // 关键点③:拉伸中 → 仅执行“最小尺寸夹紧”,不做对齐/节流/回滚,保持系统自然流畅 if (m == WM_SIZING) { RECT* prc = reinterpret_cast(l); // “尺寸异常值”快速过滤:仅保护极端值,不影响正常拖动 int currentWidth = prc->right - prc->left; int currentHeight = prc->bottom - prc->top; if (currentWidth < 0 || currentHeight < 0 || currentWidth > 10000 || currentHeight > 10000) { return TRUE; } RECT before = *prc;// 记录调整前矩形以便日志输出 ApplyMinSizeOnSizing(prc, w, h, self->minClientW, self->minClientH); //if (before.left != prc->left || before.top != prc->top || before.right != prc->right || before.bottom != prc->bottom) if (memcmp(&before, prc, sizeof(RECT)) != 0) { SX_LOGD("Resize") << SX_T("WM_SIZING 夹具:","WM_SIZING clamp: ") << SX_T("之前=(","before=(") << (before.right - before.left) << "x" << (before.bottom - before.top) << ") " << SX_T("之后=(","after=(") << (prc->right - prc->left) << "x" << (prc->bottom - prc->top) << ")"; } return TRUE; } // 关键点④:拉伸结束 → 解冻重绘 + 统一收口(记录最终尺寸 -> 标记 needResizeDirty) if (m == WM_EXITSIZEMOVE) { self->isSizing = false; RECT rc; GetClientRect(h, &rc); const int aw = rc.right - rc.left; const int ah = rc.bottom - rc.top; if (aw >= self->minClientW && ah >= self->minClientH && aw <= 10000 && ah <= 10000) { self->pendingW = aw; self->pendingH = ah; self->needResizeDirty = true; SX_LOGI("Resize") << SX_T("WM_EXITSIZEMOVE: 最终尺寸,待重绘=(","WM_EXITSIZEMOVE: end sizing, pending=(" )<< self->pendingW << "x" << self->pendingH << "), needResizeDirty=1"; } // 结束拉伸后不立即执行重绘,待事件循环统一收口。 // 立即解冻重绘标志,同时标记区域为有效,避免触发额外 WM_PAINT。 SendMessage(h, WM_SETREDRAW, TRUE, 0); ValidateRect(h, nullptr); return 0; } // 关键点⑤:系统级最小轨迹限制(与 WM_SIZING 的夹紧互相配合) if (m == WM_GETMINMAXINFO) { auto* mmi = reinterpret_cast(l); RECT rc{ 0, 0, self->minClientW, self->minClientH }; DWORD style = GetWindowLong(h, GWL_STYLE); DWORD ex = GetWindowLong(h, GWL_EXSTYLE); // 若后续添加菜单,请把第三个参数改为 HasMenu(h) AdjustWindowRectEx(&rc, style, FALSE, ex); mmi->ptMinTrackSize.x = rc.right - rc.left; mmi->ptMinTrackSize.y = rc.bottom - rc.top; return 0; } // 其它消息:回落到旧过程 return self->oldWndProc ? CallWindowProc(self->oldWndProc, h, m, w, l) : DefWindowProc(h, m, w, l); } // ---------------- 绘制 ---------------- /** * draw() * 作用:首次初始化 EasyX 窗口与子类化过程;应用可拉伸样式;清屏并批量绘制。 * 关键步骤: * 1)initgraph 拿到 hWnd; * 2)SetWindowLongPtr 子类化到 WndProcThunk(只做一次); * 3)ApplyResizableStyle 设置 WS_THICKFRAME/裁剪/(可选)合成双缓冲; * 4)去掉类样式 CS_HREDRAW/CS_VREDRAW,避免全窗无效化引发闪屏; * 5)清屏 + Begin/EndBatchDraw 批量绘制控件&对话框。 */ void Window::draw() { if (!hWnd) { hWnd = initgraph(width, height, windowMode); } // 子类化:让我们的 WndProcThunk 接管窗口消息(仅执行一次) if (!procHooked) { SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)this); oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)&Window::WndProcThunk); procHooked = (oldWndProc != nullptr); } if (!headline.empty()) { SetWindowText(hWnd, headline.c_str()); } ApplyResizableStyle(hWnd, useComposited); // 关闭类样式的整窗重绘标志(减少尺寸变化时的整窗 redraw) LONG_PTR cls = GetClassLongPtr(hWnd, GCL_STYLE); cls &= ~(CS_HREDRAW | CS_VREDRAW); SetClassLongPtr(hWnd, GCL_STYLE, cls); BeginBatchDraw(); redrawScene(true, true); EndBatchDraw(); clearManagedRepaintState(); } /** * draw(imagePath) * 作用:在 draw() 的基础上加载并绘制背景图;其它流程完全一致。 * 注意:这里按当前窗口客户区大小加载背景图(loadimage 的 w/h),保证铺满。 */ void Window::draw(std::string imagePath) { if (!hWnd) { hWnd = initgraph(width, height, windowMode); } if (!procHooked) { SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)this); oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)&Window::WndProcThunk); procHooked = (oldWndProc != nullptr); } bkImageFile = std::move(imagePath); if (!headline.empty()) { SetWindowText(hWnd, headline.c_str()); } ApplyResizableStyle(hWnd, useComposited); LONG_PTR cls = GetClassLongPtr(hWnd, GCL_STYLE); cls &= ~(CS_HREDRAW | CS_VREDRAW); SetClassLongPtr(hWnd, GCL_STYLE, cls); if (background) { background.reset(); } background = std::make_unique(); BeginBatchDraw(); redrawScene(true, true); EndBatchDraw(); clearManagedRepaintState(); } // ---------------- 事件循环 ---------------- /** * runEventLoop() * 作用:驱动输入/窗口消息;集中处理“统一收口重绘”。 * 关键策略: * - WM_SIZE:始终更新 pendingW/H(即使在拉伸中也只记录不立即绘制); * - needResizeDirty:当尺寸确实变化时置位,随后在循环尾进行一次性重绘; * - 非模态对话框优先消费事件(顶层从后往前);再交给普通控件; * - managedDispatchActive=true 期间,控件 requestRepaint 不会立即画,而是登记到 managedRepaintItems; * - 事件尾通过 flushManagedRepaint 提交本轮 root 重绘,再按需补画 Dialog。 */ int Window::runEventLoop() { ExMessage msg; bool running = true; // 说明:统一使用 needResizeDirty 作为“收口重绘”的唯一标志位 // 不再引入额外 pendingResize 等状态,避免分叉导致状态不一致。 while (running) { bool consume = false; // 事件是否被消费的标志(用于输入事件分发) if (peekmessage(&msg, EX_MOUSE | EX_KEY | EX_WINDOW, true)) { if (msg.message == WM_CLOSE) { running = false; return 0; } // 保险:如果 EX_WINDOW 转译了 GETMINMAXINFO,同样按最小客户区折算处理 if (msg.message == WM_GETMINMAXINFO) { auto* mmi = reinterpret_cast(msg.lParam); RECT rc{ 0, 0, minClientW, minClientH }; DWORD style = GetWindowLong(hWnd, GWL_STYLE); DWORD ex = GetWindowLong(hWnd, GWL_EXSTYLE); AdjustWindowRectEx(&rc, style, FALSE, ex); mmi->ptMinTrackSize.x = rc.right - rc.left; mmi->ptMinTrackSize.y = rc.bottom - rc.top; continue; } // 关键点⑥:WM_SIZE 只记录新尺寸;若非拉伸阶段则立即置位 needResizeDirty if (msg.message == WM_SIZE && msg.wParam != SIZE_MINIMIZED) { const int nw = LOWORD(msg.lParam); const int nh = HIWORD(msg.lParam); // 基本合法性校验(不小于最小值、不过大) if (nw >= minClientW && nh >= minClientH && nw <= 10000 && nh <= 10000) { if (nw != width || nh != height) { pendingW = nw; pendingH = nh; // 在“非拉伸阶段”的 WM_SIZE(例如最大化/还原/程序化调整)直接触发收口 needResizeDirty = true; SX_LOGD("Resize") <get(); if (!consume) { consume = current->handleEvent(msg); if (consume) firstConsumer = current; } else { current->clearTransientMouseState(); } } } else { for (auto it = controls.rbegin(); it != controls.rend(); ++it) { Control* current = it->get(); consume = current->handleEvent(msg); if (consume) { firstConsumer = current; break; } } } if (firstConsumer && !SxIsNoisyMsg(msg.message)) SX_LOGD("Event") << SX_T("事件被控件处理:", "Event consumed by control: ") << SxMsgName(msg.message) << SX_T(" id=", " id=") << firstConsumer->getId(); } managedDispatchActive = false; } // 对话框打开/关闭属于全局层级变化:这里仍然使用整场景重绘兜底, // 并在结束后清空本轮托管重绘登记,避免旧的 root 请求延后提交。 bool needredraw = false; if(dialogOpen) { for (auto& d : dialogs) { needredraw = d->IsVisible(); if (needredraw)break; } } if (needredraw || dialogClose) { if (dialogClose) { // 对话框关闭后,需要手动合成一个鼠标移动消息并分发给所有普通控件, // 以便它们能及时更新悬停状态(hover),否则悬停状态可能保持错误状态。 // 先把当前鼠标位置转换为客户区坐标,并合成一次 WM_MOUSEMOVE,先分发给控件更新 hover 状态 SX_LOGD("Event") << SX_T("对话框关闭,合成WM_MOUSEMOVE已下发", "Dialog closed; synthetic WM_MOUSEMOVE dispatched"); POINT pt; if (GetCursorPos(&pt)) { ScreenToClient(this->hWnd, &pt); // 只分发给 window 层控件(因为 dialog 已经关闭或即将关闭) managedDispatchActive = true; dispatchSyntheticMouseMoveToControls((short)pt.x, (short)pt.y); managedDispatchActive = false; } dialogClose = false; // 重置标志 } BeginBatchDraw(); SX_LOGD("Event") << SX_T("对话框打开/关闭,触发全量重绘", "The dialog box opens/closes, triggering a full redraw"); redrawScene(true, true); EndBatchDraw(); needredraw = false; dialogOpen = false; clearManagedRepaintState(); } // —— 统一收口(needResizeDirty 为真时执行一次性重绘)—— // resize 会改变布局和背景基线,因此仍然走整场景重绘,而不是局部 root 提交。 if (needResizeDirty) { SX_LOGI("Resize") << SX_T("调整窗口尺寸开始:width=","Resize settle start: width=") << width << " height=" << height; SX_TRACE_SCOPE(SX_T("调整尺寸","Resize"),SX_T("窗口:调整尺寸", "Window::resize_settle")); // 以“实际客户区尺寸”为准,防止 pending 与真实尺寸出现偏差 RECT clientRect; GetClientRect(hWnd, &clientRect); int actualWidth = clientRect.right - clientRect.left; int actualHeight = clientRect.bottom - clientRect.top; const int virtualScreenWidth = (std::max)(1, GetSystemMetrics(SM_CXVIRTUALSCREEN)); const int virtualScreenHeight = (std::max)(1, GetSystemMetrics(SM_CYVIRTUALSCREEN)); const int maxReasonableWidth = (std::max)(10000, virtualScreenWidth * 2); const int maxReasonableHeight = (std::max)(10000, virtualScreenHeight * 2); // 仅拦截“明显非法”的客户区尺寸,不再按“变化跨度”误杀正常最大化。 if (actualWidth <= 0 || actualHeight <= 0 || actualWidth > maxReasonableWidth || actualHeight > maxReasonableHeight) { SX_LOGD("Resize") << SX_T("尺寸调整被非法尺寸保护跳过:old=(", "Resize settle skipped by invalid-size guard: old=(") << width << "x" << height << SX_T(") pending=(", ") pending=(") << pendingW << "x" << pendingH << SX_T(") actual=(", ") actual=(") << actualWidth << "x" << actualHeight << SX_T(") virtual=(", ") virtual=(") << virtualScreenWidth << "x" << virtualScreenHeight << SX_T(") maxAllowed=(", ") maxAllowed=(") << maxReasonableWidth << "x" << maxReasonableHeight << SX_T(")", ")"); needResizeDirty = false; continue; } const int finalW = (std::max)(minClientW, actualWidth); const int finalH = (std::max)(minClientH, actualHeight); if (finalW != width || finalH != height) { const int diffW = abs(finalW - width); const int diffH = abs(finalH - height); if (diffW > 1000 || diffH > 1000) { SX_LOGD("Resize") << SX_T("检测到大跨度尺寸调整,继续执行收口:old=(", "Large-span resize detected; continue settle: old=(") << width << "x" << height << SX_T(") new=(", ") new=(") << finalW << "x" << finalH << SX_T(") diff=(", ") diff=(") << diffW << "x" << diffH << SX_T(") actual=(", ") actual=(") << actualWidth << "x" << actualHeight << SX_T(") virtual=(", ") virtual=(") << virtualScreenWidth << "x" << virtualScreenHeight << SX_T(")", ")"); } // 再次冻结窗口更新,保证批量绘制的原子性 SendMessage(hWnd, WM_SETREDRAW, FALSE, 0); BeginBatchDraw(); // 调整底层画布尺寸 if (finalW != width || finalH != height) { // 批量通知控件“窗口尺寸变化”,并标记重绘 for (auto& c : controls) adaptiveLayout(c, finalH, finalW); //重绘窗口 Resize(nullptr, finalW, finalH); // 重取一次实际客户区尺寸做确认 GetClientRect(hWnd, &clientRect); int confirmedWidth = clientRect.right - clientRect.left; int confirmedHeight = clientRect.bottom - clientRect.top; // 最终提交“当前已应用尺寸”(用于外部查询/下次比较) width = confirmedWidth; height = confirmedHeight; for (auto& d : dialogs) { if (auto dd = dynamic_cast(d.get())) { dd->recenterInHostWindow(); } } } // 统一批量绘制 redrawScene(true, true); EndBatchDraw(); // 解冻后标记区域有效,避免系统再次触发 WM_PAINT 覆盖自绘内容。 SendMessage(hWnd, WM_SETREDRAW, TRUE, 0); ValidateRect(hWnd, nullptr); } SX_LOGI("Resize") << SX_T("尺寸调整已完成:width=","Resize settle done: width=") << width << " height=" << height; needResizeDirty = false; // 收口完成,清标志 clearManagedRepaintState(); } // 普通输入事件收口:只在没有 resize / 对话框开关这种全局变化时,才提交本轮托管重绘。 if (!needResizeDirty && !dialogOpen && !dialogClose) flushManagedRepaint(); // 轻微睡眠,削峰填谷(不阻塞拖拽体验) Sleep(10); } return 1; } // ---------------- 其余接口 ---------------- void Window::setBkImage(std::string pImgFile) { // 更换背景图:立即加载并绘制一次;同时将所有控件标 dirty 并重绘 background = std::make_unique(); bkImageFile = std::move(pImgFile); BeginBatchDraw(); redrawScene(true, true); EndBatchDraw(); } void Window::setBkcolor(COLORREF c) { // 更换纯色背景:立即清屏并批量重绘控件/对话框 wBkcolor = c; background.reset(); bkImageFile.clear(); BeginBatchDraw(); redrawScene(true, true); EndBatchDraw(); } void Window::setHeadline(std::string title) { // 设置窗口标题(仅改文本,不触发重绘) headline = std::move(title); if (hWnd) SetWindowText(hWnd, headline.c_str()); } void Window::addControl(std::unique_ptr control) { // 新增控件:仅加入管理容器,具体绘制在 draw()/收口时统一进行 control->setHostWindow(this); controls.push_back(std::move(control)); } void Window::addDialog(std::unique_ptr dlg) { // 新增非模态对话框:管理顺序决定事件优先级(顶层从后往前) dlg->setHostWindow(this); dialogs.push_back(std::move(dlg)); } bool Window::hasNonModalDialogWithCaption(const std::string& caption, const std::string& message) const { // 查询是否存在“可见且非模态”的对话框(用于避免重复弹) for (const auto& dptr : dialogs) { if (!dptr) continue; if (auto* d = dynamic_cast(dptr.get())) if (d->IsVisible() && !d->model() && d->GetCaption() == caption && d->GetText() == message) { dialogOpen = true; return true; } } return false; } HWND Window::getHwnd() const { return hWnd; } int Window::getWidth() const { return width; } int Window::getHeight() const { return height; } int Window::getPendingWidth() const { return pendingW; } int Window::getPendingHeight() const { return pendingH; } std::string Window::getHeadline() const { return headline; } COLORREF Window::getBkcolor() const { return wBkcolor; } IMAGE* Window::getBkImage() const { return background.get(); } std::string Window::getBkImageFile() const { return bkImageFile; } std::vector>& Window::getControls() { return controls; } void Window::pumpResizeIfNeeded() { if (!needResizeDirty) return; SX_LOGD("Resize") << SX_T("执行 pumpResizeIfNeeded:needResizeDirty=", "pumpResizeIfNeeded: needResizeDirty=") << (needResizeDirty ? 1 : 0) << SX_T("(需要进行一次缩放收口/重排重绘)", ""); RECT rc; GetClientRect(hWnd, &rc); const int finalW = max(minClientW, rc.right - rc.left); const int finalH = max(minClientH, rc.bottom - rc.top); if (finalW == width && finalH == height) { needResizeDirty = false; return; } SendMessage(hWnd, WM_SETREDRAW, FALSE, 0); BeginBatchDraw(); // Resize + 背景 Resize(nullptr, finalW, finalH); GetClientRect(hWnd, &rc); width = rc.right - rc.left; height = rc.bottom - rc.top; // 通知控件/对话框 for (auto& c : controls) { adaptiveLayout(c, finalH, finalW); } for (auto& d : dialogs) if (auto* dd = dynamic_cast(d.get())) dd->recenterInHostWindow(); // 窗口变化时仅重新居中,不拉伸 Dialog 自身 // 重绘 redrawScene(true, true); EndBatchDraw(); SendMessage(hWnd, WM_SETREDRAW, TRUE, 0); // 原实现在此调用 InvalidateRect 导致系统再次发送 WM_PAINT,从而重复绘制, // 这里改为 ValidateRect:直接标记区域为有效,通知系统我们已完成绘制,不必再触发 WM_PAINT。 // 这样可以避免收口阶段的绘制与系统重绘叠加造成顺序错乱。 ValidateRect(hWnd, nullptr); needResizeDirty = false; clearManagedRepaintState(); } void Window::scheduleResizeFromModal(int w, int h) { if (w < minClientW) w = minClientW; if (h < minClientH) h = minClientH; if (w > 10000) w = 10000; if (h > 10000) h = 10000; if (w != width || h != height) { pendingW = w; pendingH = h; needResizeDirty = true; // 交给 pumpResizeIfNeeded 做统一收口+重绘 SX_LOGD("Resize") << SX_T("模态对话框触发缩放调度:pending=(", "scheduleResizeFromModal: pending=(") << pendingW << "x" << pendingH << SX_T("),needResizeDirty=1(标记需要缩放收口)", "), needResizeDirty=1"); } } void Window::adaptiveLayout(std::unique_ptr& c, const int finalH, const int finalW) { // 顶层窗口布局收口: // 先用统一解算器求出控件新的运行态矩形,再通知控件“外部环境已变化”。 // onWindowResize() 负责快照失效/标脏,不再承担布局公式。 const StellarX::ResolvedLayoutRect rect = c->resolveLayoutRect(this->localwidth, this->localheight, 0, 0, finalW, finalH); c->applyResolvedLayoutRect(rect); c->onWindowResize(); }