Compare commits
4 Commits
v2.3.2
...
10a91f9a64
| Author | SHA1 | Date | |
|---|---|---|---|
| 10a91f9a64 | |||
| eb96e5a64e | |||
| 5cb59b3652 | |||
| 0c1cf2938f |
@@ -7,9 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
[中文文档](CHANGELOG.md)
|
[中文文档](CHANGELOG.md)
|
||||||
|
|
||||||
|
## [v3.0.0] - 2026-01-09
|
||||||
|
|
||||||
|
### ✨ New Features
|
||||||
|
|
||||||
|
- **Logging System Demo**: Located in: examples\SXLog-LoggingSystemDemo
|
||||||
|
- **Lightweight Logging System SxLog (SxLogger / SxLogLine / SxLogScope / Sink / TagFilter / LanguageSwitch):** A unified logging entry and macro encapsulation is introduced, supporting level and tag-based filtering, console/file output, and optional file rolling based on size thresholds. It also provides bilingual text selection capabilities (via `SX_T` / `SxT`). The logging macros have a short-circuit mechanism: logs will not be constructed or string concatenated if the level or tag condition is not met. Output is serialized with mutex to ensure thread safety. The module does not depend on WinAPI debug output channels and does not introduce third-party libraries.
|
||||||
|
- Typical usage: `SX_LOGD/SX_LOGI/SX_LOGW/SX_LOGE/SX_LOGF/SX_LOG_TRACE` with stream concatenation; `SX_TRACE_SCOPE` for scope timing.
|
||||||
|
- Core configuration: `setMinLevel(...)`, `setTagFilter(...)`, `enableConsole(true/false)`, `enableFile(path, append, rotateBytes)`, `setLanguage(ZhCN/EnUS)`, `setGBK()`
|
||||||
|
|
||||||
|
### ⚙️ Changes
|
||||||
|
|
||||||
|
- **TabControl Tab Switching Logic Adjustment:** The logic for tab button switching has been modified to “first close the currently opened tab, then open the triggered tab,” avoiding potential timing issues caused by the “open first, close later” sequence in complex containers/snapshot chains. The external API remains unchanged (involving the internal switching logic of `TabControl::add`).
|
||||||
|
- **Setter Semantics Refined for Controls:** The responsibility of setters is clarified to be “updating state and marking as dirty,” with drawing now uniformly triggered by the window/container's redraw mechanism, reducing lifecycle coupling and snapshot pollution risks (see related fix entries below).
|
||||||
|
|
||||||
|
### ✅ Fixes
|
||||||
|
|
||||||
|
- **TextBox::setText Causes Interruptions Before Entering Event Loop:** Fixed the issue where calling `TextBox::setText()` before window initialization (before `initgraph()` or when the graphical context is not ready) caused access conflicts and crashes. The previous implementation coupled “state updates” with “immediate drawing,” leading to `setText()` internally triggering `draw()`, which crashed when there was no graphics context (e.g., `saveBackground()/getimage()`). Now, the setter only updates the text and marks the control as dirty, with the drawing handled by the unified redraw flow.
|
||||||
|
- **TabControl Switching and Table Content Overlap (Ghosting):** Fixed a stable ghosting issue when switching tabs or resetting the table data. The issue was caused by partial redraws happening before the snapshot was ready, leading to snapshot contamination. This was addressed by rolling back the behavior of `setIsVisible(true)` immediately triggering `requestRepaint`, and instead marking as dirty while adding safeguards to `Canvas::requestRepaint` and `TabControl::requestRepaint`: when the container is `dirty`, `hasSnap=0`, or the snapshot cache is invalid, partial fast-path is disabled and full redraw is triggered to ensure correct snapshot chains.
|
||||||
|
|
||||||
|
### ⚠️ Breaking Changes
|
||||||
|
|
||||||
|
- **Removal of Button Dimension Alias APIs:** Removed `Button::getButtonWidth()` and `Button::getButtonHeight()`, unifying the control's dimension APIs under the base class `Control::getWidth()` and `Control::getHeight()`. This change will break backward compatibility if the old methods were used, but the behavior (retrieving `width/height`) remains the same.
|
||||||
|
- **Removal of Immediate Refresh Side Effects in Setters:** The side effect of immediate drawing in setters like `setIsVisible(true)` and `setText()` has been removed. If previous business code relied on "immediate visibility update after calling," you will now need to ensure a subsequent redraw path (via window's main loop/container redraw or explicit refresh) for visual updates to be completed.
|
||||||
|
|
||||||
|
### 📌 Upgrade Instructions
|
||||||
|
|
||||||
|
1. **Button Dimension API Migration:**
|
||||||
|
- `getButtonWidth()` → `getWidth()`
|
||||||
|
- `getButtonHeight()` → `getHeight()`
|
||||||
|
2. **Adapting to Setters No Longer Triggering Immediate Drawing:**
|
||||||
|
- It is now possible to set properties (like `TextBox::setText("default")`) during initialization, with visual updates being handled by the first `Window::draw()` or the main event loop.
|
||||||
|
- If you need immediate visual updates in non-event-driven scenarios, you must ensure that a redraw path is triggered (avoid manually calling `draw()` inside the setter, as this would cause lifecycle coupling).
|
||||||
|
3. **SxLog Integration Suggestions:**
|
||||||
|
- Ensure basic configuration at the program entry (console output/minimum level/language), and use consistent tags (such as `Dirty/Resize/Table/Canvas/TabControl`) to establish traceable event chains; for high-frequency paths, control noise and I/O costs using level and tag filtering.
|
||||||
|
|
||||||
|
## [v2.3.2] - 2025 - 12 - 20
|
||||||
|
|
||||||
|
### ✨ Added
|
||||||
|
|
||||||
|
- **Table: runtime reset for headers and data:** added `Table::clearHeaders()`, `Table::clearData()`, and `Table::resetTable()`. This allows a single `Table` instance to dynamically update its headers/data at runtime, and triggers the required recalculation (cell sizing / pagination info) and redraw.
|
||||||
|
- **TextBox: password mode:** added `PASSWORD_MODE` to `TextBoxmode`. User input is stored internally, while the render layer displays masked characters (e.g., `*`). The real text can be retrieved via `TextBox::getText()`.
|
||||||
|
|
||||||
|
### ⚙️ Changed
|
||||||
|
|
||||||
|
- **TabControl: clarified default active page semantics:**
|
||||||
|
- Calling `TabControl::setActiveIndex()` **before the first draw** now only records the default active index; it no longer immediately triggers the tab button click callback.
|
||||||
|
- **After the first draw completes**, if a default active index was set, the active state is applied and the active page is drawn (if the index is out of range, the last page is activated by default).
|
||||||
|
- Calling `TabControl::setActiveIndex()` **during runtime (non-first draw)** switches the active page immediately when the index is valid; out-of-range indices are ignored.
|
||||||
|
|
||||||
|
### ✅ Fixed
|
||||||
|
|
||||||
|
- **TabControl::setActiveIndex crash when called before drawing:** fixed a null-pointer access caused by triggering the tab button click callback before initialization. The default activation is now applied after the first draw completes, preventing crashes and ensuring the active page is rendered on first draw.
|
||||||
|
- **TabControl rendering glitches when toggling visibility (hidden -> visible):** fixed multi-page overlap/ghosting caused by non-active pages being incorrectly drawn after `setIsVisible(false) -> setIsVisible(true)`. Now, when TabControl is visible, only the active page is visible/drawable; if there is no active page, nothing is drawn.
|
||||||
|
|
||||||
## [v2.3.1] - 2025-11-30
|
## [v2.3.1] - 2025-11-30
|
||||||
|
|
||||||
## 🙏 Acknowledgements
|
### 🙏 Acknowledgements
|
||||||
|
|
||||||
- Special thanks to user [@To-KongBai](https://github.com/To-KongBai) for providing stable reproduction steps and key phenomenon comparisons (container nested child control coordinate transformation issue), which helped us quickly identify and fix the control coordinate transformation problem in deeply nested containers. ([Issues#6](https://github.com/Ysm-04/StellarX/issues/6))
|
- Special thanks to user [@To-KongBai](https://github.com/To-KongBai) for providing stable reproduction steps and key phenomenon comparisons (container nested child control coordinate transformation issue), which helped us quickly identify and fix the control coordinate transformation problem in deeply nested containers. ([Issues#6](https://github.com/Ysm-04/StellarX/issues/6))
|
||||||
- In the upcoming website (currently undergoing ICP filing), we plan to add a contributors' wall to acknowledge users. We welcome everyone to report bugs or share interfaces created with StellarX, and we will carefully read and include acknowledgements.
|
- In the upcoming website (currently undergoing ICP filing), we plan to add a contributors' wall to acknowledge users. We welcome everyone to report bugs or share interfaces created with StellarX, and we will carefully read and include acknowledgements.
|
||||||
|
|||||||
59
CHANGELOG.md
59
CHANGELOG.md
@@ -3,13 +3,68 @@
|
|||||||
StellarX 项目所有显著的变化都将被记录在这个文件中。
|
StellarX 项目所有显著的变化都将被记录在这个文件中。
|
||||||
|
|
||||||
格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)
|
||||||
|
|
||||||
[English document](CHANGELOG.en.md)
|
[English document](CHANGELOG.en.md)
|
||||||
|
|
||||||
|
## [v3.0.0] - 2026 - 01 - 09
|
||||||
|
|
||||||
|
### ✨ 新增
|
||||||
|
|
||||||
|
- **日志系统使用Demo** 在:examples\SXLog-日志系统使用demo
|
||||||
|
|
||||||
|
- **轻量日志系统 SxLog(SxLogger / SxLogLine / SxLogScope / Sink / TagFilter / LanguageSwitch):**新增统一日志入口与宏封装,支持按级别与 Tag 筛选输出,支持控制台/文件落地与可选滚动(按大小阈值),并提供中英文双语文本选择能力(`SX_T` / `SxT`)。日志宏具备短路机制:未命中级别或 Tag 时不构造日志对象、不拼接字符串;输出侧以行级互斥保证多线程下不交错。该模块不依赖 WinAPI 调试输出通道、不引入第三方库。
|
||||||
|
- 典型用法:`SX_LOGD/SX_LOGI/SX_LOGW/SX_LOGE/SX_LOGF/SX_LOG_TRACE` + 流式拼接;`SX_TRACE_SCOPE` 作用域耗时统计
|
||||||
|
- 核心配置:`setMinLevel(...)`、`setTagFilter(...)`、`enableConsole(true/false)`、`enableFile(path, append, rotateBytes)`、`setLanguage(ZhCN/EnUS)`、`setGBK()`
|
||||||
|
|
||||||
|
### ⚙️ 变更
|
||||||
|
|
||||||
|
- **TabControl 页签切换时序调整:**修改页签按钮切换逻辑为“先关闭当前已打开页,再打开目标页”,避免“先打开再关闭”在复杂容器/快照链路下引入的时序不确定性;对外 API 不变(涉及 `TabControl::add` 内部切换逻辑)。
|
||||||
|
- **控件 Setter 语义收敛:**明确控件 Setter 的职责为“更新状态并标脏”,绘制统一由窗口/容器的重绘收口机制触发,降低生命周期耦合与快照污染风险(见下方相关修复条目)。
|
||||||
|
|
||||||
|
### ✅ 修复
|
||||||
|
|
||||||
|
- **TextBox::setText 在进入事件循环前调用触发中断:**修复在窗口尚未初始化(未 `initgraph()`、图形上下文未就绪)前调用 `TextBox::setText()` 导致访问冲突崩溃的问题。旧实现将“状态更新”和“立即绘制”耦合,`setText()` 内部直接触发 `draw()`,进而在无图形上下文时进入 `saveBackground()/getimage()` 路径崩溃;现已移除 Setter 内直接绘制,仅保留赋值与置脏,绘制交由统一重绘流程完成。
|
||||||
|
- **TabControl 切换/关闭后 Table 新旧内容重叠(重影):**修复页签切换与表格重置数据后出现的稳定残影问题。根因是“快照未就绪阶段发生局部重绘”导致快照污染;本次回退 `setIsVisible(true)` 的“立即向上 requestRepaint”行为,改为仅置脏,并为 `Canvas::requestRepaint` 与 `TabControl::requestRepaint` 增加护栏:当容器 `dirty` 或 `hasSnap=0` 或快照缓存无效时,禁止 partial fast-path,自动降级为全量重绘以保证快照链路正确性;同时修正 `Table::setData(...)` 列补齐边界问题,降低异常噪声。
|
||||||
|
|
||||||
|
### ⚠️ 可能不兼容
|
||||||
|
|
||||||
|
- **删除 Button 尺寸别名 API(Breaking):**移除 `Button::getButtonWidth()` / `Button::getButtonHeight()`,统一使用基类 `Control::getWidth()` / `Control::getHeight()` 获取控件尺寸。该改动会导致旧代码在升级到 v3.0.0 后编译失败,但行为语义保持一致(仍读取同一 `width/height`)。
|
||||||
|
- **可见性/文本设置的“即时刷新”副作用移除:**`setIsVisible(true)` 与 `setText()` 等 Setter 不再保证立刻触发绘制;如业务代码此前依赖“调用后立即可见”,需要确保后续存在一次重绘收口(窗口主循环/容器重绘/显式刷新)以完成视觉更新。
|
||||||
|
|
||||||
|
### 📌 升级指引
|
||||||
|
|
||||||
|
1. **Button 宽高接口迁移:**
|
||||||
|
- `getButtonWidth()` → `getWidth()`
|
||||||
|
- `getButtonHeight()` → `getHeight()`
|
||||||
|
2. **Setter 不再“立即绘制”的适配:**
|
||||||
|
- 初始化阶段可先设置属性(如 `TextBox::setText("default")`),由首次 `Window::draw()` / 主事件循环的统一绘制完成可视刷新;
|
||||||
|
- 若在非事件驱动场景下程序化更新后需要立刻刷新,请确保触发一次统一重绘路径(避免在 Setter 内手动调用 `draw()` 造成生命周期耦合)。
|
||||||
|
3. **SxLog 接入建议:**
|
||||||
|
- 在程序入口完成基础配置(控制台输出/最低级别/语言),并使用统一 Tag(如 `Dirty/Resize/Table/Canvas/TabControl`)建立可回放的事件链路;高频路径建议通过级别与 Tag 控制噪声与 I/O 成本。
|
||||||
|
|
||||||
|
## [v2.3.2] - 2025 - 12 - 20
|
||||||
|
|
||||||
|
### ✨ 新增
|
||||||
|
|
||||||
|
- **Table 支持运行期重置表头与数据:**新增 `Table::clearHeaders()`、`Table::clearData()`、`Table::resetTable()`,允许同一 `Table` 在运行过程中动态切换表头与数据,并触发必要的单元格尺寸/分页信息重算与重绘。
|
||||||
|
- **TextBox 新增密码模式:**`TextBoxmode` 新增 `PASSWORD_MODE`;输入内容内部保存,绘制层面使用掩码字符(如 `*`)替代显示,真实文本可通过 `TextBox::getText()` 获取。
|
||||||
|
|
||||||
|
### ⚙️ 变更
|
||||||
|
|
||||||
|
- **TabControl 默认激活页语义明确化:**
|
||||||
|
- 首次绘制前调用 `TabControl::setActiveIndex()`:仅记录默认激活索引,不再立即触发页签按钮点击回调;
|
||||||
|
- 首次绘制完成后:若设置了默认激活索引则应用激活状态并绘制激活页(索引越界时默认激活最后一个页);
|
||||||
|
- 程序运行过程中调用 `TabControl::setActiveIndex()`:索引合法则立即切换激活页并绘制;索引越界则不做处理。
|
||||||
|
|
||||||
|
### ✅ 修复
|
||||||
|
|
||||||
|
- **TabControl::setActiveIndex 绘制前调用导致程序中断:**修复绘制前设置默认激活索引时触发空指针访问的问题;现在默认激活逻辑延后到首次绘制完成后再生效,避免崩溃并保证首次绘制即可绘制激活页。
|
||||||
|
- **TabControl 由不可见设置为可见时绘制错乱:**修复 `setIsVisible(false) -> setIsVisible(true)` 后非激活页被错误绘制导致的多页重叠/残影;现在 TabControl 可见时仅激活页可见/可绘制,无激活页则不绘制任何页。
|
||||||
|
|
||||||
## [v2.3.1] - 2025 - 11 - 30
|
## [v2.3.1] - 2025 - 11 - 30
|
||||||
|
|
||||||
## 🙏 鸣谢
|
### 🙏 鸣谢
|
||||||
|
|
||||||
- 感谢用户 [@To-KongBai](https://github.com/To-KongBai) 提供稳定复现步骤与关键现象对比(容器嵌套孙控件坐标转换问题),帮助我们快速确认多容器嵌套时的控件坐标转换问题并修复。([Issues#6](https://github.com/Ysm-04/StellarX/issues/6))
|
- 感谢用户 [@To-KongBai](https://github.com/To-KongBai) 提供稳定复现步骤与关键现象对比(容器嵌套孙控件坐标转换问题),帮助我们快速确认多容器嵌套时的控件坐标转换问题并修复。([Issues#6](https://github.com/Ysm-04/StellarX/issues/6))
|
||||||
- 在即将上线的官网中(ICP备案中)我们计划加入一个贡献者鸣谢墙,欢迎各位用户反馈BUG或者分享自己用星垣做的界面,我们将认真阅读并收录鸣谢
|
- 在即将上线的官网中(ICP备案中)我们计划加入一个贡献者鸣谢墙,欢迎各位用户反馈BUG或者分享自己用星垣做的界面,我们将认真阅读并收录鸣谢
|
||||||
|
|||||||
@@ -7,13 +7,41 @@ project(StellarX VERSION 2.0.0 LANGUAGES CXX)
|
|||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||||
|
|
||||||
|
# 为了支持 out-of-source builds,创建构建目录
|
||||||
|
set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/build)
|
||||||
|
|
||||||
|
# 设置生成的二进制文件输出目录
|
||||||
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||||
|
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||||
|
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||||
|
|
||||||
# 包含头文件目录(目前头文件都在根目录)
|
# 包含头文件目录(目前头文件都在根目录)
|
||||||
include_directories(${CMAKE_SOURCE_DIR})
|
include_directories(${CMAKE_SOURCE_DIR})
|
||||||
|
|
||||||
# 源文件收集
|
# 通过选项设置是否启用调试信息
|
||||||
|
option(USE_DEBUG "Build with debug information" OFF)
|
||||||
|
if(USE_DEBUG)
|
||||||
|
set(CMAKE_BUILD_TYPE Debug)
|
||||||
|
else()
|
||||||
|
set(CMAKE_BUILD_TYPE Release)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# 查找源文件
|
||||||
file(GLOB_RECURSE SOURCES
|
file(GLOB_RECURSE SOURCES
|
||||||
"${CMAKE_SOURCE_DIR}/*.cpp"
|
"${CMAKE_SOURCE_DIR}/*.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 生成可执行文件
|
# 生成可执行文件
|
||||||
add_executable(StellarX ${SOURCES})
|
add_executable(StellarX ${SOURCES})
|
||||||
|
|
||||||
|
# 可以选择性地查找外部库并链接(例如 Boost,SDL2等)
|
||||||
|
# FindPackage(Boost REQUIRED)
|
||||||
|
# target_link_libraries(StellarX Boost::Boost)
|
||||||
|
|
||||||
|
# 为外部依赖配置路径
|
||||||
|
# set(Boost_DIR "path/to/boost")
|
||||||
|
# find_package(Boost REQUIRED)
|
||||||
|
|
||||||
|
# 如果有额外的库需要链接,继续在此处添加
|
||||||
|
# target_link_libraries(StellarX Boost::Boost)
|
||||||
|
|
||||||
|
|||||||
33
README.en.md
33
README.en.md
@@ -7,8 +7,8 @@
|
|||||||

|

|
||||||
[](https://github.com/Ysm-04/StellarX)
|
[](https://github.com/Ysm-04/StellarX)
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -25,29 +25,34 @@ This is a **teaching-grade and tooling-grade** framework that helps developers u
|
|||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## 🆕 V2.3.0 - Major Update
|
### 🆕V3.0.0 - Major Update
|
||||||
|
|
||||||
**This version represents a significant milestone, introducing a responsive layout system that transitions from static to dynamic layout management, and comprehensively resolves the previously encountered random rendering corruption issues caused by reentrant drawing operations.**
|
[CHANGELOG.en.md](CHANGELOG.en.md)
|
||||||
|
|
||||||
- **Optimized Window Resizing Mechanism**: Refactored `WndProcThunk`, `runEventLoop`, and `pumpResizeIfNeeded` to uniformly record size changes and perform centralized repainting at the end of the event loop, eliminating jitter and sequencing confusion caused by repeated redraws.
|
### ✨ New Features
|
||||||
|
|
||||||
- **New Dialog Size Scheduling Interface**: Introduced the combination of `Window::scheduleResizeFromModal()` and `pumpResizeIfNeeded()`, enabling modal dialogs to notify the parent window of size updates even during resizing operations. Underlying controls are relayout during unified finalization while dialogs maintain their original dimensions.
|
- **SxLog**: A lightweight logging system with support for log levels, tag filtering, bilingual (Chinese/English) output, and console/file logging with optional file rolling.
|
||||||
|
- **TabControl**: Improved tab switching logic to ensure the current tab is closed before the target tab is opened.
|
||||||
|
- **Improved Setters**: Setters now only update state and mark as dirty, with drawing handled by the unified redraw flow.
|
||||||
|
|
||||||
- **Enhanced Adaptive Layout System**: Internally added the `adaptiveLayout()` function to recalculate control positions and sizes based on anchor points, allowing dual-anchored controls (left-right or top-bottom) to adaptively stretch with window resizing.
|
### ⚙️ Changes
|
||||||
|
|
||||||
- **Fixed Modal Dialog Resizing Issues**: Resolved the problem where window resizing while modal dialogs were open prevented underlying controls from updating their positions and sizes according to anchor points; simultaneously eliminated ghosting artifacts caused by repeated dialog redraws.
|
- **TabControl Default Active Tab**: Default active tab logic clarified. First, set the active index without immediate drawing; after the first draw, the tab is activated.
|
||||||
|
|
||||||
- **Further Resolved Drawing Sequence Confusion**: Replaced `InvalidateRect` with `ValidateRect` during resizing operations, ensuring the window is marked as valid only after a single unified drawing pass, preventing system-triggered `WM_PAINT` messages from causing reentrancy.
|
### ✅ Fixes
|
||||||
|
|
||||||
- **Additional Fixes**: Corrected delayed background snapshot updates in tables and dialogs under certain edge cases.
|
- **TabControl::setActiveIndex Crash**: Fixed crash when setting the default active tab before the first draw.
|
||||||
|
- **TabControl Rendering Glitch**: Fixed issue where non-active tabs were incorrectly drawn when switching visibility.
|
||||||
|
|
||||||

|
### ⚠️ Breaking Changes
|
||||||
|
|
||||||

|
- **Button Size APIs Removed**: `getButtonWidth()` and `getButtonHeight()` removed; use `getWidth()` and `getHeight()` instead.
|
||||||
|
- **No Immediate Drawing for Setters**: Setters like `setText()` no longer trigger immediate drawing.
|
||||||
|
|
||||||
For details, please refer to the [CHANGELOG.en](CHANGELOG.en.md).
|
### 📌 Upgrade Guide
|
||||||
|
|
||||||
------
|
- **Button**: Replace `getButtonWidth()` / `getButtonHeight()` with `getWidth()` / `getHeight()`.
|
||||||
|
- **Setters**: Ensure a redraw mechanism after calling setters like `setText()`.
|
||||||
|
|
||||||
## 📦 Project Structure & Design Philosophy
|
## 📦 Project Structure & Design Philosophy
|
||||||
|
|
||||||
|
|||||||
38
README.md
38
README.md
@@ -14,8 +14,8 @@
|
|||||||

|

|
||||||
[](https://github.com/Ysm-04/StellarX)
|
[](https://github.com/Ysm-04/StellarX)
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -32,30 +32,36 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ==公告==
|
|
||||||
|
|
||||||
## 🙏 鸣谢
|
|
||||||
|
|
||||||
- 感谢用户 [@To-KongBai](https://github.com/To-KongBai) 提供稳定复现步骤与关键现象对比(容器嵌套孙控件坐标转换问题),帮助我们快速确认多容器嵌套时的控件坐标转换问题并修复。([Issues#6](https://github.com/Ysm-04/StellarX/issues/6))
|
### 🆕V3.0.0 - 重要更新
|
||||||
- 在即将上线的官网中(ICP备案中)我们计划加入一个贡献者鸣谢墙,欢迎各位用户反馈BUG或者分享自己用星垣做的界面,我们将认真阅读并收录鸣谢
|
|
||||||
- 真诚的感谢每一位反馈BUG的用户,你们的反馈将使星垣更加稳定和健壮
|
|
||||||
|
|
||||||
---
|
完整版建议查看[更新日志](CHANGELOG.md)
|
||||||
|
|
||||||
## 🆕V2.3.1——重要更新
|
### ✨ 新增功能
|
||||||
|
|
||||||
### ✨ 新增
|
- **SxLog**: 轻量级日志系统,支持日志级别、标签过滤、中英文输出及控制台/文件日志,支持文件滚动。
|
||||||
|
- **TabControl**: 改进页签切换逻辑,确保先关闭当前页再打开目标页。
|
||||||
新增一个登录界面Demo在主仓库**examples/**目录下
|
- **控件 Setter 改进**: Setter 仅更新状态并标记为脏,绘制由统一重绘流程处理。
|
||||||
|
|
||||||
### ⚙️ 变更
|
### ⚙️ 变更
|
||||||
|
|
||||||
- **Dialog背景快照机制:**`Dialog`不在自己抓取和销毁快照,**删除**重载的抓取和恢复快照的方法,完全交由基类`Canvas`处理,`Dialog`的`draw`方法中不在处理快照
|
- **TabControl 默认激活页**: 默认激活页逻辑明确,首次设置激活索引后才绘制。
|
||||||
- **窗口变化重绘时控件和窗口重绘的时机调整:**主事件循环中窗口大小发生变化时先处理控件尺寸,并回贴和释放旧快照,然后再重绘新尺寸窗口,最后绘制控件
|
|
||||||
|
|
||||||
本次针对用户反馈和已知内容进行了一些修复……
|
### ✅ 修复
|
||||||
|
|
||||||
详细变更请参阅[更新日志](CHANGELOG.md)
|
- **TabControl::setActiveIndex 崩溃**: 修复首次绘制前设置默认激活页时崩溃的问题。
|
||||||
|
- **TabControl 渲染错乱**: 修复 `setIsVisible` 切换时,非激活页错误绘制导致的重叠问题。
|
||||||
|
|
||||||
|
### ⚠️ 破坏性更改
|
||||||
|
|
||||||
|
- **Button 尺寸 API 移除**: 移除了 `getButtonWidth()` / `getButtonHeight()`,请使用 `getWidth()` / `getHeight()`。
|
||||||
|
- **Setter 不再即时绘制**: `setText()` 等 Setter 不再触发即时绘制。
|
||||||
|
|
||||||
|
### 📌 升级指南
|
||||||
|
|
||||||
|
- **Button**: 将 `getButtonWidth()` / `getButtonHeight()` 替换为 `getWidth()` / `getHeight()`。
|
||||||
|
- **Setter**: 在调用 Setter 后确保触发重绘机制。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
58
examples/SXLog-日志系统使用demo/SXLog-demo.cpp
Normal file
58
examples/SXLog-日志系统使用demo/SXLog-demo.cpp
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#include"StellarX.h"//包含SXLog头文件
|
||||||
|
|
||||||
|
#if 1
|
||||||
|
// 业务场景只需用日志系统时的最简用法示例
|
||||||
|
/*
|
||||||
|
//设置编码 按需开启
|
||||||
|
StellarX::SxLogger::setGBK();
|
||||||
|
|
||||||
|
//启用日志文件和控制台输出 按需开启
|
||||||
|
StellarX::SxLogger::Get().enableFile("stellarx.log", false, 1024);
|
||||||
|
StellarX::SxLogger::Get().enableConsole(true);
|
||||||
|
|
||||||
|
//设置最低日志级别和语言 按需设置
|
||||||
|
StellarX::SxLogger::Get().setMinLevel(StellarX::SxLogLevel::Debug); // Info/Debug/Trace 自己切
|
||||||
|
StellarX::SxLogger::Get().setLanguage(StellarX::SxLogLanguage::ZhCN); // ZhCN / EnUS
|
||||||
|
*/
|
||||||
|
using namespace StellarX;
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
|
||||||
|
// 1) 可选:把 Windows 控制台切到 GBK,避免中文乱码(内部用 system("chcp 936"))
|
||||||
|
SxLogger::setGBK();
|
||||||
|
|
||||||
|
// 2) 获取全局 logger 并开启控制台输出
|
||||||
|
SxLogger& log = SxLogger::Get();
|
||||||
|
log.enableConsole(true);
|
||||||
|
|
||||||
|
// 3) 最低输出级别(Debug 及以上都会输出)
|
||||||
|
log.setMinLevel(SxLogLevel::Debug);
|
||||||
|
|
||||||
|
// 4) 语言选择(影响 SX_T 的文本选择,不做转码)
|
||||||
|
log.setLanguage(SxLogLanguage::ZhCN);
|
||||||
|
|
||||||
|
// 5) 打几条日志(流式拼接)
|
||||||
|
SX_LOGI("Init") << SX_T("日志系统已启用", "logging enabled");
|
||||||
|
SX_LOGD("Init") << "minLevel=" << (int)log.getMinLevel();
|
||||||
|
|
||||||
|
// 6) 作用域耗时统计(只有 Trace 级别打开且 shouldLog 命中才会输出)
|
||||||
|
{
|
||||||
|
SX_TRACE_SCOPE("Perf", "demo-scope");
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7) 切换语言演示
|
||||||
|
log.setLanguage(SxLogLanguage::EnUS);
|
||||||
|
SX_LOGI("Init") << SX_T("这条不会显示中文", "this line is English");
|
||||||
|
|
||||||
|
// 8) Tag 过滤演示(只允许 Init)
|
||||||
|
log.setTagFilter(SxTagFilterMode::Whitelist, { "Init" });
|
||||||
|
SX_LOGI("Init") << "allowed";
|
||||||
|
SX_LOGI("Table") << "blocked (should not appear)";
|
||||||
|
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -21,17 +21,14 @@
|
|||||||
#include "Control.h"
|
#include "Control.h"
|
||||||
#include"label.h"
|
#include"label.h"
|
||||||
|
|
||||||
|
|
||||||
#define DISABLEDCOLOUR RGB(96, 96, 96) //禁用状态颜色
|
#define DISABLEDCOLOUR RGB(96, 96, 96) //禁用状态颜色
|
||||||
#define TEXTMARGINS_X 6
|
#define TEXTMARGINS_X 6
|
||||||
#define TEXTMARGINS_Y 4
|
#define TEXTMARGINS_Y 4
|
||||||
constexpr int bordWith = 1; //边框宽度,用于快照恢复时的偏移计算
|
constexpr int bordWith = 1; //边框宽度,用于快照恢复时的偏移计算
|
||||||
constexpr int bordHeight = 1; //边框高度,用于快照恢复时的偏移计算
|
constexpr int bordHeight = 1; //边框高度,用于快照恢复时的偏移计算
|
||||||
|
|
||||||
|
|
||||||
class Button : public Control
|
class Button : public Control
|
||||||
{
|
{
|
||||||
|
|
||||||
std::string text; // 按钮上的文字
|
std::string text; // 按钮上的文字
|
||||||
bool click; // 是否被点击
|
bool click; // 是否被点击
|
||||||
bool hover; // 是否被悬停
|
bool hover; // 是否被悬停
|
||||||
@@ -54,8 +51,6 @@ class Button : public Control
|
|||||||
StellarX::FillStyle buttonFillIma = StellarX::FillStyle::BDiagonal; //按钮填充图案
|
StellarX::FillStyle buttonFillIma = StellarX::FillStyle::BDiagonal; //按钮填充图案
|
||||||
IMAGE* buttonFileIMAGE = nullptr; //按钮填充图像
|
IMAGE* buttonFileIMAGE = nullptr; //按钮填充图像
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
std::function<void()> onClickCallback; //回调函数
|
std::function<void()> onClickCallback; //回调函数
|
||||||
std::function<void()> onToggleOnCallback; //TOGGLE模式下的回调函数
|
std::function<void()> onToggleOnCallback; //TOGGLE模式下的回调函数
|
||||||
std::function<void()> onToggleOffCallback; //TOGGLE模式下的回调函数
|
std::function<void()> onToggleOffCallback; //TOGGLE模式下的回调函数
|
||||||
@@ -192,6 +187,4 @@ private:
|
|||||||
void hideTooltip();
|
void hideTooltip();
|
||||||
// 根据当前 click 状态选择文案
|
// 根据当前 click 状态选择文案
|
||||||
void refreshTooltipTextForState();
|
void refreshTooltipTextForState();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ protected:
|
|||||||
COLORREF canvasBorderClor = RGB(0, 0, 0); //边框颜色
|
COLORREF canvasBorderClor = RGB(0, 0, 0); //边框颜色
|
||||||
COLORREF canvasBkClor = RGB(255, 255, 255); //背景颜色
|
COLORREF canvasBkClor = RGB(255, 255, 255); //背景颜色
|
||||||
|
|
||||||
|
|
||||||
// 清除所有子控件
|
// 清除所有子控件
|
||||||
void clearAllControls();
|
void clearAllControls();
|
||||||
public:
|
public:
|
||||||
@@ -71,6 +70,4 @@ public:
|
|||||||
private:
|
private:
|
||||||
//用来检查对话框是否模态,此控件不做实现
|
//用来检查对话框是否模态,此控件不做实现
|
||||||
bool model() const override { return false; };
|
bool model() const override { return false; };
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ protected:
|
|||||||
|
|
||||||
Control() : localx(0), x(0), localy(0), y(0), localWidth(100), width(100), height(100), localHeight(100) {}
|
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)
|
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){}
|
: localx(x), x(x), localy(y), y(y), localWidth(width), width(width), height(height), localHeight(height) {
|
||||||
|
}
|
||||||
public:
|
public:
|
||||||
|
|
||||||
virtual ~Control()
|
virtual ~Control()
|
||||||
|
|||||||
@@ -207,7 +207,8 @@ namespace StellarX
|
|||||||
enum class TextBoxmode
|
enum class TextBoxmode
|
||||||
{
|
{
|
||||||
INPUT_MODE, // 用户可输入模式
|
INPUT_MODE, // 用户可输入模式
|
||||||
READONLY_MODE // 只读模式
|
READONLY_MODE, // 只读模式
|
||||||
|
PASSWORD_MODE// 密码模式
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ class Dialog : public Canvas
|
|||||||
std::string message; //提示信息
|
std::string message; //提示信息
|
||||||
std::vector<std::string> lines; //消息内容按行分割
|
std::vector<std::string> lines; //消息内容按行分割
|
||||||
|
|
||||||
|
|
||||||
bool needsInitialization = true; //是否需要初始化
|
bool needsInitialization = true; //是否需要初始化
|
||||||
bool close = false; //是否关闭
|
bool close = false; //是否关闭
|
||||||
bool modal = true; //是否模态
|
bool modal = true; //是否模态
|
||||||
@@ -57,7 +56,6 @@ class Dialog : public Canvas
|
|||||||
COLORREF buttonFalseColor = RGB(215, 215, 215); //按钮未被点击颜色
|
COLORREF buttonFalseColor = RGB(215, 215, 215); //按钮未被点击颜色
|
||||||
COLORREF buttonHoverColor = RGB(224, 224, 224); //按钮悬浮颜色
|
COLORREF buttonHoverColor = RGB(224, 224, 224); //按钮悬浮颜色
|
||||||
|
|
||||||
|
|
||||||
Button* closeButton = nullptr; //关闭按钮
|
Button* closeButton = nullptr; //关闭按钮
|
||||||
|
|
||||||
StellarX::MessageBoxResult result = StellarX::MessageBoxResult::Cancel; // 对话框结果
|
StellarX::MessageBoxResult result = StellarX::MessageBoxResult::Cancel; // 对话框结果
|
||||||
@@ -78,7 +76,6 @@ public:
|
|||||||
//获取对话框消息,用以去重
|
//获取对话框消息,用以去重
|
||||||
std::string GetText() const;
|
std::string GetText() const;
|
||||||
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Dialog(Window& hWnd, std::string text, std::string message = "对话框", StellarX::MessageBoxType type = StellarX::MessageBoxType::OK, bool modal = true);
|
Dialog(Window& hWnd, std::string text, std::string message = "对话框", StellarX::MessageBoxType type = StellarX::MessageBoxType::OK, bool modal = true);
|
||||||
~Dialog();
|
~Dialog();
|
||||||
@@ -109,7 +106,6 @@ public:
|
|||||||
//初始化
|
//初始化
|
||||||
void setInitialization(bool init);
|
void setInitialization(bool init);
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// 初始化按钮
|
// 初始化按钮
|
||||||
void initButtons();
|
void initButtons();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
* @文件: StellarX.h
|
* @文件: StellarX.h
|
||||||
* @摘要: 星垣(StellarX) GUI框架 - 主包含头文件
|
* @摘要: 星垣(StellarX) GUI框架 - 主包含头文件
|
||||||
* @版本: v2.3.1
|
* @版本: v3.0.0
|
||||||
* @描述:
|
* @描述:
|
||||||
* 一个为Windows平台打造的轻量级、模块化C++ GUI框架。
|
* 一个为Windows平台打造的轻量级、模块化C++ GUI框架。
|
||||||
* 基于EasyX图形库,提供简洁易用的API和丰富的控件。
|
* 基于EasyX图形库,提供简洁易用的API和丰富的控件。
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
*
|
*
|
||||||
* @作者: 我在人间做废物
|
* @作者: 我在人间做废物
|
||||||
* @邮箱: [3150131407@qq.com] | [ysm3150131407@gmail.com]
|
* @邮箱: [3150131407@qq.com] | [ysm3150131407@gmail.com]
|
||||||
* @官网地址:https://stellarx-gui.top
|
* @官网:https://stellarx-gui.top/
|
||||||
* @仓库: [https://github.com/Ysm-04/StellarX]
|
* @仓库: [https://github.com/Ysm-04/StellarX]
|
||||||
*
|
*
|
||||||
* @许可证: MIT License
|
* @许可证: MIT License
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "CoreTypes.h"
|
#include "CoreTypes.h"
|
||||||
|
#include "SxLog.h"
|
||||||
#include "Control.h"
|
#include "Control.h"
|
||||||
#include"Canvas.h"
|
#include"Canvas.h"
|
||||||
#include"Window.h"
|
#include"Window.h"
|
||||||
@@ -41,3 +42,5 @@
|
|||||||
#include"Dialog.h"
|
#include"Dialog.h"
|
||||||
#include"MessageBox.h"
|
#include"MessageBox.h"
|
||||||
#include"TabControl.h"
|
#include"TabControl.h"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
416
include/StellarX/SxLog.h
Normal file
416
include/StellarX/SxLog.h
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
#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; // 控制台 sink(enableConsole 控制)
|
||||||
|
std::unique_ptr<FileSink> fileSink; // 文件 sink(enableFile 控制)
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ========================= 双语选择辅助 ========================= */
|
||||||
|
// 说明:
|
||||||
|
// - 只做“选择 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
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
class TabControl :public Canvas
|
class TabControl :public Canvas
|
||||||
{
|
{
|
||||||
int tabBarHeight = BUTMINWIDTH; //页签栏高度
|
int tabBarHeight = BUTMINWIDTH; //页签栏高度
|
||||||
|
bool IsFirstDraw = true; //首次绘制标记
|
||||||
|
int defaultActivation = -1; //默认激活页签索引
|
||||||
StellarX::TabPlacement tabPlacement = StellarX::TabPlacement::Top; //页签排列方式
|
StellarX::TabPlacement tabPlacement = StellarX::TabPlacement::Top; //页签排列方式
|
||||||
std::vector<std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>> controls; //页签/页列表
|
std::vector<std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>> controls; //页签/页列表
|
||||||
|
|
||||||
@@ -67,6 +69,4 @@ public:
|
|||||||
int indexOf(const std::string& tabText) const;
|
int indexOf(const std::string& tabText) const;
|
||||||
void setDirty(bool dirty) override;
|
void setDirty(bool dirty) override;
|
||||||
void requestRepaint(Control* parent)override;
|
void requestRepaint(Control* parent)override;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,4 @@ public:
|
|||||||
void setTextBkColor(COLORREF color);
|
void setTextBkColor(COLORREF color);
|
||||||
//设置标签文本
|
//设置标签文本
|
||||||
void setText(std::string text);
|
void setText(std::string text);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,6 @@
|
|||||||
#define TABLE_STR_PAGE_MID "页/共"
|
#define TABLE_STR_PAGE_MID "页/共"
|
||||||
#define TABLE_STR_PAGE_SUFFIX "页"
|
#define TABLE_STR_PAGE_SUFFIX "页"
|
||||||
|
|
||||||
|
|
||||||
class Table :public Control
|
class Table :public Control
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
@@ -67,7 +66,7 @@ private:
|
|||||||
int totalPages = 1; // 总页数
|
int totalPages = 1; // 总页数
|
||||||
|
|
||||||
bool isShowPageButton = true; // 是否显示翻页按钮
|
bool isShowPageButton = true; // 是否显示翻页按钮
|
||||||
bool isNeedDrawHeaders = true; // 是否需要绘制表头
|
bool isNeedDrawHeaders = true; // 是否需要绘制表头(暂时废弃,单做保留,后期优化可能用到)
|
||||||
bool isNeedCellSize = true; // 是否需要计算单元格尺寸
|
bool isNeedCellSize = true; // 是否需要计算单元格尺寸
|
||||||
bool isNeedButtonAndPageNum = true; // 是否需要计算翻页按钮和页码信息
|
bool isNeedButtonAndPageNum = true; // 是否需要计算翻页按钮和页码信息
|
||||||
|
|
||||||
@@ -131,6 +130,12 @@ public:
|
|||||||
void setTableLineStyle(StellarX::LineStyle style);
|
void setTableLineStyle(StellarX::LineStyle style);
|
||||||
//设置边框宽度
|
//设置边框宽度
|
||||||
void setTableBorderWidth(int width);
|
void setTableBorderWidth(int width);
|
||||||
|
//清空表头
|
||||||
|
void clearHeaders();
|
||||||
|
//清空表格数据
|
||||||
|
void clearData();
|
||||||
|
//清空表头和数据
|
||||||
|
void resetTable();
|
||||||
//窗口变化丢快照+标脏
|
//窗口变化丢快照+标脏
|
||||||
void onWindowResize() override;
|
void onWindowResize() override;
|
||||||
|
|
||||||
@@ -161,7 +166,4 @@ public:
|
|||||||
//获取表格尺寸
|
//获取表格尺寸
|
||||||
int getTableWidth() const;
|
int getTableWidth() const;
|
||||||
int getTableHeight() const;
|
int getTableHeight() const;
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "Control.h"
|
#include "Control.h"
|
||||||
|
|
||||||
|
|
||||||
class TextBox : public Control
|
class TextBox : public Control
|
||||||
{
|
{
|
||||||
std::string text; //文本
|
std::string text; //文本
|
||||||
@@ -55,5 +54,3 @@ private:
|
|||||||
//用来检查对话框是否模态,此控件不做实现
|
//用来检查对话框是否模态,此控件不做实现
|
||||||
bool model() const override { return false; };
|
bool model() const override { return false; };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
/**
|
||||||
/**
|
|
||||||
* Window(头文件)
|
* Window(头文件)
|
||||||
*
|
*
|
||||||
* 设计目标:
|
* 设计目标:
|
||||||
@@ -13,6 +12,7 @@
|
|||||||
* - WM_GETMINMAXINFO:按最小“客户区”换算到“窗口矩形”,提供系统层最小轨迹值。
|
* - WM_GETMINMAXINFO:按最小“客户区”换算到“窗口矩形”,提供系统层最小轨迹值。
|
||||||
* - runEventLoop:只记录 WM_SIZE 的新尺寸;真正绘制放在 needResizeDirty 时集中处理。
|
* - runEventLoop:只记录 WM_SIZE 的新尺寸;真正绘制放在 needResizeDirty 时集中处理。
|
||||||
*/
|
*/
|
||||||
|
//fuck windows fuck win32
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Control.h"
|
#include "Control.h"
|
||||||
@@ -54,7 +54,7 @@ class Window
|
|||||||
std::vector<std::unique_ptr<Control>> dialogs;
|
std::vector<std::unique_ptr<Control>> dialogs;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
bool dialogClose = false; // 项目内使用的状态位
|
bool dialogClose = false; // 项目内使用的状态位,对话框关闭标志
|
||||||
|
|
||||||
// —— 构造/析构 ——(仅初始化成员;实际样式与子类化在 draw() 中完成)
|
// —— 构造/析构 ——(仅初始化成员;实际样式与子类化在 draw() 中完成)
|
||||||
Window(int width, int height, int mode);
|
Window(int width, int height, int mode);
|
||||||
@@ -87,24 +87,9 @@ public:
|
|||||||
std::string getBkImageFile() const;
|
std::string getBkImageFile() const;
|
||||||
std::vector<std::unique_ptr<Control>>& getControls();
|
std::vector<std::unique_ptr<Control>>& getControls();
|
||||||
|
|
||||||
// —— 配置开关 ——(动态调整最小客户区、合成双缓冲)
|
// —— 尺寸调整 ——(供内部与外部调用的尺寸变化处理)
|
||||||
inline void setMinClientSize(int w, int h)
|
|
||||||
{
|
|
||||||
// 仅更新阈值;实际约束在 WM_GETMINMAXINFO/WM_SIZING 中生效
|
|
||||||
minClientW = w;
|
|
||||||
minClientH = h;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void setComposited(bool on)
|
|
||||||
{
|
|
||||||
// 更新标志;真正应用在 draw()/样式 SetWindowLongEx + SWP_FRAMECHANGED
|
|
||||||
useComposited = on;
|
|
||||||
}
|
|
||||||
|
|
||||||
void processWindowMessage(const ExMessage & msg); // 处理 EX_WINDOW 中的 WM_SIZE 等
|
|
||||||
void pumpResizeIfNeeded(); // 执行一次统一收口重绘
|
void pumpResizeIfNeeded(); // 执行一次统一收口重绘
|
||||||
void scheduleResizeFromModal(int w, int h);
|
void scheduleResizeFromModal(int w, int h);
|
||||||
private:
|
private:
|
||||||
void adaptiveLayout(std::unique_ptr<Control>& c, const int finalH, const int finalW);
|
void adaptiveLayout(std::unique_ptr<Control>& c, const int finalH, const int finalW);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "Button.h"
|
#include "Button.h"
|
||||||
|
#include "SxLog.h"
|
||||||
|
|
||||||
Button::Button(int x, int y, int width, int height, const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape)
|
Button::Button(int x, int y, int width, int height, const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape)
|
||||||
: Control(x, y, width, height)
|
: Control(x, y, width, height)
|
||||||
@@ -163,7 +164,6 @@ void Button::initButton(const std::string text, StellarX::ButtonMode mode, Stell
|
|||||||
tipLabel.textStyle = this->textStyle; // 复用按钮字体样式
|
tipLabel.textStyle = this->textStyle; // 复用按钮字体样式
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Button::~Button()
|
Button::~Button()
|
||||||
{
|
{
|
||||||
if (buttonFileIMAGE)
|
if (buttonFileIMAGE)
|
||||||
@@ -274,7 +274,6 @@ void Button::draw()
|
|||||||
|
|
||||||
restoreStyle();//恢复默认字体样式和颜色
|
restoreStyle();//恢复默认字体样式和颜色
|
||||||
dirty = false; //标记按钮不需要重绘
|
dirty = false; //标记按钮不需要重绘
|
||||||
|
|
||||||
}
|
}
|
||||||
// 处理鼠标事件,检测点击和悬停状态
|
// 处理鼠标事件,检测点击和悬停状态
|
||||||
// 根据按钮模式和形状进行不同的处理
|
// 根据按钮模式和形状进行不同的处理
|
||||||
@@ -283,8 +282,9 @@ bool Button::handleEvent(const ExMessage& msg)
|
|||||||
if (!show)
|
if (!show)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
bool oldHover = hover;
|
bool oldHover = hover;// 注意:只在状态变化时记录,避免 WM_MOUSEMOVE 刷屏
|
||||||
bool oldClick = click;
|
bool oldClick = click;
|
||||||
|
|
||||||
bool consume = false;//是否消耗事件
|
bool consume = false;//是否消耗事件
|
||||||
// 记录鼠标位置(用于tip定位)
|
// 记录鼠标位置(用于tip定位)
|
||||||
if (msg.message == WM_MOUSEMOVE)
|
if (msg.message == WM_MOUSEMOVE)
|
||||||
@@ -310,14 +310,19 @@ bool Button::handleEvent(const ExMessage& msg)
|
|||||||
hover = isMouseInEllipse(msg.x, msg.y, x, y, x + width, y + height);
|
hover = isMouseInEllipse(msg.x, msg.y, x, y, x + width, y + height);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (hover != oldHover)
|
||||||
|
{
|
||||||
|
SX_LOGD("Button") << SX_T("悬停变化: ","hover change: ") << "id=" << id
|
||||||
|
<< " " << (oldHover ? 1 : 0) << "->" << (hover ? 1 : 0);
|
||||||
|
}
|
||||||
// 处理鼠标点击事件
|
// 处理鼠标点击事件
|
||||||
if (msg.message == WM_LBUTTONDOWN && hover && mode != StellarX::ButtonMode::DISABLED)
|
if (msg.message == WM_LBUTTONDOWN && hover && mode != StellarX::ButtonMode::DISABLED)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (mode == StellarX::ButtonMode::NORMAL)
|
if (mode == StellarX::ButtonMode::NORMAL)
|
||||||
{
|
{
|
||||||
click = true;
|
click = true;
|
||||||
|
SX_LOGD("Button") << SX_T("被点击: ","lbtn - down:")<< "id = " << id << " mode = " << (int)mode;
|
||||||
|
|
||||||
dirty = true;
|
dirty = true;
|
||||||
consume = true;
|
consume = true;
|
||||||
}
|
}
|
||||||
@@ -334,6 +339,8 @@ bool Button::handleEvent(const ExMessage& msg)
|
|||||||
if (mode == StellarX::ButtonMode::NORMAL && click)
|
if (mode == StellarX::ButtonMode::NORMAL && click)
|
||||||
{
|
{
|
||||||
if (onClickCallback) onClickCallback();
|
if (onClickCallback) onClickCallback();
|
||||||
|
SX_LOGI("Button") << "click: id=" << id << " (NORMAL) callback=" << (onClickCallback ? "Y" : "N");
|
||||||
|
|
||||||
click = false;
|
click = false;
|
||||||
dirty = true;
|
dirty = true;
|
||||||
consume = true;
|
consume = true;
|
||||||
@@ -346,6 +353,11 @@ bool Button::handleEvent(const ExMessage& msg)
|
|||||||
click = !click;
|
click = !click;
|
||||||
if (click && onToggleOnCallback) onToggleOnCallback();
|
if (click && onToggleOnCallback) onToggleOnCallback();
|
||||||
else if (!click && onToggleOffCallback) onToggleOffCallback();
|
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;
|
dirty = true;
|
||||||
consume = true;
|
consume = true;
|
||||||
refreshTooltipTextForState();
|
refreshTooltipTextForState();
|
||||||
@@ -385,6 +397,8 @@ bool Button::handleEvent(const ExMessage& msg)
|
|||||||
// 到点就显示
|
// 到点就显示
|
||||||
if (GetTickCount64() - tipHoverTick >= (ULONGLONG)tipDelayMs)
|
if (GetTickCount64() - tipHoverTick >= (ULONGLONG)tipDelayMs)
|
||||||
{
|
{
|
||||||
|
SX_LOGD("Button") << SX_T("提示信息显示: ","tooltip show:")<<" id = " << id <<SX_T("延时时间: ", " delayMs = ") << tipDelayMs;
|
||||||
|
|
||||||
tipVisible = true;
|
tipVisible = true;
|
||||||
|
|
||||||
// 定位(跟随鼠标 or 相对按钮)
|
// 定位(跟随鼠标 or 相对按钮)
|
||||||
@@ -451,7 +465,6 @@ void Button::setROUND_RECTANGLEwidth(int width)
|
|||||||
{
|
{
|
||||||
rouRectangleSize.ROUND_RECTANGLEwidth = width;
|
rouRectangleSize.ROUND_RECTANGLEwidth = width;
|
||||||
this->dirty = true; // 标记需要重绘
|
this->dirty = true; // 标记需要重绘
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Button::setROUND_RECTANGLEheight(int height)
|
void Button::setROUND_RECTANGLEheight(int height)
|
||||||
@@ -489,7 +502,6 @@ void Button::setFillIma(std::string imaNAme)
|
|||||||
this->dirty = true;
|
this->dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Button::setButtonBorder(COLORREF Border)
|
void Button::setButtonBorder(COLORREF Border)
|
||||||
{
|
{
|
||||||
buttonBorderColor = Border;
|
buttonBorderColor = Border;
|
||||||
@@ -558,7 +570,6 @@ void Button::setButtonClick(BOOL click)
|
|||||||
requestRepaint(parent);
|
requestRepaint(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::string Button::getButtonText() const
|
std::string Button::getButtonText() const
|
||||||
{
|
{
|
||||||
return this->text;
|
return this->text;
|
||||||
@@ -619,8 +630,6 @@ int Button::getButtonHeight() const
|
|||||||
return this->height;
|
return this->height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bool Button::isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius)
|
bool Button::isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius)
|
||||||
{
|
{
|
||||||
double dis = sqrt(pow(mouseX - x, 2) + pow(mouseY - y, 2));
|
double dis = sqrt(pow(mouseX - x, 2) + pow(mouseY - y, 2));
|
||||||
@@ -666,11 +675,9 @@ void Button::cutButtonText()
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
cutText = ellipsize_cjk_pref(this->text, contentW, "…"); // 全角省略号
|
cutText = ellipsize_cjk_pref(this->text, contentW, "…"); // 全角省略号
|
||||||
|
|
||||||
}
|
}
|
||||||
isUseCutText = true;
|
isUseCutText = true;
|
||||||
needCutText = false;
|
needCutText = false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Button::hideTooltip()
|
void Button::hideTooltip()
|
||||||
@@ -691,6 +698,3 @@ void Button::refreshTooltipTextForState()
|
|||||||
else if (mode == StellarX::ButtonMode::TOGGLE)
|
else if (mode == StellarX::ButtonMode::TOGGLE)
|
||||||
tipLabel.setText(click ? tipTextOn : tipTextOff);
|
tipLabel.setText(click ? tipTextOn : tipTextOff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
#include "Canvas.h"
|
#include "Canvas.h"
|
||||||
|
#include "SxLog.h"
|
||||||
|
|
||||||
|
static bool SxIsNoisyMsg(UINT m)
|
||||||
|
{
|
||||||
|
return m == WM_MOUSEMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
Canvas::Canvas()
|
Canvas::Canvas()
|
||||||
:Control(0, 0, 100, 100)
|
:Control(0, 0, 100, 100)
|
||||||
@@ -107,18 +113,39 @@ void Canvas::draw()
|
|||||||
bool Canvas::handleEvent(const ExMessage& msg)
|
bool Canvas::handleEvent(const ExMessage& msg)
|
||||||
{
|
{
|
||||||
if (!show) return false;
|
if (!show) return false;
|
||||||
|
|
||||||
bool consumed = false;
|
bool consumed = false;
|
||||||
bool anyDirty = false;
|
bool anyDirty = false;
|
||||||
|
Control* firstConsumer = nullptr;
|
||||||
|
|
||||||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||||||
{
|
{
|
||||||
consumed |= it->get()->handleEvent(msg);
|
Control* c = it->get();
|
||||||
if (it->get()->isDirty()) anyDirty = true;
|
bool cConsumed = c->handleEvent(msg);
|
||||||
|
|
||||||
|
if (cConsumed && !firstConsumer) firstConsumer = c;
|
||||||
|
consumed |= cConsumed;
|
||||||
|
|
||||||
|
if (c->isDirty()) anyDirty = true;
|
||||||
}
|
}
|
||||||
if (anyDirty) requestRepaint(parent);
|
|
||||||
|
if (firstConsumer && !SxIsNoisyMsg(msg.message))
|
||||||
|
{
|
||||||
|
SX_LOGD("Event") << SX_T("Canvas 消耗消息: ","Canvas consumed: msg=") << msg.message
|
||||||
|
<< SX_T("子控件"," by child")<<" id=" << firstConsumer->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyDirty)
|
||||||
|
{
|
||||||
|
if (!SxIsNoisyMsg(msg.message))
|
||||||
|
SX_LOGD("Dirty") << SX_T("Canvas检测有控件为脏状态 -> 请求重绘, ","Canvas anyDirty -> requestRepaint, ")<<"id = " << id;
|
||||||
|
requestRepaint(parent);
|
||||||
|
}
|
||||||
|
|
||||||
return consumed;
|
return consumed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Canvas::addControl(std::unique_ptr<Control> control)
|
void Canvas::addControl(std::unique_ptr<Control> control)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -126,6 +153,16 @@ void Canvas::addControl(std::unique_ptr<Control> control)
|
|||||||
control->setX(control->getLocalX() + this->x);
|
control->setX(control->getLocalX() + this->x);
|
||||||
control->setY(control->getLocalY() + this->y);
|
control->setY(control->getLocalY() + this->y);
|
||||||
control->setParent(this);
|
control->setParent(this);
|
||||||
|
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));
|
controls.push_back(std::move(control));
|
||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
@@ -175,7 +212,6 @@ void Canvas::setCanvasLineStyle(StellarX::LineStyle style)
|
|||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Canvas::setLinewidth(int width)
|
void Canvas::setLinewidth(int width)
|
||||||
{
|
{
|
||||||
this->canvaslinewidth = width;
|
this->canvaslinewidth = width;
|
||||||
@@ -189,7 +225,6 @@ void Canvas::setIsVisible(bool visible)
|
|||||||
for (auto& control : controls)
|
for (auto& control : controls)
|
||||||
{
|
{
|
||||||
control->setIsVisible(visible);
|
control->setIsVisible(visible);
|
||||||
control->setDirty(true);
|
|
||||||
}
|
}
|
||||||
if (!visible)
|
if (!visible)
|
||||||
this->updateBackground();
|
this->updateBackground();
|
||||||
@@ -377,15 +412,35 @@ void Canvas::requestRepaint(Control* parent)
|
|||||||
{
|
{
|
||||||
if (this == parent)
|
if (this == parent)
|
||||||
{
|
{
|
||||||
|
if (!show)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 关键护栏:
|
||||||
|
// - Canvas 自己是脏的 / 没有快照 / 缓存图为空
|
||||||
|
// => 禁止局部重绘,直接升级为一次完整 draw(先把 dirty 置真,避免 draw() 早退)
|
||||||
|
if (dirty || !hasSnap || !saveBkImage)
|
||||||
|
{
|
||||||
|
SX_LOGD("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_LOGD("Dirty") << SX_T("Canvas 请求局部重绘:id=", "Canvas::requestRepaint(partial): id=") << id;
|
||||||
|
|
||||||
for (auto& control : controls)
|
for (auto& control : controls)
|
||||||
if (control->isDirty() && control->IsVisible())
|
if (control->isDirty() && control->IsVisible())
|
||||||
control->draw();
|
control->draw();
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
SX_LOGD("Dirty") << SX_T("Canvas 请求根级重绘:id=", "Canvas::requestRepaint(root): id=") << id;
|
||||||
onRequestRepaintAsRoot();
|
onRequestRepaintAsRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "Control.h"
|
#include "Control.h"
|
||||||
|
#include "SxLog.h"
|
||||||
#include<assert.h>
|
#include<assert.h>
|
||||||
|
|
||||||
StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text)
|
StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text)
|
||||||
@@ -44,12 +45,34 @@ bool StellarX::ControlText::operator!=(const ControlText& text)
|
|||||||
}
|
}
|
||||||
void Control::setIsVisible(bool show)
|
void Control::setIsVisible(bool show)
|
||||||
{
|
{
|
||||||
if (!show)
|
SX_LOGD("Control") << SX_T("重置可见状态: id=", "setIsVisible: id=")
|
||||||
this->updateBackground();
|
<< id
|
||||||
|
<< " show=" << (show ? 1 : 0);
|
||||||
|
|
||||||
|
if (this->show == show)
|
||||||
|
return;
|
||||||
|
|
||||||
this->show = show;
|
this->show = show;
|
||||||
|
this->dirty = true;
|
||||||
|
|
||||||
|
if (!show)
|
||||||
|
{
|
||||||
|
// 隐藏:擦除自己在屏幕上的内容,并释放快照
|
||||||
|
this->updateBackground();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 显示:不在这里 requestRepaint(避免父容器快照未就绪时子控件抢跑 draw,污染快照)
|
||||||
|
// 仅向上标脏,让事件收口阶段由容器统一重绘。
|
||||||
|
if (parent)
|
||||||
|
parent->setDirty(true);
|
||||||
|
}
|
||||||
|
|
||||||
void Control::onWindowResize()
|
void Control::onWindowResize()
|
||||||
{
|
{
|
||||||
|
SX_LOGD("Layout") << SX_T("尺寸变化:id=", "onWindowResize: id=") << id
|
||||||
|
<< SX_T(" -> 丢背景快照 + 标脏", " -> discardSnap + dirty");
|
||||||
|
|
||||||
// 自己:丢快照 + 标脏
|
// 自己:丢快照 + 标脏
|
||||||
discardBackground();
|
discardBackground();
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
@@ -100,12 +123,34 @@ void Control::restoreStyle()
|
|||||||
|
|
||||||
void Control::requestRepaint(Control* parent)
|
void Control::requestRepaint(Control* parent)
|
||||||
{
|
{
|
||||||
if (parent) parent->requestRepaint(parent); // 向上冒泡
|
// 说明:
|
||||||
else onRequestRepaintAsRoot(); // 到根控件/窗口兜底
|
// - 常规路径:子控件调用 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_LOGD("Dirty") << SX_T("请求重绘:id=","requestRepaint: id=") << id << " parent=" << (parent ? parent->getId() : "null");
|
||||||
|
|
||||||
|
if (parent) parent->requestRepaint(parent); // 交给容器处理(容器可局部重绘)
|
||||||
|
else onRequestRepaintAsRoot(); // 根兜底
|
||||||
}
|
}
|
||||||
|
|
||||||
void Control::onRequestRepaintAsRoot()
|
void Control::onRequestRepaintAsRoot()
|
||||||
{
|
{
|
||||||
|
SX_LOGI("Dirty")
|
||||||
|
<< SX_T("触发根重绘:id=", "onRequestRepaintAsRoot: id=") << id
|
||||||
|
<< SX_T("(从根节点开始重画)", " (root repaint)");
|
||||||
|
|
||||||
|
|
||||||
discardBackground();
|
discardBackground();
|
||||||
setDirty(true);
|
setDirty(true);
|
||||||
@@ -122,9 +167,13 @@ void Control::saveBackground(int x, int y, int w, int h)
|
|||||||
//尺寸变了才重建,避免反复 new/delete
|
//尺寸变了才重建,避免反复 new/delete
|
||||||
if (saveBkImage->getwidth() != w || saveBkImage->getheight() != h)
|
if (saveBkImage->getwidth() != w || saveBkImage->getheight() != h)
|
||||||
{
|
{
|
||||||
|
SX_LOGD("Snap") <<SX_T("重新保存背景快照:id=", "saveBackground rebuild: id=") << id << " size=(" << w << "x" << h << ")";
|
||||||
|
|
||||||
delete saveBkImage; saveBkImage = nullptr;
|
delete saveBkImage; saveBkImage = nullptr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
SX_LOGD("Snap") << SX_T("保存背景快照:id=", "saveBackground rebuild: id=") << id << " size=(" << w << "x" << h << ")";
|
||||||
if (!saveBkImage) saveBkImage = new IMAGE(w, h);
|
if (!saveBkImage) saveBkImage = new IMAGE(w, h);
|
||||||
|
|
||||||
SetWorkingImage(nullptr); // ★抓屏幕
|
SetWorkingImage(nullptr); // ★抓屏幕
|
||||||
@@ -145,6 +194,7 @@ void Control::discardBackground()
|
|||||||
if (saveBkImage)
|
if (saveBkImage)
|
||||||
{
|
{
|
||||||
restBackground();
|
restBackground();
|
||||||
|
SX_LOGD("Snap") << SX_T("丢弃背景快照:id=","discardBackground: id=") << id << " hasSnap=" << (hasSnap ? 1 : 0);
|
||||||
delete saveBkImage;
|
delete saveBkImage;
|
||||||
saveBkImage = nullptr;
|
saveBkImage = nullptr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "Dialog.h"
|
#include "Dialog.h"
|
||||||
|
#include "SxLog.h"
|
||||||
|
|
||||||
Dialog::Dialog(Window& h, std::string text, std::string message, StellarX::MessageBoxType type, bool modal)
|
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)
|
: Canvas(), message(message), type(type), modal(modal), hWnd(h), titleText(text)
|
||||||
@@ -7,8 +8,6 @@ Dialog::Dialog(Window& h,std::string text,std::string message, StellarX::Message
|
|||||||
show = false;
|
show = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Dialog::~Dialog()
|
Dialog::~Dialog()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -51,7 +50,6 @@ void Dialog::draw()
|
|||||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||||
|
|
||||||
|
|
||||||
int ty = y + closeButtonHeight + titleToTextMargin; // 文本起始Y坐标
|
int ty = y + closeButtonHeight + titleToTextMargin; // 文本起始Y坐标
|
||||||
for (auto& line : lines)
|
for (auto& line : lines)
|
||||||
{
|
{
|
||||||
@@ -67,8 +65,6 @@ void Dialog::draw()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bool Dialog::handleEvent(const ExMessage& msg)
|
bool Dialog::handleEvent(const ExMessage& msg)
|
||||||
{
|
{
|
||||||
bool consume = false;
|
bool consume = false;
|
||||||
@@ -134,7 +130,6 @@ void Dialog::SetModal(bool modal)
|
|||||||
this->modal = modal;
|
this->modal = modal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Dialog::SetResult(StellarX::MessageBoxResult result)
|
void Dialog::SetResult(StellarX::MessageBoxResult result)
|
||||||
{
|
{
|
||||||
this->result = result;
|
this->result = result;
|
||||||
@@ -154,6 +149,7 @@ void Dialog::Show()
|
|||||||
{
|
{
|
||||||
if (pendingCleanup)
|
if (pendingCleanup)
|
||||||
performDelayedCleanup();
|
performDelayedCleanup();
|
||||||
|
SX_LOGI("Dialog") << SX_T("对话框弹出:是否模态=","Dialog::Show: modal=") << (modal ? 1 : 0);
|
||||||
|
|
||||||
show = true;
|
show = true;
|
||||||
dirty = true;
|
dirty = true;
|
||||||
@@ -184,6 +180,7 @@ void Dialog::Show()
|
|||||||
{
|
{
|
||||||
lastW = cw;
|
lastW = cw;
|
||||||
lastH = ch;
|
lastH = ch;
|
||||||
|
SX_LOGD("Resize") <<SX_T("模态对话框检测到窗口大小变化:(", "Modal dialog detected window size change: (") << cw << "x" << ch << ")";
|
||||||
|
|
||||||
// 通知父窗口:有新尺寸 → 标记 needResizeDirty
|
// 通知父窗口:有新尺寸 → 标记 needResizeDirty
|
||||||
hWnd.scheduleResizeFromModal(cw, ch);
|
hWnd.scheduleResizeFromModal(cw, ch);
|
||||||
@@ -238,8 +235,6 @@ void Dialog::Show()
|
|||||||
dirty = true;
|
dirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void Dialog::Close()
|
void Dialog::Close()
|
||||||
{
|
{
|
||||||
if (!show) return;
|
if (!show) return;
|
||||||
@@ -249,12 +244,9 @@ void Dialog::Close()
|
|||||||
dirty = true;
|
dirty = true;
|
||||||
pendingCleanup = true; // 只标记需要清理,不立即执行
|
pendingCleanup = true; // 只标记需要清理,不立即执行
|
||||||
|
|
||||||
|
|
||||||
// 工厂模式下非模态触发回调 返回结果
|
// 工厂模式下非模态触发回调 返回结果
|
||||||
if (resultCallback && !modal)
|
if (resultCallback && !modal)
|
||||||
resultCallback(this->result);
|
resultCallback(this->result);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Dialog::setInitialization(bool init)
|
void Dialog::setInitialization(bool init)
|
||||||
@@ -267,7 +259,6 @@ void Dialog::setInitialization(bool init)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Dialog::initButtons()
|
void Dialog::initButtons()
|
||||||
{
|
{
|
||||||
controls.clear();
|
controls.clear();
|
||||||
@@ -391,7 +382,6 @@ void Dialog::initButtons()
|
|||||||
noButton->textStyle = this->textStyle;
|
noButton->textStyle = this->textStyle;
|
||||||
cancelButton->textStyle = this->textStyle;
|
cancelButton->textStyle = this->textStyle;
|
||||||
|
|
||||||
|
|
||||||
this->addControl(std::move(yesButton));
|
this->addControl(std::move(yesButton));
|
||||||
this->addControl(std::move(noButton));
|
this->addControl(std::move(noButton));
|
||||||
this->addControl(std::move(cancelButton));
|
this->addControl(std::move(cancelButton));
|
||||||
@@ -717,7 +707,6 @@ void Dialog::requestRepaint(Control* parent)
|
|||||||
for (auto& control : controls)
|
for (auto& control : controls)
|
||||||
if (control->isDirty() && control->IsVisible())
|
if (control->isDirty() && control->IsVisible())
|
||||||
control->draw();
|
control->draw();
|
||||||
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
onRequestRepaintAsRoot();
|
onRequestRepaintAsRoot();
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
#include "MessageBox.h"
|
#include "MessageBox.h"
|
||||||
|
#include "SxLog.h"
|
||||||
namespace StellarX
|
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)
|
MessageBoxType type)
|
||||||
{
|
{
|
||||||
Dialog dlg(wnd, caption, text, type, true); // 模态
|
Dialog dlg(wnd, caption, text, type, true); // 模态
|
||||||
|
SX_LOGI("MessageBox") << "show: Message=" << dlg.GetText()
|
||||||
|
<< " modal=" << (dlg.model() ? 1 : 0);
|
||||||
|
|
||||||
dlg.setInitialization(true);
|
dlg.setInitialization(true);
|
||||||
dlg.Show();
|
dlg.Show();
|
||||||
return dlg.GetResult();
|
return dlg.GetResult();
|
||||||
@@ -22,6 +25,8 @@ namespace StellarX
|
|||||||
}
|
}
|
||||||
auto dlg = std::make_unique<Dialog>(wnd, caption, text,
|
auto dlg = std::make_unique<Dialog>(wnd, caption, text,
|
||||||
type, false); // 非模态
|
type, false); // 非模态
|
||||||
|
SX_LOGI("MessageBox") << "show: Message=" << dlg->GetText()
|
||||||
|
<< " modal=" << (dlg->model() ? 1 : 0);
|
||||||
Dialog* dlgPtr = dlg.get();
|
Dialog* dlgPtr = dlg.get();
|
||||||
dlgPtr->setInitialization(true);
|
dlgPtr->setInitialization(true);
|
||||||
// 设置回调
|
// 设置回调
|
||||||
|
|||||||
446
src/SxLog.cpp
Normal file
446
src/SxLog.cpp
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
#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 与 POSIX(localtime_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 调用造成噪声与性能浪费
|
||||||
|
//
|
||||||
|
// 注意:
|
||||||
|
// - 下面原注释写“切到 UTF-8”,但实际命令是 chcp 936(GBK)
|
||||||
|
// - 为避免改动你原注释,这里补充说明事实,保持行为不变
|
||||||
|
void SxLogger::setGBK()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
static std::once_flag once;
|
||||||
|
std::call_once(once, []() {
|
||||||
|
// 切到 UTF-8,避免中文日志在 CP936 控制台下乱码
|
||||||
|
// 说明:这不是 WinAPI;是执行系统命令
|
||||||
|
std::system("chcp 936 >nul");
|
||||||
|
|
||||||
|
// 补充说明:
|
||||||
|
// - chcp 936 实际是设置为 CP936(GBK)
|
||||||
|
// - 如果你的终端本身是 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
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "TabControl.h"
|
#include "TabControl.h"
|
||||||
|
#include "SxLog.h"
|
||||||
inline void TabControl::initTabBar()
|
inline void TabControl::initTabBar()
|
||||||
{
|
{
|
||||||
if (controls.empty())return;
|
if (controls.empty())return;
|
||||||
@@ -173,7 +173,6 @@ void TabControl::setX(int x)
|
|||||||
c.first->onWindowResize();
|
c.first->onWindowResize();
|
||||||
c.second->onWindowResize();
|
c.second->onWindowResize();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabControl::setY(int y)
|
void TabControl::setY(int y)
|
||||||
@@ -192,26 +191,6 @@ void TabControl::setY(int y)
|
|||||||
void TabControl::draw()
|
void TabControl::draw()
|
||||||
{
|
{
|
||||||
if (!dirty || !show)return;
|
if (!dirty || !show)return;
|
||||||
// // 在绘制 TabControl 之前,先恢复并更新背景快照:
|
|
||||||
//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)
|
|
||||||
// {
|
|
||||||
// discardBackground();
|
|
||||||
// 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();
|
|
||||||
// 绘制画布背景和基本形状及其子画布控件
|
// 绘制画布背景和基本形状及其子画布控件
|
||||||
Canvas::draw();
|
Canvas::draw();
|
||||||
for (auto& c : controls)
|
for (auto& c : controls)
|
||||||
@@ -224,8 +203,17 @@ void TabControl::draw()
|
|||||||
c.second->setDirty(true);
|
c.second->setDirty(true);
|
||||||
c.second->draw();
|
c.second->draw();
|
||||||
}
|
}
|
||||||
dirty = false;
|
|
||||||
|
|
||||||
|
// 首次绘制时处理默认激活页签
|
||||||
|
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)
|
bool TabControl::handleEvent(const ExMessage& msg)
|
||||||
@@ -262,22 +250,36 @@ void TabControl::add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>
|
|||||||
|
|
||||||
controls[idx].first->setOnToggleOnListener([this, idx]()
|
controls[idx].first->setOnToggleOnListener([this, idx]()
|
||||||
{
|
{
|
||||||
controls[idx].second->setIsVisible(true);
|
int prevIdx = -1;
|
||||||
controls[idx].second->onWindowResize();
|
for (size_t i = 0; i < controls.size(); ++i)
|
||||||
|
{
|
||||||
|
if (controls[i].second->IsVisible())
|
||||||
|
{
|
||||||
|
prevIdx = (int)i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
for (auto& tab : controls)
|
for (auto& tab : controls)
|
||||||
{
|
{
|
||||||
if (tab.first->getButtonText() != controls[idx].first->getButtonText())
|
if (tab.first->getButtonText() != controls[idx].first->getButtonText() && tab.first->isClicked())
|
||||||
{
|
|
||||||
tab.first->setButtonClick(false);
|
tab.first->setButtonClick(false);
|
||||||
tab.second->setIsVisible(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;
|
dirty = true;
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
controls[idx].first->setOnToggleOffListener([this, idx]()
|
controls[idx].first->setOnToggleOffListener([this, idx]()
|
||||||
{
|
{
|
||||||
controls[idx].second->setIsVisible(false);
|
SX_LOGI("Tab") << SX_T("关闭选项卡:id=","deactivate tab: idx=") << (int)idx
|
||||||
|
<< " text=" << controls[idx].first->getButtonText();
|
||||||
|
|
||||||
|
controls[idx].second->setIsVisible(false);
|
||||||
dirty = true;
|
dirty = true;
|
||||||
});
|
});
|
||||||
controls[idx].second->setParent(this);
|
controls[idx].second->setParent(this);
|
||||||
@@ -298,7 +300,6 @@ void TabControl::add(std::string tabText, std::unique_ptr<Control> control)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabControl::setTabPlacement(StellarX::TabPlacement placement)
|
void TabControl::setTabPlacement(StellarX::TabPlacement placement)
|
||||||
@@ -307,7 +308,6 @@ void TabControl::setTabPlacement(StellarX::TabPlacement placement)
|
|||||||
setDirty(true);
|
setDirty(true);
|
||||||
initTabBar();
|
initTabBar();
|
||||||
initTabPage();
|
initTabPage();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabControl::setTabBarHeight(int height)
|
void TabControl::setTabBarHeight(int height)
|
||||||
@@ -321,16 +321,25 @@ void TabControl::setTabBarHeight(int height)
|
|||||||
void TabControl::setIsVisible(bool visible)
|
void TabControl::setIsVisible(bool visible)
|
||||||
{
|
{
|
||||||
// 先让基类 Canvas 处理自己的回贴/丢快照逻辑
|
// 先让基类 Canvas 处理自己的回贴/丢快照逻辑
|
||||||
Canvas::setIsVisible(visible); // <--- 新增
|
Canvas::setIsVisible(visible);
|
||||||
|
|
||||||
this->show = visible;
|
|
||||||
for (auto& tab : controls)
|
for (auto& tab : controls)
|
||||||
|
{
|
||||||
|
if(true == visible)
|
||||||
{
|
{
|
||||||
tab.first->setIsVisible(visible);
|
tab.first->setIsVisible(visible);
|
||||||
//页也要跟着关/开,否则它们会保留旧的 saveBkImage
|
//页也要跟着关/开,否则它们会保留旧的 saveBkImage
|
||||||
tab.second->setIsVisible(visible);
|
if (tab.first->isClicked())
|
||||||
|
tab.second->setIsVisible(true);
|
||||||
|
else
|
||||||
|
tab.second->setIsVisible(false);
|
||||||
tab.second->setDirty(true);
|
tab.second->setDirty(true);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tab.first->setIsVisible(visible);
|
||||||
|
tab.second->setIsVisible(visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TabControl::onWindowResize()
|
void TabControl::onWindowResize()
|
||||||
@@ -364,20 +373,13 @@ int TabControl::getActiveIndex() const
|
|||||||
|
|
||||||
void TabControl::setActiveIndex(int idx)
|
void TabControl::setActiveIndex(int idx)
|
||||||
{
|
{
|
||||||
if (idx < 0 || idx > controls.size() - 1) return;
|
if (IsFirstDraw)
|
||||||
if (controls[idx].first->getButtonMode() == StellarX::ButtonMode::DISABLED)return;
|
defaultActivation = idx;
|
||||||
if (controls[idx].first->isClicked())
|
|
||||||
{
|
|
||||||
if (controls[idx].second->IsVisible())
|
|
||||||
return;
|
|
||||||
else
|
|
||||||
controls[idx].second->setIsVisible(true);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (idx >= 0 && idx < controls.size())
|
||||||
controls[idx].first->setButtonClick(true);
|
controls[idx].first->setButtonClick(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int TabControl::count() const
|
int TabControl::count() const
|
||||||
@@ -416,7 +418,7 @@ void TabControl::requestRepaint(Control* parent)
|
|||||||
{
|
{
|
||||||
if (control.first->isDirty() && control.first->IsVisible())
|
if (control.first->isDirty() && control.first->IsVisible())
|
||||||
control.first->draw();
|
control.first->draw();
|
||||||
else if (control.second->isDirty()&&control.second->IsVisible())
|
if (control.second->isDirty() && control.second->IsVisible())
|
||||||
control.second->draw();
|
control.second->draw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,5 +71,4 @@ void Label::setText(std::string text)
|
|||||||
{
|
{
|
||||||
this->text = text;
|
this->text = text;
|
||||||
this->dirty = true;
|
this->dirty = true;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "Table.h"
|
#include "Table.h"
|
||||||
|
#include "SxLog.h"
|
||||||
// 绘制表格的当前页
|
// 绘制表格的当前页
|
||||||
// 使用双循环绘制行和列,考虑分页偏移
|
// 使用双循环绘制行和列,考虑分页偏移
|
||||||
void Table::drawTable()
|
void Table::drawTable()
|
||||||
@@ -26,12 +27,10 @@ void Table::drawTable()
|
|||||||
dY = uY;
|
dY = uY;
|
||||||
uY = dY + lineHeights.at(0) + TABLE_ROW_EXTRA;
|
uY = dY + lineHeights.at(0) + TABLE_ROW_EXTRA;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Table::drawHeader()
|
void Table::drawHeader()
|
||||||
{
|
{
|
||||||
|
|
||||||
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
|
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
|
||||||
// 内容区原点 = x+border, y+border
|
// 内容区原点 = x+border, y+border
|
||||||
dX = x + border;
|
dX = x + border;
|
||||||
@@ -132,7 +131,6 @@ void Table::initButton()
|
|||||||
int nextW = textwidth(LPCTSTR(TABLE_STR_NEXT)) + padH * 2;
|
int nextW = textwidth(LPCTSTR(TABLE_STR_NEXT)) + padH * 2;
|
||||||
int btnH = lblH + padV * 2;
|
int btnH = lblH + padV * 2;
|
||||||
|
|
||||||
|
|
||||||
// 基于“页码标签”的矩形来摆放:
|
// 基于“页码标签”的矩形来摆放:
|
||||||
// prev 在页码左侧 gap 处;next 在右侧 gap 处;Y 对齐 pY
|
// prev 在页码左侧 gap 处;next 在右侧 gap 处;Y 对齐 pY
|
||||||
int prevX = pX - gap - prevW;
|
int prevX = pX - gap - prevW;
|
||||||
@@ -162,18 +160,33 @@ void Table::initButton()
|
|||||||
|
|
||||||
prevButton->setOnClickListener([this]()
|
prevButton->setOnClickListener([this]()
|
||||||
{
|
{
|
||||||
|
int oldPage = currentPage;
|
||||||
if (currentPage > 1)
|
if (currentPage > 1)
|
||||||
{
|
{
|
||||||
--currentPage;
|
--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;
|
dirty = true;
|
||||||
if (pageNum) pageNum->setDirty(true);
|
if (pageNum) pageNum->setDirty(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
nextButton->setOnClickListener([this]()
|
nextButton->setOnClickListener([this]()
|
||||||
{
|
{
|
||||||
|
int oldPage = currentPage;
|
||||||
if (currentPage < totalPages)
|
if (currentPage < totalPages)
|
||||||
{
|
{
|
||||||
++currentPage;
|
++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;
|
dirty = true;
|
||||||
if (pageNum) pageNum->setDirty(true);
|
if (pageNum) pageNum->setDirty(true);
|
||||||
}
|
}
|
||||||
@@ -216,7 +229,6 @@ void Table::initPageNum()
|
|||||||
|
|
||||||
void Table::drawPageNum()
|
void Table::drawPageNum()
|
||||||
{
|
{
|
||||||
|
|
||||||
pageNumtext = "第";
|
pageNumtext = "第";
|
||||||
pageNumtext += std::to_string(currentPage);
|
pageNumtext += std::to_string(currentPage);
|
||||||
pageNumtext += "页/共";
|
pageNumtext += "页/共";
|
||||||
@@ -229,7 +241,6 @@ void Table::drawPageNum()
|
|||||||
if (StellarX::FillMode::Null == tableFillMode)
|
if (StellarX::FillMode::Null == tableFillMode)
|
||||||
pageNum->setTextdisap(true);
|
pageNum->setTextdisap(true);
|
||||||
pageNum->draw();
|
pageNum->draw();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Table::drawButton()
|
void Table::drawButton()
|
||||||
@@ -247,7 +258,6 @@ void Table::drawButton()
|
|||||||
this->nextButton->setDirty(true);
|
this->nextButton->setDirty(true);
|
||||||
prevButton->draw();
|
prevButton->draw();
|
||||||
nextButton->draw();
|
nextButton->draw();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Table::setX(int x)
|
void Table::setX(int x)
|
||||||
@@ -283,7 +293,8 @@ void Table::setWidth(int width)
|
|||||||
if (remainder > 0) {
|
if (remainder > 0) {
|
||||||
change += 1;
|
change += 1;
|
||||||
remainder -= 1;
|
remainder -= 1;
|
||||||
} else if (remainder < 0) {
|
}
|
||||||
|
else if (remainder < 0) {
|
||||||
change -= 1;
|
change -= 1;
|
||||||
remainder += 1;
|
remainder += 1;
|
||||||
}
|
}
|
||||||
@@ -399,10 +410,8 @@ void Table::draw()
|
|||||||
restBackground();
|
restBackground();
|
||||||
// 绘制表头
|
// 绘制表头
|
||||||
|
|
||||||
dX = x;
|
//if (!headers.empty())
|
||||||
dY = y;
|
|
||||||
drawHeader();
|
drawHeader();
|
||||||
this->isNeedDrawHeaders = false;
|
|
||||||
// 绘制当前页
|
// 绘制当前页
|
||||||
drawTable();
|
drawTable();
|
||||||
// 绘制页码标签
|
// 绘制页码标签
|
||||||
@@ -412,7 +421,6 @@ void Table::draw()
|
|||||||
if (this->isShowPageButton)
|
if (this->isShowPageButton)
|
||||||
drawButton();
|
drawButton();
|
||||||
|
|
||||||
|
|
||||||
// 恢复绘图状态
|
// 恢复绘图状态
|
||||||
restoreStyle();
|
restoreStyle();
|
||||||
dirty = false; // 标记不需要重绘
|
dirty = false; // 标记不需要重绘
|
||||||
@@ -441,24 +449,35 @@ void Table::setHeaders(std::initializer_list<std::string> headers)
|
|||||||
this->headers.clear();
|
this->headers.clear();
|
||||||
for (auto& lis : headers)
|
for (auto& lis : headers)
|
||||||
this->headers.push_back(lis);
|
this->headers.push_back(lis);
|
||||||
|
SX_LOGI("Table") << SX_T("设置表头:id=","setHeaders: id=") << id << SX_T("总数="," count=") << (int)this->headers.size();
|
||||||
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
||||||
isNeedDrawHeaders = true; // 标记需要重新绘制表头
|
isNeedDrawHeaders = true; // 标记需要重新绘制表头
|
||||||
dirty = true;
|
dirty = true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Table::setData(std::vector<std::string> data)
|
void Table::setData(std::vector<std::string> data)
|
||||||
{
|
{
|
||||||
if (data.size() < headers.size())
|
while (data.size() < headers.size())
|
||||||
for (int i = 0; data.size() <= headers.size(); i++)
|
|
||||||
data.push_back("");
|
data.push_back("");
|
||||||
|
|
||||||
this->data.push_back(data);
|
this->data.push_back(data);
|
||||||
|
|
||||||
totalPages = ((int)this->data.size() + rowsPerPage - 1) / rowsPerPage;
|
totalPages = ((int)this->data.size() + rowsPerPage - 1) / rowsPerPage;
|
||||||
if (totalPages < 1)
|
if (totalPages < 1)
|
||||||
totalPages = 1;
|
totalPages = 1;
|
||||||
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
|
||||||
|
isNeedCellSize = true;
|
||||||
dirty = true;
|
dirty = true;
|
||||||
|
|
||||||
|
SX_LOGI("Table")
|
||||||
|
<< SX_T("新增Data:id=", "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)
|
void Table::setData(std::initializer_list<std::vector<std::string>> data)
|
||||||
{
|
{
|
||||||
for (auto lis : data)
|
for (auto lis : data)
|
||||||
@@ -476,6 +495,13 @@ void Table::setData( std::initializer_list<std::vector<std::string>> data)
|
|||||||
totalPages = 1;
|
totalPages = 1;
|
||||||
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
||||||
dirty = true;
|
dirty = true;
|
||||||
|
SX_LOGI("Table")
|
||||||
|
<< SX_T("新增Data:id=", "appendRow: id=") << id
|
||||||
|
<< SX_T(" 本行列数=", " cols=") << (int)data.size()
|
||||||
|
<< SX_T(" 数据总行数=", " totalRows=") << (int)this->data.size()
|
||||||
|
<< SX_T(" 总页数=", " totalPages=") << totalPages;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Table::setRowsPerPage(int rows)
|
void Table::setRowsPerPage(int rows)
|
||||||
@@ -538,6 +564,34 @@ void Table::setTableBorderWidth(int width)
|
|||||||
this->dirty = true;
|
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 = 1;
|
||||||
|
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
||||||
|
isNeedButtonAndPageNum = true;// 标记需要重新计算翻页按钮和页码信息
|
||||||
|
dirty = true;
|
||||||
|
SX_LOGI("Table") << SX_T("清除表格数据:id=","clearData: id=") << id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Table::resetTable()
|
||||||
|
{
|
||||||
|
clearHeaders();
|
||||||
|
clearData();
|
||||||
|
}
|
||||||
|
|
||||||
void Table::onWindowResize()
|
void Table::onWindowResize()
|
||||||
{
|
{
|
||||||
Control::onWindowResize(); // 先处理自己
|
Control::onWindowResize(); // 先处理自己
|
||||||
@@ -616,5 +670,3 @@ int Table::getTableHeight() const
|
|||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// TextBox.cpp
|
// TextBox.cpp
|
||||||
#include "TextBox.h"
|
#include "TextBox.h"
|
||||||
|
#include "SxLog.h"
|
||||||
|
|
||||||
TextBox::TextBox(int x, int y, int width, int height, std::string text, StellarX::TextBoxmode mode, StellarX::ControlShape shape)
|
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)
|
:Control(x, y, width, height), text(text), mode(mode), shape(shape)
|
||||||
@@ -25,8 +25,22 @@ void TextBox::draw()
|
|||||||
|
|
||||||
settextcolor(textStyle.color);
|
settextcolor(textStyle.color);
|
||||||
setbkmode(TRANSPARENT);
|
setbkmode(TRANSPARENT);
|
||||||
int text_width = textwidth(LPCTSTR(text.c_str()));
|
|
||||||
int text_height = textheight(LPCTSTR(text.c_str()));
|
int text_width = 0;
|
||||||
|
int text_height = 0;
|
||||||
|
std::string pwdText;
|
||||||
|
if (StellarX::TextBoxmode::PASSWORD_MODE == mode)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < text.size(); ++i)
|
||||||
|
pwdText += '*';
|
||||||
|
text_width = textwidth(LPCTSTR(pwdText.c_str()));
|
||||||
|
text_height = textheight(LPCTSTR(pwdText.c_str()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
text_width = textwidth(LPCTSTR(text.c_str()));
|
||||||
|
text_height = textheight(LPCTSTR(text.c_str()));
|
||||||
|
}
|
||||||
|
|
||||||
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage)
|
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage)
|
||||||
saveBackground(this->x, this->y, this->width, this->height);
|
saveBackground(this->x, this->y, this->width, this->height);
|
||||||
@@ -37,29 +51,34 @@ void TextBox::draw()
|
|||||||
{
|
{
|
||||||
case StellarX::ControlShape::RECTANGLE:
|
case StellarX::ControlShape::RECTANGLE:
|
||||||
fillrectangle(x, y, x + width, y + height);//有边框填充矩形
|
fillrectangle(x, y, x + width, y + height);//有边框填充矩形
|
||||||
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
StellarX::TextBoxmode::PASSWORD_MODE == mode ? outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(pwdText.c_str()))
|
||||||
|
: outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||||
break;
|
break;
|
||||||
case StellarX::ControlShape::B_RECTANGLE:
|
case StellarX::ControlShape::B_RECTANGLE:
|
||||||
solidrectangle(x, y, x + width, y + height);//无边框填充矩形
|
solidrectangle(x, y, x + width, y + height);//无边框填充矩形
|
||||||
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
StellarX::TextBoxmode::PASSWORD_MODE == mode ? outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(pwdText.c_str()))
|
||||||
|
: outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||||
break;
|
break;
|
||||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||||
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//有边框填充圆角矩形
|
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()));
|
StellarX::TextBoxmode::PASSWORD_MODE == mode ? outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(pwdText.c_str()))
|
||||||
|
:outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||||
break;
|
break;
|
||||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||||
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//无边框填充圆角矩形
|
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()));
|
StellarX::TextBoxmode::PASSWORD_MODE == mode ? outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(pwdText.c_str()))
|
||||||
|
:outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
restoreStyle();
|
restoreStyle();
|
||||||
dirty = false; //标记不需要重绘
|
dirty = false; //标记不需要重绘
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TextBox::handleEvent(const ExMessage& msg)
|
bool TextBox::handleEvent(const ExMessage& msg)
|
||||||
{
|
{
|
||||||
|
if (!show) return false;
|
||||||
|
|
||||||
bool hover = false;
|
bool hover = false;
|
||||||
bool oldClick = click;
|
bool oldClick = click;
|
||||||
bool consume = false;
|
bool consume = false;
|
||||||
@@ -70,20 +89,25 @@ bool TextBox::handleEvent(const ExMessage& msg)
|
|||||||
case StellarX::ControlShape::B_RECTANGLE:
|
case StellarX::ControlShape::B_RECTANGLE:
|
||||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||||
hover = (msg.x > x && msg.x < (x + width) && msg.y > y && msg.y < (y + height));//判断鼠标是否在矩形按钮内
|
hover = (msg.x > x && msg.x < (x + width) && msg.y > y && msg.y < (y + height));
|
||||||
consume = false;
|
break;
|
||||||
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hover && msg.message == WM_LBUTTONUP)
|
if (hover && msg.message == WM_LBUTTONUP)
|
||||||
{
|
{
|
||||||
click = true;
|
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)
|
if (StellarX::TextBoxmode::INPUT_MODE == mode)
|
||||||
{
|
{
|
||||||
char* temp = new char[maxCharLen + 1];
|
char* temp = new char[maxCharLen + 1];
|
||||||
dirty = InputBox(temp, (int)maxCharLen, "输入框", NULL, text.c_str(), NULL, NULL, false);
|
dirty = InputBox(temp, (int)maxCharLen + 1, "输入框", NULL, text.c_str(), NULL, NULL, false);
|
||||||
if (dirty) text = temp;
|
if (dirty) text = temp;
|
||||||
delete[] temp;
|
delete[] temp;
|
||||||
temp = nullptr;
|
|
||||||
consume = true;
|
consume = true;
|
||||||
}
|
}
|
||||||
else if (StellarX::TextBoxmode::READONLY_MODE == mode)
|
else if (StellarX::TextBoxmode::READONLY_MODE == mode)
|
||||||
@@ -92,16 +116,39 @@ bool TextBox::handleEvent(const ExMessage& msg)
|
|||||||
InputBox(NULL, (int)maxCharLen, "输出框(输入无效!)", NULL, text.c_str(), NULL, NULL, false);
|
InputBox(NULL, (int)maxCharLen, "输出框(输入无效!)", NULL, text.c_str(), NULL, NULL, false);
|
||||||
consume = true;
|
consume = true;
|
||||||
}
|
}
|
||||||
|
else if (StellarX::TextBoxmode::PASSWORD_MODE == mode)
|
||||||
|
{
|
||||||
|
char* temp = new char[maxCharLen + 1];
|
||||||
|
// 不记录明文,只记录长度变化
|
||||||
|
dirty = InputBox(temp, (int)maxCharLen + 1, "输入框\n不可见输入,覆盖即可", NULL, NULL, NULL, NULL, false);
|
||||||
|
if (dirty) text = temp;
|
||||||
|
delete[] temp;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
flushmessage(EX_MOUSE | EX_KEY);
|
flushmessage(EX_MOUSE | EX_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dirty)
|
if (dirty)
|
||||||
requestRepaint(parent);
|
requestRepaint(parent);
|
||||||
|
|
||||||
if (click)
|
if (click)
|
||||||
click = false;
|
click = false;
|
||||||
|
|
||||||
return consume;
|
return consume;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void TextBox::setMode(StellarX::TextBoxmode mode)
|
void TextBox::setMode(StellarX::TextBoxmode mode)
|
||||||
{
|
{
|
||||||
this->mode = mode;
|
this->mode = mode;
|
||||||
@@ -154,12 +201,9 @@ void TextBox::setText(std::string text)
|
|||||||
text = text.substr(0, maxCharLen);
|
text = text.substr(0, maxCharLen);
|
||||||
this->text = text;
|
this->text = text;
|
||||||
this->dirty = true;
|
this->dirty = true;
|
||||||
draw();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string TextBox::getText() const
|
std::string TextBox::getText() const
|
||||||
{
|
{
|
||||||
return this->text;
|
return this->text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,29 @@
|
|||||||
#include "Window.h"
|
#include "Window.h"
|
||||||
#include "Dialog.h"
|
#include "Dialog.h"
|
||||||
|
#include"SxLog.h"
|
||||||
#include <easyx.h>
|
#include <easyx.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
static bool SxIsNoisyMsg(UINT m)
|
||||||
|
{
|
||||||
|
return m == WM_MOUSEMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* SxMsgName(UINT m)
|
||||||
|
{
|
||||||
|
switch (m)
|
||||||
|
{
|
||||||
|
case WM_MOUSEMOVE: return "WM_MOUSEMOVE";
|
||||||
|
case WM_LBUTTONDOWN: return "WM_LBUTTONDOWN";
|
||||||
|
case WM_LBUTTONUP: return "WM_LBUTTONUP";
|
||||||
|
case WM_RBUTTONDOWN: return "WM_RBUTTONDOWN";
|
||||||
|
case WM_RBUTTONUP: return "WM_RBUTTONUP";
|
||||||
|
case WM_KEYDOWN: return "WM_KEYDOWN";
|
||||||
|
case WM_KEYUP: return "WM_KEYUP";
|
||||||
|
case WM_CHAR: return "WM_CHAR";
|
||||||
|
case WM_SIZE: return "WM_SIZE";
|
||||||
|
default: return "WM_?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ApplyResizableStyle
|
* ApplyResizableStyle
|
||||||
@@ -154,6 +175,7 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
|
|||||||
// 关键点②:拉伸开始 → 冻结重绘(系统调整窗口矩形时不触发即时重绘,防止抖)
|
// 关键点②:拉伸开始 → 冻结重绘(系统调整窗口矩形时不触发即时重绘,防止抖)
|
||||||
if (m == WM_ENTERSIZEMOVE)
|
if (m == WM_ENTERSIZEMOVE)
|
||||||
{
|
{
|
||||||
|
SX_LOGI("Resize") << SX_T("WM_ENTERSIZEMOVE: 开始测量尺寸","WM_ENTERSIZEMOVE: begin sizing");
|
||||||
self->isSizing = true;
|
self->isSizing = true;
|
||||||
SendMessage(h, WM_SETREDRAW, FALSE, 0);
|
SendMessage(h, WM_SETREDRAW, FALSE, 0);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -163,7 +185,6 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
|
|||||||
if (m == WM_SIZING)
|
if (m == WM_SIZING)
|
||||||
{
|
{
|
||||||
RECT* prc = reinterpret_cast<RECT*>(l);
|
RECT* prc = reinterpret_cast<RECT*>(l);
|
||||||
|
|
||||||
// “尺寸异常值”快速过滤:仅保护极端值,不影响正常拖动
|
// “尺寸异常值”快速过滤:仅保护极端值,不影响正常拖动
|
||||||
int currentWidth = prc->right - prc->left;
|
int currentWidth = prc->right - prc->left;
|
||||||
int currentHeight = prc->bottom - prc->top;
|
int currentHeight = prc->bottom - prc->top;
|
||||||
@@ -173,6 +194,14 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ApplyMinSizeOnSizing(prc, w, h, self->minClientW, self->minClientH);
|
ApplyMinSizeOnSizing(prc, w, h, self->minClientW, self->minClientH);
|
||||||
|
RECT before = *prc;// 记录调整前矩形以便日志输出
|
||||||
|
if (before.left != prc->left || before.top != prc->top || before.right != prc->right || before.bottom != prc->bottom)
|
||||||
|
{
|
||||||
|
SX_LOGD("Resize")
|
||||||
|
<< SX_T("WM_SIZING 夹具:","WM_SIZING clamp: ")
|
||||||
|
<< SX_T("之前=(","before=(") << (before.right - before.left) << "x" << (before.bottom - before.top) << ") "
|
||||||
|
<< SX_T("之后=(","after=(") << (prc->right - prc->left) << "x" << (prc->bottom - prc->top) << ")";
|
||||||
|
}
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +218,7 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
|
|||||||
self->pendingW = aw;
|
self->pendingW = aw;
|
||||||
self->pendingH = ah;
|
self->pendingH = ah;
|
||||||
self->needResizeDirty = true;
|
self->needResizeDirty = true;
|
||||||
|
SX_LOGI("Resize") << SX_T("WM_EXITSIZEMOVE: 最终尺寸,待重绘=(","WM_EXITSIZEMOVE: end sizing, pending=(" )<< self->pendingW << "x" << self->pendingH << "), needResizeDirty=1";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 结束拉伸后不立即执行重绘,待事件循环统一收口。
|
// 结束拉伸后不立即执行重绘,待事件循环统一收口。
|
||||||
@@ -341,6 +371,7 @@ int Window::runEventLoop()
|
|||||||
// 不再引入额外 pendingResize 等状态,避免分叉导致状态不一致。
|
// 不再引入额外 pendingResize 等状态,避免分叉导致状态不一致。
|
||||||
while (running)
|
while (running)
|
||||||
{
|
{
|
||||||
|
|
||||||
bool consume = false;
|
bool consume = false;
|
||||||
|
|
||||||
if (peekmessage(&msg, EX_MOUSE | EX_KEY | EX_WINDOW, true))
|
if (peekmessage(&msg, EX_MOUSE | EX_KEY | EX_WINDOW, true))
|
||||||
@@ -379,6 +410,8 @@ int Window::runEventLoop()
|
|||||||
pendingH = nh;
|
pendingH = nh;
|
||||||
// 在“非拉伸阶段”的 WM_SIZE(例如最大化/还原/程序化调整)直接触发收口
|
// 在“非拉伸阶段”的 WM_SIZE(例如最大化/还原/程序化调整)直接触发收口
|
||||||
needResizeDirty = true;
|
needResizeDirty = true;
|
||||||
|
SX_LOGD("Resize") <<SX_T("WM_SIZE:待处理=(", "WM_SIZE: pending=(") << pendingW << "x" << pendingH << "), isSizing=" << (isSizing ? 1 : 0);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -392,17 +425,26 @@ int Window::runEventLoop()
|
|||||||
{
|
{
|
||||||
consume = d->handleEvent(msg);
|
consume = d->handleEvent(msg);
|
||||||
}
|
}
|
||||||
if (consume) break;
|
if (consume)
|
||||||
|
{
|
||||||
|
SX_LOGD("Event") << SX_T("事件被非模态对话框处理","Event consumed by non-modal dialog");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!consume)
|
if (!consume)
|
||||||
{
|
{
|
||||||
for (auto& c : controls)
|
for (auto& c : controls)
|
||||||
{
|
{
|
||||||
consume = c->handleEvent(msg);
|
consume = c->handleEvent(msg);
|
||||||
if (consume) break;
|
if (consume)
|
||||||
|
{
|
||||||
|
SX_LOGD("Event") << SX_T("事件被控件处理 id=","Event consumed by control id=") << c->getId();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//如果有对话框打开或者关闭强制重绘
|
//如果有对话框打开或者关闭强制重绘
|
||||||
bool needredraw = false;
|
bool needredraw = false;
|
||||||
for (auto& d : dialogs)
|
for (auto& d : dialogs)
|
||||||
@@ -441,11 +483,13 @@ int Window::runEventLoop()
|
|||||||
}
|
}
|
||||||
EndBatchDraw();
|
EndBatchDraw();
|
||||||
needredraw = false;
|
needredraw = false;
|
||||||
|
|
||||||
}
|
}
|
||||||
// —— 统一收口(needResizeDirty 为真时执行一次性重绘)——
|
// —— 统一收口(needResizeDirty 为真时执行一次性重绘)——
|
||||||
if (needResizeDirty)
|
if (needResizeDirty)
|
||||||
{
|
{
|
||||||
|
SX_LOGI("Resize") << SX_T("调整窗口尺寸开始:width=","Resize settle start: width=") << width << " height=" << height;
|
||||||
|
SX_TRACE_SCOPE(SX_T("调整尺寸","Resize"),SX_T("窗口:调整尺寸", "Window::resize_settle"));
|
||||||
|
|
||||||
// 以“实际客户区尺寸”为准,防止 pending 与真实尺寸出现偏差
|
// 以“实际客户区尺寸”为准,防止 pending 与真实尺寸出现偏差
|
||||||
RECT clientRect;
|
RECT clientRect;
|
||||||
GetClientRect(hWnd, &clientRect);
|
GetClientRect(hWnd, &clientRect);
|
||||||
@@ -524,6 +568,7 @@ int Window::runEventLoop()
|
|||||||
SendMessage(hWnd, WM_SETREDRAW, TRUE, 0);
|
SendMessage(hWnd, WM_SETREDRAW, TRUE, 0);
|
||||||
ValidateRect(hWnd, nullptr);
|
ValidateRect(hWnd, nullptr);
|
||||||
}
|
}
|
||||||
|
SX_LOGI("Resize") << SX_T("尺寸调整已完成:width=","Resize settle done: width=") << width << " height=" << height;
|
||||||
|
|
||||||
needResizeDirty = false; // 收口完成,清标志
|
needResizeDirty = false; // 收口完成,清标志
|
||||||
}
|
}
|
||||||
@@ -609,13 +654,9 @@ bool Window::hasNonModalDialogWithCaption(const std::string& caption, const std:
|
|||||||
{
|
{
|
||||||
if (!dptr) continue;
|
if (!dptr) continue;
|
||||||
if (auto* d = dynamic_cast<Dialog*>(dptr.get()))
|
if (auto* d = dynamic_cast<Dialog*>(dptr.get()))
|
||||||
{
|
|
||||||
if (d->IsVisible() && !d->model() && d->GetCaption() == caption && d->GetText() == message)
|
if (d->IsVisible() && !d->model() && d->GetCaption() == caption && d->GetText() == message)
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -662,27 +703,15 @@ std::vector<std::unique_ptr<Control>>& Window::getControls()
|
|||||||
return controls;
|
return controls;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Window::processWindowMessage(const ExMessage& msg)
|
|
||||||
{
|
|
||||||
if (msg.message == WM_SIZE && msg.wParam != SIZE_MINIMIZED)
|
|
||||||
{
|
|
||||||
const int nw = LOWORD(msg.lParam);
|
|
||||||
const int nh = HIWORD(msg.lParam);
|
|
||||||
if (nw >= minClientW && nh >= minClientH && nw <= 10000 && nh <= 10000)
|
|
||||||
{
|
|
||||||
if (nw != width || nh != height)
|
|
||||||
{
|
|
||||||
pendingW = nw;
|
|
||||||
pendingH = nh;
|
|
||||||
needResizeDirty = true; // 统一由 pumpResizeIfNeeded 来收口
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Window::pumpResizeIfNeeded()
|
void Window::pumpResizeIfNeeded()
|
||||||
{
|
{
|
||||||
if (!needResizeDirty) return;
|
if (!needResizeDirty) return;
|
||||||
|
SX_LOGD("Resize")
|
||||||
|
<< SX_T("执行 pumpResizeIfNeeded:needResizeDirty=",
|
||||||
|
"pumpResizeIfNeeded: needResizeDirty=")
|
||||||
|
<< (needResizeDirty ? 1 : 0)
|
||||||
|
<< SX_T("(需要进行一次缩放收口/重排重绘)", "");
|
||||||
|
|
||||||
|
|
||||||
RECT rc; GetClientRect(hWnd, &rc);
|
RECT rc; GetClientRect(hWnd, &rc);
|
||||||
const int finalW = max(minClientW, rc.right - rc.left);
|
const int finalW = max(minClientW, rc.right - rc.left);
|
||||||
@@ -705,7 +734,6 @@ void Window::pumpResizeIfNeeded()
|
|||||||
{
|
{
|
||||||
setbkcolor(wBkcolor);
|
setbkcolor(wBkcolor);
|
||||||
cleardevice();
|
cleardevice();
|
||||||
|
|
||||||
}
|
}
|
||||||
width = rc.right - rc.left; height = rc.bottom - rc.top;
|
width = rc.right - rc.left; height = rc.bottom - rc.top;
|
||||||
|
|
||||||
@@ -745,6 +773,14 @@ void Window::scheduleResizeFromModal(int w, int h)
|
|||||||
pendingW = w;
|
pendingW = w;
|
||||||
pendingH = h;
|
pendingH = h;
|
||||||
needResizeDirty = true; // 交给 pumpResizeIfNeeded 做统一收口+重绘
|
needResizeDirty = true; // 交给 pumpResizeIfNeeded 做统一收口+重绘
|
||||||
|
SX_LOGD("Resize")
|
||||||
|
<< SX_T("模态对话框触发缩放调度:pending=(",
|
||||||
|
"scheduleResizeFromModal: pending=(")
|
||||||
|
<< pendingW << "x" << pendingH
|
||||||
|
<< SX_T("),needResizeDirty=1(标记需要缩放收口)",
|
||||||
|
"), needResizeDirty=1");
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -818,4 +854,3 @@ void Window::adaptiveLayout(std::unique_ptr<Control>& c, const int finalH, const
|
|||||||
}
|
}
|
||||||
c->onWindowResize();
|
c->onWindowResize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user