Snapshot before max-resize threshold diagnosis

This commit is contained in:
Codex
2026-04-09 03:23:10 +08:00
parent 77a8fe568a
commit f567369300
25 changed files with 1489 additions and 36 deletions
+28 -1
View File
@@ -159,6 +159,8 @@ bool Canvas::handleEvent(const ExMessage& msg)
if (anyDirty) if (anyDirty)
{ {
// 只要任一子控件因本次事件进入 dirty,就把这笔重绘继续向上汇报。
// 在托管模式下,这不会立即绘制,而是登记为 Canvas 对应的重绘 root。
if (!SxIsNoisyMsg(msg.message)) if (!SxIsNoisyMsg(msg.message))
SX_LOGD("Dirty") << SX_T("Canvas检测有控件为脏状态 -> 请求重绘, ","Canvas anyDirty -> requestRepaint, ")<<"id = " << id; SX_LOGD("Dirty") << SX_T("Canvas检测有控件为脏状态 -> 请求重绘, ","Canvas anyDirty -> requestRepaint, ")<<"id = " << id;
requestRepaint(parent); requestRepaint(parent);
@@ -435,8 +437,9 @@ void Canvas::requestRepaint(Control* parent)
{ {
if (shouldDeferManagedRepaint()) if (shouldDeferManagedRepaint())
{ {
// 托管路径:由 Window 统一决定这次是否只重画本 Canvas,还是升级为补画 Dialog / 整体场景。
if (auto* host = getHostWindow()) if (auto* host = getHostWindow())
host->requestManagedRepaint(); host->requestManagedRepaint(this);
return; return;
} }
@@ -474,3 +477,27 @@ void Canvas::requestRepaint(Control* parent)
onRequestRepaintAsRoot(); onRequestRepaintAsRoot();
} }
bool Canvas::canCommitManagedPartialRepaint() const
{
// Canvas 只有在“自己本体不脏 + 仍持有有效背景快照”时,
// 才能安全地做局部提交(即只更新内部脏子控件)。
return show && !dirty && hasValidBackgroundSnapshot();
}
void Canvas::commitManagedRepaint()
{
if (!show)
return;
if (canCommitManagedPartialRepaint())
{
// 快照完好:沿用 Canvas 自己已有的局部重绘逻辑。
requestRepaint(this);
return;
}
// 自身已经脏了,或快照失效:必须升级为整 Canvas 重画。
this->dirty = true;
onRequestRepaintAsRoot();
}
+7 -1
View File
@@ -4,6 +4,7 @@
* @描述: * @描述:
* 作为其他控件的父容器,提供统一的背景和边框样式。 * 作为其他控件的父容器,提供统一的背景和边框样式。
* 负责将事件传递给子控件并管理它们的绘制顺序。 * 负责将事件传递给子控件并管理它们的绘制顺序。
* 在托管重绘模式下,Canvas 通常作为一组子控件的安全重绘 root。
* *
* @特性: * @特性:
* - 支持四种矩形形状(普通、圆角,各有边框和无边框版本) * - 支持四种矩形形状(普通、圆角,各有边框和无边框版本)
@@ -64,7 +65,12 @@ public:
void setIsVisible(bool visible) override; void setIsVisible(bool visible) override;
void setDirty(bool dirty) override; void setDirty(bool dirty) override;
void onWindowResize() override; void onWindowResize() override;
void requestRepaint(Control* parent)override; // 托管模式下登记为 root;非托管模式下走局部或根级重绘
void requestRepaint(Control* parent)override;
// 判断当前 Canvas 是否可安全做局部提交
bool canCommitManagedPartialRepaint() const override;
// 托管收口阶段执行 Canvas 的真正重绘
void commitManagedRepaint() override;
//获取子控件列表 //获取子控件列表
std::vector<std::unique_ptr<Control>>& getControls() { return controls; } std::vector<std::unique_ptr<Control>>& getControls() { return controls; }
private: private:
+37 -2
View File
@@ -126,8 +126,10 @@ void Control::requestRepaint(Control* parent)
{ {
if (shouldDeferManagedRepaint()) if (shouldDeferManagedRepaint())
{ {
// 托管路径:当前正在 Window 的事件分发阶段,不能立即绘制;
// 这里只登记 source,真正的 root 选择由 Window 在 requestManagedRepaint 中完成。
if (auto* host = getHostWindow()) if (auto* host = getHostWindow())
host->requestManagedRepaint(); host->requestManagedRepaint(this);
return; return;
} }
@@ -157,8 +159,10 @@ void Control::onRequestRepaintAsRoot()
{ {
if (shouldDeferManagedRepaint()) if (shouldDeferManagedRepaint())
{ {
// 即使已经冒泡到 root,只要还在托管分发期,也不能直接绘制;
// 仍然回到 Window 做统一提交。
if (auto* host = getHostWindow()) if (auto* host = getHostWindow())
host->requestManagedRepaint(); host->requestManagedRepaint(this);
return; return;
} }
@@ -178,6 +182,9 @@ bool Control::shouldDeferManagedRepaint() const
return host && host->isManagedDispatchActive(); return host && host->isManagedDispatchActive();
} }
// 获取宿主 Window
// - 顶层控件由 Window/addDialog 直接注入;
// - 子控件没有直接注入时,沿 parent 链向上回溯即可。
Window* Control::getHostWindow() const Window* Control::getHostWindow() const
{ {
if (hostWindow) if (hostWindow)
@@ -185,6 +192,18 @@ Window* Control::getHostWindow() const
return parent ? parent->getHostWindow() : nullptr; return parent ? parent->getHostWindow() : nullptr;
} }
// 托管重绘 root 选择规则:
// - 对于直接挂在 Window 下的控件,root 就是它自己;
// - 对于嵌套在 Canvas/TabControl/Dialog 内的子控件,沿 parent 向上找到与宿主 Window 同属一棵树的最上层控件。
Control* Control::getManagedRepaintRoot()
{
Control* root = this;
Window* host = getHostWindow();
while (root->parent && root->parent->getHostWindow() == host)
root = root->parent;
return root;
}
RECT Control::getBoundsRect() const RECT Control::getBoundsRect() const
{ {
RECT rc{}; RECT rc{};
@@ -195,6 +214,22 @@ RECT Control::getBoundsRect() const
return rc; return rc;
} }
bool Control::canCommitManagedPartialRepaint() const
{
// 基类默认不承诺自己能安全做局部提交;
// 只有 Canvas / TabControl / Dialog 这类“拥有完整背景语义”的 root 才会 override 为 true。
return false;
}
void Control::commitManagedRepaint()
{
if (!show)
return;
// 基类兜底:如果没有更具体的容器实现,就按根级重绘处理。
if (dirty)
onRequestRepaintAsRoot();
}
void Control::saveBackground(int x, int y, int w, int h) void Control::saveBackground(int x, int y, int w, int h)
{ {
+12 -5
View File
@@ -4,6 +4,7 @@
* @描述: * @描述:
* 提供控件的基本属性和方法,包括位置、尺寸、重绘标记等。 * 提供控件的基本属性和方法,包括位置、尺寸、重绘标记等。
* 实现绘图状态保存和恢复机制,确保控件绘制不影响全局状态。 * 实现绘图状态保存和恢复机制,确保控件绘制不影响全局状态。
* 同时提供“事件阶段登记、收口阶段统一提交”的托管重绘基础接口。
* *
* @特性: * @特性:
* - 定义控件基本属性(坐标、尺寸、脏标记) * - 定义控件基本属性(坐标、尺寸、脏标记)
@@ -44,7 +45,7 @@ protected:
Window* hostWindow = nullptr; // 宿主窗口(顶层由 Window 注入,子控件可沿 parent 回溯) 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 是否真的引发了视觉变化(用于上层判断是否需要登记重绘)
/* == 布局模式 == */ /* == 布局模式 == */
StellarX::LayoutMode layoutMode = StellarX::LayoutMode::Fixed; // 布局模式 StellarX::LayoutMode layoutMode = StellarX::LayoutMode::Fixed; // 布局模式
@@ -81,10 +82,11 @@ public:
discardBackground(); discardBackground();
} }
protected: protected:
//向上请求重绘 // 向上请求重绘:普通路径交给父容器,托管路径则登记到 Window
virtual void requestRepaint(Control* parent); virtual void requestRepaint(Control* parent);
//根控件/无父时触发重绘 // 根控件/无父时触发重绘
virtual void onRequestRepaintAsRoot(); virtual void onRequestRepaintAsRoot();
// 当前是否处于 Window 托管分发阶段;若为真,则不应立即画
bool shouldDeferManagedRepaint() const; bool shouldDeferManagedRepaint() const;
protected: protected:
//保存背景快照 //保存背景快照
@@ -127,8 +129,12 @@ public:
void setParent(Control* parent) { this->parent = parent; } void setParent(Control* parent) { this->parent = parent; }
//设置宿主窗口(通常仅由顶层 Window/对话框注入) //设置宿主窗口(通常仅由顶层 Window/对话框注入)
virtual void setHostWindow(Window* host) { this->hostWindow = host; } virtual void setHostWindow(Window* host) { this->hostWindow = host; }
Window* getHostWindow() const; Window* getHostWindow() const; // 获取宿主 Window;子控件可沿 parent 向上回溯
RECT getBoundsRect() const; RECT getBoundsRect() const; // 获取当前控件外接矩形,用于覆盖/相交判断
Control* getManagedRepaintRoot(); // 找到本控件对应的托管重绘 root
bool hasValidBackgroundSnapshot() const { return hasSnap && saveBkImage != nullptr; } // 当前是否持有可用于局部恢复的快照
virtual bool canCommitManagedPartialRepaint() const; // 当前 root 是否可安全做“局部提交”而非整 root 重画
virtual void commitManagedRepaint(); // 托管收口阶段真正执行绘制的入口
//设置是否重绘 //设置是否重绘
virtual void setDirty(bool dirty) { this->dirty = dirty; } virtual void setDirty(bool dirty) { this->dirty = dirty; }
//检查控件是否可见 //检查控件是否可见
@@ -137,6 +143,7 @@ public:
std::string getId() const { return id; } std::string getId() const { return id; }
//检查是否为脏 //检查是否为脏
bool isDirty() { return dirty; } bool isDirty() { return dirty; }
//获取控件最近一次事件处理是否引发了视觉变化
bool didEventAffectVisual() const { return eventVisualChanged; } bool didEventAffectVisual() const { return eventVisualChanged; }
//用来检查对话框是否模态,其他控件不用实现 //用来检查对话框是否模态,其他控件不用实现
virtual bool model()const = 0; virtual bool model()const = 0;
+27 -1
View File
@@ -760,8 +760,10 @@ void Dialog::requestRepaint(Control* parent)
{ {
if (shouldDeferManagedRepaint()) if (shouldDeferManagedRepaint())
{ {
// 非模态 Dialog 在 Window 主循环中也走托管提交;
// 这样底层控件和对话框的绘制顺序由 Window 统一收口控制。
if (auto* host = getHostWindow()) if (auto* host = getHostWindow())
host->requestManagedRepaint(); host->requestManagedRepaint(this);
return; return;
} }
@@ -774,3 +776,27 @@ void Dialog::requestRepaint(Control* parent)
else else
onRequestRepaintAsRoot(); onRequestRepaintAsRoot();
} }
bool Dialog::canCommitManagedPartialRepaint() const
{
// Dialog 只有在“自身底板不脏 + 仍持有有效背景快照”时,
// 才能安全地只更新内部按钮,而不重画整个对话框底板。
return show && !dirty && hasValidBackgroundSnapshot();
}
void Dialog::commitManagedRepaint()
{
if (!show)
return;
if (canCommitManagedPartialRepaint())
{
// 背景快照完好:沿用 Dialog 自己已有的局部重绘路径。
requestRepaint(this);
return;
}
// 对话框底板本身已脏,或快照失效:必须整 Dialog 重画。
this->dirty = true;
onRequestRepaintAsRoot();
}
+5 -2
View File
@@ -4,6 +4,7 @@
* @描述: * @描述:
* 实现完整的对话框功能,支持多种按钮组合和异步结果回调。 * 实现完整的对话框功能,支持多种按钮组合和异步结果回调。
* 自动处理布局、背景保存恢复和生命周期管理。 * 自动处理布局、背景保存恢复和生命周期管理。
* 在窗口托管重绘模式下,Dialog 自身也是一个独立的重绘 root。
* *
* @特性: * @特性:
* - 支持六种标准消息框类型(OK、YesNo、YesNoCancel等) * - 支持六种标准消息框类型(OK、YesNo、YesNoCancel等)
@@ -103,7 +104,7 @@ public:
// 关闭对话框 // 关闭对话框
void Close(); void Close();
//初始化 //初始化
void setInitialization(bool init); void setInitialization(bool init); // 历史接口:当前语义更接近“请求重新布局/重建”
// 宿主窗口变化时仅重新居中,不拉伸 Dialog 自身 // 宿主窗口变化时仅重新居中,不拉伸 Dialog 自身
void recenterInHostWindow(); void recenterInHostWindow();
@@ -123,10 +124,12 @@ private:
// 依据当前 Dialog 的 x/y/width/height 重新创建标题和按钮 // 依据当前 Dialog 的 x/y/width/height 重新创建标题和按钮
void rebuildChrome(); void rebuildChrome();
void addControl(std::unique_ptr<Control> control); void addControl(std::unique_ptr<Control> control);
bool canCommitManagedPartialRepaint() const override; // 判断当前 Dialog 是否可安全做局部提交
void commitManagedRepaint() override; // 托管收口阶段执行 Dialog 的真正重绘
// 清除所有控件 // 清除所有控件
void clearControls(); void clearControls();
//创建对话框按钮 //创建对话框按钮
std::unique_ptr<Button> createDialogButton(int x, int y, const std::string& text); std::unique_ptr<Button> createDialogButton(int x, int y, const std::string& text);
void requestRepaint(Control* parent) override; void requestRepaint(Control* parent) override; // 托管模式下登记为 Dialog root;非托管模式下立即更新内部按钮
}; };
+25 -1
View File
@@ -418,8 +418,9 @@ void TabControl::requestRepaint(Control* parent)
{ {
if (shouldDeferManagedRepaint()) if (shouldDeferManagedRepaint())
{ {
// 托管路径:TabControl 作为“页签栏 + 当前页面”的统一重绘 root 登记到 Window。
if (auto* host = getHostWindow()) if (auto* host = getHostWindow())
host->requestManagedRepaint(); host->requestManagedRepaint(this);
return; return;
} }
@@ -437,3 +438,26 @@ void TabControl::requestRepaint(Control* parent)
else else
onRequestRepaintAsRoot(); onRequestRepaintAsRoot();
} }
bool TabControl::canCommitManagedPartialRepaint() const
{
// TabControl 只有在自己本体不脏且背景快照有效时,才允许只更新脏页签/脏页面。
return show && !dirty && hasValidBackgroundSnapshot();
}
void TabControl::commitManagedRepaint()
{
if (!show)
return;
if (canCommitManagedPartialRepaint())
{
// 页签栏和页面基线都还有效:沿用原有局部重绘逻辑。
requestRepaint(this);
return;
}
// 自身布局或背景已经变化:升级为整 TabControl 重画。
this->dirty = true;
onRequestRepaintAsRoot();
}
+4 -1
View File
@@ -4,6 +4,7 @@
* @描述: * @描述:
* 提供页签栏布局(上/下/左/右)、选中切换、页内容区域定位; * 提供页签栏布局(上/下/左/右)、选中切换、页内容区域定位;
* 与 Button 一起工作,支持窗口大小变化、可见性联动与脏区重绘。 * 与 Button 一起工作,支持窗口大小变化、可见性联动与脏区重绘。
* 在托管重绘模式下,TabControl 作为“页签栏 + 当前页面”的统一重绘 root。
* *
* @特性: * @特性:
* - 页签栏四向排列(Top / Bottom / Left / Right * - 页签栏四向排列(Top / Bottom / Left / Right
@@ -72,5 +73,7 @@ public:
//设置脏区并请求重绘 //设置脏区并请求重绘
void setDirty(bool dirty) override; void setDirty(bool dirty) override;
//请求父控件重绘 //请求父控件重绘
void requestRepaint(Control* parent)override; void requestRepaint(Control* parent)override; // 托管模式下登记为 root;非托管模式下局部更新脏按钮/脏页面
bool canCommitManagedPartialRepaint() const override; // 判断当前 TabControl 是否可安全做局部提交
void commitManagedRepaint() override; // 托管收口阶段执行 TabControl 的真正重绘
}; };
+137 -9
View File
@@ -27,14 +27,62 @@ static const char* SxMsgName(UINT m)
} }
} }
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;
}
bool Window::isManagedDispatchActive() const bool Window::isManagedDispatchActive() const
{ {
return managedDispatchActive; return managedDispatchActive;
} }
void Window::requestManagedRepaint() /**
* requestManagedRepaint(source)
* 作用:在“事件分发期”登记一笔托管重绘请求,而不是立即绘制。
* 关键点:
* - source 是真正发生视觉变化的控件;
* - root 是后续真正安全重绘的最小层级(通常是顶层控件/容器,或 Dialog 自身);
* - coverage 记录这次变化影响的范围,用于判断哪些上层 Dialog 需要补画。
*/
void Window::requestManagedRepaint(Control* source)
{ {
if (!source)
return;
managedSceneDirty = true; managedSceneDirty = true;
Control* root = source->getManagedRepaintRoot();
if (!root)
return;
RECT coverage = root->getBoundsRect();
if (root->canCommitManagedPartialRepaint())
coverage = source->getBoundsRect();
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() void Window::drawWindowBackground()
@@ -73,17 +121,53 @@ void Window::redrawScene(bool forceControlsDirty, bool forceDialogsDirty)
} }
} }
/**
* flushManagedRepaint()
* 作用:提交当前事件分发阶段累计的托管重绘请求。
* 提交顺序:
* 1)先根据 coverage 找出需要补画的非模态 Dialog;
* 2)再按 Window::controls 的层级顺序提交受影响的普通 root;
* 3)最后把相交的 Dialog 补画回最上层。
* 说明:
* - 这里不做整场景重画,而是只画本轮登记的 root;
* - 之所以按 controls 顺序提交,而不是按登记顺序,是为了保持顶层控件原有的 z-order。
*/
void Window::flushManagedRepaint() void Window::flushManagedRepaint()
{ {
if (!managedSceneDirty || !hWnd) if (!managedSceneDirty || !hWnd)
return; return;
BeginBatchDraw(); BeginBatchDraw();
redrawScene(true, true); std::vector<Control*> overlayDialogs;
for (auto& item : managedRepaintItems)
collectManagedDialogOverlays(item.root, item.coverage, overlayDialogs);
for (auto& control : controls)
{
for (auto& item : managedRepaintItems)
{
if (item.root == control.get() && item.root && item.root->IsVisible())
{
item.root->commitManagedRepaint();
break;
}
}
}
for (auto& dialog : overlayDialogs)
{
if (!dialog || !dialog->IsVisible())
continue;
dialog->setDirty(true);
dialog->draw();
}
EndBatchDraw(); EndBatchDraw();
managedSceneDirty = false; clearManagedRepaintState();
} }
// 合成一条 WM_MOUSEMOVE 并直接分发给 Window 顶层控件;常用于同步 hover 状态
void Window::dispatchSyntheticMouseMoveToControls(short x, short y) void Window::dispatchSyntheticMouseMoveToControls(short x, short y)
{ {
ExMessage mm{}; ExMessage mm{};
@@ -94,6 +178,42 @@ void Window::dispatchSyntheticMouseMoveToControls(short x, short y)
(*it)->handleEvent(mm); (*it)->handleEvent(mm);
} }
/**
* collectManagedDialogOverlays(repaintRoot, coverage, overlays)
* 作用:找出在本轮提交后需要重新盖到最上层的非模态 Dialog。
* 规则:
* - 如果 repaintRoot 本身就是 Dialog,则从它自己开始往上层 Dialog 收集;
* - 如果 repaintRoot 是普通控件,则收集所有与 coverage 相交的可见 Dialog。
*/
void Window::collectManagedDialogOverlays(Control* repaintRoot, const RECT& coverage, std::vector<Control*>& overlays)
{
size_t startIdx = 0;
if (auto* dialogRoot = dynamic_cast<Dialog*>(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);
}
}
}
/** /**
* ApplyResizableStyle * ApplyResizableStyle
* 作用:统一设置可拉伸/裁剪样式,并按开关使用 WS_EX_COMPOSITED(合成双缓冲)。 * 作用:统一设置可拉伸/裁剪样式,并按开关使用 WS_EX_COMPOSITED(合成双缓冲)。
@@ -364,6 +484,7 @@ void Window::draw()
BeginBatchDraw(); BeginBatchDraw();
redrawScene(true, true); redrawScene(true, true);
EndBatchDraw(); EndBatchDraw();
clearManagedRepaintState();
} }
/** /**
@@ -406,6 +527,7 @@ void Window::draw(std::string imagePath)
BeginBatchDraw(); BeginBatchDraw();
redrawScene(true, true); redrawScene(true, true);
EndBatchDraw(); EndBatchDraw();
clearManagedRepaintState();
} }
// ---------------- 事件循环 ---------------- // ---------------- 事件循环 ----------------
@@ -416,7 +538,9 @@ void Window::draw(std::string imagePath)
* 关键策略: * 关键策略:
* - WM_SIZE:始终更新 pendingW/H(即使在拉伸中也只记录不立即绘制); * - WM_SIZE:始终更新 pendingW/H(即使在拉伸中也只记录不立即绘制);
* - needResizeDirty:当尺寸确实变化时置位,随后在循环尾进行一次性重绘; * - needResizeDirty:当尺寸确实变化时置位,随后在循环尾进行一次性重绘;
* - 非模态对话框优先消费事件(顶层从后往前);再交给普通控件 * - 非模态对话框优先消费事件(顶层从后往前);再交给普通控件
* - managedDispatchActive=true 期间,控件 requestRepaint 不会立即画,而是登记到 managedRepaintItems
* - 事件尾通过 flushManagedRepaint 提交本轮 root 重绘,再按需补画 Dialog。
*/ */
int Window::runEventLoop() int Window::runEventLoop()
{ {
@@ -473,7 +597,8 @@ int Window::runEventLoop()
continue; continue;
} }
// 输入优先:先给顶层“非模态对话框”,再传给普通控件 // 输入优先:先给顶层“非模态对话框”,再传给普通控件
// 在 managedDispatchActive 期间,控件只改状态并登记重绘 root,不直接画。
managedDispatchActive = true; managedDispatchActive = true;
for (auto it = dialogs.rbegin(); it != dialogs.rend(); ++it) for (auto it = dialogs.rbegin(); it != dialogs.rend(); ++it)
{ {
@@ -513,7 +638,8 @@ int Window::runEventLoop()
managedDispatchActive = false; managedDispatchActive = false;
} }
//如果有对话框打开或者关闭强制重绘 // 对话框打开/关闭属于全局层级变化:这里仍然使用整场景重绘兜底,
// 并在结束后清空本轮托管重绘登记,避免旧的 root 请求延后提交。
bool needredraw = false; bool needredraw = false;
if(dialogOpen) if(dialogOpen)
{ {
@@ -550,10 +676,11 @@ int Window::runEventLoop()
EndBatchDraw(); EndBatchDraw();
needredraw = false; needredraw = false;
dialogOpen = false; dialogOpen = false;
managedSceneDirty = false; clearManagedRepaintState();
} }
// —— 统一收口(needResizeDirty 为真时执行一次性重绘)—— // —— 统一收口(needResizeDirty 为真时执行一次性重绘)——
// resize 会改变布局和背景基线,因此仍然走整场景重绘,而不是局部 root 提交。
if (needResizeDirty) if (needResizeDirty)
{ {
SX_LOGI("Resize") << SX_T("调整窗口尺寸开始:width=","Resize settle start: width=") << width << " height=" << height; SX_LOGI("Resize") << SX_T("调整窗口尺寸开始:width=","Resize settle start: width=") << width << " height=" << height;
@@ -622,9 +749,10 @@ 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; clearManagedRepaintState();
} }
// 普通输入事件收口:只在没有 resize / 对话框开关这种全局变化时,才提交本轮托管重绘。
if (!needResizeDirty && !dialogOpen && !dialogClose) if (!needResizeDirty && !dialogOpen && !dialogClose)
flushManagedRepaint(); flushManagedRepaint();
@@ -792,7 +920,7 @@ void Window::pumpResizeIfNeeded()
ValidateRect(hWnd, nullptr); ValidateRect(hWnd, nullptr);
needResizeDirty = false; needResizeDirty = false;
managedSceneDirty = false; clearManagedRepaintState();
} }
void Window::scheduleResizeFromModal(int w, int h) void Window::scheduleResizeFromModal(int w, int h)
{ {
+32 -13
View File
@@ -5,12 +5,13 @@
* - 提供一个基于 Win32 + EasyX 的“可拉伸且稳定不抖”的窗口容器。 * - 提供一个基于 Win32 + EasyX 的“可拉伸且稳定不抖”的窗口容器。
* - 通过消息过程子类化(WndProcThunk)接管关键消息(WM_SIZING/WM_SIZE/...)。 * - 通过消息过程子类化(WndProcThunk)接管关键消息(WM_SIZING/WM_SIZE/...)。
* - 将“几何变化记录(pendingW/H)”与“统一收口重绘(needResizeDirty)”解耦。 * - 将“几何变化记录(pendingW/H)”与“统一收口重绘(needResizeDirty)”解耦。
* - 在事件分发阶段只改状态并登记重绘 root,在事件尾统一提交,减少分离绘制造成的闪烁。
* *
* 关键点(与 .cpp 中实现对应): * 关键点(与 .cpp 中实现对应):
* - isSizing:处于交互拉伸阶段时,冻结重绘;松手后统一重绘,防止抖动。 * - isSizing:处于交互拉伸阶段时,冻结重绘;松手后统一重绘,防止抖动。
* - WM_SIZING:只做“最小尺寸夹紧”,不回滚矩形、不做对齐;把其余交给系统。 * - WM_SIZING:只做“最小尺寸夹紧”,不回滚矩形、不做对齐;把其余交给系统。
* - WM_GETMINMAXINFO:按最小“客户区”换算到“窗口矩形”,提供系统层最小轨迹值。 * - WM_GETMINMAXINFO:按最小“客户区”换算到“窗口矩形”,提供系统层最小轨迹值。
* - runEventLoop只记录 WM_SIZE 的新尺寸;真正绘制放在 needResizeDirty 时集中处理。 * - runEventLoop输入事件先分发给对话框/控件,控件只登记重绘请求;真正绘制在分发或 resize 收口时统一处理。
*/ */
//fuck windows //fuck windows
//fuck win32 //fuck win32
@@ -52,14 +53,20 @@ class Window
std::string bkImageFile; // 背景图文件路径(loadimage 用) std::string bkImageFile; // 背景图文件路径(loadimage 用)
// —— 控件/对话框 ——(容器内的普通控件与非模态对话框) // —— 控件/对话框 ——(容器内的普通控件与非模态对话框)
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 managedDispatchActive = false; // 事件分发期:控件只改状态并登记重绘,不立即画
bool managedSceneDirty = false; // 本轮分发是否登记了统一重绘请求 bool managedSceneDirty = false; // 当前分发轮次是否已经登记了至少一笔托管重绘请求
struct ManagedRepaintItem // 托管重绘项:记录由哪个控件发起、需要重绘的根控件和覆盖范围(用于后续判断哪些对话框需要补画)
{
Control* root = nullptr; // 顶层重绘根(直接挂在 Window 下的控件,或 Dialog 自身)
RECT coverage{}; // 本轮脏区覆盖范围;用于判断哪些上层 Dialog 需要补画
};
std::vector<ManagedRepaintItem> managedRepaintItems; // 本轮事件分发累计的重绘项
public: public:
bool dialogClose = false; // 项目内使用的状态位,对话框关闭标志 bool dialogClose = false; // 项目内使用的状态位,对话框关闭标志
mutable bool dialogOpen = false; // 项目内使用的状态位,对话框打开标志 mutable bool dialogOpen = false; // 项目内使用的状态位,对话框打开标志
// —— 构造/析构 ——(仅初始化成员;实际样式与子类化在 draw() 中完成) // —— 构造/析构 ——(仅初始化成员;实际样式与子类化在 draw() 中完成)
Window(int width, int height, int mode); Window(int width, int height, int mode);
@@ -94,15 +101,27 @@ public:
std::string getBkImageFile() const; std::string getBkImageFile() const;
std::vector<std::unique_ptr<Control>>& getControls(); std::vector<std::unique_ptr<Control>>& getControls();
// —— 尺寸调整 ——(供内部与外部调用的尺寸变化处理 // —— 尺寸调整 / 托管重绘 ——(事件阶段登记,收口阶段提交
void pumpResizeIfNeeded(); // 执行一次统一收口重绘
void scheduleResizeFromModal(int w, int h); // 执行一次 resize 收口 + 统一重绘
bool isManagedDispatchActive() const; void pumpResizeIfNeeded();
void requestManagedRepaint(); // 供模态 Dialog 上报宿主窗口尺寸变化
void flushManagedRepaint(); void scheduleResizeFromModal(int w, int h);
// 当前是否处于“事件只改状态,不立即画”的阶段
bool isManagedDispatchActive() const;
// 记录一笔由 source 发起的托管重绘请求
void requestManagedRepaint(Control* source);
// 在事件收口阶段提交本轮登记的 root 重绘
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);
// resize / 初次绘制 / 对话框开关这类全局场景的整场景重绘
void redrawScene(bool forceControlsDirty, bool forceDialogsDirty); void redrawScene(bool forceControlsDirty, bool forceDialogsDirty);
void drawWindowBackground(); void drawWindowBackground();
void dispatchSyntheticMouseMoveToControls(short x, short y); // 合成 WM_MOUSEMOVE,用于同步底层 hover 状态
void dispatchSyntheticMouseMoveToControls(short x, short y);
// 清空本轮托管重绘登记
void clearManagedRepaintState();
// 找出需要补画到最上层的对话框
void collectManagedDialogOverlays(Control* repaintRoot, const RECT& coverage, std::vector<Control*>& overlays);
}; };
+137
View File
@@ -0,0 +1,137 @@
# 修改总览-20260409
## 说明
- 本目录用于记录本轮协作中对框架做过的修改。
- 记录范围覆盖:
- 早期未建 Git 基线前的修改
- 已提交到仓库的阶段性修改
- 当前工作区中尚未提交的第二阶段重绘架构与注释整理
- 其中相对最原始版本、但未落到 Git 基线的那部分内容,依据协作过程与当前代码状态回溯整理。
## 阶段概览
1. 初始基线
- 提交:`dde570a`
- 含义:建立仓库后的第一版基线
2. 第一阶段:基础治理与确定性问题修复
- 主要内容:
- 资源所有权收紧
- 析构顺序修正
- `Table / Label / Dialog / Button / TextBox` 等基础问题修复
3. 第二阶段:快照与对话框行为收口
- 提交:`4a6e153`
- 提交:`7f8431a`
- 主要内容:
- 快照“作废”和“回贴”语义拆分
- `Dialog` 在窗口变化时只重新居中
- 模态/非模态对话框的残影、穿透、关闭后 hover 清理等问题修复
4. 第三阶段:统一提交重绘(第一版)
- 提交:`b07a4ec`
- 主要内容:
- 将事件阶段的“改状态”与绘制阶段的“提交”分离
- 窗口开始托管控件在事件分发期间的重绘请求
5. 第四阶段:统一提交重绘(第二版,当前工作区)
- 状态:未提交
- 主要内容:
- 从“整场景统一重绘”推进到“按 root 登记并选择性补画对话框”
-`Canvas / TabControl / Dialog` 建立托管重绘提交语义
- 补充相关注释
6. 测试与文档
- 主要内容:
- `KEY == 4` 综合回归用例
- 日志降噪
- 模板体系与开发记录目录建立
## 记录索引
### BUG
- [BUG-20260409-0001 对话框重绘、快照残留与遮挡交互异常](./BUG/BUG-20260409-0001-对话框重绘快照与遮挡交互异常.md)
- [BUG-20260409-0002 基础控件生命周期与边界条件问题](./BUG/BUG-20260409-0002-基础控件生命周期与边界条件问题.md)
### Fix
- [Fix-BUG-20260409-0001 对话框重绘、快照残留与遮挡交互异常](./Fix/Fix-BUG-20260409-0001-对话框重绘快照与遮挡交互异常.md)
- [Fix-BUG-20260409-0002 基础控件生命周期与边界条件问题](./Fix/Fix-BUG-20260409-0002-基础控件生命周期与边界条件问题.md)
### 功能变更
- [Feature-20260409-0001 基础资源所有权与生命周期收口](./功能变更/Feature-20260409-0001-基础资源所有权与生命周期收口.md)
- [Feature-20260409-0002 Dialog 与 MessageBox 行为调整](./功能变更/Feature-20260409-0002-Dialog与MessageBox行为调整.md)
- [Feature-20260409-0003 输入事件、hover 与遮挡交互调整](./功能变更/Feature-20260409-0003-输入事件与遮挡交互调整.md)
- [Feature-20260409-0004 测试用例与可观测性调整](./功能变更/Feature-20260409-0004-测试用例与可观测性调整.md)
- [Feature-20260409-0005 开发记录与模板体系整理](./功能变更/Feature-20260409-0005-开发记录与模板体系整理.md)
### 模块
- [Module-20260409-0001 Window 托管重绘与覆盖合成机制](./模块/Module-20260409-0001-Window托管重绘与覆盖合成机制.md)
## 覆盖关系
### 核心代码文件覆盖
- `Window.h / Window.cpp`
- 覆盖于:
- `BUG-20260409-0001`
- `Fix-BUG-20260409-0001`
- `Feature-20260409-0002`
- `Feature-20260409-0003`
- `Module-20260409-0001`
- `Control.h / Control.cpp`
- 覆盖于:
- `BUG-20260409-0002`
- `Fix-BUG-20260409-0002`
- `Feature-20260409-0001`
- `Module-20260409-0001`
- `Canvas.h / Canvas.cpp`
- 覆盖于:
- `BUG-20260409-0001`
- `Fix-BUG-20260409-0001`
- `Feature-20260409-0003`
- `Module-20260409-0001`
- `TabControl.h / TabControl.cpp`
- 覆盖于:
- `Feature-20260409-0003`
- `Feature-20260409-0004`
- `Module-20260409-0001`
- `Dialog.h / Dialog.cpp`
- 覆盖于:
- `BUG-20260409-0001`
- `Fix-BUG-20260409-0001`
- `Feature-20260409-0002`
- `Module-20260409-0001`
- `Button.cpp / TextBox.cpp`
- 覆盖于:
- `BUG-20260409-0001`
- `Fix-BUG-20260409-0001`
- `Feature-20260409-0003`
- `Table.cpp / Table.h / Label.cpp / MessageBox.cpp`
- 覆盖于:
- `BUG-20260409-0002`
- `Fix-BUG-20260409-0002`
- `Feature-20260409-0001`
- `Feature-20260409-0002`
- `z-testDome.cpp`
- 覆盖于:
- `Feature-20260409-0004`
## 当前状态
- 当前工作区在 `77a8fe5` 之后还有未提交修改。
- 这些未提交修改主要是:
- 第二阶段托管重绘
- 注释补充与校正
- 记录与模板体系整理
@@ -0,0 +1,73 @@
# BUG-20260409-0001
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260409-0001
- 标题: 对话框重绘、快照残留与遮挡交互异常
- 状态:已修复 / 待持续回归
- 严重性:S2
- 优先级:P0
- 模块:Window / Dialog / Canvas / Button / TextBox
- 版本 / 分支:`master`
- 环境:Windows + EasyX + VS2022
- 发现人:协作过程静态审查与测试用例回归
- 关联 Fix IDFix-BUG-20260409-0001
## 问题描述
- 现象:
- 非模态对话框打开后,底层被遮挡控件仍可能收到鼠标事件。
- 模态或非模态对话框在窗口尺寸变化后,可能出现背景快照残留。
- 模态对话框标题区域曾出现抓到底层背景的问题。
- 对话框关闭后,底层按钮 hover 状态可能不能立即恢复。
- 多个按钮快速 hover 切换时,对话框补画链容易闪烁。
- 影响范围:
- 对话框交互正确性
- 重绘层级稳定性
- hover 与点击行为一致性
- 期望结果:
- 对话框始终位于正确层级。
- 被对话框覆盖的底层控件不会误交互。
- resize / 打开 / 关闭过程中无残影、无错位。
- 实际结果:
- 曾出现穿透、残影、错误快照、关闭后 hover 残留、频繁补画闪烁等问题。
## 复现信息
- 前置条件:存在非模态或模态对话框,且下层有可交互控件
- 复现步骤:
1. 打开包含按钮、Canvas、TabControl 的测试窗口。
2. 弹出非模态或模态对话框。
3. 在对话框遮挡区域附近快速移动鼠标,或拖动窗口大小。
- 复现概率:高概率 / 偶现(不同问题不同)
- 最小复现 Demo`z-testDome.cpp``KEY == 2 / 3 / 4`
- 证据:日志、静态推演、用户回归观察
## 初步分析
- 疑似位置:
- `Dialog::handleEvent`
- `Dialog::draw / recenterInHostWindow / clearControls`
- `Window::runEventLoop / pumpResizeIfNeeded / flushManagedRepaint`
- `Button::handleEvent`
- 触发条件:
- 对话框覆盖底层控件
- 窗口 resize
- hover 高频切换
- 对话框打开/关闭后的层级切换
- 相关线索:
- “快照作废”和“回贴旧背景”语义混用
- 事件阶段与绘制阶段原先分离不彻底
- 非模态对话框区域内鼠标事件吞掉后,底层控件拿不到离开消息
## 跟踪信息
- 首次发现时间:本轮协作初期
- 最后更新时间:2026-04-09
- 修复版本:[当前工作区]
- 验证版本:[当前工作区]
- 备注:快速划过多个按钮时一帧内偶发双高亮的问题暂未彻底消除,属于已接受的底层限制
@@ -0,0 +1,71 @@
# BUG-20260409-0002
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260409-0002
- 标题: 基础控件生命周期与边界条件问题
- 状态:已修复 / 待持续回归
- 严重性:S2
- 优先级:P1
- 模块:Control / Window / Table / Label / Button / TextBox / MessageBox
- 版本 / 分支:`master`
- 环境:Windows + EasyX + VS2022
- 发现人:静态审查
- 关联 Fix IDFix-BUG-20260409-0002
## 问题描述
- 现象:
- `Window` 析构时可能先关闭图形环境,再让控件析构访问绘图接口。
- `Table` 在空表头、空数据、列数不一致时存在越界或空状态问题。
- `Label::setText()` 后尺寸与背景快照不能及时同步。
- `MessageBox` 外层曾直接干预 `Dialog` 初始化流程。
- 一些资源所有权和析构职责不够清晰。
- 影响范围:
- 基础稳定性
- 边界输入正确性
- 维护成本
- 期望结果:
- 生命周期安全、边界输入可防御、接口语义清晰
- 实际结果:
- 存在潜在崩溃、越界、状态不一致和维护负担
## 复现信息
- 前置条件:特定表格输入、动态修改文本、窗口关闭、MessageBox 使用链
- 复现步骤:
1. 静态审查相关代码路径。
2. 构造空表头、长于表头的数据行、动态变更 Label 文本。
3. 观察关闭窗口或销毁相关对象时的析构顺序。
- 复现概率:高概率 / 必现(不同问题不同)
- 最小复现 Demo[可选]
- 证据:静态分析、日志、编译验证
## 初步分析
- 疑似位置:
- `Window::~Window`
- `Control` 析构与背景快照恢复链
- `Table::initTextWaH / setData / drawHeader / drawTable`
- `Label::setText`
- `MessageBox::showAsync / showModal`
- 触发条件:
- 边界数据输入
- 动态文本变化
- 对象销毁 / 窗口关闭
- 相关线索:
- 所有权层次混乱
- 空状态防御不足
- 接口暴露超出真实职责
## 跟踪信息
- 首次发现时间:本轮协作初期
- 最后更新时间:2026-04-09
- 修复版本:[当前工作区]
- 验证版本:[当前工作区]
- 备注:字符集相关问题暂未处理,仍按 MBCS 假设工作
@@ -0,0 +1,93 @@
# Fix-BUG-20260409-0001
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260409-0001
- 关联 BUG ID: BUG-20260409-0001
- 修复目标: 收紧对话框快照、遮挡、事件分发和统一提交重绘链
- 状态:已完成 / 待持续回归
- 负责人:Codex 协作修改
- 分支 / 版本:`master`
## 根因分析
- 根因:
- 旧实现中“作废快照”和“回贴旧背景”混在一起,resize 时容易把旧画面贴回屏幕。
- 非模态对话框与底层控件之间缺少明确的事件阻断和 hover 清理机制。
- 重绘长期依赖控件即时局部绘制,导致底层先画、对话框后补,容易闪烁或层级错乱。
- 标题曾作为独立 `Label` 存在,导致额外一层背景快照,时序敏感。
- 触发条件:
- 非模态遮挡下的 hover / click
- 模态或非模态 resize
- 对话框打开 / 关闭
- 为什么之前没发现:
- 问题主要集中在“高频交互 + 对话框遮挡 + resize”组合路径
## 修复方案
- 修复思路:
- 拆分快照语义
- 收紧对话框事件消费
-`Dialog` 在窗口变化时只重新居中
- 将标题绘制并入 `Dialog`
- 把重绘推进到窗口统一收口
- 关键改动:
- `discardBackground()``invalidateBackgroundSnapshot()` 分离
- `Dialog` 标题改为直接绘制,不再使用独立 `Label`
- `Dialog::handleEvent()` 对自身区域内鼠标事件统一吞掉
- 对话框关闭及遮挡场景补发合成 `WM_MOUSEMOVE`,清理底层 hover
- `Window` 引入托管重绘,先重绘受影响 root,再补画相交对话框
- 涉及文件 / 类 / 函数:
- `Window.h / Window.cpp`
- `Dialog.h / Dialog.cpp`
- `Control.h / Control.cpp`
- `Canvas.cpp`
- `TabControl.cpp`
- `Button.cpp`
- `TextBox.cpp`
- 影响的 API / 行为:
- `Dialog` 在窗口变化时的语义固定为“只重新居中,不拉伸”
- 非模态对话框覆盖区域内不再允许鼠标事件穿透到底层
- 关键约束 / 不变量:
- 对话框始终绘制在普通控件之上
- 托管分发期间控件不直接提交绘制
- resize 和对话框开关仍走整场景重绘兜底
## 影响评估
- 影响范围:
- 所有包含 `Dialog`、按钮 hover、遮挡重绘的场景
- 兼容性影响:有(行为更严格,底层控件不再穿透响应)
- 行为变化:有(对话框 resize 只居中;遮挡区域 hover 不再透传)
- 性能影响:有正向变化(减少无意义整场景补画);同时在某些兜底场景仍保留整场景重绘
- 回归风险:
- 托管重绘 root 选择不当可能导致嵌套容器局部重绘异常
- 需持续回归 `Canvas / TabControl / Table / Dialog` 组合场景
## 验证结果
- 验证步骤:
1.`KEY == 2 / 4` 中打开非模态对话框,快速在遮挡区域附近 hover 和点击。
2.`KEY == 3 / 4` 中打开模态对话框并拖动窗口大小。
3. 关闭对话框后观察底层按钮 hover 恢复。
- 验证结果:
- 穿透、残影、关闭后 hover 不恢复等主问题已压住。
- 回归检查:
- 保留对“快速划过多个按钮时偶发一帧双高亮”的已知限制记录。
- 验证证据:
- 静态推演 + 多轮编译验证 + 用户回归反馈
## 落地信息
- Commit:
- `4a6e153`
- `7f8431a`
- `b07a4ec`
- 当前工作区未提交阶段
- PR[可选]
- 发布版本:[可选]
- 备注:该修复跨多个阶段逐步收敛,不是一次性完成
@@ -0,0 +1,88 @@
# Fix-BUG-20260409-0002
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260409-0002
- 关联 BUG ID: BUG-20260409-0002
- 修复目标: 收紧资源所有权,补齐基础边界防御,修正生命周期与接口语义问题
- 状态:已完成 / 待持续回归
- 负责人:Codex 协作修改
- 分支 / 版本:`master`
## 根因分析
- 根因:
- 部分对象持有关系不清晰,析构顺序对 EasyX 上下文敏感。
- `Table / Label` 等基础控件对空状态和动态变化处理不足。
- `MessageBox` 曾通过历史接口越权干预 `Dialog` 初始化。
- 触发条件:
- 析构时机
- 空数据 / 列数不一致
- 动态变更文本
- 为什么之前没发现:
- 很多问题集中在边界输入和静态结构设计,而非主路径功能
## 修复方案
- 修复思路:
- 收紧资源所有权
- 明确析构顺序
- 给空状态和列数不一致加防御
- 让接口更贴合真实职责
- 关键改动:
- `Window` 析构先清空控件树,再 `closegraph()`
- `Control / Window / Button / Table` 中一批资源改为明确所有权
- `Table` 增加空状态、列数规范化、分页下限保护、表高计算
- `Label::setText()` 重新计算尺寸并处理旧快照
- `MessageBox` 移除外层 `setInitialization(true)` 调用
- 涉及文件 / 类 / 函数:
- `Control.h / Control.cpp`
- `Window.h / Window.cpp`
- `Button.h / Button.cpp`
- `Table.h / Table.cpp`
- `Label.cpp`
- `MessageBox.cpp`
- `TextBox.cpp`
- 影响的 API / 行为:
- `Button` 回调 setter 语义收正常
- `MessageBox` 不再依赖外层强制初始化 `Dialog`
- 关键约束 / 不变量:
- 宿主图形上下文必须在控件销毁后再关闭
- 表格行数据必须与表头列数对齐
## 影响评估
- 影响范围:
- 基础控件稳定性与维护性
- 兼容性影响:低
- 行为变化:有(边界输入会被规范化,不再放任异常状态继续传播)
- 性能影响:无明显负担
- 回归风险:
- 主要集中在表格布局与分页按钮链路
## 验证结果
- 验证步骤:
1. 编译 `Window / Control / Table / Label / MessageBox / TextBox` 相关文件。
2. 逻辑推演空表头、长行数据、动态文本变化和窗口关闭链路。
3. 在现有测试用例中回归表格和消息框基本功能。
- 验证结果:
- 已消除确定性越界和明显生命周期风险。
- 回归检查:
- 字符集相关问题暂不在本次修复范围内。
- 验证证据:
- 静态审查 + 编译通过
## 落地信息
- Commit:
- `dde570a` 之后的早期工作区修改
- `4a6e153`
- `7f8431a`
- PR[可选]
- 发布版本:[可选]
- 备注:这部分修复多为基础治理,部分早期修改发生在仓库初始化之前
@@ -0,0 +1,73 @@
# 功能变更 ID: Feature-20260409-0001
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260409-0001
- 标题: 基础资源所有权与生命周期收口
- 状态:已完成
- 类型:修改
- 级别:L2 中等
- 模块:Control / Window / Button / Table / Label
- 版本 / 分支:`master`
- 环境:Windows + EasyX + VS2022
- 负责人:Codex 协作修改
## 变更背景
- 背景:
- 初始版本中一部分资源持有关系不明确,析构顺序和边界输入处理存在隐患。
- 目标:
- 提高基础稳定性与维护性。
- 不做什么:[可选]
- 不处理字符集体系切换问题。
## 变更内容
- 变更摘要:
- 收紧资源所有权,调整析构顺序,补齐基础控件边界防御。
- 新增项:[可选]
- `Control` 提供更明确的快照有效性查询和宿主窗口回溯能力。
- 修改项:[可选]
- `Window` 析构顺序调整
- `Table` 行数据规范化、空状态防御、页数保护
- `Label` 动态文本变化处理
- 删除 / 废弃项:[可选]
- `MessageBox` 外层强制 `Dialog` 初始化的做法被移除
- 受影响的文件 / 类 / 函数:
- `Control.h / Control.cpp`
- `Window.h / Window.cpp`
- `Button.h / Button.cpp`
- `Table.h / Table.cpp`
- `Label.cpp`
- `MessageBox.cpp`
- 对外 API / 属性变化:[可选]
- `Button` 回调设置接口语义更正常
## 行为对照
- 变更前:
- 生命周期和边界输入更依赖使用者自觉,异常输入容易直接进入绘制或析构链。
- 变更后:
- 基础控件会主动规范化一部分输入,生命周期顺序更安全。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 无强制迁移要求
## 验证与落地
- 验证方式:
- 静态审查 + 编译验证 + 现有测试用例回归
- 验证结果:
- 主要基础问题已收敛
- 关联 BUG / Fix[可选]
- `BUG-20260409-0002`
- `Fix-BUG-20260409-0002`
- Commit:
- 早期工作区修改 + `4a6e153` + `7f8431a`
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
- 其中一部分修改早于仓库基线
@@ -0,0 +1,71 @@
# 功能变更 ID: Feature-20260409-0002
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260409-0002
- 标题: Dialog 与 MessageBox 行为调整
- 状态:已完成
- 类型:修改
- 级别:L2 中等
- 模块:Dialog / MessageBox / Window
- 版本 / 分支:`master`
- 环境:Windows + EasyX + VS2022
- 负责人:Codex 协作修改
## 变更背景
- 背景:
- 初始版本中 `Dialog` 的初始化、resize、标题绘制和 `MessageBox` 外层调用关系较松散。
- 目标:
- 明确 `Dialog` 行为边界,让其在 resize、显示、关闭、标题更新等场景更稳定。
- 不做什么:[可选]
- 暂不重命名 `setInitialization()` 这类历史接口。
## 变更内容
- 变更摘要:
- `Dialog` 在窗口变化时只重新居中;标题直接由 `Dialog` 自身绘制;`MessageBox` 不再外层强制初始化。
- 新增项:[可选]
- `Dialog::recenterInHostWindow()`
- 修改项:[可选]
- `SetTitle / SetMessage / SetType` 触发重新布局
- `Dialog` 标题不再使用独立 `Label`
- `Dialog` 在模态和非模态场景中的重绘语义被收紧
- 删除 / 废弃项:[可选]
- `MessageBox::showAsync/showModal()` 中对 `setInitialization(true)` 的直接调用已移除
- 受影响的文件 / 类 / 函数:
- `Dialog.h / Dialog.cpp`
- `MessageBox.cpp`
- `Window.cpp`
- 对外 API / 属性变化:[可选]
- `Dialog` 在窗口变化时的对外可观察行为改为“只居中,不拉伸”
## 行为对照
- 变更前:
- `Dialog` resize 时可能重建行为与快照行为混杂,标题区域额外依赖子控件快照。
- 变更后:
- `Dialog` 尺寸由内容决定;窗口变化只重新居中;标题直接绘制,减少一层快照时序问题。
- 兼容性说明:部分兼容
- 迁移说明:[可选]
- 如果外部逻辑曾假设 `Dialog` 会跟随窗口拉伸,现在需要改按“仅居中”理解
## 验证与落地
- 验证方式:
- `KEY == 2 / 3 / 4` 相关场景静态推演与回归
- 验证结果:
- 标题更新、消息框初始化链、resize 后位置语义更稳定
- 关联 BUG / Fix[可选]
- `BUG-20260409-0001`
- `Fix-BUG-20260409-0001`
- Commit:
- `4a6e153`
- `7f8431a`
- 当前工作区部分未提交整理
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
@@ -0,0 +1,75 @@
# 功能变更 ID: Feature-20260409-0003
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260409-0003
- 标题: 输入事件、hover 与遮挡交互调整
- 状态:已完成
- 类型:修改
- 级别:L2 中等
- 模块:Window / Canvas / Button / TextBox / Dialog / TabControl
- 版本 / 分支:`master`
- 环境:Windows + EasyX + VS2022
- 负责人:Codex 协作修改
## 变更背景
- 背景:
- 初始版本中,hover、点击、遮挡与重叠控件之间的事件传递不够稳定。
- 目标:
- 建立更一致的鼠标事件消费规则,减少穿透、残留 hover 和无意义重绘。
- 不做什么:[可选]
- 暂不彻底解决“快速划过多按钮偶发一帧双高亮”的底层限制问题。
## 变更内容
- 变更摘要:
- 收紧 `Button / TextBox / Dialog` 的事件消费语义,并补上对话框关闭后及遮挡场景下的 hover 清理。
- 新增项:[可选]
- `Window::dispatchSyntheticMouseMoveToControls(...)`
- 修改项:[可选]
- `Button` 只在合理命中路径上吞掉鼠标事件
- `TextBox` 纯 hover 不再处理
- 非模态对话框覆盖区域内阻断底层穿透
- 对话框关闭后补发合成鼠标移动同步 hover
- 删除 / 废弃项:[可选]
- 大量 hover DEBUG 日志已降噪
- 受影响的文件 / 类 / 函数:
- `Window.cpp`
- `Canvas.cpp`
- `Button.cpp`
- `TextBox.cpp`
- `Dialog.cpp`
- `TabControl.cpp`
- 对外 API / 属性变化:[可选]
- 无新增公开 API,主要是行为语义变化
## 行为对照
- 变更前:
- 遮挡区域 hover/click 可能穿透;对话框关闭后底层 hover 可能残留。
- 变更后:
- 交互更偏向“命中即消费,关闭后同步清理 hover,遮挡区域不透传”。
- 兼容性说明:部分兼容
- 迁移说明:[可选]
- 若旧代码依赖非模态对话框下方控件还能收到事件,这种行为已被收紧
## 验证与落地
- 验证方式:
- `KEY == 2 / 3 / 4` 中对按钮、Tab、Table 分页、对话框关闭链做回归
- 验证结果:
- 主要交互一致性问题已收敛
- 关联 BUG / Fix[可选]
- `BUG-20260409-0001`
- `Fix-BUG-20260409-0001`
- Commit:
- `7f8431a`
- `b07a4ec`
- 当前工作区未提交阶段
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
@@ -0,0 +1,69 @@
# 功能变更 ID: Feature-20260409-0004
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260409-0004
- 标题: 测试用例与可观测性调整
- 状态:已完成
- 类型:修改
- 级别:L1 轻量
- 模块:z-testDome / 日志
- 版本 / 分支:`master`
- 环境:Windows + EasyX + VS2022
- 负责人:Codex 协作修改
## 变更背景
- 背景:
- 对话框、遮挡、hover、分页和页签的组合问题需要更有针对性的回归入口。
- 目标:
- 增加一组综合用例,并降低 DEBUG 日志噪音。
- 不做什么:[可选]
- 不改变原有 `KEY == 1 / 2 / 3` 的默认运行逻辑
## 变更内容
- 变更摘要:
- 新增 `KEY == 4` 的综合回归用例,覆盖对话框遮挡、Tab、Table、模态 resize 等场景。
- 新增项:[可选]
- `KEY == 4` 测试分支
- 修改项:[可选]
- hover 相关日志降噪
- 事件日志补充更清晰的事件名
- 删除 / 废弃项:[可选]
-
- 受影响的文件 / 类 / 函数:
- `z-testDome.cpp`
- `Window.cpp`
- `Canvas.cpp`
- `Button.cpp`
- 对外 API / 属性变化:[可选]
-
## 行为对照
- 变更前:
- 回归更多依赖手工拼场景,hover 日志容易刷屏。
- 变更后:
- 有固定综合回归入口,日志更适合看关键路径。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 测试 `KEY == 4` 需手动切换宏值
## 验证与落地
- 验证方式:
- 编译 `z-testDome.cpp`
- 静态推演 `KEY == 4` 控件创建、显示、对话框触发和关闭链
- 验证结果:
- 当前默认 `KEY == 2` 未被带坏,`KEY == 4` 逻辑链自洽
- 关联 BUG / Fix[可选]
- 可作为 `BUG-20260409-0001` 相关问题的回归入口
- Commit:
- 当前工作区未提交阶段
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
@@ -0,0 +1,69 @@
# 功能变更 ID: Feature-20260409-0005
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260409-0005
- 标题: 开发记录与模板体系整理
- 状态:已完成
- 类型:修改
- 级别:L1 轻量
- 模块:文档模板 / 开发记录
- 版本 / 分支:`master`
- 环境:本地工作区
- 负责人:Codex 协作修改
## 变更背景
- 背景:
- 需要对本轮协作中的大量修改建立可持续维护的记录体系。
- 目标:
- 将“问题、修复、功能变化、模块设计”这四类信息分层记录,避免后续继续靠聊天上下文回忆。
- 不做什么:[可选]
- 不把模板设计成覆盖所有场景的一份大而全文档
## 变更内容
- 变更摘要:
- 重写四个文档模板,修正编码内容,并建立统一的开发记录目录。
- 新增项:[可选]
- `开发记录/BUG`
- `开发记录/Fix`
- `开发记录/功能变更`
- `开发记录/模块`
- 修改项:[可选]
- `文档模板` 目录下四个模板重写并重新划分职责
- 删除 / 废弃项:[可选]
- 原模板中“功能变更”和“模块说明”职责重叠的写法已废弃
- 受影响的文件 / 类 / 函数:
- `文档模板/*.md`
- `开发记录/*.md`
- 对外 API / 属性变化:[可选]
-
## 行为对照
- 变更前:
- 模板存在乱码,功能变更和模块说明边界不清,小改动记录成本过高。
- 变更后:
- 模板职责明确,记录粒度更合理,可以持续沉淀协作历史。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 后续新增记录优先按新模板写,不再沿用旧模板内容
## 验证与落地
- 验证方式:
- 读取模板文件确认编码与内容正常
- 创建第一批记录文件验证模板可用
- 验证结果:
- 模板可正常使用,第一批记录已按新体系落地
- 关联 BUG / Fix[可选]
-
- Commit:
- 当前工作区未提交阶段
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
@@ -0,0 +1,134 @@
# 新增功能模块 / 模块重构
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
## 基本信息
- 模块 IDModule-20260409-0001
- 模块名称:Window 托管重绘与覆盖合成机制
- 状态:已完成 / 待持续回归
- 类型:架构演进
- 所属系统 / 子系统:Window / Control / Dialog 重绘链
- 版本 / 分支:`master`
- 环境:Windows + EasyX + VS2022
- 负责人:Codex 协作修改
## 背景与目标
- 背景:
- 初始框架以控件局部即时重绘为主,非模态对话框与底层控件重叠时,容易出现底层先画、对话框后补的层级问题。
- 当前痛点:
- 对话框遮挡下的 hover / click / tooltip / 分页等交互容易闪烁
- 底层控件和上层对话框在事件里分离绘制,顺序不可控
- 目标:
- 将“事件改状态”和“绘制提交”拆开,由 `Window` 在事件尾统一提交重绘。
- 非目标:[可选]
- 暂不做真正像素级脏区裁剪
- 暂不统一布局系统
## 模块边界
- 职责:
-`Window` 主循环里托管控件重绘请求
- 选择安全的重绘 root
- 在提交阶段维护普通控件与非模态 `Dialog` 的正确层级
- 不负责什么:
- 模态 `Dialog` 自己的内部事件循环
- 布局语义统一
- 字符集问题
- 外部依赖:
- EasyX 绘制接口
- 各控件自身的快照恢复能力
- 对外能力 / API:
- `Window::requestManagedRepaint(Control* source)`
- `Window::flushManagedRepaint()`
- `Control::getManagedRepaintRoot()`
- `Control::commitManagedRepaint()`
- 关键数据 / 状态:
- `managedDispatchActive`
- `managedSceneDirty`
- `managedRepaintItems`
## 设计说明
- 核心流程:
1. `Window::runEventLoop()` 开始输入分发前,进入 `managedDispatchActive = true`
2. 控件在 `handleEvent()` 中只改状态并通过 `requestRepaint()` 上报
3. `Window::requestManagedRepaint(source)` 把 source 转换成 `root + coverage`
4. 事件收口时 `flushManagedRepaint()` 按层级提交 root
5. 最后补画和脏区相交的非模态 `Dialog`
- 关键对象 / 类关系:
- `Window`:重绘调度中心
- `Control`:提供宿主窗口回溯与 root 选择语义
- `Canvas / TabControl / Dialog`:作为安全重绘 root 的主要容器
- 生命周期:
- 托管重绘项只存在于当前分发轮次
- `clearManagedRepaintState()` 在 flush、resize 收口、对话框开关后的整场景重绘后清空
- 事件 / 渲染 / 数据流:
- 事件:Dialog 优先,普通控件其次
- 渲染:受影响普通 root 先画,Dialog 后补
- 数据流:leaf control -> requestRepaint -> Window registry -> flush
- 关键不变量:
- 分发期不直接提交托管重绘
- 顶层普通控件的提交顺序必须服从 `Window::controls` 的 z-order
- 对话框始终绘制在普通控件之上
- 降级 / 回退策略:[可选]
- `resize`、对话框打开/关闭等全局变化仍走整场景重绘
## 实现与影响
- 关键实现点:
- `Control` 增加 `hostWindow``getManagedRepaintRoot()``commitManagedRepaint()`
- `Window` 增加 `ManagedRepaintItem` 队列
- `Canvas / TabControl / Dialog` 提供局部可提交与升级为整 root 重画的判断
- 涉及文件 / 类 / 函数:
- `Window.h / Window.cpp`
- `Control.h / Control.cpp`
- `Canvas.h / Canvas.cpp`
- `TabControl.h / TabControl.cpp`
- `Dialog.h / Dialog.cpp`
- 兼容性影响:
- 行为层面更严格,底层控件不再依赖旧的“即时局部重绘”副作用
- 性能影响:
- 相比整场景重绘更细
- 相比纯即时局部重绘更稳定
- 在全局变化场景仍保留整场景兜底
- 风险点:
- root 选择错误会导致局部重绘不完整
- 嵌套容器组合场景需持续回归
## 测试与验证
- 测试范围:
- 非模态遮挡下的按钮 hover / click
- `Canvas / TabControl / Table` 组合场景
- 模态与非模态对话框 coexist 场景
- 验证步骤:
1. 编译 `Control / Canvas / TabControl / Dialog / Window` 相关源文件。
2. 回归 `KEY == 2 / 3 / 4` 的对话框与遮挡场景。
3. 检查对话框关闭、resize、hover 清理和局部重绘顺序。
- 验证结果:
- 第二阶段架构已落地,核心编译通过,主问题链路已被压住。
- 已知限制 / 遗留问题:[可选]
- 快速划过多个按钮时偶发一帧双高亮,当前接受为底层限制
- 仍未推进到真正像素级脏区裁剪
## 落地信息
- 关联功能变更 ID[可选]
- `Feature-20260409-0002`
- `Feature-20260409-0003`
- 关联 BUG / Fix[可选]
- `BUG-20260409-0001`
- `Fix-BUG-20260409-0001`
- Commit:
- `b07a4ec`
- `77a8fe5`
- 当前工作区未提交阶段
- PR[可选]
- 发布版本:[可选]
- 相关文档:[可选]
- `00-修改总览-20260409.md`
+51
View File
@@ -0,0 +1,51 @@
# BUG-YYYYMMDD-XXXX
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID:
- 标题:
- 状态:待修复 / 修复中 / 已修复 / 已验证 / 已关闭
- 严重性:S1 / S2 / S3 / S4
- 优先级:P0 / P1 / P2 / P3
- 模块:
- 版本 / 分支:
- 环境:
- 发现人:
- 关联 Fix ID[可选]
## 问题描述
- 现象:
- 影响范围:
- 期望结果:
- 实际结果:
## 复现信息
- 前置条件:[可选]
- 复现步骤:
1.
2.
3.
- 复现概率:必现 / 高概率 / 偶现 / 低概率
- 最小复现 Demo[可选]
- 证据:截图 / 日志 / 调用栈 / 录屏 / 断点观察
## 初步分析
- 疑似位置:
- 触发条件:
- 相关线索:
- 最近相关改动:[可选]
## 跟踪信息
- 首次发现时间:
- 最后更新时间:
- 修复版本:[可选]
- 验证版本:[可选]
- 备注:[可选]
+55
View File
@@ -0,0 +1,55 @@
# Fix-BUG-YYYYMMDD-XXXX
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID:
- 关联 BUG ID:
- 修复目标:
- 状态:设计中 / 开发中 / 已完成 / 已验证 / 已回滚
- 负责人:
- 分支 / 版本:
## 根因分析
- 根因:
- 触发条件:
- 为什么之前没发现:[可选]
- 关键证据:[可选]
## 修复方案
- 修复思路:
- 关键改动:
- 涉及文件 / 类 / 函数:
- 影响的 API / 行为:[可选]
- 关键约束 / 不变量:[可选]
- 回滚点 / 开关:[可选]
## 影响评估
- 影响范围:
- 兼容性影响:无 / 有(说明)
- 行为变化:无 / 有(说明)
- 性能影响:无 / 有(说明)
- 回归风险:
## 验证结果
- 验证步骤:
1.
2.
3.
- 验证结果:
- 回归检查:[可选]
- 验证证据:[可选]
## 落地信息
- Commit:
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
@@ -0,0 +1,48 @@
# 功能变更 ID: Feature-YYYYMMDD-XXXXX
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID:
- 标题:
- 状态:设计中 / 开发中 / 已完成 / 已验证 / 已发布
- 类型:新增 / 修改 / 删除 / 废弃 / 重命名
- 级别:L1 轻量 / L2 中等 / L3 重大
- 模块:
- 版本 / 分支:
- 环境:
- 负责人:
## 变更背景
- 背景:
- 目标:
- 不做什么:[可选]
## 变更内容
- 变更摘要:
- 新增项:[可选]
- 修改项:[可选]
- 删除 / 废弃项:[可选]
- 受影响的文件 / 类 / 函数:
- 对外 API / 属性变化:[可选]
## 行为对照
- 变更前:
- 变更后:
- 兼容性说明:兼容 / 部分兼容 / 不兼容
- 迁移说明:[可选]
## 验证与落地
- 验证方式:
- 验证结果:
- 关联 BUG / Fix[可选]
- Commit:
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
+68
View File
@@ -0,0 +1,68 @@
# 新增功能模块 / 模块重构
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
## 基本信息
- 模块 IDModule-YYYYMMDD-XXXXX
- 模块名称:
- 状态:设计中 / 开发中 / 已完成 / 已验证 / 已发布
- 类型:新增模块 / 模块重构 / 架构演进
- 所属系统 / 子系统:
- 版本 / 分支:
- 环境:
- 负责人:
## 背景与目标
- 背景:
- 当前痛点:
- 目标:
- 非目标:[可选]
## 模块边界
- 职责:
- 不负责什么:
- 外部依赖:
- 对外能力 / API:
- 关键数据 / 状态:
## 设计说明
- 核心流程:
- 关键对象 / 类关系:
- 生命周期:
- 事件 / 渲染 / 数据流:[按模块类型填写]
- 关键不变量:
- 降级 / 回退策略:[可选]
## 实现与影响
- 关键实现点:
- 涉及文件 / 类 / 函数:
- 兼容性影响:
- 性能影响:
- 风险点:
## 测试与验证
- 测试范围:
- 验证步骤:
1.
2.
3.
- 验证结果:
- 已知限制 / 遗留问题:[可选]
## 落地信息
- 关联功能变更 ID[可选]
- 关联 BUG / Fix[可选]
- Commit:
- PR[可选]
- 发布版本:[可选]
- 相关文档:[可选]