Snapshot before repaint phase 2

This commit is contained in:
Codex
2026-04-07 21:37:49 +08:00
parent b07a4ec370
commit 77a8fe568a
10 changed files with 395 additions and 131 deletions
+22 -7
View File
@@ -1,5 +1,6 @@
#include "Button.h" #include "Button.h"
#include "SxLog.h" #include "SxLog.h"
#include "Window.h"
Button::Button(int x, int y, int width, int height, const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape) Button::Button(int x, int y, int width, int height, const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape)
: Control(x, y, width, height) : Control(x, y, width, height)
@@ -269,6 +270,9 @@ void Button::draw()
restoreStyle();//恢复默认字体样式和颜色 restoreStyle();//恢复默认字体样式和颜色
dirty = false; //标记按钮不需要重绘 dirty = false; //标记按钮不需要重绘
if (tipEnabled && tipVisible)
tipLabel.draw();
} }
// 处理鼠标事件,检测点击和悬停状态 // 处理鼠标事件,检测点击和悬停状态
// 根据按钮模式和形状进行不同的处理 // 根据按钮模式和形状进行不同的处理
@@ -422,20 +426,28 @@ bool Button::handleEvent(const ExMessage& msg)
if (stateChanged) if (stateChanged)
dirty = true; dirty = true;
const bool tipVisibilityChanged = (tipVisible != oldTipVisible); const bool tipVisibilityChanged = (tipVisible != oldTipVisible);
if (tipVisibilityChanged)
dirty = true;
// 鼠标命中按钮区域,或按钮自身状态因此发生变化时,吞掉该事件。 // 事件吞噬规则:
// 这样可以避免被遮挡/重叠的下层控件继续收到同一事件并把自己重绘到上层。 // - 鼠标移动:只有“当前命中按钮”时才吞掉,避免前一个按钮在清 hover 时截断消息,
if (isMouseMessage && (hover || oldHover || click != oldClick)) // 导致后一个真正命中的按钮收不到 WM_MOUSEMOVE。
// - 鼠标按下/抬起:命中按钮区域时吞掉,避免点击穿透到底层控件。
if (msg.message == WM_MOUSEMOVE)
{
if (hover)
consume = true;
}
else if ((msg.message == WM_LBUTTONDOWN || msg.message == WM_LBUTTONUP) && hover)
{
consume = true; consume = true;
}
markEventVisualChanged(stateChanged || tipVisibilityChanged); markEventVisualChanged(stateChanged || tipVisibilityChanged);
// 如果需要重绘,立即执行 // 如果需要重绘,立即执行
if (dirty) if (dirty)
requestRepaint(parent); requestRepaint(parent);
if (tipEnabled && tipVisible)
tipLabel.draw();
return consume; return consume;
} }
@@ -671,7 +683,10 @@ void Button::hideTooltip()
if (tipVisible) if (tipVisible)
{ {
tipVisible = false; tipVisible = false;
tipLabel.hide(); // 还原快照+作废,防止残影 if (auto* host = getHostWindow(); host && host->isManagedDispatchActive())
tipLabel.invalidateBackgroundSnapshot();
else
tipLabel.hide(); // 还原快照+作废,防止残影
tipHoverTick = GetTickCount64(); // 重置计时基线 tipHoverTick = GetTickCount64(); // 重置计时基线
} }
} }
+8
View File
@@ -1,5 +1,6 @@
#include "Canvas.h" #include "Canvas.h"
#include "SxLog.h" #include "SxLog.h"
#include "Window.h"
static bool SxIsNoisyMsg(UINT m) static bool SxIsNoisyMsg(UINT m)
{ {
@@ -432,6 +433,13 @@ void Canvas::onWindowResize()
void Canvas::requestRepaint(Control* parent) void Canvas::requestRepaint(Control* parent)
{ {
if (shouldDeferManagedRepaint())
{
if (auto* host = getHostWindow())
host->requestManagedRepaint();
return;
}
if (this == parent) if (this == parent)
{ {
if (!show) if (!show)
+38
View File
@@ -1,6 +1,7 @@
#include "Control.h" #include "Control.h"
#include "SxLog.h" #include "SxLog.h"
#include<assert.h> #include<assert.h>
#include "Window.h"
StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text) StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text)
{ {
@@ -123,6 +124,13 @@ void Control::restoreStyle()
void Control::requestRepaint(Control* parent) void Control::requestRepaint(Control* parent)
{ {
if (shouldDeferManagedRepaint())
{
if (auto* host = getHostWindow())
host->requestManagedRepaint();
return;
}
// 说明: // 说明:
// - 常规路径:子控件调用 requestRepaint(this->parent),然后 parent 负责局部重绘(Canvas/TabControl override // - 常规路径:子控件调用 requestRepaint(this->parent),然后 parent 负责局部重绘(Canvas/TabControl override
// - 兜底路径:如果某个“容器控件”没 override requestRepaint,就会出现 parent==this 的递归风险 // - 兜底路径:如果某个“容器控件”没 override requestRepaint,就会出现 parent==this 的递归风险
@@ -147,6 +155,13 @@ void Control::requestRepaint(Control* parent)
void Control::onRequestRepaintAsRoot() void Control::onRequestRepaintAsRoot()
{ {
if (shouldDeferManagedRepaint())
{
if (auto* host = getHostWindow())
host->requestManagedRepaint();
return;
}
SX_LOG_TRACE("Dirty") SX_LOG_TRACE("Dirty")
<< SX_T("触发根重绘:id=", "onRequestRepaintAsRoot: id=") << id << SX_T("触发根重绘:id=", "onRequestRepaintAsRoot: id=") << id
<< SX_T("(从根节点开始重画)", " (root repaint)"); << SX_T("(从根节点开始重画)", " (root repaint)");
@@ -157,6 +172,29 @@ void Control::onRequestRepaintAsRoot()
draw(); // 只有“无父”时才允许立即画,不会被谁覆盖 draw(); // 只有“无父”时才允许立即画,不会被谁覆盖
} }
bool Control::shouldDeferManagedRepaint() const
{
Window* host = getHostWindow();
return host && host->isManagedDispatchActive();
}
Window* Control::getHostWindow() const
{
if (hostWindow)
return hostWindow;
return parent ? parent->getHostWindow() : nullptr;
}
RECT Control::getBoundsRect() const
{
RECT rc{};
rc.left = x;
rc.top = y;
rc.right = x + width;
rc.bottom = y + height;
return rc;
}
void Control::saveBackground(int x, int y, int w, int h) void Control::saveBackground(int x, int y, int w, int h)
{ {
+8
View File
@@ -32,6 +32,8 @@
#include <functional> #include <functional>
#include "CoreTypes.h" #include "CoreTypes.h"
class Window;
class Control class Control
{ {
protected: protected:
@@ -39,6 +41,7 @@ protected:
int localx, x, localy, y; // 左上角坐标 int localx, x, localy, y; // 左上角坐标
int localWidth, width, localHeight, height; // 控件尺寸 int localWidth, width, localHeight, height; // 控件尺寸
Control* parent = nullptr; // 父控件 Control* parent = nullptr; // 父控件
Window* hostWindow = nullptr; // 宿主窗口(顶层由 Window 注入,子控件可沿 parent 回溯)
bool dirty = true; // 是否重绘 bool dirty = true; // 是否重绘
bool show = true; // 是否显示 bool show = true; // 是否显示
bool eventVisualChanged = false; // 最近一次 handleEvent 是否真的引发了视觉变化 bool eventVisualChanged = false; // 最近一次 handleEvent 是否真的引发了视觉变化
@@ -82,6 +85,7 @@ protected:
virtual void requestRepaint(Control* parent); virtual void requestRepaint(Control* parent);
//根控件/无父时触发重绘 //根控件/无父时触发重绘
virtual void onRequestRepaintAsRoot(); virtual void onRequestRepaintAsRoot();
bool shouldDeferManagedRepaint() const;
protected: protected:
//保存背景快照 //保存背景快照
virtual void saveBackground(int x, int y, int w, int h); virtual void saveBackground(int x, int y, int w, int h);
@@ -121,6 +125,10 @@ public:
virtual void setIsVisible(bool show); virtual void setIsVisible(bool show);
//设置父容器指针 //设置父容器指针
void setParent(Control* parent) { this->parent = parent; } void setParent(Control* parent) { this->parent = parent; }
//设置宿主窗口(通常仅由顶层 Window/对话框注入)
virtual void setHostWindow(Window* host) { this->hostWindow = host; }
Window* getHostWindow() const;
RECT getBoundsRect() const;
//设置是否重绘 //设置是否重绘
virtual void setDirty(bool dirty) { this->dirty = dirty; } virtual void setDirty(bool dirty) { this->dirty = dirty; }
//检查控件是否可见 //检查控件是否可见
+8
View File
@@ -5,6 +5,7 @@ Dialog::Dialog(Window& h, std::string text, std::string message, StellarX::Messa
: Canvas(), message(message), type(type), modal(modal), hWnd(h), titleText(text) : Canvas(), message(message), type(type), modal(modal), hWnd(h), titleText(text)
{ {
this->id = "Dialog"; this->id = "Dialog";
setHostWindow(&hWnd);
show = false; show = false;
} }
@@ -757,6 +758,13 @@ std::unique_ptr<Button> Dialog::createDialogButton(int x, int y, const std::stri
void Dialog::requestRepaint(Control* parent) void Dialog::requestRepaint(Control* parent)
{ {
if (shouldDeferManagedRepaint())
{
if (auto* host = getHostWindow())
host->requestManagedRepaint();
return;
}
if (this == parent) if (this == parent)
{ {
for (auto& control : controls) for (auto& control : controls)
+8
View File
@@ -1,5 +1,6 @@
#include "TabControl.h" #include "TabControl.h"
#include "SxLog.h" #include "SxLog.h"
#include "Window.h"
inline void TabControl::initTabBar() inline void TabControl::initTabBar()
{ {
if (controls.empty())return; if (controls.empty())return;
@@ -415,6 +416,13 @@ void TabControl::setDirty(bool dirty)
void TabControl::requestRepaint(Control* parent) void TabControl::requestRepaint(Control* parent)
{ {
if (shouldDeferManagedRepaint())
{
if (auto* host = getHostWindow())
host->requestManagedRepaint();
return;
}
if (this == parent) if (this == parent)
{ {
for (auto& control : controls) for (auto& control : controls)
+5
View File
@@ -193,6 +193,9 @@ void Table::initButton()
nextButton->setY(btnY); nextButton->setY(btnY);
} }
prevButton->setParent(this);
nextButton->setParent(this);
prevButton->textStyle = this->textStyle; prevButton->textStyle = this->textStyle;
nextButton->textStyle = this->textStyle; nextButton->textStyle = this->textStyle;
prevButton->setFillMode(tableFillMode); prevButton->setFillMode(tableFillMode);
@@ -262,6 +265,8 @@ void Table::initPageNum()
pageNum->setY(pY); pageNum->setY(pY);
} }
pageNum->setParent(this);
pageNum->textStyle = this->textStyle; pageNum->textStyle = this->textStyle;
if (StellarX::FillMode::Null == tableFillMode) if (StellarX::FillMode::Null == tableFillMode)
pageNum->setTextdisap(true); // 透明文本 pageNum->setTextdisap(true); // 透明文本
+97 -122
View File
@@ -27,6 +27,73 @@ static const char* SxMsgName(UINT m)
} }
} }
bool Window::isManagedDispatchActive() const
{
return managedDispatchActive;
}
void Window::requestManagedRepaint()
{
managedSceneDirty = true;
}
void Window::drawWindowBackground()
{
if (!bkImageFile.empty())
{
if (!background || background->getwidth() != width || background->getheight() != height)
{
background = std::make_unique<IMAGE>();
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();
}
}
void Window::flushManagedRepaint()
{
if (!managedSceneDirty || !hWnd)
return;
BeginBatchDraw();
redrawScene(true, true);
EndBatchDraw();
managedSceneDirty = false;
}
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);
}
/** /**
* ApplyResizableStyle * ApplyResizableStyle
* 作用:统一设置可拉伸/裁剪样式,并按开关使用 WS_EX_COMPOSITED(合成双缓冲)。 * 作用:统一设置可拉伸/裁剪样式,并按开关使用 WS_EX_COMPOSITED(合成双缓冲)。
@@ -294,18 +361,8 @@ void Window::draw()
cls &= ~(CS_HREDRAW | CS_VREDRAW); cls &= ~(CS_HREDRAW | CS_VREDRAW);
SetClassLongPtr(hWnd, GCL_STYLE, cls); SetClassLongPtr(hWnd, GCL_STYLE, cls);
setbkcolor(wBkcolor);
cleardevice();
BeginBatchDraw(); BeginBatchDraw();
for (auto& c : controls) redrawScene(true, true);
{
c->draw();
}
for (auto& d : dialogs)
{
d->draw();
}
EndBatchDraw(); EndBatchDraw();
} }
@@ -345,19 +402,9 @@ void Window::draw(std::string imagePath)
background.reset(); background.reset();
} }
background = std::make_unique<IMAGE>(); background = std::make_unique<IMAGE>();
loadimage(background.get(), bkImageFile.c_str(), width, height, true);
putimage(0, 0, background.get());
BeginBatchDraw(); BeginBatchDraw();
for (auto& c : controls) redrawScene(true, true);
{
c->setDirty(true);
c->draw();
}
for (auto& d : dialogs)
{
d->draw();
}
EndBatchDraw(); EndBatchDraw();
} }
@@ -382,7 +429,6 @@ int Window::runEventLoop()
{ {
bool consume = false; // 事件是否被消费的标志(用于输入事件分发) bool consume = false; // 事件是否被消费的标志(用于输入事件分发)
bool redrawDialogs = false; // 是否需要重绘对话框(控件事件可能引起对话框状态变化)
if (peekmessage(&msg, EX_MOUSE | EX_KEY | EX_WINDOW, true)) if (peekmessage(&msg, EX_MOUSE | EX_KEY | EX_WINDOW, true))
{ {
@@ -427,18 +473,8 @@ int Window::runEventLoop()
continue; continue;
} }
bool hasVisibleNonModalDialog = false;
for (const auto& d : dialogs)
{
if (d->IsVisible() && !d->model())
{
hasVisibleNonModalDialog = true;
break;
}
}
bool controlVisualChanged = false;
// 输入优先:先给顶层“非模态对话框”,再传给普通控件 // 输入优先:先给顶层“非模态对话框”,再传给普通控件
managedDispatchActive = true;
for (auto it = dialogs.rbegin(); it != dialogs.rend(); ++it) for (auto it = dialogs.rbegin(); it != dialogs.rend(); ++it)
{ {
auto& d = *it; auto& d = *it;
@@ -449,6 +485,12 @@ int Window::runEventLoop()
if (!SxIsNoisyMsg(msg.message)) if (!SxIsNoisyMsg(msg.message))
SX_LOGD("Event") << SX_T("事件被非模态对话框处理:", "Event consumed by non-modal dialog: ") SX_LOGD("Event") << SX_T("事件被非模态对话框处理:", "Event consumed by non-modal dialog: ")
<< SxMsgName(msg.message); << SxMsgName(msg.message);
// 非模态对话框吞掉自己的区域内鼠标移动后,底层普通控件收不到“离开”消息,
// 会残留 hover。这里补一条落在窗口外的合成移动,只用于清理底层 hover,
// 不会让底层控件重新命中。
if (msg.message == WM_MOUSEMOVE)
dispatchSyntheticMouseMoveToControls(-32768, -32768);
break; break;
} }
} }
@@ -458,8 +500,6 @@ int Window::runEventLoop()
{ {
Control* current = it->get(); Control* current = it->get();
consume = current->handleEvent(msg); consume = current->handleEvent(msg);
if (current->didEventAffectVisual())
controlVisualChanged = true;
if (consume) if (consume)
{ {
if (!SxIsNoisyMsg(msg.message)) if (!SxIsNoisyMsg(msg.message))
@@ -469,20 +509,8 @@ int Window::runEventLoop()
break; break;
} }
} }
if (hasVisibleNonModalDialog && controlVisualChanged)
redrawDialogs = true; // 只有普通控件真的发生视觉变化时,才补画非模态对话框
} }
} managedDispatchActive = false;
// 关键点⑦:事件处理后,如果对话框状态可能变化(例如控件事件引起的控件重绘可能导致对话框被覆盖),优先重绘对话框以确保界面响应及时;后续再根据 needResizeDirty 进行一次性重绘。
if (redrawDialogs)
{
for (auto& d : dialogs)
{
if (!d->model() && d->IsVisible())
d->setDirty(true);
d->draw();
}
redrawDialogs = false; // 重置标志
} }
//如果有对话框打开或者关闭强制重绘 //如果有对话框打开或者关闭强制重绘
@@ -508,31 +536,21 @@ int Window::runEventLoop()
{ {
ScreenToClient(this->hWnd, &pt); ScreenToClient(this->hWnd, &pt);
ExMessage mm; ExMessage mm;
mm.message = WM_MOUSEMOVE;
mm.x = (short)pt.x;
mm.y = (short)pt.y;
// 只分发给 window 层控件(因为 dialog 已经关闭或即将关闭) // 只分发给 window 层控件(因为 dialog 已经关闭或即将关闭)
for (auto it = controls.rbegin(); it != controls.rend(); ++it) managedDispatchActive = true;
(*it)->handleEvent(mm); dispatchSyntheticMouseMoveToControls((short)pt.x, (short)pt.y);
managedDispatchActive = false;
} }
dialogClose = false; // 重置标志 dialogClose = false; // 重置标志
} }
BeginBatchDraw(); BeginBatchDraw();
SX_LOGD("Event") << SX_T("对话框打开/关闭,触发全量重绘", "The dialog box opens/closes, triggering a full redraw"); SX_LOGD("Event") << SX_T("对话框打开/关闭,触发全量重绘", "The dialog box opens/closes, triggering a full redraw");
// 先绘制普通控件 redrawScene(true, true);
for (auto& c : controls)
c->draw();
// 然后绘制对话框(确保对话框在最上层)
for (auto& d : dialogs)
{
if (!d->model() && d->IsVisible())
d->setDirty(true);
d->draw();
}
EndBatchDraw(); EndBatchDraw();
needredraw = false; needredraw = false;
dialogOpen = false; dialogOpen = false;
managedSceneDirty = false;
} }
// —— 统一收口(needResizeDirty 为真时执行一次性重绘)—— // —— 统一收口(needResizeDirty 为真时执行一次性重绘)——
@@ -579,25 +597,9 @@ int Window::runEventLoop()
int confirmedWidth = clientRect.right - clientRect.left; int confirmedWidth = clientRect.right - clientRect.left;
int confirmedHeight = clientRect.bottom - clientRect.top; int confirmedHeight = clientRect.bottom - clientRect.top;
int renderWidth = confirmedWidth;
int renderHeight = confirmedHeight;
// 背景:若设置了背景图则重载并铺满;否则清屏为纯色
if (background && !bkImageFile.empty())
{
background = std::make_unique<IMAGE>();
loadimage(background.get(), bkImageFile.c_str(), renderWidth, renderHeight, true);
putimage(0, 0, background.get());
}
else
{
setbkcolor(wBkcolor);
cleardevice();
}
// 最终提交“当前已应用尺寸”(用于外部查询/下次比较) // 最终提交“当前已应用尺寸”(用于外部查询/下次比较)
width = renderWidth; width = confirmedWidth;
height = renderHeight; height = confirmedHeight;
for (auto& d : dialogs) for (auto& d : dialogs)
{ {
@@ -609,8 +611,7 @@ int Window::runEventLoop()
} }
// 统一批量绘制 // 统一批量绘制
for (auto& c : controls) c->draw(); redrawScene(true, true);
for (auto& d : dialogs) d->draw();
EndBatchDraw(); EndBatchDraw();
@@ -621,8 +622,12 @@ int Window::runEventLoop()
SX_LOGI("Resize") << SX_T("尺寸调整已完成:width=","Resize settle done: width=") << width << " height=" << height; SX_LOGI("Resize") << SX_T("尺寸调整已完成:width=","Resize settle done: width=") << width << " height=" << height;
needResizeDirty = false; // 收口完成,清标志 needResizeDirty = false; // 收口完成,清标志
managedSceneDirty = false;
} }
if (!needResizeDirty && !dialogOpen && !dialogClose)
flushManagedRepaint();
// 轻微睡眠,削峰填谷(不阻塞拖拽体验) // 轻微睡眠,削峰填谷(不阻塞拖拽体验)
Sleep(10); Sleep(10);
} }
@@ -638,20 +643,8 @@ void Window::setBkImage(std::string pImgFile)
background = std::make_unique<IMAGE>(); background = std::make_unique<IMAGE>();
bkImageFile = std::move(pImgFile); bkImageFile = std::move(pImgFile);
loadimage(background.get(), bkImageFile.c_str(), width, height, true);
putimage(0, 0, background.get());
BeginBatchDraw(); BeginBatchDraw();
for (auto& c : controls) redrawScene(true, true);
{
c->setDirty(true);
c->draw();
}
for (auto& d : dialogs)
{
d->setDirty(true);
d->draw();
}
EndBatchDraw(); EndBatchDraw();
} }
@@ -659,20 +652,11 @@ void Window::setBkcolor(COLORREF c)
{ {
// 更换纯色背景:立即清屏并批量重绘控件/对话框 // 更换纯色背景:立即清屏并批量重绘控件/对话框
wBkcolor = c; wBkcolor = c;
setbkcolor(wBkcolor); background.reset();
cleardevice(); bkImageFile.clear();
BeginBatchDraw(); BeginBatchDraw();
for (auto& c : controls) redrawScene(true, true);
{
c->setDirty(true);
c->draw();
}
for (auto& d : dialogs)
{
d->setDirty(true);
d->draw();
}
EndBatchDraw(); EndBatchDraw();
} }
@@ -687,12 +671,14 @@ void Window::setHeadline(std::string title)
void Window::addControl(std::unique_ptr<Control> control) void Window::addControl(std::unique_ptr<Control> control)
{ {
// 新增控件:仅加入管理容器,具体绘制在 draw()/收口时统一进行 // 新增控件:仅加入管理容器,具体绘制在 draw()/收口时统一进行
control->setHostWindow(this);
controls.push_back(std::move(control)); controls.push_back(std::move(control));
} }
void Window::addDialog(std::unique_ptr<Control> dlg) void Window::addDialog(std::unique_ptr<Control> dlg)
{ {
// 新增非模态对话框:管理顺序决定事件优先级(顶层从后往前) // 新增非模态对话框:管理顺序决定事件优先级(顶层从后往前)
dlg->setHostWindow(this);
dialogs.push_back(std::move(dlg)); dialogs.push_back(std::move(dlg));
} }
@@ -783,17 +769,6 @@ void Window::pumpResizeIfNeeded()
// Resize + 背景 // Resize + 背景
Resize(nullptr, finalW, finalH); Resize(nullptr, finalW, finalH);
GetClientRect(hWnd, &rc); GetClientRect(hWnd, &rc);
if (background && !bkImageFile.empty())
{
background = std::make_unique<IMAGE>();
loadimage(background.get(), bkImageFile.c_str(), rc.right - rc.left, rc.bottom - rc.top, true);
putimage(0, 0, background.get());
}
else
{
setbkcolor(wBkcolor);
cleardevice();
}
width = rc.right - rc.left; height = rc.bottom - rc.top; width = rc.right - rc.left; height = rc.bottom - rc.top;
// 通知控件/对话框 // 通知控件/对话框
@@ -806,8 +781,7 @@ void Window::pumpResizeIfNeeded()
dd->recenterInHostWindow(); // 窗口变化时仅重新居中,不拉伸 Dialog 自身 dd->recenterInHostWindow(); // 窗口变化时仅重新居中,不拉伸 Dialog 自身
// 重绘 // 重绘
for (auto& c : controls) c->draw(); redrawScene(true, true);
for (auto& d : dialogs) d->draw();
EndBatchDraw(); EndBatchDraw();
SendMessage(hWnd, WM_SETREDRAW, TRUE, 0); SendMessage(hWnd, WM_SETREDRAW, TRUE, 0);
@@ -818,6 +792,7 @@ void Window::pumpResizeIfNeeded()
ValidateRect(hWnd, nullptr); ValidateRect(hWnd, nullptr);
needResizeDirty = false; needResizeDirty = false;
managedSceneDirty = false;
} }
void Window::scheduleResizeFromModal(int w, int h) void Window::scheduleResizeFromModal(int w, int h)
{ {
+8
View File
@@ -54,6 +54,8 @@ class Window
// —— 控件/对话框 ——(容器内的普通控件与非模态对话框) // —— 控件/对话框 ——(容器内的普通控件与非模态对话框)
std::vector<std::unique_ptr<Control>> controls; std::vector<std::unique_ptr<Control>> controls;
std::vector<std::unique_ptr<Control>> dialogs; std::vector<std::unique_ptr<Control>> dialogs;
bool managedDispatchActive = false; // 事件分发期:控件只改状态,不立即画
bool managedSceneDirty = false; // 本轮分发是否登记了统一重绘请求
public: public:
bool dialogClose = false; // 项目内使用的状态位,对话框关闭标志 bool dialogClose = false; // 项目内使用的状态位,对话框关闭标志
@@ -95,6 +97,12 @@ public:
// —— 尺寸调整 ——(供内部与外部调用的尺寸变化处理) // —— 尺寸调整 ——(供内部与外部调用的尺寸变化处理)
void pumpResizeIfNeeded(); // 执行一次统一收口重绘 void pumpResizeIfNeeded(); // 执行一次统一收口重绘
void scheduleResizeFromModal(int w, int h); void scheduleResizeFromModal(int w, int h);
bool isManagedDispatchActive() const;
void requestManagedRepaint();
void flushManagedRepaint();
private: private:
void adaptiveLayout(std::unique_ptr<Control>& c, const int finalH, const int finalW); void adaptiveLayout(std::unique_ptr<Control>& c, const int finalH, const int finalW);
void redrawScene(bool forceControlsDirty, bool forceDialogsDirty);
void drawWindowBackground();
void dispatchSyntheticMouseMoveToControls(short x, short y);
}; };
+193 -2
View File
@@ -1,6 +1,6 @@
// 本工具基于 StellarX 构建,轻量级的 Windows GUI 框架 // StellarX 星垣GUI框架 - 测试用例
#include"StellarX.h" #include"StellarX.h"
#define KEY 2 #define KEY 4
#if 1 == KEY #if 1 == KEY
int main() int main()
@@ -106,6 +106,197 @@ int main()
} }
#endif #endif
#if 4 == KEY
#include"StellarX.h"
#include <vector>
int main()
{
StellarX::SxLogger::setGBK();
StellarX::SxLogger::Get().enableConsole(true);
StellarX::SxLogger::Get().setMinLevel(StellarX::SxLogLevel::Debug);
StellarX::SxLogger::Get().setLanguage(StellarX::SxLogLanguage::ZhCN);
Window win(1280, 780, 1, RGB(242, 246, 250), "StellarX 综合回归测试用例 4");
auto infoPanel = std::make_unique<Canvas>(20, 20, 1240, 90);
auto infoPanelPtr = infoPanel.get();
infoPanelPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
infoPanelPtr->setCanvasBkColor(RGB(225, 234, 244));
infoPanelPtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
infoPanelPtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto infoTitle = std::make_unique<Label>(20, 10, "综合回归测试 4:对话框遮挡、快速 Hover、分页按钮、模态 Resize");
infoTitle->textStyle.nHeight = 26;
infoTitle->setTextdisap(true);
auto infoLine1 = std::make_unique<Label>(20, 42, "1. 点击右侧按钮打开非模态/模态对话框;2. 在顶部按钮、页签、分页按钮附近快速划动光标。");
infoLine1->setTextdisap(true);
auto infoLine2 = std::make_unique<Label>(20, 62, "3. 模态对话框打开后拖动窗口大小;4. 关闭对话框后观察底层按钮 Hover 是否立即恢复。");
infoLine2->setTextdisap(true);
auto openAsyncButton = std::make_unique<Button>(930, 22, 130, 42, "打开非模态");
auto openAsyncButtonPtr = openAsyncButton.get();
openAsyncButtonPtr->enableTooltip(true);
openAsyncButtonPtr->setTooltipText("打开长文本非模态对话框,覆盖顶部按钮 / 页签 / 分页区");
auto openModalButton = std::make_unique<Button>(1080, 22, 130, 42, "打开模态");
auto openModalButtonPtr = openModalButton.get();
openModalButtonPtr->enableTooltip(true);
openModalButtonPtr->setTooltipText("打开模态对话框并测试拖拽窗口后的重绘与关闭");
infoPanelPtr->addControl(std::move(infoTitle));
infoPanelPtr->addControl(std::move(infoLine1));
infoPanelPtr->addControl(std::move(infoLine2));
infoPanelPtr->addControl(std::move(openAsyncButton));
infoPanelPtr->addControl(std::move(openModalButton));
auto hoverCanvas = std::make_unique<Canvas>(20, 140, 1240, 100);
auto hoverCanvasPtr = hoverCanvas.get();
hoverCanvasPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
hoverCanvasPtr->setCanvasBkColor(RGB(230, 238, 248));
hoverCanvasPtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
hoverCanvasPtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto hoverTitle = std::make_unique<Label>(20, 8, "区域 A:打开非模态对话框后,请在下面 8 个按钮之间快速来回划动鼠标。");
hoverTitle->setTextdisap(true);
hoverCanvasPtr->addControl(std::move(hoverTitle));
for (int i = 0; i < 8; ++i)
{
auto hoverButton = std::make_unique<Button>(250 + i * 92, 38, 72, 38, "B" + std::to_string(i + 1));
hoverButton->enableTooltip(true);
hoverButton->setTooltipText("顶部 Hover 按钮 " + std::to_string(i + 1));
hoverButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
hoverCanvasPtr->addControl(std::move(hoverButton));
}
auto tabControl = std::make_unique<TabControl>(120, 260, 1040, 180);
auto tabControlPtr = tabControl.get();
tabControlPtr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
tabControlPtr->setCanvasfillMode(StellarX::FillMode::Null);
tabControlPtr->setTabPlacement(StellarX::TabPlacement::Top);
tabControlPtr->setTabBarHeight(28);
tabControlPtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
tabControlPtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
auto page1 = std::make_unique<Canvas>(0, 0, 1040, 152);
page1->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
page1->setCanvasBkColor(RGB(246, 250, 255));
auto page2 = std::make_unique<Canvas>(0, 0, 1040, 152);
page2->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
page2->setCanvasBkColor(RGB(250, 248, 242));
auto page3 = std::make_unique<Canvas>(0, 0, 1040, 152);
page3->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
page3->setCanvasBkColor(RGB(243, 250, 244));
auto tabPage1Ptr = page1.get();
auto tabPage2Ptr = page2.get();
auto tabPage3Ptr = page3.get();
tabControlPtr->add(std::make_pair(std::make_unique<Button>(0, 0, 120, 28, "页签-事件区"), std::move(page1)));
tabControlPtr->add(std::make_pair(std::make_unique<Button>(0, 0, 120, 28, "页签-空白区"), std::move(page2)));
tabControlPtr->add(std::make_pair(std::make_unique<Button>(0, 0, 120, 28, "页签-辅助区"), std::move(page3)));
tabControlPtr->setActiveIndex(0);
auto page1Label = std::make_unique<Label>(20, 10, "区域 B:打开非模态对话框后,切换页签并在页内按钮上快速 Hover。");
page1Label->setTextdisap(true);
tabPage1Ptr->addControl(std::move(page1Label));
for (int i = 0; i < 4; ++i)
{
auto pageButton = std::make_unique<Button>(190 + i * 150, 55, 110, 38, "页内按钮" + std::to_string(i + 1));
pageButton->enableTooltip(true);
pageButton->setTooltipText("页签区按钮 " + std::to_string(i + 1));
tabPage1Ptr->addControl(std::move(pageButton));
}
auto page2Label = std::make_unique<Label>(30, 55, "区域 B-2:此页保留较大空白区,用来观察 Hover 离开空白区域时的恢复。");
page2Label->setTextdisap(true);
tabPage2Ptr->addControl(std::move(page2Label));
auto page3Label = std::make_unique<Label>(20, 18, "区域 B-3:辅助页,用来测试页签切换本身的重绘是否稳定。");
page3Label->setTextdisap(true);
auto page3Button = std::make_unique<Button>(420, 65, 180, 40, "辅助按钮");
page3Button->enableTooltip(true);
page3Button->setTooltipText("辅助页按钮 Tooltip");
tabPage3Ptr->addControl(std::move(page3Label));
tabPage3Ptr->addControl(std::move(page3Button));
auto table = std::make_unique<Table>(240, 470);
auto tablePtr = table.get();
tablePtr->setHeaders({ "编号", "名称", "状态", "备注" });
tablePtr->setData({
{"01", "顶部按钮覆盖测试", "待验证", "快速 Hover 后不能残留高亮"},
{"02", "页签切换测试", "待验证", "页签按钮与页内容不应穿透"},
{"03", "分页按钮测试", "待验证", "被非模态部分覆盖时仍应稳定"},
{"04", "模态 Resize 测试", "待验证", "拖窗后标题、关闭按钮不应残影"},
{"05", "关闭后 Hover 恢复", "待验证", "模态关闭后底层按钮 Hover 应恢复"}
});
tablePtr->setRowsPerPage(2);
tablePtr->setTableBorderWidth(1);
tablePtr->setTableBorder(RGB(60, 90, 120));
tablePtr->setTableFillMode(StellarX::FillMode::Null);
tablePtr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
tablePtr->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
const std::string asyncText =
"KEY4 非模态回归测试正在进行。\n"
"请依次检查以下场景:\n"
"1. 在顶部按钮之间快速来回移动鼠标。\n"
"2. 切换页签,并在页签按钮与页内按钮上快速 Hover。\n"
"3. 将鼠标移动到表格分页按钮附近,观察是否闪烁或穿透。\n"
"4. 将鼠标移出窗口后再移回窗口。\n"
"5. 保持本对话框打开,确认它始终位于最上层。\n"
"6. 若需要重复测试,可直接再次点击右上角按钮。\n"
"7. 本对话框故意较高,以覆盖顶部按钮、页签区与部分分页区。\n"
"8. 顶部按钮区域用于检测快速 Hover 残留。\n"
"9. 页签区域用于检测被遮挡时的切换与 Hover。\n"
"10. 表格分页区用于检测分页按钮被部分覆盖时的重绘。\n"
"11. 若关闭本对话框,请观察底层 Hover 是否立即恢复。\n"
"12. 本消息框保留较多行,是为了增大对遮挡场景的覆盖范围。";
const std::string modalText =
"KEY4 模态回归测试。\n"
"请在本对话框保持打开时执行:\n"
"1. 拖拽窗口大小,观察标题、关闭按钮与功能按钮是否残影。\n"
"2. 拖拽后点击关闭按钮。\n"
"3. 拖拽后点击功能按钮。\n"
"4. 对话框关闭后,将鼠标停在底层顶部按钮上,观察 Hover 是否恢复。";
openAsyncButtonPtr->setOnClickListener([&win, asyncText]()
{
StellarX::MessageBox::showAsync(
win,
asyncText,
"KEY4 非模态综合回归",
StellarX::MessageBoxType::YesNoCancel,
[](StellarX::MessageBoxResult result)
{
SX_LOGI("KEY4") << "非模态对话框结果=" << (int)result;
});
});
openModalButtonPtr->setOnClickListener([&win, modalText]()
{
auto result = StellarX::MessageBox::showModal(
win,
modalText,
"KEY4 模态综合回归",
StellarX::MessageBoxType::OKCancel);
SX_LOGI("KEY4") << "模态对话框结果=" << (int)result;
});
win.addControl(std::move(infoPanel));
win.addControl(std::move(hoverCanvas));
win.addControl(std::move(tabControl));
win.addControl(std::move(table));
win.draw();
return win.runEventLoop();
}
#endif
#if 2 == KEY #if 2 == KEY
// 本工具基于 StellarX 构建,轻量级的 Windows GUI 框架。 // 本工具基于 StellarX 构建,轻量级的 Windows GUI 框架。
#include"StellarX.h" #include"StellarX.h"