14 Commits

Author SHA1 Message Date
霜霜 e4851e16f4 Update version 2.1.0 release date in CHANGELOG
Updated release date for version 2.1.0 and clarified features.
2025-10-27 15:00:44 +08:00
Ysm-04 dcf13895da feat: add a new awesome feature 2025-10-27 14:59:29 +08:00
Ysm-04 95149238e2 feat: add a new awesome feature 2025-10-03 16:34:58 +08:00
Ysm-04 3509b2bc39 feat: add a new awesome feature 2025-10-01 00:57:41 +08:00
Ysm-04 26f30cee39 feat: add a new awesome feature 2025-09-30 19:26:01 +08:00
Ysm-04 741987e48b feat: add a new awesome feature 2025-09-22 20:39:34 +08:00
Ysm-04 9226deaf85 feat: add a new awesome feature 2025-09-22 16:51:14 +08:00
Ysm-04 9f2b175b17 feat: add a new awesome feature 2025-09-22 15:08:49 +08:00
Ysm-04 bd4588731b feat: add a new awesome feature 2025-09-08 01:23:02 +08:00
Ysm-04 0522bffa1b feat: add a new awesome feature 2025-09-07 22:26:23 +08:00
Ysm-04 49ecb65514 feat: add a new awesome feature 2025-09-07 10:35:39 +08:00
Ysm-04 0a9e290d42 initial commit 2025-09-07 00:25:30 +08:00
Ysm-04 a29b4371fb initial commit 2025-09-07 00:22:33 +08:00
Ysm-04 d62d8a90d6 initial commit 2025-09-07 00:19:40 +08:00
107 changed files with 7047 additions and 13978 deletions
+25 -12
View File
@@ -1,16 +1,29 @@
# 编译输出目录
/build/
/bin/
/lib/
# CMake 生成文件
/CMakeFiles/
/CMakeCache.txt
/cmake_install.cmake
/Makefile
# Visual Studio 文件
.vs/
x64/
.codex-temp/
*.sln
*.vcxproj
*.vcxproj.filters
*.vcxproj.user
# 编译产物
*.obj
*.exe
*.pdb
*.ilk
*.iobj
*.ipdb
*.tlog/
*.log
*.recipe
*.idb
*.lastbuildstate
*.VC.db
*.VC.VC.opendb
*.exe
*.dll
*.lib
# 临时文件
*.cache
*.zip
+685
View File
@@ -0,0 +1,685 @@
# 中文 API 文档
[English API Documentation](English API Documentation.md)
下面是 **StellarX GUI 框架** 各主要类和函数的 API 文档。每个类的用途、接口、参数和返回值等详细信息如下:
### Control 类 (抽象基类)
**描述:** `Control` 是所有控件的抽象基类,定义了通用的属性和接口,包括位置、尺寸、重绘标记等。它提供了绘图状态保存与恢复机制,确保控件绘制不影响全局状态。`Control` 本身不能直接实例化。
- **主要属性:**
- `x, y`:控件左上角坐标。
- `width, height`:控件宽度和高度。
- `dirty`:是否需要重绘的标志。
- `show`:控件是否可见。
- `rouRectangleSize``StellarX::RouRectangle` 结构,保存圆角矩形控件圆角的宽高半径。
- **注意:** `Control` 会在内部维护当前绘图状态(字体、颜色、线型等)的备份指针,用于绘制前保存状态、绘制后恢复。
- **主要接口方法:**
- `virtual void draw() = 0;`
**描述:** 纯虚函数,绘制控件内容。具体控件类需实现自己的绘制逻辑。
- `virtual bool handleEvent(const ExMessage& msg) = 0;`
**描述:** 纯虚函数,处理事件消息(如鼠标、键盘事件)。返回值表示事件是否被该控件消费(`true` 则不再向其他控件传播)。
- `void saveBackground(int x, int y, int w, int h);`
**描述:** 保存控件区域 `(x, y, w, h)` 的背景图像快照,以便需要时恢复背景。常用于控件需要暂时覆盖背景(如弹出框)。
- `void restBackground();`
**描述:** 恢复最近保存的背景快照,将之前保存的背景图像绘制回控件原位置。典型用于控件隐藏或重绘前清除旧内容。
- `void discardBackground();`
**描述:** 丢弃当前保存的背景快照并释放相关资源。当窗口重绘或尺寸变化导致背景失效时调用,避免错误使用旧快照。
- *属性访问和设置:*
- `int getX() const, getY() const, getWidth() const, getHeight() const` 等:获取控件位置和尺寸各属性。
- `int getRight() const, getBottom() const`:获取控件右边界和下边界坐标(`x + width``y + height`)。
- `void setX(int nx), setY(int ny), setWidth(int w), setHeight(int h)`:设置控件位置或尺寸,对应属性改变后自动将控件标记为需要重绘 (`dirty = true`)。
- `void setDirty(bool d)`:手动设置控件的重绘标志。
- `void setShow(bool visible)`:设置控件可见性。如果设为 `false`,则控件将不绘制自身内容。
- `bool isVisible() const`:返回控件当前可见状态(`show` 标志)。**注意:** 控件被标记为隐藏时,其 `draw()` 通常不会被调用。
- *其他:*
- `virtual bool model() const = 0;`
**描述:** 纯虚函数,用于检查控件是否为“模态”。仅对话框控件需要实现(模态对话框返回 `true`),其他非对话框控件可忽略(返回 `false`)。窗口事件循环根据此函数区分模态对话框与普通控件的事件处理优先级。
- `void saveStyle(), restoreStyle();` *(受保护方法)*
**描述:** 保存当前全局绘图样式(字体、颜色、线型等)并恢复。控件在 `draw()` 中应先调用 `saveStyle()` 备份当前样式,绘制完毕后调用 `restoreStyle()` 以免影响其他绘制操作。
### Window 类 (应用主窗口)
**描述:** `Window` 类表示应用程序的主窗口,负责窗口的创建、消息循环、控件管理和整体渲染。一个应用通常创建一个 Window 实例作为 GUI 的根容器。
- **构造函数:**
- `Window(int width, int height, int mode, COLORREF bkColor = ..., std::string headline = "窗口")`
创建一个指定宽、高的主窗口。`mode` 参数指定图形模式(如是否双缓冲,EasyX 使用 NULL 或 `INIT_RENDERMANUAL` 等模式),`bkColor` 是窗口背景色,`headline` 是窗口标题文本。构造时不会立即显示窗口,需调用 `draw()`
- **主要方法:**
- `void draw();`
**描述:** 初始化并显示窗口,创建绘图界面。将使用设定的模式创建绘图窗口(调用 EasyX 的 `initgraph`),并应用窗口标题和背景色。然后绘制已添加的控件。通常在创建 Window 后立即调用一次。
- `void draw(std::string pImgFile);`
**描述:** 与无参版本类似,但使用指定路径的图像文件作为窗口背景图。该方法会加载图像并按照窗口尺寸绘制为背景,然后再绘制子控件。
- `void runEventLoop();`
**描述:** 进入窗口消息处理循环。该循环持续获取用户输入事件 (`peekmessage`) 并将其分发给子控件或对话框处理:
- 优先将事件传递给所有已打开的非模态对话框(`Dialog``model() == false``isVisible() == true`)。一旦某个对话框的 `handleEvent` 返回 `true`(事件被消费),停止继续传递。
- 若事件未被对话框消费,再按添加顺序将事件传递给普通控件列表中的各控件的 `handleEvent`(通常 Container 控件会向其子控件继续传播)。
- 处理完事件后,如果有对话框正在显示(或刚关闭),或者 `dialogClose` 标志为真,则强制重绘一帧界面:包括所有普通控件和对话框,以确保界面更新。
- 循环每帧暂停 10 毫秒,防止 CPU 占用过高。
- **注意:** 该函数在调用后不会返回,直到窗口接收到关闭消息 (`WM_CLOSE`),此时函数内部会跳出循环结束。
- `void setBkImage(IMAGE* img)` / `void setBkImage(std::string filePath);`
**描述:** 更改窗口背景图像。可以直接传入一个已加载的 `IMAGE` 指针,或提供图像文件路径让函数内部加载。调用此函数会触发窗口重绘当前所有控件和对话框以应用新的背景。
- `void setBkcolor(COLORREF c);`
**描述:** 设置窗口背景颜色,并立即用该颜色清除屏幕背景(不销毁已有背景图,仅覆盖绘制)。
- `void setHeadline(std::string title);`
**描述:** 设置窗口标题文本。窗口创建后可随时更改标题;如果窗口已显示,则通过 WinAPI `SetWindowText` 立即更新窗口标题栏。
- `void addControl(std::unique_ptr<Control> control);`
**描述:** 向窗口添加一个普通控件。Window 维护一个控件列表,`draw()``runEventLoop()` 会据此绘制及派发事件。此方法接收智能指针,调用后 Window 接管控件的生命周期。
- `void addDialog(std::unique_ptr<Control> dialog);`
**描述:** 向窗口添加一个对话框控件(一般为 `Dialog` 类型)。对话框与普通控件分开管理,其事件和绘制有独立逻辑。
- `bool hasNonModalDialogWithCaption(const std::string& caption) const;`
**描述:** 检查当前是否已有**非模态**对话框,其标题(caption)与给定字符串匹配。如果存在返回 `true`。通常用于避免重复打开相同对话框。
**重载版本:** `bool hasNonModalDialogWithCaption(const std::string& caption, const std::string& text) const;`
除了标题,还同时比较对话框内容文本,以更严格地判断重复。`MessageBox::showAsync` 内部使用此方法防止弹出重复的提示框。
- *信息获取方法:*
- `HWND getHwnd() const;` 获取底层窗口句柄(EasyX 初始化返回的 HWND)。
- `int getWidth() const, getHeight() const;` 获取窗口宽度和高度。
- `std::string getHeadline() const;` 获取当前窗口标题。
- `COLORREF getBkcolor() const;` 获取窗口背景色。
- `IMAGE* getBkImage() const;` 获取当前窗口背景图像对象指针(如果有)。
- `std::vector<std::unique_ptr<Control>>& getControls();` 引用返回窗口维护的控件列表(可以对其遍历,但一般不手动修改)。
### Canvas 类 (容器控件)
**描述:** `Canvas` 是一种可包含子控件的画布容器,用于将其他控件分组、统一背景和边框样式,以及实现复杂布局。Canvas 自身也是一个控件,继承自 `Control`
- **特性:**
- 支持四种矩形形状背景(普通矩形/圆角矩形,各有无边框和有边框版本),可通过 `setShape` 设置。
- 可自定义容器背景颜色 (`canvasBkColor`)、边框颜色 (`canvasBorderColor`)、边框线型 (`canvasLineStyle`)、填充模式 (`canvasFillMode`,纯色/图案/无填充) 等。
- Canvas 能自动管理子控件的生命周期(使用 `addControl` 添加的子控件在 Canvas 销毁时自动释放)。
- Canvas 会在绘制时遍历并绘制所有添加的子控件,并在处理事件时优先将事件传递给子控件列表中最后添加的控件(Z 顺序)。
- **重要数据成员:**
- `std::vector<std::unique_ptr<Control>> controls;` 子控件列表。
- `StellarX::ControlShape shape;` 容器背景形状(默认矩形)。
- `StellarX::FillMode canvasFillMode;` 背景填充模式(默认实色填充)。
- `StellarX::LineStyle canvasLineStyle;` 边框线型(默认实线)。
- `int canvaslinewidth;` 边框线宽像素值。
- `COLORREF canvasBorderColor, canvasBkColor;` 容器边框颜色与背景色。
- `StellarX::LayoutKind Kind;` (预留)布局管理类型,当前未深入实现,可用于标识布局策略(如绝对布局、水平/垂直布局等)。
- **注意:** Canvas 重载了 `isVisible()` 始终返回 false,因为 Canvas 通常不作为独立可见单元(事件循环中不直接处理 Canvas,而由其子控件处理事件)。但这不影响 Canvas 作为容器的绘制和子控件事件派发。
- **主要方法:**
- `Canvas(); Canvas(int x, int y, int w, int h);`
**描述:** 构造一个 Canvas 容器,可以指定位置 `(x,y)` 及初始尺寸。
- `void addControl(std::unique_ptr<Control> control);`
**描述:** 添加子控件到 Canvas。被添加控件将成为 Canvas 内容的一部分,其坐标相对于 Canvas 左上角(Canvas 并不主动调整子控件位置,需在添加前设置好子控件位置)。添加后 Canvas 会将自身标记为需要重绘。
- `void setShape(StellarX::ControlShape shape);`
**描述:** 设置 Canvas 背景形状。支持 `RECTANGLE``B_RECTANGLE`(无边框矩形)、`ROUND_RECTANGLE`(圆角矩形)、`B_ROUND_RECTANGLE`(无边框圆角矩形)。如果传入圆形或椭圆形(CIRCLE/ELLIPSE等),Canvas 不支持,将自动按矩形处理。
- `void setCanvasFillMode(StellarX::FillMode mode);`
**描述:** 设置背景填充模式,如纯色 (`Solid`)、无填充 (`Null`)、预定义图案填充 (`Hatched`) 或自定义图案/图像填充 (`Pattern/DibPattern`),需结合 `setfillstyle` 使用。默认为纯色。
- `void setBorderColor(COLORREF color);` / `void setCanvasBkColor(COLORREF color);` / `void setCanvasLineStyle(StellarX::LineStyle style);` / `void setLinewidth(int width);`
**描述:** 分别设置容器边框颜色、背景色、边框线型和线宽。这些改变都会使 Canvas 需要重绘。
- `void draw() override;`
**描述:** 绘制 Canvas 背景和其所有子控件。Canvas 绘制步骤:
1. 根据设定的背景颜色/填充模式和形状绘制容器背景(例如填充矩形或圆角矩形区域)。
2. 遍历子控件列表,调用每个子控件的 `draw()` 方法绘制它们。绘制前对每个子控件先设置其 `dirty = true`,确保子控件按需重绘。
3. 恢复绘图样式,标记自身为已绘制(`dirty = false`)。
*实现细节:* Canvas 使用 `saveStyle()/restoreStyle()` 保护全局绘图状态,并依赖 EasyX 的填充/绘制函数根据 `shape` 绘制矩形或圆角矩形背景。
- `bool handleEvent(const ExMessage& msg) override;`
**描述:** 将事件按照子控件的添加顺序逆序传递(后添加的子控件先处理)。对 `controls` 列表从末尾向前调用每个子控件的 `handleEvent`,若某子控件返回 `true`(表示事件已被处理),则停止传递并返回 `true`;若所有子控件都未消费事件,则返回 `false` 表示未处理。
*用途:* 使得堆叠在顶层的子控件(通常列表后面的)有更高优先权处理事件,例如多个重叠控件的情形。
- `void clearAllControls();` *(protected 方法)*
**描述:** 清除并移除所有子控件。会释放智能指针列表中所有控件对象。一般在容器销毁或重置时使用。普通情况下不直接调用,由容器析构自动完成。
### Label 类 (静态文本标签)
**描述:** `Label` 用于显示一段静态文本,可设置文字颜色、背景透明/不透明及样式等。Label 不处理用户输入事件(纯展示)。
- **构造函数:**
- `Label(); Label(int x, int y, std::string text = "标签", COLORREF textColor = BLACK, COLORREF bkColor = WHITE);`
**描述:** 创建一个 Label 控件。可指定初始位置 `(x,y)`、显示文本内容和文本颜色、背景色等。默认文本为“标签”,默认文字黑色、背景白色。
- **主要属性:**
- `std::string text;` 显示的文字内容。
- `COLORREF textColor;` 文本颜色。
- `COLORREF textBkColor;` 文本背景色(在背景不透明时生效)。
- `bool textBkDisap;` 是否背景透明显示文本。若为 `true`,Label 绘制文字时使用透明背景模式。
- `StellarX::ControlText textStyle;` 文字样式结构,包括字体、高度、粗细等属性(继承自 Control 默认值,例如 `字体:微软雅黑`)。
- **注意:** Label 大小 (`width, height`) 初始为 0,通常 Label 大小根据文本自动适应,无需手动设置宽高。Label 绘制时会截取背景图像以覆盖文字区域刷新。
- **主要方法:**
- `void setTextdisap(bool transparent);`
**描述:** 设置文本背景是否透明显示。传入 `true` 则文字背景透明,显示时不遮挡底下背景;`false` 则文字背景为 `textBkColor` 实色矩形。
- `void setTextColor(COLORREF color);` / `void setTextBkColor(COLORREF color);`
**描述:** 设置文字颜色或背景色。更改后会标记 Label 需要重绘。
- `void setText(std::string text);`
**描述:** 更改显示的文本内容,并标记需要重绘。支持中文字符串。
- `void draw() override;`
**描述:** 绘制文本标签。根据 `textBkDisap` 决定调用 EasyX 的 `setbkmode(TRANSPARENT)``OPAQUE`,并设置背景色 `setbkcolor(textBkColor)`。然后设置文本颜色和样式,调用 `outtextxy(x, y, text)` 输出文字。为防止文字重绘留痕迹,`Label::draw` 会先保存文字区域背景 (`saveBackground`),绘制完成后再恢复样式。首次绘制后将 `dirty` 置为 false。
- `bool handleEvent(...) override;`
**描述:** Label 不处理任何事件,始终返回 false。
- `void hide();`
**描述:** 将 Label 内容隐藏。其实现是调用 `restBackground()` 恢复先前保存的背景,然后 `discardBackground()` 释放快照,并将 `dirty` 设为 false。这通常在 Label 用作 Tooltip 提示框时,由宿主控件调用,用于移除提示文本的显示而不重绘整个界面。
- **使用场景:** Label 常用于界面中的静态说明文字、标题、状态栏信息等。通过配合 `textStyle` 可以修改字体、大小、是否加粗等。默认情况下 Label 背景不透明白底,如需与窗口背景融合可调用 `setTextdisap(true)` 使背景透明。
### Button 类 (按钮控件)
**描述:** `Button` 提供普通按钮和切换按钮(两态)的功能,可响应点击和悬停事件。支持设置各种样式(形状、颜色)和点击回调,是交互的主要控件之一。
- **工作模式:** 由 `StellarX::ButtonMode` 枚举定义:
- `NORMAL` 普通按钮:每次点击都会触发一次动作,按钮本身不保持按下状态。
- `TOGGLE` 切换按钮:每次点击切换自身状态(选中/未选中),并触发不同的回调。
- `DISABLED` 禁用按钮:不响应用户点击,显示为灰色且文字带删除线。
- 可通过 `setButtonMode(ButtonMode mode)` 改变按钮模式。
- **外观形状:** 由 `StellarX::ControlShape` 定义:
- 支持矩形 (`RECTANGLE`/`B_RECTANGLE`)、圆角矩形 (`ROUND_RECTANGLE`/`B_ROUND_RECTANGLE`)、圆形 (`CIRCLE`/`B_CIRCLE`)、椭圆 (`ELLIPSE`/`B_ELLIPSE`) 八种形状(B_ 前缀表示无边框填充)。通过 `setButtonShape(ControlShape shape)` 设置按钮形状。
- 注意:在切换 `ControlShape` 时,某些尺寸下圆形/椭圆以控件的 `width``height` 计算,圆形半径取较小的半径,椭圆按照矩形外接框绘制。
- **主要可配置属性:**
- 颜色:`buttonTrueColor`(被点击时颜色),`buttonFalseColor`(常态颜色),`buttonHoverColor`(鼠标悬停颜色),`buttonBorderColor`(边框颜色)。
- 填充:`buttonFillMode`(填充模式,实色/图案/图像等),`buttonFillIma`(图案填充样式,如果 `FillMode == Hatched`),`buttonFileIMAGE`(指向自定义填充图像的指针,如果 `FillMode == DibPattern`)。
- 文本:按钮显示的文字 `text` 及其样式 `textStyle`(字体、字号、颜色等)。另外提供 `cutText` 字符串用于文本过长时显示裁剪的内容。
- Tooltip`tipEnabled`(是否启用悬停提示),`tipTextClick`NORMAL模式提示文本),`tipTextOn/Off`(TOGGLE模式下两种状态的提示文本),`tipFollowCursor`(提示框是否跟随鼠标移动),`tipDelayMs`(提示延迟毫秒),`tipOffsetX/Y`(提示框相对位置偏移),`tipLabel`(内部使用的 Label 控件呈现提示文本)。
- 圆角大小:通过继承自 `Control``rouRectangleSize` 成员控制圆角矩形圆角的半径大小。提供 `setRoundRectangleWidth(int)``setRoundRectangleHeight(int)` 接口修改。
- **主要方法:**
- `void setOnClickListener(function<void()>&& callback);`
**描述:** 设置按钮普通点击时的回调函数。当按钮模式为 NORMAL,在每次鼠标释放点击时调用该回调;当模式为 TOGGLE,则仅在每次点击状态发生变化时调用(取决于 toggle 逻辑)。
- `void setOnToggleOnListener(...)` / `void setOnToggleOffListener(...);`
**描述:** 设置按钮在 TOGGLE 模式下由未选中变为选中、以及由选中变为未选中时分别触发的回调函数。当按钮模式为 NORMAL 或 DISABLED 时,这两个回调不会被调用。
- `void setButtonText(const char* text)` / `void setButtonText(std::string text);`
**描述:** 更改按钮上的显示文本。支持 C 风格字符串或 `std::string`。更改文本后会重新计算文本宽高,并标记按钮需要重绘。
- `void setButtonBorder(COLORREF border)` / `void setButtonFalseColor(COLORREF color)`
**描述:** 设置按钮边框颜色、常态下背景填充颜色。
- `void setFillMode(FillMode mode)` / `void setFillPattern(FillStyle pattern)` / `void setFillImage(const std::string& path)`
**描述:** 设置按钮背景填充方式:
- `setFillMode` 改变填充模式(纯色、图案、位图等)。
- 若填充模式为图案 (`Pattern`),需调用 `setFillPattern` 设置具体的图案样式 (`StellarX::FillStyle`)。
- 若填充模式为位图 (`DibPattern`),可调用 `setFillImage` 加载指定路径的图像并用作填充(会自动调整图像大小与按钮一致)。
- `void setButtonClick(bool click);`
**描述:** 外部设置按钮的“点击状态”。对于 TOGGLE 模式,`setButtonClick(true)` 会将按钮设为选中状态并触发 onToggleOn 回调,`false` 则触发 onToggleOff 回调。对于 NORMAL 模式,传入 true 将临时触发一次 onClick 回调(随后状态立即复位为未点击)。无论哪种模式,调用都会引起按钮重绘。**注意:** DISABLED 模式调用此函数没有效果。
- `bool isClicked() const;`
**描述:** 返回按钮当前是否处于按下状态(仅对 TOGGLE 模式有意义,表示按钮选中状态)。
- 其他状态获取方法如 `getButtonText()`, `getButtonMode()`, `getButtonShape()`, `getFillMode()`, `getFillPattern()`, `getFillImage()`, `getButtonBorder()`, `getButtonTextColor()`, `getButtonTextStyle()`, `getButtonWidth()`, `getButtonHeight()` 一一提供,返回对应属性值。
- `void draw() override;`
**描述:** 绘制按钮外观。根据按钮当前状态选择填充颜色:若禁用则用 `DISABLEDCOLOUR` 灰色并给文字加删除线效果,否则 `click` 状态优先,其次 `hover` 状态,最后默认未点击颜色。然后设置透明背景、边框颜色和文本样式,计算文字应绘制的位置并绘制文字。
如果按钮文本长度超出按钮宽度,`draw()` 会自动调用 `cutButtonText()` 对文本进行裁剪,并用省略号表示超出部分,以确保文字在按钮内完整显示。绘制结束后恢复样式并将 `dirty` 置为 false。
- `bool handleEvent(const ExMessage& msg) override;`
**描述:** 处理与按钮相关的鼠标事件,包括鼠标按下、弹起、移动等:
- 检测鼠标是否悬停在按钮区域内(根据按钮形状调用内部 `isMouseInCircle/Ellipse` 辅助函数)。
- 处理 `WM_LBUTTONDOWN`(鼠标按下):若按钮未禁用且鼠标位于按钮上,在 NORMAL 模式下将按钮标记为点击状态(按下),在 TOGGLE 模式下不改变状态(等待弹起时处理)。
- 处理 `WM_LBUTTONUP`(鼠标弹起):若鼠标在按钮上且按钮可点击:
- NORMAL 模式:如果之前已记录按下,则认为一次有效点击,调用 `onClickCallback`,然后清除点击状态。
- TOGGLE 模式:切换按钮状态 `click = !click`。根据切换后的状态调用 `onToggleOnCallback``onToggleOffCallback`
- 每次有效点击后,都会通过 `flushmessage(EX_MOUSE|EX_KEY)` 清空输入消息队列,防止此次点击产生的消息(例如弹起)重复被处理。
- 处理鼠标移出(在 `WM_MOUSEMOVE` 中检测):如果 NORMAL 模式下拖出按钮区域则取消按下状态,任何模式下 hover 状态变化都将使按钮重绘。
- Tooltip 提示:如果启用了 `tipEnabled``handleEvent` 内会管理 `tipLabel` 的显示:
- 当鼠标首次移入按钮区域,启动一个定时(根据 `tipDelayMs`)。
- 当鼠标离开或按钮点击后,立即隐藏提示(调用 `hideTooltip()`)。
- 当鼠标在按钮上停留超过延迟且按钮仍显示,则设置 `tipVisible = true` 并初始化 `tipLabel` 的文本和位置准备显示提示。`tipFollowCursor` 决定提示框锚定鼠标位置还是按钮下方。
- `refreshTooltipTextForState()` 用于当按钮为 TOGGLE 模式且未自定义提示文本时,选择显示 `tipTextOn``tipTextOff`
- 每帧事件处理后,如提示应显示,则调用 `tipLabel.draw()` 绘制提示文本。
- 函数最后,如果按钮 `hover` 状态或 `click` 状态有变化,则将自身标记为 `dirty` 并立即调用 `draw()` 重绘自身,实现交互的即时反馈。
`handleEvent` 返回值为 `true` 当且仅当按钮按下鼠标弹起事件被处理时,表示该点击事件已被消费,其它控件不应再处理。
- **使用说明:**
开发者通常通过设置按钮的各种监听器来处理点击事件。例如:
```
auto btn = std::make_unique<Button>(50, 50, 80, 30, "确定");
btn->setOnClickListener([](){ /* 点击时执行 */ });
```
对于 TOGGLE 按钮:
```
btn->setButtonMode(ButtonMode::TOGGLE);
btn->setOnToggleOnListener([](){ /* 开启时执行 */ });
btn->setOnToggleOffListener([](){ /* 取消时执行 */ });
```
也可以控制按钮的外观,如:
```
btn->textStyle.color = RGB(255,255,255); // 白色文字
btn->setButtonFalseColor(RGB(100,150,200)); // 默认背景色
btn->setButtonBorder(RGB(80,80,80)); // 边框灰色
btn->setButtonShape(StellarX::ControlShape::RECTANGLE);
```
若要提示说明按钮功能,可:
```
btn->tipEnabled = true;
btn->tipTextClick = "点击执行确定操作";
// (TOGGLE模式则设置 tipTextOn/off)
```
### TextBox 类 (文本框控件)
**描述:** `TextBox` 用于显示和输入单行文本。可配置为可编辑或只读模式。TextBox 提供基本的文本输入框功能,当用户点击输入模式的文本框时,会弹出一个输入对话框接受用户输入(利用 EasyX 的 `InputBox` 实现),并将结果显示在文本框中。
- **模式:** 由 `StellarX::TextBoxmode` 指定:
- `INPUT_MODE` 可输入模式:用户点击文本框可输入新内容。
- `READONLY_MODE` 只读模式:用户点击不会更改文本,典型地可用于展示不可修改的信息。
- **外观:**
- 支持矩形和圆角矩形四种形状(无边框或有边框),通过 `setTextBoxShape(ControlShape)` 设置。圆形/椭圆对文本框来说不常用,一律按矩形处理。
- 文本框默认有黑色边框和白色背景,可用 `setTextBoxBorder` 和 `setTextBoxBk` 改变。
- 文字样式通过 `textStyle` 调整,包含字体、大小、颜色等属性。
- **主要属性:**
- `std::string text;` 当前文本框显示的文本内容。
- `TextBoxmode mode;` 当前模式(决定是否允许输入)。
- `ControlShape shape;` 当前文本框边框形状(矩形或圆角矩形,默认 RECTANGLE)。
- `bool click;` 内部使用标志,表示是否刚被点击(用于触发输入弹窗,处理完后即重置)。
- `size_t maxCharLen;` 最大允许输入字符数。默认 255。
- `COLORREF textBoxBorderColor, textBoxBkColor;` 边框颜色和背景填充颜色。
- `StellarX::ControlText textStyle;` 文本样式(高度、宽度、字体、颜色等)。
- **主要方法:**
- `void setMode(StellarX::TextBoxmode mode);`
**描述:** 切换文本框模式。可选 INPUT_MODE 或 READONLY_MODE。更改模式不会清除已有文本,但只读模式下用户输入将被忽略。
- `void setMaxCharLen(size_t len);`
**描述:** 设置文本框最大字符长度。仅当 `len > 0` 时有效。超出长度的输入会被截断。
- `void setTextBoxShape(StellarX::ControlShape shape);`
**描述:** 设置文本框形状。支持矩形和圆角矩形,与 Button 类似。如果传入非法形状(圆/椭圆),会自动改为矩形。设置后文本框将 `dirty`,下次绘制按新形状渲染。
- `void setTextBoxBorder(COLORREF color);` / `void setTextBoxBk(COLORREF color);`
**描述:** 设置边框颜色和背景颜色。
- `void setText(std::string text);`
**描述:** 更新文本框内容文本。如果新文本长度超过 `maxCharLen`,将自动截断。设置后标记 `dirty` 并立即调用 `draw()` 重绘,使更改即时可见。
- `std::string getText() const;`
**描述:** 获取当前文本框内容字符串。
- `void draw() override;`
**描述:** 绘制文本框背景和文字。根据 `textStyle` 设置字体,如字体高度/宽度大于控件尺寸则裁剪适应;使用透明背景模式绘制文字,以便背景色通过 `fill...` 函数填充。
- 绘制顺序:先 `fillrectangle/roundrect` 绘制背景(按 `textBoxBkColor`),然后 `outtextxy` 绘制文本,文本在 X 方向留出 10 像素左右内边距,Y 方向垂直居中。
- 如果文本长度超出控件宽度,不自动裁剪(与 Button 不同),而是可能显示不下或被遮盖。通常应通过设置 `maxCharLen` 控制输入长度,或设计足够宽度的文本框。
- 绘制完成后 `dirty` 置 false。
- `bool handleEvent(const ExMessage& msg) override;`
**描述:** 处理鼠标事件:
- 判断鼠标是否在文本框区域 (`hover`)。
- 当 `WM_LBUTTONUP` 且鼠标在区域内发生:若模式为 INPUT_MODE,则调用 EasyX 的 `InputBox` 弹出输入对话框,允许用户输入文本(初始值为当前文本框内容),用户提交后 `InputBox` 返回新文本,将此文本赋值并标记 `dirty` 以便重绘显示。若模式为 READONLY_MODE,则弹出一个只读提示框(不会改变文本),并不修改 `dirty` 状态。
- 其它鼠标事件不改变文本框外观,仅根据需要返回 false(未消费事件)。
- 当完成一次输入或点击,文本框会调用自身 `draw()` 更新显示。READONLY_MODE 下点击会简单弹出提示(使用 InputBox 显示原文本并提示输入无效)。
- 函数返回 true 表示在 INPUT_MODE 下点击事件被消费(因为弹出了输入框),READONLY_MODE 下也返回 true(提示已显示),从而阻止事件继续传播。
- **用法注意:**
- 由于 EasyX 的 `InputBox` 是模态阻塞函数,`runEventLoop` 本身是非阻塞循环,**在 UI 线程直接调用 InputBox 会暂停整个消息循环** 直到输入完成。StellarX 目前采用这种简单方式处理文本输入(不是异步 UI 输入)。因此在 `TextBox::handleEvent` 中使用 `InputBox` 来得到用户输入。这简化了实现,但 UI 线程会暂时挂起。开发者应避免在非常频繁交互时滥用 InputBox。
- 可以通过 `maxCharLen` 控制可输入最大字符数,例如对于数值输入框可以设定长度防止超出范围。
- 使用 `textStyle` 可调整显示字体(如等宽字体用于代码输入等),`textStyle.color` 也可单独设置文字颜色。
### Dialog 类 (对话框控件)
**描述:** `Dialog` 提供模态/非模态对话框功能,可在应用中弹出提示、确认或输入等对话界面。Dialog 本质上是一个特殊的 Canvas 容器,内部包含文本消息和按钮等子控件,并与 Window 紧密配合实现模态阻塞效果。
- **创建与模式:**
- 构造函数 `Dialog(Window& parent, std::string title, std::string message = "对话框", MessageBoxType type = OK, bool modal = true)`
**描述:** 创建一个关联到 `parent` 窗口的对话框。`title` 将作为对话框标题文本显示在对话框顶部,`message` 是正文消息内容。`type` 参数指定对话框包含的按钮组合类型(见 MessageBoxType 枚举),例如 OK 只有一个确定按钮,YesNo 包含“是”和“否”两个按钮等。`modal` 决定对话框是模态还是非模态。构造后对话框初始为隐藏状态,需要调用 `show()` 显示。
- `MessageBoxType` 枚举定义了常见的对话框按钮组合:
- `OK`:一个“确定”按钮。
- `OKCancel`:“确定”和“取消”按钮。
- `YesNo`:“是”和“否”按钮。
- `YesNoCancel`:“是”、“否”和“取消”按钮。
- `RetryCancel`:“重试”和“取消”。
- `AbortRetryIgnore`:“中止”、“重试”、“忽略”按钮。
- 模态 (`modal=true`) 对话框:调用 `show()` 时会阻塞当前线程运行,直到对话框关闭后才返回。这期间应用的其他部分暂停响应输入。适用于必须先处理用户响应的关键情景。模态对话框关闭时,`show()` 返回后可通过 `getResult()` 获取用户点击结果。
- 非模态 (`modal=false`) 对话框:调用 `show()` 则立即返回,应用程序继续运行,用户可以在背景窗口和对话框之间自由交互。需要通过回调函数或轮询 `getResult()` 来获取对话框的结果。
- **组成元素:**
- 标题文本(使用内部的 Label 控件 `title` 实现,显示在对话框顶部)。
- 消息正文(支持多行文本,`Dialog` 自动按行分割存储在 `lines` 向量中,并在绘制时逐行居中输出)。
- 功能按钮(根据 `MessageBoxType`,包含1到3个 Button 控件,位置自动计算居于对话框底部)。
- 关闭按钮(右上角的 “×” 按钮,一个小的 Button 控件,点击相当于取消/关闭)。
- 背景样式:Dialog 采用圆角矩形背景(如 Canvas shape = ROUND_RECTANGLE)并有半透明效果(具体通过 saveBackground 恢复背景实现视觉模态效果,但在 EasyX 简化模型下实际只是截屏背景,无模糊透明实现)。
- **主要属性:**
- `bool modal;` 是否模态对话框。
- `titleText`:标题栏文本,`message`:消息内容文本。
- `MessageBoxType type;` 对话框类型(按钮组合)。
- `std::vector<std::string> lines;` 分割后的多行消息文本。
- `std::unique_ptr<Label> title;` 用于显示标题的 Label 控件。
- `Button* closeButton;` 关闭按钮指针(“×”)。
- `Window& hWnd;` 引用父窗口,用于操作对话框关闭后刷新背景等。
- `MessageBoxResult result;` 用户选择结果,枚举值对应哪个按钮被按下。常用值:OK=1, Cancel=2, Yes=6, No=7 等(见 CoreTypes 定义)。
- `std::function<void(MessageBoxResult)> resultCallback;` 非模态对话框的结果回调函数指针。如果设置,将在对话框关闭且 `modal == false` 时被调用,参数为用户最终点击结果。
- **主要方法:**
- `void show();`
**描述:** 显示对话框。如果是模态对话框,此函数将阻塞,内部运行一个事件处理循环,使该对话框独占输入焦点,直到对话框关闭为止;如果是非模态,则此函数立即返回(但对话框窗口已可见,输入由主 Window 继续分发)。`show()` 不返回任何值,但可以在模态情况下函数返回后调用 `getResult()` 获取结果。
- `void closeDialog();`
**描述:** 关闭对话框。会将对话框隐藏并清理资源。如果有设置非模态回调且对话框为非模态,则在此过程中调用 `resultCallback(result)` 通知结果。通常不直接调用,由内部逻辑根据按钮按下触发。
- `MessageBoxResult getResult() const;`
**描述:** 获取对话框的返回结果(用户按下了哪个按钮)。对模态对话框,`show()` 返回后调用此函数获取结果;对非模态,可在回调或稍后某时获取。
- `void setTitle(const std::string& title);` / `void setMessage(const std::string& msg);`
**描述:** 更改对话框标题或内容文本。更改内容会重新分割 `lines` 并调整文本区域大小,需要重新布局。
- `void setType(MessageBoxType type);`
**描述:** 更改对话框类型(按钮组合)。将重新创建按钮布局。
- `void setModal(bool modal);`
**描述:** 切换对话框模态属性(必须在 show() 前调用)。一般不会在运行中切换。
- `void setResult(MessageBoxResult res);`
**描述:** 设置对话框结果代码。通常由内部在按钮按下时调用,开发者无需手动调用。
- `void setInitialization(bool init);`
**描述:** 若传入 true,则提前计算和设置对话框尺寸,并保存背景快照。**用途:** 在通过 `MessageBox` 静态函数创建对话框时,会先调用此函数以准备好对话框以便正确显示。
- `void setResultCallback(function<void(MessageBoxResult)> cb);`
**描述:** 为非模态对话框设置结果回调函数。用户关闭对话框时将异步调用此函数,将结果传出。
- *内部实现在 `Dialog` 类私有方法中:*
- `initButtons()`:根据 `type` 创建对应数量的 Button 控件并设置其回调,将它们添加到 Dialog 容器。
- `initCloseButton()`:创建右上角关闭按钮(文本为 “×”),点击后触发取消逻辑。
- `initTitle()`:创建标题 Label 控件。
- `splitMessageLines()`:将 `message` 根据换行符分割填充到 `lines` 数组。
- `getTextSize()`:计算 `lines` 中最长行宽度和单行高度,以确定文本区域所需尺寸。
- `initDialogSize()`:综合计算对话框宽高(考虑文本区域和按钮区域的宽度、高度),然后居中放置对话框于窗口。最后调用上述 init** 方法创建各子控件。
- `performDelayedCleanup()`:用于延迟销毁对话框时释放资源。Dialog 设计上为了避免背景快照不匹配,需要在对话框关闭且完全隐去后再恢复背景并释放子控件资源;此函数执行该清理流程,由 Window 的事件循环或 `handleEvent` 在适当时机调用。
- `void draw() override;`
**描述:** 绘制对话框界面。Dialog 继承自 Canvas,通过调用 `Canvas::draw()` 绘制背景和子控件框架,然后另外绘制消息文本内容。绘制步骤:
1. 如果首次显示,对话框先保存覆盖区域的屏幕背景(便于关闭时恢复)。
2. 设置 Canvas 的背景颜色、边框等样式为对话框特定值(通常圆角矩形)。
3. 将所有子控件标记为 `dirty` 并调用 `Canvas::draw()` 绘制对话框背景和子控件(包括按钮、标题Label等)。
4. 使用对话框的 `textStyle` 设置文字样式,然后将每行 `lines` 文本居中绘制在对话框内部合适的位置(计算基于对话框宽度)。
5. 恢复样式,设置 `dirty = false`。
对话框作为一个整体,由 Window 管理,其绘制顺序通常在 Window::runEventLoop 强制刷新时绘制。模态对话框在自己的 loop 内绘制。
- `bool handleEvent(const ExMessage& msg) override;`
**描述:** 处理对话框相关事件:
- 如果对话框未显示 (`show == false`),则检查 `pendingCleanup` 标志,如果有延迟清理任务且当前未在清理,则调用 `performDelayedCleanup()` 执行清理,然后不消费事件直接返回 false。
- 如果对话框正在清理或标记延迟清理,则不处理任何事件直接返回 false。
- 若为模态对话框 (`modal == true`),并检测到用户点击了对话框区域外部(窗口背景区域)的鼠标弹起事件,则发出提示声音 (`\a`)并返回 true(**吞噬事件**),防止用户点击窗体其他部分。
- 将事件传递给 Dialog 继承的 Canvas(即其子控件)处理:`consume = Canvas::handleEvent(msg)`,这样对话框内部的按钮可以响应事件。如果子控件处理了事件则 consume 为 true。
- 最后,如果对话框有延迟清理任务,则调用 `performDelayedCleanup()` 处理。返回值为 whether 消费了事件。
- **模态对话框特别说明:** Window 主循环在有模态对话框显示时,会将背景点击等事件吃掉(上面逻辑已实现),同时对话框 `show()` 方法内部启动自己的循环处理事件,使模态对话框独占用户输入。非模态对话框则依赖 Window 主循环分发事件,由 handleEvent 参与处理。
- **使用示例:**
`Dialog` 通常不直接使用,而是通过 `StellarX::MessageBox` 工具调用快速创建并显示:
- 模态对话框:
```
StellarX::MessageBoxResult res = StellarX::MessageBox::showModal(window,
"文件已保存。是否关闭程序?", "提示",
StellarX::MessageBoxType::YesNoCancel);
if (res == StellarX::MessageBoxResult::Yes) { /* 用户选择是 */ }
```
`showModal` 内部创建 `Dialog` 并以模态方式显示,直到用户点击“是/否/取消”其中一个按钮,函数返回对应的枚举值。
- 非模态对话框:
```
StellarX::MessageBox::showAsync(window,
"处理完成!", "通知",
StellarX::MessageBoxType::OK,
[](StellarX::MessageBoxResult){ /* 用户点了OK后的处理 */ });
```
这样弹出的对话框不会阻塞程序,它包含一个“确定”按钮。用户点击后,对话框关闭并自动调用提供的 lambda 回调。Window 的事件循环会继续正常运行。
开发者也可自行实例化 Dialog:
```
Dialog dlg(mainWindow, "标题", "内容……", StellarX::MessageBoxType::OKCancel, false);
dlg.setResultCallback([](MessageBoxResult res) { /* 处理结果 */ });
dlg.show();
```
以上将创建一个非模态对话框。一般推荐使用 `MessageBox` 封装函数更方便。
## MessageBox 工具类
**描述:** `StellarX::MessageBox` 提供静态方法方便地创建标准对话框,无需直接操作 Dialog 类。类似 Windows API 的 MessageBox,但这里可以指定按钮组合类型,并支持非模态调用。
- **静态函数:**
- `static MessageBoxResult showModal(Window& wnd, const std::string& text, const std::string& caption = "提示", MessageBoxType type = MessageBoxType::OK);`
**描述:** 弹出一个模态对话框,在窗口 `wnd` 上显示,标题为 `caption`,正文为 `text`,按钮组合由 `type` 指定。此函数会阻塞当前执行直到用户在对话框上点击按钮关闭对话框。返回值为 `MessageBoxResult`,表示用户点击了哪个选项(例如 `MessageBoxResult::OK` 或 `Cancel` 等)。开发者可根据返回值执行不同逻辑。
- `static void showAsync(Window& wnd, const std::string& text, const std::string& caption = "提示", MessageBoxType type = MessageBoxType::OK, std::function<void(MessageBoxResult)> onResult = nullptr);`
**描述:** 在窗口 `wnd` 弹出一个非模态对话框。参数类似 `showModal`,但 `onResult` 是可选的回调函数。如果提供了回调函数,当用户关闭对话框时将异步调用该函数并传入用户选择结果。该函数本身立即返回,不阻塞程序流程。如果在 `wnd` 中已经存在一个内容和标题都相同的非模态对话框未关闭,则此函数会发出警报音并不创建新的对话框,以防止重复(内部使用 `Window::hasNonModalDialogWithCaption(caption,text)` 检查)。
**注意:** 如果不提供 `onResult` 回调,那么需要其他方式得知对话框结果(例如通过 Dialog 的公有接口或者全局状态),但通常建议提供回调以处理结果。
- **实现细节:** `showModal` 和 `showAsync` 内部都会创建一个 `Dialog` 对象,并根据 `type` 设置相应按钮逻辑:
- `showModal` 创建 Dialog 后,会调用 `dlg.setInitialization(true)` 准备好对话框尺寸,然后调用 `dlg.show()` 以模态方式显示,并阻塞等待结果,最后返回 `dlg.getResult()`。
- `showAsync` 则创建 Dialog 后,同样初始化,若指定回调则通过 `dlgPtr->setResultCallback(onResult)` 注册,然后把对话框通过 `wnd.addDialog()` 加入主窗口的对话框列表,最后调用 `dlgPtr->show()` 非模态显示。非模态显示不会阻塞,因此 `showAsync` 很快返回;对话框的关闭与结果传递在回调中完成。
- **典型用途:** MessageBox 提供与系统 MessageBox 类似的简明接口:
- 显示一条提示信息并等待确认:
`MessageBox::showModal(mainWindow, "操作成功完成!", "提示", MessageBoxType::OK);`
- 提示加确认选择:
`auto res = MessageBox::showModal(mainWindow, "确定要删除记录吗?", "请确认", MessageBoxType::YesNo); if (res == MessageBoxResult::Yes) { ... }`
- 非模态通知:
`MessageBox::showAsync(mainWindow, "下载已在后台进行。", "通知", MessageBoxType::OK); // 用户点OK时对话框将自行关闭`
- 非模态询问并通过回调获取结果:
```
MessageBox::showAsync(mainWindow, "发现新版本,是否现在更新?", "更新提示", MessageBoxType::YesNo,
[](MessageBoxResult res) {
if (res == MessageBoxResult::Yes) startUpdate();
});
```
通过这些静态方法,可以很方便地融合对话框提示流程,无需手动管理 Dialog 对象的生命周期和事件处理逻辑。
## Table 类 (表格控件)
**描述:****Table** 类是一个高级表格控件,支持分页显示和大数据量展示[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/include/StellarX/table.h#L2-L10)。它提供完整的数据表格功能,包括表头、数据行和分页导航等,实现类似电子表格的呈现。Table 控件可自动计算列宽和行高,支持自定义边框样式、填充模式以及文本字体样式,从而适应不同的UI设计需求。通过内置的分页机制,Table 能有效展示大量数据而不影响界面性能,并使用背景缓存来优化渲染,避免重绘闪烁。
**主要特性:**
- **自动分页:** 根据数据行数和每页行数自动计算总页数和当前页显示内容[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/include/StellarX/table.h#L8-L16)。提供 **上一页/下一页** 按钮和页码指示,方便用户浏览分页数据。
- **自定义配置:** 开发者可配置每页行数、是否显示翻页按钮,设置表格边框颜色、背景色,选择填充模式(实色填充或透明)和边框线型等,满足不同视觉风格要求。
- **高效渲染:** Table 通过缓存背景图像来提高渲染效率。在表格内容变化或首次绘制时缓存底图,绘制时仅更新差异部分,减少重复绘制带来的闪烁[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L210-L219)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L241-L249)。
- **典型场景:** 适用于数据列表、报表、记录浏览等需要表格式展示的场景[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/include/StellarX/table.h#L14-L17)。例如显示数据库查询结果、日志记录、统计报表等,让用户以行列形式查看信息。
### 公共成员函数
#### Table(int x, int y)
**函数原型:** `Table(int x, int y)`
**参数:**
- `x`:表格左上角的 X 坐标位置。
- `y`:表格左上角的 Y 坐标位置。
**返回值:** 无(构造函数无返回值)。
**功能描述:** Table 类的构造函数。在指定的坐标位置创建一个新的表格控件实例[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L164-L171)。初始状态下表格没有设置表头和数据,宽度和高度初始为0,会在设置数据后根据内容自动计算调整。`x` 和 `y` 参数用于将表格定位在窗口中的起始位置。
**使用场景:** 当需要在界面上显示表格数据时,首先调用此构造函数实例化一个 Table 对象。例如,在创建主窗口后,可以创建 `Table table(50, 50);` 来放置一个表格控件,然后再设置表头和数据。
#### ~Table()
**函数原型:** `~Table()`
**参数:** 无。
**返回值:** 无。
**功能描述:** Table 类的析构函数。在对象销毁时自动调用,负责释放表格内部分配的资源。如表格内部创建的翻页按钮、页码标签以及背景缓存图像都将在析构函数中删除以防止内存泄漏[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L169-L178)。
**使用场景:** 一般不直接调用,由系统自动管理。当 Table 对象的生命周期结束(例如其所在窗口关闭或程序结束)时,析构函数确保清理内存。不需开发者手动调用,但了解其行为有助于避免重复释放资源。
#### draw()
**函数原型:** `void draw() override`
**参数:** 无。
**返回值:** 无。
**功能描述:** 重写自 `Control` 基类的绘制函数,用于将表格绘制到屏幕[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L185-L194)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L252-L260)。调用 `draw()` 时,Table 根据当前状态绘制表头、当前页的数据行、页码标签和翻页按钮(如果启用)。绘制过程中会应用先前设置的文本样式、边框颜色、背景色等属性。为避免频繁重绘,Table 内部维护一个`dirty`标志,只有当表格内容或外观需更新时(例如调用了设置函数后)`draw()` 才会实际重绘内容[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L187-L196)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L258-L266)。
**使用场景:** 通常在设置完表头和数据、或修改了样式后调用,令表格按新内容渲染。一般框架的窗口在刷新时会自动调用各控件的 `draw()` 方法,开发者也可在需要立即刷新表格时手动调用它。例如,当表格数据更新且需要即时反映在界面上时,可以设置相应属性并调用 `draw()` 触发重绘。
#### handleEvent(const ExMessage& msg)
**函数原型:** `bool handleEvent(const ExMessage& msg) override`
**参数:**
- `msg`:事件消息对象(EasyX库的 `ExMessage` 结构),包含了键盘或鼠标事件的信息。
**返回值:** 布尔值,表示事件是否被表格控件处理。返回 `true` 表示事件已被消耗,不再需要向其他控件传播;返回 `false` 则表示表格未处理该事件。
**功能描述:** 重写自 `Control` 基类的事件处理函数,用于处理与表格相关的用户交互事件。对于 Table 而言,`handleEvent` 主要用于处理翻页按钮的点击事件。当 `isShowPageButton` 为真(显示翻页按钮)时,该方法会将传入的事件传递给内部的 “上一页” 和 “下一页” 按钮进行处理[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L274-L283)。如果用户点击了上一页或下一页按钮,按钮的内部逻辑会更新当前页码并调用 `draw()` 重绘表格,`handleEvent` 随后返回 `true` 表示事件已处理。若翻页按钮未启用(例如只有一页数据或通过 `showPageButton(false)` 隐藏了按钮),此函数直接返回 `false`[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L276-L283),不对事件进行处理。
**使用场景:** 框架的主事件循环会调用控件的 `handleEvent` 方法来分发用户输入。当使用 Table 控件时,无需手动调用此函数,但了解其行为有助于处理自定义事件。例如,可在Window的事件循环中将鼠标点击事件传给表格控件,使其内部的翻页按钮响应点击,从而实现分页浏览的数据切换。
#### setHeaders(std::initializer_liststd::string headers)
**函数原型:** `void setHeaders(std::initializer_list<std::string> headers)`
**参数:**
- `headers`:表头文本字符串的初始化列表,每个字符串对应表格一列的列名。
**返回值:** 无。
**功能描述:** 设置表格的列标题。调用此函数会清除当前已有的表头并使用提供的字符串列表作为新表头[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L288-L296)。在设置表头后,Table 会将内部状态标记为需要重新计算单元格尺寸和重新绘制表头[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L291-L295)。列标题用于在表格顶部显示,表示每列数据的含义。表头数量确定了表格的列数,后续添加的数据应与列数匹配。
**使用场景:** 通常在创建 Table 实例后、添加数据之前调用,用来定义表格的列结构。例如:`table.setHeaders({"姓名", "年龄", "职业"});` 将表格设置为包含“姓名”、“年龄”、“职业”三列。设置表头后即可调用 `setData` 添加对应列的数据行。
#### setData(std::vectorstd::string data) / setData(std::initializer_list<std::vectorstd::string> data)
**函数原型1** `void setData(std::vector<std::string> data)`
**参数:**
- `data`:包含单行表格数据的字符串向量,每个元素对应一列的内容。
**返回值:** 无。
**功能描述:** 将一个数据行添加到表格中。若传入的这行数据列数少于当前表头列数,则函数会自动在行数据末尾补空字符串,使其列数与表头一致[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L300-L308)。然后将该行数据追加到内部数据集合 `data` 中,并更新表格的总页数 (`totalPages`) 属性[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L302-L308)。更新页数时根据当前总行数和每页行数计算页数,确保至少为1页[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L303-L308)。函数调用后会标记表格需要重新计算单元格尺寸,并在下次绘制时包含新添加的数据。
**使用场景:** 可用于逐行构建表格数据,例如从数据源读取记录并一条条加入表格。适合数据逐步到达或者需要动态添加行的情形。每次调用都会在末页附加一行,并相应更新分页显示。
**函数原型2** `void setData(std::initializer_list<std::vector<std::string>> data)`
**参数:**
- `data`:一个由多行数据组成的初始化列表,每个内部的 `std::vector<std::string>` 代表一行的数据。
**返回值:** 无。
**功能描述:** 批量添加多行数据到表格中。对于提供的每一行数据,函数会检查其列数是否与表头列数匹配;若不足则在该行末尾补充空字符串以对齐列数[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L313-L321)。然后将处理后的整行数据依次追加到表格内部的数据集中。添加完所有行后,函数重新计算总数据行数下的总页数,并至少确保一页[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L322-L328)。与单行版本类似,此方法也会触发单元格尺寸重新计算和脏标记,使表格内容在下次绘制时更新。
**使用场景:** 适用于一次性导入整批数据的情形,例如应用初始化时加载整个数据表。调用此函数可以方便地用初始化列表直接提供多行初始数据,而无需多次重复调用单行的 `setData`。在需要清空已有数据并填入新数据时,可以在调用此方法前手动清除旧数据(例如重新创建Table或使用返回的data容器修改)。
#### setRowsPerPage(int rows)
**函数原型:** `void setRowsPerPage(int rows)`
**参数:**
- `rows`:每页显示的数据行数(正整数)。
**返回值:** 无。
**功能描述:** 设置表格分页时每页包含的数据行数。调用此函数会修改内部的 `rowsPerPage` 属性,并根据当前总数据行数重新计算总页数 `totalPages`[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L332-L338)。如果重新计算后总页数低于1,则强制设为1页(避免出现0页的非法状态)。改变每页行数将影响表格的高度和分页布局,因此会标记需要重新计算单元格大小以及重新绘制。在下次 `draw()` 时,表格将按照新的分页大小重新呈现内容。
**使用场景:** 当需要调整表格分页密度时使用。例如默认每页显示5行数据,可以通过 `table.setRowsPerPage(10);` 改为每页10行,以减少总页数。常用于根据用户偏好或窗口大小调整每页显示量。应注意,如果当前页码在调整后超出新的总页数范围,表格在下次翻页时会自动纠正到最后一页。
#### showPageButton(bool isShow)
**函数原型:** `void showPageButton(bool isShow)`
**参数:**
- `isShow`:布尔值,指定是否显示翻页按钮。`true` 表示显示 “上一页” 和 “下一页” 按钮,`false` 则隐藏它们。
**返回值:** 无。
**功能描述:** 控制表格分页导航按钮的可见性。调用此函数可以在界面上隐藏或显示表格底部的翻页按钮。如果设置为隐藏 (`false`),则用户将无法通过界面按钮切换页码(此时表格将始终停留在当前页);当再次显示按钮 (`true`) 时,用户可以使用按钮查看其他页的数据。该函数仅影响按钮的绘制和事件处理状态,不改变当前页码等分页数据。
**使用场景:** 适用于当数据量较少仅一页即可显示完毕,或开发者希望以其他方式控制分页时。例如,当表格内容只有一页数据时,可以隐藏翻页按钮以简化界面。需要再次启用分页功能时再调用 `showPageButton(true)` 显示按钮。
#### setTableBorder(COLORREF color)
**函数原型:** `void setTableBorder(COLORREF color)`
**参数:**
- `color`:表格边框颜色,类型为 `COLORREF`Windows RGB 颜色值,例如 `RGB(0,0,0)` 表示黑色)。
**返回值:** 无。
**功能描述:** 设置表格单元格边框线的颜色。调用该方法会将 Table 内部存储的边框颜色更新为指定的 `color`,并将表格标记为需要重绘。在随后的 `draw()` 调用中,表格的线条(包括单元格边框和外围边框)将使用新的颜色绘制[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L192-L199)。默认的边框颜色为黑色,如需与界面主题匹配可使用此函数调整。
**使用场景:** 当需要改变表格线条颜色以配合UI配色方案时使用。例如,将表格边框设为浅灰色以获得柔和的视觉效果:`table.setTableBorder(RGB(200,200,200));`。在深色模式下,可将边框设为亮色以提高对比度。调用后下一次重绘将体现颜色变化。
#### setTableBk(COLORREF color)
**函数原型:** `void setTableBk(COLORREF color)`
**参数:**
- `color`:表格背景填充颜色,类型为 `COLORREF`。
**返回值:** 无。
**功能描述:** 设置表格单元格的背景颜色。调用该函数会更新 Table 内部的背景色属性,并触发重绘标记。在绘制表格时,每个单元格将使用指定的背景颜色进行填充(当填充模式为实色时)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L192-L199)。默认背景颜色为白色,如需改变单元格底色(例如间隔行着色等)可以使用不同的颜色多次调用或自定义绘制。
**使用场景:** 用于调整表格的底色风格。例如,将表格背景设为浅黄色以突出显示内容区域:`table.setTableBk(RGB(255, 255, 224));`。结合填充模式使用,可以实现透明背景等效果。注意背景颜色变化在填充模式为Solid时可见,若填充模式为Null则背景不绘制实色。
#### setTableFillMode(StellarX::FillMode mode)
**函数原型:** `void setTableFillMode(StellarX::FillMode mode)`
**参数:**
- `mode`:填充模式,枚举类型 `StellarX::FillMode`。可选值包括 Solid(实色填充)、Null(不填充)、Hatched/Pattern 等。
**返回值:** 无。
**功能描述:** 设置表格的填充模式,即单元格背景的绘制方式。常用的模式有 Solid(纯色填充背景)或 Null(透明背景不填充)。当前版本中,仅对 Solid 和 Null 模式提供完全支持:如果传入其他模式值,内部会默认退回使用 Solid 模式[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L360-L369)。调用此方法会应用新的填充模式,并将此模式同步到表格的翻页按钮和页码标签控件上,使它们风格一致[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L365-L373)。例如,当设置为 Null 模式时,表格单元格背景将不被填充颜色,呈现透明效果,同时页码标签也设置为透明背景显示。改变填充模式会将表格及相关控件标记为需重绘。
**使用场景:** 根据界面设计需要调整表格背景绘制策略。例如,为实现镂空透明效果,可使用 `table.setTableFillMode(StellarX::FillMode::Null);` 使表格仅绘制文字和边框而无底色。默认使用 Solid 模式填充背景,实现传统表格外观。当需要图案填充等高级效果时,当前版本可能不支持,仍会以纯色替代。
#### setTableLineStyle(StellarX::LineStyle style)
**函数原型:** `void setTableLineStyle(StellarX::LineStyle style)`
**参数:**
- `style`:边框线型,枚举类型 `StellarX::LineStyle`,如 Solid(实线)、Dash(虚线)、Dot(点线)等。
**返回值:** 无。
**功能描述:** 设置表格网格线(单元格边框线条)的样式。传入不同的线型枚举值可以改变表格绘制时边框的样式,例如使用虚线或点线来绘制单元格边框。调用该方法将更新内部存储的线型并标记表格需要重绘,下次绘制时新的线型将生效。需注意实际效果取决于绘图库对线型的支持。
**使用场景:** 当希望改变表格外观以区分层次或提升美观时,可修改线型。例如,`table.setTableLineStyle(StellarX::LineStyle::Dash);` 将表格边框改为虚线风格,以在视觉上弱化表格线对界面的干扰。此功能在打印报表或特殊主题界面中会很有用。
#### setTableBorderWidth(int width)
**函数原型:** `void setTableBorderWidth(int width)`
**参数:**
- `width`:边框线宽度(像素值,正整数)。
**返回值:** 无。
**功能描述:** 设置表格边框线条的粗细(线宽)。默认情况下表格边框宽度为1像素,调用此函数可使所有单元格的边框线加粗或变细。更新线宽后表格将在下次重绘时使用新的线宽绘制边框[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L198-L201)。如果设置的宽度大于1,为避免粗线覆盖过多背景,Table 在绘制时会调整背景区域的恢复以适应粗线描边[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L220-L228)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L240-L248)。
**使用场景:** 在需要强调表格结构或适应高分辨率显示时,可增加线宽。例如,`table.setTableBorderWidth(2);` 将表格线条加粗为2像素,使表格框架更加醒目。若要获得细网格效果,可以将线宽设置为1以下(不过线宽最小为1像素)。改变线宽后请确保颜色、线型等设置搭配以获得预期视觉效果。
### 获取属性方法(Getters
以下函数用于获取 Table 当前的状态或配置信息:
- **int getCurrentPage() const**:获取当前显示的页码索引。第一页为1,第二页为2,以此类推[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L390-L398)。可用于在界面上显示当前页码或在程序逻辑中判断分页位置。
- **int getTotalPages() const**:获取根据当前数据行数和每页行数计算出的总页数[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L394-L402)。通常在设置完数据或调整分页参数后调用,以了解数据被分页成多少页。
- **int getRowsPerPage() const**:获取当前每页显示的数据行数设置[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L400-L408)。可用于在界面上提示用户分页大小,或在逻辑中根据该值调整分页处理。
- **bool getShowPageButton() const**:获取当前翻页按钮的显示状态(true 为显示,false 为隐藏)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L404-L412)。可根据此值判断界面上是否提供了翻页导航。
- **COLORREF getTableBorder() const**:获取当前表格边框颜色[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L410-L418)(返回一个COLORREF值)。可以用于在需要时与其他控件颜色保持一致或者保存当前配置。
- **COLORREF getTableBk() const**:获取当前表格背景填充颜色[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L414-L422)。
- **StellarX::FillMode getTableFillMode() const**:获取当前表格的填充模式设置[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L420-L428)。返回值为 `StellarX::FillMode` 枚举,可判断是实色填充、透明等模式。
- **StellarX::LineStyle getTableLineStyle() const**:获取当前表格边框线型设置[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L424-L432)。返回 `StellarX::LineStyle` 枚举值,如实线、虚线等。
- **std::vectorstd::string getHeaders() const**:获取当前表格的表头列表副本[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L430-L438)。返回一个字符串向量,其中包含每一列的标题。可以使用此函数查看或保存表格的列标题设置。
- **std::vector<std::vectorstd::string> getData() const**:获取当前表格的所有数据[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L434-L442)。返回一个二维字符串向量,其中每个内部向量代表表格中的一行数据(各元素对应该行每列的值)。这允许访问或遍历表格内容,例如用于导出数据。
- **int getTableBorderWidth() const**:获取当前设置的表格边框线宽度(像素)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L440-L446)。
以上获取函数均为 `const` 成员函数,不会修改 Table 的状态,只返回相应属性的值。开发者可根据需要随时调用这些函数来查询表格状态,例如在界面其他区域显示统计信息或在调试时验证设置是否正确。
### 公共成员变量
- **StellarX::ControlText textStyle**:文本样式结构体,表示表格中文字内容的字体和样式设置[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/include/StellarX/CoreTypes.h#L132-L140)。通过修改该结构体的成员,可以自定义表格文本的外观,例如字体字号、高度、宽度、字体名称 (`lpszFace`),文字颜色 (`color`),以及是否加粗 (`nWeight`)、斜体 (`bItalic`)、下划线 (`bUnderline`) 等[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/include/StellarX/CoreTypes.h#L134-L142)。Table 默认使用框架的默认字体(例如微软雅黑)和颜色绘制文字。开发者可以直接访问并调整 `textStyle` 的属性,然后调用 `draw()` 重新绘制,使新的文本样式生效。例如,`table.textStyle.nHeight = 20; table.textStyle.color = RGB(0,0,128);` 将文字调大并改为深蓝色。值得注意的是,Table 内部的翻页按钮和页码标签也会同步使用 Table 的 `textStyle` 设置,以保持表格组件整体风格的一致。
### 示例
下面是一个使用 Table 控件的示例代码[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/README.md#L268-L276)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/README.md#L278-L286),演示如何创建表格、设置表头和数据、配置分页,并将其添加到窗口中显示:
```
// 创建一个表格控件,位置在 (50, 50)
auto myTable = std::make_unique<Table>(50, 50);
// 设置表头(列名称)
myTable->setHeaders({ "ID", "姓名", "年龄", "职业" });
// 添加数据行
myTable->setData({ "1", "张三", "25", "工程师" });
myTable->setData({ "2", "李四", "30", "设计师" });
myTable->setData({ "3", "王五", "28", "产品经理" });
// 设置每页显示2行数据
myTable->setRowsPerPage(2);
// 设置表格文本和样式
myTable->textStyle.nHeight = 16; // 文本字体高度
myTable->setTableBorder(RGB(50, 50, 50)); // 表格边框颜色(深灰)
myTable->setTableBk(RGB(240, 240, 240)); // 表格背景色(浅灰)
// 将表格控件添加到主窗口(假设 mainWindow 已创建)
mainWindow.addControl(std::move(myTable));
```
上述代码首先创建了一个 Table 实例,并依次设置表头和三行示例数据。接着将每页行数设为2行,这意味着数据将分页为两页(3条数据在每页2行的设置下将分两页显示)。然后调整了文本样式和颜色,将字体高度设为16像素,边框颜色设为灰色,背景色设为浅灰,以定制表格外观。最后,将配置好的表格控件添加到主窗口中,使其随着窗口一起绘制和响应用户操作。在实际应用中,添加控件后通常需要调用主窗口的 `draw()` 来绘制界面,及调用 `runEventLoop()` 进入消息循环以响应用户交互。通过上述流程,开发者即可在应用程序窗口中成功集成并显示一个功能完善的分页表格控件。
以上即为 StellarX 框架各主要组件的接口说明和使用方法。通过这些类和函数,开发者可以组合出完整的 GUI 应用。请注意,StellarX 基于 EasyX 实现,适用于 Windows 平台的教学和轻量工具开发场景。使用时需要确保 EasyX 图形库已初始化,并在应用结束前调用 `closegraph()` 释放图形资源。
-791
View File
@@ -1,791 +0,0 @@
#include "Button.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)
: Control(x, y, width, height)
{
initButton(text, mode, shape, RGB(202, 255, 255), RGB(171, 196, 220), RGB(255, 255, 0));
}
Button::Button(int x, int y, int width, int height, const std::string text, COLORREF ct, COLORREF cf, StellarX::ButtonMode mode, StellarX::ControlShape shape)
: Control(x, y, width, height)
{
initButton(text, mode, shape, ct, cf, RGB(255, 255, 0));
}
Button::Button(int x, int y, int width, int height, const std::string text, COLORREF ct, COLORREF cf, COLORREF ch, StellarX::ButtonMode mode, StellarX::ControlShape shape)
: Control(x, y, width, height)
{
initButton(text, mode, shape, ct, cf, ch);
}
// ====== GBK/MBCS 安全:字符边界与省略号裁切 ======
static inline int gbk_char_len(const std::string& s, size_t i)
{
unsigned char b = (unsigned char)s[i];
if (b <= 0x7F) return 1; // ASCII
if (b >= 0x81 && b <= 0xFE && i + 1 < s.size())
{
unsigned char b2 = (unsigned char)s[i + 1];
if (b2 >= 0x40 && b2 <= 0xFE && b2 != 0x7F) return 2; // 合法双字节
}
return 1; // 容错
}
static inline void rtrim_spaces_gbk(std::string& s)
{
while (!s.empty() && s.back() == ' ') s.pop_back(); // ASCII 空格
while (s.size() >= 2)
{ // 全角空格 A1 A1
unsigned char a = (unsigned char)s[s.size() - 2];
unsigned char b = (unsigned char)s[s.size() - 1];
if (a == 0xA1 && b == 0xA1) s.resize(s.size() - 2);
else break;
}
}
static inline bool is_ascii_only(const std::string& s)
{
for (unsigned char c : s) if (c > 0x7F) return false;
return true;
}
static inline bool is_word_boundary_char(unsigned char c)
{
return c == ' ' || c == '-' || c == '_' || c == '/' || c == '\\' || c == '.' || c == ':';
}
// 英文优先策略:优先在“词边界”回退,再退化到逐字符;省略号为 "..."
static std::string ellipsize_ascii_pref(const std::string& text, int maxW)
{
if (maxW <= 0) return "";
if (textwidth(LPCTSTR(text.c_str())) <= maxW) return text;
const std::string ell = "...";
int ellW = textwidth(LPCTSTR(ell.c_str()));
if (ellW > maxW)
{ // 连 ... 都放不下
std::string e = ell;
while (!e.empty() && textwidth(LPCTSTR(e.c_str())) > maxW) e.pop_back();
return e; // 可能是 ".."、"." 或 ""
}
const int limit = maxW - ellW;
// 先找到能放下的最长前缀
size_t i = 0, lastFit = 0;
while (i < text.size())
{
int clen = gbk_char_len(text, i);
size_t j = text.size() < i + (size_t)clen ? text.size() : i + (size_t)clen;
int w = textwidth(LPCTSTR(text.substr(0, j).c_str()));
if (w <= limit) { lastFit = j; i = j; }
else break;
}
if (lastFit == 0) return ell;
// 在已适配前缀范围内,向左找最近的词边界
size_t cutPos = lastFit;
for (size_t k = lastFit; k > 0; --k)
{
unsigned char c = (unsigned char)text[k - 1];
if (c <= 0x7F && is_word_boundary_char(c)) { cutPos = k - 1; break; }
}
std::string head = text.substr(0, cutPos);
rtrim_spaces_gbk(head);
head += ell;
return head;
}
// 中文优先策略:严格逐“字符”(1/2字节)回退;省略号用全角 "…"
static std::string ellipsize_cjk_pref(const std::string& text, int maxW, const char* ellipsis = "")
{
if (maxW <= 0) return "";
if (textwidth(LPCTSTR(text.c_str())) <= maxW) return text;
std::string ell = ellipsis ? ellipsis : "";
int ellW = textwidth(LPCTSTR(ell.c_str()));
if (ellW > maxW)
{ // 连省略号都放不下
std::string e = ell;
while (!e.empty() && textwidth(LPCTSTR(e.c_str())) > maxW) e.pop_back();
return e;
}
const int limit = maxW - ellW;
size_t i = 0, lastFit = 0;
while (i < text.size())
{
int clen = gbk_char_len(text, i);
size_t j = text.size() < i + (size_t)clen ? text.size() : i + (size_t)clen;
int w = textwidth(LPCTSTR(text.substr(0, j).c_str()));
if (w <= limit) { lastFit = j; i = j; }
else break;
}
if (lastFit == 0) return ell;
std::string head = text.substr(0, lastFit);
rtrim_spaces_gbk(head);
head += ell;
return head;
}
void Button::setTooltipStyle(COLORREF text, COLORREF bk, bool transparent)
{
tipLabel.textStyle.color = text;
tipLabel.setTextBkColor(bk);
tipLabel.setTextdisap(transparent);
}
void Button::setTooltipTextsForToggle(const std::string& onText, const std::string& offText)
{
tipTextOn = onText;
tipTextOff = offText;
tipUserOverride = true;
}
void Button::initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch)
{
this->id = "Button";
this->text = text;
this->mode = mode;
this->shape = shape;
this->buttonTrueColor = ct;
this->buttonFalseColor = cf;
this->buttonHoverColor = ch;
this->click = false;
this->hover = false;
// === Tooltip 默认:文本=按钮文本;白底黑字;不透明;用当前按钮字体样式 ===
tipTextClick = tipTextOn = tipTextOff = this->text;
tipLabel.setText(tipTextClick);
tipLabel.textStyle.color = (RGB(167, 170, 172));
tipLabel.setTextBkColor(RGB(255, 255, 255));
tipLabel.setTextdisap(false);
tipLabel.textStyle = this->textStyle; // 复用按钮字体样式
}
Button::~Button() = default;
void Button::draw()
{
if (!dirty || !show)return;
//保存当前样式和颜色
saveStyle();
StellarX::ControlText drawTextStyle = textStyle;
if (StellarX::ButtonMode::DISABLED == mode) //设置禁用按钮色
{
setfillcolor(DISABLEDCOLOUR);
// 禁用态删除线只属于本次绘制效果,不回写用户公开 textStyle。
drawTextStyle.bStrikeOut = true;
}
else
{
// 点击状态优先级最高,然后是悬停状态,最后是默认状态
COLORREF col = click ? buttonTrueColor : (hover ? buttonHoverColor : buttonFalseColor);
setfillcolor(col);
}
//
//设置字体背景色透明
setbkmode(TRANSPARENT);
//边框颜色
setlinecolor(buttonBorderColor);
//设置字体颜色
settextcolor(drawTextStyle.color);
//设置字体样式
settextstyle(drawTextStyle.nHeight, drawTextStyle.nWidth, drawTextStyle.lpszFace,
drawTextStyle.nEscapement, drawTextStyle.nOrientation, drawTextStyle.nWeight,
drawTextStyle.bItalic, drawTextStyle.bUnderline, drawTextStyle.bStrikeOut);
if (needCutText)
cutButtonText();
//获取字符串像素高度和宽度
if ((this->oldtext_width != this->text_width || this->oldtext_height != this->text_height)
|| (-1 == oldtext_width && oldtext_height == -1))
{
if (isUseCutText)
{
this->oldtext_width = this->text_width = textwidth(LPCTSTR(this->cutText.c_str()));
this->oldtext_height = this->text_height = textheight(LPCTSTR(this->cutText.c_str()));
}
else
{
this->oldtext_width = this->text_width = textwidth(LPCTSTR(this->text.c_str()));
this->oldtext_height = this->text_height = textheight(LPCTSTR(this->text.c_str()));
}
}
//设置按钮填充模式
setfillstyle((int)buttonFillMode, (int)buttonFillIma, buttonFileIMAGE.get());
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage)
saveBackground(this->x, this->y, (this->width + bordWith), (this->height + bordHeight));
// 恢复背景(清除旧内容)
restBackground();
//根据按钮形状绘制
switch (shape)
{
case StellarX::ControlShape::RECTANGLE://有边框填充矩形
fillrectangle(x, y, x + width, y + height);
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
: outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::B_RECTANGLE://无边框填充矩形
solidrectangle(x, y, x + width, y + height);
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
: outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::ROUND_RECTANGLE://有边框填充圆角矩形
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
: outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::B_ROUND_RECTANGLE://无边框填充圆角矩形
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
: outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::CIRCLE://有边框填充圆形
fillcircle(x + width / 2, y + height / 2, min(width, height) / 2);
isUseCutText ? outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(cutText.c_str()))
: outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::B_CIRCLE://无边框填充圆形
solidcircle(x + width / 2, y + height / 2, min(width, height) / 2);
isUseCutText ? outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(cutText.c_str()))
: outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::ELLIPSE://有边框填充椭圆
fillellipse(x, y, x + width, y + height);
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
: outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::B_ELLIPSE://无边框填充椭圆
solidellipse(x, y, x + width, y + height);
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
: outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
}
restoreStyle();//恢复默认字体样式和颜色
dirty = false; //标记按钮不需要重绘
if (tipEnabled && tipVisible)
tipLabel.draw();
}
// 处理鼠标事件,检测点击和悬停状态
// 根据按钮模式和形状进行不同的处理
bool Button::handleEvent(const ExMessage& msg)
{
if (!show)
return false;
resetEventVisualChanged();
bool oldHover = hover;// 注意:只在状态变化时记录,避免 WM_MOUSEMOVE 刷屏
bool oldClick = click;
const bool oldTipVisible = tipVisible;
bool consume = false;//是否消耗事件
const bool isMouseMessage =
msg.message == WM_MOUSEMOVE ||
msg.message == WM_LBUTTONDOWN ||
msg.message == WM_LBUTTONUP;
// 记录鼠标位置(用于tip定位)
if (isMouseMessage)
{
lastMouseX = msg.x;
lastMouseY = msg.y;
}
// 检测悬停状态(根据不同形状)
if (isMouseMessage)
{
switch (shape)
{
case StellarX::ControlShape::RECTANGLE:
case StellarX::ControlShape::B_RECTANGLE:
case StellarX::ControlShape::ROUND_RECTANGLE:
case StellarX::ControlShape::B_ROUND_RECTANGLE:
hover = (msg.x > x && msg.x < (x + width) && msg.y > y && msg.y < (y + height));
break;
case StellarX::ControlShape::CIRCLE:
case StellarX::ControlShape::B_CIRCLE:
hover = isMouseInCircle(msg.x, msg.y, x + width / 2, y + height / 2, min(width, height) / 2);
break;
case StellarX::ControlShape::ELLIPSE:
case StellarX::ControlShape::B_ELLIPSE:
hover = isMouseInEllipse(msg.x, msg.y, x, y, x + width, y + height);
break;
}
}
// 处理鼠标点击事件
if (msg.message == WM_LBUTTONDOWN && hover && mode != StellarX::ButtonMode::DISABLED)
{
if (mode == StellarX::ButtonMode::NORMAL)
{
click = true;
SX_LOGD("Button") << SX_T("被点击: ","lbtn - down:")<< "id = " << id <<" text = "<<text << " mode = " << (int)mode;
dirty = true;
consume = true;
}
else if (mode == StellarX::ButtonMode::TOGGLE)
{
// TOGGLE模式在鼠标释放时处理
}
}
// NORMAL 模式:鼠标在按钮上释放时才触发点击回调,如果移出区域则取消点击状态。
// TOGGLE 模式:在释放时切换状态,并触发相应的开/关回调。
else if (msg.message == WM_LBUTTONUP && hover && mode != StellarX::ButtonMode::DISABLED)
{
hideTooltip(); // 隐藏悬停提示
if (mode == StellarX::ButtonMode::NORMAL && click)
{
if (onClickCallback) onClickCallback();
SX_LOGI("Button") << "click: id=" << id << " (NORMAL) callback=" << (onClickCallback ? "Y" : "N");
click = false;
dirty = true;
consume = true;
hideTooltip();
}
else if (mode == StellarX::ButtonMode::TOGGLE)
{
click = !click;
if (click && onToggleOnCallback) onToggleOnCallback();
else if (!click && onToggleOffCallback) onToggleOffCallback();
SX_LOGI("Button") << "toggle: id=" << id
<< " " << (oldClick ? 1 : 0) << "->" << (click ? 1 : 0)
<< " onCb=" << (onToggleOnCallback ? "Y" : "N")
<< " offCb=" << (onToggleOffCallback ? "Y" : "N");
dirty = true;
consume = true;
refreshTooltipTextForState();
hideTooltip();
}
}
// 处理鼠标移出区域的情况
else if (msg.message == WM_MOUSEMOVE)
{
if (!hover && mode == StellarX::ButtonMode::NORMAL && click)
{
click = false;
dirty = true;
}
else if (hover != oldHover)
dirty = true;
}
if (tipEnabled)
{
if (hover && !oldHover)
{
// 刚刚进入悬停:开计时,暂不显示
tipHoverTick = GetTickCount64();
tipVisible = false;
}
if (!hover && oldHover)
{
// 刚移出:立即隐藏
hideTooltip();
}
if (hover && !tipVisible)
{
// 到点就显示
if (GetTickCount64() - tipHoverTick >= (ULONGLONG)tipDelayMs)
{
tipVisible = true;
// 定位(跟随鼠标 or 相对按钮)
int tipX = tipFollowCursor ? (lastMouseX + tipOffsetX) : lastMouseX;
int tipY = tipFollowCursor ? (lastMouseY + tipOffsetY) : y + height;
// 设置文本(用户可能动态改了提示文本
if (tipUserOverride)
{
if (mode == StellarX::ButtonMode::NORMAL)
tipLabel.setText(tipTextClick);
else if (mode == StellarX::ButtonMode::TOGGLE)
tipLabel.setText(click ? tipTextOn : tipTextOff);
}
else
if (mode == StellarX::ButtonMode::TOGGLE)
tipLabel.setText(click ? tipTextOn : tipTextOff);
// 设置位置
tipLabel.setX(tipX);
tipLabel.setY(tipY);
// 标记需要绘制
tipLabel.setDirty(true);
}
}
}
// 如果状态发生变化,标记需要重绘
const bool stateChanged = (hover != oldHover || click != oldClick);
if (stateChanged)
dirty = true;
const bool tipVisibilityChanged = (tipVisible != oldTipVisible);
if (tipVisibilityChanged)
dirty = true;
// 事件吞噬规则:
// - 鼠标移动:只有“当前命中按钮”时才吞掉,避免前一个按钮在清 hover 时截断消息,
// 导致后一个真正命中的按钮收不到 WM_MOUSEMOVE。
// 这里的前提是:父容器会保证“没有拿到真实消息的后续兄弟”还能走一遍
// clearTransientMouseState(),从而清掉旧 hover / tooltip。
// - 鼠标按下/抬起:命中按钮区域时吞掉,避免点击穿透到底层控件。
if (msg.message == WM_MOUSEMOVE)
{
if (hover)
consume = true;
}
else if ((msg.message == WM_LBUTTONDOWN || msg.message == WM_LBUTTONUP) && hover)
{
consume = true;
}
markEventVisualChanged(stateChanged || tipVisibilityChanged);
// 如果需要重绘,立即执行
if (dirty)
requestRepaint(parent);
return consume;
}
bool Button::clearTransientMouseState()
{
if (!show)
return false;
bool hoverChanged = false;
bool normalClickChanged = false;
bool bodyVisualChanged = false;
bool tooltipChanged = false;
if (hover)
{
hover = false;
hoverChanged = true;
bodyVisualChanged = true;
}
// NORMAL 模式下的按下态只属于一次鼠标交互过程;
// 如果本次 WM_MOUSEMOVE 已经被同层更上层的兄弟控件消费,
// 当前按钮就不应继续保留这笔临时按下视觉。
if (mode == StellarX::ButtonMode::NORMAL && click)
{
click = false;
normalClickChanged = true;
bodyVisualChanged = true;
}
if (tipVisible)
{
hideTooltip();
tooltipChanged = true;
}
const bool stateChanged = bodyVisualChanged || tooltipChanged;
if (!stateChanged)
return false;
SX_LOG_TRACE("Event")
<< SX_T("清理按钮鼠标临时状态:id=", "clear button transient mouse state: id=")
<< id
<< SX_T(" hover=", " hover=") << (hoverChanged ? 1 : 0)
<< SX_T(" tooltip=", " tooltip=") << (tooltipChanged ? 1 : 0)
<< SX_T(" normalClick=", " normalClick=") << (normalClickChanged ? 1 : 0);
markEventVisualChanged(true);
// 只有按钮本体视觉真正变化时,才请求按钮自身区域重绘;
// 若只是 Tooltip 消失,hideTooltip() 已经通过回贴快照清掉了悬浮层区域,
// 不再额外重绘按钮本体,避免高频鼠标移动时的无意义重画。
if (bodyVisualChanged)
{
dirty = true;
requestRepaint(parent);
}
return true;
}
RECT Button::getManagedRepaintCoverageRect() const
{
RECT coverage = getBoundsRect();
if (!tipEnabled || !tipVisible)
return coverage;
const RECT tipRect = tipLabel.getManagedRepaintCoverageRect();
coverage.left = (std::min)(coverage.left, tipRect.left);
coverage.top = (std::min)(coverage.top, tipRect.top);
coverage.right = (std::max)(coverage.right, tipRect.right);
coverage.bottom = (std::max)(coverage.bottom, tipRect.bottom);
return coverage;
}
RECT Button::getManagedRepaintPersistentCoverageRect() const
{
// Tooltip 属于临时浮层,只参与完整 coverage 和上层 overlay 判断;
// 兄弟控件是否需要作废背景快照,只应由按钮本体这类持久绘制范围决定。
return getBoundsRect();
}
void Button::setOnClickListener(std::function<void()> callback)
{
this->onClickCallback = std::move(callback);
}
void Button::setOnToggleOnListener(std::function<void()> callback)
{
this->onToggleOnCallback = std::move(callback);
}
void Button::setOnToggleOffListener(std::function<void()> callback)
{
this->onToggleOffCallback = std::move(callback);
}
void Button::setbuttonMode(StellarX::ButtonMode mode)
{
//取值范围参考 buttMode的枚举注释
this->mode = mode;
dirty = true; // 标记需要重绘
}
void Button::setROUND_RECTANGLEwidth(int width)
{
rouRectangleSize.ROUND_RECTANGLEwidth = width;
this->dirty = true; // 标记需要重绘
}
void Button::setROUND_RECTANGLEheight(int height)
{
rouRectangleSize.ROUND_RECTANGLEheight = height;
this->dirty = true; // 标记需要重绘
}
bool Button::isClicked() const
{
return this->click;
}
void Button::setFillMode(StellarX::FillMode mode)
{
this->buttonFillMode = mode;
this->dirty = true; // 标记需要重绘
}
void Button::setFillIma(StellarX::FillStyle ima)
{
buttonFillIma = ima;
this->dirty = true;
}
void Button::setFillIma(std::string imaNAme)
{
if (buttonFileIMAGE)
{
buttonFileIMAGE.reset();
}
buttonFileIMAGE = std::make_unique<IMAGE>();
loadimage(buttonFileIMAGE.get(), imaNAme.c_str(), width, height);
this->dirty = true;
}
void Button::setButtonBorder(COLORREF Border)
{
buttonBorderColor = Border;
this->dirty = true;
}
void Button::setButtonFalseColor(COLORREF color)
{
this->buttonFalseColor = color;
this->dirty = true;
}
void Button::setButtonText(const char* text)
{
this->text = std::string(text);
this->text_width = textwidth(LPCTSTR(this->text.c_str()));
this->text_height = textheight(LPCTSTR(this->text.c_str()));
this->dirty = true;
this->needCutText = true;
if (!tipUserOverride)
tipTextClick = tipTextOn = tipTextOff = text;
}
void Button::setButtonText(std::string text)
{
this->text = text;
this->text_width = textwidth(LPCTSTR(this->text.c_str()));
this->text_height = textheight(LPCTSTR(this->text.c_str()));
this->dirty = true; // 标记需要重绘
this->needCutText = true;
if (!tipUserOverride)
tipTextClick = tipTextOn = tipTextOff = text;
}
void Button::setButtonShape(StellarX::ControlShape shape)
{
this->shape = shape;
this->dirty = true;
this->needCutText = true;
}
//允许通过外部函数修改按钮的点击状态,并执行相应的回调函数
void Button::setButtonClick(BOOL click)
{
const bool targetClick = (click != FALSE);
// TOGGLE 状态若没有发生变化,就不应重复触发 onToggleOn/OffCallback。
// 否则像 TabControl 这种“外部再次激活已激活页签”的场景,
// 会把页面显示/隐藏、快照失效、重绘链整条重复执行一遍,最终破坏可见页的背景恢复语义。
if (mode == StellarX::ButtonMode::TOGGLE && this->click == targetClick)
return;
this->click = targetClick;
if (mode == StellarX::ButtonMode::NORMAL && click)
{
if (onClickCallback) onClickCallback();
dirty = true;
hideTooltip();
}
else if (mode == StellarX::ButtonMode::TOGGLE)
{
if (click && onToggleOnCallback) onToggleOnCallback();
else if (!click && onToggleOffCallback) onToggleOffCallback();
dirty = true;
refreshTooltipTextForState();
hideTooltip();
}
if (dirty)
requestRepaint(parent);
}
std::string Button::getButtonText() const
{
return this->text;
}
const char* Button::getButtonText_c() const
{
return this->text.c_str();
}
StellarX::ButtonMode Button::getButtonMode() const
{
return this->mode;
}
StellarX::ControlShape Button::getButtonShape() const
{
return this->shape;
}
StellarX::FillMode Button::getFillMode() const
{
return this->buttonFillMode;
}
StellarX::FillStyle Button::getFillIma() const
{
return this->buttonFillIma;
}
IMAGE* Button::getFillImaImage() const
{
return this->buttonFileIMAGE.get();
}
COLORREF Button::getButtonBorder() const
{
return this->buttonBorderColor;
}
COLORREF Button::getButtonTextColor() const
{
return this->textStyle.color;
}
StellarX::ControlText Button::getButtonTextStyle() const
{
return this->textStyle;
}
bool Button::isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius)
{
double dis = sqrt(pow(mouseX - x, 2) + pow(mouseY - y, 2));
if (dis <= radius)
return true;
else
return false;
}
bool Button::isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height)
{
int centerX = (x + width) / 2;
int centerY = (y + height) / 2;
int majorAxis = (width - x) / 2;
int minorAxis = (height - y) / 2;
double dx = mouseX - centerX;
double dy = mouseY - centerY;
double normalizedDistance = (dx * dx) / (majorAxis * majorAxis) + (dy * dy) / (minorAxis * minorAxis);
// 判断鼠标是否在椭圆内
if (normalizedDistance <= 1.0)
return true;
else
return false;
}
void Button::cutButtonText()
{
const int contentW = 1 > this->width - 2 * padX ? 1 : this->width - 2 * padX;
// 放得下:不截断,直接用原文
if (textwidth(LPCTSTR(this->text.c_str())) <= contentW) {
isUseCutText = false;
needCutText = false;
cutText.clear();
return;
}
// 放不下:按语言偏好裁切(ASCII→词边界;CJK→逐字符,不撕裂双字节)
if (is_ascii_only(this->text))
{
cutText = ellipsize_ascii_pref(this->text, contentW); // "..."
}
else
{
cutText = ellipsize_cjk_pref(this->text, contentW, ""); // 全角省略号
}
isUseCutText = true;
needCutText = false;
}
void Button::hideTooltip()
{
if (tipVisible)
{
tipVisible = false;
// Tooltip 是 Button 的内置浮层,不属于独立控件树节点。
// 因此在隐藏时应直接回贴它自己的背景快照并作废,
// 不能仅仅作废快照,否则当本轮只重绘按钮本体区域时,
// Tooltip 占用的那块屏幕可能无人擦除,最终表现为“鼠标移开后提示框残留”。
tipLabel.hide(); // 还原快照 + 作废快照,立即清掉 Tooltip 自身绘制区域
tipHoverTick = GetTickCount64(); // 重置计时基线
}
}
void Button::refreshTooltipTextForState()
{
if (tipUserOverride) return; // 用户显式设置过 tipText,保持不变
if (mode == StellarX::ButtonMode::NORMAL)
tipLabel.setText(tipTextClick);
else if (mode == StellarX::ButtonMode::TOGGLE)
tipLabel.setText(click ? tipTextOn : tipTextOff);
}
-196
View File
@@ -1,196 +0,0 @@
/*******************************************************************************
* @类: Button
* @摘要: 多功能按钮控件,支持多种状态和样式
* @描述:
* 提供完整的按钮功能,包括普通点击、切换模式、禁用状态。
* 支持多种形状(矩形、圆形、椭圆等)和丰富的视觉样式。
* 通过回调函数机制实现灵活的交互逻辑。
*
* @特性:
* - 支持三种工作模式:普通、切换、禁用
* - 八种几何形状,各有边框和无边框版本
* - 自定义颜色(默认、悬停、点击状态)
* - 多种填充模式(纯色、图案、图像)
* - 完整的鼠标事件处理(点击、悬停、移出)
*
* @使用场景: 作为主要交互控件,用于触发动作或表示状态
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
#pragma once
#include "Control.h"
#include"label.h"
#define DISABLEDCOLOUR RGB(96, 96, 96) //禁用状态颜色
#define TEXTMARGINS_X 6
#define TEXTMARGINS_Y 4
constexpr int bordWith = 1; //边框宽度,用于快照恢复时的偏移计算
constexpr int bordHeight = 1; //边框高度,用于快照恢复时的偏移计算
class Button : public Control
{
std::string text; // 按钮上的文字
bool click; // 是否被点击
bool hover; // 是否被悬停
std::string cutText; // 切割后的文本
bool needCutText = true; // 是否需要切割文本
bool isUseCutText = false; // 是否使用切割文本
int padX = TEXTMARGINS_X; // 文本最小左右内边距
int padY = TEXTMARGINS_Y; // 文本最小上下内边距
COLORREF buttonTrueColor; // 按钮被点击后的颜色
COLORREF buttonFalseColor; // 按钮未被点击的颜色
COLORREF buttonHoverColor; // 按钮被鼠标悬停的颜色
COLORREF buttonBorderColor = RGB(0, 0, 0);// 按钮边框颜色
StellarX::ButtonMode mode; // 按钮模式
StellarX::ControlShape shape; // 按钮形状
StellarX::FillMode buttonFillMode = StellarX::FillMode::Solid; //按钮填充模式
StellarX::FillStyle buttonFillIma = StellarX::FillStyle::BDiagonal; //按钮填充图案
std::unique_ptr<IMAGE> buttonFileIMAGE; //按钮填充图像
std::function<void()> onClickCallback; //回调函数
std::function<void()> onToggleOnCallback; //TOGGLE模式下的回调函数
std::function<void()> onToggleOffCallback; //TOGGLE模式下的回调函数
StellarX::ControlText oldStyle = textStyle; // 按钮文字样式
int oldtext_width = -1;
int oldtext_height = -1;
int text_width = 0;
int text_height = 0;
// === Tooltip ===
bool tipEnabled = false; // 是否启用
bool tipVisible = false; // 当前是否显示
bool tipFollowCursor = false; // 是否跟随鼠标
bool tipUserOverride = false; // 是否用户自定义了tip文本
int tipDelayMs = 1000; // 延时(毫秒)
int tipOffsetX = 12; // 相对鼠标偏移
int tipOffsetY = 18;
ULONGLONG tipHoverTick = 0; // 开始悬停的时间戳
int lastMouseX = 0; // 最新鼠标位置(用于定位)
int lastMouseY = 0;
std::string tipTextClick; // NORMAL 模式下用
std::string tipTextOn; // click==true 时用
std::string tipTextOff; // click==false 时用
Label tipLabel; // 直接复用Label作为提示
public:
StellarX::ControlText textStyle; // 按钮文字样式
public:
//默认按钮颜色
Button(int x, int y, int width, int height, const std::string text,
StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
//自定义按钮未被点击和被点击颜色
Button(int x, int y, int width, int height, const std::string text,
COLORREF ct, COLORREF cf, StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL,
StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
//自定义按钮颜色和悬停颜色
Button(int x, int y, int width, int height, const std::string text,
COLORREF ct, COLORREF cf, COLORREF ch,
StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
//析构函数 释放图形指针内存
~Button();
//绘制按钮
void draw() override;
//按钮事件处理
bool handleEvent(const ExMessage& msg) override;
// 清理按钮的鼠标瞬时状态;用于父容器在 WM_MOUSEMOVE 短路后补做 hover/tooltip 收口。
bool clearTransientMouseState() override;
// Tooltip 可见时,按钮实际写像素范围不再等于按钮本体,需要把 Tooltip 矩形并入 coverage。
RECT getManagedRepaintCoverageRect() const override;
// Button 的持久绘制范围只包含本体;Tooltip 是临时浮层,不能污染兄弟控件背景快照。
RECT getManagedRepaintPersistentCoverageRect() const override;
// 设置 NORMAL 模式下的点击回调
void setOnClickListener(std::function<void()> callback);
// 设置 TOGGLE 模式下切换到打开状态时的回调
void setOnToggleOnListener(std::function<void()> callback);
// 设置 TOGGLE 模式下切换到关闭状态时的回调
void setOnToggleOffListener(std::function<void()> callback);
// 设置按钮模式(NORMAL / TOGGLE / DISABLED
void setbuttonMode(StellarX::ButtonMode mode);
// 设置圆角矩形的圆角宽度
void setROUND_RECTANGLEwidth(int width);
// 设置圆角矩形的圆角高度
void setROUND_RECTANGLEheight(int height);
// 设置按钮填充模式
void setFillMode(StellarX::FillMode mode);
// 设置按钮图案填充样式
void setFillIma(StellarX::FillStyle ima);
// 设置按钮图像填充资源
void setFillIma(std::string imaName);
// 设置按钮边框颜色
void setButtonBorder(COLORREF Border);
// 设置按钮默认态颜色
void setButtonFalseColor(COLORREF color);
// 设置按钮文本(char* 重载)
void setButtonText(const char* text);
// 设置按钮文本(std::string 重载)
void setButtonText(std::string text);
// 设置按钮几何形状
void setButtonShape(StellarX::ControlShape shape);
// 直接设置按钮点击状态;NORMAL 设置为 true 时触发 onClickTOGGLE 仅在状态变化时触发 on/off 回调
void setButtonClick(BOOL click);
// 查询按钮当前是否处于点击/选中状态
bool isClicked() const;
// 获取按钮文本
std::string getButtonText() const;
// 获取按钮文本的 C 字符串视图
const char* getButtonText_c() const;
// 获取按钮模式
StellarX::ButtonMode getButtonMode() const;
// 获取按钮形状
StellarX::ControlShape getButtonShape() const;
// 获取按钮填充模式
StellarX::FillMode getFillMode() const;
// 获取按钮图案填充样式
StellarX::FillStyle getFillIma() const;
// 获取按钮图像填充资源
IMAGE* getFillImaImage() const;
// 获取按钮边框颜色
COLORREF getButtonBorder() const;
// 获取按钮文字颜色
COLORREF getButtonTextColor() const;
// 获取按钮文字样式
StellarX::ControlText getButtonTextStyle() const;
public:
// === Tooltip API===
// 开关 Tooltip;关闭时只修改内部启用/显示状态,不负责智能选位
void enableTooltip(bool on) { tipEnabled = on; if (!on) tipVisible = false; }
// 设置 Tooltip 延时(毫秒)
void setTooltipDelay(int ms) { tipDelayMs = (ms < 0 ? 0 : ms); }
// 设置 Tooltip 是否跟随鼠标
void setTooltipFollowCursor(bool on) { tipFollowCursor = on; }
// 设置 Tooltip 偏移量;当前仅在 setTooltipFollowCursor(true) 时参与定位
void setTooltipOffset(int dx, int dy) { tipOffsetX = dx; tipOffsetY = dy; }
// 设置 Tooltip 的文字、背景和透明样式
void setTooltipStyle(COLORREF text, COLORREF bk, bool transparent);
// 设置 NORMAL 模式下 Tooltip 文本
void setTooltipText(const std::string& s) { tipTextClick = s; tipUserOverride = true; }
// 设置 TOGGLE 模式开/关两种状态下的 Tooltip 文本
void setTooltipTextsForToggle(const std::string& onText, const std::string& offText);
private:
// 初始化按钮内部状态、颜色和 Tooltip 默认样式
void initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch);
// 判断鼠标是否在圆形按钮内
bool isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius);
// 判断鼠标是否在椭圆按钮内
bool isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height);
//获取对话框类型
bool model() const override { return false; }
// 按当前按钮宽度和语言特征裁剪文本
void cutButtonText();
// 统一隐藏 Tooltip 并恢复其背景快照
void hideTooltip();
// 根据当前 click 状态选择 Tooltip 文案
void refreshTooltipTextForState();
};
+207
View File
@@ -0,0 +1,207 @@
# Changelog
All notable changes to the StellarX project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
[中文文档](CHANGELOG.md)
## [v2.1.0] - 2025-10-27
**Focus**: Resizable/maximized window (EasyX reinforced by Win32), first-phase layout manager (HBox/VBox), early Tabs control. We also fixed black borders, maximize “ghosts”, flicker, and the issue where controls only appeared after interaction. Control-level **background snapshot/restore**, **single-line truncation**, and **tooltips** are now standardized.
### ✨ Added
- **Window resize/maximize reinforcement (EasyX + Win32)**
- `Window::enableResize(bool enable, int minW, int minH)`; toggle at runtime and set min track size.
- Subclassed WndProc for `WM_GETMINMAXINFO / WM_SIZE / WM_EXITSIZEMOVE / WM_ERASEBKGND / WM_PAINT` with `WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_CLIPCHILDREN | WS_CLIPSIBLINGS`.
- **Full-surface background paint** in `WM_PAINT` (solid/image), removing black borders and maximize ghosts.
- **One-shot redraw** on resize with `pendingW/H + needResizeDirty` coalescing.
- **Layout manager (phase 1)**
- On `Canvas`: `LayoutKind::{Absolute, HBox, VBox, Grid(placeholder), Flow(placeholder), Stack(placeholder)}` with `LayoutParams` (`margins`, `fixedW/fixedH(-1=same)`, `weight`, `Align{Start,Center,End,Stretch}`).
- Implemented **HBox/VBox** auto layout inside containers; containers stay absolutely positioned; nesting supported.
- **Tabs control (early)**
- Decoupled tab strip and page container; background snapshot for transparent themes.
- **Button single-line truncation (MBCS; CN/EN aware)**
- Pixel-width threshold with `...` avoiding half-glyph artifacts.
- **Hover tooltip**
- Implemented via `Label` with delay & auto-hide; per-control background snapshot/restore; customizable text with sensible fallback.
### 🔧 Changed
- **Window rendering path**
- Reduced reliance on “offscreen frame blit”; in `WM_PAINT` use **GDI full background + EasyX batch drawing** (`BeginBatchDraw/EndBatchDraw + FlushBatchDraw`) to suppress flicker.
- On resize, only rebuild the scaled background (`zoomBackground`); actual painting happens next frame.
- **Control base**
- Standardized **captureBackground/restoreBackground**; transparent/stacked visuals are stable.
- Unified `dirty`: containers paint their own background when dirty; **children still evaluate/draw** as needed each frame.
- **Table**
- In transparent mode, pagination widgets now inherit table text/fill style.
- Reworked **pagination math + block centering** (header included in available width).
- Fixed background snapshot sizing (header inclusion) that caused failed restores.
- **Event loop**
- Coalesced `WM_SIZE` to the loop tail to avoid redraw storms and pointer-hover lag.
### ✅ Fixed
- **Black borders / maximize ghost / flicker**: blocked system background erase; full-surface paint in `WM_PAINT`; cleared clipping to prevent stale fragments.
- **Containers not drawn; controls only after interaction**: first-frame & post-input full-tree dirty; children draw even if the container isnt dirty.
- **Table pagination overlap & transparent shadowing**: corrected snapshot area; recomputed coordinates; instant restore + redraw after paging.
### ⚠️ Breaking
- If external code accessed `Window` private members (e.g., `dialogs`), use `getControls()` / `getdialogs()`.
- Pagination math & header inclusion may shift hard-coded offsets in custom renderers.
- Custom controls that dont restore `SetWorkingImage(nullptr)` before drawing should be reviewed.
### 📌 Upgrade notes
1. Migrate manual `cleardevice()+putimage` paths to unified **full-surface background** in `WM_PAINT`.
2. For transparent controls, `captureBackground()` before first draw; `restoreBackground()` when hiding/overdrawing.
3. For layout, set container `layout.kind = HBox/VBox` and child `LayoutParams` (`margin/fixed/weight/align`).
4. Keep a **single** truncation pass for buttons to avoid duplicate `...`.
5. Prefer built-in table pagination.
## [v2.0.1] - 2025-10-03
### Added
- New example: 32-bit register viewer tool implemented based on StellarX (supports bit inversion, left shift, right shift, hexadecimal/decimal signed/unsigned toggle, grouped binary display).
- Example path: `examples/register-viewer/`
- `TextBox` added `setText` API, allowing external setting of text box content
- `TextBox::setText` API modified: immediately calls `draw` method to redraw after setting text
- `Button` added `setButtonClick` API, allowing external functions to modify button click state and execute corresponding callback functions
- ==All documents updated with corresponding English versions(.en)==
## [v2.0.0] - 2025-09-21
### Overview
v2.0.0 is a major release. This release introduces dialog and message box factory (Dialog / MessageBox), with several important fixes and improvements to event distribution, API semantics, and internal resource management.
Some APIs/behaviors have breaking changes that are not backward compatible.
### Added
- **Dialog System**:
- Added `Dialog` class, inheriting from `Canvas`, for building modal and non-modal dialogs
- Added `MessageBox` factory class, providing two convenient APIs: `ShowModal` (synchronous blocking) and `ShowAsync` (asynchronous callback)
- Supports six standard message box types: `OK`, `OKCancel`, `YesNo`, `YesNoCancel`, `RetryCancel`, `AbortRetryIgnore`
- Automatically handles dialog layout, background saving and restoration, event propagation, and lifecycle management
- **Enhanced Event System**:
- All controls' `handleEvent` methods now return `bool` type, indicating whether the event was consumed
- Introduced event consumption mechanism, supporting finer-grained event propagation control
- Window class event loop now prioritizes dialog event processing
- **Control State Management**:
- Control base class added `dirty` flag and `setDirty()` method, uniformly managing redraw state
- All controls now implement `IsVisible()` and `model()` methods
- **API Enhancements**:
- Button class added `setButtonFalseColor()` method
- TextBox class `setMaxCharLen()` now accepts `size_t` type parameter
- Window class added dialog management methods and duplicate detection mechanism
### Breaking Changes
- **API Signature Changes**:
- All controls' `handleEvent(const ExMessage& msg)` method changed from returning `void` to returning `bool`
- Control base class added pure virtual functions `IsVisible() const` and `model() const`, all derived classes must implement them
- **Resource Management Changes**:
- Control base class style saving changed from stack objects to heap objects, managed using pointers
- Enhanced resource release safety, all resources are properly released in destructors
- **Event Handling Logic**:
- Window's `runEventLoop()` method completely rewritten, now prioritizes dialog events
- Introduced event consumption mechanism, events do not continue propagating after being consumed
### Fixed
- **Memory Management**:
- Fixed memory leak issue in `Button::setFillIma()`
- Fixed resource release issues in Control base class destructor
- Fixed background image resource management issues in Dialog class
- **Layout and Rendering**:
- Fixed pagination calculation, column width, and row height boundary issues in `Table` component
- Fixed layout disorder caused by `pX` coordinate accumulation error in `Table`
- Fixed dirty determination issue in `Canvas::draw()` that prevented child controls from being drawn
- Fixed asymmetric call issues between `TextBox::draw()` and `restoreStyle()`
- **Event Handling**:
- Fixed window event distribution logic to ensure dialog/top-level controls prioritize event processing
- Fixed delayed state updates when mouse moves out of button area
- Fixed race conditions in non-modal dialog event handling
- **Other Issues**:
- Fixed potential errors in text measurement and rendering
- Fixed incomplete background restoration after dialog closure
- Fixed z-order management issues in multi-dialog scenarios
### Changed
- **Code Quality**:
- Refactored numerous internal APIs, enhancing exception safety and maintainability
- Used smart pointers and modern C++ features to replace raw new/delete
- Unified code style and naming conventions
- **Performance Optimization**:
- Optimized event processing flow, reducing unnecessary redraws
- Improved dialog background saving and restoration mechanism
- Reduced memory allocation and copy operations
- **Documentation and Examples**:
- Added detailed usage examples for all new features
- Improved code comments and API documentation
- Updated README and CHANGELOG to reflect latest changes
## [1.1.0] - 2025-09-08
### Added
- **Window Class API Enhancements**:
- Added complete getter method set, improving class encapsulation and usability
- `getHwnd()` - Get window handle for integration with native Windows API
- `getWidth()` - Get window width
- `getHeight()` - Get window height
- `getHeadline()` - Get window title
- `getBkcolor()` - Get window background color
- `getBkImage()` - Get window background image pointer
- `getControls()` - Get reference to control management container, allowing iteration and manipulation of added controls
### Improved
- **API Consistency**: Provided symmetric setter and getter methods for all important attributes
- **Code Documentation**: Further improved class comments, making them clearer and more professional
## [1.0.0] - 2025-09-08
### Release Summary
First stable release
### Added
- First stable version of StellarX framework
- Complete control library: Button, Label, TextBox, Canvas, Table, and Window
- CMake-based build system
- Detailed documentation and example code
- **Explicit declaration: This framework is specifically designed for Windows platform**
### Released
- **First release of pre-compiled binary library files**, facilitating quick integration without compiling from source
- Provided release packages include:
- `StellarX-v1.0.0-x64-Windows-msvc-x64.zip`
- **Build Environment**: Visual Studio 2022 (MSVC v143)
- **Architecture**: x64 (64-bit)
- **Runtime Library**: `/MD`
- **Build Modes**: `Release | Debug`
- **Contents**: Includes all necessary header files (`include/`) and static library files (`lib/StellarX-Debug.lib StellarX-Release.lib`)
### Core Features
- Modular design following SOLID principles
- Unified control interface (`draw()` and `handleEvent()`)
- Support for multiple control shapes and styles
- Custom event handling callbacks
- Lightweight design with no external dependencies
## [0.1.0] - 2025-08-15
### Added
- Initial project structure and core architecture
- Control base class and basic event handling system
- Basic examples and documentation setup
+207
View File
@@ -0,0 +1,207 @@
# 更新日志
StellarX 项目所有显著的变化都将被记录在这个文件中。
格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
[English document](CHANGELOG.en.md)
## [v2.1.0] - 2025-10-27
**重点**:窗口可拉伸/最大化补强(EasyX + Win32)、布局管理器(HBox/VBox 第一阶段)、选项卡控件雏形;系统性修复黑边、最大化残影、频闪与“控件需交互才出现”等历史问题。并统一了**背景快照/恢复**、**按钮单行截断**、**Tooltip** 的机制。
### ✨ 新增
- **窗口拉伸 / 最大化补强(在 EasyX 基础上用 Win32 加固)**
- 新增 `Window::enableResize(bool enable, int minW, int minH)`;运行时开关可拉伸并设置最小跟踪尺寸。
- 子类化窗口过程:处理 `WM_GETMINMAXINFO / WM_SIZE / WM_EXITSIZEMOVE / WM_ERASEBKGND / WM_PAINT`,并启用 `WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_CLIPCHILDREN | WS_CLIPSIBLINGS`,解决 EasyX 窗口原生不可拉伸问题。
- **整窗背景绘制**:在 `WM_PAINT` 用 GDI 直写客户区(纯色/图片均支持),彻底消除黑边与最大化残影。
- **一次性重绘**:合并连续 `WM_SIZE`,使用 `pendingW/H + needResizeDirty` 在主循环尾部统一置脏重绘,避免死循环与抖动。
- **布局管理器(第一阶段)**
-`Canvas` 引入布局元数据:`LayoutKind::{Absolute, HBox, VBox, Grid(预留), Flow(预留), Stack(预留)}``LayoutParams``margin{L,R,T,B}``fixedW/fixedH(-1=沿用)``weight``Align{Start,Center,End,Stretch}`)。
- 实装 **HBox/VBox** 自动布局(容器内部自动排布;容器本身仍绝对定位,可嵌套)。
- **选项卡(Tabs)控件雏形**
- 页签条与页面容器解耦;多页签切换;为透明主题提供**背景快照**避免叠影。
- **按钮文本单行截断(MBCS:中/英分治)**
- 基于像素宽度阈值的 `...` 截断,避免半字节/半汉字。
- **悬停提示(Tooltip**
-`Label` 为载体,支持延时出现、自动隐藏、自定义文案(默认回退到按钮文本);使用控件级**背景快照/恢复**。
### 🔧 变更
- **Window 渲染路径**
- 弱化“离屏 frame→整屏贴图”的依赖;在 `WM_PAINT`**GDI 整窗背景 + EasyX 批量绘制**`BeginBatchDraw/EndBatchDraw + FlushBatchDraw`)以抑制频闪。
- 尺寸变化仅重建 `zoomBackground`(图片背景的缩放副本),显示延后到下一帧。
- **Control 基类**
- 标准化 **captureBackground/restoreBackground**;统一透明/叠放控件行为。
- 统一 `dirty` 语义:容器自身脏才重画背景,但**子控件每帧评估并按需绘制**,不再出现“容器不脏→子控件不画”的空窗。
- **Table**
- 透明模式下,分页按钮与页码标签跟随表格的文本/填充风格,避免风格漂移。
- **分页度量与整体居中**重做:可用宽度含表头;“上一页/下一页+页码”作为**一个块**水平居中。
- 修复**背景快照区域**未计入表头导致恢复失败的问题。
- **事件循环**
- 合并 `WM_SIZE` 到循环尾统一处理,降低输入延迟与重绘风暴。
### ✅ 修复
- **黑边 / 最大化残影 / 频闪**:`WM_ERASEBKGND` 返回 1 禁止系统擦背景;`WM_PAINT` 全面覆盖;清空裁剪区防止旧帧残留。
- **容器不画、控件需交互后才出现**:首帧与输入后触发**全树置脏**;子控件在容器未脏时也能正常绘制。
- **Table 翻页重叠与透明叠影**:修正快照区域、重算坐标并即时恢复+重绘。
### ⚠️ 可能不兼容
- 若外部代码直接访问 `Window` 私有成员(如 `dialogs`),请改用 `getControls()` / `getdialogs()`
- `Table` 的分页与表头度量变化可能影响外部硬编码偏移;需要对齐新公式。
- 自定义控件若未遵循绘制前 `SetWorkingImage(nullptr)` 的约定,请自查。
### 📌 升级指引
1. **窗口背景**:将手工 `cleardevice()+putimage` 迁移到 `WM_PAINT` 的**整窗覆盖**流程。
2. **透明控件**:首绘前 `captureBackground()`,隐藏/覆盖时 `restoreBackground()`
3. **布局**:容器设 `layout.kind = HBox/VBox`;子项用 `LayoutParams``margin/fixed/weight/align`)。
4. **按钮截断**:保持单次截断,避免重复 `...`
5. **表格**:移除自绘分页,使用内置导航。
## [v2.0.1] - 2025 - 10 - 03
### 新增
- 新增示例:基于 StellarX 实现的 32 位寄存器查看工具(支持位取反、左移、右移,十六进制/十进制带符号/无符号切换,分组二进制显示)。
- 示例路径:`examples/register-viewer/`
- `TextBox`新增`setText`API,可在外部设置文本框内容
- `TextBox::setText`API修改:在设置文本后立即调用`draw`方法重绘
- `Button`新增`setButtonClick`API,允许通过外部函数修改按钮的点击状态,并执行相应的回调函数
- ==所有文档更新对应英文版本==
## [v2.0.0] - 2025-09-21
### 概述
v2.0.0 为一次重大升级(major release)。本次发布新增对话框与消息框工厂(Dialog / MessageBox),并对事件分发、API语义与内部资源管理做了若干重要修复和改进。
部分 API/行为发生不向后兼容的变化(breaking changes)。
### 新增
- **对话框系统**:
- 新增 `Dialog` 类,继承自 `Canvas`,用于构建模态与非模态对话框
- 新增 `MessageBox` 工厂类,提供 `ShowModal`(同步阻塞)与 `ShowAsync`(异步回调)两种便捷API
- 支持六种标准消息框类型:`OK`, `OKCancel`, `YesNo`, `YesNoCancel`, `RetryCancel`, `AbortRetryIgnore`
- 自动处理对话框布局、背景保存与恢复、事件传播和生命周期管理
- **事件系统增强**:
- 所有控件的 `handleEvent` 方法现在返回 `bool` 类型,表示是否消费了事件
- 引入事件消费机制,支持更精细的事件传播控制
- Window 类的事件循环现在优先处理对话框事件
- **控件状态管理**:
- Control 基类新增 `dirty` 标志和 `setDirty()` 方法,统一管理重绘状态
- 所有控件现在都实现了 `IsVisible()``model()` 方法
- **API 增强**:
- Button 类新增 `setButtonFalseColor()` 方法
- TextBox 类的 `setMaxCharLen()` 现在接受 `size_t` 类型参数
- Window 类新增对话框管理方法和去重检测机制
### 重大变更(Breaking Changes
- **API 签名变更**:
- 所有控件的 `handleEvent(const ExMessage& msg)` 方法从返回 `void` 改为返回 `bool`
- Control 基类新增纯虚函数 `IsVisible() const``model() const`,所有派生类必须实现
- **资源管理变更**:
- Control 基类的样式保存从栈对象改为堆对象,使用指针管理
- 增强了资源释放的安全性,所有资源都在析构函数中正确释放
- **事件处理逻辑**:
- Window 的 `runEventLoop()` 方法完全重写,现在优先处理对话框事件
- 引入了事件消费机制,事件被消费后不会继续传播
### 修复(Fixed
- **内存管理**:
- 修复 `Button::setFillIma()` 的内存泄漏问题
- 修复 Control 基类析构函数中的资源释放问题
- 修复 Dialog 类背景图像资源管理问题
- **布局与渲染**:
- 修复 `Table` 组件的分页计算、列宽和行高越界问题
- 修复 `Table``pX` 坐标累加错误导致的布局错乱
- 修复 `Canvas::draw()` 中导致子控件不被绘制的 dirty 判定问题
- 修复 `TextBox::draw()``restoreStyle()` 的不对称调用问题
- **事件处理**:
- 修复窗口事件分发逻辑,确保对话框/顶层控件优先处理事件
- 修复鼠标移出按钮区域时状态更新不及时的问题
- 修复非模态对话框事件处理中的竞争条件
- **其他问题**:
- 修复文本测量和渲染中的潜在错误
- 修复对话框关闭后背景恢复不完全的问题
- 修复多对话框场景下的 z-order 管理问题
### 改进(Changed
- **代码质量**:
- 重构了大量内部 API,增强异常安全性与可维护性
- 使用智能指针和现代 C++ 特性替代裸 new/delete
- 统一了代码风格和命名约定
- **性能优化**:
- 优化了事件处理流程,减少不必要的重绘
- 改进了对话框背景保存和恢复机制
- 减少了内存分配和拷贝操作
- **文档与示例**:
- 为所有新增功能添加了详细的使用示例
- 完善了代码注释和 API 文档
- 更新了 README 和 CHANGELOG 反映最新变化
## [1.1.0] - 2025-09-08
### 新增
- **Window 类 API 增强**:
- 添加了完整的获取器(getter)方法集,提高类的封装性和可用性
- `getHwnd()` - 获取窗口句柄,便于与原生 Windows API 集成
- `getWidth()` - 获取窗口宽度
- `getHeight()` - 获取窗口高度
- `getHeadline()` - 获取窗口标题
- `getBkcolor()` - 获取窗口背景颜色
- `getBkImage()` - 获取窗口背景图片指针
- `getControls()` - 获取控件管理容器的引用,允许迭代和操作已添加的控件
### 改进
- **API 一致性**: 为所有重要属性提供了对称的设置器(setter)和获取器(getter)方法
- **代码文档**: 进一步完善了类注释,使其更加清晰和专业
## [1.0.0] - 2025-09-08
### 发布摘要
首个稳定版本 (Stable Release)
### 新增
- StellarX 框架的第一个稳定版本
- 完整的控件库:按钮、标签、文本框、画布、表格和窗口
- 基于 CMake 的构建系统
- 详细的文档和示例代码
- **明确声明:本框架专为 Windows 平台设计**
### Released
- **首次发布预编译的二进制库文件**,方便用户快速集成,无需从源码编译。
- 提供的发布包包括:
- `StellarX-v1.0.0-x64-Windows-msvc-x64.zip`
- **编译环境**: Visual Studio 2022 (MSVC v143)
- **架构**: x64 (64位)
- **运行时库**: `/MD`
- **构建模式**: `Release | Debug`
- **内容**: 包含所有必要的头文件(`include/`)和静态库文件(``lib/StellarX-Debug.lib StellarX-Release.lib`)
### 核心特性
- 模块化设计,遵循 SOLID 原则
- 统一的控件接口(draw() 和 handleEvent()
- 支持多种控件形状和样式
- 自定义事件处理回调
- 轻量级设计,无外部依赖
## [0.1.0] - 2025-08-15
### 新增
- 初始项目结构和核心架构
- Control 基类和基本事件处理系统
- 基础示例和文档设置
+19
View File
@@ -0,0 +1,19 @@
cmake_minimum_required(VERSION 3.10)
# 项目定义
project(StellarX VERSION 2.0.0 LANGUAGES CXX)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# 包含头文件目录(目前头文件都在根目录)
include_directories(${CMAKE_SOURCE_DIR})
# 源文件收集
file(GLOB_RECURSE SOURCES
"${CMAKE_SOURCE_DIR}/*.cpp"
)
# 生成可执行文件
add_executable(StellarX ${SOURCES})
+94
View File
@@ -0,0 +1,94 @@
# Contributing to StellarX
Thank you for your interest in contributing to StellarX! This document provides guidelines and steps for contributing.
StellarX is a C++ GUI framework built for the **Windows platform**, based on the EasyX graphics library.
## Development Environment Setup
1. Install Visual Studio 2019 or later
2. Install the corresponding version of EasyX graphics library
3. Install CMake 3.12 or later
4. Clone the project repository
5. Use CMake to generate the solution and compile
## How to Contribute
### Reporting Bugs
1. Check [Issues](../../issues) to see if the bug has already been reported.
2. If not, create a new Issue.
3. Use the "Bug Report" template.
4. Provide a **clear title and description**.
5. Include relevant code snippets, screenshots, or steps to reproduce the issue.
### Suggesting Enhancements
1. Check existing Issues to see if your idea has been suggested.
2. Create a new Issue using the "Feature Request" template.
3. Clearly describe the new feature and explain why it would be useful.
### Submitting Code Changes (Pull Requests)
1. **Fork** the repository on GitHub.
2. **Clone** your forked repository to your local machine.
3. Create a **new branch** for your feature or bug fix (`git checkout -b my-feature-branch`).
4. **Make your changes**. Ensure your code follows the project's style (see below).
5. **Commit your changes** with clear, descriptive commit messages.
6. **Push** your branch to your forked GitHub repository (`git push origin my-feature-branch`).
7. Open a **Pull Request** against the original StellarX repository's `main` branch.
## Code Style Guide
* Follow the existing code formatting and naming conventions in the project.
* Use meaningful names for variables, functions, and classes.
* Comment your code when necessary, especially for complex logic.
* Ensure your code compiles without warnings.
* Test your changes thoroughly.
* Use **4 spaces** for indentation (no tabs)
* Class names use **PascalCase** (e.g., `ClassName`)
* Functions and variables use **camelCase** (e.g., `functionName`, `variableName`)
* Constants use **UPPER_CASE** (e.g., `CONSTANT_VALUE`)
* Member variables use **m_** prefix (e.g., `m_memberVariable`)
* Use meaningful names for control properties, avoid abbreviations
* Add detailed comments for all public interfaces
* Follow RAII principles for resource management
## Example Code Style
```c++
// Good Example
class MyControl : public Control {
public:
MyControl(int x, int y, int width, int height)
: Control(x, y, width, height), m_isActive(false) {}
void draw() override {
// Drawing logic
}
private:
bool m_isActive;
};
// Bad Example
class my_control: public Control{
public:
my_control(int x,int y,int w,int h):Control(x,y,w,h),active(false){}
void Draw() override{
// Drawing logic
}
private:
bool active;
};
```
## Project Structure
Please follow the project's directory structure:
- Header files go in the `include/StellarX/` directory
- Implementation files go in the `src/` directory
- Example code goes in the `examples/` directory
## Questions?
If you have any questions, feel free to open an Issue or contact the maintainers.
+93
View File
@@ -0,0 +1,93 @@
# 为 StellarX 做贡献
感谢您有兴趣为 StellarX 做出贡献!本文档提供了贡献的指南和步骤。
StellarX 是一个为 **Windows 平台**打造的 C++ GUI 框架,基于 EasyX 图形库。
## 开发环境设置
1. 安装 Visual Studio 2019 或更高版本
2. 安装对应版本的 EasyX 图形库
3. 安装 CMake 3.12 或更高版本
4. 克隆项目仓库
5. 使用 CMake 生成解决方案并编译
## 如何贡献
### 报告错误
1. 检查 [Issues](../../issues) 看看这个错误是否已经被报告过。
2. 如果没有,请创建一个新的 Issue。
3. 使用 "Bug 报告" 模板。
4. 提供**清晰的标题和描述**。
5. 包括相关的代码片段、截图或重现问题的步骤。
### 建议功能增强
1. 检查现有的 Issue 中是否有你的想法。
2. 使用 "功能请求" 模板创建一个新的 Issue。
3. 清晰地描述新功能,并解释它为何有用。
### 提交代码更改 (拉取请求 Pull Requests)
1. **Fork** GitHub 上的仓库。
2. **Clone** 你 fork 的仓库到本地机器。
3. 为你的功能或错误修复创建一个**新分支** (`git checkout -b my-feature-branch`)。
4. **进行你的更改**。确保你的代码遵循项目的风格(见下文)。
5. **提交你的更改**,并附上清晰描述性的提交信息。
6. **Push** 你的分支到你 fork 的 GitHub 仓库 (`git push origin my-feature-branch`)。
7. 针对原始 StellarX 仓库的 `main` 分支发起一个**拉取请求 (Pull Request)**。
## 代码风格指南
* 遵循项目中现有的代码格式和命名约定。
* 为变量、函数和类使用有意义的名称。
* 在必要时注释你的代码,特别是复杂的逻辑。
* 确保你的代码编译时没有警告。
* 彻底测试你的更改。
* 使用 **4个空格** 进行缩进(不要使用制表符)
- 类名使用 **PascalCase**(如 `ClassName`
- 函数和变量使用 **camelCase**(如 `functionName`, `variableName`
- 常量使用 **UPPER_CASE**(如 `CONSTANT_VALUE`
- 成员变量以 **m_** 前缀(如 `m_memberVariable`
- 控件属性使用有意义的名称,避免缩写
- 为所有公共接口添加详细的注释
- 遵循 RAII 原则管理资源
## 示例代码风格
```c++
// 好的示例
class MyControl : public Control {
public:
MyControl(int x, int y, int width, int height)
: Control(x, y, width, height), m_isActive(false) {}
void draw() override {
// 绘制逻辑
}
private:
bool m_isActive;
};
// 不好的示例
class my_control: public Control{
public:
my_control(int x,int y,int w,int h):Control(x,y,w,h),active(false){}
void Draw() override{
// 绘制逻辑
}
private:
bool active;
};
```
## 项目结构
请遵循项目的目录结构:
- 头文件放在 `include/StellarX/` 目录
- 实现文件放在 `src/` 目录
- 示例代码放在 `examples/` 目录
## 问题?
如果你有任何问题,可以随时开一个 Issue 或联系维护者。
-575
View File
@@ -1,575 +0,0 @@
#include "Canvas.h"
#include "SxLog.h"
#include "Window.h"
static bool SxIsNoisyMsg(UINT m)
{
return m == WM_MOUSEMOVE;
}
static const char* SxCanvasMsgName(UINT m)
{
switch (m)
{
case WM_MOUSEMOVE: return "WM_MOUSEMOVE";
case WM_LBUTTONDOWN: return "WM_LBUTTONDOWN";
case WM_LBUTTONUP: return "WM_LBUTTONUP";
case WM_KEYDOWN: return "WM_KEYDOWN";
case WM_KEYUP: return "WM_KEYUP";
default: return "WM_UNKNOWN";
}
}
namespace
{
enum class SxCanvasOverlayRedrawMode
{
None,
RefreshSnapshot
};
bool SxCanvasRectValid(const RECT& rc)
{
return rc.right > rc.left && rc.bottom > rc.top;
}
bool SxCanvasRectsIntersect(const RECT& a, const RECT& b)
{
return a.left < b.right && a.right > b.left &&
a.top < b.bottom && a.bottom > b.top;
}
RECT SxCanvasUnionRect(const RECT& a, const RECT& b)
{
RECT out{};
out.left = (std::min)(a.left, b.left);
out.top = (std::min)(a.top, b.top);
out.right = (std::max)(a.right, b.right);
out.bottom = (std::max)(a.bottom, b.bottom);
return out;
}
}
Canvas::Canvas()
:Control(0, 0, 100, 100)
{
this->id = "Canvas";
// Canvas 是通用容器,当前阶段显式允许双轴 Stretch。
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = true;
}
Canvas::Canvas(int x, int y, int width, int height)
:Control(x, y, width, height)
{
this->id = "Canvas";
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = true;
}
void Canvas::relayoutManagedChildren()
{
// Canvas 负责子控件从“父局部设计矩形”到“当前世界矩形”的转换。
// 当 Canvas 自己的位置或尺寸变化后,所有受它管理的子控件都要重新走一次统一解算。
for (auto& child : controls)
{
const StellarX::ResolvedLayoutRect rect =
child->resolveLayoutRect(localWidth, localHeight, x, y, width, height);
child->applyResolvedLayoutRect(rect);
}
}
void Canvas::setX(int x)
{
// 公开 setter 在 Canvas 上不能再视为“单纯改自己的 x”:
// 一旦容器移动,子控件的世界坐标也必须整体重算。
applyRuntimeRectDirect(x, y, width, height);
relayoutManagedChildren();
onWindowResize();
}
void Canvas::setY(int y)
{
applyRuntimeRectDirect(this->x, y, width, height);
relayoutManagedChildren();
onWindowResize();
}
void Canvas::setWidth(int width)
{
applyRuntimeRectDirect(x, y, width, height);
relayoutManagedChildren();
onWindowResize();
}
void Canvas::setHeight(int height)
{
applyRuntimeRectDirect(x, y, width, height);
relayoutManagedChildren();
onWindowResize();
}
void Canvas::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
{
// 统一解算器已经给出当前运行态世界矩形;
// Canvas 在应用自身矩形后,还必须继续刷新全部子控件。
applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height);
relayoutManagedChildren();
}
void Canvas::clearAllControls()
{
controls.clear();
}
void Canvas::draw()
{
if (!dirty || !show)
{
for (auto& control : controls)
if (auto c = dynamic_cast<Table*>(control.get()))
c->draw();
return;
}
saveStyle();
setlinecolor(canvasBorderClor);//设置线色
if (StellarX::FillMode::Null != canvasFillMode)
setfillcolor(canvasBkClor);//设置填充色
setfillstyle((int)canvasFillMode);//设置填充模式
setlinestyle((int)canvasLineStyle, canvaslinewidth);
// 在绘制画布之前,先恢复并更新背景快照:
// 1. 如果已有快照,则先回贴旧快照以清除之前的内容。
// 2. 当坐标或尺寸变化,或缓存图像无效时,丢弃旧快照并重新抓取新的背景。
int margin = canvaslinewidth > 1 ? canvaslinewidth : 1;
if (hasSnap)
{
// 恢复旧快照,清除上一次绘制
restBackground();
// 如果位置或尺寸变了,或没有有效缓存,则重新抓取
if (!saveBkImage || saveBkX != this->x - margin || saveBkY != this->y - margin || saveWidth != this->width + margin * 2 || saveHeight != this->height + margin * 2)
{
invalidateBackgroundSnapshot();
saveBackground(this->x - margin, this->y - margin, this->width + margin * 2, this->height + margin * 2);
}
}
else
{
// 首次绘制或没有快照时直接抓取背景
saveBackground(this->x - margin, this->y - margin, this->width + margin * 2, this->height + margin * 2);
}
// 再次恢复最新快照,确保绘制区域干净
restBackground();
//根据画布形状绘制
switch (shape)
{
case StellarX::ControlShape::RECTANGLE:
fillrectangle(x, y, x + width, y + height);//有边框填充矩形
break;
case StellarX::ControlShape::B_RECTANGLE:
solidrectangle(x, y, x + width, y + height);//无边框填充矩形
break;
case StellarX::ControlShape::ROUND_RECTANGLE:
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//有边框填充圆角矩形
break;
case StellarX::ControlShape::B_ROUND_RECTANGLE:
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//无边框填充圆角矩形
break;
}
// 绘制所有子控件
for (auto& control : controls)
{
control->setDirty(true);
control->draw();
}
restoreStyle();
dirty = false; //标记画布不需要重绘
}
bool Canvas::handleEvent(const ExMessage& msg)
{
if (!show) return false;
resetEventVisualChanged();
bool consumed = false;
bool anyDirty = false;
bool anyVisualChanged = false;
Control* firstConsumer = nullptr;
if (msg.message == WM_MOUSEMOVE)
{
// WM_MOUSEMOVE 需要特殊处理:
// - 第一个命中的兄弟分支收到真实消息;
// - 后续兄弟不再重新命中,只清理旧 hover / tooltip 等临时状态。
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
Control* c = it->get();
if (!consumed)
{
bool cConsumed = c->handleEvent(msg);
if (c->didEventAffectVisual()) anyVisualChanged = true;
if (cConsumed)
{
firstConsumer = c;
consumed = true;
}
}
else
{
// 后续兄弟只走临时状态清理,不会再进入自己的 handleEvent()。
// Tooltip 隐藏会先回贴旧快照,再改变 coverage;因此必须先保存旧覆盖范围,
// 避免登记重绘时丢失旧 Tooltip 区域,导致上层 overlay 补画判断不完整。
const RECT previousCoverage = c->getManagedRepaintCoverageRect();
if (c->clearTransientMouseState())
{
if (Window* host = getHostWindow())
host->requestManagedRepaint(c, previousCoverage);
anyVisualChanged = true;
}
}
}
}
else
{
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
Control* c = it->get();
bool cConsumed = c->handleEvent(msg);
if (c->isDirty()) anyDirty = true;
if (c->didEventAffectVisual()) anyVisualChanged = true;
if (cConsumed)
{
firstConsumer = c;
consumed = true;
break;
}
}
}
if (firstConsumer && !SxIsNoisyMsg(msg.message))
{
SX_LOGD("Event") << SX_T("Canvas 消耗消息: ","Canvas consumed: ")
<< SxCanvasMsgName(msg.message)
<< SX_T(" 子控件 id=", " childId=") << firstConsumer->getId();
}
if (anyDirty)
{
// 只要任一子控件因本次事件进入 dirty,就把这笔重绘继续向上汇报。
// 在托管模式下,这不会立即绘制,而是登记为 Canvas 对应的重绘 root。
if (!SxIsNoisyMsg(msg.message))
SX_LOGD("Dirty") << SX_T("Canvas检测有控件为脏状态 -> 请求重绘, ","Canvas anyDirty -> requestRepaint, ")<<"id = " << id;
requestRepaint(parent);
}
markEventVisualChanged(anyVisualChanged);
return consumed;
}
bool Canvas::clearTransientMouseState()
{
bool changed = false;
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
Control* child = it->get();
if (!child->IsVisible())
continue;
if (child->clearTransientMouseState())
changed = true;
}
return changed;
}
void Canvas::addControl(std::unique_ptr<Control> control)
{
control->setParent(this);
// 新子控件加入容器时,立刻按“当前容器运行态矩形”解算一次,
// 避免后续第一次 draw / resize 前 world 坐标仍停留在设计态。
const StellarX::ResolvedLayoutRect rect =
control->resolveLayoutRect(localWidth, localHeight, this->x, this->y, this->width, this->height);
control->applyResolvedLayoutRect(rect);
SX_LOGI("Canvas")
<< SX_T("添加子控件:父=Canvas 子id=", "addControl: parent=Canvas childId=")
<< control->getId()
<< SX_T(" 相对坐标=(", " local=(")
<< control->getLocalX() << "," << control->getLocalY()
<< SX_T(") 绝对坐标=(", ") abs=(")
<< control->getX() << "," << control->getY()
<< ")";
controls.push_back(std::move(control));
dirty = true;
}
void Canvas::setShape(StellarX::ControlShape shape)
{
switch (shape)
{
case StellarX::ControlShape::RECTANGLE:
case StellarX::ControlShape::B_RECTANGLE:
case StellarX::ControlShape::ROUND_RECTANGLE:
case StellarX::ControlShape::B_ROUND_RECTANGLE:
this->shape = shape;
dirty = true;
break;
case StellarX::ControlShape::CIRCLE:
case StellarX::ControlShape::B_CIRCLE:
case StellarX::ControlShape::ELLIPSE:
case StellarX::ControlShape::B_ELLIPSE:
this->shape = StellarX::ControlShape::RECTANGLE;
dirty = true;
break;
}
}
void Canvas::setCanvasfillMode(StellarX::FillMode mode)
{
this->canvasFillMode = mode;
dirty = true;
}
void Canvas::setBorderColor(COLORREF color)
{
this->canvasBorderClor = color;
dirty = true;
}
void Canvas::setCanvasBkColor(COLORREF color)
{
this->canvasBkClor = color;
dirty = true;
}
void Canvas::setCanvasLineStyle(StellarX::LineStyle style)
{
this->canvasLineStyle = style;
dirty = true;
}
void Canvas::setLinewidth(int width)
{
this->canvaslinewidth = width;
dirty = true;
}
void Canvas::setIsVisible(bool visible)
{
this->show = visible;
dirty = true;
for (auto& control : controls)
{
control->setIsVisible(visible);
}
if (!visible)
discardBackground();
}
void Canvas::setDirty(bool dirty)
{
this->dirty = dirty;
for (auto& control : controls)
control->setDirty(dirty);
}
void Canvas::onWindowResize()
{
// resize 语义已收口:
// Canvas 不再在这里重新解算布局,只负责丢快照、标脏,并向子控件传播“环境已变化”。
Control::onWindowResize();
for (auto& child : controls)
child->onWindowResize();
}
void Canvas::requestRepaint(Control* parent)
{
if (shouldDeferManagedRepaint())
{
// 托管路径:由 Window 统一决定这次是否只重画本 Canvas,还是升级为补画 Dialog / 整体场景。
if (auto* host = getHostWindow())
host->requestManagedRepaint(this);
return;
}
if (this == parent)
{
if (!show)
return;
// 关键护栏:
// - Canvas 自己是脏的 / 没有快照 / 缓存图为空
// => 禁止局部重绘,直接升级为一次完整 draw(先把 dirty 置真,避免 draw() 早退)
if (dirty || !hasSnap || !saveBkImage)
{
SX_LOG_TRACE("Dirty")
<< SX_T("Canvas 局部重绘降级为全量重绘: id=", "Canvas partial->full draw: id=")
<< id
<< " dirty=" << (dirty ? 1 : 0)
<< " hasSnap=" << (hasSnap ? 1 : 0);
this->dirty = true;
this->draw();
return;
}
SX_LOG_TRACE("Dirty") << SX_T("Canvas 请求局部重绘:id=", "Canvas::requestRepaint(partial): id=") << id;
RECT paintCoverage{};
bool hasPaintCoverage = false;
RECT persistentCoverage{};
bool hasPersistentCoverage = false;
auto commitManagedChild = [&](Control* child, SxCanvasOverlayRedrawMode overlayMode)
{
if (!child || !child->IsVisible())
return;
const bool directDirty = child->isDirty();
const bool subtreeDirty = child->hasManagedDirtySubtree();
if (overlayMode == SxCanvasOverlayRedrawMode::RefreshSnapshot)
{
// overlay 补画必须先作废旧快照:
// 下层兄弟的持久内容刚刚已经写过像素,若继续沿用旧快照,会把旧背景再贴回来。
child->invalidateBackgroundSnapshot();
child->setDirty(true);
child->draw();
}
else if (directDirty)
{
child->draw();
}
else if (subtreeDirty)
{
// 这次真正脏的是“child 下面的子树”,而不是 child 自身。
// 例如嵌套 Canvas 中,第二层/第三层按钮脏了,但第一层 Canvas 自己并不 dirty。
// 这里必须把这条直接子分支提交下去,否则深层按钮状态永远没有机会被真正画出来。
child->commitManagedRepaint();
}
else
{
return;
}
const RECT childPaintRect = child->getManagedRepaintCoverageRect();
if (!hasPaintCoverage)
{
paintCoverage = childPaintRect;
hasPaintCoverage = true;
}
else
{
paintCoverage = SxCanvasUnionRect(paintCoverage, childPaintRect);
}
const RECT childPersistentRect = child->getManagedRepaintPersistentCoverageRect();
if (!hasPersistentCoverage)
{
persistentCoverage = childPersistentRect;
hasPersistentCoverage = true;
}
else
{
persistentCoverage = SxCanvasUnionRect(persistentCoverage, childPersistentRect);
}
};
for (auto& control : controls)
{
Control* child = control.get();
if (!child->IsVisible())
continue;
if (child->hasManagedDirtySubtree())
{
commitManagedChild(child, SxCanvasOverlayRedrawMode::None);
}
else if (hasPaintCoverage && SxCanvasRectsIntersect(child->getManagedRepaintCoverageRect(), paintCoverage))
{
// 位于本次累计 coverage 上方、且发生相交的兄弟控件,需要补画回最上层。
// 但只有下层“持久内容”影响到它时,才允许作废并重新抓背景快照;
// 如果只是被 Tooltip 等临时浮层覆盖,则跳过兄弟补画,避免透明控件回贴旧快照擦掉 Tooltip。
const bool persistentHit = hasPersistentCoverage &&
SxCanvasRectsIntersect(child->getManagedRepaintPersistentCoverageRect(), persistentCoverage);
if (persistentHit)
commitManagedChild(child, SxCanvasOverlayRedrawMode::RefreshSnapshot);
}
}
return;
}
SX_LOG_TRACE("Dirty") << SX_T("Canvas 请求根级重绘:id=", "Canvas::requestRepaint(root): id=") << id;
onRequestRepaintAsRoot();
}
bool Canvas::hasManagedDirtySubtree() const
{
if (dirty)
return true;
for (const auto& child : controls)
{
if (!child->IsVisible())
continue;
if (child->hasManagedDirtySubtree())
return true;
}
return false;
}
RECT Canvas::getManagedRepaintCoverageRect() const
{
// Canvas::draw() 会先写自身背景,再强制绘制全部可见子控件。
// 因此它的实际 coverage 不能只看本体矩形,还要把可见子控件 coverage 递归并入。
RECT coverage = getBoundsRect();
for (const auto& child : controls)
{
if (!child->IsVisible())
continue;
coverage = SxCanvasUnionRect(coverage, child->getManagedRepaintCoverageRect());
}
return coverage;
}
RECT Canvas::getManagedRepaintPersistentCoverageRect() const
{
// 持久 coverage 只描述会进入背景快照语义的范围。
// 子控件 Tooltip 等临时浮层不会被并入,避免兄弟控件补画时抓到临时像素。
RECT coverage = getBoundsRect();
for (const auto& child : controls)
{
if (!child->IsVisible())
continue;
coverage = SxCanvasUnionRect(coverage, child->getManagedRepaintPersistentCoverageRect());
}
return coverage;
}
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();
}
-97
View File
@@ -1,97 +0,0 @@
/*******************************************************************************
* @类: Canvas
* @摘要: 画布容器控件,用于分组和管理子控件
* @描述:
* 作为其他控件的父容器,提供统一的背景和边框样式。
* 负责将事件传递给子控件并管理它们的绘制顺序。
* 在托管重绘模式下,Canvas 通常作为一组子控件的安全重绘 root。
*
* @特性:
* - 支持四种矩形形状(普通、圆角,各有边框和无边框版本)
* - 可自定义填充模式、边框颜色和背景颜色
* - 自动管理子控件的生命周期和事件传递
* - 支持嵌套容器结构
*
* @使用场景: 用于分组相关控件、实现复杂布局或作为对话框基础
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
*******************************************************************************/
#pragma once
#include "Control.h"
#include"Table.h"
class Canvas : public Control
{
protected:
std::vector<std::unique_ptr<Control>> controls;
StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE; //容器形状
StellarX::FillMode canvasFillMode = StellarX::FillMode::Solid; //容器填充模式
StellarX::LineStyle canvasLineStyle = StellarX::LineStyle::Solid; //线型
int canvaslinewidth = 1; //线宽
COLORREF canvasBorderClor = RGB(0, 0, 0); //边框颜色
COLORREF canvasBkClor = RGB(255, 255, 255); //背景颜色
// 清除所有子控件
void clearAllControls();
public:
// 默认构造:创建一个基础矩形容器
Canvas();
// 指定初始位置和尺寸构造容器
Canvas(int x, int y, int width, int height);
~Canvas() {}
// 修改容器运行态 X,并同步重算全部受管理子控件的世界坐标
void setX(int x)override;
// 修改容器运行态 Y,并同步重算全部受管理子控件的世界坐标
void setY(int y)override;
// 修改容器运行态宽度,并同步重算全部受管理子控件的世界坐标
void setWidth(int width) override;
// 修改容器运行态高度,并同步重算全部受管理子控件的世界坐标
void setHeight(int height) override;
//绘制容器及其子控件
void draw() override;
bool handleEvent(const ExMessage& msg) override;
// 递归清理子树中的 hover / tooltip / 临时按下态,不把 Canvas 自己升级为整块重绘源。
bool clearTransientMouseState() override;
// 添加一个由 Canvas 托管的子控件
void addControl(std::unique_ptr<Control> control);
//设置容器样式
void setShape(StellarX::ControlShape shape);
//设置容器填充模式
void setCanvasfillMode(StellarX::FillMode mode);
//设置容器边框颜色
void setBorderColor(COLORREF color);
//设置填充颜色
void setCanvasBkColor(COLORREF color);
//设置线形
void setCanvasLineStyle(StellarX::LineStyle style);
//设置线段宽度
void setLinewidth(int width);
//设置不可见后传递给子控件重写
void setIsVisible(bool visible) override;
void setDirty(bool dirty) override;
void onWindowResize() override;
// 托管模式下登记为 root;非托管模式下走局部或根级重绘
void requestRepaint(Control* parent)override;
bool hasManagedDirtySubtree() const override;
RECT getManagedRepaintCoverageRect() const override;
RECT getManagedRepaintPersistentCoverageRect() const override;
// 判断当前 Canvas 是否可安全做局部提交
bool canCommitManagedPartialRepaint() const override;
// 托管收口阶段执行 Canvas 的真正重绘
void commitManagedRepaint() override;
// 获取直接子控件列表
std::vector<std::unique_ptr<Control>>& getControls() { return controls; }
protected:
// 统一解算后,按当前运行态矩形把所有受管理子控件重新映射到新的世界坐标。
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
private:
// 容器自己的几何变化后,需要统一刷新所有子控件的运行态矩形。
void relayoutManagedChildren();
//用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; };
};
-667
View File
@@ -1,667 +0,0 @@
#include "Control.h"
#include "SxLog.h"
#include<assert.h>
#include "Window.h"
#include <algorithm>
namespace
{
// 单轴解算后的临时结果:只描述“在父局部坐标系中的位置和尺寸”。
struct LayoutAxisResult
{
int pos = 0;
int size = 0;
};
int ClampNonNegative(int value)
{
return value < 0 ? 0 : value;
}
int RoundDiv(long long numerator, long long denominator)
{
if (denominator == 0)
return 0;
if (numerator >= 0)
return static_cast<int>((numerator + denominator / 2) / denominator);
return -static_cast<int>(((-numerator) + denominator / 2) / denominator);
}
// 旧接口兼容层:
// 将 setAnchor(a1, a2) 的双锚点输入映射为单轴 LayoutSpec。
// 这里只负责“从旧输入翻译成新模型”,不直接参与后续求解。
StellarX::AxisLayoutSpec BuildLegacyAxisSpec(StellarX::Anchor anchor1, StellarX::Anchor anchor2,
StellarX::Anchor startAnchor, StellarX::Anchor endAnchor)
{
StellarX::AxisLayoutSpec spec{};
spec.anchorStart = (anchor1 == startAnchor || anchor2 == startAnchor);
spec.anchorEnd = (anchor1 == endAnchor || anchor2 == endAnchor);
if (spec.anchorStart && spec.anchorEnd)
{
spec.sizePolicy = StellarX::AxisSizePolicy::Stretch;
spec.alignPolicy = StellarX::AxisAlignPolicy::Start;
}
else if (spec.anchorEnd)
{
spec.sizePolicy = StellarX::AxisSizePolicy::FixedSize;
spec.alignPolicy = StellarX::AxisAlignPolicy::End;
}
else
{
spec.sizePolicy = StellarX::AxisSizePolicy::FixedSize;
spec.alignPolicy = StellarX::AxisAlignPolicy::Start;
}
return spec;
}
// 单轴统一解算器:
// 输入设计态父尺寸、当前父尺寸、子控件设计态位置/尺寸,以及当前轴策略,
// 输出该轴在“父局部坐标系”下的运行态位置与尺寸。
LayoutAxisResult ResolveAxis(int parentDesignSize, int parentCurrentSize, int childDesignPos, int childDesignSize,
const StellarX::AxisLayoutSpec& spec, bool allowStretch)
{
LayoutAxisResult result{};
const int startMargin = childDesignPos;
const int endMargin = parentDesignSize - (childDesignPos + childDesignSize);
const bool wantsStretch = spec.sizePolicy == StellarX::AxisSizePolicy::Stretch;
const bool canStretch = allowStretch && spec.anchorStart && spec.anchorEnd;
if (wantsStretch && canStretch)
{
// 双边锚定 + Stretch
// 保持设计态中的起边距和终边距,让尺寸随父容器变化。
result.pos = startMargin;
result.size = ClampNonNegative(parentCurrentSize - startMargin - endMargin);
return result;
}
// 其余情况一律按固定尺寸处理;即便调用方请求 Stretch,
// 只要锚点条件或控件能力边界不满足,也会在这里自然降级为 FixedSize。
result.size = ClampNonNegative(childDesignSize);
if (spec.anchorStart && !spec.anchorEnd)
{
// 仅锚定起边:保持起边距,尺寸不变。
result.pos = startMargin;
return result;
}
if (!spec.anchorStart && spec.anchorEnd)
{
// 仅锚定终边:保持终边距,尺寸不变。
result.pos = parentCurrentSize - endMargin - result.size;
return result;
}
if (spec.anchorStart && spec.anchorEnd)
{
// 双边锚定但尺寸固定:
// 位置由 alignPolicy 决定,用于表达“只位移、不拉伸”的场景。
switch (spec.alignPolicy)
{
case StellarX::AxisAlignPolicy::End:
result.pos = parentCurrentSize - endMargin - result.size;
break;
case StellarX::AxisAlignPolicy::Center:
// 保持相对父容器中心的偏移关系。
result.pos = childDesignPos + (parentCurrentSize - parentDesignSize) / 2;
break;
case StellarX::AxisAlignPolicy::Proportional:
{
// 保持设计态中的相对位置比例。
// 注意:这里只调整位置,不改变尺寸。
const int designTravel = parentDesignSize - result.size;
const int currentTravel = parentCurrentSize - result.size;
if (designTravel <= 0 || currentTravel <= 0)
result.pos = startMargin;
else
result.pos = RoundDiv(static_cast<long long>(startMargin) * currentTravel, designTravel);
break;
}
case StellarX::AxisAlignPolicy::Start:
default:
result.pos = startMargin;
break;
}
return result;
}
// 无锚点:退回设计态位置,尺寸保持设计值。
result.pos = childDesignPos;
return result;
}
const char* SxAxisName(bool horizontal)
{
return horizontal ? "X" : "Y";
}
void LogAxisDowngradeIfNeeded(const std::string& id, bool horizontal,
const StellarX::AxisLayoutSpec& spec, bool allowStretch)
{
if (spec.sizePolicy != StellarX::AxisSizePolicy::Stretch)
return;
if (!spec.anchorStart || !spec.anchorEnd)
{
SX_LOGD("Layout")
<< SX_T("布局降级:id=", "layout downgrade: id=") << id
<< SX_T(" axis=", " axis=") << SxAxisName(horizontal)
<< SX_T(" 原因=Stretch 但未同时锚定起止边", " reason=stretch without dual anchors");
return;
}
if (!allowStretch)
{
SX_LOGD("Layout")
<< SX_T("能力边界拦截:id=", "layout capability intercept: id=") << id
<< SX_T(" axis=", " axis=") << SxAxisName(horizontal)
<< SX_T(" 原因=当前控件禁止该轴 Stretch", " reason=stretch disabled by capability");
}
}
void ActivateExplicitLayoutSpecMode(StellarX::LayoutMode& mode)
{
// 新公开布局 API 明确属于“按边约束解算”的语义层。
// 一旦调用,说明外部希望控件进入统一锚定布局模型,
// 因此直接切换到 AnchorToEdges,避免出现“策略已设但 layoutMode 仍是 Fixed”的半失效状态。
mode = StellarX::LayoutMode::AnchorToEdges;
}
}
StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text)
{
{
nHeight = text.nHeight;
nWidth = text.nWidth;
lpszFace = text.lpszFace;
color = text.color;
nEscapement = text.nEscapement;
nOrientation = text.nOrientation;
nWeight = text.nWeight;
bItalic = text.bItalic;
bUnderline = text.bUnderline;
bStrikeOut = text.bStrikeOut;
return *this;
}
}
bool StellarX::ControlText::operator!=(const ControlText& text)
{
if(nHeight != text.nHeight)
return true;
else if (nWidth != text.nWidth)
return true;
else if (lpszFace != text.lpszFace)
return true;
else if (color != text.color)
return true;
else if (nEscapement != text.nEscapement)
return true;
else if (nOrientation != text.nOrientation)
return true;
else if (nWeight != text.nWeight)
return true;
else if (bItalic != text.bItalic)
return true;
else if (bUnderline != text.bUnderline)
return true;
else if (bStrikeOut != text.bStrikeOut)
return true;
return false;
}
void Control::setIsVisible(bool show)
{
SX_LOGD("Control") << SX_T("重置可见状态: id=", "setIsVisible: id=")
<< id
<< " show=" << (show ? 1 : 0);
if (this->show == show)
return;
this->show = show;
this->dirty = true;
if (!show)
{
// 隐藏:擦除自己在屏幕上的内容,并释放快照
discardBackground();
return;
}
// 显示:不在这里 requestRepaint(避免父容器快照未就绪时子控件抢跑 draw,污染快照)
// 仅向上标脏,让事件收口阶段由容器统一重绘。
if (parent)
parent->setDirty(true);
}
void Control::onWindowResize()
{
SX_LOGD("Layout") << SX_T("尺寸变化:id=", "onWindowResize: id=") << id
<< SX_T(" -> 丢背景快照 + 标脏", " -> discardSnap + dirty");
// 自己:丢快照 + 标脏
invalidateBackgroundSnapshot();
setDirty(true);
}
void Control::setLayoutMode(StellarX::LayoutMode layoutMode_)
{
this->layoutMode = layoutMode_;
}
void Control::setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2)
{
this->anchor_1 = anchor_1;
this->anchor_2 = anchor_2;
// 旧 API 只作为兼容输入层存在:
// 这里把历史上的 anchor_1 / anchor_2 映射为新的水平/垂直轴布局规格,
// 后续统一解算全部以 layoutSpec 为准。
this->layoutSpec.horizontal = BuildLegacyAxisSpec(anchor_1, anchor_2, StellarX::Anchor::Left, StellarX::Anchor::Right);
this->layoutSpec.vertical = BuildLegacyAxisSpec(anchor_1, anchor_2, StellarX::Anchor::Top, StellarX::Anchor::Bottom);
}
void Control::setHorizontalLayoutSpec(const StellarX::AxisLayoutSpec& spec)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.horizontal = spec;
}
void Control::setVerticalLayoutSpec(const StellarX::AxisLayoutSpec& spec)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.vertical = spec;
}
void Control::setHorizontalAnchors(bool left, bool right)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.horizontal.anchorStart = left;
this->layoutSpec.horizontal.anchorEnd = right;
}
void Control::setVerticalAnchors(bool top, bool bottom)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.vertical.anchorStart = top;
this->layoutSpec.vertical.anchorEnd = bottom;
}
void Control::setHorizontalSizePolicy(StellarX::AxisSizePolicy policy)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.horizontal.sizePolicy = policy;
}
void Control::setVerticalSizePolicy(StellarX::AxisSizePolicy policy)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.vertical.sizePolicy = policy;
}
void Control::setHorizontalAlignPolicy(StellarX::AxisAlignPolicy policy)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.horizontal.alignPolicy = policy;
}
void Control::setVerticalAlignPolicy(StellarX::AxisAlignPolicy policy)
{
ActivateExplicitLayoutSpecMode(this->layoutMode);
this->layoutSpec.vertical.alignPolicy = policy;
}
StellarX::Anchor Control::getAnchor_1() const
{
return this->anchor_1;
}
StellarX::Anchor Control::getAnchor_2() const
{
return this->anchor_2;
}
StellarX::LayoutMode Control::getLayoutMode() const
{
return this->layoutMode;
}
StellarX::AxisLayoutSpec Control::getHorizontalLayoutSpec() const
{
return layoutSpec.horizontal;
}
StellarX::AxisLayoutSpec Control::getVerticalLayoutSpec() const
{
return layoutSpec.vertical;
}
const StellarX::LayoutSpec& Control::getLayoutSpec() const
{
return layoutSpec;
}
const StellarX::LayoutCapability& Control::getLayoutCapability() const
{
return layoutCapability;
}
void Control::setX(int x)
{
this->x = x;
dirty = true;
}
void Control::setY(int y)
{
this->y = y;
dirty = true;
}
void Control::setWidth(int width)
{
this->width = width;
dirty = true;
}
void Control::setHeight(int height)
{
this->height = height;
dirty = true;
}
bool Control::clearTransientMouseState()
{
// 基类默认没有 hover / tooltip / 临时按下态等鼠标瞬时状态。
// 具体控件若需要在“未收到本次 WM_MOUSEMOVE”时做清理,重写此接口即可。
return false;
}
void Control::commitCurrentGeometryAsDesignRect()
{
// 该接口是“显式提交新的设计基线”的唯一入口之一。
// 普通布局解算、父容器重排、窗口 resize 均不得自动回写 local*
// 否则会导致设计基线漂移,后续解算越来越不稳定。
SX_LOGD("Layout")
<< SX_T("提交设计基线:id=", "commit design rect: id=") << id
<< SX_T(" world=(", " world=(") << x << "," << y << "," << width << "," << height << ")"
<< SX_T(" parentLocalBefore=(", " parentLocalBefore=(")
<< localx << "," << localy << "," << localWidth << "," << localHeight << ")";
localx = parent ? (x - parent->getX()) : x;
localy = parent ? (y - parent->getY()) : y;
localWidth = width;
localHeight = height;
SX_LOGD("Layout")
<< SX_T("提交设计基线完成:id=", "commit design rect done: id=") << id
<< SX_T(" parentLocalAfter=(", " parentLocalAfter=(")
<< localx << "," << localy << "," << localWidth << "," << localHeight << ")";
}
StellarX::ResolvedLayoutRect Control::resolveLayoutRect(int parentDesignW, int parentDesignH,
int parentWorldX, int parentWorldY, int parentCurrentW, int parentCurrentH) const
{
StellarX::ResolvedLayoutRect rect{};
if (layoutMode != StellarX::LayoutMode::AnchorToEdges)
{
// 非锚点布局模式:直接使用设计态矩形,再映射到当前世界坐标。
rect.localX = localx;
rect.localY = localy;
rect.width = localWidth;
rect.height = localHeight;
rect.worldX = parentWorldX + rect.localX;
rect.worldY = parentWorldY + rect.localY;
return rect;
}
// 第 1 层:先在父局部坐标系内分别解算水平轴和垂直轴。
LogAxisDowngradeIfNeeded(id, true, layoutSpec.horizontal, layoutCapability.allowStretchX);
LogAxisDowngradeIfNeeded(id, false, layoutSpec.vertical, layoutCapability.allowStretchY);
const LayoutAxisResult horizontal = ResolveAxis(parentDesignW, parentCurrentW, localx, localWidth,
layoutSpec.horizontal, layoutCapability.allowStretchX);
const LayoutAxisResult vertical = ResolveAxis(parentDesignH, parentCurrentH, localy, localHeight,
layoutSpec.vertical, layoutCapability.allowStretchY);
// 第 2 层:把父局部矩形映射到世界坐标。
rect.localX = horizontal.pos;
rect.localY = vertical.pos;
rect.width = horizontal.size;
rect.height = vertical.size;
rect.worldX = parentWorldX + rect.localX;
rect.worldY = parentWorldY + rect.localY;
return rect;
}
void Control::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
{
// 默认实现只应用运行态世界矩形。
// 若某个控件还需要在应用后继续刷新内部布局,可重写此函数。
applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height);
}
void Control::applyRuntimeRectDirect(int worldX, int worldY, int width, int height)
{
// 最底层运行态赋值入口:
// 只修改当前世界坐标和运行态尺寸,不触碰设计基线 local*。
this->x = worldX;
this->y = worldY;
this->width = width;
this->height = height;
dirty = true;
}
// 保存当前的绘图状态(字体、颜色、线型等)
// 在控件绘制前调用,确保不会影响全局绘图状态
void Control::saveStyle()
{
gettextstyle(&currentFont); // 获取当前字体样式
currentColor = gettextcolor(); // 获取当前字体颜色
currentBorderColor = getlinecolor(); //保存当前边框颜色
getlinestyle(&currentLineStyle); //保存当前线型
currentBkColor = getfillcolor(); //保存当前填充色
}
// 恢复之前保存的绘图状态
// 在控件绘制完成后调用,恢复全局绘图状态
void Control::restoreStyle()
{
settextstyle(&currentFont); // 恢复默认字体样式
settextcolor(currentColor); // 恢复默认字体颜色
setfillcolor(currentBkColor);
setlinestyle(&currentLineStyle);
setlinecolor(currentBorderColor);
setfillstyle(BS_SOLID);//恢复填充
}
void Control::requestRepaint(Control* parent)
{
if (shouldDeferManagedRepaint())
{
// 托管路径:当前正在 Window 的事件分发阶段,不能立即绘制;
// 这里只登记 source,真正的 root 选择由 Window 在 requestManagedRepaint 中完成。
if (auto* host = getHostWindow())
host->requestManagedRepaint(this);
return;
}
// 说明:
// - 常规路径:子控件调用 requestRepaint(this->parent),然后 parent 负责局部重绘(Canvas/TabControl override
// - 兜底路径:如果某个“容器控件”没 override requestRepaint,就会出现 parent==this 的递归风险
// 此时我们改为向更上层冒泡,直到根重绘。
if (parent == this)
{
SX_LOGW("Dirty")
<< SX_T("requestRepaint(默认容器兜底):id=", "requestRepaint(default-container-fallback): id=")
<< id
<< SX_T("parent==this,向上层 parent 继续冒泡", " parent==this, bubble to upper parent");
if (this->parent) this->parent->requestRepaint(this->parent);
else onRequestRepaintAsRoot();
return;
}
SX_LOG_TRACE("Dirty") << SX_T("请求重绘:id=","requestRepaint: id=") << id << " parent=" << (parent ? parent->getId() : "null");
if (parent) parent->requestRepaint(parent); // 交给容器处理(容器可局部重绘)
else onRequestRepaintAsRoot(); // 根兜底
}
void Control::onRequestRepaintAsRoot()
{
if (shouldDeferManagedRepaint())
{
// 即使已经冒泡到 root,只要还在托管分发期,也不能直接绘制;
// 仍然回到 Window 做统一提交。
if (auto* host = getHostWindow())
host->requestManagedRepaint(this);
return;
}
SX_LOG_TRACE("Dirty")
<< SX_T("触发根重绘:id=", "onRequestRepaintAsRoot: id=") << id
<< SX_T("(从根节点开始重画)", " (root repaint)");
discardBackground();
setDirty(true);
draw(); // 只有“无父”时才允许立即画,不会被谁覆盖
}
bool Control::shouldDeferManagedRepaint() const
{
Window* host = getHostWindow();
return host && host->isManagedDispatchActive();
}
// 获取宿主 Window
// - 顶层控件由 Window/addDialog 直接注入;
// - 子控件没有直接注入时,沿 parent 链向上回溯即可。
Window* Control::getHostWindow() const
{
if (hostWindow)
return hostWindow;
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;
}
Control* Control::getManagedRepaintDirectBranch(Control* root)
{
if (!root)
return nullptr;
Control* branch = this;
while (branch->parent && branch->parent != root)
branch = branch->parent;
return branch;
}
RECT Control::getBoundsRect() const
{
RECT rc{};
rc.left = x;
rc.top = y;
rc.right = x + width;
rc.bottom = y + height;
return rc;
}
RECT Control::getManagedRepaintCoverageRect() const
{
// 基类默认认为“实际绘制 coverage == 控件本体矩形”。
// 复合控件或带浮层的控件会 override,把附加绘制区域一并并入。
return getBoundsRect();
}
RECT Control::getManagedRepaintPersistentCoverageRect() const
{
// 基类默认认为持久绘制范围等于控件本体。
// 只有 Tooltip 这类临时浮层需要从持久范围中剔除。
return getBoundsRect();
}
bool Control::canCommitManagedPartialRepaint() const
{
// 基类默认不承诺自己能安全做局部提交;
// 只有 Canvas / TabControl / Dialog 这类“拥有完整背景语义”的 root 才会 override 为 true。
return false;
}
bool Control::hasManagedDirtySubtree() const
{
// 基类只感知自身是否为脏;
// 容器类会 override 为“自身或其受管理子树是否有脏内容”。
return dirty;
}
void Control::commitManagedRepaint()
{
if (!show)
return;
// 基类兜底:如果没有更具体的容器实现,就按根级重绘处理。
if (dirty)
onRequestRepaintAsRoot();
}
void Control::saveBackground(int x, int y, int w, int h)
{
if (w <= 0 || h <= 0) return;
saveBkX = x; saveBkY = y; saveWidth = w; saveHeight = h;
if (saveBkImage)
{
//尺寸变了才重建,避免反复 new/delete
if (saveBkImage->getwidth() != w || saveBkImage->getheight() != h)
{
SX_LOGD("Snap") <<SX_T("重新保存背景快照:id=", "saveBackground rebuild: id=") << id << " size=(" << w << "x" << h << ")";
saveBkImage.reset();
}
}
else
SX_LOGD("Snap") << SX_T("保存背景快照:id=", "saveBackground rebuild: id=") << id << " size=(" << w << "x" << h << ")";
if (!saveBkImage) saveBkImage = std::make_unique<IMAGE>(w, h);
SetWorkingImage(nullptr); // ★抓屏幕
getimage(saveBkImage.get(), x, y, w, h);
hasSnap = true;
}
void Control::restBackground()
{
if (!hasSnap || !saveBkImage) return;
// 直接回贴屏幕(与抓取一致)
SetWorkingImage(nullptr);
putimage(saveBkX, saveBkY, saveBkImage.get());
}
void Control::discardBackground()
{
if (saveBkImage)
{
restBackground();
SX_LOGD("Snap") << SX_T("丢弃背景快照:id=","discardBackground: id=") << id << " hasSnap=" << (hasSnap ? 1 : 0);
saveBkImage.reset();
}
hasSnap = false; saveWidth = saveHeight = 0;
}
void Control::invalidateBackgroundSnapshot()
{
if (saveBkImage)
{
SX_LOGD("Snap") << SX_T("作废背景快照:id=", "invalidateBackgroundSnapshot: id=") << id
<< " hasSnap=" << (hasSnap ? 1 : 0);
saveBkImage.reset();
}
hasSnap = false;
saveBkX = saveBkY = 0;
saveWidth = saveHeight = 0;
}
-235
View File
@@ -1,235 +0,0 @@
/*******************************************************************************
* @类: Control
* @摘要: 所有控件的抽象基类,定义通用接口和基础功能
* @描述:
* 提供控件的基本属性和方法,包括位置、尺寸、重绘标记等。
* 实现绘图状态保存和恢复机制,确保控件绘制不影响全局状态。
* 同时提供“事件阶段登记、收口阶段统一提交”的托管重绘基础接口。
*
* @特性:
* - 定义控件基本属性(坐标、尺寸、脏标记)
* - 提供绘图状态管理(saveStyle/restoreStyle
* - 声明纯虚接口(draw、handleEvent等)
* - 禁止移动语义,禁止拷贝语义
*
* @使用场景: 作为所有具体控件类的基类,不直接实例化
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
#pragma once
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif
#ifndef WINVER
#define WINVER _WIN32_WINNT
#endif
#include <windows.h>
#include <vector>
#include <memory>
#include <easyx.h>
#undef MessageBox
#include <iostream>
#include <string>
#include <functional>
#include "CoreTypes.h"
class Window;
class Control
{
friend class Window;
friend class Canvas;
protected:
std::string id; // 控件ID
int localx, x, localy, y; // local* 为设计态父局部坐标,x/y 为运行态世界坐标
int localWidth, width, localHeight, height; // local* 为设计态尺寸,width/height 为运行态尺寸
Control* parent = nullptr; // 父控件
Window* hostWindow = nullptr; // 宿主窗口(顶层由 Window 注入,子控件可沿 parent 回溯)
bool dirty = true; // 是否重绘
bool show = true; // 是否显示
bool eventVisualChanged = false; // 最近一次 handleEvent 是否真的引发了视觉变化(用于上层判断是否需要登记重绘)
/* == 布局模式 == */
StellarX::LayoutMode layoutMode = StellarX::LayoutMode::Fixed; // 布局模式
StellarX::Anchor anchor_1 = StellarX::Anchor::Top; // 旧版兼容锚点 1
StellarX::Anchor anchor_2 = StellarX::Anchor::Right; // 旧版兼容锚点 2
// 新布局模型:内部统一使用“水平轴 + 垂直轴”的规格描述,不再直接依赖旧锚点求解。
StellarX::LayoutSpec layoutSpec{
{ false, true, StellarX::AxisSizePolicy::FixedSize, StellarX::AxisAlignPolicy::End },
{ true, false, StellarX::AxisSizePolicy::FixedSize, StellarX::AxisAlignPolicy::Start }
};
// 控件能力边界:用于限制某些控件在特定轴上是否允许 Stretch。
StellarX::LayoutCapability layoutCapability{};
/* == 背景快照 == */
std::unique_ptr<IMAGE> saveBkImage;
int saveBkX = 0, saveBkY = 0; // 快照保存起始坐标
int saveWidth = 0, saveHeight = 0; // 快照保存尺寸
bool hasSnap = false; // 当前是否持有有效快照
StellarX::RouRectangle rouRectangleSize; // 圆角矩形椭圆宽度和高度
LOGFONT currentFont{}; // 保存当前字体样式和颜色
COLORREF currentColor{};
COLORREF currentBkColor{}; // 保存当前填充色
COLORREF currentBorderColor{}; // 边框颜色
LINESTYLE currentLineStyle{}; // 保存当前线型
Control(const Control&) = delete;
Control& operator=(const Control&) = delete;
Control(Control&&) = delete;
Control& operator=(Control&&) = delete;
Control() : localx(0), x(0), localy(0), y(0), localWidth(100), width(100), height(100), localHeight(100) {}
Control(int x, int y, int width, int height)
: localx(x), x(x), localy(y), y(y), localWidth(width), width(width), height(height), localHeight(height) {
}
public:
virtual ~Control()
{
discardBackground();
}
protected:
// 向上请求重绘:普通路径交给父容器,托管路径则登记到 Window
virtual void requestRepaint(Control* parent);
// 根控件/无父时触发重绘
virtual void onRequestRepaintAsRoot();
// 当前是否处于 Window 托管分发阶段;若为真,则不应立即画
bool shouldDeferManagedRepaint() const;
protected:
//保存背景快照
virtual void saveBackground(int x, int y, int w, int h);
// putimage 回屏
virtual void restBackground();
// 回贴旧背景并释放快照
void discardBackground();
public:
// 仅作废快照,不回贴旧背景
void invalidateBackgroundSnapshot();
//“纯作废快照 + 标脏”,不再在 resize 路径里回贴旧背景
virtual void onWindowResize();
// 获取位置和尺寸
int getX() const { return x; }
int getY() const { return y; }
int getWidth() const { return width; }
int getHeight() const { return height; }
int getRight() const { return x + width; }
int getBottom() const { return y + height; }
int getLocalX() const { return localx; }
int getLocalY() const { return localy; }
int getLocalWidth() const { return localWidth; }
int getLocalHeight() const { return localHeight; }
int getLocalRight() const { return localx + localWidth; }
int getLocalBottom() const { return localy + localHeight; }
// 公开几何 setter
// - 面向用户使用
// - 默认只修改运行态矩形
// - 不允许自动回写设计基线 local*
// 若某个复合控件需要在 setter 后维护自己拥有的运行态子树一致性,
// 由该控件在 override 中自行处理,但仍不得越权承担统一布局器职责。
virtual void setX(int x);
virtual void setY(int y);
virtual void setWidth(int width);
virtual void setHeight(int height);
public:
virtual void draw() = 0;
virtual bool handleEvent(const ExMessage& msg) = 0;//返回true代表事件已消费
// 轻量清理接口:仅用于“没有机会收到本次 WM_MOUSEMOVE”的兄弟分支,
// 清掉 hover / tooltip / 临时按下态等鼠标瞬时状态。
// 该接口不是事件分发入口,不参与新的命中判断,也不允许回写设计基线。
virtual bool clearTransientMouseState();
//设置是否显示
virtual void setIsVisible(bool show);
//设置父容器指针
void setParent(Control* parent) { this->parent = parent; }
//设置宿主窗口(通常仅由顶层 Window/对话框注入)
virtual void setHostWindow(Window* host) { this->hostWindow = host; }
Window* getHostWindow() const; // 获取宿主 Window;子控件可沿 parent 向上回溯
RECT getBoundsRect() const; // 获取当前控件外接矩形,用于覆盖/相交判断
// 获取本控件“本轮实际可能写到屏幕上的覆盖范围”。
// 默认等于控件本体矩形;像 Button 这类会额外绘制 Tooltip 的控件,可 override 后扩大范围。
// 托管重绘 coverage、overlay 相交判断统一走这个接口,而不再默认使用控件本体 bounds。
virtual RECT getManagedRepaintCoverageRect() const;
// 获取会影响后续控件背景快照的“持久绘制范围”。
// Tooltip 等临时浮层不应进入该范围,避免上层兄弟补画时把临时浮层抓进背景快照。
virtual RECT getManagedRepaintPersistentCoverageRect() const;
Control* getManagedRepaintRoot(); // 找到本控件对应的托管重绘 root
Control* getManagedRepaintDirectBranch(Control* root); // 找到“root 下面承接本次脏变化的直接子分支”
bool hasValidBackgroundSnapshot() const { return hasSnap && saveBkImage != nullptr; } // 当前是否持有可用于局部恢复的快照
virtual bool hasManagedDirtySubtree() const; // 当前控件或其受管理子树中是否存在待提交的脏内容
virtual bool canCommitManagedPartialRepaint() const; // 当前 root 是否可安全做“局部提交”而非整 root 重画
virtual void commitManagedRepaint(); // 托管收口阶段真正执行绘制的入口
//设置是否重绘
virtual void setDirty(bool dirty) { this->dirty = dirty; }
//检查控件是否可见
bool IsVisible() const { return show; };
//获取控件id
std::string getId() const { return id; }
//检查是否为脏
bool isDirty() { return dirty; }
//获取控件最近一次事件处理是否引发了视觉变化
bool didEventAffectVisual() const { return eventVisualChanged; }
//用来检查对话框是否模态,其他控件不用实现
virtual bool model()const = 0;
//布局
// 设置旧版布局模式(兼容入口)
void setLayoutMode(StellarX::LayoutMode layoutMode_);
// 设置旧版双锚点输入,并映射到内部统一 LayoutSpec
void setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2);
// 直接设置水平轴布局规格。
// 调用该接口后会自动切换到 AnchorToEdges 布局模式;
// 这是新布局模型的公开入口,后设置者会覆盖旧 setAnchor() 对水平轴的映射结果。
void setHorizontalLayoutSpec(const StellarX::AxisLayoutSpec& spec);
// 直接设置垂直轴布局规格。
// 调用该接口后会自动切换到 AnchorToEdges 布局模式;
// 这是新布局模型的公开入口,后设置者会覆盖旧 setAnchor() 对垂直轴的映射结果。
void setVerticalLayoutSpec(const StellarX::AxisLayoutSpec& spec);
// 设置水平轴锚定边集合(left / right)。
void setHorizontalAnchors(bool left, bool right);
// 设置垂直轴锚定边集合(top / bottom)。
void setVerticalAnchors(bool top, bool bottom);
// 设置水平轴尺寸策略(Stretch / FixedSize)。
void setHorizontalSizePolicy(StellarX::AxisSizePolicy policy);
// 设置垂直轴尺寸策略(Stretch / FixedSize)。
void setVerticalSizePolicy(StellarX::AxisSizePolicy policy);
// 设置水平轴固定尺寸位移策略(Start / End / Center / Proportional)。
void setHorizontalAlignPolicy(StellarX::AxisAlignPolicy policy);
// 设置垂直轴固定尺寸位移策略(Start / End / Center / Proportional)。
void setVerticalAlignPolicy(StellarX::AxisAlignPolicy policy);
// 获取旧版锚点 1(兼容读取入口)
StellarX::Anchor getAnchor_1() const;
// 获取旧版锚点 2(兼容读取入口)
StellarX::Anchor getAnchor_2() const;
// 获取旧版布局模式
StellarX::LayoutMode getLayoutMode() const;
// 获取水平轴布局规格;返回的是当前生效的新模型状态,不要求可逆回旧 anchor 语义。
StellarX::AxisLayoutSpec getHorizontalLayoutSpec() const;
// 获取垂直轴布局规格;返回的是当前生效的新模型状态,不要求可逆回旧 anchor 语义。
StellarX::AxisLayoutSpec getVerticalLayoutSpec() const;
// 获取内部统一布局规格;供 Window / Canvas 等统一解算入口使用。
const StellarX::LayoutSpec& getLayoutSpec() const;
// 获取控件能力边界;用于判断某个轴是否允许 Stretch。
const StellarX::LayoutCapability& getLayoutCapability() const;
// 显式将当前运行态矩形提交为新的设计基线。
// 这是“运行态 -> 设计态”的受控入口之一:
// - 普通 resize / 重排过程中不得自动调用
// - 公开 setter 也不得隐式调用
// - 仅用于少数需要把当前结果固化为新设计基线的场景
void commitCurrentGeometryAsDesignRect();
protected:
// 第 1 层:根据父设计尺寸、父当前尺寸和本控件设计矩形,解算出当前运行态局部矩形。
StellarX::ResolvedLayoutRect resolveLayoutRect(int parentDesignW, int parentDesignH,
int parentWorldX, int parentWorldY, int parentCurrentW, int parentCurrentH) const;
// 内部受控路径:把统一解算结果应用到运行态矩形。不要把公开 setter 当成纯赋值接口来复用。
virtual void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect);
// 最底层运行态赋值入口:仅修改运行态矩形,不回写 local*。
void applyRuntimeRectDirect(int worldX, int worldY, int width, int height);
void saveStyle();
void restoreStyle();
void resetEventVisualChanged() { eventVisualChanged = false; }
void markEventVisualChanged(bool changed = true) { eventVisualChanged = changed; }
};
-488
View File
@@ -1,488 +0,0 @@
/*******************************************************************************
* @文件: CoreTypes.h
* @摘要: 星垣(StellarX)框架核心类型定义文件
* @描述:
* 集中定义框架中使用的所有枚举类型和结构体,确保类型一致性。
* 这是框架的类型系统基础,所有控件都依赖于此文件中定义的类型。
*
* @作者: 我在人间做废物
* @日期: September 2025
******************************************************************************/
#pragma once
#include "easyx.h"
/**
* @命名空间: StellarX
*
* @详细说明:
* 集中定义框架中使用的所有枚举类型和结构体,确保类型一致性。
* 这是框架的类型系统基础,所有控件都依赖于此文件中定义的类型
*
* @使用示例:
* StellarX::FillStyle::Horizontal - 填充样式
*
* @备注:
* 不用单独包含此头文件,已在StellarX.h中包含,包含唯一对外头文件即可
*/
namespace StellarX
{
/**
* @枚举类名称: FillStyle
* @功能描述: 用来定义控件填充图案的枚举类
*
* @详细说明:
* 根据此枚举类可以自定义控件填充图案
* 可以在控件初始化时设置填充图案
* 根据具体情况选择不同的填充图案
* 默认填充图案为水平线
*
* @取值说明:
* Horizontal - 水平线
* Vertical - 垂直线
* FDiagonal - 反斜线
* BDiagonal - 正斜线
* Cross - 十字
* DiagCross - 网格
*
* @使用示例:
* FillStyle var = FillStyle::Horizontal;
*
* @备注:
* 此枚举类仅支持图案填充模式
*/
enum class FillStyle
{
Horizontal = HS_HORIZONTAL, // 水平线
Vertical = HS_VERTICAL, // 垂直线
FDiagonal = HS_FDIAGONAL, // 反斜线
BDiagonal = HS_BDIAGONAL, // 正斜线
Cross = HS_CROSS, // 十字
DiagCross = HS_DIAGCROSS // 网格
};
/**
* @枚举类名称: FillMode
* @功能描述: 用来定义控件填充模式的枚举类
*
* @详细说明:
* 根据此枚举类可以自定义控件填充模式
* 可以在控件初始化时设置填充模式
* 根据具体情况选择不同的填充模式
* 默认填充模式为固实填充
*
* @取值说明:
* Solid - 固实填充
* Null - 不填充
* Hatched - 图案填充
* Pattern - 自定义图案填充
* DibPattern - 自定义图像填充
*
* @使用示例:
* FillMode var = FillMode::Solid;
*/
enum class FillMode
{
Solid = BS_SOLID, //固实填充
Null = BS_NULL, // 不填充
Hatched = BS_HATCHED, // 图案填充
Pattern = BS_PATTERN, // 自定义图案填充
DibPattern = BS_DIBPATTERN // 自定义图像填充
};
/**
* @枚举类名称: LineStyle
* @功能描述: 此枚举类用来定义控件边框线型
*
* @详细说明:
* 根据此枚举类可以自定义控件边框线型
* 可以在控件初始化时设置边框线型
* 根据具体情况选择不同的线型
* 默认线型为实线
*
* @取值说明:
* Solid // 实线
* Dash // 虚线
* Dot // 点线
* DashDot // 点划线
* DashDotDot // 双点划线
* Null // 无线
*
* @使用示例:
* LineStyle var = LineStyle::Solid;
*/
enum class LineStyle
{
Solid = PS_SOLID, // 实线
Dash = PS_DASH, // 虚线
Dot = PS_DOT, // 点线
DashDot = PS_DASHDOT, // 点划线
DashDotDot = PS_DASHDOTDOT, // 双点划线
Null = PS_NULL // 无线
};
/**
* @结构体名称: ControlText
* @功能描述: 控件字体样式 可以自定义不同的样式
*
* @详细说明:
* 主要使用的场景为:需要修改或想自定义控件字体大小,字体样式,颜色等
*
* @成员说明:
* int nHeight = 0; - 字体高度
* int nWidth = 0; - 字体宽度 如果为0则自适应
* LPCTSTR lpszFace = "宋体"; - 字体名称
* COLORREF color = RGB(0, 0, 0); - 字体颜色
* int nEscapement = 0; - 字符串旋转角度
* int nOrientation = 0; - 字符旋转角度
* int nWeight = 0; - 字体粗细 范围0~1000 0表示默认
* bool bItalic = false; - 是否斜体
* bool bUnderline = false; - 是否下划线
* bool bStrikeOut = false; - 是否删除线
*/
struct ControlText
{
int nHeight = 0; //- 字体高度
int nWidth = 0; //- 字体宽度 如果为0则自适应
LPCTSTR lpszFace = "微软雅黑"; //- 字体名称
COLORREF color = RGB(0, 0, 0); //- 字体颜色
int nEscapement = 0; //- 字符串旋转角度
int nOrientation = 0; //- 字符旋转角度
int nWeight = 0; //- 字体粗细 范围0~1000 0表示默认
bool bItalic = false; //- 是否斜体
bool bUnderline = false; //- 是否下划线
bool bStrikeOut = false; //- 是否删除线
bool operator!=(const ControlText& text);
ControlText& operator=(const ControlText& text);
};
/**
* @枚举名称: ControlShape
* @功能描述: 枚举控件的不同几何样式
*
* @详细说明:
* 定义了四种(有无边框算一种)不同的几何样式,可以根据具体需求
* 自定义控件的形状。
*
* @取值说明:
* RECTANGLE = 1, //有边框矩形
* B_RECTANGLE, //无边框矩形
* ROUND_RECTANGLE, //有边框圆角矩形
* B_ROUND_RECTANGLE, //无边框圆角矩形
* CIRCLE, //有边框圆形
* B_CIRCLE, //无边框圆形
* ELLIPSE, //有边框椭圆
* B_ELLIPSE //无边框椭圆
*
* @使用示例:
* ControlShape shape = ControlShape::ELLIPSE;
*
* @备注:
* 按钮类支持所有形状,部分控件只支持部分形状,具体请参考控件类。
*/
enum class ControlShape
{
RECTANGLE = 1, //有边框矩形
B_RECTANGLE, //无边框矩形
ROUND_RECTANGLE, //有边框圆角矩形
B_ROUND_RECTANGLE, //无边框圆角矩形
CIRCLE, //有边框圆形
B_CIRCLE, //无边框圆形
ELLIPSE, //有边框椭圆
B_ELLIPSE //无边框椭圆
};
/**
* @枚举类名称: TextBoxmode
* @功能描述: 定义了文本框的三种模式
*
* @详细说明:
* 需要限制文本框是否接受用户输入时使用
*
* @取值说明:
* INPUT_MODE, // 用户可输入模式
* READONLY_MODE // 只读模式
* PASSWORD_MODE // 密码模式
*/
enum class TextBoxmode
{
INPUT_MODE, // 用户可输入模式
READONLY_MODE, // 只读模式
PASSWORD_MODE // 密码模式
};
/**
* @枚举名称: ButtonMode
* @功能描述: 定义按钮的工作模式
*
* @详细说明:
* 根据按钮的工作模式,按钮可以有不同的行为。
* 用户可以在具体情况下设置按钮的工作模式。
*
* @取值说明:
* NORMAL = 1, - 普通模式,点击后触发回调,但不会保持状态。
* TOGGLE, - 切换模式,点击后会在选中和未选中之间切换,触发不同的回调函数。
* DISABLED - 禁用模式,按钮不可点击,显示为灰色,文本显示删除线。
*
* @使用示例:
* ButtonMode mode = ButtonMode::NORMAL;
*/
enum class ButtonMode
{
NORMAL = 1, //普通模式
TOGGLE, //切换模式
DISABLED //禁用模式
};
/**
* @结构体名称: RouRectangle
* @功能描述: 定义了控件圆角矩形样式时圆角的椭圆尺寸
*
* @详细说明:
* 需要修改控件圆角矩形样式时的圆角椭圆。
*
* @成员说明:
* int ROUND_RECTANGLEwidth = 20; //构成圆角矩形的圆角的椭圆的宽度。
* int ROUND_RECTANGLEheight = 20; //构成圆角矩形的圆角的椭圆的高度。
*/
struct RouRectangle
{
int ROUND_RECTANGLEwidth = 20; //构成圆角矩形的圆角的椭圆的宽度。
int ROUND_RECTANGLEheight = 20; //构成圆角矩形的圆角的椭圆的高度。
};
// 消息框类型
enum class MessageBoxType
{
OK, // 只有确定按钮
OKCancel, // 确定和取消按钮
YesNo, // 是和否按钮
YesNoCancel, // 是、否和取消按钮
RetryCancel, // 重试和取消按钮
AbortRetryIgnore, // 中止、重试和忽略按钮
};
// 消息框返回值
enum class MessageBoxResult
{
OK = 1, // 确定按钮
Cancel = 2, // 取消按钮
Yes = 6, // 是按钮
No = 7, // 否按钮
Abort = 3, // 中止按钮
Retry = 4, // 重试按钮
Ignore = 5 // 忽略按钮
};
#if 0 //布局管理器相关 —待实现—
/*
*
*@枚举名称: LayoutKind
* @功能描述 : 定义布局管理类型
*
*@详细说明 :
* 根据布局管理类型,控件可以有不同的布局方式。
* 用户可以在具体情况下设置布局管理类型。
*
* @取值说明 :
* Absolute:不管,保持子控件自己的坐标(向后兼容)。
* HBox: 水平方向排队;支持固定宽、权重分配、对齐、拉伸。
* VBox: 竖直方向排队;同上。
* Grid(网格):按行列摆;支持固定/自适应/权重行列;支持跨行/跨列;单元内对齐/拉伸。
*
*/
// 布局类型
enum class LayoutKind
{
Absolute = 1,
HBox,
VBox,
Grid,
Flow,
Stack
};
// 布局参数
struct LayoutParams
{
// 边距左、右、上、下
int marginL = 0, marginR = 0, marginT = 0, marginB = 0;
// 固定尺寸(>=0 强制;-1 用控件当前尺寸)
int fixedW = -1, fixedH = -1;
// 主轴权重(HBox=宽度、VBox=高度、Grid见下)
float weight = 0.f;
// 对齐(非拉伸时生效)
enum Align { Start = 0, Center = 1, End = 2, Stretch = 3 };
int alignX = Start; // HBox: 次轴=YVBox: 次轴=XGrid: 单元内
int alignY = Start; // Grid :控制单元内垂直(HBox / VBox通常只用 alignX
// Grid 专用(可先不做)
int gridRow = 0, gridCol = 0, rowSpan = 1, colSpan = 1;
// Flow 专用(可先不做)
int flowBreak = 0; // 1=强制换行
};
#endif
/*
* @枚举名称: TabPlacement
* @功能描述: 定义了选项卡页签的不同位置
*
* @详细说明:
* 根据选项卡页签的位置,选项卡页签可以有不同的布局方式。
*
* @成员说明:
* Top, - 选项卡页签位于顶部
* Bottom, - 选项卡页签位于底部
* Left, - 选项卡页签位于左侧
* Right - 选项卡页签位于右侧
*
* @使用示例:
* TabPlacement placement = TabPlacement::Top;
*/
enum class TabPlacement
{
Top,
Bottom,
Left,
Right
};
/*
* @枚举名称: LayoutMode
* @功能描述: 定义了两种布局模式
*
* @详细说明:
* 根据不同模式,在窗口拉伸时采用不同的布局策略
*
* @成员说明:
* Fixed, - 固定布局
* AnchorToEdges - 锚定布局
*
* @使用示例:
* LayoutMode mode = LayoutMode::Fixed;
*/
enum class LayoutMode
{
Fixed,
AnchorToEdges
};
/*
* @枚举名称: Anchor
* @功能描述: 定义了控件相对于窗口锚定的位置
*
* @详细说明:
* 根据不同的锚定位置,有不同的拉伸策略
*
* @成员说明:
* Top, - 锚定上边,控件上边与窗口上侧距离保持不变
* Bottom, - 锚定底边,控件底边与窗口底边距离保持不变
* Left, - 锚定左边,控件左边与窗口左侧距离保持不变
* Right - 锚定右边,控件上边与窗口右侧距离保持不变
*
* @使用示例:
* Anchor a = Anchor::Top;
*/
enum class Anchor
{
NoAnchor = 0,
Left = 1,
Right,
Top,
Bottom
};
/*******************************************************************************
* @枚举: AxisSizePolicy
* @摘要: 单轴尺寸策略,决定该轴是否允许因父容器变化而拉伸
* @详细说明:
* 该枚举只决定“尺寸是否变化”,不决定“位置如何求解”。
* 若需要在固定尺寸下决定控件如何位移,应结合 AxisAlignPolicy 一起使用。
*
* @成员说明:
* Stretch - 允许尺寸随父容器变化而变化
* FixedSize - 尺寸保持设计态大小,仅位置按锚点/位移策略变化
******************************************************************************/
enum class AxisSizePolicy
{
Stretch,
FixedSize
};
/*******************************************************************************
* @枚举: AxisAlignPolicy
* @摘要: 单轴固定尺寸时的位置策略
* @详细说明:
* 该策略只在“该轴不拉伸”时生效,用于决定控件在父容器变化后如何重新定位。
* 其中 Proportional 只用于位置求解,不参与尺寸缩放。
*
* @成员说明:
* Start - 保持起边距离
* End - 保持终边距离
* Center - 保持相对父容器中心的偏移关系
* Proportional - 保持设计态中的相对位置比例
******************************************************************************/
enum class AxisAlignPolicy
{
Start,
End,
Center,
Proportional
};
/*******************************************************************************
* @结构体: AxisLayoutSpec
* @摘要: 描述单个坐标轴上的锚点与解算策略
* @详细说明:
* 一个控件完整布局由“水平轴 + 垂直轴”两份 AxisLayoutSpec 组成。
* anchorStart / anchorEnd 只表达当前轴是否锚定起止边;
* sizePolicy 决定该轴是否允许拉伸;
* alignPolicy 则用于固定尺寸时的位置求解。
******************************************************************************/
struct AxisLayoutSpec
{
bool anchorStart = false;
bool anchorEnd = false;
AxisSizePolicy sizePolicy = AxisSizePolicy::FixedSize;
AxisAlignPolicy alignPolicy = AxisAlignPolicy::Start;
};
/*******************************************************************************
* @结构体: LayoutSpec
* @摘要: 控件完整布局规格,由水平轴和垂直轴两部分组成
******************************************************************************/
struct LayoutSpec
{
AxisLayoutSpec horizontal{};
AxisLayoutSpec vertical{};
};
/*******************************************************************************
* @结构体: LayoutCapability
* @摘要: 控件能力边界,声明控件在哪些轴上允许 Stretch
* @详细说明:
* 规则表负责定义“某种组合如何解算”,LayoutCapability 负责声明
* “该控件是否允许采用这种组合”。例如 Table 当前阶段仅允许 X 轴拉伸,
* 因此 allowStretchY 会被显式关闭。
******************************************************************************/
struct LayoutCapability
{
bool allowStretchX = true;
bool allowStretchY = true;
};
/*******************************************************************************
* @结构体: ResolvedLayoutRect
* @摘要: 统一布局解算后的运行态矩形结果
* @详细说明:
* localX/localY 表示解算完成后的“父局部坐标”;
* worldX/worldY 表示映射到窗口世界坐标后的最终绘制位置;
* width/height 为运行态尺寸。
******************************************************************************/
struct ResolvedLayoutRect
{
int localX = 0;
int localY = 0;
int width = 0;
int height = 0;
int worldX = 0;
int worldY = 0;
};
}
+219
View File
@@ -0,0 +1,219 @@
# Doxyfile 1.9.1
# 项目相关配置
DOXYFILE_ENCODING = UTF-8
PROJECT_NAME = "星垣 (StellarX) GUI Framework"
PROJECT_NUMBER = 1.0.0
PROJECT_BRIEF = "A lightweight, modular C++ GUI framework for Windows"
PROJECT_LOGO =
OUTPUT_DIRECTORY = docs
CREATE_SUBDIRS = NO
ALLOW_UNICODE_NAMES = YES
OUTPUT_LANGUAGE = English
BRIEF_MEMBER_DESC = YES
REPEAT_BRIEF = YES
ABBREVIATE_BRIEF =
ALWAYS_DETAILED_SEC = NO
INLINE_INHERITED_MEMB = NO
FULL_PATH_NAMES = YES
STRIP_FROM_PATH =
STRIP_FROM_INC_PATH =
SHORT_NAMES = NO
JAVADOC_AUTOBRIEF = YES
JAVADOC_BANNER = NO
QT_AUTOBRIEF = NO
MULTILINE_CPP_IS_BRIEF = NO
INHERIT_DOCS = YES
SEPARATE_MEMBER_PAGES = NO
TAB_SIZE = 4
ALIASES =
OPTIMIZE_OUTPUT_FOR_C = NO
OPTIMIZE_OUTPUT_JAVA = NO
OPTIMIZE_FOR_FORTRAN = NO
OPTIMIZE_OUTPUT_VHDL = NO
EXTENSION_MAPPING =
MARKDOWN_SUPPORT = YES
# 构建类型
EXTRACT_ALL = YES
EXTRACT_PRIVATE = YES
EXTRACT_PACKAGE = YES
EXTRACT_STATIC = YES
EXTRACT_LOCAL_CLASSES = YES
EXTRACT_LOCAL_METHODS = NO
EXTRACT_ANON_NSPACES = NO
HIDE_UNDOC_MEMBERS = NO
HIDE_UNDOC_CLASSES = NO
HIDE_FRIEND_COMPOUNDS = NO
HIDE_IN_BODY_DOCS = NO
INTERNAL_DOCS = NO
CASE_SENSE_NAMES = NO
HIDE_SCOPE_NAMES = NO
HIDE_COMPOUND_REFERENCE= NO
SHOW_INCLUDE_FILES = YES
SHOW_GROUPED_MEMB_INC = NO
FORCE_LOCAL_INCLUDES = NO
INLINE_INFO = YES
SORT_MEMBER_DOCS = YES
SORT_BRIEF_DOCS = NO
SORT_MEMBERS_CTOR_1ST = NO
SORT_GROUP_NAMES = YES
SORT_BY_SCOPE_NAME = YES
STRICT_PROTO_MATCHING = NO
GENERATE_TODOLIST = YES
GENERATE_TESTLIST = YES
GENERATE_BUGLIST = YES
GENERATE_DEPRECATEDLIST= YES
MAX_INITIALIZER_LINES = 30
SHOW_USED_FILES = YES
SHOW_FILES = YES
SHOW_NAMESPACES = YES
FILE_VERSION_FILTER =
LAYOUT_FILE =
CITE_BIB_FILES =
# 输入配置
INPUT = include src README.md
INPUT_ENCODING = UTF-8
FILE_PATTERNS = *.h *.hpp *.cpp *.c *.cc *.cxx *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.idl *.ddl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py *.f90 *.f *.for *.vhd *.vhdl *.md
RECURSIVE = YES
EXCLUDE =
EXCLUDE_SYMLINKS = NO
EXCLUDE_PATTERNS =
EXCLUDE_SYMBOLS =
EXAMPLE_PATH = examples
EXAMPLE_PATTERNS = *
EXAMPLE_RECURSIVE = NO
IMAGE_PATH =
INPUT_FILTER =
FILTER_PATTERNS =
FILTER_SOURCE_FILES = NO
FILTER_SOURCE_PATTERNS =
USE_MDFILE_AS_MAINPAGE = README.md
# 源代码浏览配置
SOURCE_BROWSER = YES
INLINE_SOURCES = NO
STRIP_CODE_COMMENTS = YES
REFERENCED_BY_RELATION = YES
REFERENCES_RELATION = YES
REFERENCES_LINK_SOURCE = YES
SOURCE_TOOLTIPS = YES
USE_HTAGS = NO
VERBATIM_HEADERS = YES
# HTML输出配置
GENERATE_HTML = YES
HTML_OUTPUT = html
HTML_FILE_EXTENSION = .html
HTML_HEADER =
HTML_FOOTER =
HTML_STYLESHEET =
HTML_EXTRA_STYLESHEET =
HTML_EXTRA_FILES =
HTML_COLORSTYLE_HUE = 220
HTML_COLORSTYLE_SAT = 100
HTML_COLORSTYLE_GAMMA = 80
HTML_TIMESTAMP = YES
HTML_DYNAMIC_SECTIONS = NO
HTML_INDEX_NUM_ENTRIES = 100
GENERATE_DOCSET = NO
DOCSET_FEEDNAME = "Doxygen generated docs"
DOCSET_BUNDLE_ID = org.doxygen.Project
DOCSET_PUBLISHER_ID = org.doxygen.Publisher
DOCSET_PUBLISHER_NAME = Publisher
GENERATE_HTMLHELP = NO
GENERATE_CHI = NO
GENERATE_QHP = NO
GENERATE_ECLIPSEHELP = NO
DISABLE_INDEX = NO
ENUM_VALUES_PER_LINE = 4
GENERATE_TREEVIEW = YES
TREEVIEW_WIDTH = 250
EXT_LINKS_IN_WINDOW = NO
FORMULA_FONTSIZE = 10
FORMULA_TRANSPARENT = YES
USE_MATHJAX = NO
MATHJAX_FORMAT = HTML-CSS
MATHJAX_RELPATH =
MATHJAX_EXTENSIONS =
MATHJAX_CODEFILE =
SEARCHENGINE = YES
SERVER_BASED_SEARCH = NO
EXTERNAL_SEARCH = NO
SEARCHDATA_FILE = searchdata.xml
EXTERNAL_SEARCH_ID =
EXTRA_SEARCH_MAPPINGS =
# LaTeX输出配置
GENERATE_LATEX = NO
# RTF输出配置
GENERATE_RTF = NO
# Man page输出配置
GENERATE_MAN = NO
# XML输出配置
GENERATE_XML = NO
# DOCBOOK输出配置
GENERATE_DOCBOOK = NO
# 自动生成配置
GENERATE_AUTOGEN_DEF = NO
# 配置生成器
ENABLE_PREPROCESSING = YES
MACRO_EXPANSION = YES
EXPAND_ONLY_PREDEF = NO
SEARCH_INCLUDES = YES
INCLUDE_PATH =
INCLUDE_FILE_PATTERNS =
PREDEFINED = WIN32=1 \
_WINDOWS=1 \
UNICODE=1 \
_UNICODE=1
EXPAND_AS_DEFINED =
SKIP_FUNCTION_MACROS = YES
# 外部引用
TAGFILES =
GENERATE_TAGFILE =
ALLEXTERNALS = NO
EXTERNAL_GROUPS = YES
EXTERNAL_PAGES = YES
# 图表生成
CLASS_DIAGRAMS = YES
MSCGEN_PATH =
DIA_PATH =
HIDE_UNDOC_RELATIONS = YES
HAVE_DOT = YES
DOT_NUM_THREADS = 0
DOT_FONTNAME = Helvetica
DOT_FONTSIZE = 10
DOT_FONTPATH =
CLASS_GRAPH = YES
COLLABORATION_GRAPH = YES
GROUP_GRAPHS = YES
UML_LOOK = NO
UML_LIMIT_NUM_FIELDS = 10
TEMPLATE_RELATIONS = YES
INCLUDE_GRAPH = YES
INCLUDED_BY_GRAPH = YES
CALL_GRAPH = YES
CALLER_GRAPH = YES
GRAPHICAL_HIERARCHY = YES
DIRECTORY_GRAPH = YES
DOT_IMAGE_FORMAT = png
INTERACTIVE_SVG = NO
DOT_PATH =
DOTFILE_DIRS =
MSCFILE_DIRS =
DOT_GRAPH_MAX_NODES = 50
MAX_DOT_GRAPH_DEPTH = 0
DOT_TRANSPARENT = YES
DOT_MULTI_TARGETS = NO
GENERATE_LEGEND = YES
DOT_CLEANUP = YES
+872
View File
@@ -0,0 +1,872 @@
# English API Documentation
[API文档](API 文档.md)
Below is the API documentation for the **StellarX GUI framework** classes and functions, in English. It covers the usage, parameters, and behavior of each primary component:
### Control Class (Abstract Base Class)
**Description:** `Control` is the abstract base class for all GUI controls. It defines common properties and interfaces such as position, size, and dirty (redraw) flags. It also implements saving and restoring of drawing state to ensure that drawing a control does not affect global graphics state. `Control` itself is not instantiated directly.
- **Key Properties:**
- `x, y`: Coordinates of the control's top-left corner.
- `width, height`: Dimensions of the control.
- `dirty`: A flag indicating whether the control needs to be redrawn.
- `show`: Visibility flag (whether the control is visible).
- `rouRectangleSize`: A `StellarX::RouRectangle` struct that stores the ellipse width and height for rounded rectangle corners (used by controls with round-rect shape).
- **Note:** `Control` internally maintains pointers (`currentFont`, etc.) to backup current drawing state (font, colors, line style) so that it can restore the state after custom drawing.
- **Main Interface Methods:**
- `virtual void draw() = 0;`
**Description:** Pure virtual function to draw the control. Each derived control class must implement its own drawing logic.
- `virtual bool handleEvent(const ExMessage& msg) = 0;`
**Description:** Pure virtual function to handle an input event message (mouse or keyboard). Returns true if the event is consumed by this control (meaning it should not propagate further).
- `void saveBackground(int x, int y, int w, int h);`
**Description:** Saves a background snapshot of the specified area `(x, y, w, h)` (in screen coordinates) where the control is drawn. Used to restore background when needed (e.g., hiding a popup).
- `void restBackground();`
**Description:** Restores the last saved background image by putting it back to the saved position. Typically used when a control is hidden or needs to erase its drawing.
- `void discardBackground();`
**Description:** Discards the currently saved background snapshot and frees its resources. Should be called when the saved background is no longer valid (e.g., after window resize) to avoid using stale snapshots.
- *Property Accessors and Mutators:*
- `int getX() const, getY() const, getWidth() const, getHeight() const`: Get the control's position and size.
- `int getRight() const, getBottom() const`: Get the coordinate of the control's right boundary (`x + width`) and bottom boundary (`y + height`).
- `void setX(int nx), setY(int ny), setWidth(int w), setHeight(int h)`: Set the control's position or size. Setting these will mark the control as dirty (need redraw).
- `void setDirty(bool d)`: Manually mark the control as needing redraw or not.
- `void setShow(bool visible)`: Set the control's visibility (`show` flag). If set to false, the control will not draw itself.
- `bool isVisible() const`: Returns the current visibility state of the control (`show` flag). **Note:** If a control is hidden, its `draw()` is typically not called.
- *Other Methods:*
- `virtual bool model() const = 0;`
**Description:** Pure virtual function to check if the control is "modal". Only dialog controls need to implement this (modal dialogs return true), other controls can ignore (return false). The window event loop uses this to prioritize modal dialog events.
- `void saveStyle(); void restoreStyle();` *(protected)*
**Description:** Saves the current global drawing style (font, colors, line style, etc.) and restores it. Controls should call `saveStyle()` at the beginning of `draw()` to backup global settings, and `restoreStyle()` at the end of `draw()` to not pollute the global state for other drawings.
### Window Class (Application Main Window)
**Description:** The `Window` class represents the main application window. It manages the window creation, message loop, and acts as the root container for all controls and dialogs. Typically, an application creates one Window instance as the main GUI container.
- **Constructors:**
- `Window(int width, int height, int mode, COLORREF bkColor = ..., std::string headline = "窗口")`
**Description:** Creates a Window with specified width and height. `mode` is the graphics mode (such as double-buffering or manual flush flags in EasyX, use NULL for default). `bkColor` is the window background color, and `headline` is the window title text. Construction does not immediately display the window; you need to call `draw()` to open it.
- **Main Methods:**
- `void draw();`
**Description:** Initializes and opens the graphics window. It uses the specified mode to create the drawing window (calls EasyX `initgraph`), sets up the window's title and background color, and then draws all added controls. Usually called once right after creating the Window object.
- `void draw(std::string pImgFile);`
**Description:** Similar to the above, but uses an image file as the window background. This loads the image and draws it scaled to the window size, then draws all child controls on top.
- `void runEventLoop();`
**Description:** Enters the main event processing loop for the window. This loop continuously retrieves user input events (`peekmessage`) and dispatches them to child controls or dialogs:
- Dialogs (non-modal ones) get first priority: for each event, it iterates over `dialogs` and if a dialog is visible (`isVisible() == true`) and not modal (`model() == false`), it calls that dialog's `handleEvent`. If any dialog consumes the event (returns true), it stops further propagation.
- If no dialog consumed the event, it then iterates through `controls` (regular controls) in order and calls each control's `handleEvent` until one returns true (meaning the event was handled).
- After handling an event (or if none present), it checks if any dialog is open or was just closed (`dialogClose` flag). If so, it forces a redraw of the entire interface to keep things updated:
- It synthesizes a `WM_MOUSEMOVE` message to update hover states (ensuring controls properly update their highlight states), then calls `draw()` on all controls and dialogs, and flushes the drawing.
- It resets `dialogClose` to false after the redraw.
- Sleeps for 10 ms at each loop iteration to prevent high CPU usage.
- The loop continues until a `WM_CLOSE` message is received (e.g., user closes the window), at which point it sets `running` false and breaks out.
- `void setBkImage(IMAGE* img);` / `void setBkImage(std::string filePath);`
**Description:** Changes the window's background image. You can pass an already loaded `IMAGE*`, or specify a file path to load. After setting, it immediately repaints the window (drawing all controls and dialogs) with the new background.
- `void setBkcolor(COLORREF c);`
**Description:** Sets the window background color and immediately clears the screen with that color (this does not remove an existing background image if one was set; it just overlays the color).
- `void setHeadline(std::string title);`
**Description:** Sets the window title text. If the window is already open, it updates the title bar via `SetWindowText` immediately.
- `void addControl(std::unique_ptr<Control> control);`
**Description:** Adds a regular control to the window. The Window maintains a list of such controls; `draw()` and `runEventLoop()` use this list for rendering and event dispatch. Ownership is transferred (control is managed by Window after adding).
- `void addDialog(std::unique_ptr<Control> dialog);`
**Description:** Adds a dialog control to the window (usually an instance of `Dialog`). Dialogs are managed separately from normal controls, with their own event and drawing handling.
- `bool hasNonModalDialogWithCaption(const std::string& caption) const;`
**Description:** Checks if there is a currently open **non-modal** dialog with the given title caption. Returns true if found. Typically used to avoid opening duplicate dialogs with the same purpose.
- **Overloaded variant:** `bool hasNonModalDialogWithCaption(const std::string& caption, const std::string& text) const;`
Additionally compares the dialog's message text. This provides a stricter duplicate check (same caption **and** same content). It is used internally by `MessageBox::showAsync` to prevent popping up the same notification multiple times.
- *Information Getters:*
- `HWND getHwnd() const;` Returns the underlying window handle (HWND from EasyX).
- `int getWidth() const, getHeight() const;` Returns the window's dimensions.
- `std::string getHeadline() const;` Returns the current window title string.
- `COLORREF getBkcolor() const;` Returns the background color.
- `IMAGE* getBkImage() const;` Returns the current background image pointer (if any).
- `std::vector<std::unique_ptr<Control>>& getControls();` Returns a reference to the list of child controls (for iteration or debugging; generally you don't modify this directly).
### Canvas Class (Container Control)
**Description:** `Canvas` is a container control that can hold child controls, grouping them with a unified background and border style, and enabling composite layouts. Canvas itself is a control (derives from `Control`), and typically used for panels or dialog surfaces.
- **Features:**
- Supports four rectangular background shapes (normal rectangle or rounded rectangle, each with border or borderless version). Set via `setShape`.
- Customizable background color (`canvasBkColor`), border color (`canvasBorderColor`), border line style (`canvasLineStyle`), and fill mode (`canvasFillMode`, e.g., solid color, hatched pattern, no fill).
- Automatically manages the lifecycle of child controls (added via `addControl`, and destroyed when Canvas is destroyed).
- When drawing, Canvas will draw its background and then iterate through its child controls to draw them. For event handling, it propagates events to children (with last-added getting first chance).
- **Important Members:**
- `std::vector<std::unique_ptr<Control>> controls;` The list of child controls.
- `StellarX::ControlShape shape;` Background shape of the container (default is RECTANGLE).
- `StellarX::FillMode canvasFillMode;` Background fill mode (default Solid color).
- `StellarX::LineStyle canvasLineStyle;` Border line style (default Solid line).
- `int canvaslinewidth;` Border line width in pixels.
- `COLORREF canvasBorderColor, canvasBkColor;` Border and background colors of the container.
- `StellarX::LayoutKind Kind;` (Reserved) Layout management type (Absolute/HBox/VBox/Grid). Not fully implemented but can be used to indicate layout strategy.
- **Note:** Canvas overrides `isVisible()` to always return false because a Canvas itself is not considered a standalone visible entity for event loop prioritization (the event loop doesn't handle Canvas directly, only its children). This doesn't affect drawing or child event dispatch.
- **Main Methods:**
- `Canvas(); Canvas(int x, int y, int width, int height);`
**Description:** Constructors to create a Canvas container at position `(x,y)` with given size.
- `void addControl(std::unique_ptr<Control> control);`
**Description:** Adds a child control to the canvas. The child control's coordinates are relative to the canvas (Canvas does not reposition children automatically; you should set the child's position accordingly before adding). After adding, the Canvas is marked dirty so it will redraw with the new content.
- `void setShape(StellarX::ControlShape shape);`
**Description:** Sets the canvas background shape. Supports `RECTANGLE`, `B_RECTANGLE` (borderless rectangle), `ROUND_RECTANGLE` (rounded corners with border), `B_ROUND_RECTANGLE` (rounded without border). If a circular/ellipse shape is passed, Canvas does not support it (it will treat it as RECTANGLE internally).
- `void setCanvasFillMode(StellarX::FillMode mode);`
**Description:** Sets the background fill mode, e.g., Solid, Null (no fill), Hatched pattern, or pattern/bitmap fill. This influences how the background is drawn (using EasyX `setfillstyle`). Default is solid fill.
- `void setBorderColor(COLORREF color);` / `void setCanvasBkColor(COLORREF color);` / `void setCanvasLineStyle(StellarX::LineStyle style);` / `void setLinewidth(int width);`
**Description:** Set the container's border color, background color, border line style, and line width, respectively. Changing any of these will mark the Canvas dirty (to be redrawn).
- `void draw() override;`
**Description:** Draws the canvas background and all its child controls. Steps:
1. If not dirty, returns immediately (no redraw needed).
2. Saves current style and sets line color, fill color, fill style, and line style based on canvas properties.
3. Draws the background shape: e.g., calls `fillrectangle` or `fillroundrect` depending on `shape`. If an unsupported shape (Circle/Ellipse) was set, it defaults to rectangle.
4. Iterates over `controls` and calls each child's `setDirty(true)` then `draw()`, ensuring each child is redrawn on this canvas.
5. Restores the style and marks itself clean (`dirty = false`).
*Note:* Canvas uses `saveStyle()`/`restoreStyle()` to preserve global drawing state, and uses EasyX drawing functions to fill the background according to its shape.
- `bool handleEvent(const ExMessage& msg) override;`
**Description:** Propagates the event to its children in reverse order (so the last added child gets the event first). It iterates `controls` from end to start and calls each child's `handleEvent(msg)`. If any child returns true (meaning it consumed the event), Canvas stops and returns true. If none consumed it, returns false.
*Use case:* This ensures that in overlapping or layered controls, the topmost (last added) gets first crack at the event, implementing a basic Z-order.
- `void clearAllControls();` *(protected)*
**Description:** Clears all child controls from the canvas, deleting them. This releases all smart pointers in the `controls` list. Typically used in destructor or when resetting the UI. In normal use, you don't call this directly (the Canvas destructor will automatically free children).
### Label Class (Static Text Label)
**Description:** `Label` displays a static text string on the UI. It supports transparent background and custom text style, but does not handle user input (no interactive events). It is lightweight and intended for captions, status messages, etc.
- **Constructors:**
- `Label(); Label(int x, int y, std::string text = "标签", COLORREF textColor = BLACK, COLORREF bkColor = WHITE);`
**Description:** Creates a Label control at `(x,y)`. You can specify initial text, text color, and background color. The default text is "标签" (label in Chinese), default text color is black, background is white.
- **Key Properties:**
- `std::string text;` The text content displayed.
- `COLORREF textColor;` The text color.
- `COLORREF textBkColor;` The background color behind the text when not transparent.
- `bool textBkDisap;` Flag indicating if the background is transparent. If true, the label is drawn with a transparent background (so whatever is behind it shows through); if false, it draws an opaque rectangle behind the text using `textBkColor`.
- `StellarX::ControlText textStyle;` The text style struct (includes font face, size, weight, etc. as well as text color).
- **Note:** Label's width and height are initially 0; usually a Label's size is determined by its text content automatically when drawn. You normally don't need to set width/height for labels.
- **Main Methods:**
- `void setTextdisap(bool transparent);`
**Description:** Sets whether the label's background is transparent. If true, when drawing, the text is rendered with `TRANSPARENT` background mode (not overwriting the background behind it). If false, the text is drawn on an opaque background colored `textBkColor`.
- `void setTextColor(COLORREF color);` / `void setTextBkColor(COLORREF color);`
**Description:** Sets the text color and background color, respectively. After setting, the label is marked dirty for redraw.
- `void setText(std::string text);`
**Description:** Changes the label's displayed text content. Marks the label dirty (so it will redraw with the new text).
- `void draw() override;`
**Description:** Draws the label's text (and background if not transparent). It:
- Calls `saveStyle()`.
- If `textBkDisap` is true, calls `setbkmode(TRANSPARENT)`; otherwise `setbkmode(OPAQUE)` and sets `bkcolor` to `textBkColor`.
- Sets the text color via `settextcolor(textColor)` and font via `settextstyle` based on `textStyle`.
- Saves the area behind where the text will be drawn using `saveBackground` (so it can restore it later if needed).
- Writes the text at `(x,y)` using `outtextxy`.
- Restores the drawing style and marks `dirty = false`.
This approach ensures that if the label text is redrawn, the background behind it is handled properly.
- `bool handleEvent(...) override;`
**Description:** Label does not handle any events; it always returns false (meaning it never consumes events).
- `void hide();`
**Description:** Hides the label. Specifically, it uses `restBackground()` to put back the saved background (erasing the text from the screen) and then `discardBackground()` to free the snapshot, and sets `dirty = false`. This is typically used when a Label is serving as a transient tooltip or overlay and needs to be removed without causing a full screen redraw.
- **Usage Scenarios:**
Label is used for static text like descriptions, titles, or status information. You can adjust `textStyle` to change the font and size (by default, the font might be "微软雅黑" with height 0 meaning default height). For example:
```
Label *status = new Label(10, 10, "Ready", RGB(0,128,0));
status->textStyle.nHeight = 20; // set font size if needed
```
If you want the label to blend into a custom background, you can do:
```
status->setTextdisap(true);
```
to make the background transparent.
### Button Class (Button Control)
**Description:** `Button` provides a clickable button control, supporting both standard push-button behavior and toggle (on/off) behavior. It handles click and hover events, and allows setting various styles (shape, colors). Buttons are one of the primary interactive controls.
- **Operating Modes:** Determined by `StellarX::ButtonMode`:
- `NORMAL` Standard push-button. Each click triggers an action but does not maintain a pressed state.
- `TOGGLE` Toggle button. Each click changes the button's state (pressed vs not pressed) and triggers different callbacks for each state.
- `DISABLED` Disabled button. It does not respond to user clicks and typically displays in a grayed-out style with strikeout text.
- The mode can be changed via `setButtonMode(ButtonMode mode)`.
- **Appearance Shape:** Determined by `StellarX::ControlShape`:
- Supports rectangle (`RECTANGLE`/`B_RECTANGLE`), rounded rectangle (`ROUND_RECTANGLE`/`B_ROUND_RECTANGLE`), circle (`CIRCLE`/`B_CIRCLE`), and ellipse (`ELLIPSE`/`B_ELLIPSE`) eight shape options in total (`B_` prefix indicates borderless). Use `setButtonShape(ControlShape shape)` to set.
- Note: When switching shapes, ensure the button's width/height are appropriate (for circle/ellipse shapes, the drawing will use width and height differently; circle uses min(width,height)/2 as radius, ellipse uses width,height as bounding box).
- The button's border will be drawn for shapes without the `B_` prefix; borderless shapes omit the border.
- **Key Configurable Properties:**
- **Colors:**
- `buttonTrueColor` color when button is in pressed state (for toggle or momentary press in normal mode).
- `buttonFalseColor` color when button is not pressed (normal default state).
- `buttonHoverColor` color when mouse is hovering over the button.
- `buttonBorderColor` border outline color.
- **Fill:**
- `buttonFillMode` fill mode for the button background (solid, hatched pattern, custom pattern, custom image).
- `buttonFillIma` pattern style for hatched fills (if `FillMode == Hatched`).
- `buttonFileIMAGE` pointer to an `IMAGE` for custom image fill (if `FillMode == DibPattern`).
- **Text:**
- `text` the text label displayed on the button.
- `textStyle` text style (font face, size, weight, etc., including text color).
- Additionally, `cutText` is used internally if the text is too long to fit; the button can automatically truncate and add "..." or a Chinese ellipsis to fit.
- **Tooltip (hover hint):**
- `tipEnabled` whether a tooltip is enabled on hover.
- `tipTextClick` tooltip text for the NORMAL mode (single-state).
- `tipTextOn` / `tipTextOff` tooltip texts for toggle mode when the button is ON or OFF.
- `tipFollowCursor` if true, tooltip appears near the cursor; if false, tooltip appears at a fixed offset below the button.
- `tipDelayMs` delay in milliseconds before tooltip appears when hovering.
- `tipOffsetX, tipOffsetY` offset of tooltip position relative to cursor or button (depending on `tipFollowCursor`).
- `tipLabel` an internal `Label` object used to display the tooltip text.
- **Rounded Corner Size:** In round-rectangle shapes, the corner radii come from the inherited `rouRectangleSize` (`ROUND_RECTANGLEwidth` and `height`). Set via `setRoundRectangleWidth(int)` and `setRoundRectangleHeight(int)`.
- **Main Methods:**
- `void setOnClickListener(const std::function<void()>&& callback);`
**Description:** Sets the callback function to be invoked when the button is clicked in NORMAL mode. For a toggle button, this callback is called every time the button is clicked (but typically you might use toggle-specific callbacks instead). The callback is executed when the user releases the mouse button over the button.
- `void setOnToggleOnListener(const std::function<void()>&& callback);`
**Description:** Sets the callback for when a toggle button is switched to the "on/pressed" state.
- `void setOnToggleOffListener(const std::function<void()>&& callback);`
**Description:** Sets the callback for when a toggle button is switched to the "off/released" state.
- `void setButtonText(const char* text)` / `void setButtonText(std::string text);`
**Description:** Changes the button's label text. Accepts a C-string or an std::string. After changing, it recalculates the text width/height (for centering and truncation logic) and marks the button dirty to redraw.
- `void setButtonBorder(COLORREF border);` / `void setButtonFalseColor(COLORREF color);`
**Description:** Sets the border color and the default (false) state fill color, respectively.
- `void setFillMode(FillMode mode);` / `void setFillPattern(FillStyle pattern);` / `void setFillImage(const std::string& path);`
**Description:** Configures the background fill of the button:
- `setFillMode` changes the fill mode (solid, hatched pattern, custom bitmap, etc).
- If setting to a hatched pattern fill, call `setFillPattern` to choose the hatch style (`StellarX::FillStyle`).
- If setting to a custom image fill, call `setFillImage` with the image file path; it will load the image (resizing to button size) and store it in `buttonFileIMAGE`.
- `void setButtonClick(bool click);`
**Description:** Programmatically sets the button's "clicked" state.
- For NORMAL mode: setting true will simulate a click it triggers the onClick callback (if any) and then immediately resets to false (not pressed).
- For TOGGLE mode: setting true or false will force the button into that state and trigger the corresponding onToggle callback if provided.
- In any case, it marks the button dirty and redraws it (ensuring visual state update).
- **Note:** If the button is disabled, this method has no effect.
- `bool isClicked() const;`
**Description:** Returns whether the button is currently in the pressed state. This is primarily meaningful for toggle buttons (true if toggled on, false if off). For a normal button, this is typically false except during the brief moment of click handling.
- Other getters such as `getButtonText()`, `getButtonMode()`, `getButtonShape()`, `getFillMode()`, `getFillPattern()`, `getFillImage()`, `getButtonBorder()`, `getButtonTextColor()`, `getButtonTextStyle()`, `getButtonWidth()`, `getButtonHeight()` provide read access to the button's corresponding properties.
- `void draw() override;`
**Description:** Draws the button's background and text according to its current state and properties:
- Chooses the fill color based on state: if disabled, uses `DISABLEDCOLOUR` (gray) and applies a strike-out to the text; otherwise, if `click` is true (pressed or toggled on) uses `buttonTrueColor`, else if `hover` is true uses `buttonHoverColor`, otherwise `buttonFalseColor`.
- Sets transparent background mode for text, sets border color and text style (color, font).
- If `needCutText` is true, calls `cutButtonText()` to possibly truncate the text with ellipsis if it doesn't fit in the button width.
- Recalculates `text_width` and `text_height` if the text content or style changed (to ensure text is centered correctly).
- Sets the fill style for background (using `buttonFillMode`, pattern or image as needed).
- Draws the shape:
- For rectangle shapes: uses `fillrectangle` or `solidrectangle`.
- For round-rectangle: uses `fillroundrect` or `solidroundrect` with `rouRectangleSize` radii.
- For circle: uses `fillcircle` or `solidcircle` with radius = min(width,height)/2.
- For ellipse: uses `fillellipse` or `solidellipse` with bounding box corners.
- Draws the text:
- It calculates the position to center the text/cutText horizontally and vertically within the button.
- If `isUseCutText` is true (meaning the original text was truncated), it draws `cutText` (with "..." or Chinese ellipsis).
- Otherwise, draws the full `text`.
- Restores style and marks dirty false after drawing.
- `bool handleEvent(const ExMessage& msg) override;`
**Description:** Handles mouse events for the button:
- If the button is hidden (`show == false`), returns false immediately.
- Tracks the previous `hover` and `click` states to detect changes.
- If the message is `WM_MOUSEMOVE`, updates `lastMouseX/Y` for tooltip positioning.
- Determines `hover` state by checking if the mouse `(msg.x, msg.y)` is within the button's shape (calls `isMouseInCircle` or `isMouseInEllipse` for those shapes, otherwise simple rectangle bounds check).
- Handling `WM_LBUTTONDOWN`: if button is enabled and mouse is over it:
- In NORMAL mode: set `click = true` (button appears pressed), mark dirty, and mark event consumed.
- In TOGGLE mode: do nothing on down (toggle action is deferred to release).
- Handling `WM_LBUTTONUP`: if button is enabled and mouse is currently over it:
- In NORMAL mode: if it was pressed (`click` was true), trigger the onClick callback (if set), then set `click = false` (release it). Mark dirty, consume event, and hide tooltip (if any). Also flush the message queue of any pending mouse/keyboard events using `flushmessage` to prevent processing a duplicate click message.
- In TOGGLE mode: flip the `click` state (`click = !click`). If it becomes true, trigger onToggleOn callback; if false, trigger onToggleOff callback. Mark dirty, consume event, update tooltip text via `refreshTooltipTextForState()`, hide tooltip, and flush message queue similarly.
- Handling `WM_MOUSEMOVE`:
- If moving outside (not hover anymore) while in NORMAL mode and the button was pressed (`click == true`), then user is dragging out: set `click = false` (cancel the press) and mark dirty (so it returns to unpressed visual).
- If hover state changed (entered or exited), mark dirty.
- Tooltip management:
- If tooltip enabled (`tipEnabled`):
- If just hovered (hover true and oldHover false): record the timestamp via `tipHoverTick` and set `tipVisible = false` (starting hover timer).
- If just exited (hover false and oldHover true): immediately call `hideTooltip()` (which hides the tooltip label if it was visible).
- If still hovering and tooltip not yet visible:
- Check if current time minus `tipHoverTick` >= `tipDelayMs`; if so:
- Set `tipVisible = true`.
- Determine tooltip position: if `tipFollowCursor` is true, `tipX = lastMouseX + tipOffsetX`, `tipY = lastMouseY + tipOffsetY`. If false, perhaps `tipX = lastMouseX` (or button center) and `tipY = y + height` (so it appears below the button).
- Set the tooltip text: if `tipUserOverride` is true (meaning user explicitly set tooltip text via `setTooltipText` or `setTooltipTextsForToggle`), then:
- For NORMAL mode: always use `tipTextClick` (explicitly set text or maybe button text by default).
- For TOGGLE mode: use `click ? tipTextOn : tipTextOff`.
- If `tipUserOverride` is false (no explicit text provided):
- If mode is TOGGLE: default behavior might be to use `tipTextOn/Off` (which could be set to default values like "On"/"Off" or left empty).
- If mode is NORMAL: possibly do nothing (the code currently only sets text in else for toggle, leaving normal with no dynamic text unless overridden).
- Position the internal `tipLabel` at (tipX, tipY), mark it dirty.
- After updating tooltip state,
- If `hover` or `click` state changed compared to old states, mark the button dirty so it will redraw to reflect highlight or press/unpress.
- If the button is dirty, call `draw()` immediately for real-time feedback.
- If tooltip is enabled and now visible (`tipVisible`), call `tipLabel.draw()` to draw the tooltip label.
- Return true if the event was handled (for example, a click or certain modal conditions), else false.
- (In summary, the event is considered consumed if a click occurred or if the modal logic swallowed it. Hover alone doesnt consume the event.)
- **Usage Notes:**
Typically, you create a Button and set its callback like:
```
auto btn = std::make_unique<Button>(50, 50, 80, 30, "OK");
btn->setOnClickListener([](){ /* handle click */ });
```
For toggle functionality:
```
btn->setButtonMode(StellarX::ButtonMode::TOGGLE);
btn->setOnToggleOnListener([](){ /* handle toggle on */ });
btn->setOnToggleOffListener([](){ /* handle toggle off */ });
```
Adjusting appearance:
```
btn->textStyle.color = RGB(255,255,255); // white text
btn->setButtonFalseColor(RGB(100,150,200)); // default background
btn->setButtonBorder(RGB(80,80,80)); // dark gray border
btn->setButtonShape(StellarX::ControlShape::RECTANGLE);
```
To provide a tooltip on hover:
```
btn->tipEnabled = true;
btn->tipTextClick = "Click to confirm"; // for normal mode
// for toggle mode:
// btn->setTooltipTextsForToggle("On state hint", "Off state hint");
```
The Button control will handle highlighting itself when hovered, pressing down on click, toggling, etc. Ensure to keep the `runEventLoop` running so these UI feedbacks happen in real time.
### TextBox Class (Single-line Text Box)
**Description:** `TextBox` is a single-line text input box, which can either allow user input or be read-only. It uses an EasyX input box for user text entry (which is a modal popup), then displays the entered text in the control. It's a simple text field primarily for small inputs like numbers or short strings.
- **Modes:** Controlled by `StellarX::TextBoxmode`:
- `INPUT_MODE` the user can click and enter text. On click, a modal input dialog appears where the user can type.
- `READONLY_MODE` the text box is for display only. Clicking it will not change the text; it might just show an alert that input is not allowed (the current implementation pops an InputBox with a message).
- Set via `setMode(TextBoxmode mode)`.
- **Appearance:**
- Supports rectangular shapes (normal or rounded, with or without border). Set with `setTextBoxShape(ControlShape)`. Circular/ellipse shapes are not logically typical for text entry and are treated as rectangle in the implementation.
- By default, a TextBox has a black border and white background. Use `setTextBoxBorder` and `setTextBoxBk` to change those.
- Text style is adjustable via `textStyle` (including font face, size, color).
- **Key Properties:**
- `std::string text;` The content string displayed in the text box.
- `TextBoxmode mode;` Current mode (input or read-only).
- `ControlShape shape;` Current border shape (default RECTANGLE or B_RECTANGLE).
- `bool click;` (Internal flag) Indicates if the text box was clicked (used to trigger the InputBox; it's set true on LButtonUp and then immediately reset after processing).
- `size_t maxCharLen;` Maximum number of characters allowed. Default is 255.
- `COLORREF textBoxBorderColor, textBoxBkColor;` Border color and background color of the text box.
- `StellarX::ControlText textStyle;` Text style for the content (e.g., can use monospace font for numeric input if desired, or change the color).
- **Main Methods:**
- `void setMode(StellarX::TextBoxmode mode);`
**Description:** Switches the text box between input mode and read-only mode. Changing mode does not clear the current text content.
- `void setMaxCharLen(size_t len);`
**Description:** Sets the maximum number of characters that can be input. If len > 0 (non-zero positive), it will enforce that limit. Input beyond this length will be truncated.
- `void setTextBoxShape(StellarX::ControlShape shape);`
**Description:** Sets the shape of the text box. Supports rectangle or rounded rectangle (with or without border). If a shape like circle/ellipse is passed, it's internally treated as rectangle (the implementation falls back to rectangle for unsupported shapes). Changing shape marks the text box dirty for redraw.
- `void setTextBoxBorder(COLORREF color);` / `void setTextBoxBk(COLORREF color);`
**Description:** Sets the border color and background color of the text box.
- `void setText(std::string text);`
**Description:** Updates the displayed text content of the text box. If the new text exceeds `maxCharLen`, it is automatically truncated to that length. Marks the text box dirty and immediately calls `draw()` to update the displayed text.
- `std::string getText() const;`
**Description:** Returns the current text content of the text box.
- `void draw() override;`
**Description:** Draws the text box background and the text:
- Saves style and sets `fillcolor` to `textBoxBkColor`, `linecolor` to `textBoxBorderColor`.
- Ensures the font height/width do not exceed the control's dimensions (if `textStyle.nHeight` is larger than `height`, it sets it equal to height; similarly for width).
- Sets the font and text color via `settextstyle` and `settextcolor` according to `textStyle`.
- Transparent background mode (`setbkmode(TRANSPARENT)` is used to avoid drawing a separate background behind text, since the background is already filled by fillrectangle).
- Calculates the pixel width and height of the text (`textwidth` and `textheight`).
- Draws the background shape:
- If shape is RECTANGLE (with border): calls `fillrectangle` (the border is then drawn by outline because linecolor is set).
- If shape is B_RECTANGLE: calls `solidrectangle` (no border outline).
- If shape is ROUND_RECTANGLE: calls `fillroundrect` using `rouRectangleSize` for corners.
- If shape is B_ROUND_RECTANGLE: calls `solidroundrect`.
- (Other shapes default to rectangle).
- Draws the text: It positions the text 10 pixels from the left inside the box (`x + 10`) and vertically centers it (`y + (height - text_h) / 2`).
- Restores the style and sets dirty false.
- **Note:** If text content is larger than the control width, it will overflow/clipped; there's no automatic horizontal scrolling or ellipsis for text box implemented. The developer should ensure the width is sufficient or the `maxCharLen` is set appropriately.
- `bool handleEvent(const ExMessage& msg) override;`
**Description:** Handles the mouse events for the text box:
- Determines if the mouse is over the text box (`hover`) using the shape logic (similar to button, but generally rectangular).
- For `WM_LBUTTONUP` when the cursor is over the text box:
- If in `INPUT_MODE`: It sets `click = true` and calls `InputBox` (EasyX's modal input dialog). The `InputBox` parameters include:
- The output buffer (here passing `LPTSTR(text.c_str())` to supply initial text).
- Max characters (`maxCharLen`).
- A title like "输入框" (Input Box).
- It passes `text.c_str()` as default content, etc.
- The returned value (the function returns nonzero if text changed, 0 if canceled, but here they capture it as `dirty`).
- After `InputBox` returns, if any input was done, the `text` variable will contain the new text (because the first parameter was the buffer referencing `text` content).
- They mark `dirty` based on InputBox result (the code sets `dirty` to whatever InputBox returned, which in EasyX indicates whether text was changed).
- They mark `consume = true` to indicate the click was handled.
- If in `READONLY_MODE`: They do not change the text. Instead, they possibly call `InputBox(NULL, maxCharLen, "输出框(输入无效!)", ... text ...)` which essentially shows the text in an output box stating input is invalid (effectively a message box).
- They keep `dirty = false` (since no visual change), set `consume = true`.
- In both cases, after handling, they flush the input messages with `flushmessage(EX_MOUSE | EX_KEY)` to clear any leftover events (similar reason to button: to avoid re-triggering).
- For other messages (like `WM_LBUTTONDOWN` or `WM_MOUSEMOVE`):
- `WM_LBUTTONDOWN` is not explicitly handled (the code simply sets `hover` and sets `consume = false` in the switch).
- `WM_MOUSEMOVE`: The code sets `hover` accordingly (and uses `consume = false`).
- After event processing:
- If `dirty` is true (text changed), it calls `draw()` to refresh the display with new text.
- If `click` was set (meaning an input happened), it resets `click = false` after handling to ensure state is consistent.
- Returns `consume` (which would be true if a click caused an InputBox or a read-only alert, false otherwise).
- Essentially, clicking the text box in input mode opens an input dialog for user to type, and clicking it in read-only mode just shows a dummy output box (so user knows it's not editable).
- **Usage Considerations:**
- Because `InputBox` is modal, it will pause the entire program's event loop until the user closes the input dialog. This simplifies input handling but means the UI doesn't update during text entry (the `runEventLoop` is blocked). For small apps and short inputs, this is acceptable, but for more advanced usage you might want to implement a custom non-blocking input field.
- Always set an appropriate `maxCharLen` if expecting certain input sizes (like numeric values of certain length) to avoid overflow of the display region.
- Use `textStyle` to set a suitable font for the input. For instance, for numeric input, you might choose a monospace font for alignment.
- Example:
```
auto field = std::make_unique<TextBox>(100, 100, 200, 30, "0");
field->setMaxCharLen(10);
field->textStyle.color = RGB(255,69,0); // orange text
field->textStyle.nHeight = 18;
// field is input mode by default. Optionally:
// field->setMode(StellarX::TextBoxmode::READONLY_MODE);
```
- After user input (in input mode), you can retrieve the text via `field->getText()`. In read-only mode, you may set the text programmatically via `setText` and it will just display.
### Dialog Class (Dialog Control)
**Description:** `Dialog` implements modal and modeless dialog windows with rich content and multiple buttons (like message boxes). It inherits from `Canvas` (so it is a container), and includes a caption, message text, and standard buttons combination. It integrates with `Window` to manage blocking input for modal dialogs.
- **Creation & Modes:**
- Constructor `Dialog(Window& parent, std::string title, std::string message = "对话框", MessageBoxType type = OK, bool modal = true)`
**Description:** Creates a dialog associated with the `parent` window. `title` is the dialog title text (often shown at top of dialog), `message` is the body text content. `type` specifies which buttons are included (see `MessageBoxType` enum for combinations, e.g., OK, YesNo, etc.). `modal` determines if this is a modal dialog (true) or modeless (false). The dialog is initially not visible (you must call `show()` to display it).
- `MessageBoxType` enumeration defines common button sets:
- `OK` only an "OK" button.
- `OKCancel` "OK" and "Cancel" buttons.
- `YesNo` "Yes" and "No" buttons.
- `YesNoCancel` "Yes", "No", and "Cancel".
- `RetryCancel` "Retry" and "Cancel".
- `AbortRetryIgnore` "Abort", "Retry", "Ignore".
- Modal dialogs (`modal == true`): When `show()` is called, it will block the current thread until the dialog is closed (the internal `show()` implementation runs its own loop). This is useful for confirmations or critical inputs that need immediate resolution. After `show()` returns, you can get the result via `getResult()`.
- Modeless dialogs (`modal == false`): When `show()` is called, it returns immediately (the dialog remains open, but the program flow continues and the main event loop will handle the dialogs interactions concurrently). To get the result, you typically provide a callback or check `getResult()` after the dialog closes (e.g., via a callback or a flag).
- **Component Elements of Dialog:**
- **Title label:** A `Label` (stored in `title`) showing the dialog title at top.
- **Message text:** The main content text, which can be multi-line. It's internally split into `lines` and drawn in the dialog client area.
- **Buttons:** One to three `Button` controls at the bottom of the dialog, depending on `MessageBoxType`. They are usually centered or evenly spaced.
- **Close (X) button:** A small `Button` in the top-right corner to close the dialog (especially for modeless dialogs, acts like a Cancel).
- **Background style:** Typically a rounded rectangle background with a slight border. There is no actual transparency or blur (the background might just capture what's behind for restoration when closed).
- **Key Properties:**
- `bool modal;` Whether the dialog is modal.
- `titleText` The dialog's title text string.
- `message` The dialog's message content string.
- `MessageBoxType type;` The type of dialog (determines buttons).
- `std::vector<std::string> lines;` The message content split into lines (populated internally).
- `std::unique_ptr<Label> title;` Label control for title.
- `Button* closeButton;` Pointer to the "X" close button.
- `Window& hWnd;` Reference to the parent window (for sending refresh signals, etc.).
- `StellarX::MessageBoxResult result;` The result of the dialog (which button was clicked by the user). Common values: OK=1, Cancel=2, Yes=6, No=7, etc. (Matches typical message box results).
- `std::function<void(MessageBoxResult)> resultCallback;` If set (for modeless use), a callback function to be called when the dialog is closed with the final result.
- (Other internal flags track if initialization is needed, if a cleanup is pending, etc.)
- **Main Methods:**
- `void show();`
**Description:** Displays the dialog.
- If `modal == true`, this function enters a loop that blocks the rest of the application until the dialog is closed. During this period, it intercepts events in its own loop (preventing main window events).
- If `modal == false`, this function simply makes the dialog visible and returns immediately; the dialog will be handled by the main window's event loop.
- Note: The implementation sets `dirty` and possibly runs its own flush for modals, and uses `shouldClose` to break out of the modal loop if needed.
- `void closeDialog();`
**Description:** Closes the dialog.
- It hides the dialog (`show = false`), marks it for cleanup, and signals the parent window to refresh other controls (by marking them dirty).
- If a `resultCallback` is set and the dialog is modeless, it invokes the callback with the result.
- This is typically called internally when a button is clicked (like Cancel or the X button).
- `StellarX::MessageBoxResult getResult() const;`
**Description:** Returns the `MessageBoxResult` code indicating which button the user pressed. For modal dialogs, you call this after `show()` returns to get the user's choice. For modeless, you'll typically get the result from the callback rather than polling this function.
- `void setTitle(const std::string& title);` / `void setMessage(const std::string& message);`
**Description:** Changes the dialog's title or message text. Changing the message triggers re-splitting into lines and recalculating sizes (the code marks dirty and reinitializes layout).
- `void setType(StellarX::MessageBoxType newType);`
**Description:** Changes the dialog's button configuration. This will recreate the buttons layout (calls `initButtons()` internally) and mark the dialog dirty.
- `void setModal(bool modal);`
**Description:** Sets the modal property (should be done before showing). Typically you decide at creation whether a dialog is modal or not; toggling at runtime is rare.
- `void setResult(StellarX::MessageBoxResult res);`
**Description:** Sets the dialog's result code. This is called internally when a button is clicked (for example, clicking "Yes" calls `setResult(MessageBoxResult::Yes)`).
- `void setInitialization(bool init);`
**Description:** If `init` is true, it performs an initialization of dialog size and captures the background. This method is used before showing the dialog (particularly by `MessageBox::showModal` and `showAsync`) to prepare the dialog geometry off-screen. Essentially, it calls `initDialogSize()` and saves the background behind where the dialog will appear (so that it can restore it on close).
- `void setResultCallback(std::function<void(MessageBoxResult)> cb);`
**Description:** Sets a callback function to be called with the dialog result when a modeless dialog is closed. Use this for asynchronous dialog handling.
- *Private internal methods (for implementation):*
- `initButtons()` Creates the appropriate `Button` objects for the dialog's `type`, positions them, and sets their onClick listeners to call `setResult` and `closeDialog`. For example, in a YesNo dialog, it creates "Yes" and "No" buttons and sets their listeners to `SetResult(Yes)` / `...No` and `closeDialog()` accordingly.
- `initCloseButton()` Creates the "X" close button at top-right of dialog (small size). Its onClick calls `SetResult(Cancel)` (or similarly appropriate default) and triggers `hWnd.dialogClose = true` and `closeDialog()`.
- `initTitle()` Creates the title Label at the top inside the dialog.
- `splitMessageLines()` Splits the `message` string by newline characters into the `lines` vector for drawing.
- `getTextSize()` Calculates the maximum text width and single line height among the `lines`. It does this by setting the text style and measuring each line (`textwidth` & `textheight` calls). It stores results in `textWidth` and `textHeight`.
- `initDialogSize()` Computes the dialog's required width and height based on:
- The greater of text area width (textWidth + margins) and button area width (depending on number of buttons).
- For height: sums title bar height, spacing to text, total text block height (line count * textHeight + line spacing), and button area height.
- Enforces a minimum width (in code, 200 px).
- Centers the dialog on the parent window by setting `x,y` such that the dialog is at window center.
- Also sets a default text style height (e.g., 20 px).
- Then calls `initButtons()`, `initTitle()`, and `initCloseButton()` to create all child controls.
- `performDelayedCleanup()` Handles the delayed destruction of the dialog's content after its closed. It:
- Marks `isCleaning = true` to avoid reentry.
- Clears all child controls (`controls.clear()`).
- Resets pointers (closeButton, title).
- If a background image was saved and still present (`hasSnap` is true), it restores the screen background (`restBackground()`) and discards the snapshot (`discardBackground()`) to remove any ghost image of dialog.
- Resets flags (needsInitialization, pendingCleanup, isCleaning, shouldClose).
- This is called by the event loop after the dialog is hidden, to safely remove the dialog from UI and restore background.
- `void draw() override;`
**Description:** Draws the dialog. It combines container drawing with additional text drawing:
- If dialog is not visible (`show == false`), it does nothing (except if `pendingCleanup` and not cleaning, it calls `performDelayedCleanup()`, but that's in handleEvent rather than draw).
- If `needsInitialization` is true and dialog is now visible, it calls `initDialogSize()` to compute layout and then sets `needsInitialization = false`. This ensures the dialog's size and children are all properly set up on first draw.
- It then checks `dirty` and `show`, and if both are true:
- Saves drawing style.
- If it's the first draw and `saveBkImage` is null, it calls `saveBackground(...)` to capture what's behind the dialog.
- Sets the border color, border width, background color, and shape on the base Canvas part (calls `Canvas::setBorderColor`, etc.) to round rectangle and appropriate style.
- Marks all child controls dirty (so they will be redrawn).
- Calls `Canvas::draw()` to draw the rounded rectangle background and all the child controls (like buttons, title).
- Then sets text color and style (for the message text) and calculates a starting y coordinate `ty` just below the title area (using `closeButtonHeight` and a margin).
- For each line in `lines`, it computes an x coordinate `tx` to center that line in the dialog (x + (width - textwidth(line))/2), and then outputs the text with `outtextxy(tx, ty, line)`.
- Moves `ty` down by textheight + 5 for the next line (5 px line spacing).
- Restores style, sets `dirty = false`.
- The base Canvas::draw already drew the background and the title label and buttons. So these text drawing steps are to overlay the message text content (which is not a separate control, but drawn directly).
- `bool handleEvent(const ExMessage& msg) override;`
**Description:** Handles events for the dialog:
- If the dialog is not visible (`!show`):
- If there's a `pendingCleanup` and not already cleaning, it calls `performDelayedCleanup()`.
- Returns false (no event consumed).
- If a cleanup is pending or in progress (`pendingCleanup || isCleaning`), it immediately returns false (dialog isn't interactive at this moment).
- If modal:
- If the user clicked outside the dialog (LButtonUp at a point outside `[x, y, x+width, y+height]`):
- It prints a bell (`std::cout << "\a"`) to alert (like system beep).
- Returns true (consumes the event) to prevent underlying controls from receiving the click.
- Then it attempts to dispatch the event to dialog's child controls:
- `consume = Canvas::handleEvent(msg)` calls the base container logic to let any button inside the dialog handle the event (e.g., if user clicked "OK", the button's handleEvent will consume it).
- If a child consumed it, `consume` becomes true.
- After dispatching, if `pendingCleanup && !isCleaning`, it calls `performDelayedCleanup()` (maybe if a button triggered immediate closure).
- Returns `consume` (so if a dialog button or modal logic handled the event, it returns true, otherwise false).
- The effect is that normal controls behind won't receive events while a modal dialog is open (due to the modal check swallowing outside clicks and because the Window event loop prioritizes dialogs).
- For modeless dialogs, events outside are not swallowed by Dialog (so user can interact with background controls as well, which is typical for modeless).
- **Usage Examples:**
It's more common to use `MessageBox` static methods to show dialogs (documented next), but you can use `Dialog` directly:
- **Modal example:**
```
Dialog confirm(window, "Confirm Delete", "Are you sure you want to delete?", StellarX::MessageBoxType::YesNo, true);
confirm.show();
if (confirm.getResult() == StellarX::MessageBoxResult::Yes) {
// proceed with delete
}
```
- **Modeless example:**
```
Dialog *notify = new Dialog(window, "Notice", "Background task started.", StellarX::MessageBoxType::OK, false);
notify->setResultCallback([](StellarX::MessageBoxResult res) {
// maybe log that user acknowledged
});
notify->show();
```
Here we allocate on heap (since for modeless we need it to persist beyond function scope), set a callback to handle result asynchronously, and show it. The main loop will handle its events. The callback will be invoked on close (OK click).
- Typically, prefer using `MessageBox::showModal` and `showAsync` which handle these patterns for you, including cleanup of the `Dialog` object when done.
### MessageBox Utility Class
**Description:** `StellarX::MessageBox` provides convenient static methods to display standard dialogs (message boxes) without manually managing `Dialog` objects. It mimics the idea of a system message box where you specify a message and get a user response.
- **Static Functions:**
- `static MessageBoxResult showModal(Window& wnd, const std::string& text, const std::string& caption = "提示", MessageBoxType type = MessageBoxType::OK);`
**Description:** Displays a modal message box on window `wnd` with given content. `caption` is the title of the dialog (e.g., "Error", "Confirmation"), `text` is the message to display, and `type` is the combination of buttons to include. This function blocks until the user closes the dialog. It returns a `MessageBoxResult` indicating which button the user clicked (e.g., `MessageBoxResult::OK` or `Cancel`, etc.). The caller can then branch logic based on this result.
- `static void showAsync(Window& wnd, const std::string& text, const std::string& caption = "提示", MessageBoxType type = MessageBoxType::OK, std::function<void(MessageBoxResult)> onResult = nullptr);`
**Description:** Displays a modeless (asynchronous) message box. It returns immediately, leaving the dialog on screen. If `onResult` callback is provided, it will be called once the user closes the dialog, with the `MessageBoxResult` of what they clicked. If a dialog with the same caption **and text** is already open on `wnd`, this function will emit a beep (`\a`) and not open another (to prevent duplicates).
- This duplicate check uses `wnd.hasNonModalDialogWithCaption(caption, text)` internally.
- If no callback is provided, the function simply opens the dialog and returns; you'll have to find another way to know when it's closed (perhaps by checking the `Dialog` via some global or so, but ideally you provide a callback).
- **Implementation Details:**
- `showModal`:
- Creates a `Dialog` (with modal = true) on the stack.
- Calls `dlg.setInitialization(true)` to prepare its layout and capture background before showing (to minimize flicker).
- Calls `dlg.show()`, which will block until the dialog is closed.
- When `show()` returns, it retrieves the result with `dlg.getResult()` and returns that.
- All memory is cleaned up on function exit since `dlg` was on stack.
- `showAsync`:
- First checks for duplicates; if found, prints a bell and returns without doing anything.
- Otherwise, creates a `Dialog` on the heap (with modal = false) using a unique_ptr. It then:
- Calls `dlgPtr->setInitialization(true)` to layout and capture background.
- If an `onResult` callback is provided, calls `dlgPtr->setResultCallback(std::move(onResult))` to register it.
- Uses `wnd.addDialog(std::move(dlg))` to add the dialog to the window's management list (transferring ownership to the window).
- Calls `dlgPtr->show()` to display the dialog modelessly.
- Then returns immediately. The dialog will operate asynchronously; when closed, it triggers `resultCallback` if set.
- **Typical Uses:**
- To display an information message and wait for user acknowledgment:
```
StellarX::MessageBox::showModal(mainWindow, "Operation completed successfully.", "Info", MessageBoxType::OK);
```
This will show an OK dialog and block until user presses OK.
- To ask a yes/no question:
```
auto res = StellarX::MessageBox::showModal(mainWindow, "Delete this file?", "Confirm", MessageBoxType::YesNo);
if (res == MessageBoxResult::Yes) {
// delete the file
}
```
- To show a non-blocking notification:
```
StellarX::MessageBox::showAsync(mainWindow, "The update is downloading in the background.", "Update", MessageBoxType::OK);
// user can continue using app; the dialog will close when they press OK.
```
- To ask something in the background and get result via callback:
```
StellarX::MessageBox::showAsync(mainWindow, "New version available. Download now?", "Update Available", MessageBoxType::YesNo,
[](StellarX::MessageBoxResult res) {
if (res == StellarX::MessageBoxResult::Yes) {
startDownload();
}
});
```
Here, the lambda will execute once the user clicks Yes or No (Yes triggers `startDownload()` in this example).
Using these static methods is recommended because they handle the creation and cleanup of `Dialog` internally, and provide a simple synchronous interface for modal dialogs and an event-driven interface for modeless dialogs.
## Table Class
### Class Overview and Purpose
The **Table** class is an advanced table UI control that supports pagination and displaying large datasets[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/include/StellarX/table.h#L2-L10). It provides a comprehensive data grid functionality, including column headers, data rows, and pagination navigation, similar to a spreadsheet. The Table control automatically computes appropriate column widths and row heights, and it allows customization of border style, fill mode, and text font style to fit various UI design requirements. With its built-in paging mechanism, Table can efficiently handle and present large amounts of data without overwhelming the interface, utilizing background buffering to optimize rendering and prevent flicker during redraws.
**Key Features:**
- **Pagination Support:** Automatically handles pagination by computing total pages based on data size and configurable rows-per-page, and provides **Previous/Next** navigation buttons with a page indicator[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/README.md#L274-L282). This enables users to browse through data that spans multiple pages easily.
- **Customization:** Developers can configure the number of rows per page, toggle the visibility of page navigation buttons, and adjust visual aspects such as border color, background color, fill mode (solid fill or transparent), and line style to match the application's theme.
- **Efficient Rendering:** The Table control uses double-buffering techniques by capturing the background under the table and only redrawing changed areas[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L210-L219)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L241-L249). This minimizes redraw artifacts and improves performance when updating the table content or appearance.
- **Typical Use Cases:** Ideal for displaying tabular data like lists, reports, and records in a GUI application[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/include/StellarX/table.h#L14-L17). For example, it can be used to show database query results, log entries, or statistical reports in a scrollable, paginated view, allowing users to easily read and navigate the information.
### Public Member Functions
#### Table(int x, int y)
**Prototype:** `Table(int x, int y)`
**Parameters:**
- `x` The X coordinate for the tables top-left corner position.
- `y` The Y coordinate for the tables top-left corner position.
**Return Value:** None (constructor).
**Description:** Constructs a new Table control at the specified position on the screen[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L164-L171). Initially, the table has no headers or data; its width and height are set to 0 and will be calculated once data is provided and headers are set. The `x` and `y` coordinates position the table within the parent window or container. Internally, Table inherits from the base `Control` class, so it is created with an initial area that will expand based on content.
**Usage Scenario:** Use this constructor when you need to create a table in your applications UI. For example, after creating a main window, you might call `auto table = std::make_unique<Table>(50, 50);` to place a Table at coordinates (50,50). You would then set up its headers and data before rendering it. The Table can be added to a Window or Canvas as a sub-control for display.
#### ~Table()
**Prototype:** `~Table()`
**Parameters:** None.
**Return Value:** None.
**Description:** Destructor for the Table class. It is called automatically when a Table object is destroyed, and it ensures that all internally allocated resources are freed. This includes deleting any internal controls such as the pagination buttons, page number label, and the background image buffer that were created during the Tables operation[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L169-L178). By cleaning up these resources, the destructor prevents memory leaks.
**Usage Scenario:** Generally, you do not need to call this explicitly; it will be invoked when the Table goes out of scope or the program terminates. Understanding the destructors behavior is useful for developers to ensure that if a Table is dynamically allocated (e.g., via `new` or `make_unique`), it should be deleted or allowed to go out of scope to trigger cleanup of internal components.
#### draw()
**Prototype:** `void draw() override`
**Parameters:** None.
**Return Value:** None.
**Description:** Overrides the `Control::draw()` method to render the table onto the screen[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L185-L194)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L252-L260). When `draw()` is called, the Table will draw its column headers, the data rows for the current page, the page number label, and the navigation buttons (if they are enabled). The drawing routine uses the current text style, border color, background color, fill mode, and line style settings that have been configured. The Table employs a “dirty flag” mechanism to optimize rendering: it will only redraw its content if something has changed (such as data or style) that marks it as needing an update[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L187-L196)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L258-L266). If `dirty` is false, calling `draw()` has no effect. Internally, the function also takes care of restoring any saved background and drawing with minimal flicker.
**Usage Scenario:** In most cases, you do not need to call `table.draw()` manually, as the frameworks window or event loop will handle drawing. Typically, after adding the Table to a Window and calling the windows `draw()`, the Table will be drawn. However, if you update the Tables content or appearance at runtime (for example, in response to an event) and want to refresh immediately, you may call `table.draw()` to force a redraw. This will ensure the tables display is updated with any new data or style changes.
#### handleEvent(const ExMessage& msg)
**Prototype:** `bool handleEvent(const ExMessage& msg) override`
**Parameters:**
- `msg` An event message (`ExMessage` from EasyX library) containing details of a user input event (mouse click, key press, etc.).
**Return Value:** Boolean, indicating whether the event was handled by the Table. Returns `true` if the event was consumed (handled) by this Table control, or `false` if the Table did not handle the event.
**Description:** Overrides the `Control::handleEvent()` method to process user input events relevant to the Table. For the Table control, `handleEvent` primarily deals with events on the pagination buttons. If page navigation buttons are enabled (`isShowPageButton` is true), this function will forward the incoming event to the internal "Previous Page" and "Next Page" Button controls for handling[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L274-L283). For example, if the user clicks one of these buttons, the respective internal Button will capture the event, update the Tables `currentPage` (decrement or increment it), and trigger a redraw of the table to show the new page. In such a case, `handleEvent` returns `true` to indicate the event was handled (and no further processing is needed). If the page buttons are hidden (`isShowPageButton == false`), the Table will ignore all events and always return `false`[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L276-L283), as there is no interactive element for the user to manipulate directly in the Table.
**Usage Scenario:** In normal usage, you do not call this method directly. The frameworks main loop will call `handleEvent` on each control (including Table) to propagate user inputs. However, understanding this function is useful if you plan to extend the Table or integrate it into a custom event handling flow. For instance, when integrating Table into a Window, the Windows `runEventLoop()` will pass events to `table.handleEvent(msg)` automatically. If you have disabled the default page buttons, you might implement a custom paging control elsewhere; in that case, Tables `handleEvent` would not consume events, and you could handle them in your custom controls.
#### setHeaders(std::initializer_liststd::string headers)
**Prototype:** `void setHeaders(std::initializer_list<std::string> headers)`
**Parameters:**
- `headers` An initializer list of strings, each representing a column header title.
**Return Value:** None.
**Description:** Defines the column headers of the table. This function clears any existing headers and sets up new headers as provided by the list[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L288-L296). Each string in the list becomes the title of a column in the table header. After calling this, the Table marks itself as needing to recalculate cell dimensions and to redraw the headers[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L291-L295) on the next draw. The number of headers determines the number of columns in the table; any subsequent data rows added should match this column count.
**Usage Scenario:** Call this after creating a Table and before adding data. For example: `table.setHeaders({"Name", "Age", "Occupation"});` will configure the table to have three columns titled "Name", "Age", and "Occupation". Defining headers is typically the first step in preparing the Table for use. Once headers are set, you can proceed to populate data rows with `setData`. Changing headers after data has been added is possible but not recommended, as the existing data might not align with the new columns (if the counts differ); if you must do so, you should reset or adjust the data accordingly.
#### setData(std::vectorstd::string data) / setData(std::initializer_list<std::vectorstd::string> data)
**Prototype 1:** `void setData(std::vector<std::string> data)`
**Parameters:**
- `data` A vector of strings representing a single row of data, where each element corresponds to a cell under the respective column.
**Return Value:** None.
**Description:** Adds one row of data to the table. If the number of elements in the provided vector is less than the number of headers (columns), the function will append empty strings to the vector until it matches the header count[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L300-L308). This ensures that every row has a value (or placeholder) for each column. The row is then appended to the Tables internal data storage, and the total page count (`totalPages`) is recalculated based on the new total number of rows and the current `rowsPerPage` setting[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L302-L308). The calculation rounds up to ensure at least one page[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L303-L308). After adding the row, the Table is marked as needing a layout recompute and redraw.
**Usage Scenario:** Use this function to build the table row by row. For example, in a loop reading records from a file or database, you might call `table.setData({val1, val2, val3});` for each record to append it as a new row. This is especially useful for dynamically updating the table with new entries (e.g., real-time log updates). Each call adds the row at the end of the data and the Tables display will include it on the appropriate page (immediately if it falls on the current page and you redraw, or after navigation).
**Prototype 2:** `void setData(std::initializer_list<std::vector<std::string>> data)`
**Parameters:**
- `data` An initializer list of rows, where each row is represented by a `std::vector<std::string>` (as described above).
**Return Value:** None.
**Description:** Adds multiple rows of data to the table in one call. For each row provided in the initializer list, the function checks if its number of elements matches the number of table headers; if a row is shorter, empty strings will be appended until its length equals the header count[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L313-L321). Each row (adjusted as needed) is then appended to the tables data. After inserting all the rows, the total page count is recalculated accordingly, ensuring it is at least 1[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L322-L328). Like the single-row version, this overload marks the table as needing re-layout and redraw. Note that this function does **not** clear existing data; new rows are appended to any data already present.
**Usage Scenario:** This is convenient for initializing the table with a batch of data. For example, if you already have a collection of rows ready (perhaps from a data structure or file), you can call `table.setData({row1, row2, row3});` to add them all at once instead of calling the single-row version repeatedly. It simplifies populating the table with initial data. If you intend to replace all existing data, you may want to clear the old data (by resetting the Table or using the returned data vector) before calling this overload. Use this when you have a static set of data to display, especially during initialization or when loading a new dataset into the table.
#### setRowsPerPage(int rows)
**Prototype:** `void setRowsPerPage(int rows)`
**Parameters:**
- `rows` The number of data rows to display per page.
**Return Value:** None.
**Description:** Sets the pagination size of the table by specifying how many rows are shown on each page. This function updates the internal `rowsPerPage` setting and recalculates the total number of pages (`totalPages`) based on the current total number of data rows[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L332-L338). If the recalculation results in less than 1 page, it defaults to 1 to maintain a valid state. Changing this value will affect the layout (particularly the vertical size of the table and the distribution of data across pages), so the Table marks itself for resizing of cells and redraw. The current page (`currentPage`) is not explicitly adjusted in this function, so if the new `rowsPerPage` significantly reduces the total pages, the table may internally handle out-of-range page indices on the next draw by capping the current page to the last page.
**Usage Scenario:** Use this to adjust how dense the table appears in terms of data per page. For instance, if a table initially shows 5 rows per page by default but you want to show more data on a larger screen, calling `table.setRowsPerPage(10);` will double the number of rows shown at once, reducing the total page count roughly by half. This is useful for user preferences (show more or fewer items per page) or responsive design considerations. It can be called before or after adding data. If called after data is loaded and the current page is beyond the new total pages, the next redraw will naturally show the last page of data.
#### showPageButton(bool isShow)
**Prototype:** `void showPageButton(bool isShow)`
**Parameters:**
- `isShow` A boolean flag indicating whether the page navigation buttons (Previous/Next) should be displayed. `true` to show the buttons; `false` to hide them.
**Return Value:** None.
**Description:** Toggles the visibility of the pagination control buttons of the table. When set to `false`, the “Previous Page” and “Next Page” buttons will not be drawn, and the user will not have a UI control to navigate between pages (the table will effectively remain on whatever the current page is, unless changed programmatically). When set to `true`, the buttons are visible, allowing the user to click them to change pages. Hiding the buttons does not alter the data or current page internally; it purely affects the UI and event handling (when hidden, `handleEvent` will ignore navigation clicks as described above). Any call to this function will mark the table as dirty so that on the next draw the buttons will be shown/hidden accordingly.
**Usage Scenario:** Useful when the dataset fits on one page or when you want to control pagination through a different means. For example, if your table only has a few rows that all fit on a single page, you might call `table.showPageButton(false);` to simplify the interface by removing unnecessary controls. Conversely, if you later load more data that requires paging, you can call `showPageButton(true)` to reveal the navigation. It can also be toggled in response to user actions (like a "show all data" toggle that disables manual paging).
#### setTableBorder(COLORREF color)
**Prototype:** `void setTableBorder(COLORREF color)`
**Parameters:**
- `color` A COLORREF value specifying the new color for the tables border lines (grid lines). This is typically created by the `RGB(r,g,b)` macro on Windows (e.g., `RGB(0,0,0)` for black).
**Return Value:** None.
**Description:** Sets the color used to draw the tables cell borders and outline. After calling this, the Table updates its internal border color and marks itself for redraw. The next time the table is drawn, all grid lines and borders will be rendered in the specified color[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L192-L199). By default, the tables border color is black. Changing the border color can be useful to match the overall UI theme or to highlight the table. This affects the lines around each cell and the table perimeter.
**Usage Scenario:** Use this to customize the appearance of the tables grid. For example, in a dark-themed application you might use a lighter border color for contrast, or in a light theme, a subtle gray to avoid a heavy look: `table.setTableBorder(RGB(200, 200, 200));`. This is purely a cosmetic setting and can be changed anytime (even at runtime) to alter the tables look. Remember to refresh the display (via `draw()` or window redraw) to see the effect immediately.
#### setTableBk(COLORREF color)
**Prototype:** `void setTableBk(COLORREF color)`
**Parameters:**
- `color` A COLORREF specifying the background fill color for the tables cells.
**Return Value:** None.
**Description:** Sets the background color used to fill each cell of the table. This updates the internal background color property and marks the Table as needing redraw. During the next `draw()`, each cells background will be filled with the given color (provided the fill mode is solid). By default, table cell backgrounds are white. Changing this can provide alternating row colors or highlight the table area against the window background. If the fill mode is set to Null (transparent), this color will not be visible since cells wont be filled with a solid color.
**Usage Scenario:** To change the background color of the table for styling or readability. For instance, `table.setTableBk(RGB(240, 240, 240));` would set a light gray background for all cells, which can reduce glare compared to bright white. This can be paired with custom text colors for better contrast. If you want to implement alternating row colors (like “zebra striping”), the current Table API does not support per-row color via this function directly you would need to draw custom backgrounds or modify the data drawing logic.
#### setTableFillMode(StellarX::FillMode mode)
**Prototype:** `void setTableFillMode(StellarX::FillMode mode)`
**Parameters:**
- `mode` A `StellarX::FillMode` value indicating the fill pattern mode for the cell backgrounds. Common values are `FillMode::Solid` (solid color fill) or `FillMode::Null` (no fill/transparent background). Other modes like `Hatched`, `Pattern`, etc., are also defined but may fall back to Solid if not supported.
**Return Value:** None.
**Description:** Configures how the tables cell background is filled when drawn. In practice, this toggles between solid fill and no fill for the Table (the current implementation treats any mode other than Solid or Null as Solid)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L360-L369). Setting `FillMode::Solid` means cells will be drawn with the current background color (`tableBk`), whereas `FillMode::Null` means cell backgrounds will not be drawn (making them transparent, showing whatever was behind the table). This function also propagates the fill mode and text style to the internal navigation buttons and page number label to ensure visual consistency[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L365-L373). For example, if you choose Null mode (transparent), the page number labels background will be made transparent as well. Any change here marks the Table (and its sub-controls) for redraw.
**Usage Scenario:** Use this when you need to adjust whether the tables background is visible. A typical use is to set `table.setTableFillMode(StellarX::FillMode::Null);` if you want the table to overlay on a custom background (so only text and borders render, letting the underlying image or color show through). Otherwise, keep it as Solid to use a uniform background color for cells. The default is Solid fill. Be aware that patterns or hatched fills are not fully supported in this version of the framework; even if set, they will revert to a solid fill for simplicity[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L360-L368).
#### setTableLineStyle(StellarX::LineStyle style)
**Prototype:** `void setTableLineStyle(StellarX::LineStyle style)`
**Parameters:**
- `style` A `StellarX::LineStyle` value indicating the style of the grid lines (e.g., `LineStyle::Solid` for solid lines, `LineStyle::Dash` for dashed lines, `LineStyle::Dot` for dotted lines, etc.).
**Return Value:** None.
**Description:** Sets the style used for drawing the tables border and grid lines. Changing this will cause the table to use the specified line style (if supported by the underlying graphics library) for all cell borders and the outline on the next redraw. Examples include dashed or dotted lines for a lighter look compared to solid lines. After calling this, the line style property is updated and the table is marked dirty for redrawing. Note that the actual rendering of different line styles depends on the graphics library capabilities.
**Usage Scenario:** This is a visual customization for the tables grid. For instance, calling `table.setTableLineStyle(StellarX::LineStyle::Dash);` will make the table draw its cell borders with dashed lines, which might be desirable in a print preview or a specialized UI theme. Use it to differentiate sections or to achieve a stylistic effect. Combine it with appropriate border color and width for best results. If a particular line style is not obvious on screen, ensure that line width is 1 and the color contrasts with the background.
#### setTableBorderWidth(int width)
**Prototype:** `void setTableBorderWidth(int width)`
**Parameters:**
- `width` The thickness of the border lines in pixels. Must be a positive integer.
**Return Value:** None.
**Description:** Sets the width (thickness) of the lines used for the tables cell borders and outline. By default, grid lines are 1 pixel thick. Increasing this value will make the tables grid lines thicker/bolder, while a value of 1 keeps them thin. When this property is changed, the table marks itself for redraw and will use the new line width for drawing borders on the next `draw()`. If a thicker border is set (greater than 1), the Tables drawing logic takes into account the larger stroke when restoring the background to avoid artifacts (adjusting the area it refreshes)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L220-L228)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L240-L248).
**Usage Scenario:** Use a larger border width when you want the table grid to stand out more, such as in a presentation or when displaying on a high-DPI screen where a 1-pixel line may appear too thin. For example, `table.setTableBorderWidth(2);` will draw grid lines at 2 pixels thickness, making the table look more pronounced. Be cautious with very thick lines as they can clutter the appearance and might overlap cell content if too large relative to cell size. Always test the appearance after changing this setting.
#### Accessor Methods (Getters)
The following methods allow you to query the Tables current state or configuration:
- **int getCurrentPage() const** Returns the current page index (1-based) that the table is displaying[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L390-L398). For example, if the table is showing the first page of data, this returns 1; if the user has navigated to the second page, it returns 2, and so on. This can be used to display or log the current page number.
- **int getTotalPages() const** Returns the total number of pages of data in the table given the current dataset and rows-per-page setting[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L394-L402). This is calculated as ceil(total_rows / rowsPerPage). Its useful for informing the user (e.g., “Page 2 of 5”) or for logic that might depend on whether more pages are available.
- **int getRowsPerPage() const** Returns the number of data rows the table is set to display on each page[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L400-L408). This reflects the value last set by `setRowsPerPage`. It can be used to verify the current pagination setting or to adjust external controls accordingly.
- **bool getShowPageButton() const** Returns the current setting of whether the pagination buttons are visible[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L404-L412) (`true` if they are shown, `false` if hidden). This could be checked to decide if manual navigation UI should be enabled or if an alternate navigation method is needed.
- **COLORREF getTableBorder() const** Returns the current color (COLORREF) used for the tables border/grid lines[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L410-L418). For instance, `RGB(0,0,0)` for black. This allows you to retrieve the color if you need to apply the same color elsewhere or for saving the configuration.
- **COLORREF getTableBk() const** Returns the current background fill color (COLORREF) of the tables cells[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L414-L422).
- **StellarX::FillMode getTableFillMode() const** Returns the current fill mode for the tables cells[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L420-L428), as a `StellarX::FillMode` enum (e.g., Solid or Null). This lets you check whether the table is drawing solid backgrounds or is in transparent mode.
- **StellarX::LineStyle getTableLineStyle() const** Returns the current line style used for the tables grid lines[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L424-L432) (as a `StellarX::LineStyle` enum). You can use this to inspect the style (solid, dashed, etc.) programmatically.
- **std::vectorstd::string getHeaders() const** Returns a copy of the current list of header strings[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L430-L438). The returned vector contains each columns header text in order. This can be used to retrieve the headers for display elsewhere or for serialization.
- **std::vector<std::vectorstd::string> getData() const** Returns a copy of all the tables data[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L434-L442). The returned value is a 2D vector of strings, where each inner vector represents one row of the table (with elements corresponding to each columns value in that row). This is useful for exporting the table data or performing computations on the data outside of the table (like searching or sorting, which would typically be done on the data and then reapplied to the table).
- **int getTableBorderWidth() const** Returns the current width (in pixels) of the tables border lines[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/src/table.cpp#L440-L446).
All these getter functions are `const` and do not modify the Tables state. They are intended for retrieving the tables configuration and content at any moment. For example, after populating the table, you might call `getTotalPages()` to update a UI element that shows the total page count, or use `getData()` to verify the data loaded into the table.
### Public Member Variables
- **StellarX::ControlText textStyle** A struct that defines the font and style used for text rendered in the table (including header text, cell content, and the page number label). By modifying this public member, you can customize the appearance of the tables text. For instance, you can set `textStyle.nHeight` to change the font height (size), `textStyle.lpszFace` to change the font family name, `textStyle.color` to change the text color, and other attributes such as `nWeight` for boldness, `bItalic` for italics, `bUnderline` for underline, etc.[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/include/StellarX/CoreTypes.h#L134-L142). The Table uses these settings when drawing text via EasyX functions. The default `ControlText` values are initialized to a standard font (e.g., a default Chinese font "微软雅黑" as in the code, or "SimSun") at a default size and black color. Developers can adjust `textStyle` any time before calling `draw()` to apply new text styles. Notably, the Tables internal prev/next Buttons and page number Label also use the Tables `textStyle` to ensure consistency in typography across the entire control. For example, to make the table text larger and blue:
```
table.textStyle.nHeight = 20; // set font height
table.textStyle.color = RGB(0, 0, 128); // set text color to navy blue
```
After adjusting, calling `table.draw()` (or a window redraw) will render the table with the new font settings.
### Example
Below is an example demonstrating how to create and use a Table control in a StellarX application[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/README.en.md#L275-L283)[GitHub](https://github.com/Ysm-04/StellarX/blob/95149238e221b19a15bfa750bcf937620e23fd47/README.en.md#L284-L290):
```c++
// Create a table control at position (50, 50)
auto myTable = std::make_unique<Table>(50, 50);
// Set column headers
myTable->setHeaders({ "ID", "Name", "Age", "Occupation" });
// Add data rows
myTable->setData({ "1", "Zhang San", "25", "Engineer" });
myTable->setData({ "2", "Li Si", "30", "Designer" });
myTable->setData({ "3", "Wang Wu", "28", "Product Manager" });
// Set to display 2 rows per page
myTable->setRowsPerPage(2);
// Customize text style and table appearance
myTable->textStyle.nHeight = 16; // set font height
myTable->setTableBorder(RGB(50, 50, 50)); // set border color (dark gray)
myTable->setTableBk(RGB(240, 240, 240)); // set background color (light gray)
// Add the table to the main window (assume mainWindow is a Window instance)
mainWindow.addControl(std::move(myTable));
```
In this code, we create a `Table` at coordinates (50, 50) on the window. We then define four column headers: "ID", "Name", "Age", "Occupation". Next, three rows of sample data are added to the table. The `setRowsPerPage(2)` call configures the table to show only 2 rows per page; since we added 3 rows of data, the table will have 2 pages (the first page showing the first 2 rows, and the second page showing the remaining 1 row). We then adjust the tables appearance by setting the font size (making text slightly larger), changing the border color to a dark gray, and the background color of cells to a light gray for better contrast. Finally, we add the table to a `mainWindow` (which is an instance of `Window`) so that it becomes part of the GUI. Once added, the windows drawing routine will render the table, and the event loop will ensure the tables pagination buttons respond to user clicks. Typically, after adding controls to a window, you would call `mainWindow.draw()` to render the initial UI, and then `mainWindow.runEventLoop()` to start processing events, allowing interactions like clicking the "Next" button to flip pages. This example illustrates the basic steps to integrate the Table control into an application and configure its data and appearance.
The above documentation covers all key classes in the StellarX framework. With these, developers can construct fully-featured GUI applications with windows, interactive buttons, text fields, and pop-up dialogs. Keep in mind StellarX is built on EasyX and thus inherits its platform limitation (Windows only) and certain modal behaviors (like `InputBox` use). It is ideal for educational purposes and lightweight tools requiring a simple GUI.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 我在人间做废物
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-194
View File
@@ -1,194 +0,0 @@
#include "Label.h"
namespace
{
// Label 的内容尺寸刷新不依赖 EasyX 当前绘图上下文,
// 直接使用 GDI 依据“文本 + 字体样式”测量宽高。
// 这样无论是文本变化、字体样式变化,还是控件在窗口正式绘制前进入 dirty,
// 都可以先把运行态尺寸收口好,而不必等到 draw() 再决定几何。
void MeasureLabelText(const std::string& text, const StellarX::ControlText& style, int& width, int& height)
{
width = 0;
height = 0;
HDC hdc = CreateCompatibleDC(nullptr);
if (!hdc)
return;
LOGFONTA fontDesc{};
fontDesc.lfHeight = style.nHeight;
fontDesc.lfWidth = style.nWidth;
fontDesc.lfEscapement = style.nEscapement;
fontDesc.lfOrientation = style.nOrientation;
fontDesc.lfWeight = style.nWeight;
fontDesc.lfItalic = style.bItalic ? TRUE : FALSE;
fontDesc.lfUnderline = style.bUnderline ? TRUE : FALSE;
fontDesc.lfStrikeOut = style.bStrikeOut ? TRUE : FALSE;
fontDesc.lfCharSet = DEFAULT_CHARSET;
fontDesc.lfOutPrecision = OUT_DEFAULT_PRECIS;
fontDesc.lfClipPrecision = CLIP_DEFAULT_PRECIS;
fontDesc.lfQuality = DEFAULT_QUALITY;
fontDesc.lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE;
lstrcpynA(fontDesc.lfFaceName, style.lpszFace ? style.lpszFace : "微软雅黑", LF_FACESIZE);
HFONT font = CreateFontIndirectA(&fontDesc);
HFONT oldFont = nullptr;
if (font)
oldFont = (HFONT)SelectObject(hdc, font);
if (text.empty())
{
TEXTMETRICA tm{};
if (GetTextMetricsA(hdc, &tm))
height = tm.tmHeight;
}
else
{
SIZE size{};
if (GetTextExtentPoint32A(hdc, text.c_str(), (int)text.size(), &size))
{
width = size.cx;
height = size.cy;
}
if (height == 0)
{
TEXTMETRICA tm{};
if (GetTextMetricsA(hdc, &tm))
height = tm.tmHeight;
}
}
if (font)
{
if (oldFont)
SelectObject(hdc, oldFont);
DeleteObject(font);
}
DeleteDC(hdc);
}
}
Label::Label()
:Control(0, 0, 0, 0)
{
this->id = "Label";
// Label 当前阶段明确走“内容驱动尺寸”语义:
// 外部布局器只安置位置,不通过 Stretch 改写它的宽高。
this->layoutCapability.allowStretchX = false;
this->layoutCapability.allowStretchY = false;
this->text = "默认标签";
textStyle.color = RGB(0, 0, 0);
textBkColor = RGB(255, 255, 255);; //默认白色背景
}
Label::Label(int x, int y, std::string text, COLORREF textcolor, COLORREF bkColor)
:Control(x, y, 0, 0)
{
this->id = "Label";
this->layoutCapability.allowStretchX = false;
this->layoutCapability.allowStretchY = false;
this->text = text;
textStyle.color = textcolor;
textBkColor = bkColor; //默认白色背景
}
void Label::refreshContentDrivenRuntimeGeometry()
{
// Label 的尺寸来源于“当前文本 + 当前字体样式”。
// 这里只更新运行态 width/height,不自动回写 localWidth/localHeight
// 避免内容变化把设计基线偷偷带偏。
if (contentMeasureValid &&
lastMeasuredText == text &&
!(lastMeasuredStyle != textStyle))
{
return;
}
int newWidth = 0;
int newHeight = 0;
MeasureLabelText(text, textStyle, newWidth, newHeight);
const bool sizeChanged = (newWidth != this->width) || (newHeight != this->height);
if (sizeChanged && hasSnap)
{
// 运行态尺寸变化时,先恢复旧背景,避免文本缩短后旧像素残留在屏幕上。
discardBackground();
}
this->width = newWidth;
this->height = newHeight;
lastMeasuredText = text;
lastMeasuredStyle = textStyle;
contentMeasureValid = true;
dirty = true;
}
void Label::draw()
{
if (dirty && show)
{
saveStyle();
if (textBkDisap)
setbkmode(TRANSPARENT); //设置背景透明
else
{
setbkmode(OPAQUE); //设置背景不透明
setbkcolor(textBkColor); //设置背景颜色
}
settextcolor(textStyle.color);
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut); //设置字体样式
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage)
saveBackground(this->x, this->y, this->width, this->height);
// 恢复背景(清除旧内容)
restBackground();
outtextxy(x, y, LPCTSTR(text.c_str()));
restoreStyle();
dirty = false;
}
}
void Label::hide()
{
discardBackground(); // 还原并释放快照
dirty = false;
}
void Label::setDirty(bool dirty)
{
if (dirty)
{
// 只要 Label 进入脏状态,就在绘制前把内容驱动尺寸同步到运行态。
// 这样文本变化和字体样式变化都会在 draw() 之前完成几何刷新。
refreshContentDrivenRuntimeGeometry();
}
this->dirty = dirty;
}
void Label::setTextdisap(bool key)
{
textBkDisap = key;
this->dirty = true;
}
void Label::setTextBkColor(COLORREF color)
{
textBkColor = color;
this->dirty = true;
}
void Label::setText(std::string text)
{
this->text = text;
refreshContentDrivenRuntimeGeometry();
}
void Label::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
{
// 通用布局器给出的 rect.width/rect.height 只代表“外部几何解算结果”。
// Label 当前阶段的语义是内容驱动尺寸,因此这里只接位置,不接外部宽高,
// 宽高继续由 refreshContentDrivenRuntimeGeometry() 按文本和字体样式刷新。
refreshContentDrivenRuntimeGeometry();
applyRuntimeRectDirect(rect.worldX, rect.worldY, this->width, this->height);
}
-70
View File
@@ -1,70 +0,0 @@
/*******************************************************************************
* @类: Label
* @摘要: 简单文本标签控件,用于显示静态文本
* @描述:
* 提供基本的文本显示功能,支持透明背景和自定义样式。
* 不支持用户交互,专注于文本呈现。
*
* @特性:
* - 支持背景透明/不透明模式
* - 完整的文本样式控制(字体、颜色、效果)
* - 自动适应文本内容
* - 轻量级无事件处理开销
*
* @使用场景: 显示说明文字、标题、状态信息等静态内容
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
#pragma once
#include "Control.h"
class Label : public Control
{
std::string text; //标签文本
COLORREF textBkColor; //标签背景颜色
bool textBkDisap = false; //标签背景是否透明
std::string lastMeasuredText; // 最近一次参与尺寸测量的文本
StellarX::ControlText lastMeasuredStyle; // 最近一次参与尺寸测量的字体样式
bool contentMeasureValid = false; // 当前内容尺寸缓存是否有效
//标签事件处理(标签无事件)不实现具体代码
bool handleEvent(const ExMessage& msg) override { return false; }
//用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; };
// 根据当前文本和字体样式刷新运行态尺寸。
// 这是 Label 的内容驱动几何入口:
// - 覆盖文本变化
// - 覆盖字体样式变化
// - 只更新运行态宽高,不自动提交设计基线 local*
void refreshContentDrivenRuntimeGeometry();
public:
// 标签字体样式保持公开,便于用户直接改字段。
// 但直接修改 textStyle 后,必须显式调用 setDirty(true)
// 才会触发内容驱动尺寸刷新并让新的运行态宽高生效。
StellarX::ControlText textStyle; //标签文本样式
public:
// 默认构造:创建一个内容驱动尺寸的标签
Label();
// 指定位置、文本和基础样式构造标签
Label(int x, int y, std::string text = "标签", COLORREF textcolor = BLACK, COLORREF bkColor = RGB(255, 255, 255));
// 绘制标签文本
void draw() override;
//用于“隐藏提示框”时调用(还原并释放快照)
void hide();
// Label 的内容尺寸由文本和字体样式共同决定。
// 因此只要进入 dirty 状态,就在绘制前先刷新一次运行态尺寸,
// 避免 draw() 阶段继续承担新的几何决策。
void setDirty(bool dirty) override;
// 设置标签背景是否透明
void setTextdisap(bool key);
// 设置标签背景颜色
void setTextBkColor(COLORREF color);
// 设置标签文本,并触发内容驱动尺寸刷新
void setText(std::string text);
protected:
// 统一布局器只负责安置 Label 的运行态位置;
// 宽高仍由 Label 自己的内容驱动路径决定。
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
};
+451
View File
@@ -0,0 +1,451 @@
# StellarX GUI Framework
[中文文档](README.md)
------
![GitHub all releases](https://img.shields.io/github/downloads/Ysm-04/StellarX/total)
[![Star GitHub Repo](https://img.shields.io/github/stars/Ysm-04/StellarX.svg?style=social&label=Star%20This%20Repo)](https://github.com/Ysm-04/StellarX)
![Version](https://img.shields.io/badge/Version-2.0.0-brightgreen.svg)
![Download](https://img.shields.io/badge/Download-2.0.0_Release-blue.svg)
![C++](https://img.shields.io/badge/C++-17+-00599C?logo=cplusplus&logoColor=white)
![Windows](https://img.shields.io/badge/Platform-Windows-0078D6?logo=windows)
![EasyX](https://img.shields.io/badge/Based_on-EasyX-00A0EA)
![License](https://img.shields.io/badge/License-MIT-blue.svg)
![Architecture](https://img.shields.io/badge/Architecture-Modular%20OOP-brightgreen)
![CMake](https://img.shields.io/badge/Build-CMake-064F8C?logo=cmake)
> **"Bound by Stars, Light as Dust"** — A native C++ GUI framework for Windows platform, featuring extreme lightweight and high modularity.
`StellarX` was born from resistance against "overly bloated" modern GUI frameworks. It rejects dependencies that often reach hundreds of MB, long compilation times, and steep learning curves, choosing to return to the essence: solving core desktop application development needs with the most concise code, clearest architecture, and highest efficiency.
It is a **pure teaching-level, tool-level framework** designed to help developers deeply understand GUI principles and quickly build lightweight Windows tools.
---
## 📦 Project Structure & Design Philosophy
StellarX framework adopts classic **Object-Oriented** and **modular** design with a clear and standardized project structure:
```markdown
StellarX/
├── include/ # Header files directory
│ └── StellarX/ # Framework header files
│ ├── StellarX.h # Main include header - one-click import of entire framework
│ ├── CoreTypes.h # ★ Core ★ - Single source of truth for all enums and structs
│ ├── Control.h # Abstract base class - defines unified interface for all controls
│ ├── Button.h # Button control
│ ├── Window.h # Window management
│ ├── Label.h # Label control
│ ├── TextBox.h # Text box control
│ ├── Canvas.h # Canvas container
│ ├── Dialog.h # Dialog control (new in v2.0.0)
│ ├── MessageBox.h # Message box factory (new in v2.0.0)
│ └── Table.h # Table control
├── src/ # Source files directory
│ ├── Control.cpp
│ ├── Button.cpp
│ ├── Window.cpp
│ ├── Label.cpp
│ ├── TextBox.cpp
│ ├── Canvas.cpp
│ ├── Table.cpp
│ ├── Dialog.cpp # v2.0.0 new
│ └── MessageBox.cpp # v2.0.0 new
├── examples/ # Example code directory
│ └── demo.cpp # Basic demonstration
├── docs/ # Documentation directory
│ └── CODE_OF_CONDUCT.md # Code of Conduct
├── CMakeLists.txt # CMake build configuration
├── CONTRIBUTING.md # Contribution guide
├── CHANGELOG.md # Changelog
├── Doxyfile # Doxygen configuration
├── LICENSE # MIT License
└── README.md # Project description
```
### **Design Philosophy:**
1. **Single Responsibility Principle (SRP)**: Each class/file is responsible for one thing only.
2. **Dependency Inversion Principle (DIP)**: High-level modules (like `Window`) don't depend on low-level modules (like `Button`), both depend on abstractions (`Control`).
3. **Open/Closed Principle (OCP)**: New controls can be easily extended by inheriting the `Control` base class without modifying existing code.
4. **Consistency**: All controls share unified `draw()` and `handleEvent()` interfaces.
## 🚀 Core Features
- **Extreme Lightweight**: Core library compiles to only ~12MB, with zero external dependencies. Generated applications are compact.
- **Clear Modular Architecture**: Uses `CoreTypes.h` to uniformly manage all types, eliminating duplicate definitions and greatly improving maintainability.
- **Native C++ Performance**: Built directly on EasyX and Win32 API, providing near-native execution efficiency with very low memory footprint (typically <10MB).
- **Complete Control System**: Button, Label, TextBox, Canvas, Table, Dialog, and MessageBox factory.
- **Highly Customizable**: From control colors, shapes (rectangle, rounded, circle, ellipse) to fill modes and font styles, all have detailed enum support for easy customization.
- **Simple & Intuitive API**: Uses classic object-oriented design, code as documentation, low learning curve.
- **Standard Project Structure**: Adopts standard include/src separation structure, supports CMake build, easy to integrate and use.
- **Enhanced Event System**: v2.0.0 introduces event consumption mechanism, all `handleEvent` methods return `bool` indicating whether event was consumed, supporting finer-grained event propagation control.
- **Dialog System**: New complete dialog support, including modal and non-modal dialogs, automatically handling background saving and restoration.
## ⚡ Quick Start (5 Minutes to Get Started)
> **🎯 Latest Version Download**
> Download pre-compiled library files and header files from [GitHub Releases](https://github.com/Ysm-04/StellarX/releases/latest) for quick integration into your project.
### Environment Requirements
- **OS**: Windows 10 or higher
- **Compiler**: C++17 supported compiler (e.g., **Visual Studio 2019+**)
- **Graphics Library**: [EasyX](https://easyx.cn/) (2022 version or higher, please select the version matching your compiler during installation)
- **Build Tool**: CMake 3.12+ (optional, recommended)
### Install EasyX
1. Visit [EasyX official website](https://easyx.cn/) to download the latest version
2. Run the installer, select the version matching your Visual Studio version
3. After installation, no additional configuration is needed, StellarX framework will automatically link EasyX
### Method 1: Using CMake Build (Recommended)
1. **Clone the project**:
```bash
git clone https://github.com/Ysm-04/StellarX.git
cd StellarX
```
2. **Generate build system**:
```bash
mkdir build
cd build
cmake ..
```
3. **Compile the project**:
```bash
cmake --build .
```
4. **Run the example**:
```bash
./examples/Demo
```
### Method 2: Manual Integration into Existing Project
1. **Copy the include and src directories** to your project
2. **Configure include paths** to ensure the compiler can find the `include/StellarX/` directory
3. **Add all .cpp files** to your project for compilation
### Create Your First StellarX Application
```cpp
// Just include this one header to use all features
#include "StellarX.h"
// Program entry point (use WinMain for better compatibility)
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
{
// 1. Create a 640x480 window with white background, titled "My App"
Window mainWindow(640, 480, 0, RGB(255, 255, 255), "My First StellarX App");
// 2. Create a button (managed with smart pointer)
auto myButton = std::make_unique<Button>(
250, 200, 140, 40, // x, y, width, height
"Click Me", // button text
StellarX::ButtonMode::NORMAL,
StellarX::ControlShape::ROUND_RECTANGLE
);
// 3. Set click event for the button (using Lambda expression)
myButton->setOnClickListener([&mainWindow]() {
// Use message box factory to create modal dialog
auto result = StellarX::MessageBox::ShowModal(
mainWindow,
"Welcome to StellarX GUI\r\nAuthor: Ysm-04",
"Greeting",
StellarX::MessageBoxType::OKCancel
);
// Handle dialog result
if (result == StellarX::MessageBoxResult::OK) {
// User clicked OK button
}
});
// 4. (Optional) Set button style
myButton->textStyle.nHeight = 20;
myButton->textStyle.color = RGB(0, 0, 128); // Dark blue text
myButton->setButtonBorder(RGB(0, 128, 255)); // Blue border
// 5. Add button to window
mainWindow.addControl(std::move(myButton));
// 6. Draw the window
mainWindow.draw();
// 7. Enter message loop, wait for user interaction
mainWindow.runEventLoop();
return 0;
}
```
1. **Compile and run!** You'll see a window with a blue rounded button, clicking it will pop up a message box.
## 📚 Core Types Detailed Explanation (`CoreTypes.h`)
All visual and behavioral properties of the StellarX framework are controlled through the elegant enums and structs defined in `CoreTypes.h`.
### Enum Types (Enums)
| Enum Type | Description | Common Values |
| :--------------------- | :---------------------- | :----------------------------------------------------------- |
| **`ControlShape`** | Control geometric shape | `RECTANGLE`, `B_RECTANGLE`, `ROUND_RECTANGLE`, `CIRCLE`, `ELLIPSE`, etc. |
| **`ButtonMode`** | Button behavior mode | `NORMAL`, `TOGGLE`, `DISABLED` |
| **`TextBoxMode`** | Text box mode | `INPUT_MODE`, `READONLY_MODE` |
| **`FillMode`** | Graphics fill mode | `SOLID`, `NULL`, `HATCHED`, etc. |
| **`FillStyle`** | Pattern fill style | `HORIZONTAL`, `CROSS`, etc. |
| **`LineStyle`** | Border line style | `SOLID`, `DASH`, `DOT`, etc. |
| **`MessageBoxType`** | Message box type | `OK`, `OKCancel`, `YesNo`, `YesNoCancel`, `RetryCancel`, `AbortRetryIgnore` |
| **`MessageBoxResult`** | Message box result | `OK`, `Cancel`, `Yes`, `No`, `Abort`, `Retry`, `Ignore` |
### Structs (Structs)
| Struct | Description |
| :----------------- | :----------------------------------------------------------- |
| **`ControlText`** | Encapsulates all text style attributes, including font, size, color, bold, italic, underline, strikethrough, etc. |
| **`RouRectangle`** | Defines rounded rectangle corner ellipse dimensions, contains width and height properties. |
**Usage Example:**
```c++
// Create a complex text style
StellarX::ControlText myStyle;
myStyle.nHeight = 25; // Font height
myStyle.lpszFace = _T("Microsoft YaHei"); // Font
myStyle.color = RGB(255, 0, 0); // Red color
myStyle.nWeight = FW_BOLD; // Bold
myStyle.bUnderline = true; // Underline
// Apply to controls
myLabel->textStyle = myStyle;
myButton->textStyle = myStyle;
```
## 🧩 Complete Control Library
### 1. Basic Controls
| Control | Header File | Description | Key Features |
| :---------- | :---------- | :-------------------- | :----------------------------------------------------------- |
| **Button** | `Button.h` | Multi-function button | Supports multiple modes/shapes/states, hover/click colors, custom callbacks |
| **Label** | `Label.h` | Text label | Supports transparent/opaque background, custom font styles |
| **TextBox** | `TextBox.h` | Input box/Display box | Supports input and read-only modes, integrates EasyX's `InputBox` |
### 2. Container Controls
| Control | Header File | Description |
| :--------- | :---------- | :----------------------------------------------------------- |
| **Canvas** | `Canvas.h` | Container control, can serve as parent container for other controls, supports custom borders and background. |
| **Window** | `Window.h` | Top-level window, ultimate container for all controls, responsible for message loop and dispatching. |
### 3. Advanced Controls
| Control | Header File | Description | Key Features |
| :--------- | :---------- | :----------- | :----------------------------------------------------------- |
| **Table** | `Table.h` | Data table | **Framework highlight feature**, supports paginated display, custom headers and data, automatic column width calculation, page turn buttons. |
| **Dialog** | `Dialog.h` | Dialog class | Implements complete dialog functionality, supports multiple button combinations and asynchronous result callbacks. Automatically handles layout, background save/restore and lifecycle management. |
**Table Control Example:**
```c++
// Create a table
auto myTable = std::make_unique<Table>(50, 50);
// Set headers
myTable->setHeaders({ "ID", "Name", "Age", "Occupation" });
// Add data rows
myTable->setData({ "1", "Zhang San", "25", "Engineer" });
myTable->setData({ "2", "Li Si", "30", "Designer" });
myTable->setData({ "3", "Wang Wu", "28", "Product Manager" });
// Set rows per page
myTable->setRowsPerPage(2);
// Set table style
myTable->textStyle.nHeight = 16;
myTable->setTableBorder(RGB(50, 50, 50));
myTable->setTableBk(RGB(240, 240, 240));
// Add to window
mainWindow.addControl(std::move(myTable));
```
### 4. Static Factory Class
| Control | Header File | Description | Key Features |
| :------------- | :------------- | :------------------- | :----------------------------------------------------------- |
| **MessageBox** | `MessageBox.h` | Dialog factory calls | Static method calls, no instantiation needed, automatically handles modal and non-modal logic differences, integrated into window's dialog management system, provides deduplication mechanism to prevent duplicate dialogs |
**MessageBox Usage Example:**
```c++
// Modal message box (blocks until closed)
auto result = StellarX::MessageBox::ShowModal(
mainWindow,
"Confirm to perform this operation?",
"Confirmation",
StellarX::MessageBoxType::YesNo
);
if (result == StellarX::MessageBoxResult::Yes)
{
// User selected "Yes"
}
// Non-modal message box (asynchronous callback)
StellarX::MessageBox::ShowAsync(
mainWindow,
"Operation completed",
"Notification",
StellarX::MessageBoxType::OK,
[](StellarX::MessageBoxResult result) {
// Asynchronously handle result
}
);
```
## Examples
- **Register Viewer (≈450 lines)** — An interactive 32-bit register visualization tool implemented based on StellarX (supports bit inversion, left/right shift, hex/decimal conversion, signed/unsigned toggle, binary grouping display).
Path: `examples/register-viewer/`
## 🔧 Advanced Topics & Best Practices
### 1. Custom Controls
You can create custom controls by inheriting from the `Control` base class. Just implement the two pure virtual functions `draw()` and `handleEvent()`.
```c++
class MyCustomControl : public Control {
public:
MyCustomControl(int x, int y) : Control(x, y, 100, 100) {}
void draw() override {
saveStyle();
// Your custom drawing logic
setfillcolor(RGB(255, 100, 100));
fillrectangle(x, y, x + width, y + height);
restoreStyle();
}
bool handleEvent(const ExMessage& msg) override {
// Your custom event handling logic
if (msg.message == WM_LBUTTONDOWN &&
msg.x > x && msg.x < x + width &&
msg.y > y && msg.y < y + height) {
// Handle click
return true; // Event consumed
}
return false; // Event not consumed
}
bool IsVisible() const override { return true; }
bool model() const override { return false; }
};
```
### 2. Layout Management
Current version of StellarX primarily uses **absolute positioning**. For simple layouts, you can achieve this by calculating coordinates. For complex layouts, consider:
- Nesting controls in `Canvas` to achieve relative positioning.
- Implementing simple flow layout or grid layout managers yourself.
### 3. Performance Optimization
- **Dirty Rectangle Rendering**: Implemented internally in the framework, controls set `dirty=true` when state changes, only redraw when necessary.
- **Image Resources**: Use `IMAGE` objects to load images, then reuse them to avoid multiple loads.
- **Reduce Operations in Loops**: Avoid heavy calculations in `draw()` and `handleEvent()`.
- **Event Consumption Mechanism**: Use event consumption return values appropriately to avoid unnecessary event propagation.
### 4. Dialog Usage Tips
- **Modal Dialogs**: Use `ShowModal` method, blocks current thread until dialog closes, suitable for important operations requiring user confirmation.
- **Non-modal Dialogs**: Use `ShowAsync` method, doesn't block main thread, suitable for informational prompts or background tasks.
- **Dialog Deduplication**: Framework has built-in non-modal dialog deduplication mechanism to prevent multiple dialogs for the same message from appearing simultaneously.
## ⚠️ Important Limitations & Suitable Scenarios
**StellarX framework's design goals are lightness, clarity, and teaching value, therefore it explicitly is NOT suitable for the following scenarios:**
- **High-performance games or complex animations**: Rendering is based on EasyX's CPU software rendering, performance is limited.
- **Applications requiring high DPI scaling**: Limited support for high DPI displays, interface may not display correctly.
- **Applications requiring accessibility features**: No support for screen readers and other assistive technologies.
- **Cross-platform applications**: Deeply dependent on Windows API and EasyX, cannot run directly on Linux/macOS.
- **Complex commercial software frontends**: Lacks advanced controls (like tree views, rich text boxes, tabs, advanced lists) and mature automatic layout managers.
**If you need to develop the aforementioned types of applications, please consider the following mature solutions:**
- **Qt**: Extremely powerful, cross-platform, suitable for large commercial applications.
- **wxWidgets**: Native appearance, cross-platform.
- **Dear ImGui**: Immediate mode GUI, very suitable for tools and debug interfaces.
- **Web Technology Stack (Electron/CEF)**: Suitable for scenarios requiring web technologies.
## 📜 License
This project uses the **MIT License**.
You are free to:
- Use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the framework.
- Use it for private or commercial projects.
The only requirement is:
- Please retain the original copyright notice in your projects.
See the [LICENSE](https://license/) file in the project root directory for details.
## 👥 Contribution Guide
We welcome all forms of contributions! If you want to contribute to the StellarX framework, please read the following guidelines:
1. **Code Style**: Please follow the existing Google C++ style guide (use spaces for indentation, braces on new lines, etc.).
2. **New Features**: Must provide **example code** and update relevant parts of this README document.
3. **Submitting PRs**: Please ensure your code is tested before submission and clearly describe your changes and motivation.
4. **Issue Reporting**: If you find a bug or have new ideas, welcome to submit Issues on GitHub.
Detailed contribution guide please refer to [CONTRIBUTING.md](CONTRIBUTING.en.md).
## 🙏 Acknowledgements
- Thanks to [EasyX Graphics Library](https://easyx.cn/) for providing the simple and easy-to-use graphics foundation for this project, making C++ GUI programming education possible.
- Thanks to all developers pursuing the **concise, efficient, clear** coding philosophy, you are the inspiration for StellarX's birth.
------
**Star Ocean Vast, Code as Boat.**
May `StellarX` become a reliable cornerstone for your exploration of the GUI world, whether for learning, teaching, or creating practical tools.
## 📞 Support & Feedback
If you encounter problems during use or have any suggestions:
1. Check [Example Code](examples/) for usage reference
2. Check [Changelog](CHANGELOG.en.md) to learn about latest changes
3. Submit Issues on GitHub repository to report problems
------
*StellarX Framework - Light as Dust, Bound by Stars*
+455
View File
@@ -0,0 +1,455 @@
# 星垣 (StellarX) GUI Framework
[English document](README.en.md)
------
![GitHub all releases](https://img.shields.io/github/downloads/Ysm-04/StellarX/total)
[![Star GitHub Repo](https://img.shields.io/github/stars/Ysm-04/StellarX.svg?style=social&label=Star%20This%20Repo)](https://github.com/Ysm-04/StellarX)
![Version](https://img.shields.io/badge/Version-2.0.0-brightgreen.svg)
![Download](https://img.shields.io/badge/Download-2.0.0_Release-blue.svg)
![C++](https://img.shields.io/badge/C++-17+-00599C?logo=cplusplus&logoColor=white)
![Windows](https://img.shields.io/badge/Platform-Windows-0078D6?logo=windows)
![EasyX](https://img.shields.io/badge/Based_on-EasyX-00A0EA)
![License](https://img.shields.io/badge/License-MIT-blue.svg)
![Architecture](https://img.shields.io/badge/Architecture-Modular%20OOP-brightgreen)
![CMake](https://img.shields.io/badge/Build-CMake-064F8C?logo=cmake)
> **「繁星为界,轻若尘埃」** —— 一个为Windows平台打造的、极致轻量级、高度模块化的C++原生GUI框架。
`星垣 (StellarX)` 诞生于对现代GUI框架"过度臃肿"的反抗。它拒绝动辄数百MB的依赖、漫长的编译时间和复杂的学习曲线,选择回归本质:用最精简的代码、最清晰的架构和最高的效率,解决桌面应用开发的核心需求。
它是一个**纯粹的教学级、工具级框架**,旨在让开发者深入理解GUI原理,并快速构建轻量级Windows工具。
---
## 📦 项目结构与设计哲学
星垣框架采用经典的**面向对象**和**模块化**设计,项目结构清晰规范:
```markdown
StellarX/
├── include/ # 头文件目录
│ └── StellarX/ # 框架头文件
│ ├── StellarX.h # 主包含头文件 - 一键引入整个框架
│ ├── CoreTypes.h # ★ 核心 ★ - 所有枚举、结构体的唯一定义源
│ ├── Control.h # 抽象基类 - 定义所有控件的统一接口
│ ├── Button.h # 按钮控件
│ ├── Window.h # 窗口管理
│ ├── Label.h # 标签控件
│ ├── TextBox.h # 文本框控件
│ ├── Canvas.h # 画布容器
│ ├── Dialog.h # 对话框控件(v2.0.0新增)
│ ├── MessageBox.h # 消息框工厂(v2.0.0新增)
│ └── Table.h # 表格控件
├── src/ # 源文件目录
│ ├── Control.cpp
│ ├── Button.cpp
│ ├── Window.cpp
│ ├── Label.cpp
│ ├── TextBox.cpp
│ ├── Canvas.cpp
│ ├── Table.cpp
│ ├── Dialog.cpp # v2.0.0新增
│ └── MessageBox.cpp # v2.0.0新增
├── examples/ # 示例代码目录
│ └── demo.cpp # 基础演示
├── docs/ # 文档目录
│ └── CODE_OF_CONDUCT.md # 行为准则
├── CMakeLists.txt # CMake 构建配置
├── CONTRIBUTING.md # 贡献指南
├── CHANGELOG.md # 更新日志
├── Doxyfile # Doxygen 配置
├── LICENSE # MIT 许可证
└── README.md # 项目说明
```
**设计理念:**
1. **单一职责原则 (SRP)**: 每个类/文件只负责一件事。
2. **依赖倒置原则 (DIP)**: 高层模块(如`Window`)不依赖低层模块(如`Button`),二者都依赖其抽象(`Control`)。
3. **开闭原则 (OCP)**: 通过继承`Control`基类,可以轻松扩展新的控件,而无需修改现有代码。
4. **一致性**: 所有控件共享统一的`draw()``handleEvent()`接口。
## 🚀 核心特性
- **极致的轻量级**: 核心库编译后仅 ~12MB,无任何外部依赖。生成的应用程序小巧玲珑。
- **清晰的模块化架构**: 使用`CoreTypes.h`统一管理所有类型,消除重复定义,极大提升可维护性。
- **原生C++性能**: 直接基于EasyX和Win32 API,提供接近原生的执行效率,内存占用极低(通常<10MB)。
- **完整的控件体系**: 按钮(Button)、标签(Label)、文本框(TextBox)、画布(Canvas)、表格(Table)、对话框(Dialog)与消息框工厂(MessageBox)。
- **高度可定制化**: 从控件颜色、形状(矩形、圆角、圆形、椭圆)到填充模式、字体样式,均有详尽枚举支持,可轻松定制。
- **简洁直观的API**: 采用经典的面向对象设计,代码即文档,学习成本极低。
- **标准项目结构**: 采用标准的include/src分离结构,支持CMake构建,易于集成和使用。
- **增强的事件系统**: v2.0.0引入事件消费机制,所有`handleEvent`方法返回`bool`表示是否消费事件,支持更精细的事件传播控制。
- **对话框系统**: 新增完整的对话框支持,包括模态和非模态对话框,自动处理背景保存和恢复。
## ⚡ 快速开始(5分钟上手)
> **🎯 最新版本下载**
> 从 [GitHub Releases](https://github.com/Ysm-04/StellarX/releases/latest) 下载预编译的库文件和头文件,即可快速集成到你的项目中。
### 环境要求
- **操作系统**: Windows 10 或更高版本
- **编译器**: 支持C++17的编译器 (如: **Visual Studio 2019+**)
- **图形库**: [EasyX](https://easyx.cn/) (2022版本或更高,安装时请选择与您编译器匹配的版本)
- **构建工具**: CMake 3.12+ (可选,推荐使用)
### 安装 EasyX
1. 访问 [EasyX 官网](https://easyx.cn/) 下载最新版本
2. 运行安装程序,选择与您的 Visual Studio 版本匹配的版本
3. 安装完成后,无需额外配置,星垣框架会自动链接 EasyX
### 方法一:使用CMake构建(推荐)
1. **克隆项目**:
```bash
git clone https://github.com/Ysm-04/StellarX.git
cd StellarX
```
2. **生成构建系统**:
```bash
mkdir build
cd build
cmake ..
```
3. **编译项目**:
```bash
cmake --build .
```
4. **运行示例**:
```bash
./examples/Demo
```
### 方法二:手动集成到现有项目
1. **将include和src目录复制**到您的项目中
2. **配置包含路径**,确保编译器可以找到`include/StellarX/`目录
3. **将所有.cpp文件**添加到您的项目中编译
### 创建你的第一个星垣应用
```cpp
// 只需包含这一个头文件即可使用所有功能
#include "StellarX.h"
// 程序入口点(请使用WinMain以获得更好的兼容性)
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
{
// 1. 创建一个640x480的窗口,背景为白色,标题为"我的应用"
Window mainWindow(640, 480, 0, RGB(255, 255, 255), "我的第一个星垣应用");
// 2. 创建一个按钮 (使用智能指针管理)
auto myButton = std::make_unique<Button>(
250, 200, 140, 40, // x, y, 宽度, 高度
"点击我", // 按钮文本
StellarX::ButtonMode::NORMAL,
StellarX::ControlShape::ROUND_RECTANGLE
);
// 3. 为按钮设置点击事件(使用Lambda表达式)
myButton->setOnClickListener([&mainWindow]() {
// 使用消息框工厂创建模态对话框
auto result = StellarX::MessageBox::ShowModal(
mainWindow,
"欢迎使用星垣GUI\r\n作者:我在人间做废物",
"问候",
StellarX::MessageBoxType::OKCancel
);
// 处理对话框结果
if (result == StellarX::MessageBoxResult::OK) {
// 用户点击了确定按钮
}
});
// 4. (可选)设置按钮样式
myButton->textStyle.nHeight = 20;
myButton->textStyle.color = RGB(0, 0, 128); // 深蓝色文字
myButton->setButtonBorder(RGB(0, 128, 255)); // 蓝色边框
// 5. 将按钮添加到窗口
mainWindow.addControl(std::move(myButton));
// 6. 绘制窗口
mainWindow.draw();
// 7. 进入消息循环,等待用户交互
mainWindow.runEventLoop();
return 0;
}
```
1. **编译并运行!** 您将看到一个带有蓝色圆角按钮的窗口,点击它将会弹出消息框。
## 📚 核心类型详解 (`CoreTypes.h`)
星垣框架的所有视觉和行为属性都通过`CoreTypes.h`中定义的精美枚举和结构体来控制。
### 枚举类型 (Enums)
| 枚举类型 | 描述 | 常用值 |
| :--------------------- | :----------- | :----------------------------------------------------------- |
| **`ControlShape`** | 控件几何形状 | `RECTANGLE`, `B_RECTANGLE`, `ROUND_RECTANGLE`, `CIRCLE`, `ELLIPSE`等 |
| **`ButtonMode`** | 按钮行为模式 | `NORMAL`(普通), `TOGGLE`(切换), `DISABLED`(禁用) |
| **`TextBoxMode`** | 文本框模式 | `INPUT_MODE`(输入), `READONLY_MODE`(只读) |
| **`FillMode`** | 图形填充模式 | `SOLID`(实心), `NULL`(空心), `HATCHED`(图案)等 |
| **`FillStyle`** | 图案填充样式 | `HORIZONTAL`(水平线), `CROSS`(十字线)等 |
| **`LineStyle`** | 边框线型 | `SOLID`(实线), `DASH`(虚线), `DOT`(点线)等 |
| **`MessageBoxType`** | 消息框类型 | `OK`, `OKCancel`, `YesNo`, `YesNoCancel`, `RetryCancel`, `AbortRetryIgnore` |
| **`MessageBoxResult`** | 消息框结果 | `OK`, `Cancel`, `Yes`, `No`, `Abort`, `Retry`, `Ignore` |
### 结构体 (Structs)
| 结构体 | 描述 |
| :----------------- | :----------------------------------------------------------- |
| **`ControlText`** | 封装了所有文本样式属性,包括字体、大小、颜色、粗体、斜体、下划线、删除线等。 |
| **`RouRectangle`** | 定义圆角矩形的圆角椭圆尺寸,包含宽度和高度属性。 |
**使用示例:**
cpp
```c++
// 创建一个复杂的文本样式
StellarX::ControlText myStyle;
myStyle.nHeight = 25; // 字体高度
myStyle.lpszFace = _T("微软雅黑"); // 字体
myStyle.color = RGB(255, 0, 0); // 红色
myStyle.nWeight = FW_BOLD; // 粗体
myStyle.bUnderline = true; // 下划线
// 应用于控件
myLabel->textStyle = myStyle;
myButton->textStyle = myStyle;
```
## 🧩 控件库大全
### 1. 基础控件
| 控件 | 头文件 | 描述 | 关键特性 |
| :---------- | :---------- | :------------ | :------------------------------------------------------ |
| **Button** | `Button.h` | 多功能按钮 | 支持多种模式/形状/状态,可设置悬停/点击颜色,自定义回调 |
| **Label** | `Label.h` | 文本标签 | 支持背景透明/不透明,自定义字体样式 |
| **TextBox** | `TextBox.h` | 输入框/显示框 | 支持输入和只读模式,集成EasyX的`InputBox` |
### 2. 容器控件
| 控件 | 头文件 | 描述 |
| :--------- | :--------- | :------------------------------------------------------- |
| **Canvas** | `Canvas.h` | 容器控件,可作为其他控件的父容器,支持自定义边框和背景。 |
| **Window** | `Window.h` | 顶级窗口,所有控件的最终容器,负责消息循环和调度。 |
### 3. 高级控件
| 控件 | 头文件 | 描述 | 关键特性 |
| :--------- | :--------- | :------- | :----------------------------------------------------------- |
| **Table** | `Table.h` | 数据表格 | **框架功能亮点**,支持分页显示、自定义表头和数据、自动计算列宽、翻页按钮。 |
| **Dialog** | `Dialog.h` | 对话框类 | 实现完整的对话框功能,支持多种按钮组合和异步结果回调。自动处理布局、背景保存恢复和生命周期管理。 |
**表格控件示例:**
cpp
```c++
// 创建一个表格
auto myTable = std::make_unique<Table>(50, 50);
// 设置表头
myTable->setHeaders({ "ID", "姓名", "年龄", "职业" });
// 添加数据行
myTable->setData({ "1", "张三", "25", "工程师" });
myTable->setData({ "2", "李四", "30", "设计师" });
myTable->setData({ "3", "王五", "28", "产品经理" });
// 设置每页显示2行
myTable->setRowsPerPage(2);
// 设置表格样式
myTable->textStyle.nHeight = 16;
myTable->setTableBorder(RGB(50, 50, 50));
myTable->setTableBk(RGB(240, 240, 240));
// 添加到窗口
mainWindow.addControl(std::move(myTable));
```
### 4. 静态工厂类
| 控件 | 头文件 | 描述 | 关键特性 |
| :------------- | :------------- | :--------------- | :----------------------------------------------------------- |
| **MessageBox** | `MessageBox.h` | 对话框的工厂调用 | 静态方法调用,无需实例化,自动处理模态和非模态的逻辑差异集成到窗口的对话框管理系统中提供去重机制防止重复对话框 |
**消息框使用示例:**
cpp
```c++
// 模态消息框(阻塞直到关闭)
auto result = StellarX::MessageBox::ShowModal(
mainWindow,
"确认要执行此操作吗?",
"确认",
StellarX::MessageBoxType::YesNo
);
if (result == StellarX::MessageBoxResult::Yes)
{
// 用户选择了"是"
}
// 非模态消息框(异步回调)
StellarX::MessageBox::ShowAsync(
mainWindow,
"操作已完成",
"提示",
StellarX::MessageBoxType::OK,
[](StellarX::MessageBoxResult result) {
// 异步处理结果
}
);
```
## 示例
- **寄存器查看器 (≈450 行)** — 一个基于 StellarX 实现的交互式 32 位寄存器可视化工具(支持位取反、左/右移、十六进制/十进制转换、带有符号/无符号切换、二进制分组显示)。
路径:`examples/register-viewer/`
## 🔧 高级主题与最佳实践
### 1. 自定义控件
您可以通过继承`Control`基类来创建自定义控件。只需实现`draw()`和`handleEvent()`两个纯虚函数即可。
cpp
```c++
class MyCustomControl : public Control {
public:
MyCustomControl(int x, int y) : Control(x, y, 100, 100) {}
void draw() override {
saveStyle();
// 您的自定义绘制逻辑
setfillcolor(RGB(255, 100, 100));
fillrectangle(x, y, x + width, y + height);
restoreStyle();
}
bool handleEvent(const ExMessage& msg) override {
// 您的自定义事件处理逻辑
if (msg.message == WM_LBUTTONDOWN &&
msg.x > x && msg.x < x + width &&
msg.y > y && msg.y < y + height) {
// 处理点击
return true; // 事件已消费
}
return false; // 事件未消费
}
bool IsVisible() const override { return true; }
bool model() const override { return false; }
};
```
### 2. 布局管理
当前版本星垣主要采用**绝对定位**。对于简单布局,您可以通过计算坐标来实现。对于复杂布局,可以考虑:
- 在`Canvas`中嵌套控件,实现相对定位。
- 自行实现简单的流式布局或网格布局管理器。
### 3. 性能优化
- **脏矩形渲染**: 框架内部已实现,控件状态改变时`dirty=true`,仅在需要时重绘。
- **图像资源**: 使用`IMAGE`对象加载图片后,可重复使用,避免多次加载。
- **减少循环内操作**: 在`draw()`和`handleEvent()`中避免进行重型计算。
- **事件消费机制**: 合理使用事件消费返回值,避免不必要的事件传播。
### 4. 对话框使用技巧
- **模态对话框**: 使用`ShowModal`方法,会阻塞当前线程直到对话框关闭,适合需要用户确认的重要操作。
- **非模态对话框**: 使用`ShowAsync`方法,不会阻塞主线程,适合提示性信息或后台任务。
- **对话框去重**: 框架内置了非模态对话框去重机制,防止同一消息的多个对话框同时出现。
## ⚠️ 重要限制与适用场景
**星垣框架的设计目标是轻便、清晰和教学价值,因此它明确** **不适用于** **以下场景:**
- **高性能游戏或复杂动画**: 渲染基于EasyX的CPU软件渲染,性能有限。
- **需要高DPI缩放的应用**: 对高DPI显示器的支持有限,界面可能显示不正确。
- **需要无障碍功能的应用**: 未提供对屏幕阅读器等辅助技术的支持。
- **跨平台应用**: 深度依赖Windows API和EasyX,无法直接在Linux/macOS上运行。
- **复杂的商业软件前端**: 缺乏高级控件(如树形图、富文本框、选项卡、高级列表等)和成熟的自动布局管理器。
**如果您需要开发上述类型的应用,请考虑使用以下成熟方案:**
- **Qt**: 功能极其强大,跨平台,适合大型商业应用。
- **wxWidgets**: 原生外观,跨平台。
- **Dear ImGui**: 即时模式GUI,非常适合工具和调试界面。
- **Web技术栈 (Electron/CEF)**: 适合需要Web技术的场景。
## 📜 许可证 (License)
本项目采用 **MIT 许可证**。
您可以自由地:
- 使用、复制、修改、合并、出版发行、散布、再授权及销售本框架的副本。
- 将其用于私人或商业项目。
唯一要求是:
- 请在您的项目中保留原始的版权声明。
详见项目根目录的 [LICENSE](LICENSE) 文件。
## 👥 贡献指南 (Contributing)
我们欢迎任何形式的贡献!如果您想为星垣框架添砖加瓦,请阅读以下指南:
1. **代码风格**: 请遵循现有的Google C++规范风格(使用空格缩进,大括号换行等)。
2. **新增功能**: 必须提供**示例代码**,并更新本README文档的相关部分。
3. **提交PR**: 请确保您的代码在提交前已经过测试,并描述清楚您的更改内容和动机。
4. **问题反馈**: 如果您发现了Bug或有新的想法,欢迎在GitHub提交Issue。
详细贡献指南请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
## 🙏 致谢 (Acknowledgements)
- 感谢 [EasyX Graphics Library](https://easyx.cn/) 为这个项目提供了简单易用的图形基础,使得用C++教学GUI编程成为可能。
- 感谢所有追求**简洁、高效、清晰**编码理念的开发者,你们是"星垣"诞生的灵感源泉。
------
**星辰大海,代码为舟。**
愿 `星垣 (StellarX)` 能成为您探索GUI世界的一颗可靠基石,无论是用于学习、教学还是创造实用的工具。
## 📞 支持与反馈
如果您在使用过程中遇到问题或有任何建议:
1. 查看 [示例代码](examples/) 获取使用参考
2. 查阅 [更新日志](CHANGELOG.md) 了解最新变化
3. 在GitHub仓库提交Issue反馈问题
------
*星垣框架 - 轻若尘埃,繁星为界*
-443
View File
@@ -1,443 +0,0 @@
#include "SxLog.h"
#include <cstdlib>
#include <clocale>
/********************************************************************************
* @文件: SxLog.cpp
* @摘要: StellarX 日志系统实现(过滤/格式化/输出/文件滚动/RAII提交/作用域计时)
* @描述:
* 该实现文件主要包含 4 个关键点:
* 1) FileSink: 文件打开、写入、flush 与按阈值滚动
* 2) SxLogger: shouldLog 过滤、formatPrefix 前缀拼接、logLine 统一输出出口
* 3) SxLogLine: 析构提交(RAII)确保“一条语句输出一整行”
* 4) SxLogScope: 按需启用计时,析构输出耗时
*
* @实现难点提示:
* - shouldLog 必须“零副作用”,否则宏短路会带来不可预测行为
* - logLine 是统一出口,必须保证行级一致性,且避免在持锁状态下递归打日志
* - 文件滚动要处理文件名安全性与跨平台 rename 行为差异
* - 时间戳生成需要兼容 Windows 与 POSIXlocaltime_s/localtime_r
********************************************************************************/
namespace StellarX
{
// -------- FileSink --------
// 打开文件输出
// 难点:
// - 需要支持追加与清空两种模式
// - open 前先 close,避免重复打开导致句柄泄漏
bool FileSink::open(const std::string& path, bool append)
{
close();
filePath = path;
appendMode = append;
std::ios::openmode mode = std::ios::out;
mode |= (append ? std::ios::app : std::ios::trunc);
ofs.open(path.c_str(), mode);
return ofs.is_open();
}
// 关闭文件输出(可重复调用)
void FileSink::close()
{
if (ofs.is_open()) ofs.close();
}
// 查询是否已打开
bool FileSink::isOpen() const
{
return ofs.is_open();
}
// 写入一整行
// 难点:
// - 写入后若启用 rotateBytes,需要及时检测文件大小是否到阈值
void FileSink::writeLine(const std::string& line)
{
if (!ofs.is_open()) return;
ofs << line;
if (rotateBytes > 0) rotateIfNeeded();
}
// flush 文件缓冲
void FileSink::flush()
{
if (ofs.is_open()) ofs.flush();
}
// 滚动文件
// 难点:
// 1) tellp() 返回的是当前写指针位置,通常可近似视为文件大小
// 2) 时间戳用于文件名时需要做字符清洗,避免出现不友好字符
// 3) rename 行为与权限/占用有关,失败时需要保证不崩溃(此处选择“尽力而为”)
bool FileSink::rotateIfNeeded()
{
if (!ofs.is_open() || rotateBytes == 0) return false;
const std::streampos pos = ofs.tellp();
if (pos < 0) return false;
const std::size_t size = static_cast<std::size_t>(pos);
if (size < rotateBytes) return false;
ofs.flush();
ofs.close();
// xxx.log -> xxx.log.YYYYmmdd_HHMMSS
// 说明:
// - makeTimestampLocal 形如 "2026-01-09 12:34:56"
// - 文件名中把 '-' ' ' ':' 替换为 '_',只保留数字与 '_',降低环境差异
const std::string ts = SxLogger::makeTimestampLocal();
std::string safeTs;
safeTs.reserve(ts.size());
for (char ch : ts)
{
if (ch >= '0' && ch <= '9') safeTs.push_back(ch);
else if (ch == '-' || ch == ' ' || ch == ':') safeTs.push_back('_');
}
if (safeTs.empty()) safeTs = "rotated";
const std::string rotated = filePath + "." + safeTs;
std::rename(filePath.c_str(), rotated.c_str());
// 重新打开新文件
// 注意: 这里用 append=false,确保新文件从空开始
return open(filePath, false);
}
// -------- SxLogger --------
// 设置 Windows 控制台 codepage(只执行一次)
// 难点:
// - 只影响终端解释输出字节的方式,不影响源码文件编码
// - 使用 once_flag 避免重复 system 调用造成噪声与性能浪费
//
void SxLogger::setGBK()
{
#ifdef _WIN32
static std::once_flag once;
std::call_once(once, []() {
// 切到chcp 936GBK),避免中文日志在 CP936 控制台下乱码
// 说明:这不是 WinAPI;是执行系统命令
std::system("chcp 936 >nul");
// 补充说明:
// - chcp 936 实际是设置为 CP936GBK
// - 如果你的终端本身是 UTF-8 环境,调用它可能反而改变显示行为
// - 该函数建议只在“明确需要 GBK 控制台输出”的场景调用
// 尝试让 C/C++ 运行库按 UTF-8 工作(对部分流输出有帮助)
// std::setlocale(LC_ALL, ".UTF8");
});
#endif
}
// 获取单例
// 难点:
// - 作为全局入口,初始化必须线程安全
// - C++11 起函数内静态对象初始化由标准保证线程安全
SxLogger& SxLogger::Get()
{
static SxLogger inst;
return inst;
}
// 构造:设置默认语言
SxLogger::SxLogger()
: lang(SxLogLanguage::ZhCN)
{
}
// 设置最低输出级别
void SxLogger::setMinLevel(SxLogLevel level)
{
std::lock_guard<std::mutex> lock(mtx);
cfg.minLevel = level;
}
// 获取最低输出级别
SxLogLevel SxLogger::getMinLevel() const
{
std::lock_guard<std::mutex> lock(mtx);
return cfg.minLevel;
}
// 设置语言
// 难点:
// - 语言只影响 SX_T 的字符串选择
// - 这里用 atomic relaxed,避免频繁加锁
void SxLogger::setLanguage(SxLogLanguage l)
{
lang.store(l, std::memory_order_relaxed);
}
// 获取语言
SxLogLanguage SxLogger::getLanguage() const
{
return lang.load(std::memory_order_relaxed);
}
// 设置 Tag 过滤
// 难点:
// - 当前实现是 vector<string> 线性匹配,适合 tag 数量不大
// - 若未来 tag 很多,可考虑 unordered_set 优化(但会增加依赖与复杂度)
void SxLogger::setTagFilter(SxTagFilterMode mode, const std::vector<std::string>& tags)
{
std::lock_guard<std::mutex> lock(mtx);
cfg.tagFilterMode = mode;
cfg.tagList = tags;
}
// 清空 Tag 过滤
void SxLogger::clearTagFilter()
{
std::lock_guard<std::mutex> lock(mtx);
cfg.tagFilterMode = SxTagFilterMode::None;
cfg.tagList.clear();
}
// 开关控制台输出
// 难点:
// - ConsoleSink 持有 ostream 引用,不管理其生命周期
void SxLogger::enableConsole(bool enable)
{
std::lock_guard<std::mutex> lock(mtx);
if (enable)
{
if (!consoleSink) consoleSink.reset(new ConsoleSink(std::cout));
}
else
{
consoleSink.reset();
}
}
// 开启文件输出
// 难点:
// - enableFile 成功与否决定 cfg.fileEnabled
// - 需要把 rotateBytes 同步到 FileSink
bool SxLogger::enableFile(const std::string& path, bool append, std::size_t rotateBytes_)
{
std::lock_guard<std::mutex> lock(mtx);
if (!fileSink) fileSink.reset(new FileSink());
fileSink->setRotateBytes(rotateBytes_);
const bool ok = fileSink->open(path, append);
cfg.fileEnabled = ok;
cfg.filePath = path;
cfg.fileAppend = append;
cfg.rotateBytes = rotateBytes_;
return ok;
}
// 关闭文件输出
void SxLogger::disableFile()
{
std::lock_guard<std::mutex> lock(mtx);
if (fileSink) fileSink->close();
cfg.fileEnabled = false;
}
// 获取配置副本
// 难点:
// - 返回副本避免外部拿到内部引用后绕过锁修改
SxLogConfig SxLogger::getConfigCopy() const
{
std::lock_guard<std::mutex> lock(mtx);
return cfg;
}
// 设置配置(整体替换)
void SxLogger::setConfig(const SxLogConfig& c)
{
std::lock_guard<std::mutex> lock(mtx);
cfg = c;
}
// 级别转字符串
const char* SxLogger::levelToString(SxLogLevel level)
{
switch (level)
{
case SxLogLevel::Trace: return "TRACE";
case SxLogLevel::Debug: return "DEBUG";
case SxLogLevel::Info: return "INFO ";
case SxLogLevel::Warn: return "WARN ";
case SxLogLevel::Error: return "ERROR";
case SxLogLevel::Fatal: return "FATAL";
default: return "OFF ";
}
}
// 判断 tag 是否允许输出
// 难点:
// - 精确匹配 tag 字符串
// - tag==nullptr 时默认允许,避免“无 tag 日志被误杀”
bool SxLogger::tagAllowed(const SxLogConfig& c, const char* tag)
{
if (c.tagFilterMode == SxTagFilterMode::None) return true;
if (!tag) return true;
bool found = false;
for (const auto& t : c.tagList)
{
if (t == tag) { found = true; break; }
}
if (c.tagFilterMode == SxTagFilterMode::Whitelist) return found;
if (c.tagFilterMode == SxTagFilterMode::Blacklist) return !found;
return true;
}
// 快速判定是否需要输出(宏短路依赖)
// 难点:
// 1) 必须无副作用:返回 false 时调用端不会构造对象也不会拼接
// 2) 过滤维度要完整:级别、tag、sink 是否启用
// 3) 当前实现加锁保证 cfg 与 sink 状态一致;代价是高频路径会有锁开销
bool SxLogger::shouldLog(SxLogLevel level, const char* tag) const
{
std::lock_guard<std::mutex> lock(mtx);
if (cfg.minLevel == SxLogLevel::Off) return false;
if (level < cfg.minLevel) return false;
if (!tagAllowed(cfg, tag)) return false;
if (!consoleSink && !cfg.fileEnabled) return false;
return true;
}
// 生成本地时间戳字符串
// 难点:
// - Windows 与 POSIX 的线程安全 localtime API 不同
std::string SxLogger::makeTimestampLocal()
{
using namespace std::chrono;
const auto now = system_clock::now();
const std::time_t t = system_clock::to_time_t(now);
std::tm tmv{};
#if defined(_WIN32)
localtime_s(&tmv, &t);
#else
localtime_r(&t, &tmv);
#endif
std::ostringstream oss;
oss << std::put_time(&tmv, "%Y-%m-%d %H:%M:%S");
return oss.str();
}
// 拼接日志前缀(调用方已持锁)
// 难点:
// - 前缀拼接必须与配置项严格对应,且尽量避免多余开销
// - showSource 会输出 (file:line func),对定位时序问题很有价值
std::string SxLogger::formatPrefixUnlocked(
const SxLogConfig& c,
SxLogLevel level,
const char* tag,
const char* file,
int line,
const char* func) const
{
std::ostringstream oss;
if (c.showTimestamp) oss << "[" << makeTimestampLocal() << "] ";
if (c.showLevel) oss << "[" << levelToString(level) << "] ";
if (c.showTag && tag) oss << "[" << tag << "] ";
if (c.showThreadId)
{
oss << "[T:" << std::this_thread::get_id() << "] ";
}
if (c.showSource && file && func)
{
oss << "(" << file << ":" << line << " " << func << ") ";
}
return oss.str();
}
// 统一输出出口
// 难点:
// 1) 行级一致性:必须把 prefix + msg + "\n" 当作整体写入
// 2) 线程安全:持锁写入可避免不同线程日志互相穿插
// 3) 避免重入:在持锁期间不要再调用 SX_LOG...(会导致死锁)
void SxLogger::logLine(
SxLogLevel level,
const char* tag,
const char* file,
int line,
const char* func,
const std::string& msg)
{
std::lock_guard<std::mutex> lock(mtx);
if (cfg.minLevel == SxLogLevel::Off) return;
if (level < cfg.minLevel) return;
if (!tagAllowed(cfg, tag)) return;
const std::string prefix = formatPrefixUnlocked(cfg, level, tag, file, line, func);
const std::string lineText = prefix + msg + "\n";
if (consoleSink) consoleSink->writeLine(lineText);
if (cfg.fileEnabled && fileSink && fileSink->isOpen())
{
fileSink->writeLine(lineText);
}
if (cfg.autoFlush)
{
if (consoleSink) consoleSink->flush();
if (cfg.fileEnabled && fileSink) fileSink->flush();
}
}
// -------- SxLogLine --------
// 构造:只记录元信息
SxLogLine::SxLogLine(SxLogLevel level, const char* tag, const char* file, int line, const char* func)
: lvl(level), tg(tag), srcFile(file), srcLine(line), srcFunc(func)
{
}
// 析构:提交输出
// 难点:
// - 这是 RAII 设计的核心:保证语句结束时日志自动落地
// - 也要求调用端不要把临时对象跨语句保存(宏用法本身也不支持那样做)
SxLogLine::~SxLogLine()
{
SxLogger::Get().logLine(lvl, tg, srcFile, srcLine, srcFunc, ss.str());
}
// -------- SxLogScope --------
// 构造:按需启用计时
// 难点:
// - 只有 shouldLog 为 true 才记录起点,避免在未输出场景做无意义计时
SxLogScope::SxLogScope(SxLogLevel level, const char* tag, const char* file, int line, const char* func, const char* name)
: lvl(level), tg(tag), srcFile(file), srcLine(line), srcFunc(func), scopeName(name)
{
enabled = SxLogger::Get().shouldLog(lvl, tg);
if (enabled) t0 = std::chrono::steady_clock::now();
}
// 析构:输出耗时
// 难点:
// - steady_clock 用于衡量耗时,避免系统时间调整造成跳变
SxLogScope::~SxLogScope()
{
if (!enabled) return;
const auto t1 = std::chrono::steady_clock::now();
const auto us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
std::ostringstream oss;
oss << "SCOPE " << (scopeName ? scopeName : "") << " cost=" << us << "us";
SxLogger::Get().logLine(lvl, tg, srcFile, srcLine, srcFunc, oss.str());
}
} // namespace StellarX
-416
View File
@@ -1,416 +0,0 @@
#pragma once
/********************************************************************************
* @文件: SxLog.h
* @摘要: StellarX 日志系统对外接口定义(控制台/文件输出 + 级别过滤 + Tag过滤 + 中英文选择)
* @描述:
* 该日志系统采用“宏 + RAII(析构提交)”的方式实现:
* - 调用端通过 SX_LOGD/SX_LOGI... 写日志
* - 宏内部先 shouldLog 短路过滤,未命中时不构造对象、不拼接字符串
* - 命中时构造 SxLogLine,使用 operator<< 拼接内容
* - 语句结束时 SxLogLine 析构,统一提交到 SxLogger::logLine 输出
*
* 输出通道(Sink)目前提供:
* - ConsoleSink: 写入 std::cout(不走 WinAPI 调试输出通道)
* - FileSink: 写入文件,支持按字节阈值滚动
*
* @特性:
* - 日志级别:Trace/Debug/Info/Warn/Error/Fatal/Off
* - Tag 过滤:None/Whitelist/Blacklist
* - 可选前缀:时间戳/级别/Tag/线程ID/源码位置
* - 中英文选择:SX_T(zh, en) / setLanguage
* - 文件滚动:rotateBytes > 0 时按阈值滚动
*
* @使用场景:
* - 排查重绘链路、脏标记传播、Tab 切换、Table 数据刷新等时序问题
* - 输出可复现日志,配合回归验证
*
* @注意:
* - SX_T 仅做“字符串选择”,不做编码转换
* - 控制台显示是否乱码由“终端 codepage/字体/环境”决定
* - 该头文件只声明接口,实现位于 SxLog.cpp
*
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
********************************************************************************/
// SxLog.h - header-only interface (implementation in SxLog.cpp)
// Pure standard library: std::cout and optional file sink.
#include <atomic>
#include <chrono>
#include <cstdint>
#include <ctime>
#include <cstdio>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#ifndef SX_LOG_ENABLE
#define SX_LOG_ENABLE 1
#endif
namespace StellarX
{
/* ========================= 日志级别 ========================= */
// 说明:
// - minLevel 表示最低输出级别,小于 minLevel 的日志会被 shouldLog 直接过滤
// - Off 表示全局关闭
enum class SxLogLevel : int
{
Trace = 0, // 最细粒度:高频路径追踪(谨慎开启)
Debug = 1, // 调试信息:状态变化/关键分支
Info = 2, // 业务信息:关键流程节点
Warn = 3, // 警告:非致命但异常的情况
Error = 4, // 错误:功能失败、需要关注
Fatal = 5, // 致命:通常意味着无法继续运行
Off = 6 // 关闭全部日志
};
/* ========================= 语言选择 ========================= */
// 说明:仅用于 SX_T 选择输出哪一段文本,不做编码转换
enum class SxLogLanguage : int
{
ZhCN = 0, // 中文
EnUS = 1 // 英文
};
/* ========================= Tag 过滤模式 ========================= */
// None : 不过滤,全部输出
// Whitelist : 只输出 tagList 中包含的 tag
// Blacklist : 输出除 tagList 以外的 tag
enum class SxTagFilterMode : int
{
None = 0,
Whitelist = 1,
Blacklist = 2
};
/* ========================= 日志配置 ========================= */
// 说明:SxLogger 内部持有该配置,shouldLog 与 logLine 都依赖它
struct SxLogConfig
{
SxLogLevel minLevel = SxLogLevel::Info; // 最低输出级别
bool showTimestamp = true; // 是否输出时间戳前缀
bool showLevel = true; // 是否输出级别前缀
bool showTag = true; // 是否输出 tag 前缀
bool showThreadId = false; // 是否输出线程ID(排查并发时开启)
bool showSource = false; // 是否输出源码位置(file:line func
bool autoFlush = true; // 每行写完是否 flush(排查问题更稳,性能略差)
SxTagFilterMode tagFilterMode = SxTagFilterMode::None; // Tag 过滤模式
std::vector<std::string> tagList; // Tag 列表(白名单/黑名单)
bool fileEnabled = false; // 文件输出是否启用(enableFile 成功才为 true
std::string filePath; // 文件路径
bool fileAppend = true; // 是否追加写入
std::size_t rotateBytes = 0; // 滚动阈值(0 表示不滚动)
};
/* ========================= Sink 接口 ========================= */
// 说明:
// - Sink 负责“把完整的一行日志写到某个地方”
// - SxLogger 负责过滤/格式化/分发
class ILogSink
{
public:
virtual ~ILogSink() = default;
// 返回 Sink 名称,用于调试识别(例如 "console"/"file"
virtual const char* name() const = 0;
// 写入一整行(调用方保证 line 已包含换行或按约定追加换行)
virtual void writeLine(const std::string& line) = 0;
// 刷新缓冲(可选实现)
virtual void flush() {}
};
/* ========================= 控制台输出 Sink ========================= */
// 作用:把日志写入指定输出流(默认用 std::cout)
class ConsoleSink : public ILogSink
{
public:
// 绑定一个输出流引用(常见用法:std::cout)
explicit ConsoleSink(std::ostream& os) : out(os) {}
const char* name() const override { return "console"; }
// 写入一行(不自动追加换行,换行由上层统一拼接)
void writeLine(const std::string& line) override { out << line; }
// 立即 flush(当 autoFlush=true 时由 SxLogger 调用)
void flush() override { out.flush(); }
private:
std::ostream& out; // 输出流引用(不负责生命周期)
};
/* ========================= 文件输出 Sink ========================= */
// 作用:把日志写入文件,支持按字节阈值滚动
class FileSink : public ILogSink
{
public:
FileSink() = default;
const char* name() const override { return "file"; }
// 打开文件
// path : 文件路径
// append : true 追加写;false 清空重写
bool open(const std::string& path, bool append);
// 关闭文件(安全可重复调用)
void close();
// 查询文件是否处于打开状态
bool isOpen() const;
// 设置滚动阈值(字节)
// bytes = 0 表示不滚动
void setRotateBytes(std::size_t bytes) { rotateBytes = bytes; }
// 写入一行,并在需要时触发滚动
void writeLine(const std::string& line) override;
// flush 文件缓冲
void flush() override;
private:
// 检查并执行滚动
// 返回值:是否发生滚动(或是否重新打开)
bool rotateIfNeeded();
std::ofstream ofs; // 文件输出流
std::string filePath; // 当前文件路径
bool appendMode = true; // 是否追加模式(用于 reopen)
std::size_t rotateBytes = 0; // 滚动阈值
};
/* ========================= 日志中心 SxLogger ========================= */
// 作用:
// - 保存配置(SxLogConfig
// - 过滤(level/tag/sink enabled
// - 格式化前缀(时间/级别/tag/线程/源码位置)
// - 分发到 console/file 等 sink
class SxLogger
{
public:
// 仅用于 Windows 控制台:把 codepage 切到 GBK,解决中文乱码。
// 不使用 WinAPI:内部通过 system("chcp 936") 实现
// 注意:这只影响终端解释输出字节的方式,不影响源码文件编码
static void setGBK();
// 获取全局单例
// 说明:函数内静态对象,C++11 起保证线程安全初始化
static SxLogger& Get();
// 设置最低输出级别
void setMinLevel(SxLogLevel level);
// 获取最低输出级别
SxLogLevel getMinLevel() const;
// 设置语言(用于 SX_T 选择)
void setLanguage(SxLogLanguage lang);
// 获取当前语言
SxLogLanguage getLanguage() const;
// 设置 Tag 过滤
// mode: None/Whitelist/Blacklist
// tags: 过滤列表(精确匹配)
void setTagFilter(SxTagFilterMode mode, const std::vector<std::string>& tags);
// 清空 Tag 过滤(恢复 None
void clearTagFilter();
// 开关控制台输出
void enableConsole(bool enable);
// 开启文件输出
// path : 文件路径
// append : 追加写/清空写
// rotateBytes: 滚动阈值(0 不滚动)
// 返回值:是否打开成功
bool enableFile(const std::string& path, bool append = true, std::size_t rotateBytes = 0);
// 关闭文件输出(不影响控制台输出)
void disableFile();
// 快速判定是否需要输出(宏层面的短路依赖它)
// 说明:
// - shouldLog 一定要“副作用为 0”
// - 若返回 false,调用端不会创建 SxLogLine,也不会拼接字符串
bool shouldLog(SxLogLevel level, const char* tag) const;
// 输出一条完整日志
// 说明:这是统一出口,SxLogLine 析构最终会走到这里
void logLine(
SxLogLevel level,
const char* tag,
const char* file,
int line,
const char* func,
const std::string& msg);
// 获取配置副本(避免外部直接改内部 cfg)
SxLogConfig getConfigCopy() const;
// 批量设置配置(整体替换)
void setConfig(const SxLogConfig& cfg);
// 工具:把级别转为字符串(用于前缀)
static const char* levelToString(SxLogLevel level);
// 工具:生成本地时间戳字符串(用于前缀与文件滚动名)
static std::string makeTimestampLocal();
private:
SxLogger();
// 判断 tag 是否允许输出(根据 Tag 过滤模式与 tagList
static bool tagAllowed(const SxLogConfig& cfg, const char* tag);
// 生成前缀(调用方需已持有锁)
std::string formatPrefixUnlocked(
const SxLogConfig& cfg,
SxLogLevel level,
const char* tag,
const char* file,
int line,
const char* func) const;
mutable std::mutex mtx; // 保护 cfg 与 sink 写入,确保多线程行级一致性
SxLogConfig cfg; // 当前配置
std::atomic<SxLogLanguage> lang; // 语言开关(仅影响 SX_T 选择)
std::unique_ptr<ConsoleSink> consoleSink; // 控制台 sinkenableConsole 控制)
std::unique_ptr<FileSink> fileSink; // 文件 sinkenableFile 控制)
};
/* ========================= 双语选择辅助 ========================= */
// 说明:
// - 只做“选择 zhCN 或 enUS”,不做编码转换
// - 输出显示是否正常由终端环境决定
inline const char* SxT(const char* zhCN, const char* enUS)
{
return (SxLogger::Get().getLanguage() == SxLogLanguage::ZhCN) ? zhCN : enUS;
}
#if defined(__cpp_char8_t) && (__cpp_char8_t >= 201811L)
// 说明:
// - C++20 的 u8"xxx" 是 char8_t*,为了兼容调用端,这里提供重载
// - reinterpret_cast 只是改指针类型,不做 UTF-8 -> GBK 转码
inline const char* SxT(const char8_t* zhCN, const char* enUS)
{
return (SxLogger::Get().getLanguage() == SxLogLanguage::ZhCN)
? reinterpret_cast<const char*>(zhCN)
: enUS;
}
#endif
/* ========================= RAII 日志行对象 ========================= */
// 作用:
// - 构造时记录 level/tag/源码位置
// - operator<< 拼接内容
// - 析构时统一提交给 SxLogger::logLine 输出
//
// 设计意义:
// - 避免调用端忘记写换行
// - 保证一行日志作为整体写出
class SxLogLine
{
public:
// 构造:记录元信息(不输出)
SxLogLine(SxLogLevel level, const char* tag, const char* file, int line, const char* func);
// 析构:提交输出(真正写出发生在这里)
~SxLogLine();
// 拼接内容(流式写法)
template<typename T>
SxLogLine& operator<<(const T& v)
{
ss << v;
return *this;
}
private:
SxLogLevel lvl; // 日志级别
const char* tg; // Tag(不拥有内存)
const char* srcFile; // 源文件名(来自 __FILE__
int srcLine; // 行号(来自 __LINE__
const char* srcFunc; // 函数名(来自 __func__
std::ostringstream ss; // 内容拼接缓冲
};
/* ========================= RAII 作用域计时对象 ========================= */
// 作用:
// - 仅在 shouldLog(Trace, tag) 为 true 时启用计时
// - 析构时输出耗时(微秒)
//
// 使用建议:
// - 只在需要定位性能瓶颈时开启 Trace
// - name 建议传入常量字符串,便于检索
class SxLogScope
{
public:
// 构造:根据 shouldLog 决定是否启用计时
SxLogScope(SxLogLevel level, const char* tag, const char* file, int line, const char* func, const char* name);
// 析构:若启用则输出耗时
~SxLogScope();
private:
bool enabled = false; // 是否启用(未启用则析构无输出)
SxLogLevel lvl = SxLogLevel::Trace; // 级别(通常用 Trace
const char* tg = nullptr; // Tag
const char* srcFile = nullptr; // 源文件
int srcLine = 0; // 行号
const char* srcFunc = nullptr; // 函数
const char* scopeName = nullptr; // 作用域名
std::chrono::steady_clock::time_point t0; // 起始时间点
};
} // namespace StellarX
#if SX_LOG_ENABLE
// SX_T:双语选择宏,调用 SxT 根据当前语言选择输出
#define SX_T(zh, en) ::StellarX::SxT(zh, en)
// 日志宏说明:
// 1) 先 shouldLog 短路过滤,未命中则不会构造 SxLogLine,也不会执行 else 分支的表达式
// 2) 命中则构造临时 SxLogLine,并允许继续使用 operator<< 拼接
// 3) 语句结束时临时对象析构,触发真正输出
#define SX_LOG_TRACE(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Trace, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Trace, tag, __FILE__, __LINE__, __func__)
#define SX_LOGD(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Debug, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Debug, tag, __FILE__, __LINE__, __func__)
#define SX_LOGI(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Info, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Info, tag, __FILE__, __LINE__, __func__)
#define SX_LOGW(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Warn, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Warn, tag, __FILE__, __LINE__, __func__)
#define SX_LOGE(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Error, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Error, tag, __FILE__, __LINE__, __func__)
#define SX_LOGF(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Fatal, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Fatal, tag, __FILE__, __LINE__, __func__)
// 作用域耗时统计宏:默认用 Trace 级别
#define SX_TRACE_SCOPE(tag, nameLiteral) ::StellarX::SxLogScope sx_scope_##__LINE__(::StellarX::SxLogLevel::Trace, tag, __FILE__, __LINE__, __func__, nameLiteral)
#else
// 关闭日志时的兼容宏:保证调用端代码不需要改动
#define SX_T(zh, en) (en)
#define SX_LOG_TRACE(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGD(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGI(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGW(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGE(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGF(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_TRACE_SCOPE(tag, nameLiteral) do {} while(0)
#endif
-676
View File
@@ -1,676 +0,0 @@
#include "TabControl.h"
#include "SxLog.h"
#include "Window.h"
namespace
{
enum class SxTabOverlayRedrawMode
{
None,
RefreshSnapshot
};
bool SxTabRectsIntersect(const RECT& a, const RECT& b)
{
return a.left < b.right && a.right > b.left &&
a.top < b.bottom && a.bottom > b.top;
}
RECT SxTabUnionRect(const RECT& a, const RECT& b)
{
RECT out{};
out.left = (std::min)(a.left, b.left);
out.top = (std::min)(a.top, b.top);
out.right = (std::max)(a.right, b.right);
out.bottom = (std::max)(a.bottom, b.bottom);
return out;
}
}
inline void TabControl::initTabBar()
{
if (controls.empty())return;
int butW = max(this->width / (int)controls.size(), BUTMINWIDTH);
int butH = max(this->height / (int)controls.size(), BUTMINHEIGHT);
if (this->tabPlacement == StellarX::TabPlacement::Top || this->tabPlacement == StellarX::TabPlacement::Bottom)
for (auto& c : controls)
{
c.first->setHeight(tabBarHeight);
c.first->setWidth(butW);
}
else if (this->tabPlacement == StellarX::TabPlacement::Left || this->tabPlacement == StellarX::TabPlacement::Right)
for (auto& c : controls)
{
c.first->setHeight(butH);
c.first->setWidth(tabBarHeight);
}
int i = 0;
switch (this->tabPlacement)
{
case StellarX::TabPlacement::Top:
for (auto& c : controls)
{
c.first->setX(this->x + i * butW);
c.first->setY(this->y);
i++;
}
break;
case StellarX::TabPlacement::Bottom:
for (auto& c : controls)
{
c.first->setX(this->x + i * butW);
c.first->setY(this->y + this->height - tabBarHeight);
i++;
}
break;
case StellarX::TabPlacement::Left:
for (auto& c : controls)
{
c.first->setX(this->x);
c.first->setY(this->y + i * butH);
i++;
}
break;
case StellarX::TabPlacement::Right:
for (auto& c : controls)
{
c.first->setX(this->x + this->width - tabBarHeight);
c.first->setY(this->y + i * butH);
i++;
}
break;
default:
break;
}
}
inline void TabControl::initTabPage()
{
if (controls.empty())return;
// TabControl 内部页签页仍然保留专用布局:
// 这里负责把“当前选项卡容器矩形”拆分成页签栏和页面区,
// 不把这部分细节下放到通用布局解算器里。
// 但页面内容子树的世界坐标映射职责,继续归页面 Canvas 自己管理。
// 因此这里不再手工遍历页内子控件做 setX/setY,
// 只安置每个页面 Canvas 的矩形,让 Canvas 通过自身 relayoutManagedChildren() 收口。
switch (this->tabPlacement)
{
case StellarX::TabPlacement::Top:
for (auto& c : controls)
{
c.second->setX(this->x);
c.second->setY(this->y + tabBarHeight);
c.second->setWidth(this->width);
c.second->setHeight(this->height - tabBarHeight);
}
break;
case StellarX::TabPlacement::Bottom:
for (auto& c : controls)
{
c.second->setX(this->x);
c.second->setY(this->y);
c.second->setWidth(this->width);
c.second->setHeight(this->height - tabBarHeight);
}
break;
case StellarX::TabPlacement::Left:
for (auto& c : controls)
{
c.second->setX(this->x + tabBarHeight);
c.second->setY(this->y);
c.second->setWidth(this->width - tabBarHeight);
c.second->setHeight(this->height);
}
break;
case StellarX::TabPlacement::Right:
for (auto& c : controls)
{
c.second->setX(this->x);
c.second->setY(this->y);
c.second->setWidth(this->width - tabBarHeight);
c.second->setHeight(this->height);
}
break;
default:
break;
}
}
void TabControl::refreshRuntimeLayout()
{
// 这是 TabControl 的内部专用布局入口:
// 外层先通过统一解算得到 TabControl 自身矩形,
// 再由这里继续安置页签按钮和页面区。
initTabBar();
initTabPage();
}
TabControl::TabControl() :Canvas()
{
this->id = "TabControl";
// TabControl 作为外层容器,当前阶段显式允许双轴 Stretch;
// 内部页签栏和页面区仍由自己的专用布局逻辑管理。
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = true;
}
TabControl::TabControl(int x, int y, int width, int height)
: Canvas(x, y, width, height)
{
this->id = "TabControl";
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = true;
}
TabControl::~TabControl()
{
}
void TabControl::setX(int x)
{
applyRuntimeRectDirect(x, y, width, height);
refreshRuntimeLayout();
onWindowResize();
}
void TabControl::setY(int y)
{
applyRuntimeRectDirect(x, y, width, height);
refreshRuntimeLayout();
onWindowResize();
}
void TabControl::setWidth(int width)
{
applyRuntimeRectDirect(x, y, width, height);
refreshRuntimeLayout();
onWindowResize();
}
void TabControl::setHeight(int height)
{
applyRuntimeRectDirect(x, y, width, height);
refreshRuntimeLayout();
onWindowResize();
}
void TabControl::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
{
// TabControl 作为外层控件接入统一解算;
// 但页签栏和页面区仍由自身专用逻辑继续排布。
applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height);
refreshRuntimeLayout();
}
void TabControl::draw()
{
if (!dirty || !show)return;
// 绘制画布背景和基本形状及其子画布控件
Canvas::draw();
for (auto& c : controls)
{
c.second->setDirty(true);
c.second->draw();
}
for (auto& c : controls)
{
c.first->setDirty(true);
c.first->draw();
}
// 首次绘制时处理默认激活页签
if (IsFirstDraw)
{
if (defaultActivation >= 0 && defaultActivation < (int)controls.size())
controls[defaultActivation].first->setButtonClick(true);
else if (defaultActivation >= (int)controls.size())//索引越界则激活最后一个
controls[controls.size() - 1].first->setButtonClick(true);
IsFirstDraw = false;//避免重复处理
}
dirty = false;
}
bool TabControl::handleEvent(const ExMessage& msg)
{
if (!show)return false;
resetEventVisualChanged();
bool consume = false;
bool anyVisualChanged = false;
// TabControl 的同层页签按钮/页面区事件分发顺序,
// 与 Window / Canvas 保持一致:按倒序处理,后加入者视为更靠上层。
if (msg.message == WM_MOUSEMOVE)
{
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
if (!consume)
{
if (it->first->handleEvent(msg))
{
consume = true;
}
if (it->first->didEventAffectVisual())
anyVisualChanged = true;
}
else if (it->first->clearTransientMouseState())
{
anyVisualChanged = true;
}
}
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
if (!it->second->IsVisible())
continue;
if (!consume)
{
if (it->second->handleEvent(msg))
{
consume = true;
}
if (it->second->didEventAffectVisual())
anyVisualChanged = true;
}
else if (it->second->clearTransientMouseState())
{
anyVisualChanged = true;
}
}
}
else
{
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
if (it->first->handleEvent(msg))
{
consume = true;
break;
}
if (!consume)
{
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
if (it->second->IsVisible())
if (it->second->handleEvent(msg))
{
consume = true;
break;
}
}
}
if (dirty)
requestRepaint(parent);
markEventVisualChanged(anyVisualChanged || dirty);
return consume;
}
bool TabControl::clearTransientMouseState()
{
bool changed = false;
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
if (it->first->IsVisible() && it->first->clearTransientMouseState())
changed = true;
if (it->second->IsVisible() && it->second->clearTransientMouseState())
changed = true;
}
return changed;
}
void TabControl::add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>&& control)
{
controls.push_back(std::move(control));
initTabBar();
initTabPage();
size_t idx = controls.size() - 1;
controls[idx].first->setParent(this);
controls[idx].first->enableTooltip(true);
controls[idx].first->setbuttonMode(StellarX::ButtonMode::TOGGLE);
controls[idx].first->setOnToggleOnListener([this, idx]()
{
int prevIdx = -1;
for (size_t i = 0; i < controls.size(); ++i)
{
if (controls[i].second->IsVisible())
{
prevIdx = (int)i;
break;
}
}
for (auto& tab : controls)
{
if (tab.first->getButtonText() != controls[idx].first->getButtonText() && tab.first->isClicked())
tab.first->setButtonClick(false);
}
SX_LOGI("Tab") << SX_T("激活选项卡:","activate tab: ") << prevIdx << "->" << (int)idx
<< " text=" << controls[idx].first->getButtonText();
controls[idx].second->onWindowResize();
controls[idx].second->setIsVisible(true);
dirty = true;
});
controls[idx].first->setOnToggleOffListener([this, idx]()
{
SX_LOGI("Tab") << SX_T("关闭选项卡:id=","deactivate tab: idx=") << (int)idx
<< " text=" << controls[idx].first->getButtonText();
controls[idx].second->setIsVisible(false);
dirty = true;
});
controls[idx].second->setParent(this);
controls[idx].second->setLinewidth(canvaslinewidth);
controls[idx].second->setIsVisible(false);
}
void TabControl::add(std::string tabText, std::unique_ptr<Control> control)
{
control->setDirty(true);
for (auto& tab : controls)
{
if (tab.first->getButtonText() == tabText)
{
control->setParent(tab.second.get());
control->setIsVisible(tab.second->IsVisible());
tab.second->addControl(std::move(control));
break;
}
}
}
void TabControl::setTabPlacement(StellarX::TabPlacement placement)
{
this->tabPlacement = placement;
setDirty(true);
initTabBar();
initTabPage();
}
void TabControl::setTabBarHeight(int height)
{
tabBarHeight = height;
setDirty(true);
initTabBar();
initTabPage();
}
void TabControl::setIsVisible(bool visible)
{
// 先让基类 Canvas 处理自己的回贴/丢快照逻辑
Canvas::setIsVisible(visible);
for (auto& tab : controls)
{
if(true == visible)
{
tab.first->setIsVisible(visible);
//页也要跟着关/开,否则它们会保留旧的 saveBkImage
if (tab.first->isClicked())
tab.second->setIsVisible(true);
else
tab.second->setIsVisible(false);
tab.second->setDirty(true);
}
else
{
tab.first->setIsVisible(visible);
tab.second->setIsVisible(visible);
}
}
}
void TabControl::onWindowResize()
{
// 本轮不再在 onWindowResize 中重做页签布局,
// 这里只负责失效快照、标脏,并把 resize 语义向页签按钮和页面传递。
Control::onWindowResize();
for (auto& c : controls)
{
c.first->onWindowResize();
c.second->onWindowResize();
}
dirty = true;
}
int TabControl::getActiveIndex() const
{
int idx = -1;
for (auto& c : controls)
{
idx++;
if (c.first->isClicked())
return idx;
}
return -1;
}
void TabControl::setActiveIndex(int idx)
{
if (IsFirstDraw)
defaultActivation = idx;
else
{
// 外部重复激活“已经处于激活状态”的页签时,不应再把整条 onToggleOn 链重跑一遍。
// 否则当前可见页面会重复执行 onWindowResize()/setIsVisible(true)
// 对页内像 Table 这种会越出页面边界绘制的控件,容易把快照链再次扰乱,留下残影。
if (idx >= 0 && idx < controls.size())
{
if (getActiveIndex() == idx)
return;
controls[idx].first->setButtonClick(true);
}
}
}
int TabControl::count() const
{
return (int)controls.size();
}
int TabControl::indexOf(const std::string& tabText) const
{
int idx = -1;
for (auto& c : controls)
{
idx++;
if (c.first->getButtonText() == tabText)
return idx;
}
return -1;
}
void TabControl::setDirty(bool dirty)
{
this->dirty = dirty;
for (auto& c : controls)
{
c.first->setDirty(dirty);
c.second->setDirty(dirty);
}
}
void TabControl::requestRepaint(Control* parent)
{
if (shouldDeferManagedRepaint())
{
// 托管路径:TabControl 作为“页签栏 + 当前页面”的统一重绘 root 登记到 Window。
if (auto* host = getHostWindow())
host->requestManagedRepaint(this);
return;
}
if (this == parent)
{
RECT paintCoverage{};
bool hasPaintCoverage = false;
RECT persistentCoverage{};
bool hasPersistentCoverage = false;
auto commitTabUnit = [&](Control* unit, SxTabOverlayRedrawMode overlayMode)
{
if (!unit || !unit->IsVisible())
return;
const bool directDirty = unit->isDirty();
const bool subtreeDirty = unit->hasManagedDirtySubtree();
if (overlayMode == SxTabOverlayRedrawMode::RefreshSnapshot)
{
// 下层单元的持久内容已经写过像素,上层页签/页面作为 overlay 补画时,
// 必须先丢掉旧快照,重新抓取当前背景后再画,否则会把旧背景再贴回来。
unit->invalidateBackgroundSnapshot();
unit->setDirty(true);
unit->draw();
}
else if (directDirty)
{
unit->draw();
}
else if (subtreeDirty)
{
unit->commitManagedRepaint();
}
else
{
return;
}
const RECT paintRect = unit->getManagedRepaintCoverageRect();
if (!hasPaintCoverage)
{
paintCoverage = paintRect;
hasPaintCoverage = true;
}
else
{
paintCoverage = SxTabUnionRect(paintCoverage, paintRect);
}
const RECT persistentRect = unit->getManagedRepaintPersistentCoverageRect();
if (!hasPersistentCoverage)
{
persistentCoverage = persistentRect;
hasPersistentCoverage = true;
}
else
{
persistentCoverage = SxTabUnionRect(persistentCoverage, persistentRect);
}
};
// 局部重绘必须和 draw() 维持同一套顺序:
// 先页面,再页签按钮。
// 否则页签按钮上的 Tooltip 会被后画的页面盖掉,
// 表现为“有页面打开时 Tooltip 看不到,所有页关闭时才正常”。
for (auto& control : controls)
{
Control* page = control.second.get();
if (!page->IsVisible())
continue;
if (page->hasManagedDirtySubtree())
{
commitTabUnit(page, SxTabOverlayRedrawMode::None);
}
else if (hasPaintCoverage && SxTabRectsIntersect(page->getManagedRepaintCoverageRect(), paintCoverage))
{
const bool persistentHit = hasPersistentCoverage &&
SxTabRectsIntersect(page->getManagedRepaintPersistentCoverageRect(), persistentCoverage);
if (persistentHit)
commitTabUnit(page, SxTabOverlayRedrawMode::RefreshSnapshot);
}
}
for (auto& control : controls)
{
Control* button = control.first.get();
if (!button->IsVisible())
continue;
if (button->hasManagedDirtySubtree())
{
commitTabUnit(button, SxTabOverlayRedrawMode::None);
}
else if (hasPaintCoverage && SxTabRectsIntersect(button->getManagedRepaintCoverageRect(), paintCoverage))
{
const bool persistentHit = hasPersistentCoverage &&
SxTabRectsIntersect(button->getManagedRepaintPersistentCoverageRect(), persistentCoverage);
if (persistentHit)
commitTabUnit(button, SxTabOverlayRedrawMode::RefreshSnapshot);
}
}
return;
}
onRequestRepaintAsRoot();
}
bool TabControl::hasManagedDirtySubtree() const
{
if (dirty)
return true;
for (const auto& control : controls)
{
if (control.first->IsVisible() && control.first->hasManagedDirtySubtree())
return true;
if (control.second->IsVisible() && control.second->hasManagedDirtySubtree())
return true;
}
return false;
}
RECT TabControl::getManagedRepaintCoverageRect() const
{
// TabControl 的 draw() 会写自身背景、全部页签按钮以及全部页面。
// 因此 coverage 也必须按“页签按钮 + 页面”递归并集,避免内部 Tooltip 等附加绘制区域被漏算。
RECT coverage = getBoundsRect();
for (const auto& control : controls)
{
if (control.first->IsVisible())
coverage = SxTabUnionRect(coverage, control.first->getManagedRepaintCoverageRect());
if (control.second->IsVisible())
coverage = SxTabUnionRect(coverage, control.second->getManagedRepaintCoverageRect());
}
return coverage;
}
RECT TabControl::getManagedRepaintPersistentCoverageRect() const
{
// 持久 coverage 排除页签按钮 Tooltip 等临时浮层,
// 用于判断上层页签/页面补画时是否允许刷新背景快照。
RECT coverage = getBoundsRect();
for (const auto& control : controls)
{
if (control.first->IsVisible())
coverage = SxTabUnionRect(coverage, control.first->getManagedRepaintPersistentCoverageRect());
if (control.second->IsVisible())
coverage = SxTabUnionRect(coverage, control.second->getManagedRepaintPersistentCoverageRect());
}
return coverage;
}
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();
}
-93
View File
@@ -1,93 +0,0 @@
/*******************************************************************************
* @类: TabControl
* @摘要: 选项卡容器控件,管理“页签按钮 + 对应页面(Canvas)”
* @描述:
* 提供页签栏布局(上/下/左/右)、选中切换、页内容区域定位;
* 与 Button 一起工作,支持窗口大小变化、可见性联动与脏区重绘。
* 在托管重绘模式下,TabControl 作为“页签栏 + 当前页面”的统一重绘 root。
*
* @特性:
* - 页签栏四向排列(Top / Bottom / Left / Right
* - 一键添加“页签+页”或为指定页添加子控件
* - 获取/设置当前激活页签索引
* - 自适应窗口变化,重算页签与页面区域
* - 与 Button 的 TOGGLE 模式联动显示/隐藏页面
*
* @使用场景: 在同一区域内承载多张页面,使用页签进行快速切换
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
#pragma once
#include "CoreTypes.h"
#include "Button.h"
#include "Canvas.h"
#define BUTMINHEIGHT 15 //页签按钮最小尺寸,过小会导致显示问题
#define BUTMINWIDTH 30 //页签按钮最小尺寸,过小会导致显示问题
class TabControl :public Canvas
{
int tabBarHeight = BUTMINWIDTH; //页签栏高度
bool IsFirstDraw = true; //首次绘制标记
int defaultActivation = -1; //默认激活页签索引
StellarX::TabPlacement tabPlacement = StellarX::TabPlacement::Top; //页签排列方式
std::vector<std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>> controls; //页签/页列表
private:
using Canvas::addControl; // 禁止外部误用
void addControl(std::unique_ptr<Control>) = delete; // 精准禁用该重载
private:
// 初始化页签按钮位置和尺寸
inline void initTabBar();
inline void initTabPage();
// 统一刷新 TabControl 当前运行态下的页签栏和页面区布局。
void refreshRuntimeLayout();
public:
// 默认构造:创建一个空 TabControl
TabControl();
// 指定初始位置和尺寸构造 TabControl
TabControl(int x, int y, int width, int height);
~TabControl();
//重写位置设置以适应页签和页面布局
void setX(int x)override;
void setY(int y)override;
void setWidth(int width) override;
void setHeight(int height) override;
void draw() override;
bool handleEvent(const ExMessage& msg) override;
// 只转发清理页签按钮和当前可见页面中的鼠标临时状态,不升级为整块 TabControl 重绘。
bool clearTransientMouseState() override;
// 添加一个“页签按钮 + 页面 Canvas”组合
void add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>&& control);
// 按页签文本把控件添加到对应页面
void add(std::string tabText, std::unique_ptr<Control> control);
//设置页签位置
void setTabPlacement(StellarX::TabPlacement placement);
//设置页签栏高度 两侧排列时为宽度
void setTabBarHeight(int height);
//设置不可见后传递给子控件重写
void setIsVisible(bool visible) override;
void onWindowResize() override;
//获取当前激活页签索引
int getActiveIndex() const;
// 设置当前激活页签索引;若已是当前激活页则直接返回
void setActiveIndex(int idx);
// 获取页签数量
int count() const;
// 通过页签文本查找索引
int indexOf(const std::string& tabText) const;
//设置脏区并请求重绘
void setDirty(bool dirty) override;
//请求父控件重绘
void requestRepaint(Control* parent)override; // 托管模式下登记为 root;非托管模式下局部更新脏按钮/脏页面
bool hasManagedDirtySubtree() const override;
RECT getManagedRepaintCoverageRect() const override;
RECT getManagedRepaintPersistentCoverageRect() const override;
bool canCommitManagedPartialRepaint() const override; // 判断当前 TabControl 是否可安全做局部提交
void commitManagedRepaint() override; // 托管收口阶段执行 TabControl 的真正重绘
protected:
// 外层统一解算后,TabControl 需要同步刷新其内部页签栏和页面区。
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
};
-889
View File
@@ -1,889 +0,0 @@
#include "Table.h"
#include "SxLog.h"
namespace
{
std::vector<std::string> NormalizeTableRow(std::vector<std::string> row, size_t headerCount)
{
if (headerCount == 0)
return row;
if (row.size() > headerCount)
row.resize(headerCount);
else if (row.size() < headerCount)
row.resize(headerCount, "");
return row;
}
int CalculateTotalPages(size_t rowCount, int rowsPerPage)
{
if (rowsPerPage < 1)
rowsPerPage = 1;
const int total = static_cast<int>((rowCount + static_cast<size_t>(rowsPerPage) - 1) / static_cast<size_t>(rowsPerPage));
return total > 0 ? total : 1;
}
}
// 绘制表格的当前页
// 使用双循环绘制行和列,考虑分页偏移
void Table::drawTable()
{
if (lineHeights.empty() || colWidths.empty())
return;
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
// 表体从“表头之下”开始
dX = x + border;
dY = y + border + lineHeights.at(0) + TABLE_HEADER_EXTRA; // 表头高度
uY = dY + lineHeights.at(0) + TABLE_ROW_EXTRA;
size_t startRow = (currentPage - 1) * rowsPerPage;
size_t endRow = startRow + (size_t)rowsPerPage < data.size() ? startRow + (size_t)rowsPerPage : data.size();
for (size_t i = startRow; i < endRow; ++i)
{
for (size_t j = 0; j < data[i].size(); ++j)
{
uX = dX + colWidths.at(j) + TABLE_COL_GAP;
fillrectangle(dX, dY, uX, uY);
outtextxy(dX + TABLE_PAD_X, dY + TABLE_PAD_Y, LPCTSTR(data[i][j].c_str()));
dX += colWidths.at(j) + TABLE_COL_GAP;
}
dX = x + border;
dY = uY;
uY = dY + lineHeights.at(0) + TABLE_ROW_EXTRA;
}
}
void Table::drawHeader()
{
if (headers.empty() || lineHeights.empty() || colWidths.empty())
return;
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
// 内容区原点 = x+border, y+border
dX = x + border;
dY = y + border;
uY = dY + lineHeights.at(0) + TABLE_HEADER_EXTRA;
for (size_t i = 0; i < headers.size(); i++)
{
uX = dX + colWidths.at(i) + TABLE_COL_GAP; // 注意这里是 +20,和表体一致
fillrectangle(dX, dY, uX, uY);
outtextxy(dX + TABLE_PAD_X, dY + TABLE_PAD_Y, LPCTSTR(headers[i].c_str()));
dX += colWidths.at(i) + TABLE_COL_GAP; // 列间距 20
}
}
// 遍历所有数据单元和表头,计算每列的最大宽度和每行的最大高度,
// 为后续绘制表格单元格提供尺寸依据。此计算在数据变更时自动触发。
void Table::initTextWaH()
{
// 和绘制一致的单元内边距
const int padX = TABLE_PAD_X; // 左右 padding
const int padY = TABLE_PAD_Y; // 上下 padding
const int colGap = TABLE_COL_GAP; // 列间距
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
size_t maxCols = headers.size();
for (const auto& row : data)
if (row.size() > maxCols)
maxCols = row.size();
// 统计每列最大文本宽 & 每列最大行高(包含数据 + 表头)
colWidths.assign(maxCols, 0);
lineHeights.assign(maxCols, 0);
// 先看数据
for (size_t i = 0; i < data.size(); ++i)
{
for (size_t j = 0; j < data[i].size(); ++j)
{
const int w = textwidth(LPCTSTR(data[i][j].c_str()));
const int h = textheight(LPCTSTR(data[i][j].c_str()));
if (w > colWidths[j]) colWidths[j] = w;
if (h > lineHeights[j]) lineHeights[j] = h;
}
}
// 再用表头更新(谁大取谁)
for (size_t j = 0; j < headers.size(); ++j)
{
const int w = textwidth(LPCTSTR(headers[j].c_str()));
const int h = textheight(LPCTSTR(headers[j].c_str()));
if (w > colWidths[j]) colWidths[j] = w;
if (h > lineHeights[j]) lineHeights[j] = h;
}
// 用“所有列的最大行高”作为一行的基准高度
int maxLineH = 0;
for (int h : lineHeights)
if (h > maxLineH)
maxLineH = h;
if (maxLineH == 0)
maxLineH = textheight(LPCTSTR("A"));
if (rowsPerPage < 1)
rowsPerPage = 1;
// 列宽包含左右 padding:在计算完最大文本宽度后,加上 2*padX 作为单元格内边距
for (size_t j = 0; j < colWidths.size(); ++j) {
colWidths[j] += 2 * padX;
}
// 表内容总宽 = Σ(列宽 + 列间距)
int contentW = 0;
for (size_t j = 0; j < colWidths.size(); ++j)
contentW += colWidths[j] + colGap;
// 表头高 & 行高(与 drawHeader/drawTable 内部一致:+上下 padding
const int headerH = maxLineH + 2 * padY;
const int rowH = maxLineH + 2 * padY;
const int rowsH = rowH * rowsPerPage;
// 页脚:
const int pageTextH = textheight(LPCTSTR(pageNumtext.c_str()));
const int btnTextH = textheight(LPCTSTR("上一页"));
const int btnPadV = TABLE_BTN_TEXT_PAD_V;
const int btnH = btnTextH + 2 * btnPadV;
const int footerPad = TABLE_FOOTER_PAD;
const int footerH = (pageTextH > btnH ? pageTextH : btnH) + footerPad;
// 最终表宽/高:内容 + 对称边框
this->width = contentW + (border << 1);
this->height = headerH + rowsH + footerH + (border << 1);
// 这是 Table 自身受控的“结构尺寸 -> 设计基线”刷新点:
// 表格总宽高本来就由表头、表体、页脚和分页按钮共同决定,
// 因此这里允许同步 localWidth/localHeight,作为后续锚点解算参考。
const bool baselineChanged = (this->localWidth != this->width) || (this->localHeight != this->height);
this->localWidth = this->width;
this->localHeight = this->height;
if (baselineChanged)
{
SX_LOGD("Layout")
<< SX_T("Table 刷新结构设计基线:id=", "Table refresh structural design rect: id=") << id
<< SX_T(" size=(", " size=(") << this->width << "," << this->height << ")";
}
}
void Table::initButton()
{
const int gap = TABLE_BTN_GAP;
const int padH = TABLE_BTN_PAD_H;
const int padV = TABLE_BTN_PAD_V; // 按钮垂直内边距
int pageW = textwidth(LPCTSTR(pageNumtext.c_str()));
int lblH = textheight(LPCTSTR(pageNumtext.c_str()));
// 统一按钮尺寸(用按钮文字自身宽高 + padding)
int prevW = textwidth(LPCTSTR(TABLE_STR_PREV)) + padH * 2;
int nextW = textwidth(LPCTSTR(TABLE_STR_NEXT)) + padH * 2;
int btnH = lblH + padV * 2;
// 基于“页码标签”的矩形来摆放:
// prev 在页码左侧 gap 处;next 在右侧 gap 处;Y 对齐 pY
int prevX = pX - gap - prevW;
int nextX = pX + pageW + gap;
int btnY = pY; // 和页码同一基线
if (!prevButton)
prevButton = std::make_unique<Button>(prevX, btnY, prevW, btnH, TABLE_STR_PREV, RGB(0, 0, 0), RGB(255, 255, 255));
else
{
prevButton->setX(prevX);
prevButton->setY(btnY);
}
if (!nextButton)
nextButton = std::make_unique<Button>(nextX, btnY, nextW, btnH, TABLE_STR_NEXT, RGB(0, 0, 0), RGB(255, 255, 255));
else
{
nextButton->setX(nextX);
nextButton->setY(btnY);
}
prevButton->setParent(this);
nextButton->setParent(this);
prevButton->textStyle = this->textStyle;
nextButton->textStyle = this->textStyle;
prevButton->setFillMode(tableFillMode);
nextButton->setFillMode(tableFillMode);
prevButton->setOnClickListener([this]()
{
int oldPage = currentPage;
if (currentPage > 1)
{
--currentPage;
SX_LOGI("Table")
<< SX_T("翻页:id=", "page change: id=") << id
<< " " << oldPage << "->" << currentPage
<< SX_T(" 总页数=", " total=") << totalPages
<< SX_T(" 行数=", " rows=") << (int)data.size();
dirty = true;
if (pageNum) pageNum->setDirty(true);
}
});
nextButton->setOnClickListener([this]()
{
int oldPage = currentPage;
if (currentPage < totalPages)
{
++currentPage;
SX_LOGI("Table")
<< SX_T("翻页:id=", "page change: id=") << id
<< " " << oldPage << "->" << currentPage
<< SX_T(" 总页数=", " total=") << totalPages
<< SX_T(" 行数=", " rows=") << (int)data.size();
dirty = true;
if (pageNum) pageNum->setDirty(true);
}
});
isNeedButtonAndPageNum = false;
}
void Table::initPageNum()
{
// 统一坐标系
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
const int baseH = lineHeights.empty() ? 0 : lineHeights.at(0);
const int headerH = baseH + TABLE_HEADER_EXTRA;
const int rowsH = baseH * rowsPerPage + rowsPerPage * TABLE_ROW_EXTRA;
// 内容宽度 = sum(colWidths + 20)initTextWaH() 已把 this->width += 2*border
// 因此 contentW = this->width - 2*border 更稳妥
const int contentW = this->width - (border << 1);
// 页脚顶部位置(表头 + 可视数据区 之后)
pY = y + border + headerH + rowsH + TABLE_FOOTER_BLANK; // +8 顶部留白
// 按理来说 x + (this->width - textW) / 2;就可以
// 但是在绘制时,发现控件偏右,因此减去40
int textW = textwidth(LPCTSTR(pageNumtext.c_str()));
pX = x + TABLE_PAGE_TEXT_OFFSET_X + (this->width - textW) / 2;
if (!pageNum)
pageNum = std::make_unique<Label>(pX, pY, pageNumtext);
else
{
pageNum->setX(pX);
pageNum->setY(pY);
}
pageNum->setParent(this);
pageNum->textStyle = this->textStyle;
if (StellarX::FillMode::Null == tableFillMode)
pageNum->setTextdisap(true); // 透明文本
}
void Table::drawPageNum()
{
pageNumtext = "";
pageNumtext += std::to_string(currentPage);
pageNumtext += "页/共";
pageNumtext += std::to_string(totalPages);
pageNumtext += "";
if (nullptr == pageNum || isNeedButtonAndPageNum)
initPageNum();
pageNum->setText(pageNumtext);
pageNum->textStyle = this->textStyle;
if (StellarX::FillMode::Null == tableFillMode)
pageNum->setTextdisap(true);
// Table 每次整表重绘前都会先回贴自己的背景快照,
// 页码区域会被一起清空。即便页码文本本身没变化,也必须强制把 Label 重新设脏,
// 否则 Label::draw() 会因为 dirty=false 直接跳过,导致页码在表格重绘后消失。
pageNum->setDirty(true);
pageNum->draw();
}
void Table::drawButton()
{
if ((nullptr == prevButton || nullptr == nextButton) || isNeedButtonAndPageNum)
initButton();
this->prevButton->textStyle = this->textStyle;
this->nextButton->textStyle = this->textStyle;
this->prevButton->setFillMode(tableFillMode);
this->nextButton->setFillMode(tableFillMode);
this->prevButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
this->nextButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
this->prevButton->setDirty(true);
this->nextButton->setDirty(true);
prevButton->draw();
nextButton->draw();
}
void Table::setX(int x)
{
this->x = x;
isNeedButtonAndPageNum = true;
dirty = true;
}
void Table::setY(int y)
{
this->y = y;
isNeedButtonAndPageNum = true;
dirty = true;
}
void Table::setWidth(int width)
{
// 调整列宽以匹配新的表格总宽度。不修改 localWidth,避免累计误差。
// 当 width 与当前 width 不同时,根据差值平均分配到各列,余数依次累加/扣减。
const int ncols = static_cast<int>(colWidths.size());
if (ncols <= 0) {
this->width = width;
isNeedButtonAndPageNum = true;
return;
}
int diff = width - this->width;
// 基础增量:整除部分
int baseChange = diff / ncols;
int remainder = diff % ncols;
for (int i = 0; i < ncols; ++i) {
int change = baseChange;
if (remainder > 0) {
change += 1;
remainder -= 1;
}
else if (remainder < 0) {
change -= 1;
remainder += 1;
}
int newWidth = colWidths[i] + change;
// 限制最小宽度为 1,防止出现负值
if (newWidth < 1) newWidth = 1;
colWidths[i] = newWidth;
}
this->width = width;
// 需要重新布局页脚元素
isNeedButtonAndPageNum = true;
}
void Table::setHeight(int height)
{
// 当前阶段 Table 明确不支持纵向 Stretch。
// 高度链路依赖表头、表体、页脚、按钮和页码计算,
// 因此这里保持空实现,避免被通用布局层错误拉高/压缩。
}
Table::Table(int x, int y)
:Control(x, y, 0, 0)
{
this->id = "Table";
// Table 当前正式能力边界:
// 仅允许 X 轴 StretchY 轴固定尺寸。
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = false;
}
Table::~Table() = default;
void Table::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
{
// Table 不能像普通控件那样直接写入 width/height
// 它的 setWidth() 内部会联动列宽与页脚布局,因此这里必须复用原有 setter 副作用。
// 同时 Table 的 Y Fixed 是当前版本的正式行为边界:
// 即便统一解算器给出了新的 rect.height,这里也不会接收该高度。
if (this->x != rect.worldX)
setX(rect.worldX);
if (this->y != rect.worldY)
setY(rect.worldY);
if (this->width != rect.width)
setWidth(rect.width);
if (this->height != rect.height)
{
SX_LOGD("Layout")
<< SX_T("Table 忽略 Y 轴运行态高度写入:id=", "Table ignore runtime Y resize: id=")
<< id
<< SX_T(" currentH=", " currentH=") << this->height
<< SX_T(" requestedH=", " requestedH=") << rect.height;
}
dirty = true;
}
void Table::draw()
{
//在这里先初始化保证翻页按钮不为空
// 在一些容器中,Table不会被立即绘制可能导致事件事件传递时触发空指针警报
// 由于单元格初始化依赖字体数据所以先设置一次字体样式
// 先保存当前绘图状态
saveStyle();
// 设置表格样式
setfillcolor(tableBkClor);
setlinecolor(tableBorderClor);
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
settextcolor(textStyle.color);
setlinestyle((int)tableLineStyle, tableBorderWidth);
setfillstyle((int)tableFillMode);
// 是否需要计算单元格尺寸
if (isNeedCellSize)
{
initTextWaH();
isNeedCellSize = false;
}
restoreStyle();
if (this->dirty && this->show)
{
// 先保存当前绘图状态
saveStyle();
// 设置表格样式
setfillcolor(tableBkClor);
setlinecolor(tableBorderClor);
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
settextcolor(textStyle.color);
setlinestyle((int)tableLineStyle, tableBorderWidth);
setfillstyle((int)tableFillMode);
setbkmode(TRANSPARENT);
if (isNeedDrawHeaders)
{
// 重新设置表格样式
setfillcolor(tableBkClor);
setlinecolor(tableBorderClor);
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
settextcolor(textStyle.color);
setlinestyle((int)tableLineStyle, tableBorderWidth);
setfillstyle((int)tableFillMode);
setbkmode(TRANSPARENT);
}
// 在绘制前先恢复并更新背景快照:
// 如果已有快照且尺寸发生变化,先恢复旧快照以清除上一次绘制,然后丢弃旧快照再重新抓取新的区域。
if (hasSnap)
{
// 始终先恢复旧背景,清除上一帧内容
restBackground();
// 当尺寸变化或缓存图像无效时,需要重新截图
if (!saveBkImage || saveWidth != this->width || saveHeight != this->height)
{
invalidateBackgroundSnapshot();
saveBackground(this->x, this->y, this->width, this->height);
}
}
else
{
// 首次绘制时无背景缓存,直接抓取
saveBackground(this->x, this->y, this->width, this->height);
}
// 恢复最新的背景,保证绘制区域干净
restBackground();
// 绘制表头
//if (!headers.empty())
drawHeader();
// 绘制当前页
drawTable();
// 绘制页码标签
drawPageNum();
// 绘制翻页按钮
if (this->isShowPageButton)
drawButton();
// 恢复绘图状态
restoreStyle();
dirty = false; // 标记不需要重绘
}
}
bool Table::handleEvent(const ExMessage& msg)
{
if (!show)return false;
resetEventVisualChanged();
bool consume = false;
bool anyVisualChanged = false;
if (!this->isShowPageButton)
return consume;
if (msg.message == WM_MOUSEMOVE)
{
// Table 内部分页按钮也按“后绘制者优先”的倒序语义处理。
// drawButton() 里是 prev 再 next,因此这里先 next 再 prev。
if (nextButton)
{
if (!consume)
{
consume = nextButton->handleEvent(msg);
if (nextButton->didEventAffectVisual())
anyVisualChanged = true;
}
else if (nextButton->clearTransientMouseState())
{
anyVisualChanged = true;
}
}
if (prevButton)
{
if (!consume)
{
consume = prevButton->handleEvent(msg);
if (prevButton->didEventAffectVisual())
anyVisualChanged = true;
}
else if (prevButton->clearTransientMouseState())
{
anyVisualChanged = true;
}
}
}
else
{
// 点击类消息仍保持“后绘制者优先”的倒序命中语义。
// 但和 WM_MOUSEMOVE 一样,分页按钮的视觉状态变化必须向上提升为 Table 重绘,
// 否则按钮内部已经变成 hover/click,父级却不知道需要重画整张 Table,
// 就会出现“状态实际变了,但屏幕上直到下一次翻页才看见”的现象。
if (nextButton)
{
consume = nextButton->handleEvent(msg);
if (nextButton->didEventAffectVisual())
anyVisualChanged = true;
}
if (prevButton && !consume)
{
consume = prevButton->handleEvent(msg);
if (prevButton->didEventAffectVisual())
anyVisualChanged = true;
}
}
// Table 当前还没有像 TabControl 那样把分页按钮做成可独立提交的内部绘制单元。
// 因此只要内部按钮的 hover / click / leave 造成视觉变化,就必须把整张 Table 设脏,
// 让父容器通过现有的 Table::draw() 链把页脚区域正确重画出来。
if (anyVisualChanged)
dirty = true;
if (dirty)
requestRepaint(parent);
markEventVisualChanged(anyVisualChanged || dirty);
return consume;
}
bool Table::clearTransientMouseState()
{
bool changed = false;
if (nextButton && nextButton->clearTransientMouseState())
changed = true;
if (prevButton && prevButton->clearTransientMouseState())
changed = true;
return changed;
}
RECT Table::getManagedRepaintCoverageRect() const
{
RECT coverage = getBoundsRect();
if (pageNum && pageNum->IsVisible())
{
const RECT pageRect = pageNum->getManagedRepaintCoverageRect();
coverage.left = (std::min)(coverage.left, pageRect.left);
coverage.top = (std::min)(coverage.top, pageRect.top);
coverage.right = (std::max)(coverage.right, pageRect.right);
coverage.bottom = (std::max)(coverage.bottom, pageRect.bottom);
}
if (prevButton && prevButton->IsVisible())
{
const RECT prevRect = prevButton->getManagedRepaintCoverageRect();
coverage.left = (std::min)(coverage.left, prevRect.left);
coverage.top = (std::min)(coverage.top, prevRect.top);
coverage.right = (std::max)(coverage.right, prevRect.right);
coverage.bottom = (std::max)(coverage.bottom, prevRect.bottom);
}
if (nextButton && nextButton->IsVisible())
{
const RECT nextRect = nextButton->getManagedRepaintCoverageRect();
coverage.left = (std::min)(coverage.left, nextRect.left);
coverage.top = (std::min)(coverage.top, nextRect.top);
coverage.right = (std::max)(coverage.right, nextRect.right);
coverage.bottom = (std::max)(coverage.bottom, nextRect.bottom);
}
return coverage;
}
RECT Table::getManagedRepaintPersistentCoverageRect() const
{
// Table 当前仍按整表绘制保证正确性;分页按钮 Tooltip 属于临时浮层,
// 不能进入持久 coverage,否则可能污染外层兄弟控件快照。
RECT coverage = getBoundsRect();
if (pageNum && pageNum->IsVisible())
{
const RECT pageRect = pageNum->getManagedRepaintPersistentCoverageRect();
coverage.left = (std::min)(coverage.left, pageRect.left);
coverage.top = (std::min)(coverage.top, pageRect.top);
coverage.right = (std::max)(coverage.right, pageRect.right);
coverage.bottom = (std::max)(coverage.bottom, pageRect.bottom);
}
if (prevButton && prevButton->IsVisible())
{
const RECT prevRect = prevButton->getManagedRepaintPersistentCoverageRect();
coverage.left = (std::min)(coverage.left, prevRect.left);
coverage.top = (std::min)(coverage.top, prevRect.top);
coverage.right = (std::max)(coverage.right, prevRect.right);
coverage.bottom = (std::max)(coverage.bottom, prevRect.bottom);
}
if (nextButton && nextButton->IsVisible())
{
const RECT nextRect = nextButton->getManagedRepaintPersistentCoverageRect();
coverage.left = (std::min)(coverage.left, nextRect.left);
coverage.top = (std::min)(coverage.top, nextRect.top);
coverage.right = (std::max)(coverage.right, nextRect.right);
coverage.bottom = (std::max)(coverage.bottom, nextRect.bottom);
}
return coverage;
}
void Table::setHeaders(std::initializer_list<std::string> headers)
{
this->headers.clear();
for (auto& lis : headers)
this->headers.push_back(lis);
if (!this->headers.empty())
{
for (auto& row : this->data)
row = NormalizeTableRow(std::move(row), this->headers.size());
}
totalPages = CalculateTotalPages(this->data.size(), rowsPerPage);
if (currentPage > totalPages)
currentPage = totalPages;
SX_LOGI("Table") << SX_T("设置表头:id=","setHeaders: id=") << id << SX_T("总数="," count=") << (int)this->headers.size();
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
isNeedDrawHeaders = true; // 标记需要重新绘制表头
isNeedButtonAndPageNum = true;
dirty = true;
}
void Table::setData(std::vector<std::string> data)
{
data = NormalizeTableRow(std::move(data), headers.size());
this->data.push_back(data);
totalPages = CalculateTotalPages(this->data.size(), rowsPerPage);
if (currentPage > totalPages)
currentPage = totalPages;
isNeedCellSize = true;
isNeedButtonAndPageNum = true;
dirty = true;
SX_LOGI("Table")
<< SX_T("新增Dataid=", "appendRow: id=") << id
<< SX_T(" 本行列数=", " cols=") << (int)data.size()
<< SX_T(" 数据总行数=", " totalRows=") << (int)this->data.size()
<< SX_T(" 总页数=", " totalPages=") << totalPages;
}
void Table::setData(std::initializer_list<std::vector<std::string>> data)
{
int addedRows = 0;
for (auto row : data)
{
this->data.push_back(NormalizeTableRow(std::move(row), headers.size()));
++addedRows;
}
totalPages = CalculateTotalPages(this->data.size(), rowsPerPage);
if (currentPage > totalPages)
currentPage = totalPages;
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
isNeedButtonAndPageNum = true;
dirty = true;
SX_LOGI("Table")
<< SX_T("新增多行Dataid=", "appendRows: id=") << id
<< SX_T(" 新增行数=", " addedRows=") << addedRows
<< SX_T(" 数据总行数=", " totalRows=") << (int)this->data.size()
<< SX_T(" 总页数=", " totalPages=") << totalPages;
}
void Table::setRowsPerPage(int rows)
{
this->rowsPerPage = rows;
if (this->rowsPerPage < 1)
this->rowsPerPage = 1;
totalPages = CalculateTotalPages(data.size(), rowsPerPage);
if (currentPage > totalPages)
currentPage = totalPages;
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
isNeedButtonAndPageNum = true;
dirty = true;
}
void Table::showPageButton(bool isShow)
{
this->isShowPageButton = isShow;
this->dirty = true;
}
void Table::setTableBorder(COLORREF color)
{
this->tableBorderClor = color;
this->dirty = true;
}
void Table::setTableBk(COLORREF color)
{
this->tableBkClor = color;
this->dirty = true;
}
void Table::setTableFillMode(StellarX::FillMode mode)
{
if (StellarX::FillMode::Solid == mode || StellarX::FillMode::Null == mode)
this->tableFillMode = mode;
else
this->tableFillMode = StellarX::FillMode::Solid;
if (this->prevButton && this->nextButton && this->pageNum)
{
this->prevButton->textStyle = this->textStyle;
this->nextButton->textStyle = this->textStyle;
this->prevButton->setFillMode(tableFillMode);
this->nextButton->setFillMode(tableFillMode);
if (StellarX::FillMode::Null == tableFillMode)
pageNum->setTextdisap(true);
this->prevButton->setDirty(true);
this->nextButton->setDirty(true);
}
this->dirty = true;
}
void Table::setTableLineStyle(StellarX::LineStyle style)
{
this->tableLineStyle = style;
this->dirty = true;
}
void Table::setTableBorderWidth(int width)
{
this->tableBorderWidth = width;
this->dirty = true;
}
void Table::clearHeaders()
{
this->headers.clear();
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
isNeedDrawHeaders = true; // 标记需要重新绘制表头
isNeedButtonAndPageNum = true;// 标记需要重新计算翻页按钮和页码信息
dirty = true;
SX_LOGI("Table") << SX_T("清除表头:id=","clearHeaders: id=" )<< id;
}
void Table::clearData()
{
this->data.clear();
this->currentPage = 1;
this->totalPages = CalculateTotalPages(this->data.size(), rowsPerPage);
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
isNeedButtonAndPageNum = true;// 标记需要重新计算翻页按钮和页码信息
dirty = true;
SX_LOGI("Table") << SX_T("清除表格数据:id=","clearData: id=") << id;
}
void Table::resetTable()
{
clearHeaders();
clearData();
}
void Table::onWindowResize()
{
Control::onWindowResize(); // 先处理自己
if (this->prevButton && this->nextButton && this->pageNum)
{
prevButton->onWindowResize();
nextButton->onWindowResize();
pageNum->onWindowResize();
}
}
int Table::getCurrentPage() const
{
return this->currentPage;
}
int Table::getTotalPages() const
{
return this->totalPages;
}
int Table::getRowsPerPage() const
{
return this->rowsPerPage;
}
bool Table::getShowPageButton() const
{
return this->isShowPageButton;
}
COLORREF Table::getTableBorder() const
{
return this->tableBorderClor;
}
COLORREF Table::getTableBk() const
{
return this->tableBkClor;
}
StellarX::FillMode Table::getTableFillMode() const
{
return this->tableFillMode;
}
StellarX::LineStyle Table::getTableLineStyle() const
{
return this->tableLineStyle;
}
std::vector<std::string> Table::getHeaders() const
{
return this->headers;
}
std::vector<std::vector<std::string>> Table::getData() const
{
return this->data;
}
int Table::getTableBorderWidth() const
{
return this->tableBorderWidth;
}
int Table::getTableWidth() const
{
int temp = 0;
for (auto& w : colWidths)
temp += w;
return temp;
}
int Table::getTableHeight() const
{
return this->height;
}
-344
View File
@@ -1,344 +0,0 @@
// TextBox.cpp
#include "TextBox.h"
#include "SxLog.h"
namespace
{
int SxTextBoxMbcCharLen(const std::string& s, size_t i)
{
unsigned char b = static_cast<unsigned char>(s[i]);
if (b <= 0x7F)
return 1;
if (b >= 0x81 && b <= 0xFE && i + 1 < s.size())
{
unsigned char b2 = static_cast<unsigned char>(s[i + 1]);
if (b2 >= 0x40 && b2 <= 0xFE && b2 != 0x7F)
return 2;
}
// 非法或不完整字节按单字节容错,避免死循环。
return 1;
}
void SxTextBoxTrimTrailingSpaces(std::string& s)
{
while (!s.empty() && s.back() == ' ')
s.pop_back();
while (s.size() >= 2)
{
unsigned char a = static_cast<unsigned char>(s[s.size() - 2]);
unsigned char b = static_cast<unsigned char>(s[s.size() - 1]);
if (a == 0xA1 && b == 0xA1)
s.resize(s.size() - 2);
else
break;
}
}
std::string SxTextBoxTruncateByMbcBoundary(const std::string& text, size_t maxBytes)
{
size_t i = 0;
size_t lastSafe = 0;
while (i < text.size())
{
const int charLen = SxTextBoxMbcCharLen(text, i);
const size_t next = i + static_cast<size_t>(charLen);
if (next > maxBytes || next > text.size())
break;
lastSafe = next;
i = next;
}
return text.substr(0, lastSafe);
}
std::string SxTextBoxEllipsizeMbc(const std::string& text, int maxWidth)
{
if (maxWidth <= 0)
return "";
if (textwidth(LPCTSTR(text.c_str())) <= maxWidth)
return text;
const std::string ellipsis = "...";
const int ellipsisWidth = textwidth(LPCTSTR(ellipsis.c_str()));
if (ellipsisWidth > maxWidth)
{
std::string clippedEllipsis = ellipsis;
while (!clippedEllipsis.empty() && textwidth(LPCTSTR(clippedEllipsis.c_str())) > maxWidth)
clippedEllipsis.pop_back();
return clippedEllipsis;
}
const int contentLimit = maxWidth - ellipsisWidth;
size_t i = 0;
size_t lastFit = 0;
while (i < text.size())
{
const int charLen = SxTextBoxMbcCharLen(text, i);
const size_t next = i + static_cast<size_t>(charLen);
if (next > text.size())
break;
const std::string candidate = text.substr(0, next);
if (textwidth(LPCTSTR(candidate.c_str())) <= contentLimit)
{
lastFit = next;
i = next;
}
else
{
break;
}
}
if (lastFit == 0)
return ellipsis;
std::string head = text.substr(0, lastFit);
SxTextBoxTrimTrailingSpaces(head);
return head + ellipsis;
}
}
TextBox::TextBox(int x, int y, int width, int height, std::string text, StellarX::TextBoxmode mode, StellarX::ControlShape shape)
:Control(x, y, width, height), text(text), mode(mode), shape(shape)
{
this->id = "TextBox";
// TextBox 当前阶段的默认布局策略:
// - 水平方向允许通过锚点语义拉伸
// - 垂直方向当前实现保持固定尺寸,不在本轮引入随高度变化的视觉自适应
this->layoutCapability.allowStretchX = true;
this->layoutCapability.allowStretchY = false;
}
void TextBox::draw()
{
if (dirty && show)
{
saveStyle();
setfillcolor(textBoxBkClor);
setlinecolor(textBoxBorderClor);
StellarX::ControlText drawStyle = textStyle;
if (drawStyle.nHeight > height)
drawStyle.nHeight = height;
if (drawStyle.nWidth > width)
drawStyle.nWidth = width;
settextstyle(drawStyle.nHeight, drawStyle.nWidth, drawStyle.lpszFace,
drawStyle.nEscapement, drawStyle.nOrientation, drawStyle.nWeight,
drawStyle.bItalic, drawStyle.bUnderline, drawStyle.bStrikeOut);
settextcolor(drawStyle.color);
setbkmode(TRANSPARENT);
int text_width = 0;
int text_height = 0;
std::string pwdText;
std::string displayText; // 用于显示的文本(可能被截断)
if (StellarX::TextBoxmode::PASSWORD_MODE == mode)
{
for (size_t i = 0; i < text.size(); ++i)
pwdText += '*';
displayText = pwdText;
}
else
{
displayText = text;
}
// 计算可用宽度(留出左右边距)
int availableWidth = width - 20; // 左右各10像素边距
// 截断文本以适应可用宽度
int currentWidth = textwidth(LPCTSTR(displayText.c_str()));
if (currentWidth > availableWidth && availableWidth > 0)
{
// 按 GBK/MBCS 字符边界裁切,避免中文文本被截断到半个字节。
displayText = SxTextBoxEllipsizeMbc(displayText, availableWidth);
currentWidth = textwidth(LPCTSTR(displayText.c_str()));
}
text_width = currentWidth;
text_height = textheight(LPCTSTR(displayText.c_str()));
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage)
saveBackground(this->x, this->y, this->width, this->height);
// 恢复背景(清除旧内容)
restBackground();
//根据形状绘制
switch (shape)
{
case StellarX::ControlShape::RECTANGLE:
fillrectangle(x, y, x + width, y + height);//有边框填充矩形
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str()));
break;
case StellarX::ControlShape::B_RECTANGLE:
solidrectangle(x, y, x + width, y + height);//无边框填充矩形
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str()));
break;
case StellarX::ControlShape::ROUND_RECTANGLE:
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//有边框填充圆角矩形
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str()));
break;
case StellarX::ControlShape::B_ROUND_RECTANGLE:
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//无边框填充圆角矩形
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str()));
break;
}
restoreStyle();
dirty = false; //标记不需要重绘
}
}
bool TextBox::handleEvent(const ExMessage& msg)
{
if (!show) return false;
resetEventVisualChanged();
bool hover = false;
bool oldClick = click;
bool consume = false;
const bool isMouseMessage =
msg.message == WM_MOUSEMOVE ||
msg.message == WM_LBUTTONDOWN ||
msg.message == WM_LBUTTONUP;
if (isMouseMessage)
{
switch (shape)
{
case StellarX::ControlShape::RECTANGLE:
case StellarX::ControlShape::B_RECTANGLE:
case StellarX::ControlShape::ROUND_RECTANGLE:
case StellarX::ControlShape::B_ROUND_RECTANGLE:
hover = (msg.x > x && msg.x < (x + width) && msg.y > y && msg.y < (y + height));
break;
default:
break;
}
}
if (hover && msg.message == WM_LBUTTONUP)
{
click = true;
const size_t oldLen = text.size();
SX_LOGI("TextBox") << SX_T("激活:id=","activate: id=") << id << " mode=" << (int)mode << " oldLen=" << oldLen;
if (StellarX::TextBoxmode::INPUT_MODE == mode)
{
std::vector<char> temp(maxCharLen + 1, '\0');
dirty = InputBox(temp.data(), (int)maxCharLen + 1, "输入框", NULL, text.c_str(), NULL, NULL, false);
if (dirty) text = temp.data();
consume = true;
}
else if (StellarX::TextBoxmode::READONLY_MODE == mode)
{
dirty = false;
InputBox(NULL, (int)maxCharLen, "输出框(输入无效!)", NULL, text.c_str(), NULL, NULL, false);
consume = true;
}
else if (StellarX::TextBoxmode::PASSWORD_MODE == mode)
{
std::vector<char> temp(maxCharLen + 1, '\0');
// 不记录明文,只记录长度变化
dirty = InputBox(temp.data(), (int)maxCharLen + 1, "输入框\n不可见输入,覆盖即可", NULL, NULL, NULL, NULL, false);
if (dirty) text = temp.data();
consume = true;
}
if (dirty)
{
SX_LOGI("TextBox") << SX_T("文本已更改: id=","text changed: id=") << id
<< " oldLen=" << oldLen << " newLen=" << text.size();
}
else
{
SX_LOGD("TextBox") << SX_T("文本无变化:id=","no change: id=") << id;
}
}
if (dirty)
requestRepaint(parent);
markEventVisualChanged(dirty);
if (click)
click = false;
return consume;
}
void TextBox::setMode(StellarX::TextBoxmode mode)
{
this->mode = mode;
this->dirty = true;
}
void TextBox::setMaxCharLen(size_t len)
{
if (len > 0)
maxCharLen = len;
this->dirty = true;
}
void TextBox::setTextBoxshape(StellarX::ControlShape shape)
{
switch (shape)
{
case StellarX::ControlShape::RECTANGLE:
case StellarX::ControlShape::B_RECTANGLE:
case StellarX::ControlShape::ROUND_RECTANGLE:
case StellarX::ControlShape::B_ROUND_RECTANGLE:
this->shape = shape;
this->dirty = true;
break;
case StellarX::ControlShape::CIRCLE:
case StellarX::ControlShape::B_CIRCLE:
case StellarX::ControlShape::ELLIPSE:
case StellarX::ControlShape::B_ELLIPSE:
this->shape = StellarX::ControlShape::RECTANGLE;
this->dirty = true;
break;
}
}
void TextBox::setTextBoxBorder(COLORREF color)
{
textBoxBorderClor = color;
this->dirty = true;
}
void TextBox::setTextBoxBk(COLORREF color)
{
textBoxBkClor = color;
this->dirty = true;
}
void TextBox::setText(std::string text)
{
if(text == this->text)
return; // 文本未改变,无需更新和重绘
if (text.size() > maxCharLen)
text = SxTextBoxTruncateByMbcBoundary(text, maxCharLen);
this->text = text;
this->dirty = true; // 标记需要重绘,不论是否窗口图形上下文是否已初始化,等第一次绘制时由窗口真正调用 draw() 来重绘显示文本
//有父控件时请求父控件重绘,无父控件时直接重绘,确保文本更新后界面正确刷新显示
if (nullptr != parent)
{
//通过hasSnap是否持有有效快照,判断控件是否已经绘制过,避免在控件未绘制前/窗口图形上下文未初始化调用draw()导致的错误
if (hasSnap)
requestRepaint(parent);
}
else
if (hasSnap)
draw();
}
std::string TextBox::getText() const
{
return this->text;
}
-59
View File
@@ -1,59 +0,0 @@
/*******************************************************************************
* @类: TextBox
* @摘要: 文本框控件,支持输入和只读两种模式
* @描述:
* 提供文本输入和显示功能,集成EasyX的InputBox用于数据输入。
* 支持有限的形状样式和视觉定制。
*
* @特性:
* - 两种工作模式:输入模式和只读模式
* - 最大字符长度限制
* - 集成系统输入框简化文本输入
* - 支持四种矩形形状变体
*
* @使用场景: 数据输入、文本显示、表单字段等
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
#pragma once
#include "Control.h"
class TextBox : public Control
{
std::string text; //文本
StellarX::TextBoxmode mode; //模式
StellarX::ControlShape shape; //形状
bool click = false; //是否点击
size_t maxCharLen = 10;//最大字符长度
COLORREF textBoxBkClor = RGB(255, 255, 255); //背景颜色
COLORREF textBoxBorderClor = RGB(0, 0, 0); //边框颜色
public:
StellarX::ControlText textStyle; //文本样式
// 构造文本框;支持输入模式和只读模式
TextBox(int x, int y, int width, int height, std::string text = "", StellarX::TextBoxmode mode = StellarX::TextBoxmode::INPUT_MODE, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
// 绘制文本框
void draw() override;
// 处理输入框鼠标交互和文本录入
bool handleEvent(const ExMessage& msg) override;
// 设置文本框模式
void setMode(StellarX::TextBoxmode mode);
// 设置可输入最大长度;当前按 std::string 字节长度限制,不等同于 Unicode 字符数
void setMaxCharLen(size_t len);
// 设置文本框形状;仅支持矩形/圆角矩形,圆形和椭圆会回退为 RECTANGLE
void setTextBoxshape(StellarX::ControlShape shape);
// 设置边框颜色
void setTextBoxBorder(COLORREF color);
// 设置背景颜色
void setTextBoxBk(COLORREF color);
// 设置当前文本
void setText(std::string text);
// 获取当前文本
std::string getText() const;
private:
//用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; };
};
-1261
View File
File diff suppressed because it is too large Load Diff
-144
View File
@@ -1,144 +0,0 @@
/**
* Window(头文件)
*
* 设计目标:
* - 提供一个基于 Win32 + EasyX 的“可拉伸且稳定不抖”的窗口容器。
* - 通过消息过程子类化(WndProcThunk)接管关键消息(WM_SIZING/WM_SIZE/...)。
* - 将“几何变化记录(pendingW/H)”与“统一收口重绘(needResizeDirty)”解耦。
* - 在事件分发阶段只改状态并登记重绘 root,在事件尾统一提交,减少分离绘制造成的闪烁。
*
* 关键点(与 .cpp 中实现对应):
* - isSizing:处于交互拉伸阶段时,冻结重绘;松手后统一重绘,防止抖动。
* - WM_SIZING:只做“最小尺寸夹紧”,不回滚矩形、不做对齐;把其余交给系统。
* - WM_GETMINMAXINFO:按最小“客户区”换算到“窗口矩形”,提供系统层最小轨迹值。
* - runEventLoop:输入事件先分发给对话框/控件,控件只登记重绘请求;真正绘制在分发或 resize 收口时统一处理。
*/
//fuck windows
//fuck win32
//fuck xiaomi
#pragma once
#include "Control.h"
#include <string>
#include <vector>
#include <memory>
#include <windows.h>
class Window
{
// —— 尺寸状态 ——(绘制尺寸与待应用尺寸分离;收口时一次性更新)
int width; // 当前有效宽(已应用到画布/控件的客户区宽)
int height; // 当前有效高(已应用到画布/控件的客户区高)
int localwidth; // 基准宽(创建时的宽度)
int localheight; // 基准高(创建是的高度)
int pendingW; // 待应用宽(WM_SIZE/拉伸中记录用)
int pendingH; // 待应用高
int minClientW; // 业务设定的最小客户区宽(用于 GETMINMAXINFO 与 SIZING 夹紧)
int minClientH; // 业务设定的最小客户区高
int windowMode = NULL; // EasyX 初始化模式(EX_SHOWCONSOLE/EX_TOPMOST/...
bool needResizeDirty = false; // 统一收口重绘标志(置位后在事件环末尾处理)
bool isSizing = false; // 是否处于拖拽阶段(ENTER/EXIT SIZEMOVE 切换)
// —— 原生窗口句柄与子类化钩子 ——(子类化 EasyX 的窗口过程以拦截关键消息)
HWND hWnd = NULL; // EasyX 初始化后的窗口句柄
WNDPROC oldWndProc = nullptr; // 保存旧过程(CallWindowProc 回落)
bool procHooked = false; // 避免重复子类化
static LRESULT CALLBACK WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l); // 静态过程分发到 this
// —— 绘制相关 ——(是否使用合成双缓冲、窗口标题、背景等)
bool useComposited = true; // 是否启用 WS_EX_COMPOSITED(部分机器可能增加一帧观感延迟)
std::string headline; // 窗口标题文本
COLORREF wBkcolor = BLACK; // 纯色背景(无背景图时使用)
std::unique_ptr<IMAGE> background; // 背景图对象指针(存在时优先绘制)
std::string bkImageFile; // 背景图文件路径(loadimage 用)
// —— 控件/对话框 ——(容器内的普通控件与非模态对话框)
std::vector<std::unique_ptr<Control>> controls; // 普通顶层控件;绘制顺序也决定层级顺序
std::vector<std::unique_ptr<Control>> dialogs; // 非模态对话框;始终位于普通控件之上
bool managedDispatchActive = false; // 事件分发期:控件只改状态并登记重绘,不立即画
bool managedSceneDirty = false; // 当前分发轮次是否已经登记了至少一笔托管重绘请求
struct ManagedRepaintItem // 托管重绘项:记录由哪个控件发起、需要重绘的根控件和覆盖范围(用于后续判断哪些对话框需要补画)
{
Control* root = nullptr; // 顶层重绘根(直接挂在 Window 下的控件,或 Dialog 自身)
RECT coverage{}; // 本轮脏区覆盖范围;用于判断哪些上层 Dialog 需要补画
};
std::vector<ManagedRepaintItem> managedRepaintItems; // 本轮事件分发累计的重绘项
public:
bool dialogClose = false; // 项目内使用的状态位,对话框关闭标志
mutable bool dialogOpen = false; // 项目内使用的状态位,对话框打开标志
// —— 构造/析构 ——(仅初始化成员;实际样式与子类化在 draw() 中完成)
// 创建纯色背景窗口
Window(int width, int height, int mode);
// 创建指定背景色窗口
Window(int width, int height, int mode, COLORREF bkcloc);
// 创建带标题与背景色的窗口
Window(int width, int height, int mode, COLORREF bkcloc, std::string headline);
// 析构窗口并释放背景、控件与子类化资源
~Window();
// —— 绘制与事件循环 ——(draw* 完成一次全量绘制;runEventLoop 驱动事件与统一收口)
void draw(); // 以纯色背景执行一次全量绘制
void draw(std::string pImgFile); // 以背景图执行一次全量绘制
int runEventLoop(); // 主事件循环(PeekMessage + 统一收口重绘)
// —— 背景/标题设置 ——(更换背景、背景色与标题;立即触发一次批量绘制)
void setBkImage(std::string pImgFile); // 设置窗口背景图
void setBkcolor(COLORREF c); // 设置窗口纯色背景
void setHeadline(std::string headline); // 设置窗口标题
// —— 控件/对话框管理 ——(添加到容器,或做存在性判断)
void addControl(std::unique_ptr<Control> control); // 添加普通顶层控件
void addDialog(std::unique_ptr<Control> dialogs); // 添加非模态对话框
bool hasNonModalDialogWithCaption(const std::string& caption, const std::string& message) const; // 检查是否已有同 caption/message 的非模态对话框
// —— 访问器 ——(只读接口,供外部查询当前窗口/标题/背景等)
HWND getHwnd() const; // 获取窗口句柄
int getWidth() const; // 获取当前有效客户区宽度
int getHeight() const; // 获取当前有效客户区高度
int getPendingWidth() const; // 获取待应用宽度
int getPendingHeight() const; // 获取待应用高度
std::string getHeadline() const; // 获取窗口标题
COLORREF getBkcolor() const; // 获取纯色背景
IMAGE* getBkImage() const; // 获取背景图对象
std::string getBkImageFile() const; // 获取背景图路径
std::vector<std::unique_ptr<Control>>& getControls(); // 获取普通顶层控件列表
// —— 尺寸调整 / 托管重绘 ——(事件阶段登记,收口阶段提交)
// 执行一次 resize 收口 + 统一重绘
void pumpResizeIfNeeded();
// 供模态 Dialog 上报宿主窗口尺寸变化
void scheduleResizeFromModal(int w, int h);
// 当前是否处于“事件只改状态,不立即画”的阶段
bool isManagedDispatchActive() const;
// 记录一笔由 source 发起的托管重绘请求
void requestManagedRepaint(Control* source);
// 记录一笔托管重绘请求,并显式并入状态清理前的旧覆盖范围
void requestManagedRepaint(Control* source, const RECT& previousCoverage);
// 在事件收口阶段提交本轮登记的 root 重绘
void flushManagedRepaint();
private:
void adaptiveLayout(std::unique_ptr<Control>& c, const int finalH, const int finalW);
// resize / 初次绘制 / 对话框开关这类全局场景的整场景重绘
void redrawScene(bool forceControlsDirty, bool forceDialogsDirty);
void drawWindowBackground();
// 合成 WM_MOUSEMOVE,用于同步底层 hover 状态
void dispatchSyntheticMouseMoveToControls(short x, short y);
enum class OverlayMouseSyncMode
{
ClearBehindOverlay, // overlay 吞掉鼠标移动:只清理底层 hover / tooltip,不重新命中底层控件
RestoreAtCursor // overlay 关闭:按当前鼠标位置刷新底层 hover 状态
};
// overlay 层级变化后,同步底层鼠标瞬时状态;内部仍复用既有 synthetic WM_MOUSEMOVE 路径
void syncMouseStateAfterOverlayChanged(OverlayMouseSyncMode mode);
// 在事件安全点清理已关闭的非模态 Dialog,避免 dialogs 容器长期积累无效对象
void sweepClosedDialogs();
// 事件回调可能改脏其他 root;flush 前补收集这些跨 root 脏子树
void collectDirtyRootsForManagedRepaint();
// 清空本轮托管重绘登记
void clearManagedRepaintState();
// 找出需要补画到最上层的对话框
void collectManagedDialogOverlays(Control* repaintRoot, const RECT& coverage, std::vector<Control*>& overlays);
};
+25
View File
@@ -0,0 +1,25 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and maintainers pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Being considerate and respectful of others
* Respecting different viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
...
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project maintainers. All complaints will be reviewed and investigated promptly and fairly, and will result in a response that is deemed necessary and appropriate to the circumstances.
...
For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
+25
View File
@@ -0,0 +1,25 @@
# 贡献者公约行为准则
## 我们的承诺
为了营造一个开放和受欢迎的环境,我们作为贡献者和维护者承诺:无论年龄、体型、身体健全与否、民族、性征、性别认同与表达、经验水平、教育程度、社会地位、国籍、个人外貌、种族、宗教信仰、性取向,我们的项目和社区对每一个人都不存在歧视。
## 我们的标准
有助于为我们社区创造积极环境的行为包括但不限于:
* 对他人的体贴和尊重
* 尊重不同的观点和经历
* 优雅地接受建设性批评
* 关注对社区最有利的事情
* 对其他社区成员表现出同理心
......
## 执行
虐待、骚扰或其他不可接受行为的实例可通过项目维护团队进行报告。所有投诉都将被认真、迅速地审查和调查,并将得出被认为在情况下必要和适当的回应。
......
有关此行为准则的常见问题解答,请参阅 https://www.contributor-covenant.org/faq。翻译版本可在 https://www.contributor-covenant.org/translations 获取。
+56
View File
@@ -0,0 +1,56 @@
/**
* @file demo.cpp
* @brief 一个简单的演示程序,展示 StellarX GUI 框架的基本用法。
* @description 创建一个带有按钮的窗口,点击按钮会弹出对话框
*/
// 只需包含这一个头文件即可使用所有功能
#include "StellarX.h"
// 程序入口点(请使用WinMain以获得更好的兼容性)
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
{
// 1. 创建一个640x480的窗口,背景为白色,标题为"我的应用"
Window mainWindow(640, 480, 0, RGB(255, 255, 255), "我的第一个星垣应用");
// 2. 创建一个按钮 (使用智能指针管理)
auto myButton = std::make_unique<Button>(
250, 200, 140, 40, // x, y, 宽度, 高度
"点击我", // 按钮文本
StellarX::ButtonMode::NORMAL,
StellarX::ControlShape::ROUND_RECTANGLE
);
// 3. 为按钮设置点击事件(使用Lambda表达式)
myButton->setOnClickListener([&mainWindow]() {
// 使用消息框工厂创建模态对话框
auto result = StellarX::MessageBox::ShowModal(
mainWindow,
"欢迎使用星垣GUI\r\n作者:我在人间做废物",
"问候",
StellarX::MessageBoxType::OKCancel
);
// 处理对话框结果
if (result == StellarX::MessageBoxResult::OK) {
// 用户点击了确定按钮
}
});
// 4. (可选)设置按钮样式
myButton->textStyle.nHeight = 20;
myButton->textStyle.color = RGB(0, 0, 128); // 深蓝色文字
myButton->setButtonBorder(RGB(0, 128, 255)); // 蓝色边框
// 5. 将按钮添加到窗口
mainWindow.addControl(std::move(myButton));
// 6. 绘制窗口
mainWindow.draw();
// 7. 进入消息循环,等待用户交互
mainWindow.runEventLoop();
return 0;
}
+17
View File
@@ -0,0 +1,17 @@
# Register Viewer (StellarX example)
**A 32-bit register visualizer built with StellarX** (Windows + EasyX).
Features:
- 32 toggle bits (MSB→LSB = 31..0)
- Range invert, logical left/right shift
- Hex/Decimal display with signed/unsigned toggle
- Grouped binary view with "last/current" snapshots
- One-click set all 0 / all 1
## Build & Run
1. Ensure StellarX and EasyX are in your include/lib paths.
2. Build as a normal C++ Win32 desktop app (e.g., VS 2019+).
3. Run the executable.
This example demonstrates StellarX APIs with a single `main.cpp` (~450 LOC):
`Window / Canvas / Button / Label / TextBox` + simple event callbacks.
+462
View File
@@ -0,0 +1,462 @@
// 本工具基于 StellarX 构建,轻量级的 Windows GUI 框架。
#include"StellarX.h"
#include <sstream>
#include<iomanip>
#include<array>
auto blackColor = RGB(202, 255, 255);
char initData[33] = "00000000000000000000000000000000";//初始数据
bool gSigned = false; //是否为有符号数
void main()
{
Window mainWindow(700,500,NULL,RGB(255,255,255), "寄存器查看工具 V1.0——我在人间做废物 (同类工具定制:3150131407(Q / V))");
//选择区控件
auto selectionAreaLabel = std::make_unique<Label>(18, 0,"32位选择区");
selectionAreaLabel->setTextdisap(true);
std::vector<std::unique_ptr<Label>>selectionAreaButtonLabel;
std::vector<std::unique_ptr<Button>>selectionAreaButton;
std::vector<Button*>selectionAreaButton_ptr;
auto selectionArea = std::make_unique <Canvas>(10, 10, 680, 150);
selectionArea->setCanvasBkColor(blackColor);
selectionArea->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
for (int y = 0; y < 2; y ++)
{
std::ostringstream os;
for (int x = 0; x <16; x++)
{
if (0 == y)
{
selectionAreaButtonLabel.push_back(std::make_unique<Label>(x * 35 + 40 + 28 * (x / 4), 26, "", RGB(208, 208, 208)));
os << std::setw(2) << std::setfill('0') << 31 - x;
selectionAreaButtonLabel.back()->setText(os.str());
selectionAreaButtonLabel.back()->setTextdisap(true);
selectionAreaButton.push_back(
std::make_unique<Button>(x * 35 + 42 + 28 * (x / 4), 58,20,32,"0",
blackColor,RGB(171, 196, 220),StellarX::ButtonMode::TOGGLE));
selectionAreaButton.back()->textStyle.color = RGB(226, 116, 152);
selectionAreaButton.back()->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
selectionAreaButton_ptr.push_back(selectionAreaButton.back().get());
int k = 32 - x - 1;
//选择区按钮被点击后在二进制0和1之间切换,并更新initData
selectionAreaButton_ptr.back()->setOnToggleOnListener([k, btn = selectionAreaButton_ptr.back()]()
{
btn->setButtonText("1");
initData[k] = '1';
});
selectionAreaButton_ptr.back()->setOnToggleOffListener([k, btn = selectionAreaButton_ptr.back()]()
{
btn->setButtonText("0");
initData[k] = '0';
});
}
else
{
selectionAreaButtonLabel.push_back(std::make_unique<Label>(x * 35 + 40 + 28 * (x / 4), 90, "", RGB(208, 208, 208)));
os << std::setw(2) << std::setfill('0') << 15-x;
selectionAreaButtonLabel.back()->setText(os.str());
selectionAreaButtonLabel.back()->setTextdisap(true);
selectionAreaButton.push_back(
std::make_unique<Button>(x * 35 + 42 + 28 * (x / 4), 120, 20, 32, "0",
blackColor, RGB(171, 196, 220), StellarX::ButtonMode::TOGGLE));
selectionAreaButton.back()->textStyle.color = RGB(226, 116, 152);
selectionAreaButton.back()->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
selectionAreaButton_ptr.push_back(selectionAreaButton.back().get());
int k =15 - x;
selectionAreaButton.back()->setOnToggleOnListener([k,btn = selectionAreaButton_ptr.back()]()
{
btn->setButtonText("1");
initData[k] = '1';
});
selectionAreaButton.back()->setOnToggleOffListener([k, btn = selectionAreaButton_ptr.back()]()
{
btn->setButtonText("0");
initData[k] = '0';
});
}
os.str("");
os.clear();
}
}
selectionArea->addControl(std::move(selectionAreaLabel));
for (auto& s : selectionAreaButton)
selectionArea->addControl(std::move(s));
for (auto& s : selectionAreaButtonLabel)
selectionArea->addControl(std::move(s));
//功能区控件
//功能区总容器
auto function = std::make_unique<Canvas>(0, 0, 0, 0);
function->setCanvasfillMode(StellarX::FillMode::Null);
auto bitInvert = std::make_unique<Canvas>(10,170,220,70);
auto leftShift = std::make_unique<Canvas>(240, 170, 220, 70);
auto rightShift = std::make_unique<Canvas>(470, 170, 220, 70);
bitInvert->setCanvasBkColor(blackColor);
bitInvert->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
leftShift->setCanvasBkColor(blackColor);
leftShift->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
rightShift->setCanvasBkColor(blackColor);
rightShift->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
auto bitInvertLabel = std::make_unique<Label>(18,160,"位取反");
bitInvertLabel->setTextdisap(true);
auto leftShiftLabel = std::make_unique<Label>(248, 160, "左移位");
leftShiftLabel->setTextdisap(true);
auto rightShiftLabel = std::make_unique<Label>(478, 160, "右移位");
rightShiftLabel->setTextdisap(true);
// ====== 公用小工具======
auto clamp = [](int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); };
auto toInt = [](const std::string& s, int def = 0) {
try { return std::stoi(s); }
catch (...) { return def; }
};
// bit号(31..0) -> selectionAreaButton下标(0..31)
auto vecIndexFromBit = [](int bit) { return 31 - bit; };
// 读取当前32位点击态
auto snapshotBits = [&]() {
std::array<bool, 32> a{};
for (int b = 0; b < 32; ++b)
a[b] = selectionAreaButton_ptr[vecIndexFromBit(b)]->isClicked();
return a;
};
// 应用目标态:仅当不同才 setButtonClick
auto applyBits = [&](const std::array<bool, 32>& a) {
for (int b = 0; b < 32; ++b) {
auto btn = selectionAreaButton_ptr[vecIndexFromBit(b)];
if (btn->isClicked() != a[b]) btn->setButtonClick(a[b]);
}
};
//取反区控件
std::array<std::unique_ptr<Label>, 4> bitInvertFunctionLabel;
bitInvertFunctionLabel[0] = std::make_unique<Label>(35, 180, "低位");
bitInvertFunctionLabel[1] = std::make_unique<Label>(90, 180, "高位");
bitInvertFunctionLabel[2] = std::make_unique<Label>(15, 198, "");
bitInvertFunctionLabel[3] = std::make_unique<Label>(75, 198, "");
std::array<std::unique_ptr<TextBox>, 2> bitInvertFunctionTextBox;
bitInvertFunctionTextBox[0] = std::make_unique<TextBox>(35, 203, 35, 30, "0");
bitInvertFunctionTextBox[1] = std::make_unique<TextBox>(95, 203, 35, 30, "0");
auto invL = bitInvertFunctionTextBox[0].get();
auto invH = bitInvertFunctionTextBox[1].get();
auto bitInvertFunctionButton = std::make_unique<Button>(150,195, 70, 35, "位取反",
blackColor, RGB(171, 196, 220));
bitInvertFunctionButton->textStyle.color = RGB(226, 116, 152);
bitInvertFunctionButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
auto bitInvertFunctionButton_ptr = bitInvertFunctionButton.get();
bitInvert->addControl(std::move(bitInvertFunctionButton));
bitInvert->addControl(std::move(bitInvertLabel));
for (auto& b : bitInvertFunctionTextBox)
{
b->setMaxCharLen(3);
b->textStyle.color = RGB(226, 116, 152);
b->setTextBoxBk(RGB(244, 234, 142));
b->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
bitInvert->addControl(std::move(b));
}
for (auto& b : bitInvertFunctionLabel)
{
b->setTextdisap(true);
bitInvert->addControl(std::move(b));
}
//左移控件
auto leftShiftFunctionLabel = std::make_unique<Label>(435, 198, "");
leftShiftFunctionLabel->setTextdisap(true);
auto leftShiftFunctionTextBox = std::make_unique<TextBox>(325, 195, 100, 30, "0");
leftShiftFunctionTextBox->setMaxCharLen(3);
leftShiftFunctionTextBox->textStyle.color = RGB(226, 116, 152);
leftShiftFunctionTextBox->setTextBoxBk(RGB(244, 234, 142));
leftShiftFunctionTextBox->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
auto shlBox = leftShiftFunctionTextBox.get();
auto leftShiftFunctionButton = std::make_unique<Button>(250, 195, 60, 30, "左移",
blackColor, RGB(171, 196, 220));
leftShiftFunctionButton->textStyle.color = RGB(226, 116, 152);
leftShiftFunctionButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
auto leftShiftFunctionButton_ptr = leftShiftFunctionButton.get();
leftShift->addControl(std::move(leftShiftFunctionButton));
leftShift->addControl(std::move(leftShiftFunctionTextBox));
leftShift->addControl(std::move(leftShiftLabel));
leftShift->addControl(std::move(leftShiftFunctionLabel));
//右移控件
auto rightShiftFunctionLabel = std::make_unique<Label>(665, 198, "");
rightShiftFunctionLabel->setTextdisap(true);
auto rightShiftFunctionTextBox = std::make_unique<TextBox>(555, 195, 100, 30, "0");
rightShiftFunctionTextBox->setMaxCharLen(3);
rightShiftFunctionTextBox->textStyle.color = RGB(226, 116, 152);
rightShiftFunctionTextBox->setTextBoxBk(RGB(244, 234, 142));
rightShiftFunctionTextBox->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
auto shrBox = rightShiftFunctionTextBox.get();
auto rightShiftFunctionButton = std::make_unique<Button>(480, 195, 60, 30, "右移",
blackColor, RGB(171, 196, 220));
rightShiftFunctionButton->textStyle.color = RGB(226, 116, 152);
rightShiftFunctionButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
auto rightShiftFunctionButton_ptr = rightShiftFunctionButton.get();
rightShift->addControl(std::move(rightShiftFunctionButton));
rightShift->addControl(std::move(rightShiftFunctionTextBox));
rightShift->addControl(std::move(rightShiftLabel));
rightShift->addControl(std::move(rightShiftFunctionLabel));
function->addControl(std::move(bitInvert));
function->addControl(std::move(leftShift));
function->addControl(std::move(rightShift));
//显示区控件
//数值显示
auto NumericalDisplayArea = std::make_unique<Canvas>(10, 255, 680, 70);
NumericalDisplayArea->setCanvasBkColor(blackColor);
NumericalDisplayArea->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
std::array<std::unique_ptr<Label>, 3> NumericalDisplayAreaLabel;
NumericalDisplayAreaLabel[0] = std::make_unique<Label>(18, 245, "数值显示区");
NumericalDisplayAreaLabel[1] = std::make_unique<Label>(20, 278, "十六进制");
NumericalDisplayAreaLabel[2] = std::make_unique<Label>(330, 278, "十进制");
std::array<std::unique_ptr<TextBox>, 2> NumericalDisplayAreaTextBox;
NumericalDisplayAreaTextBox[0] = std::make_unique<TextBox>(110, 275, 200, 30, "0");
NumericalDisplayAreaTextBox[1] = std::make_unique<TextBox>(400, 275, 200, 30, "0");
auto hex = NumericalDisplayAreaTextBox[0].get();
auto dec = NumericalDisplayAreaTextBox[1].get();
for (auto& b : NumericalDisplayAreaLabel)
{
b->setTextdisap(true);
NumericalDisplayArea->addControl(std::move(b));
}
for (auto& b : NumericalDisplayAreaTextBox)
{
b->setMaxCharLen(11);
b->textStyle.color = RGB(255, 69, 0);
b->setTextBoxBk(RGB(141, 141, 141));
b->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
b->setMode(StellarX::TextBoxmode::READONLY_MODE);
NumericalDisplayArea->addControl(std::move(b));
}
//二进制显示
auto BinaryDisplayArea = std::make_unique<Canvas>(10, 335, 680, 110);
BinaryDisplayArea->setCanvasBkColor(blackColor);
BinaryDisplayArea->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
std::array<std::unique_ptr<Label>, 3> BinaryDisplayAreaLabel;
BinaryDisplayAreaLabel[0] = std::make_unique<Label>(18, 325, "二进制显示区");
BinaryDisplayAreaLabel[1] = std::make_unique<Label>(35, 353, "上次值");
BinaryDisplayAreaLabel[2] = std::make_unique<Label>(35, 400, "本次值");
std::array<std::unique_ptr<TextBox>, 2> BinaryDisplayAreaTextBox;
BinaryDisplayAreaTextBox[0] = std::make_unique<TextBox>(110, 350, 520, 30, "0000_0000_0000_0000_0000_0000_0000_0000");
BinaryDisplayAreaTextBox[1] = std::make_unique<TextBox>(110, 400, 520, 30, "0000_0000_0000_0000_0000_0000_0000_0000");
auto Last = BinaryDisplayAreaTextBox[0].get();
auto This = BinaryDisplayAreaTextBox[1].get();
for (auto& b : BinaryDisplayAreaLabel)
{
b->setTextdisap(true);
BinaryDisplayArea->addControl(std::move(b));
}
for (auto& b : BinaryDisplayAreaTextBox)
{
b->setMaxCharLen(40);
b->textStyle.color = RGB(255, 69, 0);
b->setTextBoxBk(RGB(141, 141, 141));
b->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
b->setMode(StellarX::TextBoxmode::READONLY_MODE);
BinaryDisplayArea->addControl(std::move(b));
}
// 由位图 bits 生成数值
auto valueFromBits = [](const std::array<bool, 32>& bits) -> uint32_t {
uint32_t v = 0;
for (int b = 0; b < 32; ++b) if (bits[b]) v |= (1u << b);
return v;
};
// 由位图 bits 生成 "0000_0000_..._0000"MSB→LSB31..0
auto binaryGroupedFromBits = [](const std::array<bool, 32>& bits) -> std::string {
std::string s; s.reserve(39);
for (int b = 31; b >= 0; --b) {
s.push_back(bits[b] ? '1' : '0');
if (b % 4 == 0 && b != 0) s.push_back('_');
}
return s;
};
// 用“目标位图 bits”刷新显示区
auto refreshDisplaysWithBits = [&](const std::string& prevThis,
const std::array<bool, 32>& bits,
TextBox* hex, TextBox* dec, TextBox* Last, TextBox* This)
{
const uint32_t val = valueFromBits(bits);
const int32_t s = static_cast<int32_t>(val);
char hexbuf[16];
std::snprintf(hexbuf, sizeof(hexbuf), "%08X", val);
hex->setText(hexbuf); // HEX(大写8位)
dec->setText(gSigned ? std::to_string(s) : std::to_string(val)); // DEC
Last->setText(prevThis); // 上次值 ← 刷新前的本次值
This->setText(binaryGroupedFromBits(bits)); // 本次值 ← 由“目标位图”生成
};
bitInvertFunctionButton_ptr->setOnClickListener([=, &snapshotBits, &applyBits, &refreshDisplaysWithBits]() {
const std::string prevThis = This->getText();
int L = clamp(toInt(invL->getText(), 0), 0, 31);
int H = clamp(toInt(invH->getText(), 0), 0, 31);
if (L > H) std::swap(L, H);
auto cur = snapshotBits();
for (int b = L; b <= H; ++b) cur[b] = !cur[b];
applyBits(cur); // 只改按钮点击态(触发位按钮自回调)
refreshDisplaysWithBits(prevThis, cur, hex, dec, Last, This);
});
leftShiftFunctionButton_ptr->setOnClickListener([=, &snapshotBits, &applyBits, &refreshDisplaysWithBits]() {
const std::string prevThis = This->getText();
int n = clamp(toInt(shlBox->getText(), 0), 0, 31);
auto cur = snapshotBits();
std::array<bool, 32> nxt{}; // 默认全 0
// 逻辑左移:高位丢弃、低位补 0
for (int b = 31; b >= 0; --b) nxt[b] = (b >= n) ? cur[b - n] : false;
applyBits(nxt);
refreshDisplaysWithBits(prevThis, nxt, hex, dec, Last, This);
});
rightShiftFunctionButton_ptr->setOnClickListener([=, &snapshotBits, &applyBits, &refreshDisplaysWithBits]() {
const std::string prevThis = This->getText();
int n = clamp(toInt(shrBox->getText(), 0), 0, 31);
auto cur = snapshotBits();
std::array<bool, 32> nxt{};
// 逻辑右移:低位丢弃、高位补 0
for (int b = 0; b < 32; ++b) nxt[b] = (b + n <= 31) ? cur[b + n] : false;
applyBits(nxt);
refreshDisplaysWithBits(prevThis, nxt, hex, dec, Last, This);
});
//配置区控件clearrectangle(10, 440, 690, 490);
auto configuration = std::make_unique<Canvas>(10, 455, 680, 40);
configuration->setCanvasBkColor(blackColor);
configuration->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
auto configurationLabel = std::make_unique<Label>(20, 445, "配置区");
configurationLabel->setTextdisap(true);
std::array<std::unique_ptr<Button>,2> configurationButton;
configurationButton[0] = std::make_unique<Button>(450, 465, 80, 20, "一键置0",
blackColor, RGB(171, 196, 220));
configurationButton[0]->textStyle.color = RGB(226, 116, 152);
configurationButton[0]->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
configurationButton[1] = std::make_unique<Button>(550, 465, 80, 20, "一键置1",
blackColor, RGB(171, 196, 220));
configurationButton[1]->textStyle.color = RGB(226, 116, 152);
configurationButton[1]->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
configurationButton[0]->setOnClickListener(
[&]() {
for (auto& s : selectionAreaButton_ptr)
if (s->isClicked()) s->setButtonClick(false);
// 刷新显示:prevThis 用当前 This 文本
const std::string prevThis = This->getText();
auto cur = snapshotBits();
{
char hexbuf[16];
uint32_t u = 0; for (int b = 0; b < 32; ++b) if (cur[b]) u |= (1u << b);
int32_t s = static_cast<int32_t>(u);
std::snprintf(hexbuf, sizeof(hexbuf), "%08X", u);
hex->setText(hexbuf);
dec->setText(gSigned ? std::to_string(s) : std::to_string(u));
Last->setText(prevThis);
This->setText(binaryGroupedFromBits(cur));
}
});
configurationButton[1]->setOnClickListener(
[&]() {
for (auto& s : selectionAreaButton_ptr)
if (!s->isClicked()) s->setButtonClick(true);
const std::string prevThis = This->getText();
auto cur = snapshotBits();
char hexbuf[16];
uint32_t u = 0; for (int b = 0; b < 32; ++b) if (cur[b]) u |= (1u << b);
int32_t s = static_cast<int32_t>(u);
std::snprintf(hexbuf, sizeof(hexbuf), "%08X", u);
hex->setText(hexbuf);
dec->setText(gSigned ? std::to_string(s) : std::to_string(u));
Last->setText(prevThis);
This->setText(binaryGroupedFromBits(cur));
});
auto signedToggle = std::make_unique<Button>(
350, 465, 80, 20, "无符号",
blackColor, RGB(171, 196, 220), StellarX::ButtonMode::TOGGLE);
signedToggle->textStyle.color = RGB(226, 116, 152);
signedToggle->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
auto* signedTogglePtr = signedToggle.get();
signedTogglePtr->setOnToggleOnListener([&]() {
gSigned = true;
signedTogglePtr->setButtonText("有符号");
// 立即刷新十进制显示:用当前位图算出新值,仅改 dec
auto cur = snapshotBits();
const uint32_t u = [&] { uint32_t v = 0; for (int b = 0; b < 32; ++b) if (cur[b]) v |= (1u << b); return v; }();
const int32_t s = static_cast<int32_t>(u);
dec->setText(std::to_string(s));
});
signedTogglePtr->setOnToggleOffListener([&]() {
gSigned = false;
signedTogglePtr->setButtonText("无符号");
auto cur = snapshotBits();
const uint32_t u = [&] { uint32_t v = 0; for (int b = 0; b < 32; ++b) if (cur[b]) v |= (1u << b); return v; }();
dec->setText(std::to_string(u));
});
configuration->addControl(std::move(configurationButton[0]));
configuration->addControl(std::move(configurationButton[1]));
configuration->addControl(std::move(signedToggle));
configuration->addControl(std::move(configurationLabel));
mainWindow.addControl(std::move(selectionArea));
mainWindow.addControl(std::move(function));
mainWindow.addControl(std::move(NumericalDisplayArea));
mainWindow.addControl(std::move(BinaryDisplayArea));
mainWindow.addControl(std::move(configuration));
mainWindow.draw();
return mainWindow.runEventLoop();
}
-31
View File
@@ -1,31 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35828.75 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "imGui-easyX", "imGui-easyX.vcxproj", "{FD33B55B-FD97-4087-8B95-6BF8285AAE73}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Debug|x64.ActiveCfg = Debug|x64
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Debug|x64.Build.0 = Debug|x64
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Debug|x86.ActiveCfg = Debug|Win32
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Debug|x86.Build.0 = Debug|Win32
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x64.ActiveCfg = Release|x64
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x64.Build.0 = Release|x64
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x86.ActiveCfg = Release|Win32
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8E5EB082-F5AD-4E8A-8809-29A47F920B2B}
EndGlobalSection
EndGlobal
-174
View File
@@ -1,174 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Button.h" />
<ClInclude Include="Canvas.h" />
<ClInclude Include="Control.h" />
<ClInclude Include="CoreTypes.h" />
<ClInclude Include="Dialog.h" />
<ClInclude Include="label.h" />
<ClInclude Include="MessageBox.h" />
<ClInclude Include="StellarX.h" />
<ClInclude Include="SxLog.h" />
<ClInclude Include="TabControl.h" />
<ClInclude Include="table.h" />
<ClInclude Include="textBox.h" />
<ClInclude Include="window.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="Button.cpp" />
<ClCompile Include="Canvas.cpp" />
<ClCompile Include="Control.cpp" />
<ClCompile Include="Dialog.cpp" />
<ClCompile Include="label.cpp" />
<ClCompile Include="MessageBox.cpp" />
<ClCompile Include="SxLog.cpp" />
<ClCompile Include="TabControl.cpp" />
<ClCompile Include="table.cpp" />
<ClCompile Include="textBox.cpp" />
<ClCompile Include="window.cpp" />
<ClCompile Include="z-testDome.cpp" />
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{fd33b55b-fd97-4087-8b95-6bf8285aae73}</ProjectGuid>
<RootNamespace>imGuieasyX</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>
</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PrecompiledHeaderFile />
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>
</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PrecompiledHeaderFile />
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>
</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PrecompiledHeaderFile />
<LanguageStandard_C>stdc17</LanguageStandard_C>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>
</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PrecompiledHeaderFile />
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
-96
View File
@@ -1,96 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Control.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="Button.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="window.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="label.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="textBox.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="Canvas.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="table.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="StellarX.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="CoreTypes.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="Dialog.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="MessageBox.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="TabControl.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="SxLog.h">
<Filter>头文件</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Control.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="Button.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="z-testDome.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="window.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="label.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="textBox.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="Canvas.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="table.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="Dialog.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="MessageBox.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="TabControl.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="SxLog.cpp">
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup>
</Project>
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ShowAllFiles>false</ShowAllFiles>
</PropertyGroup>
</Project>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

+204
View File
@@ -0,0 +1,204 @@
/*******************************************************************************
* @类: Button
* @摘要: 多功能按钮控件,支持多种状态和样式
* @描述:
* 提供完整的按钮功能,包括普通点击、切换模式、禁用状态。
* 支持多种形状(矩形、圆形、椭圆等)和丰富的视觉样式。
* 通过回调函数机制实现灵活的交互逻辑。
*
* @特性:
* - 支持三种工作模式:普通、切换、禁用
* - 八种几何形状,各有边框和无边框版本
* - 自定义颜色(默认、悬停、点击状态)
* - 多种填充模式(纯色、图案、图像)
* - 完整的鼠标事件处理(点击、悬停、移出)
*
* @使用场景: 作为主要交互控件,用于触发动作或表示状态
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
#pragma once
#include "Control.h"
#include"label.h"
#define DISABLEDCOLOUR RGB(96, 96, 96) //禁用状态颜色
#define TEXTMARGINS_X 6
#define TEXTMARGINS_Y 4
class Button : public Control
{
std::string text; // 按钮上的文字
bool click; // 是否被点击
bool hover; // 是否被悬停
std::string cutText; // 切割后的文本
bool needCutText = true; // 是否需要切割文本
bool isUseCutText = false; // 是否使用切割文本
int padX = TEXTMARGINS_X; // 文本最小左右内边距
int padY = TEXTMARGINS_Y; // 文本最小上下内边距
COLORREF buttonTrueColor; // 按钮被点击后的颜色
COLORREF buttonFalseColor; // 按钮未被点击的颜色
COLORREF buttonHoverColor; // 按钮被鼠标悬停的颜色
COLORREF buttonBorderColor = RGB(0,0,0);// 按钮边框颜色
StellarX::ButtonMode mode; // 按钮模式
StellarX::ControlShape shape; // 按钮形状
StellarX::FillMode buttonFillMode = StellarX::FillMode::Solid; //按钮填充模式
StellarX::FillStyle buttonFillIma = StellarX::FillStyle::BDiagonal; //按钮填充图案
IMAGE* buttonFileIMAGE = nullptr; //按钮填充图像
std::function<void()> onClickCallback; //回调函数
std::function<void()> onToggleOnCallback; //TOGGLE模式下的回调函数
std::function<void()> onToggleOffCallback; //TOGGLE模式下的回调函数
StellarX::ControlText oldStyle = textStyle; // 按钮文字样式
int oldtext_width = -1;
int oldtext_height = -1;
int text_width = 0;
int text_height = 0;
// === Tooltip ===
bool tipEnabled = false; // 是否启用
bool tipVisible = false; // 当前是否显示
bool tipFollowCursor = false; // 是否跟随鼠标
bool tipUserOverride = false; // 是否用户自定义了tip文本
int tipDelayMs = 1000; // 延时(毫秒)
int tipOffsetX = 12; // 相对鼠标偏移
int tipOffsetY = 18;
ULONGLONG tipHoverTick = 0; // 开始悬停的时间戳
int lastMouseX = 0; // 最新鼠标位置(用于定位)
int lastMouseY = 0;
std::string tipTextClick; //NORMAL 模式下用
std::string tipTextOn; // click==true 时用
std::string tipTextOff; // click==false 时用
Label tipLabel; // 直接复用Label作为提示
public:
StellarX::ControlText textStyle; // 按钮文字样式
public:
//默认按钮颜色
Button(int x, int y, int width, int height, const std::string text,
StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
//自定义按钮未被点击和被点击颜色
Button(int x, int y, int width, int height, const std::string text,
COLORREF ct, COLORREF cf, StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL,
StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
//自定义按钮颜色和悬停颜色
Button(int x, int y, int width, int height, const std::string text,
COLORREF ct, COLORREF cf,COLORREF ch,
StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
//析构函数 释放图形指针内存
~Button();
//绘制按钮
void draw() override;
//按钮事件处理
bool handleEvent(const ExMessage& msg) override;
//设置回调函数
void setOnClickListener(const std::function<void()>&& callback);
//设置TOGGLE模式下被点击的回调函数
void setOnToggleOnListener(const std::function<void()>&& callback);
//设置TOGGLE模式下取消点击的回调函数
void setOnToggleOffListener(const std::function<void()>&& callback);
//设置按钮模式
void setbuttonMode(StellarX::ButtonMode mode);
//设置圆角矩形椭圆宽度
void setROUND_RECTANGLEwidth(int width);
//设置圆角矩形椭圆高度
void setROUND_RECTANGLEheight(int height);
//设置按钮填充模式
void setFillMode(StellarX::FillMode mode);
//设置按钮填充图案
void setFillIma(StellarX::FillStyle ima);
//设置按钮填充图像
void setFillIma(std::string imaName);
//设置按钮边框颜色
void setButtonBorder(COLORREF Border);
//设置按钮未被点击颜色
void setButtonFalseColor(COLORREF color);
//设置按钮文本
void setButtonText(const char* text);
void setButtonText(std::string text);
//设置按钮形状
void setButtonShape(StellarX::ControlShape shape);
//设置按钮点击状态
void setButtonClick(BOOL click);
//判断按钮是否被点击
bool isClicked() const;
//获取按钮文字
std::string getButtonText() const;
const char* getButtonText_c() const;
//获取按钮模式
StellarX::ButtonMode getButtonMode() const;
//获取按钮形状
StellarX::ControlShape getButtonShape() const;
//获取按钮填充模式
StellarX::FillMode getFillMode() const;
//获取按钮填充图案
StellarX::FillStyle getFillIma() const;
//获取按钮填充图像
IMAGE* getFillImaImage() const;
//获取按钮边框颜色
COLORREF getButtonBorder() const;
//获取按钮文字颜色
COLORREF getButtonTextColor() const;
//获取按钮文字样式
StellarX::ControlText getButtonTextStyle() const;
//获取按钮宽度
int getButtonWidth() const;
//获取按钮高度
int getButtonHeight() const;
public:
// === Tooltip API===
//设置是否启用提示框
void enableTooltip(bool on) { tipEnabled = on; if (!on) tipVisible = false; }
//设置提示框延时
void setTooltipDelay(int ms) { tipDelayMs = (ms < 0 ? 0 : ms); }
//设置提示框是否跟随鼠标
void setTooltipFollowCursor(bool on) { tipFollowCursor = on; }
//设置提示框位置偏移
void setTooltipOffset(int dx, int dy) { tipOffsetX = dx; tipOffsetY = dy; }
//设置提示框样式
void setTooltipStyle(COLORREF text, COLORREF bk, bool transparent)
{
tipLabel.setTextColor(text);
tipLabel.setTextBkColor(bk);
tipLabel.setTextdisap(transparent);
}
//设置提示框文本
void setTooltipText(const std::string& s){ tipTextClick = s; tipUserOverride = true; }
void setTooltipTextsForToggle(const std::string& onText, const std::string& offText)
{
tipTextOn = onText;
tipTextOff = offText;
tipUserOverride = true;
}
private:
//初始化按钮
void initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch);
//判断鼠标是否在圆形按钮内
bool isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius);
//判断鼠标是否在椭圆按钮内
bool isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height);
//获取对话框类型
bool model() const override { return false; }
void cutButtonText();
// 统一隐藏&恢复背景
void hideTooltip();
// 根据当前 click 状态选择文案
void refreshTooltipTextForState();
};
+64
View File
@@ -0,0 +1,64 @@
/*******************************************************************************
* @类: Canvas
* @摘要: 画布容器控件,用于分组和管理子控件
* @描述:
* 作为其他控件的父容器,提供统一的背景和边框样式。
* 负责将事件传递给子控件并管理它们的绘制顺序。
*
* @特性:
* - 支持四种矩形形状(普通、圆角,各有边框和无边框版本)
* - 可自定义填充模式、边框颜色和背景颜色
* - 自动管理子控件的生命周期和事件传递
* - 支持嵌套容器结构
*
* @使用场景: 用于分组相关控件、实现复杂布局或作为对话框基础
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
*******************************************************************************/
#pragma once
#include "Control.h"
class Canvas : public Control
{
protected:
std::vector<std::unique_ptr<Control>> controls;
StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE; //容器形状
StellarX::FillMode canvasFillMode = StellarX::FillMode::Solid; //容器填充模式
StellarX::LineStyle canvasLineStyle = StellarX::LineStyle::Solid; //线型
int canvaslinewidth = 1; //线宽
COLORREF canvasBorderClor = RGB(0, 0, 0); //边框颜色
COLORREF canvasBkClor = RGB(255,255,255); //背景颜色
// 清除所有子控件
void clearAllControls();
public:
Canvas();
Canvas(int x, int y, int width, int height);
~Canvas() {}
//绘制容器及其子控件
void draw() override;
bool handleEvent(const ExMessage& msg) override;
//添加控件
void addControl(std::unique_ptr<Control> control);
//设置容器样式
void setShape(StellarX::ControlShape shape);
//设置容器填充模式
void setCanvasfillMode(StellarX::FillMode mode);
//设置容器边框颜色
void setBorderColor(COLORREF color);
//设置填充颜色
void setCanvasBkColor(COLORREF color);
//设置线形
void setCanvasLineStyle(StellarX::LineStyle style);
//设置线段宽度
void setLinewidth(int width);
private:
//用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; };
};
+120
View File
@@ -0,0 +1,120 @@
/*******************************************************************************
* @类: Control
* @摘要: 所有控件的抽象基类,定义通用接口和基础功能
* @描述:
* 提供控件的基本属性和方法,包括位置、尺寸、重绘标记等。
* 实现绘图状态保存和恢复机制,确保控件绘制不影响全局状态。
*
* @特性:
* - 定义控件基本属性(坐标、尺寸、脏标记)
* - 提供绘图状态管理(saveStyle/restoreStyle
* - 声明纯虚接口(draw、handleEvent等)
* - 支持移动语义,禁止拷贝语义
*
* @使用场景: 作为所有具体控件类的基类,不直接实例化
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
#pragma once
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif
#ifndef WINVER
#define WINVER _WIN32_WINNT
#endif
#include <windows.h>
#include <vector>
#include <memory>
#include <easyx.h>
#undef MessageBox
#include <iostream>
#include <string>
#include <functional>
#include "CoreTypes.h"
class Control
{
protected:
int x, y; // 左上角坐标
int width, height; // 控件尺寸
bool dirty = true; // 是否重绘
bool show = true; // 是否显示
/* == 背景快照 == */
IMAGE* saveBkImage = nullptr;
int saveBkX = 0, saveBkY = 0; // 快照保存起始坐标
int saveWidth = 0, saveHeight = 0; // 快照保存尺寸
bool hasSnap = false; // 当前是否持有有效快照
StellarX::RouRectangle rouRectangleSize; // 圆角矩形椭圆宽度和高度
LOGFONT* currentFont = new LOGFONT(); // 保存当前字体样式和颜色
COLORREF* currentColor = new COLORREF();
COLORREF* currentBkColor = new COLORREF(); // 保存当前填充色
COLORREF* currentBorderColor = new COLORREF(); // 边框颜色
LINESTYLE* currentLineStyle = new LINESTYLE(); // 保存当前线型
Control(const Control&) = delete;
Control& operator=(const Control&) = delete;
Control(Control&&) = default;
Control& operator=(Control&&) = default;
Control() : x(0), y(0), width(100), height(100) {}
Control(int x, int y, int width, int height)
: x(x), y(y), width(width), height(height) {}
public:
virtual ~Control()
{
delete currentFont;
delete currentColor;
delete currentBkColor;
delete currentBorderColor;
delete currentLineStyle;
currentFont = nullptr;
currentColor = nullptr;
currentBkColor = nullptr;
currentBorderColor = nullptr;
currentLineStyle = nullptr;
}
protected:
void saveBackground(int x, int y, int w, int h);
void restBackground(); // putimage 回屏
void discardBackground(); // 释放快照(窗口重绘/尺寸变化后必须作废)
public:
//释放快照重新保存,在尺寸变化时更新背景快照避免尺寸变化导致显示错位
void updateBackground();
// 获取位置和尺寸
int getX() const { return x; }
int getY() const { return y; }
int getWidth() const { return width; }
int getHeight() const { return height; }
int getRight() const { return x + width; }
int getBottom() const { return y + height; }
void setX(int x) { this->x = x; dirty = true; }
void setY(int y) { this->y = y; dirty = true; }
void setWidth(int width) { this->width = width; dirty = true; }
void setHeight(int height) { this->height = height; dirty = true; }
public:
//设置是否重绘
void setDirty(bool dirty) { this->dirty = dirty; }
virtual void draw() = 0;
virtual bool handleEvent(const ExMessage& msg) = 0;//返回true代表事件已消费
//设置是否显示
void setShow(bool show) { this->show = show; }
//检查控件是否可见
bool IsVisible() const { return show; };
//用来检查对话框是否模态,其他控件不用实现
virtual bool model()const = 0;
protected:
void saveStyle();
void restoreStyle();
};
+322
View File
@@ -0,0 +1,322 @@
/*******************************************************************************
* @文件: CoreTypes.h
* @摘要: 星垣(StellarX)框架核心类型定义文件
* @描述:
* 集中定义框架中使用的所有枚举类型和结构体,确保类型一致性。
* 这是框架的类型系统基础,所有控件都依赖于此文件中定义的类型。
*
* @作者: 我在人间做废物
* @日期: September 2025
******************************************************************************/
#pragma once
#include "easyx.h"
/**
* @命名空间: StellarX
*
* @详细说明:
* 集中定义框架中使用的所有枚举类型和结构体,确保类型一致性。
* 这是框架的类型系统基础,所有控件都依赖于此文件中定义的类型
*
* @使用示例:
* StellarX::FillStyle::Horizontal - 填充样式
*
* @备注:
* 不用单独包含此头文件,已在StellarX.h中包含,包含唯一对外头文件即可
*/
namespace StellarX
{
/**
* @枚举类名称: FillStyle
* @功能描述: 用来定义控件填充图案的枚举类
*
* @详细说明:
* 根据此枚举类可以自定义控件填充图案
* 可以在控件初始化时设置填充图案
* 根据具体情况选择不同的填充图案
* 默认填充图案为水平线
*
* @取值说明:
* Horizontal - 水平线
* Vertical - 垂直线
* FDiagonal - 反斜线
* BDiagonal - 正斜线
* Cross - 十字
* DiagCross - 网格
*
* @使用示例:
* FillStyle var = FillStyle::Horizontal;
*
* @备注:
* 此枚举类仅支持图案填充模式
*/
enum class FillStyle
{
Horizontal = HS_HORIZONTAL, // 水平线
Vertical = HS_VERTICAL, // 垂直线
FDiagonal = HS_FDIAGONAL, // 反斜线
BDiagonal = HS_BDIAGONAL, // 正斜线
Cross = HS_CROSS, // 十字
DiagCross = HS_DIAGCROSS // 网格
};
/**
* @枚举类名称: FillMode
* @功能描述: 用来定义控件填充模式的枚举类
*
* @详细说明:
* 根据此枚举类可以自定义控件填充模式
* 可以在控件初始化时设置填充模式
* 根据具体情况选择不同的填充模式
* 默认填充模式为固实填充
*
* @取值说明:
* Solid - 固实填充
* Null - 不填充
* Hatched - 图案填充
* Pattern - 自定义图案填充
* DibPattern - 自定义图像填充
*
* @使用示例:
* FillMode var = FillMode::Solid;
*/
enum class FillMode
{
Solid = BS_SOLID, //固实填充
Null = BS_NULL, // 不填充
Hatched = BS_HATCHED, // 图案填充
Pattern = BS_PATTERN, // 自定义图案填充
DibPattern = BS_DIBPATTERN // 自定义图像填充
};
/**
* @枚举类名称: LineStyle
* @功能描述: 此枚举类用来定义控件边框线型
*
* @详细说明:
* 根据此枚举类可以自定义控件边框线型
* 可以在控件初始化时设置边框线型
* 根据具体情况选择不同的线型
* 默认线型为实线
*
* @取值说明:
* Solid // 实线
* Dash // 虚线
* Dot // 点线
* DashDot // 点划线
* DashDotDot // 双点划线
* Null // 无线
*
* @使用示例:
* LineStyle var = LineStyle::Solid;
*/
enum class LineStyle
{
Solid = PS_SOLID, // 实线
Dash = PS_DASH, // 虚线
Dot = PS_DOT, // 点线
DashDot = PS_DASHDOT, // 点划线
DashDotDot = PS_DASHDOTDOT, // 双点划线
Null = PS_NULL // 无线
};
/**
* @结构体名称: ControlText
* @功能描述: 控件字体样式 可以自定义不同的样式
*
* @详细说明:
* 主要使用的场景为:需要修改或想自定义控件字体大小,字体样式,颜色等
*
* @成员说明:
* int nHeight = 0; - 字体高度
* int nWidth = 0; - 字体宽度 如果为0则自适应
* LPCTSTR lpszFace = "宋体"; - 字体名称
* COLORREF color = RGB(0, 0, 0); - 字体颜色
* int nEscapement = 0; - 字符串旋转角度
* int nOrientation = 0; - 字符旋转角度
* int nWeight = 0; - 字体粗细 范围0~1000 0表示默认
* bool bItalic = false; - 是否斜体
* bool bUnderline = false; - 是否下划线
* bool bStrikeOut = false; - 是否删除线
*/
struct ControlText
{
int nHeight = 0; //- 字体高度
int nWidth = 0; //- 字体宽度 如果为0则自适应
LPCTSTR lpszFace = "微软雅黑"; //- 字体名称
COLORREF color = RGB(0, 0, 0); //- 字体颜色
int nEscapement = 0; //- 字符串旋转角度
int nOrientation = 0; //- 字符旋转角度
int nWeight = 0; //- 字体粗细 范围0~1000 0表示默认
bool bItalic = false; //- 是否斜体
bool bUnderline = false; //- 是否下划线
bool bStrikeOut = false; //- 是否删除线
bool operator!=(const ControlText& text);
ControlText& operator=(const ControlText& text);
};
/**
* @枚举名称: ControlShape
* @功能描述: 枚举控件的不同几何样式
*
* @详细说明:
* 定义了四种(有无边框算一种)不同的几何样式,可以根据具体需求
* 自定义控件的形状。
*
* @取值说明:
* RECTANGLE = 1, //有边框矩形
* B_RECTANGLE, //无边框矩形
* ROUND_RECTANGLE, //有边框圆角矩形
* B_ROUND_RECTANGLE, //无边框圆角矩形
* CIRCLE, //有边框圆形
* B_CIRCLE, //无边框圆形
* ELLIPSE, //有边框椭圆
* B_ELLIPSE //无边框椭圆
*
* @使用示例:
* ControlShape shape = ControlShape::ELLIPSE;
*
* @备注:
* 按钮类支持所有形状,部分控件只支持部分形状,具体请参考控件类。
*/
enum class ControlShape
{
RECTANGLE = 1, //有边框矩形
B_RECTANGLE, //无边框矩形
ROUND_RECTANGLE, //有边框圆角矩形
B_ROUND_RECTANGLE, //无边框圆角矩形
CIRCLE, //有边框圆形
B_CIRCLE, //无边框圆形
ELLIPSE, //有边框椭圆
B_ELLIPSE //无边框椭圆
};
/**
* @枚举类名称: TextBoxmode
* @功能描述: 定义了文本框的两种模式
*
* @详细说明:
* 需要限制文本框是否接受用户输入时使用
*
* @取值说明:
* INPUT_MODE, // 用户可输入模式
* READONLY_MODE // 只读模式
*/
enum class TextBoxmode
{
INPUT_MODE, // 用户可输入模式
READONLY_MODE // 只读模式
};
/**
* @枚举名称: ButtonMode
* @功能描述: 定义按钮的工作模式
*
* @详细说明:
* 根据按钮的工作模式,按钮可以有不同的行为。
* 用户可以在具体情况下设置按钮的工作模式。
*
* @取值说明:
* NORMAL = 1, - 普通模式,点击后触发回调,但不会保持状态。
* TOGGLE, - 切换模式,点击后会在选中和未选中之间切换,触发不同的回调函数。
* DISABLED - 禁用模式,按钮不可点击,显示为灰色,文本显示删除线。
*
* @使用示例:
* ButtonMode mode = ButtonMode::NORMAL;
*/
enum class ButtonMode
{
NORMAL = 1, //普通模式
TOGGLE, //切换模式
DISABLED //禁用模式
};
/**
* @结构体名称: RouRectangle
* @功能描述: 定义了控件圆角矩形样式时圆角的椭圆尺寸
*
* @详细说明:
* 需要修改控件圆角矩形样式时的圆角椭圆。
*
* @成员说明:
* int ROUND_RECTANGLEwidth = 20; //构成圆角矩形的圆角的椭圆的宽度。
* int ROUND_RECTANGLEheight = 20; //构成圆角矩形的圆角的椭圆的高度。
*/
struct RouRectangle
{
int ROUND_RECTANGLEwidth = 20; //构成圆角矩形的圆角的椭圆的宽度。
int ROUND_RECTANGLEheight = 20; //构成圆角矩形的圆角的椭圆的高度。
};
// 消息框类型
enum class MessageBoxType
{
OK, // 只有确定按钮
OKCancel, // 确定和取消按钮
YesNo, // 是和否按钮
YesNoCancel, // 是、否和取消按钮
RetryCancel, // 重试和取消按钮
AbortRetryIgnore, // 中止、重试和忽略按钮
};
// 消息框返回值
enum class MessageBoxResult
{
OK = 1, // 确定按钮
Cancel = 2, // 取消按钮
Yes = 6, // 是按钮
No = 7, // 否按钮
Abort = 3, // 中止按钮
Retry = 4, // 重试按钮
Ignore = 5 // 忽略按钮
};
#if 0 //布局管理器相关 —待实现—
/*
*
*@枚举名称: LayoutKind
* @功能描述 : 定义布局管理类型
*
*@详细说明 :
* 根据布局管理类型,控件可以有不同的布局方式。
* 用户可以在具体情况下设置布局管理类型。
*
* @取值说明 :
* Absolute:不管,保持子控件自己的坐标(向后兼容)。
* HBox: 水平方向排队;支持固定宽、权重分配、对齐、拉伸。
* VBox: 竖直方向排队;同上。
* Grid(网格):按行列摆;支持固定/自适应/权重行列;支持跨行/跨列;单元内对齐/拉伸。
*
*/
// 布局类型
enum class LayoutKind
{
Absolute = 1,
HBox,
VBox,
Grid,
Flow,
Stack
};
// 布局参数
struct LayoutParams
{
// 边距左、右、上、下
int marginL = 0, marginR = 0, marginT = 0, marginB = 0;
// 固定尺寸(>=0 强制;-1 用控件当前尺寸)
int fixedW = -1, fixedH = -1;
// 主轴权重(HBox=宽度、VBox=高度、Grid见下)
float weight = 0.f;
// 对齐(非拉伸时生效)
enum Align { Start = 0, Center = 1, End = 2, Stretch = 3 };
int alignX = Start; // HBox: 次轴=YVBox: 次轴=XGrid: 单元内
int alignY = Start; // Grid :控制单元内垂直(HBox / VBox通常只用 alignX
// Grid 专用(可先不做)
int gridRow = 0, gridCol = 0, rowSpan = 1, colSpan = 1;
// Flow 专用(可先不做)
int flowBreak = 0; // 1=强制换行
};
#endif
}
+35 -36
View File
@@ -4,7 +4,6 @@
* @:
*
*
* Dialog root
*
* @:
* - OKYesNoYesNoCancel等
@@ -21,7 +20,7 @@
#pragma once
#include"StellarX.h"
#define closeButtonWidth 30 //关闭按钮宽度
#define closeButtonWidth 23 //关闭按钮宽度
#define closeButtonHeight 20 //关闭按钮高度 同时作为对话框标题栏高度
#define functionButtonWidth 70 //按钮宽度
#define functionButtonHeight 30 //按钮高度
@@ -30,54 +29,58 @@
#define buttonAreaHeight 50 //按钮区域高度
#define titleToTextMargin 10 //标题到文本的距离
#define textToBorderMargin 10 //文本到边框的距离
#define BorderWidth 3 //边框宽度
class Dialog : public Canvas
{
friend class Window;
class Dialog : public Canvas
{
Window& hWnd; //窗口引用
int textWidth = 0; //文本宽度
int textHeight = 0; //文本高度
int buttonNum = 0; // 按钮数量
int BorderWidth = 2; //边框宽度
StellarX::MessageBoxType type = StellarX::MessageBoxType::OK; //对话框类型
std::string titleText = "提示"; //标题文本
std::unique_ptr<Label> title = nullptr; //标题标签
std::string message; //提示信息
std::vector<std::string> lines; //消息内容按行分割
bool needsInitialization = true; // 是否需要根据当前内容重新初始化布局和内部按钮
bool needsInitialization = true; //是否需要初始化
bool close = false; //是否关闭
bool modal = true; //是否模态
COLORREF backgroundColor = RGB(240, 240, 240); //背景颜色
COLORREF backgroundColor = RGB(240, 240, 240); //背景颜色
COLORREF borderColor = RGB(100, 100, 100); //边框颜色
COLORREF buttonTrueColor = RGB(211, 190, 190); //按钮被点击颜色
COLORREF buttonFalseColor = RGB(215, 215, 215); //按钮未被点击颜色
COLORREF buttonHoverColor = RGB(224, 224, 224); //按钮悬浮颜色
Button* closeButton = nullptr; //关闭按钮
StellarX::MessageBoxResult result = StellarX::MessageBoxResult::Cancel; // 对话框结果
bool shouldClose = false; //是否应该关闭
bool isCleaning = false; //是否正在清理
bool pendingCleanup = false; //延迟清理
public:
StellarX::ControlText textStyle; // 字体样式
// 在事件安全点执行延迟清理;用于非模态关闭后统一回收
// 清理方法声明
void performDelayedCleanup();
// 非模态工厂调用时用于回传结果
//工厂模式下调用非模态对话框时用来返回结果
std::function<void(StellarX::MessageBoxResult)> resultCallback;
// 设置非模态结果回调
//设置非模态获取结果回调函数
void SetResultCallback(std::function<void(StellarX::MessageBoxResult)> cb);
// 获取标题文本,用于非模态去重
//获取对话框消息,用以去重
std::string GetCaption() const;
// 获取正文文本,用于非模态去重
std::string GetText() const;
//获取对话框消息,用以去重
std::string GetText() const;
public:
// 构造对话框;modal=false 时按非模态浮层工作
Dialog(Window& hWnd, std::string text, std::string message = "对话框", StellarX::MessageBoxType type = StellarX::MessageBoxType::OK, bool modal = true);
~Dialog();
//绘制对话框
@@ -97,39 +100,35 @@ public:
// 获取对话框结果
StellarX::MessageBoxResult GetResult() const;
// 返回当前对话框是否为模态
//获取对话框类型
bool model() const override;
// 显示对话框
void Show();
// 关闭对话框
void Close();
// 宿主窗口变化时仅重新居中,不拉伸 Dialog 自身
void recenterInHostWindow();
void Close();
//初始化
void setInitialization(bool init);
private:
// 初始化按钮
void initButtons();
void initButtons();
// 初始化关闭按钮
void initCloseButton();
void initCloseButton();
// 初始化标题
void initTitle();
// 按行分割消息内容
void splitMessageLines();
void splitMessageLines();
// 获取文本大小
void getTextSize();
// 标记需要重新布局并按需清空旧控件
void invalidateLayout(bool clearChildren);
void getTextSize();
//初始化对话框尺寸
void initDialogSize();
// 依据当前 Dialog 的 x/y/width/height 重新创建标题和按钮
void rebuildChrome();
// 禁止走普通 Canvas 添加路径;Dialog 内部控件由自身 chrome 重建逻辑统一维护
void addControl(std::unique_ptr<Control> control);
bool canCommitManagedPartialRepaint() const override; // 判断当前 Dialog 是否可安全做局部提交
void commitManagedRepaint() override; // 托管收口阶段执行 Dialog 的真正重绘
void initDialogSize();
// 清除所有控件
void clearControls();
// 创建标准对话框功能按钮
void clearControls();
//创建对话框按钮
std::unique_ptr<Button> createDialogButton(int x, int y, const std::string& text);
void requestRepaint(Control* parent) override; // 托管模式下登记为 Dialog root;非托管模式下立即更新内部按钮
};
};
@@ -28,14 +28,12 @@ namespace StellarX
{
public:
// 模态:阻塞直到关闭,返回结果
// 直接创建并显示模态消息框,直到用户关闭后返回结果
static MessageBoxResult showModal(Window& wnd,
const std::string& text,
const std::string& caption = "提示",
MessageBoxType type = MessageBoxType::OK);
// 非模态:立即返回,通过回调异步获取结果
// 创建并显示非模态消息框,关闭时通过回调返回结果
static void showAsync(Window& wnd,
const std::string& text,
const std::string& caption = "提示",
+9 -13
View File
@@ -1,19 +1,17 @@
/*******************************************************************************
* @: StellarX.h
* @: (StellarX) GUI框架 -
* @: v3.1.0
* @: v2.0.0
* @:
* Windows平台打造的轻量级C++ GUI框架
* EasyX图形库API和丰富的控件
*
* 使
*
*
*
* @:
* @: [3150131407@qq.com] | [ysm3150131407@gmail.com]
* @https://stellarx-gui.top/
* @: https://github.com/Ysm-04/StellarX
* @https://blog.stellarx-gui.top/
* @: [https://github.com/Ysm-04/StellarX]
*
* @: MIT License
* @: Copyright (c) 2025
@@ -21,16 +19,17 @@
* @使:
* 使
* : #include "StellarX.h"
* @:
* CoreTypes.h / SxLog.h / Control.h / Canvas.h / Window.h
* Button.h / Label.h / TextBox.h / Table.h
* Dialog.h / MessageBox.h / TabControl.h
* @:
* 1. CoreTypes.h -
* 2. Control.h -
* 3. ...
* 4. Dialog CanvasDialog
* 5. MessageBox便/
******************************************************************************/
#pragma once
#include "CoreTypes.h"
#include "SxLog.h"
#include "Control.h"
#include"Canvas.h"
#include"Window.h"
@@ -40,6 +39,3 @@
#include"Table.h"
#include"Dialog.h"
#include"MessageBox.h"
#include"TabControl.h"
+53
View File
@@ -0,0 +1,53 @@
/*******************************************************************************
* @类: Label
* @摘要: 简单文本标签控件,用于显示静态文本
* @描述:
* 提供基本的文本显示功能,支持透明背景和自定义样式。
* 不支持用户交互,专注于文本呈现。
*
* @特性:
* - 支持背景透明/不透明模式
* - 完整的文本样式控制(字体、颜色、效果)
* - 自动适应文本内容
* - 轻量级无事件处理开销
*
* @使用场景: 显示说明文字、标题、状态信息等静态内容
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
#pragma once
#include "Control.h"
class Label : public Control
{
std::string text; //标签文本
COLORREF textColor; //标签文本颜色
COLORREF textBkColor; //标签背景颜色
bool textBkDisap = false; //标签背景是否透明
//标签事件处理(标签无事件)不实现具体代码
bool handleEvent(const ExMessage& msg) override { return false; }
//用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; };
public:
StellarX::ControlText textStyle; //标签文本样式
public:
Label();
Label(int x, int y, std::string text = "标签",COLORREF textcolor = BLACK, COLORREF bkColor= RGB(255,255,255));
void draw() override;
void hide();
//设置标签背景是否透明
void setTextdisap(bool key);
//设置标签文本颜色
void setTextColor(COLORREF color);
//设置标签背景颜色
void setTextBkColor(COLORREF color);
//设置标签文本
void setText(std::string text);
};
+18 -72
View File
@@ -16,39 +16,12 @@
* @: (StellarX) GUI框架
* @:
******************************************************************************/
#pragma once
#include "Control.h"
#include "Button.h"
#include "Label.h"
// === Table metrics (layout) ===
#define TABLE_PAD_X 10 // 单元格左右内边距
#define TABLE_PAD_Y 5 // 单元格上下内边距
#define TABLE_COL_GAP 20 // 列间距(列与列之间)
#define TABLE_HEADER_EXTRA 10 // 表头额外高度(若不想复用 pad 计算)
#define TABLE_ROW_EXTRA 10 // 行额外高度(同上;或直接用 2*TABLE_PAD_Y
#define TABLE_BTN_GAP 12 // 页码与按钮的水平间距
#define TABLE_BTN_PAD_H 12 // 按钮水平 padding
#define TABLE_BTN_PAD_V 0 // 按钮垂直 paddinginitButton
#define TABLE_BTN_TEXT_PAD_V 8 // 计算页脚高度时的按钮文字垂直 paddinginitTextWaH
#define TABLE_FOOTER_PAD 16 // 页脚额外高度(底部留白)
#define TABLE_FOOTER_BLANK 8 // 页脚顶部留白
#define TABLE_PAGE_TEXT_OFFSET_X (-40) // 页码文本的临时水平修正
// === Table defaults (theme) ===
#define TABLE_DEFAULT_ROWS_PER_PAGE 5
#define TABLE_DEFAULT_BORDER_WIDTH 1
#define TABLE_DEFAULT_BORDER_COLOR RGB(0,0,0)
#define TABLE_DEFAULT_BG_COLOR RGB(255,255,255)
// === Strings (i18n ready) ===
#define TABLE_STR_PREV "上一页"
#define TABLE_STR_NEXT "下一页"
#define TABLE_STR_PAGE_PREFIX "第"
#define TABLE_STR_PAGE_MID "页/共"
#define TABLE_STR_PAGE_SUFFIX "页"
class Table :public Control
{
private:
@@ -56,24 +29,23 @@ private:
std::vector<std::string> headers; // 表格表头
std::string pageNumtext = "页码标签"; // 页码标签文本
int tableBorderWidth = TABLE_DEFAULT_BORDER_WIDTH; // 边框宽度
int tableBorderWidth = 1; // 边框宽度
std::vector<int> colWidths; // 每列的宽度
std::vector<int> lineHeights; // 每行的高度
int rowsPerPage = TABLE_DEFAULT_ROWS_PER_PAGE; // 每页显示的行数
int rowsPerPage = 5; // 每页显示的行数
int currentPage = 1; // 当前页码
int totalPages = 1; // 总页数
int totalPages = 1; // 总页数
bool isShowPageButton = true; // 是否显示翻页按钮
bool isNeedDrawHeaders = true; // 是否需要绘制表头(暂时废弃,单做保留,后期优化可能用到)
bool isNeedDrawHeaders = true; // 是否需要绘制表头
bool isNeedCellSize = true; // 是否需要计算单元格尺寸
bool isNeedButtonAndPageNum = true; // 是否需要计算翻页按钮和页码信息
std::unique_ptr<Button> prevButton; // 上一页按钮
std::unique_ptr<Button> nextButton; // 下一页按钮
std::unique_ptr<Label> pageNum; //页码文本
Button* prevButton = nullptr; // 上一页按钮
Button* nextButton = nullptr; // 下一页按钮
Label* pageNum = nullptr; //页码文本
int dX = x, dY = y; // 单元格的开始坐标
int uX = x, uY = y; // 单元格的结束坐标
@@ -82,8 +54,8 @@ private:
StellarX::FillMode tableFillMode = StellarX::FillMode::Solid; //填充模式
StellarX::LineStyle tableLineStyle = StellarX::LineStyle::Solid; // 线型
COLORREF tableBorderClor = TABLE_DEFAULT_BORDER_COLOR; // 表格边框颜色
COLORREF tableBkClor = TABLE_DEFAULT_BG_COLOR; // 表格背景颜色
COLORREF tableBorderClor = RGB(0, 0, 0); // 表格边框颜色
COLORREF tableBkClor = RGB(255, 255, 255); // 表格背景颜色
void initTextWaH(); //初始化文本像素宽度和高度
void initButton(); //初始化翻页按钮
@@ -93,24 +65,13 @@ private:
void drawHeader(); //绘制表头
void drawPageNum(); //绘制页码信息
void drawButton(); //绘制翻页按钮
// 统一解算后的内部应用入口:
// Table 需要复用自己已有的 setWidth/setX 等副作用逻辑,因此单独接管应用过程。
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
private:
//用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; };
public:
StellarX::ControlText textStyle; // 文本样式
// 修改表格运行态 X,并同步内部页脚元素位置
void setX(int x) override;
// 修改表格运行态 Y,并同步内部页脚元素位置
void setY(int y) override;
// 修改表格运行态宽度;当前支持横向拉伸
void setWidth(int width) override;
// 修改表格运行态高度;当前版本作为行为边界明确忽略纵向 Stretch
void setHeight(int height) override;
public:
// 构造一个基础表格,后续通过表头和数据初始化内容
Table(int x, int y);
~Table();
@@ -118,18 +79,11 @@ public:
void draw() override;
//事件处理
bool handleEvent(const ExMessage& msg) override;
// 只清理分页按钮的 hover / tooltip / 临时按下态,不触发表格整体重绘。
bool clearTransientMouseState() override;
// Table 重绘时会一并绘制页码 Label 和分页按钮,coverage 需要把这些内部绘制单元并入。
RECT getManagedRepaintCoverageRect() const override;
// 持久 coverage 排除分页按钮 Tooltip 等临时浮层。
RECT getManagedRepaintPersistentCoverageRect() const override;
// 设置表头
//设置表头
void setHeaders(std::initializer_list<std::string> headers);
// 设置表格数据(单行追加)
//设置表格数据
void setData(std::vector<std::string> data);
// 设置表格数据(多行追加);如需覆盖请先 clearData() 或 resetTable()
void setData(std::initializer_list<std::vector<std::string>> data);
//设置每页显示的行数
void setRowsPerPage(int rows);
@@ -145,14 +99,6 @@ public:
void setTableLineStyle(StellarX::LineStyle style);
//设置边框宽度
void setTableBorderWidth(int width);
//清空表头
void clearHeaders();
//清空表格数据
void clearData();
//清空表头和数据
void resetTable();
//窗口变化丢快照+标脏
void onWindowResize() override;
//************************** 获取属性 *****************************/
@@ -173,12 +119,12 @@ public:
//获取线型
StellarX::LineStyle getTableLineStyle() const;
//获取表头
std::vector<std::string> getHeaders() const;
std::vector<std::string> getHeaders () const;
//获取表格数据
std::vector<std::vector<std::string>> getData() const;
//获取表格边框宽度
int getTableBorderWidth() const;
//获取表格尺寸
int getTableWidth() const;
int getTableHeight() const;
};
+59
View File
@@ -0,0 +1,59 @@
/*******************************************************************************
* @类: TextBox
* @摘要: 文本框控件,支持输入和只读两种模式
* @描述:
* 提供文本输入和显示功能,集成EasyX的InputBox用于数据输入。
* 支持有限的形状样式和视觉定制。
*
* @特性:
* - 两种工作模式:输入模式和只读模式
* - 最大字符长度限制
* - 集成系统输入框简化文本输入
* - 支持四种矩形形状变体
*
* @使用场景: 数据输入、文本显示、表单字段等
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
#pragma once
#include "Control.h"
class TextBox : public Control
{
std::string text; //文本
StellarX::TextBoxmode mode; //模式
StellarX::ControlShape shape; //形状
bool click = false; //是否点击
size_t maxCharLen = 10;//最大字符长度
COLORREF textBoxBkClor = RGB(255, 255, 255); //背景颜色
COLORREF textBoxBorderClor = RGB(0,0,0); //边框颜色
public:
StellarX::ControlText textStyle; //文本样式
TextBox(int x, int y, int width, int height, std::string text = "", StellarX::TextBoxmode mode = StellarX::TextBoxmode::INPUT_MODE, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
void draw() override;
bool handleEvent(const ExMessage& msg) override;
//设置模式
void setMode(StellarX::TextBoxmode mode);
//设置可输入最大字符长度
void setMaxCharLen(size_t len);
//设置形状
void setTextBoxshape(StellarX::ControlShape shape);
//设置边框颜色
void setTextBoxBorder(COLORREF color);
//设置背景颜色
void setTextBoxBk(COLORREF color);
//设置文本
void setText(std::string text);
//获取文本
std::string getText() const;
private:
//用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; };
};
+84
View File
@@ -0,0 +1,84 @@
#pragma once
#include"Control.h"
/*******************************************************************************
* @类: Window
* @摘要: 应用程序主窗口类,管理窗口生命周期和消息循环
* @描述:
* 创建和管理应用程序主窗口,作为所有控件的根容器。
* 处理消息分发、事件循环和渲染调度。
*
* @特性:
* - 多种窗口模式配置(双缓冲、控制台等)
* - 背景图片和颜色支持
* - 集成的对话框管理系统
* - 完整的消息处理循环
* - 控件和对话框的生命周期管理
*
* @使用场景: 应用程序主窗口,GUI程序的入口和核心
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
class Window
{
int width; //窗口宽度
int height; //窗口高度
int windowMode = NULL; //窗口模式
// --- 尺寸变化去抖用 ---
int pendingW;
int pendingH;
bool needResizeDirty = false;
HWND hWnd = NULL; //窗口句柄
std::string headline; //窗口标题
COLORREF wBkcolor = BLACK; //窗口背景
IMAGE* background = nullptr; //窗口背景图片
std::string bkImageFile; //窗口背景图片文件名
std::vector<std::unique_ptr<Control>> controls; //控件管理
std::vector<std::unique_ptr<Control>> dialogs; //对话框管理
public:
bool dialogClose = false; //是否有对话框关闭
Window(int width, int height, int mode);
Window(int width, int height, int mode, COLORREF bkcloc);
Window(int width, int height, int mode , COLORREF bkcloc, std::string headline = "窗口");
~Window();
//绘制窗口
void draw();
void draw(std::string pImgFile);
//事件循环
int runEventLoop();
//设置窗口背景图片
void setBkImage(std::string pImgFile);
//设置窗口背景颜色
void setBkcolor(COLORREF c);
//设置窗口标题
void setHeadline(std::string headline);
//添加控件
void addControl(std::unique_ptr<Control> control);
//添加对话框
void addDialog(std::unique_ptr<Control> dialogs);
//检查是否已有对话框显示用于去重,防止工厂模式调用非模态对话框,多次打开污染对话框背景快照
bool hasNonModalDialogWithCaption(const std::string& caption, const std::string& message) const;
//获取窗口句柄
HWND getHwnd() const;
//获取窗口宽度
int getWidth() const;
//获取窗口高度
int getHeight() const;
//获取窗口标题
std::string getHeadline() const;
//获取窗口背景颜色
COLORREF getBkcolor() const;
//获取窗口背景图片
IMAGE* getBkImage() const;
//获取控件管理
std::vector<std::unique_ptr<Control>>& getControls();
};
+674
View File
@@ -0,0 +1,674 @@
#include "Button.h"
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)
{
initButton(text, mode, shape, RGB(202, 255, 255), RGB(171, 196, 220), RGB(255, 255, 0));
}
Button::Button(int x, int y, int width, int height, const std::string text, COLORREF ct, COLORREF cf, StellarX::ButtonMode mode, StellarX::ControlShape shape)
: Control(x, y, width, height)
{
initButton(text, mode, shape, ct, cf, RGB(255, 255, 0));
}
Button::Button(int x, int y, int width, int height, const std::string text, COLORREF ct, COLORREF cf, COLORREF ch, StellarX::ButtonMode mode, StellarX::ControlShape shape)
: Control(x, y, width, height)
{
initButton(text, mode, shape, ct, cf, ch);
}
// ====== GBK/MBCS 安全:字符边界与省略号裁切 ======
static inline int gbk_char_len(const std::string& s, size_t i)
{
unsigned char b = (unsigned char)s[i];
if (b <= 0x7F) return 1; // ASCII
if (b >= 0x81 && b <= 0xFE && i + 1 < s.size())
{
unsigned char b2 = (unsigned char)s[i + 1];
if (b2 >= 0x40 && b2 <= 0xFE && b2 != 0x7F) return 2; // 合法双字节
}
return 1; // 容错
}
static inline void rtrim_spaces_gbk(std::string& s)
{
while (!s.empty() && s.back() == ' ') s.pop_back(); // ASCII 空格
while (s.size() >= 2)
{ // 全角空格 A1 A1
unsigned char a = (unsigned char)s[s.size() - 2];
unsigned char b = (unsigned char)s[s.size() - 1];
if (a == 0xA1 && b == 0xA1) s.resize(s.size() - 2);
else break;
}
}
static inline bool is_ascii_only(const std::string& s)
{
for (unsigned char c : s) if (c > 0x7F) return false;
return true;
}
static inline bool is_word_boundary_char(unsigned char c)
{
return c == ' ' || c == '-' || c == '_' || c == '/' || c == '\\' || c == '.' || c == ':';
}
// 英文优先策略:优先在“词边界”回退,再退化到逐字符;省略号为 "..."
static std::string ellipsize_ascii_pref(const std::string& text, int maxW)
{
if (maxW <= 0) return "";
if (textwidth(LPCTSTR(text.c_str())) <= maxW) return text;
const std::string ell = "...";
int ellW = textwidth(LPCTSTR(ell.c_str()));
if (ellW > maxW)
{ // 连 ... 都放不下
std::string e = ell;
while (!e.empty() && textwidth(LPCTSTR(e.c_str())) > maxW) e.pop_back();
return e; // 可能是 ".."、"." 或 ""
}
const int limit = maxW - ellW;
// 先找到能放下的最长前缀
size_t i = 0, lastFit = 0;
while (i < text.size())
{
int clen = gbk_char_len(text, i);
size_t j = text.size() < i + (size_t)clen ? text.size() : i + (size_t)clen;
int w = textwidth(LPCTSTR(text.substr(0, j).c_str()));
if (w <= limit) { lastFit = j; i = j; }
else break;
}
if (lastFit == 0) return ell;
// 在已适配前缀范围内,向左找最近的词边界
size_t cutPos = lastFit;
for (size_t k = lastFit; k > 0; --k)
{
unsigned char c = (unsigned char)text[k - 1];
if (c <= 0x7F && is_word_boundary_char(c)) { cutPos = k - 1; break; }
}
std::string head = text.substr(0, cutPos);
rtrim_spaces_gbk(head);
head += ell;
return head;
}
// 中文优先策略:严格逐“字符”(1/2字节)回退;省略号用全角 "…"
static std::string ellipsize_cjk_pref(const std::string& text, int maxW, const char* ellipsis = "")
{
if (maxW <= 0) return "";
if (textwidth(LPCTSTR(text.c_str())) <= maxW) return text;
std::string ell = ellipsis ? ellipsis : "";
int ellW = textwidth(LPCTSTR(ell.c_str()));
if (ellW > maxW)
{ // 连省略号都放不下
std::string e = ell;
while (!e.empty() && textwidth(LPCTSTR(e.c_str())) > maxW) e.pop_back();
return e;
}
const int limit = maxW - ellW;
size_t i = 0, lastFit = 0;
while (i < text.size())
{
int clen = gbk_char_len(text, i);
size_t j = text.size() < i + (size_t)clen ? text.size() : i + (size_t)clen;
int w = textwidth(LPCTSTR(text.substr(0, j).c_str()));
if (w <= limit) { lastFit = j; i = j; }
else break;
}
if (lastFit == 0) return ell;
std::string head = text.substr(0, lastFit);
rtrim_spaces_gbk(head);
head += ell;
return head;
}
void Button::initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch)
{
this->text = text;
this->mode = mode;
this->shape = shape;
this->buttonTrueColor = ct;
this->buttonFalseColor = cf;
this->buttonHoverColor = ch;
this->click = false;
this->hover = false;
// === Tooltip 默认:文本=按钮文本;白底黑字;不透明;用当前按钮字体样式 ===
tipTextClick = tipTextOn = tipTextOff = this->text;
tipLabel.setText(tipTextClick);
tipLabel.setTextColor(RGB(167, 170, 172));
tipLabel.setTextBkColor(RGB(255, 255, 255));
tipLabel.setTextdisap(false);
tipLabel.textStyle = this->textStyle; // 复用按钮字体样式
}
Button::~Button()
{
if (buttonFileIMAGE)
delete buttonFileIMAGE;
buttonFileIMAGE = nullptr;
}
void Button::draw()
{
if (dirty && show)
{
//保存当前样式和颜色
saveStyle();
if (StellarX::ButtonMode::DISABLED == mode) //设置禁用按钮色
{
setfillcolor(DISABLEDCOLOUR);
textStyle.bStrikeOut = true;
}
else
{
// 点击状态优先级最高,然后是悬停状态,最后是默认状态
COLORREF col = click ? buttonTrueColor : (hover ? buttonHoverColor : buttonFalseColor);
setfillcolor(col);
}
//
//设置字体背景色透明
setbkmode(TRANSPARENT);
//边框颜色
setlinecolor(buttonBorderColor);
//设置字体颜色
settextcolor(textStyle.color);
//设置字体样式
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
if (needCutText)
cutButtonText();
//获取字符串像素高度和宽度
if ((this->oldtext_width != this->text_width || this->oldtext_height != this->text_height)
|| (-1 == oldtext_width && oldtext_height == -1))
{
if(isUseCutText)
{
this->oldtext_width = this->text_width = textwidth(LPCTSTR(this->cutText.c_str()));
this->oldtext_height = this->text_height = textheight(LPCTSTR(this->cutText.c_str()));
}
else
{
this->oldtext_width = this->text_width = textwidth(LPCTSTR(this->text.c_str()));
this->oldtext_height = this->text_height = textheight(LPCTSTR(this->text.c_str()));
}
}
//设置按钮填充模式
setfillstyle((int)buttonFillMode, (int)buttonFillIma, buttonFileIMAGE);
//根据按钮形状绘制
switch (shape)
{
case StellarX::ControlShape::RECTANGLE://有边框填充矩形
fillrectangle(x, y, x + width, y + height);
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
:outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::B_RECTANGLE://无边框填充矩形
solidrectangle(x, y, x + width, y + height);
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
:outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::ROUND_RECTANGLE://有边框填充圆角矩形
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);
isUseCutText? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
:outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::B_ROUND_RECTANGLE://无边框填充圆角矩形
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);
isUseCutText? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
:outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::CIRCLE://有边框填充圆形
fillcircle(x + width / 2, y + height / 2, min(width, height) / 2);
isUseCutText? outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(cutText.c_str()))
:outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::B_CIRCLE://无边框填充圆形
solidcircle(x + width / 2, y + height / 2, min(width, height) / 2);
isUseCutText ? outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(cutText.c_str()))
:outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::ELLIPSE://有边框填充椭圆
fillellipse(x, y, x + width, y + height);
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
:outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::B_ELLIPSE://无边框填充椭圆
solidellipse(x, y, x + width, y + height);
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
:outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
}
restoreStyle();//恢复默认字体样式和颜色
dirty = false; //标记按钮不需要重绘
}
}
// 处理鼠标事件,检测点击和悬停状态
// 根据按钮模式和形状进行不同的处理
bool Button::handleEvent(const ExMessage& msg)
{
if (!show)
return false;
bool oldHover = hover;
bool oldClick = click;
bool consume = false;//是否消耗事件
// 记录鼠标位置(用于tip定位)
if (msg.message == WM_MOUSEMOVE)
{
lastMouseX = msg.x;
lastMouseY = msg.y;
}
// 检测悬停状态(根据不同形状)
switch (shape)
{
case StellarX::ControlShape::RECTANGLE:
case StellarX::ControlShape::B_RECTANGLE:
case StellarX::ControlShape::ROUND_RECTANGLE:
case StellarX::ControlShape::B_ROUND_RECTANGLE:
hover = (msg.x > x && msg.x < (x + width) && msg.y > y && msg.y < (y + height));
break;
case StellarX::ControlShape::CIRCLE:
case StellarX::ControlShape::B_CIRCLE:
hover = isMouseInCircle(msg.x, msg.y, x + width / 2, y + height / 2, min(width, height) / 2);
break;
case StellarX::ControlShape::ELLIPSE:
case StellarX::ControlShape::B_ELLIPSE:
hover = isMouseInEllipse(msg.x, msg.y, x, y, x + width, y + height);
break;
}
// 处理鼠标点击事件
if (msg.message == WM_LBUTTONDOWN && hover && mode != StellarX::ButtonMode::DISABLED)
{
if (mode == StellarX::ButtonMode::NORMAL)
{
click = true;
dirty = true;
consume = true;
}
else if (mode == StellarX::ButtonMode::TOGGLE)
{
// TOGGLE模式在鼠标释放时处理
}
}
// NORMAL 模式:鼠标在按钮上释放时才触发点击回调,如果移出区域则取消点击状态。
// TOGGLE 模式:在释放时切换状态,并触发相应的开/关回调。
else if (msg.message == WM_LBUTTONUP && hover && mode != StellarX::ButtonMode::DISABLED)
{
hideTooltip(); // 隐藏悬停提示
if (mode == StellarX::ButtonMode::NORMAL && click)
{
if (onClickCallback) onClickCallback();
click = false;
dirty = true;
consume = true;
hideTooltip();
// 清除消息队列中积压的鼠标和键盘消息,防止本次点击事件被重复处理
flushmessage(EX_MOUSE | EX_KEY);
}
else if (mode == StellarX::ButtonMode::TOGGLE)
{
click = !click;
if (click && onToggleOnCallback) onToggleOnCallback();
else if (!click && onToggleOffCallback) onToggleOffCallback();
dirty = true;
consume = true;
refreshTooltipTextForState();
hideTooltip();
// 清除消息队列中积压的鼠标和键盘消息,防止本次点击事件被重复处理
flushmessage(EX_MOUSE | EX_KEY);
}
}
// 处理鼠标移出区域的情况
else if (msg.message == WM_MOUSEMOVE)
{
if (!hover && mode == StellarX::ButtonMode::NORMAL && click)
{
click = false;
dirty = true;
}
else if (hover != oldHover)
dirty = true;
}
if (tipEnabled)
{
if (hover && !oldHover)
{
// 刚刚进入悬停:开计时,暂不显示
tipHoverTick = GetTickCount64();
tipVisible = false;
}
if (!hover && oldHover)
{
// 刚移出:立即隐藏
hideTooltip();
}
if (hover && !tipVisible)
{
// 到点就显示
if (GetTickCount64() - tipHoverTick >= (ULONGLONG)tipDelayMs)
{
tipVisible = true;
// 定位(跟随鼠标 or 相对按钮)
int tipX = tipFollowCursor ? (lastMouseX + tipOffsetX) : lastMouseX;
int tipY = tipFollowCursor ? (lastMouseY + tipOffsetY) : y + height;
// 设置文本(用户可能动态改了提示文本
if (tipUserOverride)
{
if (mode == StellarX::ButtonMode::NORMAL)
tipLabel.setText(tipTextClick);
else if (mode == StellarX::ButtonMode::TOGGLE)
tipLabel.setText(click ? tipTextOn : tipTextOff);
}
else
if (mode == StellarX::ButtonMode::TOGGLE)
tipLabel.setText(click ? tipTextOn : tipTextOff);
// 设置位置
tipLabel.setX(tipX);
tipLabel.setY(tipY);
// 标记需要绘制
tipLabel.setDirty(true);
}
}
}
// 如果状态发生变化,标记需要重绘
if (hover != oldHover || click != oldClick)
dirty = true;
// 如果需要重绘,立即执行
if (dirty)
draw();
if(tipEnabled && tipVisible)
tipLabel.draw();
return consume;
}
void Button::setOnClickListener(const std::function<void()>&& callback)
{
this->onClickCallback = callback;
}
void Button::setOnToggleOnListener(const std::function<void()>&& callback)
{
this->onToggleOnCallback = callback;
}
void Button::setOnToggleOffListener(const std::function<void()>&& callback)
{
this->onToggleOffCallback = callback;
}
void Button::setbuttonMode(StellarX::ButtonMode mode)
{
//取值范围参考 buttMode的枚举注释
this->mode = mode;
}
void Button::setROUND_RECTANGLEwidth(int width)
{
rouRectangleSize.ROUND_RECTANGLEwidth = width;
this->dirty = true; // 标记需要重绘
}
void Button::setROUND_RECTANGLEheight(int height)
{
rouRectangleSize.ROUND_RECTANGLEheight = height;
this->dirty = true; // 标记需要重绘
}
bool Button::isClicked() const
{
return this->click;
}
void Button::setFillMode(StellarX::FillMode mode)
{
this->buttonFillMode = mode;
this->dirty = true; // 标记需要重绘
}
void Button::setFillIma(StellarX::FillStyle ima)
{
buttonFillIma = ima;
this->dirty = true;
}
void Button::setFillIma(std::string imaNAme)
{
if (buttonFileIMAGE)
{
delete buttonFileIMAGE;
buttonFileIMAGE = nullptr;
}
buttonFileIMAGE = new IMAGE;
loadimage(buttonFileIMAGE, imaNAme.c_str(),width,height);
this->dirty = true;
}
void Button::setButtonBorder(COLORREF Border)
{
buttonBorderColor = Border;
this->dirty = true;
}
void Button::setButtonFalseColor(COLORREF color)
{
this->buttonFalseColor = color;
this->dirty = true;
}
void Button::setButtonText(const char* text)
{
this->text = std::string(text);
this->text_width = textwidth(LPCTSTR(this->text.c_str()));
this->text_height = textheight(LPCTSTR(this->text.c_str()));
this->dirty = true;
this->needCutText = true;
if (!tipUserOverride)
tipTextClick = tipTextOn = tipTextOff = text;
}
void Button::setButtonText(std::string text)
{
this->text = text;
this->text_width = textwidth(LPCTSTR(this->text.c_str()));
this->text_height = textheight(LPCTSTR(this->text.c_str()));
this->dirty = true; // 标记需要重绘
this->needCutText = true;
if (!tipUserOverride)
tipTextClick = tipTextOn = tipTextOff = text;
}
void Button::setButtonShape(StellarX::ControlShape shape)
{
this->shape = shape;
this->dirty = true;
this->needCutText = true;
}
//允许通过外部函数修改按钮的点击状态,并执行相应的回调函数
void Button::setButtonClick(BOOL click)
{
this->click = click;
if (mode == StellarX::ButtonMode::NORMAL && click)
{
if (onClickCallback) onClickCallback();
dirty = true;
hideTooltip();
// 清除消息队列中积压的鼠标和键盘消息,防止本次点击事件被重复处理
flushmessage(EX_MOUSE | EX_KEY);
}
else if (mode == StellarX::ButtonMode::TOGGLE)
{
if (click && onToggleOnCallback) onToggleOnCallback();
else if (!click && onToggleOffCallback) onToggleOffCallback();
dirty = true;
refreshTooltipTextForState();
hideTooltip();
// 清除消息队列中积压的鼠标和键盘消息,防止本次点击事件被重复处理
flushmessage(EX_MOUSE | EX_KEY);
}
if (dirty)
draw();
}
std::string Button::getButtonText() const
{
return this->text;
}
const char* Button::getButtonText_c() const
{
return this->text.c_str();
}
StellarX::ButtonMode Button::getButtonMode() const
{
return this->mode;
}
StellarX::ControlShape Button::getButtonShape() const
{
return this->shape;
}
StellarX::FillMode Button::getFillMode() const
{
return this->buttonFillMode;
}
StellarX::FillStyle Button::getFillIma() const
{
return this->buttonFillIma;
}
IMAGE* Button::getFillImaImage() const
{
return this->buttonFileIMAGE;
}
COLORREF Button::getButtonBorder() const
{
return this->buttonBorderColor;
}
COLORREF Button::getButtonTextColor() const
{
return this->textStyle.color;
}
StellarX::ControlText Button::getButtonTextStyle() const
{
return this->textStyle;
}
int Button::getButtonWidth() const
{
return this->width;
}
int Button::getButtonHeight() const
{
return this->height;
}
bool Button::isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius)
{
double dis = sqrt(pow(mouseX - x, 2) + pow(mouseY - y, 2));
if (dis <= radius)
return true;
else
return false;
}
bool Button::isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height)
{
int centerX = (x + width) / 2;
int centerY = (y + height) / 2;
int majorAxis = (width - x) / 2;
int minorAxis = (height - y) / 2;
double dx = mouseX - centerX;
double dy = mouseY - centerY;
double normalizedDistance = (dx * dx) / (majorAxis * majorAxis) + (dy * dy) / (minorAxis * minorAxis);
// 判断鼠标是否在椭圆内
if (normalizedDistance <= 1.0)
return true;
else
return false;
}
void Button::cutButtonText()
{
const int contentW = 1 > this->width - 2 * padX ? 1 : this->width - 2 * padX;
// 放得下:不截断,直接用原文
if (textwidth(LPCTSTR(this->text.c_str())) <= contentW) {
isUseCutText = false;
needCutText = false;
cutText.clear();
return;
}
// 放不下:按语言偏好裁切(ASCII→词边界;CJK→逐字符,不撕裂双字节)
if (is_ascii_only(this->text))
{
cutText = ellipsize_ascii_pref(this->text, contentW); // "..."
}
else
{
cutText = ellipsize_cjk_pref(this->text, contentW, ""); // 全角省略号
}
isUseCutText = true;
needCutText = false;
}
void Button::hideTooltip()
{
if (tipVisible)
{
tipVisible = false;
tipLabel.hide(); // 还原快照+作废,防止残影
tipHoverTick = GetTickCount64(); // 重置计时基线
}
}
void Button::refreshTooltipTextForState()
{
if (!tipUserOverride) return;
if (tipUserOverride) return; // 用户显式设置过 tipText,保持不变
if(mode==StellarX::ButtonMode::NORMAL)
tipLabel.setText(tipTextClick);
else if(mode==StellarX::ButtonMode::TOGGLE)
tipLabel.setText(click ? tipTextOn : tipTextOff);
}
+121
View File
@@ -0,0 +1,121 @@
#include "Canvas.h"
Canvas::Canvas()
:Control(0, 0, 100, 100) {}
Canvas::Canvas(int x, int y, int width, int height)
:Control(x, y, width, height) {}
void Canvas::clearAllControls()
{
controls.clear();
}
void Canvas::draw()
{
if (!dirty && !show)return;
saveStyle();
setlinecolor(canvasBorderClor);//设置线色
setfillcolor(canvasBkClor);//设置填充色
setfillstyle((int)canvasFillMode);//设置填充模式
setlinestyle((int)canvasLineStyle, canvaslinewidth);
//根据画布形状绘制
switch (shape)
{
case StellarX::ControlShape::RECTANGLE:
fillrectangle(x,y,x+width,y+height);//有边框填充矩形
break;
case StellarX::ControlShape::B_RECTANGLE:
solidrectangle(x, y, x + width, y + height);//无边框填充矩形
break;
case StellarX::ControlShape::ROUND_RECTANGLE:
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//有边框填充圆角矩形
break;
case StellarX::ControlShape::B_ROUND_RECTANGLE:
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//无边框填充圆角矩形
break;
}
// 绘制所有子控件
for (auto& control : controls)
{
control->setDirty(true);
control->draw();
}
restoreStyle();
dirty = false; //标记画布不需要重绘
}
bool Canvas::handleEvent(const ExMessage& msg)
{
if(!show)return false;
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
if ((*it)->handleEvent(msg))
return true; // 事件被消费短路传递,立即返回true 否则返回false
return false;
}
void Canvas::addControl(std::unique_ptr<Control> control)
{
controls.push_back(std::move(control));
dirty = true;
}
void Canvas::setShape(StellarX::ControlShape shape)
{
switch (shape)
{
case StellarX::ControlShape::RECTANGLE:
case StellarX::ControlShape::B_RECTANGLE:
case StellarX::ControlShape::ROUND_RECTANGLE:
case StellarX::ControlShape::B_ROUND_RECTANGLE:
this->shape = shape;
dirty = true;
break;
case StellarX::ControlShape::CIRCLE:
case StellarX::ControlShape::B_CIRCLE:
case StellarX::ControlShape::ELLIPSE:
case StellarX::ControlShape::B_ELLIPSE:
this->shape = StellarX::ControlShape::RECTANGLE;
dirty = true;
break;
}
}
void Canvas::setCanvasfillMode(StellarX::FillMode mode)
{
this->canvasFillMode = mode;
dirty = true;
}
void Canvas::setBorderColor(COLORREF color)
{
this->canvasBorderClor = color;
dirty = true;
}
void Canvas::setCanvasBkColor(COLORREF color)
{
this->canvasBkClor = color;
dirty = true;
}
void Canvas::setCanvasLineStyle(StellarX::LineStyle style)
{
this->canvasLineStyle = style;
dirty = true;
}
void Canvas::setLinewidth(int width)
{
this->canvaslinewidth = width;
dirty = true;
}
+114
View File
@@ -0,0 +1,114 @@
#include "Control.h"
#include<assert.h>
StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text)
{
{
nHeight = text.nHeight;
nWidth = text.nWidth;
lpszFace = text.lpszFace;
color = text.color;
nEscapement = text.nEscapement;
nOrientation = text.nOrientation;
nWeight = text.nWeight;
bItalic = text.bItalic;
bUnderline = text.bUnderline;
bStrikeOut = text.bStrikeOut;
return *this;
}
}
bool StellarX::ControlText::operator!=(const ControlText& text)
{
if(nHeight != text.nHeight)
return true;
else if (nWidth != text.nWidth)
return true;
else if (lpszFace != text.lpszFace)
return true;
else if (color != text.color)
return true;
else if (nEscapement != text.nEscapement)
return true;
else if (nOrientation != text.nOrientation)
return true;
else if (nWeight != text.nWeight)
return true;
else if (bItalic != text.bItalic)
return true;
else if (bUnderline != text.bUnderline)
return true;
else if (bStrikeOut != text.bStrikeOut)
return true;
return false;
}
// 保存当前的绘图状态(字体、颜色、线型等)
// 在控件绘制前调用,确保不会影响全局绘图状态
void Control::saveStyle()
{
gettextstyle(currentFont); // 获取当前字体样式
*currentColor = gettextcolor(); // 获取当前字体颜色
*currentBorderColor = getlinecolor(); //保存当前边框颜色
getlinestyle(currentLineStyle); //保存当前线型
*currentBkColor = getfillcolor(); //保存当前填充色
}
// 恢复之前保存的绘图状态
// 在控件绘制完成后调用,恢复全局绘图状态
void Control::restoreStyle()
{
settextstyle(currentFont); // 恢复默认字体样式
settextcolor(*currentColor); // 恢复默认字体颜色
setfillcolor(*currentBkColor);
setlinestyle(currentLineStyle);
setlinecolor(*currentBorderColor);
setfillstyle(BS_SOLID);//恢复填充
}
void Control::saveBackground(int x, int y, int w, int h)
{
if (w <= 0 || h <= 0) return;
saveBkX = x; saveBkY = y; saveWidth = w; saveHeight = h;
if (saveBkImage)
{
//尺寸变了才重建,避免反复 new/delete
if (saveBkImage->getwidth() != w || saveBkImage->getheight() != h)
{
delete saveBkImage; saveBkImage = nullptr;
}
}
if (!saveBkImage) saveBkImage = new IMAGE(w, h);
SetWorkingImage(nullptr); // ★抓屏幕
getimage(saveBkImage, x, y, w, h);
hasSnap = true;
}
void Control::restBackground()
{
if (!hasSnap || !saveBkImage) return;
// 直接回贴屏幕(与抓取一致)
SetWorkingImage(nullptr);
putimage(saveBkX, saveBkY, saveBkImage);
}
void Control::discardBackground()
{
if (saveBkImage)
{
delete saveBkImage;
saveBkImage = nullptr;
}
hasSnap = false; saveWidth = saveHeight = 0;
}
void Control::updateBackground()
{
if (saveBkImage)
{
delete saveBkImage;
saveBkImage = nullptr;
}
hasSnap = false; saveWidth = saveHeight = 0;
}
+157 -272
View File
@@ -1,25 +1,20 @@
#include "Dialog.h"
#include "SxLog.h"
Dialog::Dialog(Window& h, std::string text, std::string message, StellarX::MessageBoxType type, bool modal)
: Canvas(), message(message), type(type), modal(modal), hWnd(h), titleText(text)
Dialog::Dialog(Window& h,std::string text,std::string message, StellarX::MessageBoxType type, bool modal)
: Canvas(),message(message), type(type), modal(modal), hWnd(h), titleText(text)
{
this->id = "Dialog";
// Dialog 当前阶段只做“居中 + 自管内部 chrome”,
// 不参与外部 Stretch;宿主窗口变化时只重新居中。
this->layoutCapability.allowStretchX = false;
this->layoutCapability.allowStretchY = false;
setHostWindow(&hWnd);
show = false;
}
Dialog::~Dialog()
{
}
void Dialog::draw()
{
if (!show)
if(!show)
{
// 如果对话框不可见且需要清理,执行清理
if (pendingCleanup && !isCleaning)
@@ -28,55 +23,63 @@ void Dialog::draw()
}
return;
}
// 如果需要初始化,则执行初始化
if (needsInitialization && show)
{
initDialogSize();
needsInitialization = false;
}
// 如果需要初始化,则执行初始化
if (needsInitialization && show)
{
initDialogSize();
needsInitialization = false;
}
if (dirty && show)
{
// 保存当前绘图状态
saveStyle();
if (dirty && show)
{
// 保存当前绘图状态
saveStyle();
// 保存背景(仅在第一次绘制时)
if (saveBkImage == nullptr)
saveBackground((x - BorderWidth), (y - BorderWidth), (width + 2 * BorderWidth), (height + 2 * BorderWidth));
Canvas::setBorderColor(this->borderColor);
Canvas::setLinewidth(BorderWidth);
Canvas::setLinewidth(this->BorderWidth);
Canvas::setCanvasBkColor(this->backgroundColor);
Canvas::setShape(StellarX::ControlShape::ROUND_RECTANGLE);
//设置所有控件为脏状态
for(auto& c :this->controls)
c->setDirty(true);
Canvas::draw();
//绘制消息文本
settextcolor(textStyle.color);
setbkmode(TRANSPARENT);
//设置字体样式
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
outtextxy(x + 5, y + 5, LPCTSTR(titleText.c_str()));
int ty = y + closeButtonHeight + titleToTextMargin; // 文本起始Y坐标
for (auto& line : lines)
for (auto line:lines)
{
int tx = this->x + ((this->width - textwidth(line.c_str())) / 2); // 文本起始X坐标
outtextxy(tx, ty, LPCTSTR(line.c_str()));
ty = ty + textheight(LPCTSTR(line.c_str())) + 5; // 每行文本高度加5像素间距
}
// 恢复绘图状态
restoreStyle();
// 恢复绘图状态
restoreStyle();
dirty = false;
}
dirty = false;
}
}
bool Dialog::handleEvent(const ExMessage& msg)
{
bool consume = false;
resetEventVisualChanged();
if (!show)
{
if (pendingCleanup && !isCleaning)
@@ -89,30 +92,18 @@ bool Dialog::handleEvent(const ExMessage& msg)
// 如果正在清理或标记为待清理,则不处理事件
if (pendingCleanup || isCleaning)
return false;
const bool isMouseMessage =
msg.message == WM_MOUSEMOVE ||
msg.message == WM_LBUTTONDOWN ||
msg.message == WM_LBUTTONUP;
const bool insideDialog =
msg.x >= x && msg.x <= x + width &&
msg.y >= y && msg.y <= y + height;
// 模态对话框区域外鼠标事件不允许落到底层背景。
if (modal && isMouseMessage && !insideDialog)
// 模态对话框:点击对话框外部区域时,发出提示音(\a)并吞噬该事件,不允许操作背景内容。
if (modal && msg.message == WM_LBUTTONUP &&
(msg.x < x || msg.x > x + width || msg.y < y || msg.y > y + height))
{
if (msg.message == WM_LBUTTONUP)
std::cout << "\a" << std::endl;
std::cout << "\a" << std::endl;
// 模态对话框不允许点击外部区域
return true;
}
// 将事件传递给子控件处理
if (!consume)
consume = Canvas::handleEvent(msg);
// 对话框矩形范围内的鼠标事件一律由对话框吞掉,避免穿透到底层控件。
if (isMouseMessage && insideDialog)
consume = true;
if (!consume)
consume = Canvas::handleEvent(msg);
// 每次事件处理后检查是否需要执行延迟清理
if (pendingCleanup && !isCleaning)
@@ -123,19 +114,27 @@ bool Dialog::handleEvent(const ExMessage& msg)
void Dialog::SetTitle(const std::string& title)
{
this->titleText = title;
invalidateLayout(true);
if (this->title)
{
this->title->setText(title);
}
dirty = true;
}
void Dialog::SetMessage(const std::string& message)
{
this->message = message;
invalidateLayout(true);
splitMessageLines();
getTextSize();
dirty = true;
}
void Dialog::SetType(StellarX::MessageBoxType type)
{
this->type = type;
invalidateLayout(true);
// 重新初始化按钮
initButtons();
dirty = true;
}
void Dialog::SetModal(bool modal)
@@ -143,6 +142,7 @@ void Dialog::SetModal(bool modal)
this->modal = modal;
}
void Dialog::SetResult(StellarX::MessageBoxResult result)
{
this->result = result;
@@ -162,68 +162,44 @@ void Dialog::Show()
{
if (pendingCleanup)
performDelayedCleanup();
SX_LOGI("Dialog") << SX_T("对话框弹出:是否模态=","Dialog::Show: modal=") << (modal ? 1 : 0);
show = true;
dirty = true;
needsInitialization = true;
hWnd.dialogOpen = true;// 通知窗口有对话框打开
close = false;
shouldClose = false;
if (modal)
{
// 模态对话框需要阻塞当前线程直到对话框关闭
// 记录当前窗口客户区尺寸,供轮询对比
RECT rc0;
GetClientRect(hWnd.getHwnd(), &rc0);
int lastW = rc0.right - rc0.left;
int lastH = rc0.bottom - rc0.top;
while (show)
while (show && !close)
{
// ① 轮询窗口尺寸(不依赖 WM_SIZE)
RECT rc;
GetClientRect(hWnd.getHwnd(), &rc);
const int cw = rc.right - rc.left;
const int ch = rc.bottom - rc.top;
if (cw != lastW || ch != lastH)
{
lastW = cw;
lastH = ch;
SX_LOGD("Resize") <<SX_T("模态对话框检测到窗口大小变化:(", "Modal dialog detected window size change: (") << cw << "x" << ch << ")";
// 通知父窗口:有新尺寸 → 标记 needResizeDirty
hWnd.scheduleResizeFromModal(cw, ch);
// 立即统一收口:父窗重绘 背景+普通控件(不会画到这只模态)
hWnd.pumpResizeIfNeeded();
// 这只模态只重新居中,不参与拉伸;背景快照需要在新位置重抓。
recenterInHostWindow();
}
// ② 处理这只对话框的鼠标/键盘(沿用原来 EX_MOUSE | EX_KEY
// 处理消息
ExMessage msg;
if (peekmessage(&msg, EX_MOUSE | EX_KEY))
{
handleEvent(msg);
if (!show)
// 检查是否需要关闭
if (shouldClose)
{
Close();
break;
}
}
// ③ 最后一笔:只画这只模态,保证永远在最上层
// 重绘
if (dirty)
{
BeginBatchDraw();
this->draw(); // 注意:不要 requestRepaint(parent),只画自己
EndBatchDraw();
dirty = false;
draw();
FlushBatchDraw();
}
// 避免CPU占用过高
Sleep(10);
}
// 模态对话框关闭后执行清理
if (pendingCleanup && !isCleaning)
performDelayedCleanup();
}
@@ -232,43 +208,41 @@ void Dialog::Show()
dirty = true;
}
void Dialog::Close()
{
if (!show) return;
show = false;
close = true;
dirty = true;
pendingCleanup = true; // 只标记需要清理,不立即执行
auto& c = hWnd.getControls();
for(auto& control:c)
control->setDirty(true);
// 工厂模式下非模态触发回调 返回结果
if (resultCallback && !modal)
if (resultCallback&& !modal)
resultCallback(this->result);
}
void Dialog::recenterInHostWindow()
void Dialog::setInitialization(bool init)
{
if (!show)
return;
// 尚未完成首次初始化时,保持“延迟初始化”语义,由首次 draw 统一创建子控件。
if (needsInitialization || width <= 0 || height <= 0)
if (init)
{
dirty = true;
return;
initDialogSize();
saveBackground((x - BorderWidth), (y - BorderWidth), (width + 2 * BorderWidth), (height + 2 * BorderWidth));
}
const int newX = (hWnd.getWidth() - width) / 2;
const int newY = (hWnd.getHeight() - height) / 2;
invalidateBackgroundSnapshot();
x = newX;
y = newY;
rebuildChrome();
dirty = true;
}
void Dialog::initButtons()
{
controls.clear();
switch (this->type)
{
case StellarX::MessageBoxType::OK: // 只有确定按钮
@@ -276,49 +250,48 @@ void Dialog::initButtons()
auto okbutton = createDialogButton((this->x + (this->width - (functionButtonWidth * buttonNum + buttonMargin * (buttonNum - 1))) / 2),
((this->y + (this->height - buttonAreaHeight)) + (buttonAreaHeight - functionButtonHeight) / 2),
"确定"
);
);
okbutton->setOnClickListener([this]()
{
this->SetResult(StellarX::MessageBoxResult::OK);
this->hWnd.dialogClose = true;
this->Close(); });
okbutton->textStyle = this->textStyle;
this->addControl(std::move(okbutton));
}
break;
break;
case StellarX::MessageBoxType::OKCancel: // 确定和取消按钮
{
auto okButton = createDialogButton(
(this->x + (this->width - (functionButtonWidth * buttonNum + buttonMargin * (buttonNum - 1))) / 2),
((this->y + (this->height - buttonAreaHeight)) + (buttonAreaHeight - functionButtonHeight) / 2),
"确定"
);
((this->y + (this->height - buttonAreaHeight)) + (buttonAreaHeight - functionButtonHeight) / 2),
"确定"
);
okButton->setOnClickListener([this]()
{
this->SetResult(StellarX::MessageBoxResult::OK);
this->hWnd.dialogClose = true;
this->Close(); });
this->Close(); });
auto cancelButton = createDialogButton(
(okButton.get()->getX() + okButton.get()->getWidth() + buttonMargin),
okButton.get()->getY(),
(okButton.get()->getX() + okButton.get()->getButtonWidth() + buttonMargin),
okButton.get()->getY(),
"取消"
);
cancelButton->setOnClickListener([this]()
{
this->SetResult(StellarX::MessageBoxResult::Cancel);
this->hWnd.dialogClose = true;
this->Close(); });
this->Close(); });
okButton->textStyle = this->textStyle;
cancelButton->textStyle = this->textStyle;
this->addControl(std::move(okButton));
this->addControl(std::move(cancelButton));
}
break;
break;
case StellarX::MessageBoxType::YesNo: // 是和否按钮
{
auto yesButton = createDialogButton(
@@ -330,10 +303,10 @@ void Dialog::initButtons()
{
this->SetResult(StellarX::MessageBoxResult::Yes);
this->hWnd.dialogClose = true;
this->Close(); });
this->Close(); });
auto noButton = createDialogButton(
(yesButton.get()->getX() + yesButton.get()->getWidth() + buttonMargin),
(yesButton.get()->getX() + yesButton.get()->getButtonWidth() + buttonMargin),
yesButton.get()->getY(),
""
);
@@ -341,7 +314,7 @@ void Dialog::initButtons()
{
this->SetResult(StellarX::MessageBoxResult::No);
this->hWnd.dialogClose = true;
this->Close(); });
this->Close(); });
yesButton->textStyle = this->textStyle;
noButton->textStyle = this->textStyle;
@@ -349,7 +322,7 @@ void Dialog::initButtons()
this->addControl(std::move(yesButton));
this->addControl(std::move(noButton));
}
break;
break;
case StellarX::MessageBoxType::YesNoCancel: // 是、否和取消按钮
{
auto yesButton = createDialogButton(
@@ -361,21 +334,21 @@ void Dialog::initButtons()
{
this->SetResult(StellarX::MessageBoxResult::Yes);
this->hWnd.dialogClose = true;
this->Close(); });
this->Close(); });
auto noButton = createDialogButton(
yesButton.get()->getX() + yesButton.get()->getWidth() + buttonMargin,
yesButton.get()->getY(),
yesButton.get()->getX() + yesButton.get()->getButtonWidth() + buttonMargin,
yesButton.get()->getY(),
""
);
noButton->setOnClickListener([this]()
{
this->SetResult(StellarX::MessageBoxResult::No);
this->hWnd.dialogClose = true;
this->Close(); });
this->Close(); });
auto cancelButton = createDialogButton(
noButton.get()->getX() + noButton.get()->getWidth() + buttonMargin,
noButton.get()->getX() + noButton.get()->getButtonWidth() + buttonMargin,
noButton.get()->getY(),
"取消"
);
@@ -383,17 +356,18 @@ void Dialog::initButtons()
{
this->SetResult(StellarX::MessageBoxResult::Cancel);
this->hWnd.dialogClose = true;
this->Close(); });
this->Close(); });
yesButton->textStyle = this->textStyle;
noButton->textStyle = this->textStyle;
cancelButton->textStyle = this->textStyle;
this->addControl(std::move(yesButton));
this->addControl(std::move(noButton));
this->addControl(std::move(cancelButton));
}
break;
break;
case StellarX::MessageBoxType::RetryCancel: // 重试和取消按钮
{
auto retryButton = createDialogButton(
@@ -405,18 +379,18 @@ void Dialog::initButtons()
{
this->SetResult(StellarX::MessageBoxResult::Retry);
this->hWnd.dialogClose = true;
this->Close(); });
this->Close(); });
auto cancelButton = createDialogButton(
retryButton.get()->getX() + retryButton.get()->getWidth() + buttonMargin,
retryButton.get()->getY(),
retryButton.get()->getX() + retryButton.get()->getButtonWidth() + buttonMargin,
retryButton.get()->getY(),
"取消"
);
cancelButton->setOnClickListener([this]()
{
this->SetResult(StellarX::MessageBoxResult::Cancel);
this->hWnd.dialogClose = true;
this->Close(); });
this->Close(); });
retryButton->textStyle = this->textStyle;
cancelButton->textStyle = this->textStyle;
@@ -424,11 +398,11 @@ void Dialog::initButtons()
this->addControl(std::move(retryButton));
this->addControl(std::move(cancelButton));
}
break;
break;
case StellarX::MessageBoxType::AbortRetryIgnore: // 中止、重试和忽略按钮
{
auto abortButton = createDialogButton(
(this->x + (this->width - (functionButtonWidth * buttonNum + buttonMargin * (buttonNum - 1))) / 2),
(this->x + (this->width - (functionButtonWidth * buttonNum + buttonMargin* (buttonNum-1))) / 2),
((this->y + (this->height - buttonAreaHeight)) + (buttonAreaHeight - functionButtonHeight) / 2),
"中止"
);
@@ -436,21 +410,21 @@ void Dialog::initButtons()
{
this->SetResult(StellarX::MessageBoxResult::Abort);
this->hWnd.dialogClose = true;
this->Close();
this->Close();
});
auto retryButton = createDialogButton(
abortButton.get()->getX() + abortButton.get()->getWidth() + buttonMargin,
abortButton.get()->getY(),
abortButton.get()->getX() + abortButton.get()->getButtonWidth() + buttonMargin,
abortButton.get()->getY(),
"重试"
);
retryButton->setOnClickListener([this]()
{
this->SetResult(StellarX::MessageBoxResult::Retry);
this->hWnd.dialogClose = true;
this->Close();
this->Close();
});
auto ignoreButton = createDialogButton(
retryButton.get()->getX() + retryButton.get()->getWidth() + buttonMargin,
retryButton.get()->getX() + retryButton.get()->getButtonWidth() + buttonMargin,
retryButton.get()->getY(),
"忽略"
);
@@ -458,7 +432,7 @@ void Dialog::initButtons()
{
this->SetResult(StellarX::MessageBoxResult::Ignore);
this->hWnd.dialogClose = true;
this->Close();
this->Close();
});
abortButton->textStyle = this->textStyle;
@@ -469,7 +443,7 @@ void Dialog::initButtons()
this->addControl(std::move(retryButton));
this->addControl(std::move(ignoreButton));
}
break;
break;
}
}
@@ -478,7 +452,7 @@ void Dialog::initCloseButton()
//初始化关闭按钮
auto but = std::make_unique<Button>
(
(this->x + this->width - closeButtonWidth) - 3, (this->y + 3), closeButtonWidth - 1, closeButtonHeight,
(this->x + this->width - closeButtonWidth) - 3, (this->y+3), closeButtonWidth-1, closeButtonHeight,
"X", // 按钮文本
RGB(255, 0, 0), // 按钮被点击颜色
this->canvasBkClor, // 按钮背景颜色
@@ -487,16 +461,24 @@ void Dialog::initCloseButton()
StellarX::ControlShape::B_RECTANGLE
);
but.get()->setButtonFalseColor(this->backgroundColor);
but.get()->enableTooltip(false);
but->setOnClickListener([this]() {
this->SetResult(StellarX::MessageBoxResult::Cancel);
this->hWnd.dialogClose = true;
this->Close(); });
this->closeButton = but.get();
this->addControl(std::move(but));
}
void Dialog::initTitle()
{
this->title = std::make_unique<Label>(this->x+5,this->y+5,titleText,textStyle.color);
title->setTextdisap(true);
title->textStyle = this->textStyle;
this->addControl(std::move(title));
}
void Dialog::splitMessageLines()
{
lines.clear(); // 先清空现有的行
@@ -504,17 +486,14 @@ void Dialog::splitMessageLines()
std::string currentLine;
for (size_t i = 0; i < message.length(); i++) {
// 处理 换行符 \r\n \n \r
const bool hasNext = (i + 1 < message.length());
const bool isLineBreak = (message[i] == '\r' || message[i] == '\n');
const bool isCrLf = hasNext && message[i] == '\r' && message[i + 1] == '\n';
if (isLineBreak || isCrLf)
if (i + 1 < message.length() && (message[i] == '\r' || message[i] == '\n')||(message[i] == '\r' && message[i+1] == '\n'))
{
if (!currentLine.empty()) {
lines.push_back(currentLine);
currentLine.clear();
}
if (isCrLf)
if (message[i] == '\r' && message[i + 1] == '\n')
i++;
continue;
}
@@ -523,13 +502,13 @@ void Dialog::splitMessageLines()
}
// 添加最后一行(如果有内容)
if (!currentLine.empty())
if (!currentLine.empty())
{
lines.push_back(currentLine);
}
// 如果消息为空,至少添加一个空行
if (lines.empty())
if (lines.empty())
{
lines.push_back("");
}
@@ -541,9 +520,7 @@ void Dialog::getTextSize()
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
this->textHeight = 0;
this->textWidth = 0;
for (auto& text : lines)
for (auto text : lines)
{
int w = textwidth(LPCTSTR(text.c_str()));
int h = textheight(LPCTSTR(text.c_str()));
@@ -552,34 +529,12 @@ void Dialog::getTextSize()
if (this->textWidth < w)
this->textWidth = w;
}
restoreStyle();
}
void Dialog::invalidateLayout(bool clearChildren)
{
if (clearChildren)
clearControls();
this->textWidth = 0;
this->textHeight = 0;
this->buttonNum = 0;
this->needsInitialization = true;
this->dirty = true;
}
void Dialog::rebuildChrome()
{
clearControls();
initButtons();
initCloseButton();
}
// 计算逻辑:对话框宽度取【文本区域最大宽度】和【按钮区域总宽度】中的较大值。
// 对话框高度 = 标题栏 + 文本区 + 按钮区 + 各种间距。
void Dialog::initDialogSize()
{
this->textStyle.nHeight = 20;
splitMessageLines(); // 分割消息行
getTextSize(); // 获取文本最大尺寸
@@ -602,26 +557,18 @@ void Dialog::initDialogSize()
// 计算按钮区域宽度
int buttonAreaWidth = buttonNum * functionButtonWidth +
(buttonNum > 0 ? (buttonNum + 1) * buttonMargin : 0);
(buttonNum > 0 ? (buttonNum +1) * buttonMargin : 0);
// 计算文本区域宽度(包括边距)
int textAreaWidth = textWidth + textToBorderMargin * 2;
saveStyle();
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
int titleAreaWidth = textwidth(LPCTSTR(titleText.c_str())) + textToBorderMargin * 2 + closeButtonWidth + buttonMargin;
restoreStyle();
int textAreaWidth = textWidth + textToBorderMargin * 2 ;
// 对话框宽度取文本、标题和按钮区域中的较大值,并确保最小宽度
// 对话框宽度取两者中的较大值,并确保最小宽度
this->width = buttonAreaWidth > textAreaWidth ? buttonAreaWidth : textAreaWidth;
if (titleAreaWidth > this->width)
this->width = titleAreaWidth;
this->width = this->width > 200 ? this->width : 200;
// 计算对话框高度
// 高度 = 标题栏高度 + 文本区域高度 + 按钮区域高度 + 间距
int textAreaHeight = textHeight * (int)lines.size() + 5 * ((int)lines.size() - 1); // 文本行高+行间距
int textAreaHeight = textHeight * (int)lines.size() + 5*((int)lines.size()-1); // 文本行高+行间距
this->height = closeButtonHeight + // 标题栏高度
titleToTextMargin + // 标题到文本的间距
textAreaHeight + // 文本区域高度
@@ -631,15 +578,14 @@ void Dialog::initDialogSize()
this->x = (hWnd.getWidth() - this->width) / 2;
this->y = (hWnd.getHeight() - this->height) / 2;
rebuildChrome();
//this->textStyle.nWidth = 10;
this->textStyle.nHeight = 20;
initButtons(); // 初始化按钮
initTitle(); // 初始化标题标签
initCloseButton(); // 初始化关闭按钮
}
void Dialog::addControl(std::unique_ptr<Control> control)
{
control->setParent(this);
controls.push_back(std::move(control));
dirty = true;
}
// 延迟清理策略:由于对话框绘制时保存了背景快照,必须在对话框隐藏后、
// 所有控件析构前恢复背景,否则会导致背景图像被错误覆盖。
@@ -650,41 +596,25 @@ void Dialog::performDelayedCleanup()
isCleaning = true;
auto& c = hWnd.getControls();
for (auto& control : c)
control->setDirty(true);
// 清除所有控件
controls.clear();
// 重置指针
closeButton = nullptr;
title.reset();
// 释放背景图像资源
if (saveBkImage && hasSnap)
{
restBackground();
FlushBatchDraw();
invalidateBackgroundSnapshot();
}
if (!(saveBkImage && hasSnap))
{
// 没有背景快照:强制一次完整重绘,立即擦掉残影
hWnd.pumpResizeIfNeeded(); // 如果正好有尺寸标志,顺便统一收口
// 即使没有尺寸变化,也重绘一帧
BeginBatchDraw();
// 背景
if (hWnd.getBkImage() && !hWnd.getBkImageFile().empty())
putimage(0, 0, hWnd.getBkImage());
else { setbkcolor(hWnd.getBkcolor()); cleardevice(); }
// 所有普通控件
for (auto& c : hWnd.getControls()) c->draw();
// 其他对话框(this 已经 show=false,会早退不绘)
EndBatchDraw();
FlushBatchDraw();
discardBackground();
}
// 重置状态
needsInitialization = true;
pendingCleanup = false;
isCleaning = false;
shouldClose = false;
}
void Dialog::SetResultCallback(std::function<void(StellarX::MessageBoxResult)> cb)
@@ -704,12 +634,10 @@ std::string Dialog::GetText() const
void Dialog::clearControls()
{
for (auto& control : controls)
control->invalidateBackgroundSnapshot();
controls.clear();
// 重置按钮指针
closeButton = nullptr;
title.reset(); // 释放标题资源
}
std::unique_ptr<Button> Dialog::createDialogButton(int x, int y, const std::string& text)
@@ -723,51 +651,8 @@ std::unique_ptr<Button> Dialog::createDialogButton(int x, int y, const std::stri
StellarX::ButtonMode::NORMAL,
StellarX::ControlShape::RECTANGLE
);
return btn;
}
void Dialog::requestRepaint(Control* parent)
{
if (shouldDeferManagedRepaint())
{
// 非模态 Dialog 在 Window 主循环中也走托管提交;
// 这样底层控件和对话框的绘制顺序由 Window 统一收口控制。
if (auto* host = getHostWindow())
host->requestManagedRepaint(this);
return;
}
if (this == parent)
{
for (auto& control : controls)
if (control->isDirty() && control->IsVisible())
control->draw();
}
else
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();
}
+6 -9
View File
@@ -1,32 +1,29 @@
#include "MessageBox.h"
#include "SxLog.h"
namespace StellarX
{
MessageBoxResult MessageBox::showModal(Window& wnd, const std::string& text, const std::string& caption,
MessageBoxResult MessageBox::showModal(Window& wnd,const std::string& text,const std::string& caption,
MessageBoxType type)
{
Dialog dlg(wnd, caption, text, type, true); // 模态
SX_LOGI("MessageBox") << "show: Message=" << dlg.GetText()
<< " modal=" << (dlg.model() ? 1 : 0);
dlg.setInitialization(true);
dlg.Show();
return dlg.GetResult();
}
void MessageBox::showAsync(Window& wnd, const std::string& text, const std::string& caption, MessageBoxType type,
void MessageBox::showAsync(Window& wnd,const std::string& text,const std::string& caption,MessageBoxType type,
std::function<void(MessageBoxResult)> onResult)
{
//去重,如果窗口内已有相同的对话框被触发,则不再创建
if (wnd.hasNonModalDialogWithCaption(caption, text))
{
std::cout << "\a" << std::endl;
std::cout << "\a" << std::endl;
return;
}
auto dlg = std::make_unique<Dialog>(wnd, caption, text,
type, false); // 非模态
SX_LOGI("MessageBox") << "show: Message=" << dlg->GetText()
<< " modal=" << (dlg->model() ? 1 : 0);
Dialog* dlgPtr = dlg.get();
dlgPtr->setInitialization(true);
// 设置回调
if (onResult)
dlgPtr->SetResultCallback(std::move(onResult));
+71
View File
@@ -0,0 +1,71 @@
#include "Label.h"
Label::Label()
:Control(0, 0, 0, 0)
{
this->text = "默认标签";
textColor = RGB(0,0,0);
textBkColor = RGB(255, 255, 255);; //默认白色背景
}
Label::Label(int x, int y, std::string text, COLORREF textcolor, COLORREF bkColor)
:Control(x, y, 0, 0)
{
this->text = text;
textColor = textcolor;
textBkColor = bkColor; //默认白色背景
}
void Label::draw()
{
if (dirty && show)
{
saveStyle();
if (textBkDisap)
setbkmode(TRANSPARENT); //设置背景透明
else
{
setbkmode(OPAQUE); //设置背景不透明
setbkcolor(textBkColor); //设置背景颜色
}
settextcolor(textColor);
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut); //设置字体样式
this->saveBackground(x, y,textwidth(text.c_str()),textheight(text.c_str()));
this-> restBackground();
outtextxy(x, y, LPCTSTR(text.c_str()));
this->restoreStyle();
dirty = false;
}
}
//用于“隐藏提示框”时调用(还原并释放快照)
void Label::hide()
{
restBackground(); // 还原屏幕像素
discardBackground(); // 作废快照,防止错贴旧图
dirty = false;
}
void Label::setTextdisap(bool key)
{
textBkDisap = key;
this->dirty = true;
}
void Label::setTextColor(COLORREF color)
{
textColor = color;
this->dirty = true;
}
void Label::setTextBkColor(COLORREF color)
{
textBkColor = color;
this->dirty = true;
}
void Label::setText(std::string text)
{
this->text = text;
this->dirty = true;
}
+507
View File
@@ -0,0 +1,507 @@
#include "Table.h"
// 绘制表格的当前页
// 使用双循环绘制行和列,考虑分页偏移
void Table::drawTable()
{
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
// 表体从“表头之下”开始
dX = x + border;
dY = y + border + lineHeights.at(0) + 10; // 表头高度
uY = dY + lineHeights.at(0) + 10;
size_t startRow = (currentPage - 1) * rowsPerPage;
size_t endRow = startRow + (size_t)rowsPerPage < data.size() ? startRow + (size_t)rowsPerPage : data.size();
for (size_t i = startRow; i < endRow; ++i)
{
for (size_t j = 0; j < data[i].size(); ++j)
{
uX = dX + colWidths.at(j) + 20; // 列宽 + 20
fillrectangle(dX, dY, uX, uY);
outtextxy(dX + 10, dY + 5, LPCTSTR(data[i][j].c_str()));
dX += colWidths.at(j) + 20;
}
dX = x + border;
dY = uY;
uY = dY + lineHeights.at(0) + 10;
}
}
void Table::drawHeader()
{
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
// 内容区原点 = x+border, y+border
dX = x + border;
dY = y + border;
uY = dY + lineHeights.at(0) + 10;
for (size_t i = 0; i < headers.size(); i++)
{
uX = dX + colWidths.at(i) + 20; // 注意这里是 +20,和表体一致
fillrectangle(dX, dY, uX, uY);
outtextxy(dX + 10, dY + 5, LPCTSTR(headers[i].c_str()));
dX += colWidths.at(i) + 20; // 列间距 20
}
}
// 遍历所有数据单元和表头,计算每列的最大宽度和每行的最大高度,
// 为后续绘制表格单元格提供尺寸依据。此计算在数据变更时自动触发。
void Table::initTextWaH()
{
// 和绘制一致的单元内边距
const int padX = 10; // 左右 padding
const int padY = 5; // 上下 padding
const int colGap = 20; // 列间距
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
// 统计每列最大文本宽 & 每列最大行高(包含数据 + 表头)
colWidths.assign(headers.size(), 0);
lineHeights.assign(headers.size(), 0);
// 先看数据
for (size_t i = 0; i < data.size(); ++i)
{
for (size_t j = 0; j < data[i].size(); ++j)
{
const int w = textwidth(LPCTSTR(data[i][j].c_str()));
const int h = textheight(LPCTSTR(data[i][j].c_str()));
if (w > colWidths[j]) colWidths[j] = w;
if (h > lineHeights[j]) lineHeights[j] = h;
}
}
// 再用表头更新(谁大取谁)
for (size_t j = 0; j < headers.size(); ++j)
{
const int w = textwidth(LPCTSTR(headers[j].c_str()));
const int h = textheight(LPCTSTR(headers[j].c_str()));
if (w > colWidths[j]) colWidths[j] = w;
if (h > lineHeights[j]) lineHeights[j] = h;
}
// 用“所有列的最大行高”作为一行的基准高度
int maxLineH = 0;
for (int h : lineHeights)
if (h > maxLineH)
maxLineH = h;
// 列的像素宽 = 内容宽 + 左右 padding
// 表内容总宽 = Σ(列宽 + 列间距)
int contentW = 0;
for (size_t j = 0; j < colWidths.size(); ++j)
contentW += (colWidths[j] + 2 * padX) + colGap;
// 表头高 & 行高(与 drawHeader/drawTable 内部一致:+上下 padding
const int headerH = maxLineH + 2 * padY;
const int rowH = maxLineH + 2 * padY;
const int rowsH = rowH * rowsPerPage;
// 页脚:
const int pageTextH = textheight(LPCTSTR(pageNumtext.c_str()));
const int btnTextH = textheight(LPCTSTR("上一页"));
const int btnPadV = 8;
const int btnH = btnTextH + 2 * btnPadV;
const int footerPad = 16;
const int footerH = (pageTextH > btnH ? pageTextH : btnH) + footerPad;
// 最终表宽/高:内容 + 对称边框
this->width = contentW + (border << 1);
this->height = headerH + rowsH + footerH + (border << 1);
}
void Table::initButton()
{
const int gap = 12; // 页码与按钮之间的固定间距
const int padH = 12; // 按钮水平内边距
const int padV = 0; // 按钮垂直内边距
int pageW = textwidth(LPCTSTR(pageNumtext.c_str()));
int lblH = textheight(LPCTSTR(pageNumtext.c_str()));
// 统一按钮尺寸(用按钮文字自身宽高 + padding)
int prevW = textwidth(LPCTSTR("上一页")) + padH * 2;
int nextW = textwidth(LPCTSTR("下一页")) + padH * 2;
int btnH = lblH + padV * 2;
// 基于“页码标签”的矩形来摆放:
// prev 在页码左侧 gap 处;next 在右侧 gap 处;Y 对齐 pY
int prevX = pX - gap - prevW;
int nextX = pX + pageW + gap;
int btnY = pY; // 和页码同一基线
if (!prevButton)
prevButton = new Button(prevX, btnY, prevW, btnH, "上一页", RGB(0, 0, 0), RGB(255, 255, 255));
else
{
prevButton->setX(prevX);
prevButton->setY(btnY);
}
if (!nextButton)
nextButton = new Button(nextX, btnY, nextW, btnH, "下一页", RGB(0, 0, 0), RGB(255, 255, 255));
else
{
nextButton->setX(nextX);
nextButton->setY(btnY);
}
prevButton->textStyle = this->textStyle;
nextButton->textStyle = this->textStyle;
prevButton->setFillMode(tableFillMode);
nextButton->setFillMode(tableFillMode);
prevButton->setOnClickListener([this]()
{
if (currentPage > 1)
{
--currentPage;
dirty = true;
if (pageNum) pageNum->setDirty(true);
}
});
nextButton->setOnClickListener([this]()
{
if (currentPage < totalPages)
{
++currentPage;
dirty = true;
if (pageNum) pageNum->setDirty(true);
}
});
}
void Table::initPageNum()
{
// 统一坐标系
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
const int baseH = lineHeights.empty() ? 0 : lineHeights.at(0);
const int headerH = baseH + 10;
const int rowsH = baseH * rowsPerPage + rowsPerPage * 10;
// 内容宽度 = sum(colWidths + 20)initTextWaH() 已把 this->width += 2*border
// 因此 contentW = this->width - 2*border 更稳妥
const int contentW = this->width - (border << 1);
// 页脚顶部位置(表头 + 可视数据区 之后)
pY = y + border + headerH + rowsH + 8; // +8 顶部留白
// 按理来说 x + (this->width - textW) / 2;就可以
// 但是在绘制时,发现控件偏右,因此减去40
int textW = textwidth(LPCTSTR(pageNumtext.c_str()));
pX = x - 40 +(this->width - textW) / 2;
if (!pageNum)
pageNum = new Label(pX, pY, pageNumtext);
else
{
pageNum->setX(pX);
pageNum->setY(pY);
}
pageNum->textStyle = this->textStyle;
if (StellarX::FillMode::Null == tableFillMode)
pageNum->setTextdisap(true); // 透明文本
}
void Table::drawPageNum()
{
pageNumtext = "";
pageNumtext+= std::to_string(currentPage);
pageNumtext += "页/共";
pageNumtext += std::to_string(totalPages);
pageNumtext += "";
if (nullptr == pageNum)
initPageNum();
pageNum->setText(pageNumtext);
if (StellarX::FillMode::Null == tableFillMode)
pageNum->setTextdisap(true);
pageNum->draw();
}
void Table::drawButton()
{
if (nullptr == prevButton || nullptr == nextButton)
initButton();
this->prevButton->textStyle = this->textStyle;
this->nextButton->textStyle = this->textStyle;
this->prevButton->setFillMode(tableFillMode);
this->nextButton->setFillMode(tableFillMode);
this->prevButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
this->nextButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
this->prevButton->setDirty(true);
this->nextButton->setDirty(true);
prevButton->draw();
nextButton->draw();
}
Table::Table(int x, int y)
:Control(x, y, 0, 0)
{
}
Table::~Table()
{
if (this->prevButton)
delete this->prevButton;
if (this->nextButton)
delete this->nextButton;
if (this->pageNum)
delete this->pageNum;
if (this->saveBkImage)
delete this->saveBkImage;
this->prevButton = nullptr;
this->nextButton = nullptr;
this->pageNum = nullptr;
this->saveBkImage = nullptr;
}
void Table::draw()
{
if (this->dirty && this->show)
{
// 先保存当前绘图状态
saveStyle();
// 设置表格样式
setfillcolor(tableBkClor);
setlinecolor(tableBorderClor);
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
settextcolor(textStyle.color);
setlinestyle((int)tableLineStyle, tableBorderWidth);
setfillstyle((int)tableFillMode);
setbkmode(TRANSPARENT);
// 是否需要计算单元格尺寸
if (isNeedCellSize)
{
initTextWaH();
isNeedCellSize = false;
}
if (isNeedDrawHeaders)
{
// 重新设置表格样式
setfillcolor(tableBkClor);
setlinecolor(tableBorderClor);
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
settextcolor(textStyle.color);
setlinestyle((int)tableLineStyle, tableBorderWidth);
setfillstyle((int)tableFillMode);
setbkmode(TRANSPARENT);
}
//确保在绘制任何表格内容之前捕获背景
// 临时恢复样式,确保捕获正确的背景
if (!saveBkImage)
saveBackground(this->x, this->y, this->width, this->height);
// 恢复背景(清除旧内容)
restBackground();
// 绘制表头
dX = x;
dY = y;
drawHeader();
this->isNeedDrawHeaders = false;
// 绘制当前页
drawTable();
// 绘制页码标签
drawPageNum();
// 绘制翻页按钮
if (this->isShowPageButton)
drawButton();
// 恢复绘图状态
restoreStyle();
dirty = false; // 标记不需要重绘
}
}
bool Table::handleEvent(const ExMessage& msg)
{
if(!show)return false;
bool consume = false;
if(!this->isShowPageButton)
return consume;
else
{
consume = prevButton->handleEvent(msg);
if (!consume)
consume = nextButton->handleEvent(msg);
}
if (dirty)
draw();
return consume;
}
void Table::setHeaders(std::initializer_list<std::string> headers)
{
this->headers.clear();
for (auto lis : headers)
this->headers.push_back(lis);
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
isNeedDrawHeaders = true; // 标记需要重新绘制表头
dirty = true;
}
void Table::setData( std::vector<std::string> data)
{
if (data.size() < headers.size())
for (int i = 0; data.size() <= headers.size(); i++)
data.push_back("");
this->data.push_back(data);
totalPages = ((int)this->data.size() + rowsPerPage - 1) / rowsPerPage;
if (totalPages < 1)
totalPages = 1;
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
dirty = true;
}
void Table::setData( std::initializer_list<std::vector<std::string>> data)
{
for (auto lis : data)
if (lis.size() < headers.size())
{
for (size_t i = lis.size(); i< headers.size(); i++)
lis.push_back("");
this->data.push_back(lis);
}
else
this->data.push_back(lis);
totalPages = ((int)this->data.size() + rowsPerPage - 1) / rowsPerPage;
if (totalPages < 1)
totalPages = 1;
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
dirty = true;
}
void Table::setRowsPerPage(int rows)
{
this->rowsPerPage = rows;
totalPages = ((int)data.size() + rowsPerPage - 1) / rowsPerPage;
if (totalPages < 1)
totalPages = 1;
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
dirty = true;
}
void Table::showPageButton(bool isShow)
{
this->isShowPageButton = isShow;
this->dirty = true;
}
void Table::setTableBorder(COLORREF color)
{
this->tableBorderClor = color;
this->dirty = true;
}
void Table::setTableBk(COLORREF color)
{
this->tableBkClor = color;
this->dirty = true;
}
void Table::setTableFillMode(StellarX::FillMode mode)
{
if (StellarX::FillMode::Solid == mode || StellarX::FillMode::Null == mode)
this->tableFillMode = mode;
else
this->tableFillMode = StellarX::FillMode::Solid;
if (this->prevButton && this->nextButton && this->pageNum)
{
this->prevButton->textStyle = this->textStyle;
this->nextButton->textStyle = this->textStyle;
this->prevButton->setFillMode(tableFillMode);
this->nextButton->setFillMode(tableFillMode);
if (StellarX::FillMode::Null == tableFillMode)
pageNum->setTextdisap(true);
this->prevButton->setDirty(true);
this->nextButton->setDirty(true);
}
this->dirty = true;
}
void Table::setTableLineStyle(StellarX::LineStyle style)
{
this->tableLineStyle = style;
this->dirty = true;
}
void Table::setTableBorderWidth(int width)
{
this->tableBorderWidth = width;
this->dirty = true;
}
int Table::getCurrentPage() const
{
return this->currentPage;
}
int Table::getTotalPages() const
{
return this->totalPages;
}
int Table::getRowsPerPage() const
{
return this->rowsPerPage;
}
bool Table::getShowPageButton() const
{
return this->isShowPageButton;
}
COLORREF Table::getTableBorder() const
{
return this->tableBorderClor;
}
COLORREF Table::getTableBk() const
{
return this->tableBkClor;
}
StellarX::FillMode Table::getTableFillMode() const
{
return this->tableFillMode;
}
StellarX::LineStyle Table::getTableLineStyle() const
{
return this->tableLineStyle;
}
std::vector<std::string> Table::getHeaders() const
{
return this->headers;
}
std::vector<std::vector<std::string>> Table::getData() const
{
return this->data;
}
int Table::getTableBorderWidth() const
{
return this->tableBorderWidth;
}
+157
View File
@@ -0,0 +1,157 @@
// TextBox.cpp
#include "TextBox.h"
TextBox::TextBox(int x, int y, int width, int height, std::string text, StellarX::TextBoxmode mode, StellarX::ControlShape shape)
:Control(x,y,width,height),text(text), mode(mode), shape(shape)
{
}
void TextBox::draw()
{
if (dirty && show)
{
saveStyle();
setfillcolor(textBoxBkClor);
setlinecolor(textBoxBorderClor);
if (textStyle.nHeight > height)
textStyle.nHeight = height;
if (textStyle.nWidth > width)
textStyle.nWidth = width;
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
settextcolor(textStyle.color);
setbkmode(TRANSPARENT);
int text_width = textwidth(LPCTSTR(text.c_str()));
int text_height = textheight(LPCTSTR(text.c_str()));
//根据形状绘制
switch (shape)
{
case StellarX::ControlShape::RECTANGLE:
fillrectangle(x,y,x+width,y+height);//有边框填充矩形
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::B_RECTANGLE:
solidrectangle(x, y, x + width, y + height);//无边框填充矩形
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::ROUND_RECTANGLE:
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//有边框填充圆角矩形
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
case StellarX::ControlShape::B_ROUND_RECTANGLE:
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//无边框填充圆角矩形
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
break;
}
restoreStyle();
dirty = false; //标记不需要重绘
}
}
bool TextBox::handleEvent(const ExMessage& msg)
{
bool hover = false;
bool oldClick = click;
bool consume = false;
switch (shape)
{
case StellarX::ControlShape::RECTANGLE:
case StellarX::ControlShape::B_RECTANGLE:
case StellarX::ControlShape::ROUND_RECTANGLE:
case StellarX::ControlShape::B_ROUND_RECTANGLE:
hover = (msg.x > x && msg.x < (x + width) && msg.y > y && msg.y < (y + height));//判断鼠标是否在矩形按钮内
consume = false;
break;
}
if (hover && msg.message == WM_LBUTTONUP)
{
click = true;
if(StellarX::TextBoxmode::INPUT_MODE == mode)
{
dirty = InputBox(LPTSTR(text.c_str()), (int)maxCharLen, "输入框", NULL, text.c_str(), NULL, NULL, false);
consume = true;
}
else if (StellarX::TextBoxmode::READONLY_MODE == mode)
{
dirty = false;
InputBox(NULL, (int)maxCharLen, "输出框(输入无效!)", NULL, text.c_str(), NULL, NULL, false);
consume = true;
}
flushmessage(EX_MOUSE | EX_KEY);
}
if (dirty)
draw();
if (click)
click = false;
return consume;
}
void TextBox::setMode(StellarX::TextBoxmode mode)
{
this->mode = mode;
this->dirty = true;
}
void TextBox::setMaxCharLen(size_t len)
{
if (len > 0)
maxCharLen = len;
this->dirty = true;
}
void TextBox::setTextBoxshape(StellarX::ControlShape shape)
{
switch (shape)
{
case StellarX::ControlShape::RECTANGLE:
case StellarX::ControlShape::B_RECTANGLE:
case StellarX::ControlShape::ROUND_RECTANGLE:
case StellarX::ControlShape::B_ROUND_RECTANGLE:
this->shape = shape;
this->dirty = true;
break;
case StellarX::ControlShape::CIRCLE:
case StellarX::ControlShape::B_CIRCLE:
case StellarX::ControlShape::ELLIPSE:
case StellarX::ControlShape::B_ELLIPSE:
this->shape = StellarX::ControlShape::RECTANGLE;
this->dirty = true;
break;
}
}
void TextBox::setTextBoxBorder(COLORREF color)
{
textBoxBorderClor = color;
this->dirty = true;
}
void TextBox::setTextBoxBk(COLORREF color)
{
textBoxBkClor = color;
this->dirty = true;
}
void TextBox::setText(std::string text)
{
if(text.size() > maxCharLen)
text = text.substr(0, maxCharLen);
this->text = text;
this->dirty = true;
draw();
}
std::string TextBox::getText() const
{
return this->text;
}
+339
View File
@@ -0,0 +1,339 @@
#include "Window.h"
#include "Dialog.h"
#include <windows.h> // 确保包含 Windows API 头文件
Window::Window(int width, int height, int mode)
{
this->pendingW = this->width = width;
this->pendingH = this->height = height;
this->windowMode = mode;
}
Window::Window(int width, int height, int mode, COLORREF bkcloc)
{
this->pendingW = this->width = width;
this->pendingH = this->height = height;
this->windowMode = mode;
this->wBkcolor = bkcloc;
}
Window::Window(int width, int height, int mode, COLORREF bkcloc, std::string headline)
{
this->pendingW = this->width = width;
this->pendingH = this->height = height;
this->windowMode = mode;
this->wBkcolor = bkcloc;
this->headline = headline;
}
Window::~Window()
{
if (background)
delete background;
background = nullptr;
closegraph(); // 确保关闭图形上下文
}
void Window::draw() {
// 使用 EasyX 创建基本窗口
hWnd = initgraph(width, height, windowMode);
SetWindowText(hWnd, headline.c_str());
// **启用窗口拉伸支持**:添加厚边框和最大化按钮样式
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);
// 设置背景色并清屏
setbkcolor(wBkcolor);
cleardevice();
// 初次绘制所有控件(双缓冲)
BeginBatchDraw();
for (auto& control : controls)
{
control->draw();
}
// (如果有初始对话框,也可绘制 dialogs)
EndBatchDraw();
}
void Window::draw(std::string imagePath)
{
// 使用指定图片绘制窗口背景(铺满窗口)
this->background = new IMAGE(width, height);
bkImageFile = imagePath;
hWnd = initgraph(width, height, windowMode);
SetWindowText(hWnd, headline.c_str());
loadimage(background, imagePath.c_str(), width, height, true);
putimage(0, 0, background);
// 同样应用可拉伸样式
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);
// 绘制控件(含对话框)到窗口
BeginBatchDraw();
for (auto& control : controls) control->draw();
for (auto& dlg : dialogs) dlg->draw();
EndBatchDraw();
}
// 运行主事件循环,处理用户输入和窗口消息
// 此方法会阻塞直到窗口关闭
// 主消息循环优先级:对话框 > 普通控件。
// 重绘策略:为保证视觉一致性,每次有对话框状态变化(打开/关闭)时,
// 会强制重绘所有控件。先绘制普通控件,再绘制对话框(确保对话框在最上层)。
int Window::runEventLoop()
{
ExMessage msg;
bool running = true;
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 (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;
}
//普通控件
if (!consume)
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
consume = (*it)->handleEvent(msg);
if (consume)
break;
}
}
//如果有对话框打开或者关闭强制重绘
bool needredraw = false;
for (auto& d : dialogs)
{
needredraw = d->IsVisible();
if (needredraw)break;
}
if (needredraw || dialogClose)
{
// 对话框关闭后,需要手动合成一个鼠标移动消息并分发给所有普通控件,
// 以便它们能及时更新悬停状态(hover),否则悬停状态可能保持错误状态。
// 先把当前鼠标位置转换为客户区坐标,并合成一次 WM_MOUSEMOVE,先分发给控件更新 hover 状态
POINT pt;
if (GetCursorPos(&pt))
{
ScreenToClient(this->hWnd, &pt);
ExMessage mm;
mm.message = WM_MOUSEMOVE;
mm.x = (short)pt.x;
mm.y = (short)pt.y;
// 只分发给 window 层控件(因为 dialog 已经关闭或即将关闭)
for (auto& c : controls)
c->handleEvent(mm);
}
BeginBatchDraw();
// 先绘制普通控件
for (auto& c : controls)
c->draw();
// 然后绘制对话框(确保对话框在最上层)
for (auto& d : dialogs)
{
if (!d->model() && d->IsVisible())
d->setDirty(true);
d->draw();
}
EndBatchDraw();
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->setDirty(true);
c->updateBackground();
c->draw();
}
for (auto& d : dialogs)
{
auto dd = dynamic_cast<Dialog*>(d.get());
dd->setDirty(true);
dd->setInitialization(true);
dd->draw();
}
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();
}
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();
}
void Window::setHeadline(std::string headline)
{
this->headline = headline;
SetWindowText(this->hWnd, headline.c_str());
}
void Window::addControl(std::unique_ptr<Control> control)
{
this->controls.push_back(std::move(control));
}
void Window::addDialog(std::unique_ptr<Control> dialogs)
{
this->dialogs.push_back(std::move(dialogs));
}
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;
}
HWND Window::getHwnd() const
{
return hWnd;
}
int Window::getWidth() const
{
return this->pendingW;
}
int Window::getHeight() const
{
return this->pendingH;
}
std::string Window::getHeadline() const
{
return this->headline;
}
COLORREF Window::getBkcolor() const
{
return this->wBkcolor;
}
IMAGE* Window::getBkImage() const
{
return this->background;
}
std::vector<std::unique_ptr<Control>>& Window::getControls()
{
return this->controls;
}
-1674
View File
File diff suppressed because it is too large Load Diff
-137
View File
@@ -1,137 +0,0 @@
# 修改总览-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` 之后还有未提交修改。
- 这些未提交修改主要是:
- 第二阶段托管重绘
- 注释补充与校正
- 记录与模板体系整理
@@ -1,55 +0,0 @@
# 补充记录-20260409-窗口最大化收口保护
## 说明
- 本文件用于补充记录 2026-04-09 当天新增的窗口最大化收口问题。
- 对应修改发生在 `f567369` 快照之后,当前仍位于工作区未提交状态。
## 问题概述
- 现象:
- 某些窗口初始尺寸下,直接点击最大化会出现黑背景、控件残影。
- 若先手动拖拽窗口略微放大,再最大化,则问题消失。
- 影响用例:
- `KEY == 1`
- `KEY == 2`
- 不受影响用例:
- `KEY == 3`
- `KEY == 4`
## 根因
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 的 `runEventLoop()` resize 收口中存在一段历史“跨度保护”。
- 该保护把“宽高变化超过 1000”视为异常帧,直接终止收口:
- 不再执行 `adaptiveLayout`
- 不再执行 `Resize`
- 不再执行 `redrawScene(true, true)`
- 因此新暴露区域不会被完整刷新,最终表现为黑背景与残影。
## 本次新增记录
- BUG
- [`BUG-20260409-0003 直接最大化触发收口保护导致黑背景与残影`](./BUG/BUG-20260409-0003-直接最大化触发收口保护导致黑背景与残影.md)
- Fix
- [`Fix-BUG-20260409-0003 直接最大化触发收口保护导致黑背景与残影`](./Fix/Fix-BUG-20260409-0003-直接最大化触发收口保护导致黑背景与残影.md)
- 功能变更:
- [`Feature-20260409-0006 窗口最大化收口保护调整`](./功能变更/Feature-20260409-0006-窗口最大化收口保护调整.md)
## 修改摘要
- 将“跨度过大即跳过”改为“仅非法尺寸才跳过”。
- 非法尺寸标准:
- `actualWidth <= 0`
- `actualHeight <= 0`
- `actualWidth > max(10000, virtualScreenWidth * 2)`
- `actualHeight > max(10000, virtualScreenHeight * 2)`
- 对“大跨度但合法”的 resize 保留 `DEBUG` 日志,但继续执行完整收口。
- 顺手删除了 `Window.cpp` 中未使用的 `ExMessage mm;` 遗留变量。
## 当前状态
- 代码修改:已完成,待提交
- 文档记录:已补充
- 验证状态:
- 已完成源码级编译验证
- 待用户做 GUI 回归
@@ -1,73 +0,0 @@
# 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
- 修复版本:[当前工作区]
- 验证版本:[当前工作区]
- 备注:快速划过多个按钮时一帧内偶发双高亮的问题暂未彻底消除,属于已接受的底层限制
@@ -1,71 +0,0 @@
# 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 假设工作
@@ -1,68 +0,0 @@
# BUG-20260409-0003
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260409-0003
- 标题: 直接最大化触发收口保护,导致黑背景与控件残影
- 状态:已修复 / 待回归验证
- 严重性:S2
- 优先级:P0
- 模块:Window / Resize / Scene Redraw
- 版本 / 分支:`master`
- 环境:Windows 桌面 + EasyX + VS2022
- 发现人:用户反馈
- 关联 Fix ID`Fix-BUG-20260409-0003`
## 问题描述
- 现象:
- 某些用例在窗口初始大小下直接点击最大化后,新暴露出的区域显示为黑色背景。
- 已有控件可能出现残留、残影或旧内容未被覆盖。
- 若先手动拖动边框略微放大,再执行最大化,则问题消失。
- 影响范围:
- 顶层窗口 resize 收口
- 背景完整重绘
- 控件 resize 后的快照/标脏收口
- 期望结果:
- 无论是拖拽 resize 还是直接最大化,只要 `WM_SIZE` 进入收口流程,都应完成一次完整 redraw。
- 实际结果:
- 某些“大跨度但合法”的最大化尺寸变化,会在收口中途被直接跳过,导致背景和控件未完成刷新。
## 复现信息
- 复现条件:
- 初始窗口尺寸与最大化后的客户区尺寸跨度较大。
- 典型用例:
1. `KEY == 1``1200x400 -> 2160x1417`
2. `KEY == 2``700x510 -> 2160x1417`
3. `KEY == 3 / 4`:初始尺寸较大时不复现
- 复现概率:高
- 证据:
- `WM_SIZE:待处理=(2160x1417), isSizing=0`
- `调整窗口尺寸开始:width=... height=...`
- `尺寸调整被跨度保护跳过:old=(...) new=(...) diff=(...)`
- 异常日志中缺失 `尺寸调整已完成`
## 初步分析
- 疑似位置:
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 的 `runEventLoop()` resize 收口逻辑
- 可疑代码特征:
- 使用 `abs(finalW - width) > 1000 || abs(finalH - height) > 1000` 作为跳过条件
- 命中后直接:
- `needResizeDirty = false`
- `continue`
- 根因判断:
- 该分支把“跨度较大的合法最大化”误判为异常帧
- 导致后续 `Resize / adaptiveLayout / redrawScene / done log` 全部被跳过
## 跟踪信息
- 首次发现时间:2026-04-09
- 最后更新时间:2026-04-09
- 修复版本:当前工作区
- 验证版本:待用户回归
- 备注:
- 当前证据表明问题与 DPI 不是同一级根因,更直接的是跨度阈值误杀。
@@ -1,68 +0,0 @@
# BUG-20260410-0004
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: `BUG-20260410-0004`
- 标题: 按钮 Tooltip 显示后,鼠标移出按钮区域提示框不消失
- 状态:已修复
- 严重性:S3
- 优先级:P1
- 模块: Button / Tooltip / 托管重绘
- 版本 / 分支: 当前工作区 / 下一版本开发中
- 环境: Windows + EasyX,本地 GUI Demo
- 发现人: 用户
- 关联 Fix ID: `Fix-BUG-20260410-0004`
## 问题描述
- 现象:
- 按钮启用 Tooltip 后,提示框成功显示。
- 鼠标离开按钮区域后,Tooltip 仍残留在屏幕上,不会及时消失。
- 影响范围:
- 所有启用了 Tooltip 的 `Button`
- 典型场景包括 `KEY == 4` 的综合回归用例
- 期望结果:
- 鼠标离开按钮后,Tooltip 应立即回贴并消失,不留下残影
- 实际结果:
- Tooltip 逻辑状态已切换为隐藏,但屏幕上的提示框区域未被及时擦除
## 复现信息
- 前置条件:
- 使用启用 Tooltip 的按钮
- 复现步骤:
1. 运行带 Tooltip 按钮的 Demo
2. 将鼠标悬停在按钮上,等待 Tooltip 显示
3. 将鼠标移出按钮区域,观察 Tooltip 是否立即消失
- 复现概率:高概率
- 最小复现 Demo:
- `z-testDome.cpp``KEY == 4`
- 证据:现象观察 + 代码链路分析
## 初步分析
- 疑似位置:
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp)
- `Button::hideTooltip()`
- 触发条件:
- Tooltip 已经显示
- 鼠标离开按钮,进入 Tooltip 隐藏分支
- 相关线索:
- Tooltip 不是独立控件树成员,而是 `Button::draw()` 末尾补画的内置浮层
- 隐藏时若只作废快照而不回贴 Tooltip 自己的背景,按钮本体重绘并不会覆盖 Tooltip 占用区域
- 最近相关改动:
- 事件改状态 / Window 托管重绘收口
- 布局系统第一阶段重构
## 跟踪信息
- 首次发现时间: 2026-04-10
- 最后更新时间: 2026-04-10
- 修复版本: 待定
- 验证版本: 当前工作区
- 备注:
- 本文档仅记录问题本身,具体修复方案见关联 Fix 文档
@@ -1,73 +0,0 @@
# BUG-20260415-0005
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260415-0005
- 标题: 局部重绘未补画上层兄弟导致遮挡错误
- 状态:已修复
- 严重性:S2
- 优先级:P1
- 模块: Window / Canvas / TabControl 局部重绘与合成
- 版本 / 分支: 当前工作区
- 环境: Windows + EasyX
- 发现人: 用户回归测试
- 关联 Fix ID[可选]
- `Fix-BUG-20260415-0005`
## 问题描述
- 现象:
- 下层控件发生局部重绘后,位于其上方且与 coverage 相交的兄弟控件没有被补画回来。
- 表现为上层 `Canvas / Dialog / 页 / 页签按钮` 被“切掉”或被下层重新盖住。
- 影响范围:
- 顶层普通控件之间
- `Canvas` 直接子控件之间
- `TabControl` 页签按钮与页面之间
- 期望结果:
- 任意局部重绘提交后,父容器应按实际绘制顺序把 coverage 上方相交的兄弟重新合成回来。
- 实际结果:
- 仅 dirty root 或 dirty child 被重画,上层兄弟未恢复。
## 复现信息
- 前置条件:[可选]
- 使用 `KEY5` 测试场景
- 复现步骤:
1. 进入 `KEY5`
2. 触发下层控件局部重绘,例如 `Table` 翻页或相交子控件 hover / click
3. 观察上层相交兄弟区域
- 复现概率:高概率
- 最小复现 Demo[可选]
- `KEY5``Table` 与顶层粉色浮层重叠场景
- 同父 `Canvas` 兄弟相交场景
- 证据:截图 / 日志 / 调用栈 / 录屏 / 断点观察
- 断点观察显示 dirty root 已提交,但上层 sibling 未重新执行 draw
## 初步分析
- 疑似位置:
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
- 触发条件:
- 局部重绘提交写入了下层像素,但没有做 overlay 兄弟补画
- 相关线索:
- 该问题不依赖“是否回贴背景快照”,直接重绘同样会破坏上层合成结果
- `Dialog` 之前之所以需要补画,本质上是同一类问题
- 最近相关改动:[可选]
- 第二阶段开始引入更严格的局部重绘与 overlay 收口后,问题被系统性暴露
## 跟踪信息
- 首次发现时间: 2026-04-15
- 最后更新时间: 2026-04-15
- 修复版本:[可选]
- 当前工作区
- 验证版本:[可选]
- 当前工作区
- 备注:[可选]
- 后续仍需继续关注 `Dialog` 旧 synthetic move 机制与新模型的统一。
@@ -1,53 +0,0 @@
# BUG-20260415-0006
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260415-0006
- 标题: 托管局部重绘未正确提交脏子树导致嵌套 Canvas 按钮状态不刷新
- 状态:已修复
- 严重性:S2
- 优先级:P1
- 模块: 重绘 / 托管局部提交 / Canvas 嵌套
- 版本 / 分支: 当前工作区
- 环境: Windows / EasyX / KEY5 回归场景
- 发现人: 用户
- 关联 Fix IDFix-BUG-20260415-0006
## 问题描述
- 现象: KEY5 中第二层、第三层 Canvas 内按钮可正常触发 hover / click 回调,但视觉状态不刷新。
- 影响范围: 嵌套 Canvas 内的深层按钮、页内嵌套容器按钮、所有依赖托管局部重绘的脏后代场景。
- 期望结果: 只要深层按钮状态变化,最外层托管 root 应能正确提交对应脏子树并把视觉结果画出来。
- 实际结果: 日志显示事件和回调已执行,但屏幕上不出现 hover / press / release 反馈,直到触发更大范围重绘才会补出来。
## 复现信息
- 前置条件: 使用 KEY5 场景,启用三层 Canvas 嵌套回归区。
- 复现步骤:
1. 打开 KEY5。
2. 将鼠标移到第二层或第三层 Canvas 内的按钮上,观察 hover。
3. 点击按钮并观察按下、松开状态。
- 复现概率:必现
- 最小复现 DemoKEY5 A 区三层 Canvas 嵌套
- 证据:日志显示按钮回调已执行,但按钮视觉状态不刷新
## 初步分析
- 疑似位置: Canvas 托管局部重绘提交链、Window 托管重绘登记 coverage
- 触发条件: root 的直接子控件本身不 dirty,但其下存在 dirty descendant
- 相关线索:
- 托管局部提交只认直接 dirty child
- 深层按钮状态变化未提升到 root 下直接脏分支
- 最近相关改动:第二阶段布局与托管重绘收口
## 跟踪信息
- 首次发现时间: 2026-04-15
- 最后更新时间: 2026-04-15
- 修复版本:当前工作区
- 验证版本:KEY5 编译级验证通过,待用户手测
- 备注:该问题已按机制修复,不再使用临时整容器重绘补丁
@@ -1,53 +0,0 @@
# BUG-20260415-0007
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260415-0007
- 标题: 实际绘制 coverage 低估导致 Tooltip 与 overlay 补画漏算
- 状态:已修复
- 严重性:S2
- 优先级:P1
- 模块: Tooltip / overlay 重组 / 托管 coverage
- 版本 / 分支: 当前工作区
- 环境: Windows / EasyX / KEY5 回归场景
- 发现人: 用户
- 关联 Fix IDFix-BUG-20260415-0007
## 问题描述
- 现象: C 米区容器内按钮 Tooltip 会画到 D 橙区上层;类似问题也可能出现在带附加绘制区域的其他控件上。
- 影响范围: Tooltip、页签按钮 Tooltip、内部按钮与浮层、overlay 补画依赖 coverage 的所有链路。
- 期望结果: overlay 补画应按“实际写到屏幕上的区域”判断,而不是只按控件本体 bounds。
- 实际结果: 按钮本体未与上层控件相交时,Tooltip 超出部分不会触发 overlay 补画,导致 Tooltip 压到不应被压住的上层控件之上。
## 复现信息
- 前置条件: 使用 KEY5,保证 C 米区按钮 Tooltip 会伸进 D 橙区。
- 复现步骤:
1. 打开 KEY5。
2. 将鼠标移到 C 米区容器内按钮上,触发 Tooltip。
3. 观察 Tooltip 与 D 橙区的遮挡关系。
- 复现概率:必现
- 最小复现 DemoKEY5 C / D 交界区
- 证据:Tooltip 可见区域超出按钮 bounds,但 overlay 补画未跟上
## 初步分析
- 疑似位置: Button Tooltip coverage、Window/Canvas/TabControl 的 coverage 计算链
- 触发条件: 控件本体 bounds 未与 overlay 相交,但附加绘制区域(例如 Tooltip)已伸入 overlay 区
- 相关线索:
- 之前的 coverage 默认等于 `getBoundsRect()`
- `Button` 的 Tooltip 由 `tipLabel` 额外绘制,不属于按钮本体矩形
- 最近相关改动:顶层 overlay 传递式补画机制已补齐,但 coverage 仍按本体矩形计算
## 跟踪信息
- 首次发现时间: 2026-04-15
- 最后更新时间: 2026-04-15
- 修复版本:当前工作区
- 验证版本:KEY5 编译级验证通过,待用户手测
- 备注:Tooltip 智能选位明确后置,本次只修根因链路
@@ -1,68 +0,0 @@
# BUG-20260415-0008
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260415-0008
- 标题: TabControl 页签层级与重复激活链路导致 Tooltip 和残影异常
- 状态:已修复
- 严重性:S2
- 优先级:P1
- 模块: TabControl / 页签绘制 / 外部激活页签
- 版本 / 分支: 当前工作区
- 环境: Windows / EasyX / KEY1 / KEY5
- 发现人: 用户
- 关联 Fix IDFix-BUG-20260415-0008
## 问题描述
- 现象:
- 有任意页签打开时,页签按钮 Tooltip 无法正常显示,只有全部页签关闭时正常。
- KEY1 中页签 1 的表格超出 TabControl 区域,外部按钮重复激活同一页签后,再切页或关闭页签会留下超出部分残影。
- 影响范围: TabControl 页签按钮 Tooltip、外部 `setActiveIndex()` 重复调用、页内超出页面边界绘制的控件。
- 期望结果:
- 页签按钮应始终位于页面之上,Tooltip 正常显示。
- 外部重复激活已激活页签不应破坏当前可见页面的快照链。
- 实际结果:
- 页签按钮 Tooltip 会被页面盖掉。
- 外部重复激活同一页签后,页内超出页面区域的 Table 在切页/关页时会残留。
## 复现信息
- 前置条件:
- KEY5:存在可见页面和页签按钮 Tooltip
- KEY1:页签 1 内的 Table 长于 TabControl 页面区域,存在外部按钮 `test`
- 复现步骤:
1. 在 KEY5 中打开任意页签,将鼠标移到页签按钮上触发 Tooltip。
2. 观察 Tooltip 是否被页面盖掉。
3. 在 KEY1 中点击一次 `test` 激活页签 1。
4. 再点击一次 `test`,然后切页或关闭页签 1。
- 复现概率:必现
- 最小复现 DemoKEY5 TabControl 区、KEY1 页签 1 + 外部 `test` 按钮
- 证据:Tooltip 被页面覆盖;重复激活后切页/关页出现残影
## 初步分析
- 疑似位置:
- `TabControl::draw()`
- `TabControl::requestRepaint()`
- `TabControl::setActiveIndex()`
- `Button::setButtonClick()`
- 触发条件:
- 页签按钮先画、页面后画
- `TOGGLE` 同状态重复 set 仍触发 onToggleOn
- 相关线索:
- 页面绘制层级高于页签按钮时,页签 Tooltip 会被后画的页面盖掉
- 重复激活已激活页签会重复执行页面 `onWindowResize()` / `setIsVisible(true)`
- 最近相关改动:TabControl overlay / Tooltip 回归专项修复
## 跟踪信息
- 首次发现时间: 2026-04-15
- 最后更新时间: 2026-04-15
- 修复版本:当前工作区
- 验证版本:KEY1 / KEY5 编译级验证通过,待用户手测
- 备注:本次只修页签层级和重复激活链路,不扩到 TabControl 其它内部布局语义
@@ -1,51 +0,0 @@
# BUG-20260511-0009
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260511-0009
- 标题: TabControl::indexOf 未命中时返回最后索引
- 状态:已修复
- 严重性:S3
- 优先级:P2
- 模块: TabControl
- 版本 / 分支: 当前工作区
- 环境: Windows / EasyX / z-testDome 回归场景
- 发现人: Codex 巡检
- 关联 Fix ID: Fix-BUG-20260511-0009
## 问题描述
- 现象: `TabControl::indexOf(const std::string& tabText) const` 在未找到目标页签时返回循环后的最后索引。
- 影响范围: 依赖 `indexOf()` 判断页签是否存在的调用方可能把“不存在”误判为最后一个页签。
- 期望结果: 找到匹配页签时返回对应索引;未找到时返回 `-1`
- 实际结果: 未找到时返回最后一个页签的索引。
## 复现信息
- 前置条件:[可选]
- 复现步骤:
1. 构造包含至少一个页签的 `TabControl`
2. 调用 `indexOf()` 查询不存在的页签文本。
3. 观察返回值。
- 复现概率:必现
- 最小复现 Demo[可选]
- 证据:源码巡检可证
## 初步分析
- 疑似位置: `TabControl.cpp::indexOf`
- 触发条件: 查询文本不存在。
- 相关线索: 函数名和预期语义要求“不存在返回 -1”,但旧实现返回 `idx`
- 最近相关改动:[可选]
## 跟踪信息
- 首次发现时间: 2026-05-11
- 最后更新时间: 2026-05-11
- 修复版本: 当前工作区
- 验证版本: KEY1 ~ KEY6 编译验证通过
- 备注:[可选]
@@ -1,52 +0,0 @@
# BUG-20260511-0010
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260511-0010
- 标题: 局部提交内部 coverage 扩张未反馈导致 Dialog 漏补
- 状态:已修复
- 严重性:S2
- 优先级:P1
- 模块: Window / Canvas / Dialog / 托管重绘
- 版本 / 分支: 当前工作区
- 环境: Windows / EasyX / KEY6
- 发现人: 用户
- 关联 Fix ID: Fix-BUG-20260511-0010
## 问题描述
- 现象: KEY6 中非模态 Dialog 打开后,触发旁边按钮 Tooltip,某些位置会导致下层按钮重绘覆盖 Dialog。
- 影响范围: 可局部提交的 root 在内部补画兄弟控件后,如果实际写屏范围扩大,Window 可能仍按初始 coverage 判断上层 overlay。
- 期望结果: 下层 root 内部扩张后的实际写屏区域能触发上层 Dialog 补画。
- 实际结果: Window 使用初始 coverage,导致 Dialog overlay 补画漏判。
## 复现信息
- 前置条件:[可选] 打开 KEY6,触发非模态 `AbortRetryIgnore` 对话框。
- 复现步骤:
1. 最大化窗口。
2. 打开 KEY6 非模态 `AbortRetryIgnore` 对话框。
3.`RetryCancel` 按钮特定区域触发 Tooltip。
4. 观察非模态 Dialog 是否被下层按钮局部重绘覆盖。
- 复现概率:高概率
- 最小复现 DemoKEY6
- 证据:临时 `OverlayTemp` 日志显示 Canvas 内部 coverage 从 `(250,512,490,583)` 扩张到 `(250,512,700,583)`,但 Window 仍用旧 working coverage 判断 Dialog,相交结果为 `hit=0`
## 初步分析
- 疑似位置: `Window::flushManagedRepaint()``Canvas::requestRepaint(this)` 的 coverage 交接。
- 触发条件: root 支持局部提交,内部补画后序兄弟导致实际 coverage 扩张。
- 相关线索: Dialog 区域与内部最终 coverage 相交,但与 Window 初始 working coverage 不相交。
- 最近相关改动:[可选] 托管局部重绘与 overlay 补画收口。
## 跟踪信息
- 首次发现时间: 2026-05-11
- 最后更新时间: 2026-05-11
- 修复版本: 当前工作区
- 验证版本: KEY6 编译通过,待用户手测
- 备注:[可选] 精确 actual coverage 回传作为下版本优化项。
@@ -1,51 +0,0 @@
# BUG-20260511-0011
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260511-0011
- 标题: 跨 root 回调改脏未同轮提交导致 Label 延迟刷新
- 状态:已修复
- 严重性:S3
- 优先级:P1
- 模块: Window / 托管重绘 / Label / TabControl
- 版本 / 分支: 当前工作区
- 环境: Windows / EasyX / KEY6
- 发现人: 用户
- 关联 Fix ID: Fix-BUG-20260511-0011
## 问题描述
- 现象: KEY6 中点击 Right 页签内按钮后,A 区状态 Label 不立即刷新,需要下一次左键消息才显示。
- 影响范围: 事件回调修改了另一个顶层 root 下的控件状态时,目标 root 可能不会进入本轮托管重绘队列。
- 期望结果: 回调中改脏的跨 root 控件能在同一轮事件尾刷新。
- 实际结果: 只有当前事件分发链所在 root 被提交,其他 root 的 dirty 子树滞留到下一次事件。
## 复现信息
- 前置条件:[可选] 打开 KEY6,激活右侧页签第一页。
- 复现步骤:
1. 点击 Right 页签中的“页内按钮”。
2. 观察 A 区状态 Label。
3. 再点击空白处或其他控件。
- 复现概率:必现
- 最小复现 DemoKEY6
- 证据:`Label::setText()` 只标脏目标 Label,不主动把其所属 `shapePanel` root 登记到 Window。
## 初步分析
- 疑似位置: `Window` 事件尾托管重绘收口。
- 触发条件: 当前事件 root 与被回调修改的目标 root 不同。
- 相关线索: A 区 Label 在下一次左键消息后才刷新,说明 dirty 状态存在但未同轮提交。
- 最近相关改动:[可选] 托管重绘统一收口。
## 跟踪信息
- 首次发现时间: 2026-05-11
- 最后更新时间: 2026-05-11
- 修复版本: 当前工作区
- 验证版本: KEY6 编译通过,待用户手测
- 备注:[可选]
@@ -1,54 +0,0 @@
# BUG-20260511-0012
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
## 基本信息
- ID: BUG-20260511-0012
- 标题: Tooltip 临时浮层污染或擦除兄弟控件快照
- 状态:已修复
- 严重性:S2
- 优先级:P1
- 模块: Tooltip / Canvas / TabControl / 托管重绘 / 背景快照
- 版本 / 分支: 当前工作区
- 环境: Windows / EasyX / KEY6
- 发现人: 用户
- 关联 Fix ID: Fix-BUG-20260511-0012
## 问题描述
- 现象: KEY6 A 区禁用按钮触发 Tooltip 后,如果 Tooltip 位于状态 Label 下方,状态 Label 可能捕获 Tooltip 到背景快照,或用透明 Label 的旧快照擦掉 Tooltip 一部分。
- 影响范围: 同一容器内,绘制顺序在 Tooltip 触发控件之后、且与 Tooltip coverage 相交的兄弟控件。
- 期望结果: Tooltip 作为临时浮层显示,不应污染兄弟控件背景快照,也不应被透明 Label 的旧快照擦除。
- 实际结果: 旧逻辑把 Tooltip coverage 与持久绘制 coverage 混用,导致兄弟控件作废快照并重抓,或保留快照重画时擦掉 Tooltip。
## 复现信息
- 前置条件:[可选] 打开 KEY6。
- 复现步骤:
1. 将鼠标移到 A 区禁用按钮上触发 Tooltip。
2. 让 Tooltip 显示区域位于状态 Label 下方。
3. 观察状态 Label 区域是否覆盖 / 擦除 Tooltip。
4. 触发下一次重绘后观察是否有 Tooltip 残留。
- 复现概率:高概率
- 最小复现 DemoKEY6 A 区
- 证据:`Label::draw()` 即使透明也会先 `restBackground()`;如果被临时浮层触发补画,会回贴旧快照。
## 初步分析
- 疑似位置: `Canvas::requestRepaint(this)` / `TabControl::requestRepaint(this)` 的局部 overlay 补画策略。
- 触发条件: 下层 Button Tooltip 的完整 coverage 与后序兄弟控件相交。
- 相关线索:
- `Button::getManagedRepaintCoverageRect()` 包含 Tooltip。
- Canvas 旧逻辑用同一 coverage 同时决定“是否补画兄弟”和“是否刷新兄弟背景快照”。
- 最近相关改动:[可选] Tooltip coverage 纳入托管重绘链。
## 跟踪信息
- 首次发现时间: 2026-05-11
- 最后更新时间: 2026-05-11
- 修复版本: 当前工作区
- 验证版本: KEY1 ~ KEY6 编译通过,待用户手测
- 备注:[可选]
@@ -1,93 +0,0 @@
# 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[可选]
- 发布版本:[可选]
- 备注:该修复跨多个阶段逐步收敛,不是一次性完成
@@ -1,88 +0,0 @@
# 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[可选]
- 发布版本:[可选]
- 备注:这部分修复多为基础治理,部分早期修改发生在仓库初始化之前
@@ -1,80 +0,0 @@
# Fix-BUG-20260409-0003
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: `Fix-BUG-20260409-0003`
- 关联 BUG ID: `BUG-20260409-0003`
- 修复目标: 取消对合法大跨度最大化的误拦截,仅保留非法尺寸保护
- 状态:已完成 / 待用户回归
- 负责人:Codex 协作修改
- 分支 / 版本:`master`
## 根因分析
- 旧逻辑问题:
- `runEventLoop()` 的 resize 收口中存在“跨度保护”:
- `abs(finalW - width) > 1000 || abs(finalH - height) > 1000`
- 命中后直接跳过整个收口流程。
- 为什么会出问题:
- 该判断关注的是“变化跨度”,而不是“尺寸是否合法”。
- 在大屏、窗口初始尺寸较小、直接最大化等正常场景下,跨度超过 `1000` 很常见。
- 为什么之前未必稳定暴露:
- 当初始尺寸较大时,最大化跨度较小,不会命中阈值。
- 先手动拉大一点后再最大化,也会避开该阈值。
## 修复方案
- 修复思路:
- 删除“跨度过大即跳过”的保护语义。
- 改成“仅非法客户区尺寸才跳过”。
- 保留关键 `DEBUG` 日志,方便追踪极端尺寸帧。
- 关键改动:
- 删除旧的 `>1000` 误拦截分支。
- 引入虚拟桌面尺寸:
- `SM_CXVIRTUALSCREEN`
- `SM_CYVIRTUALSCREEN`
- 仅在以下情况下跳过本次收口:
- `actualWidth <= 0`
- `actualHeight <= 0`
- `actualWidth > max(10000, virtualScreenWidth * 2)`
- `actualHeight > max(10000, virtualScreenHeight * 2)`
- 对“大跨度但合法”的 resize 增加 `DEBUG` 日志,但继续执行收口。
- 涉及文件 / 类 / 函数:
- `Window.cpp`
- `Window::runEventLoop()`
- 输出日志:
- `尺寸调整被非法尺寸保护跳过`
- `检测到大跨度尺寸调整,继续执行收口`
## 影响评估
- 对外行为变化:
- 有。直接最大化不再被 `>1000` 阈值拦截。
- 兼容性影响:
- 正向兼容。正常拖拽 resize、正常最大化都仍然走原有收口流程。
- 风险:
- 如果系统偶发产生一次极端异常客户区尺寸,可能会更多依赖当前“非法尺寸保护”而不是原来的跨度阈值。
- 但相较于误杀合法最大化,这个风险更合理。
## 验证结果
- 验证方式:
1.`>1000` 旧保护分支中先加 `DEBUG` 日志,确认 `KEY == 1 / 2` 命中。
2. 替换为非法尺寸保护后,编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)。
3.`KEY == 1 ~ 4` 做逻辑推演。
- 理论结果:
- `KEY == 1 / 2`:现在应打印“大跨度但继续执行收口”日志,并继续完成 `尺寸调整已完成`
- `KEY == 3 / 4`:原本正常,行为保持一致。
- 备注:
- 当前这轮无法直接替用户跑 GUI 自动化,只能做逻辑推演和源码级编译验证。
## 落地信息
- Commit
- `f567369`:修改前快照
- 当前工作区:已替换跨度保护,待提交
- 发布版本:待定
- 备注:
- 同轮顺手删除了 `Window.cpp` 中一个未使用的 `ExMessage mm;` 遗留变量。
@@ -1,88 +0,0 @@
# Fix-BUG-20260410-0004
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: `Fix-BUG-20260410-0004`
- 关联 BUG ID: `BUG-20260410-0004`
- 修复目标: 让按钮 Tooltip 在鼠标移出后立即回贴并消失,不再残留在屏幕上
- 状态:已验证
- 负责人: Codex 协作修改
- 分支 / 版本: 当前工作区 / 下一版本开发中
## 根因分析
- 根因:
- Tooltip 由 `Button` 在绘制末尾以内置浮层方式直接补画,不属于独立控件树节点。
- 隐藏路径里根据托管分发状态分叉处理,在托管分发阶段仅调用 `invalidateBackgroundSnapshot()`,只作废 Tooltip 快照,不执行回贴。
- 当本轮后续重绘只覆盖按钮本体区域时,Tooltip 占用的屏幕区域不会被擦除,因此表现为 Tooltip 留在屏幕上。
- 触发条件:
- Tooltip 已显示
- 鼠标移出按钮区域,进入 `hideTooltip()` 分支
- 为什么之前没发现:
- 之前更关注 Tooltip 的显示、Hover 切换和对话框遮挡链路
- “逻辑状态已隐藏,但屏幕区域无人回贴”的问题只有在当前托管重绘路径下才更稳定暴露
- 关键证据:
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp) 中 `hideTooltip()` 在托管分发阶段只作废快照,不回贴 Tooltip 自身区域
## 修复方案
- 修复思路:
- Tooltip 既然是 `Button` 的内置浮层,就按“内置浮层”语义处理隐藏逻辑
- 隐藏时直接调用 Tooltip 现有接口回贴背景快照并作废,不再区分托管分发阶段
- 关键改动:
- 简化 `Button::hideTooltip()` 分支
- 只要 `tipVisible == true`,统一调用 `tipLabel.hide()`
- 保留 `tipHoverTick` 重置逻辑
- 涉及文件 / 类 / 函数:
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp)
- `Button::hideTooltip()`
- 影响的 API / 行为:
- 无公开 API 变化
- 内部行为调整为“Tooltip 隐藏时总是立即回贴其自身区域”
- 关键约束 / 不变量:
- Tooltip 继续作为 `Button` 内置浮层存在
- 不引入新的 Tooltip 控件树托管协议
- 不扩大 `Window` 托管重绘 coverage
- 回滚点 / 开关:
- 若后续需要回退,只需恢复 `Button::hideTooltip()` 的旧分支逻辑
## 影响评估
- 影响范围:
- 启用 Tooltip 的 `Button`
- `KEY == 4` 等包含 Tooltip 的回归场景
- 兼容性影响:无
- 行为变化:有(Tooltip 隐藏时由“部分路径只作废快照”改为“统一立即回贴并作废”)
- 性能影响:无
- 回归风险:
- Tooltip 隐藏时会发生一次即时局部回贴
- 由于 Tooltip 本身就是按钮内置浮层,这个行为与其显示方式一致,风险相对可控
## 验证结果
- 验证步骤:
1. 修改 [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp) 中 `Button::hideTooltip()` 的 Tooltip 隐藏分支
2. 编译 `Button.cpp`
3. 编译 `z-testDome.cpp` 并指定 `KEY=4`,确认 Tooltip 相关 demo 入口可正常编译
- 验证结果:
- 代码已完成修改
- 源码级编译验证通过
- 基于当前代码链路推演,Tooltip 离开按钮后会立即回贴自身区域,不再依赖按钮本体重绘覆盖 Tooltip 区域
- 回归检查:
- 未做 GUI 手动交互回归
- 需用户进一步确认实际 Tooltip 行为
- 验证证据:
- `Button.cpp` 编译通过
- `z-testDome.cpp /DKEY=4` 编译通过
## 落地信息
- Commit: 当前工作区未提交
- PR[可选]
- 发布版本:[可选]
- 备注:
- 本次修复未改动 Tooltip 对外接口,只调整内部隐藏路径
@@ -1,83 +0,0 @@
# Fix-BUG-20260415-0005
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260415-0005
- 关联 BUG ID: BUG-20260415-0005
- 修复目标: 收口局部重绘提交后的 overlay 兄弟补画机制
- 状态:已完成
- 负责人: Codex 协作修改
- 分支 / 版本: 当前工作区
## 根因分析
- 根因:
- 局部重绘提交只重画了 dirty root 或 dirty child,没有按父容器真实绘制顺序把 coverage 上方相交的兄弟补画回来。
- 触发条件:
- 下层区域写入像素后,上层 sibling 与本次 coverage 相交。
- 为什么之前没发现:[可选]
- 旧场景多集中在对话框覆盖链,普通 sibling overlay 问题没有被系统化回归。
- 关键证据:[可选]
- `KEY5``Table` 翻页覆盖顶层粉色浮层可稳定复现。
- 同父 `Canvas` 相交子控件在局部重绘后会出现相同症状。
## 修复方案
- 修复思路:
- 不做控件之间的长期联动标记。
- 改为由父容器在局部重绘提交后,按真实绘制顺序动态补画 coverage 上方相交的直接绘制单元。
- 关键改动:
- `Window`:补画普通顶层控件 overlay,而不再只补 `Dialog`
- `Canvas`:补画 coverage 上方相交的直接子控件
- `TabControl`:按自己的真实绘制顺序补画页签按钮与页面
- overlay 补画前统一 `invalidateBackgroundSnapshot()`,避免旧快照反贴旧背景
- 涉及文件 / 类 / 函数:
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
- 影响的 API / 行为:[可选]
- 无对外 API 变化
- 局部重绘行为更严格遵守视觉合成顺序
- 关键约束 / 不变量:[可选]
- 父容器只处理自己的直接绘制单元
- overlay 补画顺序必须与实际 `draw()` 顺序一致
- 不升级为整窗 / 整容器重绘,除非父容器自身快照或 dirty 条件不满足
- 回滚点 / 开关:[可选]
- 无单独开关,回滚需回退相关局部重绘提交逻辑
## 影响评估
- 影响范围:
- `Window / Canvas / TabControl` 的局部重绘提交路径
- 兼容性影响:无
- 行为变化:有(局部重绘后会补画上层 overlay)
- 性能影响:有(增加必要的 overlay 补画),但仍显著小于整窗 / 整容器重绘
- 回归风险:
- 若 coverage 计算不准,仍可能遗漏或多画
- `TabControl` 局部重绘顺序必须和真实 `draw()` 顺序保持一致
## 验证结果
- 验证步骤:
1. 编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)、[`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)、[`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
2. 编译 [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp) `KEY=5`
3. 回归 `Table` 翻页、相交 `Canvas``TabControl` 页签/页面遮挡场景
- 验证结果:
- 编译级验证通过
- GUI 手动回归需继续在本机确认
- 回归检查:[可选]
- `Table` 分页按钮与页码链已同步收口
- 验证证据:[可选]
- `KEY5` 已新增更明确的 overlay 专项场景
## 落地信息
- Commit: 未提交(当前工作区)
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
- `Dialog` 旧 synthetic move 逻辑本轮保留,后续可再评估是否纳入通用清理模型。
@@ -1,69 +0,0 @@
# Fix-BUG-20260415-0006
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260415-0006
- 关联 BUG ID: BUG-20260415-0006
- 修复目标: 让托管局部重绘能够正确提交嵌套容器中的脏后代
- 状态:已完成
- 负责人: Codex
- 分支 / 版本: 当前工作区
## 根因分析
- 根因: 托管局部重绘提交只识别 root 的直接 dirty child,不识别“自己不脏,但下面有 dirty descendant”的直接子分支。
- 触发条件: 深层按钮状态变化后,叶子控件已 dirty,但中间层 Canvas 自己未 dirty。
- 为什么之前没发现: 第一阶段主要覆盖顶层和浅层容器,三层嵌套专项场景在 KEY5 重构后才稳定暴露。
- 关键证据:
- 按钮 hover / click 回调正常执行
- 日志正常,但视觉状态直到更大重绘才补出来
## 修复方案
- 修复思路: 把托管重绘提交从“只认直接 dirty child”改成“识别 dirty descendant,并提升到 root 下直接脏分支提交”。
- 关键改动:
- 新增 `hasManagedDirtySubtree()`
- 新增 `getManagedRepaintDirectBranch(root)`
- `Canvas / TabControl` 局部提交时,直接子单元若拥有 dirty descendant,也继续 `commitManagedRepaint()`
- `Window::requestManagedRepaint()` 的 coverage 从最深叶子提升到 root 下直接脏分支
- 涉及文件 / 类 / 函数:
- `Control.h / Control.cpp`
- `Canvas.cpp`
- `TabControl.cpp`
- `Window.cpp`
- 影响的 API / 行为:无公开 API 变化
- 关键约束 / 不变量:
- dirty descendant 不能再被中间层漏掉
- 托管局部提交顺序仍需保持与真实绘制顺序一致
- 回滚点 / 开关:无
## 影响评估
- 影响范围: 嵌套 Canvas、页内嵌套容器、深层按钮 hover / click / tooltip 状态刷新
- 兼容性影响:无
- 行为变化:有(深层按钮视觉状态会及时刷新)
- 性能影响:有(coverage 更保守,局部补画可能略增)
- 回归风险:
- 局部提交 coverage 扩大后,上层 overlay 补画次数可能增加
- 需重点回归嵌套容器与顶层 overlay 场景
## 验证结果
- 验证步骤:
1. 编译 `Control.cpp / Canvas.cpp / TabControl.cpp / Window.cpp / z-testDome.cpp`
2. 使用 KEY5 三层嵌套区作为主回归入口
3. 检查深层按钮 hover / press / release 是否能即时刷新
- 验证结果: 编译通过;逻辑推演闭合;GUI 需用户本机手测
- 回归检查:KEY5 三层嵌套、页内嵌套、overlay 补画主线
- 验证证据:编译级验证通过
## 落地信息
- Commit: 未单独提交
- PR[可选]
- 发布版本:[可选]
- 备注:该修复为机制修正,不是 KEY5 专项补丁
@@ -1,71 +0,0 @@
# Fix-BUG-20260415-0007
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260415-0007
- 关联 BUG ID: BUG-20260415-0007
- 修复目标: 让托管 coverage 与 overlay 补画基于实际绘制范围,而非控件本体 bounds
- 状态:已完成
- 负责人: Codex
- 分支 / 版本: 当前工作区
## 根因分析
- 根因: `Window / Canvas / TabControl` 的 coverage 计算链默认使用 `getBoundsRect()`,没有把 Tooltip 这类附加绘制区域纳入。
- 触发条件: 控件本体未与 overlay 相交,但附加绘制区域超出本体并写入更上层区域。
- 为什么之前没发现: 第一轮主要解决普通控件和顶层 overlay;Tooltip 属于本体外附加绘制,直到 C / D 交界回归才稳定暴露。
- 关键证据:
- 顶层按钮 Tooltip 正常,但容器内按钮 Tooltip 压到 D 橙区
- 根因与按钮本体 bounds 无关,与 Tooltip 超出范围有关
## 修复方案
- 修复思路: 引入“实际绘制 coverage”接口,让托管重绘与局部提交统一走真实覆盖范围。
- 关键改动:
-`Control` 增加 `getManagedRepaintCoverageRect()`
- `Button` 在 Tooltip 可见时返回“按钮矩形 + Tooltip 矩形”的并集
- `Canvas / TabControl / Table` 将可见内部绘制单元 coverage 递归并入
- `Window::requestManagedRepaint()` 和顶层 overlay 重组改为走 coverage 接口
- 涉及文件 / 类 / 函数:
- `Control.h / Control.cpp`
- `Button.h / Button.cpp`
- `Canvas.h / Canvas.cpp`
- `TabControl.h / TabControl.cpp`
- `Table.h / Table.cpp`
- `Window.cpp`
- 影响的 API / 行为:新增内部 virtual 接口,无公开 API 变化
- 关键约束 / 不变量:
- Tooltip 智能选位不在本次修复范围内
- overlay 补画继续按父容器真实绘制顺序工作
- 回滚点 / 开关:无
## 影响评估
- 影响范围: Tooltip、overlay 补画、复杂容器内附加绘制区域
- 兼容性影响:无
- 行为变化:有(Tooltip 与 overlay 层级恢复正确)
- 性能影响:有(coverage 更保守,可能触发更多 overlay 补画)
- 回归风险:
- coverage 扩大后顶层/容器层局部提交会多画一些 overlay
- 需重点回归 Tooltip 与多跳 overlay 场景
## 验证结果
- 验证步骤:
1. 编译 `Control.cpp / Button.cpp / Canvas.cpp / TabControl.cpp / Table.cpp / Window.cpp / z-testDome.cpp`
2. 在 KEY5 中验证 C 米区容器内按钮 Tooltip 与 D 橙区的遮挡关系
3. 回归顶层按钮 Tooltip、页签按钮 Tooltip、浮层按钮 Tooltip
- 验证结果: 编译通过;逻辑推演闭合;GUI 需用户本机手测
- 回归检查:KEY5 C/D 交界区、TabControl 页签、三层嵌套按钮 Tooltip
- 验证证据:编译级验证通过
## 落地信息
- Commit: 未单独提交
- PR[可选]
- 发布版本:[可选]
- 备注:Tooltip 智能选位明确延期,本次只修“coverage 正确性”
@@ -1,75 +0,0 @@
# Fix-BUG-20260415-0008
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260415-0008
- 关联 BUG ID: BUG-20260415-0008
- 修复目标: 修正 TabControl 页签层级和重复激活已激活页签的回调链
- 状态:已完成
- 负责人: Codex
- 分支 / 版本: 当前工作区
## 根因分析
- 根因:
- `TabControl::draw()` 与局部重绘顺序为“先页签按钮,后页面”,导致页签 Tooltip 被页面盖掉。
- `Button::setButtonClick()``TOGGLE` 没有做“同状态短路”,外部重复激活同一页签会重复触发 `onToggleOnCallback`
- `TabControl::setActiveIndex()` 也没有对“目标已是当前激活页”做保护,导致当前可见页的 `onWindowResize()` / `setIsVisible(true)` 链被重复执行。
- 触发条件:
- 任意页打开时触发页签 Tooltip
- 外部对已激活页签再次 `setActiveIndex()`
- 为什么之前没发现: KEY1 外部按钮重复激活场景和 KEY5 页签 Tooltip 场景是后续专项回归才补出来的。
- 关键证据:
- 所有页关闭时页签 Tooltip 正常,说明问题在页面层级覆盖
- 只点一次外部激活正常,重复激活后残影出现,说明问题在重复回调链
## 修复方案
- 修复思路:
- 把 TabControl 绘制/局部重绘顺序改为“先页面、后页签按钮”
-`TOGGLE` 状态设置和 `setActiveIndex()` 增加同状态短路
- 关键改动:
- `TabControl::draw()` 改成先画页面、后画页签按钮
- `TabControl::requestRepaint(this)` 的局部提交顺序同步调整
- `Button::setButtonClick()``TOGGLE` 同状态时直接返回
- `TabControl::setActiveIndex()` 若目标已是当前激活页则直接返回
- 涉及文件 / 类 / 函数:
- `TabControl.cpp`
- `Button.cpp`
- 影响的 API / 行为:
- 重复设置相同 `TOGGLE` 状态不再重复触发回调
- 关键约束 / 不变量:
- 页签按钮视觉层级应始终高于页面
- 外部重复激活同一页签不应重新走页面显示链
- 回滚点 / 开关:无
## 影响评估
- 影响范围: TabControl 页签 Tooltip、外部页签激活、页内超出页面边界的复合控件
- 兼容性影响:有(收紧了 TOGGLE 同状态重复 set 的回调语义)
- 行为变化:有(页签 Tooltip 层级恢复正确;重复激活同一页签不再重复触发回调)
- 性能影响:无
- 回归风险:
- 如有外部代码依赖“同状态重复 set 也要重复触发回调”,该行为将被收掉
- 需重点回归 KEY1 和 KEY5 的 TabControl 场景
## 验证结果
- 验证步骤:
1. 编译 `Button.cpp / TabControl.cpp`
2. 编译 `z-testDome.cpp``KEY=1`
3. 回归 `KEY5` 页签 Tooltip 与 `KEY1` 外部按钮重复激活页签场景
- 验证结果: 编译通过;逻辑推演闭合;GUI 需用户本机手测
- 回归检查:KEY1 页签 1 表格超出区、KEY5 页签 Tooltip
- 验证证据:编译级验证通过
## 落地信息
- Commit: 未单独提交
- PR[可选]
- 发布版本:[可选]
- 备注:此修复不包含 TabControl 其它内部布局专题
@@ -1,55 +0,0 @@
# Fix-BUG-20260511-0009
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260511-0009
- 关联 BUG ID: BUG-20260511-0009
- 修复目标: 保证 `TabControl::indexOf()` 未命中时返回 `-1`
- 状态:已完成
- 负责人: Codex
- 分支 / 版本: 当前工作区
## 根因分析
- 根因: `indexOf()` 循环结束后直接返回 `idx`,没有显式处理未命中分支。
- 触发条件: 查询不存在的页签文本。
- 为什么之前没发现:[可选] 当前测试更多关注页签切换行为,未覆盖未命中返回值。
- 关键证据:[可选] `TabControl.cpp` 旧实现未命中路径返回最后索引。
## 修复方案
- 修复思路: 未找到匹配项时显式返回 `-1`
- 关键改动: 将 `TabControl::indexOf()` 末尾返回值从 `idx` 改为 `-1`
- 涉及文件 / 类 / 函数: `TabControl.cpp::TabControl::indexOf`
- 影响的 API / 行为:[可选] 行为修正;符合函数语义。
- 关键约束 / 不变量:[可选] 找到时仍返回真实索引。
- 回滚点 / 开关:[可选] 无
## 影响评估
- 影响范围: 仅影响 `indexOf()` 未命中返回值。
- 兼容性影响:无
- 行为变化:有(未命中从最后索引修正为 `-1`
- 性能影响:无
- 回归风险: 低
## 验证结果
- 验证步骤:
1. 编译 `TabControl.cpp`
2. 编译 `z-testDome.cpp``KEY1 ~ KEY6`
3. 静态确认未命中路径返回 `-1`
- 验证结果: 编译通过
- 回归检查:[可选] KEY1 ~ KEY6 编译级回归通过
- 验证证据:[可选]
## 落地信息
- Commit: 未提交
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
@@ -1,57 +0,0 @@
# Fix-BUG-20260511-0010
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260511-0010
- 关联 BUG ID: BUG-20260511-0010
- 修复目标: 避免局部 root 内部 coverage 扩张后漏补 Dialog / 上层 overlay
- 状态:已完成
- 负责人: Codex
- 分支 / 版本: 当前工作区
## 根因分析
- 根因: `commitManagedRepaint()` 只执行绘制,不返回本轮实际写屏 coverage;Window 仍按登记时的初始 coverage 进行上层补画判断。
- 触发条件: Canvas / TabControl 内部局部提交中补画了后序兄弟控件,实际写屏范围大于初始 source coverage。
- 为什么之前没发现:[可选] 需要 Dialog、Tooltip、后序兄弟补画三者同时满足。
- 关键证据:[可选] 临时日志中 `Canvas partial end coverage` 已与 Dialog 相交,但 `dialog overlay check` 使用旧 working coverage 结果为不相交。
## 修复方案
- 修复思路: 在 Window 层进行保守兜底。root 局部提交完成后,如果 root 仍支持局部提交,则将 root 当前 managed coverage 并入上层补画判断 coverage。
- 关键改动:
- `Window::flushManagedRepaint()``root->commitManagedRepaint()` 后合并 `root->getManagedRepaintCoverageRect()`
- 只扩大上层补画判断,不改变 root 自身局部提交策略。
- 涉及文件 / 类 / 函数: `Window.cpp::flushManagedRepaint`
- 影响的 API / 行为:[可选] 无公开 API 变化。
- 关键约束 / 不变量:[可选] Dialog 仍保持顶层 overlay 语义。
- 回滚点 / 开关:[可选] 回退该 coverage 合并逻辑。
## 影响评估
- 影响范围: 托管重绘后的上层普通控件 / Dialog 补画判断。
- 兼容性影响:无
- 行为变化:有(Dialog / 上层控件补画更保守)
- 性能影响:有(可能多补画少量 overlay)
- 回归风险: 中低;主要风险是局部 root 大范围覆盖时上层补画次数增加。
## 验证结果
- 验证步骤:
1. 编译 `Window.cpp / Canvas.cpp`
2. 编译 `z-testDome.cpp /DKEY=6`
3. 编译 `z-testDome.cpp``KEY1 ~ KEY6`
- 验证结果: 编译通过;临时日志已删除
- 回归检查:[可选] KEY6 非模态 Dialog + Tooltip 覆盖链待用户手测
- 验证证据:[可选]
## 落地信息
- Commit: 未提交
- PR[可选]
- 发布版本:[可选]
- 备注: 下版本建议让 `commitManagedRepaint()` 返回 actual coverage,替代 root 全覆盖兜底。
@@ -1,60 +0,0 @@
# Fix-BUG-20260511-0011
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260511-0011
- 关联 BUG ID: BUG-20260511-0011
- 修复目标: 事件尾补收集跨 root dirty 子树,保证同轮托管提交
- 状态:已完成
- 负责人: Codex
- 分支 / 版本: 当前工作区
## 根因分析
- 根因: `Window::requestManagedRepaint()` 只登记当前事件链触发的 source/root;回调里直接修改其他 root 的控件时,该 root 只变 dirty,没有进入 `managedRepaintItems`
- 触发条件: 点击一个 root 中的控件,回调修改另一个 root 下的 Label / 控件状态。
- 为什么之前没发现:[可选] 需要跨 root 回调才能稳定暴露。
- 关键证据:[可选] 下一次鼠标消息后 Label 才刷新,说明目标 root dirty 状态滞留。
## 修复方案
- 修复思路: 在普通输入事件收口阶段、`flushManagedRepaint()` 前扫描所有顶层普通控件和可见 Dialog,发现未登记但存在 dirty 子树的 root,则补登记为托管重绘项。
- 关键改动:
- 新增 `Window::collectDirtyRootsForManagedRepaint()`
- `runEventLoop()``flushManagedRepaint()` 前调用该函数。
- 涉及文件 / 类 / 函数:
- `Window.h`
- `Window.cpp::collectDirtyRootsForManagedRepaint`
- `Window.cpp::runEventLoop`
- 影响的 API / 行为:[可选] 无公开 API 变化。
- 关键约束 / 不变量:[可选] 只处理可见 root;已登记 root 不重复登记。
- 回滚点 / 开关:[可选] 移除事件尾补收集调用。
## 影响评估
- 影响范围: 普通输入事件尾的托管重绘收口。
- 兼容性影响:无
- 行为变化:有(跨 root dirty 会更及时刷新)
- 性能影响:有(事件尾多一次顶层 dirty 子树扫描)
- 回归风险: 中;如果存在历史残留 dirty,可能更早暴露重绘问题。
## 验证结果
- 验证步骤:
1. 编译 `Window.cpp`
2. 编译 `z-testDome.cpp /DKEY=6`
3. 编译 `z-testDome.cpp``KEY1 ~ KEY6`
- 验证结果: 编译通过
- 回归检查:[可选] KEY6 Right 页内按钮更新 A 区状态 Label 待用户手测
- 验证证据:[可选]
## 落地信息
- Commit: 未提交
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
@@ -1,68 +0,0 @@
# Fix-BUG-20260511-0012
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
## 关联信息
- Fix ID: Fix-BUG-20260511-0012
- 关联 BUG ID: BUG-20260511-0012
- 修复目标: 区分 Tooltip 临时浮层 coverage 与会污染背景快照的持久 coverage
- 状态:已完成
- 负责人: Codex
- 分支 / 版本: 当前工作区
## 根因分析
- 根因: 托管重绘只有一类 coverage,既用于完整绘制范围判断,也用于兄弟控件背景快照刷新判断。Tooltip 属于临时浮层,不应进入后者。
- 触发条件: Tooltip coverage 覆盖到后序兄弟控件,兄弟控件被局部 overlay 补画。
- 为什么之前没发现:[可选] 需要 Tooltip 与透明 Label 或带快照兄弟控件重叠。
- 关键证据:[可选] 透明 Label 已设置 `setTextdisap(true)`,但 `draw()` 仍会先回贴背景快照。
## 修复方案
- 修复思路: 拆分完整 coverage 与持久 coverage。Tooltip 进入完整 coverage,用于 Window/Dialog overlay 判断;但不进入持久 coverage,避免兄弟控件刷新快照时捕获 Tooltip。
- 关键改动:
- `Control` 新增内部虚函数 `getManagedRepaintPersistentCoverageRect()`
- `Button` 的完整 coverage 包含 Tooltip,持久 coverage 仅包含按钮本体。
- `Canvas / TabControl / Table` 递归合并子控件持久 coverage。
- `Canvas / TabControl` 局部提交中,只有命中持久 coverage 的兄弟控件才作废快照并补画。
- 只命中 Tooltip 等临时 coverage 的兄弟控件不再补画,避免透明控件回贴旧快照擦掉 Tooltip。
- 涉及文件 / 类 / 函数:
- `Control.h / Control.cpp`
- `Button.h / Button.cpp`
- `Canvas.h / Canvas.cpp`
- `TabControl.h / TabControl.cpp`
- `Table.h / Table.cpp`
- 影响的 API / 行为:[可选] 新增内部 virtual 接口,无公开用户 API 变化。
- 关键约束 / 不变量:[可选] Tooltip 智能选位仍不在本轮范围内。
- 回滚点 / 开关:[可选] 回退持久 coverage 拆分逻辑。
## 影响评估
- 影响范围: Tooltip、Canvas / TabControl 局部 overlay 补画、背景快照捕获。
- 兼容性影响:无
- 行为变化:有(Tooltip 作为临时浮层时,不再触发普通兄弟控件补画)
- 性能影响:轻微(多维护一组矩形并集和相交判断)
- 回归风险:
- 如果某控件持久 coverage 定义过小,可能漏补兄弟快照。
- Tooltip 会短暂显示在同容器后序普通控件上方,这是本轮有意选择。
## 验证结果
- 验证步骤:
1. 编译 `Control.cpp / Button.cpp / Canvas.cpp / TabControl.cpp / Table.cpp / Window.cpp`
2. 编译 `z-testDome.cpp``KEY1 ~ KEY6`
3. 手测 KEY6 A 区禁用按钮 Tooltip 与状态 Label 交叠。
4. 手测 TabControl 页签 / 页面内 Tooltip。
- 验证结果: 编译通过;GUI 行为待用户手测
- 回归检查:[可选] KEY6 A 区、KEY5 Tooltip/overlay、KEY4 Dialog overlay
- 验证证据:[可选]
## 落地信息
- Commit: 未提交
- PR[可选]
- 发布版本:[可选]
- 备注: 下版本可进一步引入 actual coverage 返回值和正式 coverage 诊断日志。
@@ -1,73 +0,0 @@
# 功能变更 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[可选]
- 发布版本:[可选]
- 备注:[可选]
- 其中一部分修改早于仓库基线
@@ -1,71 +0,0 @@
# 功能变更 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[可选]
- 发布版本:[可选]
- 备注:[可选]
@@ -1,75 +0,0 @@
# 功能变更 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[可选]
- 发布版本:[可选]
- 备注:[可选]
@@ -1,69 +0,0 @@
# 功能变更 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[可选]
- 发布版本:[可选]
- 备注:[可选]
@@ -1,69 +0,0 @@
# 功能变更 ID: Feature-20260409-0005
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260409-0005
- 标题: 开发记录与模板体系整理
- 状态:已完成
- 类型:修改
- 级别:L1 轻量
- 模块:文档模板 / 开发记录
- 版本 / 分支:`master`
- 环境:本地工作区
- 负责人:Codex 协作修改
## 变更背景
- 背景:
- 需要对本轮协作中的大量修改建立可持续维护的记录体系。
- 目标:
- 将“问题、修复、功能变化、模块设计”这四类信息分层记录,避免后续继续靠聊天上下文回忆。
- 不做什么:[可选]
- 不把模板设计成覆盖所有场景的一份大而全文档
## 变更内容
- 变更摘要:
- 重写四个文档模板,修正编码内容,并建立统一的开发记录目录。
- 新增项:[可选]
- `开发记录/BUG`
- `开发记录/Fix`
- `开发记录/功能变更`
- `开发记录/模块`
- 修改项:[可选]
- `文档模板` 目录下四个模板重写并重新划分职责
- 删除 / 废弃项:[可选]
- 原模板中“功能变更”和“模块说明”职责重叠的写法已废弃
- 受影响的文件 / 类 / 函数:
- `文档模板/*.md`
- `开发记录/*.md`
- 对外 API / 属性变化:[可选]
-
## 行为对照
- 变更前:
- 模板存在乱码,功能变更和模块说明边界不清,小改动记录成本过高。
- 变更后:
- 模板职责明确,记录粒度更合理,可以持续沉淀协作历史。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 后续新增记录优先按新模板写,不再沿用旧模板内容
## 验证与落地
- 验证方式:
- 读取模板文件确认编码与内容正常
- 创建第一批记录文件验证模板可用
- 验证结果:
- 模板可正常使用,第一批记录已按新体系落地
- 关联 BUG / Fix[可选]
-
- Commit:
- 当前工作区未提交阶段
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
@@ -1,54 +0,0 @@
# Feature-20260409-0006
> 适用场景:记录小到中等规模的接口、行为、默认值、交互和内部机制变化。
## 基本信息
- ID: `Feature-20260409-0006`
- 标题: 窗口最大化收口保护从跨度阈值改为非法尺寸判断
- 类型:修改
- 级别:L2
- 模块:Window / Resize
- 状态:已完成 / 待用户回归
- 关联:
- `BUG-20260409-0003`
- `Fix-BUG-20260409-0003`
## 背景
- 旧实现把“大跨度尺寸变化”视为异常帧。
- 在初始窗口较小、显示器较大时,正常最大化会被误伤。
## 变更内容
- 删除旧的跨度保护语义:
- 不再以 `abs(finalW - width)``abs(finalH - height)` 决定是否跳过。
- 新增非法尺寸保护语义:
- 仅拦截 `<= 0` 的客户区尺寸
- 仅拦截明显超出虚拟桌面合理范围的客户区尺寸
- 新增两类调试日志:
- 非法尺寸跳过日志
- 大跨度但继续执行日志
## 对外影响
- 直接最大化的行为更稳定。
- 之前会黑背景/残影的场景,现在理论上应恢复为正常收口。
## 兼容性
- 向后兼容。
- 不涉及公开 API 变化。
## 验证方式
- 用户回归 `KEY == 1 ~ 4`
- 日志检查是否继续打印 `尺寸调整已完成`
- 单文件编译验证 `Window.cpp`
## 落地信息
- 涉及文件:
- `Window.cpp`
- 落地状态:
- 当前工作区已完成,待提交
@@ -1,78 +0,0 @@
# 功能变更 ID: Feature-20260415-0008
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260415-0008
- 标题: KEY5 第二阶段专项回归场景增强
- 状态:已完成
- 类型:修改
- 级别:L2 中等
- 模块: 测试用例 / 回归场景
- 版本 / 分支: 当前工作区
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 变更背景
- 背景:
- 旧版 `KEY5` 已能覆盖第一阶段布局主线,但场景层次不够集中,说明文字偏长,部分区域排版紧。
- 第二阶段引入了内容驱动、overlay 补画、容器职责边界等新验证点,旧 `KEY5` 覆盖不完整。
- 目标:
-`KEY5` 重构成第二阶段专项回归入口。
- 增加三层 `Canvas` 嵌套、通用 overlay、跨容器 hover 清理、`Label` 内容驱动规则验证。
- 不做什么:[可选]
- 不追求视觉美化。
- 不把测试用例变成完整 demo 页。
## 变更内容
- 变更摘要:
- 重构 `KEY5` 的区域布局与说明文案。
- 引入三层嵌套 `Canvas` 场景。
- 增加 `Window / Canvas / TabControl / Table` 四类关键回归点。
- 新增项:[可选]
- 三层 `Canvas` 嵌套与多层锚点链回归。
- `Label` 文本变化 / 样式变化按钮。
- 跨容器相邻按钮 hover/tooltip 回归点。
- 顶层浮层覆盖 `Table` 的 overlay 回归点。
- 修改项:[可选]
- 缩短说明 `Label` 文案,减少普通排版重叠。
- 重新组织分区顺序,提升单区可读性。
- 删除 / 废弃项:[可选]
- 删除过长、重复的说明文案。
- 受影响的文件 / 类 / 函数:
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
- 对外 API / 属性变化:[可选]
-
## 行为对照
- 变更前:
- `KEY5` 主要覆盖第一阶段布局规则,层次和交互链回归点相对分散。
- 说明文字较长,部分区域排版偏挤。
- 变更后:
- `KEY5` 成为第二阶段主回归入口。
- 三层嵌套、overlay、跨容器 hover、`Label` 内容驱动、`Table` 分页与页码都能直接观察。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 仍然通过 `#define KEY 5` 进入,无额外迁移。
## 验证与落地
- 验证方式:
- 编译 `z-testDome.cpp /DKEY=5`
- 手动观察窗口 resize、hover、tooltip、overlay、分页和 `Label` 变化
- 验证结果:
- 编译级验证通过
- GUI 手动验证待本机执行
- 关联 BUG / Fix[可选]
- `BUG-20260415-0005`
- `Fix-BUG-20260415-0005`
- Commit: 未提交(当前工作区)
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
- `Label` 样式变化验证明确依赖 `setDirty(true)` 使用约定。
@@ -1,87 +0,0 @@
# 功能变更 ID: Feature-20260416-0009
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260416-0009
- 标题: KEY2 使用新布局 API 重构与联动刷新
- 状态:开发中
- 类型:修改
- 级别:L2 中等
- 模块: 测试用例 / Layout API 迁移样例
- 版本 / 分支: 当前工作区
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 变更背景
- 背景:
- `KEY2` 原有布局主要依赖旧 `setAnchor(...)` 语义,无法清晰表达“固定尺寸 + 比例位移”。
- 32 位位号标签与位按钮分别按各自尺寸参与比例位移,窗口拉伸后会发生错位。
- 位按钮点击后,下面的十六进制、十进制与二进制显示区刷新链散落在多个回调中,维护成本高且容易漏更新。
- 目标:
-`KEY2` 改成新布局 API 的首个完整迁移样例。
- 修复位标签与位按钮 resize 后错位问题。
- 让位按钮、位操作按钮、配置区按钮共用一条统一的显示刷新链。
- 不做什么:[可选]
- 不重构 `KEY1 / KEY5`
- 不引入 `KEY2` 以外的新功能
## 变更内容
- 变更摘要:
- 重构 `KEY2` 位选择区为“每一位一个小 Canvas 单元”。
- 使用新布局 API 重排 `KEY2` 的选择区、功能区、数值显示区、二进制显示区与配置区。
- 抽出统一的 bit 快照、应用、显示刷新链,收口位按钮与功能按钮的联动。
- 新增项:[可选]
- `KEY2` 内部的布局辅助策略函数
- `snapshotBits / applyBits / refreshDisplaysWithBits` 这类统一刷新路径
- 修改项:[可选]
- 32 位位标签与位按钮的组织方式
- 顶层区域的拉伸策略
- 一键置 0 / 一键置 1 / 左移 / 右移 / 位取反 / signed toggle 的显示刷新方式
- 删除 / 废弃项:[可选]
- 废弃旧的“位标签和位按钮分别做比例位移”的选择区组织方式
- 废弃散在多个回调中的局部显示刷新
- 受影响的文件 / 类 / 函数:
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
- 对外 API / 属性变化:[可选]
- 无框架级 API 变化,属于测试用例迁移
## 行为对照
- 变更前:
- 32 位位标签与位按钮在窗口拉伸后会发生错位。
- 位按钮点击后,显示区更新链不统一,后续维护难度高。
- `KEY2` 的多个区域仍依赖旧锚点表达,窗口拉伸时不够稳定。
- 变更后:
- 每一位作为独立单元整体位移,位标签与位按钮不再分离漂移。
- 位按钮、一键置 0/1、位取反、左移、右移、signed toggle 均通过统一刷新链更新显示区。
- 顶层五个区域在窗口拉伸后继续铺满可用空间,内部小控件保持固定尺寸。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 这是新布局 API 的示例迁移,不影响其它 `KEY` 入口。
## 验证与落地
- 验证方式:
- 编译 `z-testDome.cpp /DKEY=2`
- 编译 `z-testDome.cpp /DKEY=5`
- 手动验证 `KEY2`
- 窗口横向拉伸
- 位按钮点击
- 位取反 / 左移 / 右移
- 一键置 0 / 一键置 1
- 有符号 / 无符号切换
- 验证结果:
- 编译级验证通过。
- GUI 手动验证待本机继续确认。
- 关联 BUG / Fix[可选]
- 无单独 BUG 记录,本次属于布局 API 落地后的示例迁移
- Commit:
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
- 当前功能区采用左/中/右固定宽度面板布局;窗口变宽时可稳定铺满,若后续需要进一步适配极窄窗口,再考虑把功能区做成等比分栏。
@@ -1,156 +0,0 @@
# 功能变更 ID: Feature-20260418-0010
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260418-0010
- 标题: 当前阶段测试用例矩阵与定位
- 状态:已完成
- 类型:修改
- 级别:L1 轻量
- 模块: 测试用例 / 回归策略
- 版本 / 分支: 当前工作区
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 变更背景
- 背景:
- 第二阶段主线、布局策略公开 API、`KEY2` 迁移以及近期重绘 / tooltip / overlay / `TabControl` / `Table` 修复已基本收口。
- 当前项目中同时存在 `KEY1 ~ KEY5` 多组测试入口,但各自负责覆盖什么、哪些是主集、哪些是专项补充,若不显式记录,后续容易继续依赖聊天上下文记忆。
- 目标:
- 把当前阶段各测试入口的职责、覆盖范围和预期行为正式写成测试矩阵。
- 明确本阶段建议主回归集与补充专项。
- 不做什么:[可选]
- 不新增测试逻辑。
- 不修改运行时代码。
- 不把测试矩阵扩成自动化测试体系。
## 变更内容
- 变更摘要:
- 新增当前阶段测试矩阵记录。
- 同步 `KEY1 ~ KEY5` 的阶段定位与行为预期。
- 新增项:[可选]
- 当前阶段建议主回归集:`KEY1 + KEY2 + KEY5`
- Dialog / MessageBox 补充专项:`KEY4`
- 修改项:[可选]
- 无代码修改,仅补充测试约定与阶段定位。
- 删除 / 废弃项:[可选]
-
- 受影响的文件 / 类 / 函数:
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
- 对外 API / 属性变化:[可选]
-
## 行为对照
- 变更前:
- `KEY1 ~ KEY5` 的职责划分主要依赖源码注释和聊天上下文。
- 很难快速判断“当前阶段主回归集”与“专项补充集”分别是什么。
- 变更后:
- 每个 `KEY` 入口的阶段定位、覆盖重点和行为预期都有正式记录。
- 可以按矩阵组织回归,而不再只靠记忆。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 无需迁移,属于当前阶段测试约定落地。
## 验证与落地
- 验证方式:
- 对照当前 [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp) 中 `KEY1 ~ KEY5` 的实际结构与近期修复范围进行人工核对。
- 验证结果:
- 当前矩阵与现有测试入口和阶段主线一致。
- 关联 BUG / Fix[可选]
- [Module-20260415-0004-布局系统第二阶段验收与封口.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260415-0004-布局系统第二阶段验收与封口.md)
- [Module-20260416-0005-布局策略公开API落地.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260416-0005-布局策略公开API落地.md)
- Commit:
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
- resize 过程中若开启高频 console 日志,可能出现一帧视觉延迟;当前判断属于调试态 I/O 现象,不按 bug 处理。
## 当前阶段测试矩阵
### KEY1:旧页签链路 + Table 超界残留回归
- 覆盖目标:
- `TabControl` 重复激活同一页签
- 切页 / 关页后的快照与清理链
- 页内容超出页范围时的残影与恢复
- 当前预期:
- 外部重复激活当前页签,不再扰动当前页快照链。
- 页签 1 中 `Table` 超出页区域的部分,在切页或关闭页签时不会残留。
- 正常全量重绘后,不应再出现额外残影或层级错乱。
- 阶段定位:
- 当前主回归集之一。
### KEY2:公开布局 API 首个迁移样例
- 覆盖目标:
- 新公开布局 API 的真实使用
- 位选择区容器化重构
- 显示区统一刷新链
- 顶层区块拉伸铺满
- 当前预期:
- 32 位选择区中的“位号 + 位按钮”以单元整体位移,窗口拉伸后保持对齐。
- 位按钮点击、位取反、左移、右移、一键置 0/1、签名切换,会统一刷新十六进制、十进制、上次值、本次值和 `initData`
- 顶层五个区块在正常拉伸 / 最大化后继续铺满窗口。
- 当前更偏重横向铺满验证;极窄窗口下功能区进一步自适应,不作为本阶段硬指标。
- 阶段定位:
- 当前主回归集之一。
- 同时是新布局 API 的首个迁移示例。
### KEY3:旧业务大场景保留用例
- 覆盖目标:
- 保留老业务页示例
- 观察新主线是否把旧业务场景直接打坏
- 当前预期:
- 登录页、`TabControl`、旧业务页基本行为保持可运行。
- 不要求覆盖新布局 API、overlay、tooltip 或第二阶段专项行为。
- 阶段定位:
- 保留用例,不纳入当前主回归集。
### KEY4Dialog / MessageBox 专项回归
- 覆盖目标:
- 模态 / 非模态 `Dialog`
- 遮挡交互
- 关闭后 hover 恢复
- 拖拽 resize
- 当前预期:
- 非模态 `Dialog` 遮挡底层按钮时,不应出现 hover / tooltip 穿透或残留。
- 模态 `Dialog` 打开后拖拽窗口,标题、关闭按钮和底层恢复链保持稳定。
- 对话框关闭后,底层按钮 hover 能及时恢复。
- 阶段定位:
- 当前补充专项,不纳入每轮主回归集。
### KEY5:第二阶段专项主回归
- 覆盖目标:
- 三层 `Canvas` 嵌套
- 跨容器 hover / tooltip
- overlay 补画与 coverage 链
- `TabControl`
- `Table`
- 页码与分页按钮
- 当前预期:
- 三层 `Canvas` 嵌套下,深层按钮 hover / press / release / tooltip 都能正确刷新。
- `Window / Canvas / TabControl` 的 overlay 补画与 coverage 链闭合,不再出现遮挡错层。
- `TabControl` 页签按钮与页面层级正确;页签 tooltip 不会再被页面盖掉。
- `Table` 分页按钮、页码标签、与上层浮层相交时的重绘链保持正确。
- 阶段定位:
- 当前主回归集之一。
- 第二阶段及重绘主线的核心专项入口。
## 当前建议
- 当前阶段建议主回归集:
- `KEY1 + KEY2 + KEY5`
- 当前补充专项:
- `KEY4`
- 暂不纳入每轮主集:
- `KEY3`
@@ -1,295 +0,0 @@
# 功能变更 ID: Feature-20260509-0011
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260509-0011
- 标题: 布局 API 使用说明与旧接口覆盖规则
- 状态:已验证
- 类型:新增
- 级别:L2 中等
- 模块: Layout API / Control
- 版本 / 分支: 当前工作区
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 变更背景
- 背景:
- 当前布局系统内部已经以 `LayoutSpec` 为统一解算依据。
- 旧接口 `setLayoutMode(...)` / `setAnchor(...)` 仍然保留,但只能表达有限的双锚点语义。
- 新增公开布局 API 后,需要明确用户应如何设置轴向锚点、尺寸策略和固定尺寸位移策略。
- 目标:
- 说明新布局 API 的职责和使用方式。
- 明确新旧接口混用时的覆盖规则。
- 给出常见布局组合和 `KEY2` 迁移场景中的推荐写法。
- 不做什么:[可选]
- 不开放 `LayoutCapability` 的普通外部写入接口。
- 不修改 `Table` 当前版本 `Y Fixed` 的能力边界。
- 不处理 Tooltip 智能选位、`Table` 内部局部重绘、`Dialog` synthetic move 统一等后置项。
## 变更内容
- 变更摘要:
- 增加布局 API 使用说明文档。
- 明确新接口与旧接口的覆盖关系。
- 汇总常见布局模式的代码写法。
- 新增项:[可选]
- 新公开接口说明
- 旧接口映射说明
- 混用覆盖规则
- 常见写法示例
- 调试态日志说明
- 修改项:[可选]
- 无运行时代码修改,本文件只记录使用说明。
- 删除 / 废弃项:[可选]
-
- 受影响的文件 / 类 / 函数:
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
- 对外 API / 属性变化:[可选]
- `setHorizontalLayoutSpec(...)`
- `setVerticalLayoutSpec(...)`
- `setHorizontalAnchors(...)`
- `setVerticalAnchors(...)`
- `setHorizontalSizePolicy(...)`
- `setVerticalSizePolicy(...)`
- `setHorizontalAlignPolicy(...)`
- `setVerticalAlignPolicy(...)`
- `getHorizontalLayoutSpec()`
- `getVerticalLayoutSpec()`
## 行为对照
- 变更前:
- 用户主要通过 `setLayoutMode(...)``setAnchor(...)` 设置布局。
- 固定尺寸 + 居中、固定尺寸 + 比例位移等组合无法通过旧接口清晰表达。
- 旧双锚点在某些测试场景中会被迫承担 Stretch 语义,导致固定按钮和内容驱动标签在 resize 后错位。
- 变更后:
- 用户可按水平轴 / 垂直轴分别设置锚点集合、尺寸策略和固定尺寸位移策略。
- 旧接口仍可用,但只作为兼容输入层。
- 新旧接口混用时,最后调用的接口覆盖对应轴的布局规格。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 新代码建议优先使用新布局 API。
- 旧代码可以继续使用 `setAnchor(...)`,但复杂布局应逐步迁移到新接口。
## 验证与落地
- 验证方式:
- 编译 `Control.cpp`
- 编译 `z-testDome.cpp /DKEY=2`
- 编译 `z-testDome.cpp /DKEY=5`
- 手测 `KEY1 / KEY2 / KEY5`
- 验证结果:
- 编译级验证通过。
- 用户手测当前暂未发现问题。
- 关联 BUG / Fix[可选]
- [Feature-20260416-0009-KEY2-使用新布局API重构与联动刷新.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/功能变更/Feature-20260416-0009-KEY2-使用新布局API重构与联动刷新.md)
- [Feature-20260418-0010-当前阶段测试用例矩阵与定位.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/功能变更/Feature-20260418-0010-当前阶段测试用例矩阵与定位.md)
- Commit:
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
- resize 收口阶段若开启高频 console 日志,可能出现一帧视觉延迟;当前按调试态 I/O 现象记录,不作为 bug 处理。
## API 总览
### 完整轴向规格接口
```cpp
void setHorizontalLayoutSpec(const StellarX::AxisLayoutSpec& spec);
void setVerticalLayoutSpec(const StellarX::AxisLayoutSpec& spec);
```
- 直接覆盖对应轴的完整布局规格。
- 调用后自动切换到 `StellarX::LayoutMode::AnchorToEdges`
- 适合调用方已经准备好完整 `AxisLayoutSpec` 的场景。
### 锚点集合接口
```cpp
void setHorizontalAnchors(bool left, bool right);
void setVerticalAnchors(bool top, bool bottom);
```
- 水平轴对应 `left / right`
- 垂直轴对应 `top / bottom`
- 只修改对应轴的锚点集合,不主动改变尺寸策略或位移策略。
### 尺寸策略接口
```cpp
void setHorizontalSizePolicy(StellarX::AxisSizePolicy policy);
void setVerticalSizePolicy(StellarX::AxisSizePolicy policy);
```
- `Stretch`:允许该轴尺寸随父容器变化。
- `FixedSize`:该轴尺寸保持设计态尺寸。
- 控件能力边界仍然生效。例如 `Table` 当前版本不允许 `Y Stretch`,即使用户设置垂直 `Stretch`,也会被能力边界降级。
### 固定尺寸位移策略接口
```cpp
void setHorizontalAlignPolicy(StellarX::AxisAlignPolicy policy);
void setVerticalAlignPolicy(StellarX::AxisAlignPolicy policy);
```
- 只在 `FixedSize` 语义下决定位置。
- 不负责改变尺寸。
- 常用值:
- `Start`:保持起边关系,例如左固定、上固定。
- `End`:保持终边关系,例如右固定、下固定。
- `Center`:保持居中关系。
- `Proportional`:保持设计态相对位置比例,适合固定尺寸控件在可变宽容器中按比例移动。
### 读取接口
```cpp
StellarX::AxisLayoutSpec getHorizontalLayoutSpec() const;
StellarX::AxisLayoutSpec getVerticalLayoutSpec() const;
```
- 返回当前生效的新模型轴向规格。
- 不保证能完整逆映射回旧 `anchor_1 / anchor_2` 语义。
## 新旧接口覆盖规则
### 旧接口仍然保留
```cpp
void setLayoutMode(StellarX::LayoutMode layoutMode);
void setAnchor(StellarX::Anchor anchor1, StellarX::Anchor anchor2);
```
- 旧接口是兼容入口。
- `setAnchor(...)` 内部会把旧双锚点映射到新的 `layoutSpec.horizontal``layoutSpec.vertical`
- 旧接口无法表达所有新模型状态,例如固定尺寸 + 比例位移、固定尺寸 + 居中等。
### 后调用者生效
规则:
- 先调用 `setAnchor(...)`,再调用新 API,则新 API 覆盖对应轴规格。
- 先调用新 API,再调用 `setAnchor(...)`,则旧接口重新覆盖水平轴和垂直轴规格。
- 新 API 只覆盖自己负责的轴或字段。例如 `setHorizontalSizePolicy(...)` 只修改水平轴尺寸策略,不修改垂直轴。
示例:
```cpp
button->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
button->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
// 覆盖水平轴:改为固定尺寸 + 居中,不再沿用旧接口映射出的 Stretch。
button->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
button->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Center);
```
### 新 API 自动切换布局模式
所有新布局 setter 都会自动将 `layoutMode` 切换为 `AnchorToEdges`
因此新代码通常不需要再额外写:
```cpp
control->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
```
## 常见布局写法
### 左固定
```cpp
control->setHorizontalAnchors(true, false);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Start);
```
### 右固定
```cpp
control->setHorizontalAnchors(false, true);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::End);
```
### 水平拉伸
```cpp
control->setHorizontalAnchors(true, true);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::Stretch);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Start);
```
### 固定尺寸居中
```cpp
control->setHorizontalAnchors(true, true);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Center);
```
### 固定尺寸比例位移
```cpp
control->setHorizontalAnchors(true, true);
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Proportional);
```
适用场景:
- 一组固定尺寸按钮需要随着父容器变宽按比例展开。
- `KEY2` 中每个 bit 单元就是该策略的典型使用方式。
## KEY2 迁移要点
`KEY2` 原问题不是单纯参数错误,而是布局单元拆分不合理:
- 位号 `Label` 是内容驱动尺寸。
- 位按钮是固定尺寸。
- 二者分别使用比例位移时,因为自身尺寸不同,resize 后会出现错位。
当前迁移方式:
- 每一位创建一个小 `Canvas` 单元。
- 单元内部固定放置位号 `Label` 与位按钮。
- 单元自身使用 `FixedSize + Proportional`
- 这样移动的是整个 bit 单元,而不是标签和按钮分别移动。
示意写法:
```cpp
auto cell = std::make_unique<Canvas>(cellX, 0, 32, 50);
cell->setHorizontalAnchors(true, true);
cell->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
cell->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Proportional);
```
## 能力边界说明
布局 API 表达的是“调用方希望如何布局”,但控件能力边界仍然优先。
当前关键边界:
- `Canvas``X / Y` 均允许 Stretch。
- `TabControl`:外层允许 Stretch,内部页签栏 / 页面区仍由自身管理。
- `Table`:当前版本 `X Stretch / Y Fixed`,这是当前实现边界,不是永久产品结论。
- `TextBox`:默认更适合 `X Stretch / Y FixedSize`
- `Label`:默认固定或内容驱动刷新运行态尺寸,不建议做双轴 Stretch。
- `Dialog`:只居中,不参与父级拉伸。
如果用户设置的策略超出控件能力边界,统一解算层会按现有降级规则处理,并保留必要日志。
## 调试态说明
resize / 最大化 / 还原过程中,如果开启大量 console 日志,可能出现一帧视觉延迟或短暂残影。
当前判断原因是日志系统同步写控制台且默认逐行 flush,导致主线程 resize 收口绘制被 I/O 拖慢。
处理建议:
- 常规手测可关闭 console 日志。
- 需要日志时可优先输出到文件。
- 当前不作为布局或重绘 bug 处理,后续可在官网/API 文档中作为调试说明记录。
@@ -1,105 +0,0 @@
# 功能变更 ID: Feature-20260511-0012
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
## 基本信息
- ID: Feature-20260511-0012
- 标题: 发布前托管重绘与 KEY6 封版收口
- 状态:已完成
- 类型:修改
- 级别:L3 重大
- 模块: Window / Canvas / TabControl / Tooltip / Dialog / 测试用例
- 版本 / 分支: 当前工作区
- 环境: Windows / EasyX
- 负责人: Codex
## 变更背景
- 背景:
- 发布前封版阶段需要稳定现有布局、托管重绘、Dialog / overlay / hover 状态同步链路。
- KEY6 补充覆盖后暴露了非模态 Dialog、Tooltip、跨 root dirty、透明 Label 快照等组合问题。
- 目标:
- 修复确定性小问题。
- 收口 Dialog / overlay 后鼠标状态同步语义。
- 让 KEY6 覆盖已实现但原主回归未覆盖的分支。
- 修复托管局部重绘中 coverage 和背景快照语义混用导致的问题。
- 不做什么:[可选]
- 不实现焦点系统。
- 不实现键盘事件系统。
- 不做 Tooltip 智能选位。
- 不做 Table 纵向 Stretch。
- 不实现 Table 内部局部重绘体系。
## 变更内容
- 变更摘要:
- 修复 `TabControl::indexOf()` 未命中返回值。
- 抽取 Dialog / overlay 后鼠标状态同步辅助函数。
- 新增 KEY6 分支覆盖用例。
- `Window` 增加局部 root 提交后 overlay coverage 保守兜底。
- `Window` 事件尾补收集跨 root dirty 子树。
- 托管重绘拆分完整 coverage 与持久 coverage,避免 Tooltip 污染背景快照。
- 新增项:[可选]
- `Window::syncMouseStateAfterOverlayChanged(...)`
- `Window::collectDirtyRootsForManagedRepaint()`
- `Control::getManagedRepaintPersistentCoverageRect()`
- KEY6 测试入口
- 修改项:[可选]
- `Canvas / TabControl` 局部 overlay 补画策略。
- `Button` Tooltip coverage 语义。
- `Table` 托管 coverage 语义补充。
- 删除 / 废弃项:[可选]
-
- 受影响的文件 / 类 / 函数:
- `Window.h / Window.cpp`
- `Control.h / Control.cpp`
- `Button.h / Button.cpp`
- `Canvas.h / Canvas.cpp`
- `TabControl.h / TabControl.cpp`
- `Table.h / Table.cpp`
- `z-testDome.cpp`
- 对外 API / 属性变化:[可选]
- 无公开用户 API 变化。
- 新增接口均为内部托管重绘语义。
## 行为对照
- 变更前:
- `indexOf()` 未命中返回最后索引。
- Dialog 关闭 / 非模态 Dialog 吞鼠标移动后的 synthetic move 逻辑散落在 `runEventLoop()`
- 局部 root 内部 coverage 扩张后,Window 可能漏补 Dialog。
- 跨 root 回调改脏后,目标 root 可能等下一次事件才刷新。
- Tooltip 临时浮层与持久 coverage 混用,可能污染或擦除兄弟控件快照。
- 变更后:
- `indexOf()` 未命中返回 `-1`
- overlay 后鼠标状态同步由语义函数集中处理。
- Window 对可局部提交 root 的上层补画判断更保守。
- 事件尾会补收集未登记但已 dirty 的 root。
- Tooltip 进入完整 coverage,但不进入持久 coverage;只命中临时浮层的兄弟控件不再补画。
- 兼容性说明:兼容
- 迁移说明:[可选]
- 用户代码无需迁移。
## 验证与落地
- 验证方式:
- 编译核心变更文件。
- 编译 `z-testDome.cpp``KEY1 ~ KEY6`
- 根据用户手测反馈修正 KEY6 Tooltip / Dialog / Label 组合问题。
- 验证结果:
- `Control.cpp / Button.cpp / Canvas.cpp / TabControl.cpp / Table.cpp / Window.cpp` 编译通过。
- `KEY1 ~ KEY6` 编译通过。
- GUI 行为需继续由用户在本机手测确认。
- 关联 BUG / Fix[可选]
- BUG-20260511-0009 / Fix-BUG-20260511-0009
- BUG-20260511-0010 / Fix-BUG-20260511-0010
- BUG-20260511-0011 / Fix-BUG-20260511-0011
- BUG-20260511-0012 / Fix-BUG-20260511-0012
- Commit: 未提交
- PR[可选]
- 发布版本:[可选]
- 备注:[可选]
- 下版本建议引入 `commitManagedRepaint()` actual coverage 返回值,替代当前 root coverage 保守兜底。
- 下版本建议建立正式 coverage 诊断日志开关,避免再临时散加日志。
@@ -1,134 +0,0 @@
# 新增功能模块 / 模块重构
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
## 基本信息
- 模块 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`
@@ -1,161 +0,0 @@
# 新增功能模块 / 模块重构
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
## 基本信息
- 模块 ID`Module-20260410-0002`
- 模块名称: 锚点与布局系统第一阶段重构
- 状态:已验证
- 类型:模块重构
- 所属系统 / 子系统: GUI 框架 / Layout
- 版本 / 分支: 当前工作区 / 下一版本开发中
- 环境: Windows + EasyX
- 负责人: Codex 协作修改
## 背景与目标
- 背景:
- 原有锚点系统仍依赖 `anchor_1 / anchor_2`
- `Window::adaptiveLayout()``Canvas::onWindowResize()` 长期并存两套布局语义
- `Canvas` 布局层存在 `Table` 外部特判
- `TabControl` 外层布局与内部布局耦合较深
- 当前痛点:
- 双锚点表达能力不足,难以覆盖更完整的边集合语义
- 顶层窗口与容器子控件的解算规则不统一,维护成本高
- 特殊控件能力边界没有收回自身语义层
- 缺少针对布局系统的专项回归用例
- 目标:
- 建立统一的布局数据模型与统一解算入口
- 正式区分设计态矩形与运行态矩形
- 保留旧 API 的兼容输入能力
-`Table` 的当前能力边界收回控件自身
- 增加布局专项回归用例 `KEY == 5`
- 非目标:
- 不做字体随控件缩放
- 不做 `Table` 纵向拉伸
- 不重构重绘系统
## 模块边界
- 职责:
- 提供统一的布局规格描述
- 统一顶层窗口与容器子控件的几何解算
- 在保持旧 API 可用的前提下,将内部布局实现迁移到新模型
- 通过控件能力边界约束非法或暂不支持的拉伸组合
- 不负责什么:
- 字体缩放与内容排版自适应
- `Dialog` 内部布局语义重构
- `Table` 纵向拉伸能力
- 外部依赖:
- EasyX 绘制环境
- 现有 `Control / Window / Canvas / TabControl / Table` 控件体系
- 对外能力 / API:
- 继续保留 `setLayoutMode(...)`
- 继续保留 `setAnchor(a1, a2)`
- 当前阶段新增能力主要用于内部统一实现,不额外新增用户层 API
- 关键数据 / 状态:
- `localx / localy / localWidth / localHeight`
- `x / y / width / height`
- `LayoutSpec`
- `LayoutCapability`
- `ResolvedLayoutRect`
## 设计说明
- 核心流程:
- 先在父局部坐标系内按水平轴 / 垂直轴独立解算
- 再将局部矩形映射为世界坐标矩形
- 由控件内部受控路径应用运行态矩形
- 关键对象 / 类关系:
- [`Control`](D:/programming/imGUI-easyX/imGui-easyX/Control.h) 作为统一布局规格与基础解算入口
- [`Window`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 负责顶层控件统一收口
- [`Canvas`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp) 负责容器子控件重映射
- [`TabControl`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp) 外层接入统一解算,内部页签栏 / 页面区仍自管
- [`Table`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp) 通过 `LayoutCapability` 显式禁止 `Y` 轴 Stretch
- 生命周期:
- 设计态矩形 `local*` 在普通 resize / 重排过程中不自动回写
- 运行态矩形由统一解算器产出,再通过内部路径应用
- 若确需同步设计基线,只能显式调用 `commitCurrentGeometryAsDesignRect()`
- 事件 / 渲染 / 数据流:
- 事件阶段只改状态,不直接扩散成多套布局公式
- `Window``Canvas` 共用 `resolveLayoutRect()`
- `onWindowResize()` 收口为“快照失效 + 标脏 + 必要传播”,不再承担布局求解
- 关键不变量:
- `local*` 始终表示设计态父局部矩形
- `x / y / width / height` 始终表示运行态绘制矩形
- 旧 API 只作兼容输入层,不再作为内部解算依据
- `Table` 当前阶段只允许 `X` 轴 Stretch
- 降级 / 回退策略:
- 对不满足能力边界的拉伸请求,自然降级为固定尺寸位移
- 旧接口输入通过映射层退回到新模型的有限子集
## 实现与影响
- 关键实现点:
- 引入 `AxisSizePolicy / AxisAlignPolicy / AxisLayoutSpec / LayoutSpec / LayoutCapability / ResolvedLayoutRect`
-`Control` 中增加统一解算与内部受控应用路径
-`Window::adaptiveLayout()` 改为统一解算入口
-`Canvas` 子控件布局从旧比例缩放逻辑切换为统一解算
-`TabControl` 外层接入统一解算,同时保留内部页签系统专用布局
-`Table``Y` 轴固定能力边界收回控件自身
-`z-testDome.cpp` 增加 `KEY == 5` 布局专项回归
- 涉及文件 / 类 / 函数:
- [`CoreTypes.h`](D:/programming/imGUI-easyX/imGui-easyX/CoreTypes.h)
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
- [`Canvas.h`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.h)
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
- [`TabControl.h`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.h)
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
- [`Table.h`](D:/programming/imGUI-easyX/imGui-easyX/Table.h)
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp)
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
- 兼容性影响:
- 向后兼容旧锚点 API
- 当前阶段未删除旧 getter / setter
- 性能影响:
- 无明显新增性能负担
- 主要是布局求解路径从多处分散逻辑收口为统一函数
- 风险点:
- 若某些控件运行态尺寸变化后未明确同步设计基线,后续 resize 仍可能出现“回到旧设计态”的现象
- `TabControl` 的外层统一解算与内部专用布局之间存在边界风险
- `Table` 纵向仍为固定尺寸,后续若扩展能力需单独立项
## 测试与验证
- 测试范围:
- 顶层窗口 resize
- `Canvas` 嵌套布局
- `TabControl` 外层布局接入
- `Table` 横向拉伸与纵向固定
- `KEY == 5` 布局专项回归
- 验证步骤:
1. 编译 `Control.cpp / Canvas.cpp / Table.cpp / TabControl.cpp / Window.cpp`
2. 编译 `z-testDome.cpp /DKEY=2`
3. 编译 `z-testDome.cpp /DKEY=5`
- 验证结果:
- 源码级编译验证通过
- `KEY == 5` 已补齐布局专项回归用例
- GUI 交互仍需用户本机手动确认
- 已知限制 / 遗留问题:
- 本轮不包含字体缩放
- 本轮不包含 `Table` 纵向拉伸
- Tooltip 问题已另外拆分为独立 `BUG / Fix`
## 落地信息
- 关联功能变更 ID[可选]
- 关联 BUG / Fix:
- `BUG-20260410-0004`
- `Fix-BUG-20260410-0004`
- Commit: 当前工作区未提交
- PR[可选]
- 发布版本:[可选]
- 相关文档:
- [`BUG-20260410-0004-按钮Tooltip移出后不消失.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/BUG/BUG-20260410-0004-按钮Tooltip移出后不消失.md)
- [`Fix-BUG-20260410-0004-按钮Tooltip移出后不消失.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/Fix/Fix-BUG-20260410-0004-按钮Tooltip移出后不消失.md)

Some files were not shown because too many files have changed in this diff Show More