feat: add a new awesome feature

This commit is contained in:
2025-11-08 01:06:37 +08:00
parent cc08187ced
commit 5420bfd644
16 changed files with 918 additions and 421 deletions
+611 -214
View File
@@ -1,159 +1,414 @@
#include "Window.h"
#include "Dialog.h"
#include <windows.h> // 确保包含 Windows API 头文件
Window::Window(int width, int height, int mode)
#include <graphics.h>
#include <algorithm>
/**
* ApplyResizableStyle
* 作用:统一设置可拉伸/裁剪样式,并按开关使用 WS_EX_COMPOSITED(合成双缓冲)。
* 关键点:
* - WS_THICKFRAME:允许从四边/四角拖动改变尺寸。
* - WS_CLIPCHILDREN / WS_CLIPSIBLINGS:避免子控件互相覆盖时闪烁。
* - WS_EX_COMPOSITED:在一些环境更平滑,但个别显卡/驱动可能带来一帧延迟感。
* - SWP_FRAMECHANGED:通知窗口样式已变更,强制系统重算非客户区(标题栏/边框)。
*/
static void ApplyResizableStyle(HWND h, bool useComposited)
{
this->pendingW = this->width = width;
this->pendingH = this->height = height;
this->windowMode = mode;
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);
}
Window::Window(int width, int height, int mode, COLORREF bkcloc)
/**
* ApplyMinSizeOnSizing
* 作用:在 WM_SIZING 阶段执行“最小尺寸夹紧”。
* 规则:只回推“被拖动的那一侧”,另一侧当锚点(避免几何回弹/位置漂移)。
* 步骤:
* 1)将“最小客户区尺寸”通过 AdjustWindowRectEx 换算为“最小窗口矩形”(含非客户区)。
* 2)若当前矩形比最小还小,则根据 edge(哪条边/角在被拖)调整对应边,另一侧保持不动。
* 说明:仅保证不小于最小值;不做对齐/回滚等操作,把其余交给系统尺寸逻辑。
*/
static void ApplyMinSizeOnSizing(RECT* prc, WPARAM edge, HWND hWnd, int minClientW, int minClientH)
{
this->pendingW = this->width = width;
this->pendingH = this->height = height;
this->windowMode = mode;
this->wBkcolor = bkcloc;
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;
}
}
}
Window::Window(int width, int height, int mode, COLORREF bkcloc, std::string headline)
// ---------------- 构造 / 析构 ----------------
/**
* 构造:初始化当前尺寸、待应用尺寸、最小客户区尺寸与 EasyX 模式。
* 注意:样式设置与子类化放在 draw() 内第一次绘制时完成。
*/
Window::Window(int w, int h, int mode)
{
this->pendingW = this->width = width;
this->pendingH = this->height = height;
this->windowMode = mode;
this->wBkcolor = bkcloc;
this->headline = headline;
minClientW = pendingW = width = w;
minClientH = pendingH = height = h;
windowMode = mode;
}
Window::Window(int w, int h, int mode, COLORREF bk)
{
minClientW = pendingW = width = w;
minClientH = pendingH = height = h;
windowMode = mode;
wBkcolor = bk;
}
Window::Window(int w, int h, int mode, COLORREF bk, std::string title)
{
minClientW = pendingW = width = w;
minClientH = pendingH = height = h;
windowMode = mode;
wBkcolor = bk;
headline = std::move(title);
}
Window::~Window()
{
if (background)
delete background;
// 析构:释放背景图对象并关闭 EasyX 图形环境
if (background) delete background;
background = nullptr;
closegraph(); // 确保关闭图形上下文
closegraph();
}
// ---------------- 原生消息钩子----------------
void Window::draw() {
// 使用 EasyX 创建基本窗口
if (!hWnd)
hWnd = initgraph(width, height, windowMode);
// **启用窗口拉伸支持**:添加厚边框和最大化按钮样式
LONG style = GetWindowLong(hWnd, GWL_STYLE);
style |= WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX; // 可调整边框,启用最大化/最小化按钮
SetWindowLong(hWnd, GWL_STYLE, style);
// 通知窗口样式变化生效
SetWindowPos(hWnd, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
/**
* WndProcThunk
* 作用:替换 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<Window*>(GetWindowLongPtr(h, GWLP_USERDATA));
if (!self)
{
return DefWindowProc(h, m, w, l);
}
// 关键点①:禁止系统擦背景,避免和我们自己的清屏/双缓冲打架造成闪烁
if (m == WM_ERASEBKGND)
{
return 1;
}
// 关键点②:拉伸开始 → 冻结重绘(系统调整窗口矩形时不触发即时重绘,防止抖)
if (m == WM_ENTERSIZEMOVE)
{
self->isSizing = true;
SendMessage(h, WM_SETREDRAW, FALSE, 0);
return 0;
}
// 关键点③:拉伸中 → 仅执行“最小尺寸夹紧”,不做对齐/节流/回滚,保持系统自然流畅
if (m == WM_SIZING)
{
RECT* prc = reinterpret_cast<RECT*>(l);
// “尺寸异常值”快速过滤:仅保护极端值,不影响正常拖动
int currentWidth = prc->right - prc->left;
int currentHeight = prc->bottom - prc->top;
if (currentWidth < 0 || currentHeight < 0 || currentWidth > 10000 || currentHeight > 10000)
{
return TRUE;
}
ApplyMinSizeOnSizing(prc, w, h, self->minClientW, self->minClientH);
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;
}
// 关键:立刻做统一收口,不用等下一条消息
self->pumpResizeIfNeeded();
// 不擦背景、不触发立即 WM_PAINT
// InvalidateRect(h, nullptr, TRUE);
// UpdateWindow(h);
InvalidateRect(h, nullptr, FALSE);
// 解冻 + 触发一次刷新(InvalidateRect TRUE 会擦背景;如需无擦背景可传 FALSE)
SendMessage(h, WM_SETREDRAW, TRUE, 0);
InvalidateRect(h, nullptr, TRUE);
UpdateWindow(h);
return 0;
}
// 关键点⑤:系统级最小轨迹限制(与 WM_SIZING 的夹紧互相配合)
if (m == WM_GETMINMAXINFO)
{
auto* mmi = reinterpret_cast<MINMAXINFO*>(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 窗口与子类化过程;应用可拉伸样式;清屏并批量绘制。
* 关键步骤:
* 1initgraph 拿到 hWnd
* 2SetWindowLongPtr 子类化到 WndProcThunk(只做一次);
* 3ApplyResizableStyle 设置 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);
}
ApplyResizableStyle(hWnd, useComposited);
// 关闭类样式的整窗重绘标志(减少尺寸变化时的整窗 redraw)
LONG_PTR cls = GetClassLongPtr(hWnd, GCL_STYLE);
cls &= ~(CS_HREDRAW | CS_VREDRAW);
SetClassLongPtr(hWnd, GCL_STYLE, cls);
// 设置背景色并清屏
setbkcolor(wBkcolor);
cleardevice();
// 初次绘制所有控件(双缓冲)
BeginBatchDraw();
for (auto& control : controls)
{
control->draw();
for (auto& c : controls)
{
c->draw();
}
for (auto& d : dialogs)
{
d->draw();
}
// (如果有初始对话框,也可绘制 dialogs)
EndBatchDraw();
}
/**
* draw(imagePath)
* 作用:在 draw() 的基础上加载并绘制背景图;其它流程完全一致。
* 注意:这里按当前窗口客户区大小加载背景图(loadimage 的 w/h),保证铺满。
*/
void Window::draw(std::string imagePath)
{
// 使用指定图片绘制窗口背景(铺满窗口)
this->background = new IMAGE(width, height);
bkImageFile = imagePath;
if (!hWnd)
hWnd = initgraph(width, height, windowMode);
SetWindowText(hWnd, headline.c_str());
loadimage(background, imagePath.c_str(), width, height, true);
if(background)
putimage(0, 0, background);
else
{
// 设置背景色并清屏
setbkcolor(wBkcolor);
cleardevice();
}
// 同样应用可拉伸样式
LONG style = GetWindowLong(hWnd, GWL_STYLE);
style |= WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX;
SetWindowLong(hWnd, GWL_STYLE, style);
SetWindowPos(hWnd, NULL, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
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)
{
delete background;
background = nullptr;
}
background = new IMAGE;
loadimage(background, bkImageFile.c_str(), width, height, true);
putimage(0, 0, background);
// 绘制控件(含对话框)到窗口
BeginBatchDraw();
for (auto& control : controls)
{
control->setDirty(true);
control->draw();
}
for (auto& dlg : dialogs) dlg->draw();
for (auto& c : controls)
{
c->setDirty(true);
c->draw();
}
for (auto& d : dialogs)
{
d->draw();
}
EndBatchDraw();
}
// 运行主事件循环,处理用户输入和窗口消息
// 此方法会阻塞直到窗口关闭
// 主消息循环优先级:对话框 > 普通控件。
// 重绘策略:为保证视觉一致性,每次有对话框状态变化(打开/关闭)时,
// 会强制重绘所有控件。先绘制普通控件,再绘制对话框(确保对话框在最上层)
// ---------------- 事件循环 ----------------
/**
* runEventLoop()
* 作用:驱动输入/窗口消息;集中处理“统一收口重绘”
* 关键策略:
* - WM_SIZE:始终更新 pendingW/H(即使在拉伸中也只记录不立即绘制);
* - needResizeDirty:当尺寸确实变化时置位,随后在循环尾进行一次性重绘;
* - 非模态对话框优先消费事件(顶层从后往前);再交给普通控件。
*/
int Window::runEventLoop()
{
ExMessage msg;
bool running = true;
ExMessage msg;
bool running = true;
while (running)
{
bool consume = false;// 是否处理了消息
// 说明:统一使用 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;
}
if (msg.message == WM_SIZE)
{
if (msg.wParam != SIZE_MINIMIZED)
{
const int nw = LOWORD(msg.lParam);
const int nh = HIWORD(msg.lParam);
if (peekmessage(&msg, EX_MOUSE | EX_KEY | EX_WINDOW, true))
{
if (msg.message == WM_CLOSE)
{
running = false;
return 0;
}
// 仅在尺寸真的变化时标脏
if (nw > 0 && nh > 0 || (nw != width || nh != height))
{
pendingW = nw;
pendingH = nh;
needResizeDirty = true;
}
}
continue;//在末尾重绘制窗口
}
// 优先处理对话框事件
for (auto it = dialogs.rbegin(); it != dialogs.rend(); ++it)
{
auto& d = *it;
if (d->IsVisible() && !d->model())
consume = d->handleEvent(msg);
if (consume)
break;
}
// 保险:如果 EX_WINDOW 转译了 GETMINMAXINFO,同样按最小客户区折算处理
if (msg.message == WM_GETMINMAXINFO)
{
auto* mmi = reinterpret_cast<MINMAXINFO*>(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;
}
//普通控件
if (!consume)
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
consume = (*it)->handleEvent(msg);
if (consume)
break;
}
// 关键点⑥: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;
}
}
continue;
}
// 输入优先:先给顶层“非模态对话框”,再传给普通控件
for (auto it = dialogs.rbegin(); it != dialogs.rend(); ++it)
{
auto& d = *it;
if (d->IsVisible() && !d->model())
{
consume = d->handleEvent(msg);
}
if (consume) break;
}
if (!consume)
{
for (auto& c : controls)
{
consume = c->handleEvent(msg);
if (consume) break;
}
}
}
//如果有对话框打开或者关闭强制重绘
bool needredraw = false;
for (auto& d : dialogs)
{
@@ -193,163 +448,305 @@ int Window::runEventLoop()
needredraw = false;
}
//—— 统一收口”:尺寸变化后的** 一次性** 重绘 ——
if (needResizeDirty)
{
//确保窗口不会小于初始尺寸
if (pendingW >= width && pendingH >= height)
Resize(nullptr, pendingW, pendingH);
else
Resize(nullptr, width, height);
if (background)
{
delete background;
background = new IMAGE;
loadimage(background, bkImageFile.c_str(), pendingW, pendingH);
putimage(0, 0, background);
}
// 标记所有控件/对话框为脏,确保都补一次背景/外观
for (auto& c : controls)
{
c->onWindowResize();
c->draw();
}
for (auto& d : dialogs)
{
auto dd = dynamic_cast<Dialog*>(d.get());
dd->setDirty(true);
dd->setInitialization(true);
d->draw();
}
needResizeDirty = false;
}
// —— 统一收口needResizeDirty 为真时执行一次性重绘——
if (needResizeDirty)
{
// 以“实际客户区尺寸”为准,防止 pending 与真实尺寸出现偏差
RECT clientRect;
GetClientRect(hWnd, &clientRect);
int actualWidth = clientRect.right - clientRect.left;
int actualHeight = clientRect.bottom - clientRect.top;
// 降低占用
Sleep(10);
}
return 1;
const int finalW = (std::max)(minClientW, actualWidth);
const int finalH = (std::max)(minClientH, actualHeight);
// 变化过大/异常场景保护
if (finalW != width || finalH != height)
{
if (abs(finalW - width) > 1000 || abs(finalH - height) > 1000)
{
// 认为是异常帧,跳过本次(不改变任何状态)
needResizeDirty = false;
continue;
}
// 再次冻结窗口更新,保证批量绘制的原子性
SendMessage(hWnd, WM_SETREDRAW, FALSE, 0);
BeginBatchDraw();
// 调整底层画布尺寸
if (finalW != width || finalH != height)
{
Resize(nullptr, finalW, finalH);
// 重取一次实际客户区尺寸做确认
GetClientRect(hWnd, &clientRect);
int confirmedWidth = clientRect.right - clientRect.left;
int confirmedHeight = clientRect.bottom - clientRect.top;
int renderWidth = confirmedWidth;
int renderHeight = confirmedHeight;
// 背景:若设置了背景图则重载并铺满;否则清屏为纯色
if (background && !bkImageFile.empty())
{
delete background;
background = new IMAGE;
loadimage(background, bkImageFile.c_str(), renderWidth, renderHeight, true);
putimage(0, 0, background);
}
else
{
setbkcolor(wBkcolor);
cleardevice();
}
// 最终提交“当前已应用尺寸”(用于外部查询/下次比较)
width = renderWidth;
height = renderHeight;
}
// 批量通知控件“窗口尺寸变化”,并标记重绘
for (auto& c : controls)
{
c->onWindowResize();
c->setDirty(true);
}
for (auto& d : dialogs)
{
if (auto dd = dynamic_cast<Dialog*>(d.get()))
{
dd->setDirty(true);
dd->setInitialization(true);
}
}
// 统一批量绘制
for (auto& c : controls) c->draw();
for (auto& d : dialogs) d->draw();
EndBatchDraw();
// 解冻并触发一次无效化(这里 TRUE 表示会擦背景;如要避免闪白可改 FALSE)
SendMessage(hWnd, WM_SETREDRAW, TRUE, 0);
InvalidateRect(hWnd, nullptr, TRUE);
}
needResizeDirty = false; // 收口完成,清标志
}
// 轻微睡眠,削峰填谷(不阻塞拖拽体验)
Sleep(10);
}
return 1;
}
// ---------------- 其余接口 ----------------
void Window::setBkImage(std::string pImgFile)
{
if(nullptr == background)
this->background = new IMAGE;
else
delete background;
this->background = new IMAGE;
this->bkImageFile = pImgFile;
loadimage(background, pImgFile.c_str(), width, height, true);
putimage(0, 0, background);
BeginBatchDraw();
for (auto& c : controls)
{
c->setDirty(true);
c->draw();
}
for (auto& c : dialogs)
{
c->setDirty(true);
c->draw();
}
EndBatchDraw();
// 更换背景图:立即加载并绘制一次;同时将所有控件标 dirty 并重绘
if (background) delete background;
background = new IMAGE;
bkImageFile = std::move(pImgFile);
loadimage(background, bkImageFile.c_str(), width, height, true);
putimage(0, 0, background);
BeginBatchDraw();
for (auto& c : controls)
{
c->setDirty(true);
c->draw();
}
for (auto& d : dialogs)
{
d->setDirty(true);
d->draw();
}
EndBatchDraw();
}
void Window::setBkcolor(COLORREF c)
{
wBkcolor = c;
setbkcolor(wBkcolor);
cleardevice();
// 初次绘制所有控件(双缓冲)
BeginBatchDraw();
for (auto& c : controls)
{
c->setDirty(true);
c->draw();
}
for (auto& c : dialogs)
{
c->setDirty(true);
c->draw();
}
EndBatchDraw();
// 更换纯色背景:立即清屏并批量重绘控件/对话框
wBkcolor = c;
setbkcolor(wBkcolor);
cleardevice();
BeginBatchDraw();
for (auto& c : controls)
{
c->setDirty(true);
c->draw();
}
for (auto& d : dialogs)
{
d->setDirty(true);
d->draw();
}
EndBatchDraw();
}
void Window::setHeadline(std::string headline)
void Window::setHeadline(std::string title)
{
this->headline = headline;
SetWindowText(this->hWnd, headline.c_str());
// 设置窗口标题(仅改文本,不触发重绘)
headline = std::move(title);
if (hWnd)
{
SetWindowText(hWnd, headline.c_str());
}
}
void Window::addControl(std::unique_ptr<Control> control)
{
this->controls.push_back(std::move(control));
// 新增控件:仅加入管理容器,具体绘制在 draw()/收口时统一进行
controls.push_back(std::move(control));
}
void Window::addDialog(std::unique_ptr<Control> dialogs)
void Window::addDialog(std::unique_ptr<Control> dlg)
{
this->dialogs.push_back(std::move(dialogs));
// 新增非模态对话框:管理顺序决定事件优先级(顶层从后往前)
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;
// 只检查 Dialog 类型的控件
Dialog* d = dynamic_cast<Dialog*>(dptr.get());
//检查是否有非模态对话框可见,并且消息内容一致
if (d && d->IsVisible() && !d->model() && d->GetCaption() == caption && d->GetText() == message)
return true;
}
return false;
// 查询是否存在“可见且非模态”的对话框(用于避免重复弹)
for (const auto& dptr : dialogs)
{
if (!dptr) continue;
if (auto* d = dynamic_cast<Dialog*>(dptr.get()))
{
if (d->IsVisible() && !d->model() && d->GetCaption() == caption && d->GetText() == message)
{
return true;
}
}
}
return false;
}
HWND Window::getHwnd() const
{
return hWnd;
return hWnd;
}
int Window::getWidth() const
{
return this->pendingW;
// 注意:这里返回 pendingW
// 表示“最近一次收到的尺寸”(可能尚未应用到画布,最终以收口时的 width 为准)
return pendingW;
}
int Window::getHeight() const
{
return this->pendingH;
// 同上,返回 pendingH(与 getWidth 对应)
return pendingH;
}
std::string Window::getHeadline() const
{
return this->headline;
return headline;
}
COLORREF Window::getBkcolor() const
{
return this->wBkcolor;
return wBkcolor;
}
IMAGE* Window::getBkImage() const
{
return this->background;
return background;
}
std::string Window::getBkImageFile() const
{
return this->bkImageFile;
return bkImageFile;
}
std::vector<std::unique_ptr<Control>>& Window::getControls()
{
return this->controls;
return controls;
}
void Window::processWindowMessage(const ExMessage& msg)
{
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;
needResizeDirty = true; // 统一由 pumpResizeIfNeeded 来收口
}
}
}
}
void Window::pumpResizeIfNeeded()
{
if (!needResizeDirty) return;
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);
if (background && !bkImageFile.empty())
{
delete background; background = new IMAGE;
loadimage(background, bkImageFile.c_str(), rc.right - rc.left, rc.bottom - rc.top, true);
putimage(0, 0, background);
}
else { setbkcolor(wBkcolor); cleardevice(); }
width = rc.right - rc.left; height = rc.bottom - rc.top;
// 通知控件/对话框
for (auto& c : controls)
c->onWindowResize();
for (auto& d : dialogs)
if (auto* dd = dynamic_cast<Dialog*>(d.get()))
dd->setInitialization(true); // 强制对话框在新尺寸下重建布局/快照
// 重绘
for (auto& c : controls) c->draw();
for (auto& d : dialogs) d->draw();
EndBatchDraw();
SendMessage(hWnd, WM_SETREDRAW, TRUE, 0);
// 原来是 TRUE:会擦背景再触发 WM_PAINT,容易把刚画好的层“盖掉”
// InvalidateRect(hWnd, nullptr, TRUE);
InvalidateRect(hWnd, nullptr, FALSE);
needResizeDirty = false;
}
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 做统一收口+重绘
}
}