feat: add a new awesome feature

This commit is contained in:
2026-01-09 20:17:47 +08:00
parent 0c1cf2938f
commit 5cb59b3652
18 changed files with 1301 additions and 102 deletions

View File

@@ -7,6 +7,41 @@ 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 ## [v2.3.2] - 2025 - 12 - 20
### ✨ Added ### ✨ Added

View File

@@ -3,10 +3,46 @@
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
- **轻量日志系统 SxLogSxLogger / 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 尺寸别名 APIBreaking**移除 `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 ## [v2.3.2] - 2025 - 12 - 20
### ✨ 新增 ### ✨ 新增

View File

@@ -25,6 +25,35 @@ This is a **teaching-grade and tooling-grade** framework that helps developers u
------ ------
### 🆕V3.0.0 - Major Update
[CHANGELOG.en.md](CHANGELOG.en.md)
### ✨ New Features
- **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.
### ⚙️ Changes
- **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.
### ✅ Fixes
- **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.
### 📌 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
StellarX adopts classic **OOP** and **modular** design with a clear structure: StellarX adopts classic **OOP** and **modular** design with a clear structure:

View File

@@ -14,8 +14,8 @@
![GitHub all releases](https://img.shields.io/github/downloads/Ysm-04/StellarX/total) ![GitHub all releases](https://img.shields.io/github/downloads/Ysm-04/StellarX/total)
[![Star GitHub Repo](https://img.shields.io/github/stars/Ysm-04/StellarX.svg?style=social&label=Star%20This%20Repo)](https://github.com/Ysm-04/StellarX) [![Star GitHub Repo](https://img.shields.io/github/stars/Ysm-04/StellarX.svg?style=social&label=Star%20This%20Repo)](https://github.com/Ysm-04/StellarX)
![Version](https://img.shields.io/badge/Version-2.3.2-brightgreen.svg) ![Version](https://img.shields.io/badge/Version-3.0.0-brightgreen.svg)
![Download](https://img.shields.io/badge/Download-2.3.2_Release-blue.svg) ![Download](https://img.shields.io/badge/Download-3.0.0_Release-blue.svg)
![C++](https://img.shields.io/badge/C++-17+-00599C?logo=cplusplus&logoColor=white) ![C++](https://img.shields.io/badge/C++-17+-00599C?logo=cplusplus&logoColor=white)
![Windows](https://img.shields.io/badge/Platform-Windows-0078D6?logo=windows) ![Windows](https://img.shields.io/badge/Platform-Windows-0078D6?logo=windows)
@@ -34,24 +34,34 @@
## 🆕V2.3.2——重要更新 ### 🆕V3.0.0 - 重要更新
### 新增 完整版建议查看[更新日志](CHANGELOG.md)
- **Table 支持运行期重置表头与数据:**新增 `Table::clearHeaders()``Table::clearData()``Table::resetTable()`,允许同一 `Table` 在运行过程中动态切换表头与数据,并触发必要的单元格尺寸/分页信息重算与重绘。 ### ✨ 新增功能
- **TextBox 新增密码模式:**`TextBoxmode` 新增 `PASSWORD_MODE`;输入内容内部保存,绘制层面使用掩码字符(如 `*`)替代显示,真实文本可通过 `TextBox::getText()` 获取。
- **SxLog**: 轻量级日志系统,支持日志级别、标签过滤、中英文输出及控制台/文件日志,支持文件滚动。
- **TabControl**: 改进页签切换逻辑,确保先关闭当前页再打开目标页。
- **控件 Setter 改进**: Setter 仅更新状态并标记为脏,绘制由统一重绘流程处理。
### ⚙️ 变更 ### ⚙️ 变更
- **TabControl 默认激活页语义明确化:** - **TabControl 默认激活页**: 默认激活页逻辑明确,首次设置激活索引后才绘制。
- 首次绘制前调用 `TabControl::setActiveIndex()`:仅记录默认激活索引,不再立即触发页签按钮点击回调;
- 首次绘制完成后:若设置了默认激活索引则应用激活状态并绘制激活页(索引越界时默认激活最后一个页);
- 程序运行过程中调用 `TabControl::setActiveIndex()`:索引合法则立即切换激活页并绘制;索引越界则不做处理。
### ✅ 修复 ### ✅ 修复
- **TabControl::setActiveIndex 绘制前调用导致程序中断:**修复绘制前设置默认激活索引时触发空指针访问的问题;现在默认激活逻辑延后到首次绘制完成后再生效,避免崩溃并保证首次绘制即可绘制激活页 - **TabControl::setActiveIndex 崩溃**: 修复首次绘制前设置默认激活页时崩溃的问题
- **TabControl 由不可见设置为可见时绘制错乱**修复 `setIsVisible(false) -> setIsVisible(true)`非激活页错误绘制导致的多页重叠/残影;现在 TabControl 可见时仅激活页可见/可绘制,无激活页则不绘制任何页 - **TabControl 渲染错乱**: 修复 `setIsVisible` 切换时,非激活页错误绘制导致的重叠问题
### ⚠️ 破坏性更改
- **Button 尺寸 API 移除**: 移除了 `getButtonWidth()` / `getButtonHeight()`,请使用 `getWidth()` / `getHeight()`
- **Setter 不再即时绘制**: `setText()` 等 Setter 不再触发即时绘制。
### 📌 升级指南
- **Button**: 将 `getButtonWidth()` / `getButtonHeight()` 替换为 `getWidth()` / `getHeight()`
- **Setter**: 在调用 Setter 后确保触发重绘机制。
--- ---

View File

@@ -1,7 +1,7 @@
/******************************************************************************* /*******************************************************************************
* @文件: StellarX.h * @文件: StellarX.h
* @摘要: 星垣(StellarX) GUI框架 - 主包含头文件 * @摘要: 星垣(StellarX) GUI框架 - 主包含头文件
* @版本: v2.3.2 * @版本: v3.0.0
* @描述: * @描述:
* 一个为Windows平台打造的轻量级、模块化C++ GUI框架。 * 一个为Windows平台打造的轻量级、模块化C++ GUI框架。
* 基于EasyX图形库提供简洁易用的API和丰富的控件。 * 基于EasyX图形库提供简洁易用的API和丰富的控件。
@@ -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
View 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; // 控制台 sinkenableConsole 控制)
std::unique_ptr<FileSink> fileSink; // 文件 sinkenableFile 控制)
};
/* ========================= 双语选择辅助 ========================= */
// 说明:
// - 只做“选择 zhCN 或 enUS”不做编码转换
// - 输出显示是否正常由终端环境决定
inline const char* SxT(const char* zhCN, const char* enUS)
{
return (SxLogger::Get().getLanguage() == SxLogLanguage::ZhCN) ? zhCN : enUS;
}
#if defined(__cpp_char8_t) && (__cpp_char8_t >= 201811L)
// 说明:
// - C++20 的 u8"xxx" 是 char8_t*,为了兼容调用端,这里提供重载
// - reinterpret_cast 只是改指针类型,不做 UTF-8 -> GBK 转码
inline const char* SxT(const char8_t* zhCN, const char* enUS)
{
return (SxLogger::Get().getLanguage() == SxLogLanguage::ZhCN)
? reinterpret_cast<const char*>(zhCN)
: enUS;
}
#endif
/* ========================= RAII 日志行对象 ========================= */
// 作用:
// - 构造时记录 level/tag/源码位置
// - operator<< 拼接内容
// - 析构时统一提交给 SxLogger::logLine 输出
//
// 设计意义:
// - 避免调用端忘记写换行
// - 保证一行日志作为整体写出
class SxLogLine
{
public:
// 构造:记录元信息(不输出)
SxLogLine(SxLogLevel level, const char* tag, const char* file, int line, const char* func);
// 析构:提交输出(真正写出发生在这里)
~SxLogLine();
// 拼接内容(流式写法)
template<typename T>
SxLogLine& operator<<(const T& v)
{
ss << v;
return *this;
}
private:
SxLogLevel lvl; // 日志级别
const char* tg; // Tag不拥有内存
const char* srcFile; // 源文件名(来自 __FILE__
int srcLine; // 行号(来自 __LINE__
const char* srcFunc; // 函数名(来自 __func__
std::ostringstream ss; // 内容拼接缓冲
};
/* ========================= RAII 作用域计时对象 ========================= */
// 作用:
// - 仅在 shouldLog(Trace, tag) 为 true 时启用计时
// - 析构时输出耗时(微秒)
//
// 使用建议:
// - 只在需要定位性能瓶颈时开启 Trace
// - name 建议传入常量字符串,便于检索
class SxLogScope
{
public:
// 构造:根据 shouldLog 决定是否启用计时
SxLogScope(SxLogLevel level, const char* tag, const char* file, int line, const char* func, const char* name);
// 析构:若启用则输出耗时
~SxLogScope();
private:
bool enabled = false; // 是否启用(未启用则析构无输出)
SxLogLevel lvl = SxLogLevel::Trace; // 级别(通常用 Trace
const char* tg = nullptr; // Tag
const char* srcFile = nullptr; // 源文件
int srcLine = 0; // 行号
const char* srcFunc = nullptr; // 函数
const char* scopeName = nullptr; // 作用域名
std::chrono::steady_clock::time_point t0; // 起始时间点
};
} // namespace StellarX
#if SX_LOG_ENABLE
// SX_T双语选择宏调用 SxT 根据当前语言选择输出
#define SX_T(zh, en) ::StellarX::SxT(zh, en)
// 日志宏说明:
// 1) 先 shouldLog 短路过滤,未命中则不会构造 SxLogLine也不会执行 else 分支的表达式
// 2) 命中则构造临时 SxLogLine并允许继续使用 operator<< 拼接
// 3) 语句结束时临时对象析构,触发真正输出
#define SX_LOG_TRACE(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Trace, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Trace, tag, __FILE__, __LINE__, __func__)
#define SX_LOGD(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Debug, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Debug, tag, __FILE__, __LINE__, __func__)
#define SX_LOGI(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Info, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Info, tag, __FILE__, __LINE__, __func__)
#define SX_LOGW(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Warn, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Warn, tag, __FILE__, __LINE__, __func__)
#define SX_LOGE(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Error, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Error, tag, __FILE__, __LINE__, __func__)
#define SX_LOGF(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Fatal, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Fatal, tag, __FILE__, __LINE__, __func__)
// 作用域耗时统计宏:默认用 Trace 级别
#define SX_TRACE_SCOPE(tag, nameLiteral) ::StellarX::SxLogScope sx_scope_##__LINE__(::StellarX::SxLogLevel::Trace, tag, __FILE__, __LINE__, __func__, nameLiteral)
#else
// 关闭日志时的兼容宏:保证调用端代码不需要改动
#define SX_T(zh, en) (en)
#define SX_LOG_TRACE(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGD(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGI(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGW(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGE(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGF(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_TRACE_SCOPE(tag, nameLiteral) do {} while(0)
#endif

View File

@@ -66,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; // 是否需要计算翻页按钮和页码信息

View File

@@ -12,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"
@@ -53,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);
@@ -86,21 +87,7 @@ 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:

View File

@@ -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)
@@ -281,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)
@@ -308,13 +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;
} }
@@ -331,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;
@@ -343,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();
@@ -382,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 相对按钮)

View File

@@ -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,24 +113,56 @@ 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)
{ {
//坐标转化 //坐标转化
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;
} }
@@ -187,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();
@@ -375,10 +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();
} }

View File

@@ -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,17 +45,34 @@ bool StellarX::ControlText::operator!=(const ControlText& text)
} }
void Control::setIsVisible(bool show) void Control::setIsVisible(bool show)
{ {
SX_LOGD("Control") << SX_T("重置可见状态: id=", "setIsVisible: id=")
<< id
<< " show=" << (show ? 1 : 0);
if (this->show == show)
return;
this->show = show; this->show = show;
dirty = true; this->dirty = true;
if (!show) if (!show)
{
// 隐藏:擦除自己在屏幕上的内容,并释放快照
this->updateBackground(); this->updateBackground();
else return;
requestRepaint(parent);
} }
// 显示:不在这里 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);
@@ -105,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);
@@ -127,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); // ★抓屏幕
@@ -150,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;
} }

View File

@@ -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)
@@ -148,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;
@@ -178,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);

View File

@@ -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
View 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 与 POSIXlocaltime_s/localtime_r
********************************************************************************/
namespace StellarX
{
// -------- FileSink --------
// 打开文件输出
// 难点:
// - 需要支持追加与清空两种模式
// - open 前先 close避免重复打开导致句柄泄漏
bool FileSink::open(const std::string& path, bool append)
{
close();
filePath = path;
appendMode = append;
std::ios::openmode mode = std::ios::out;
mode |= (append ? std::ios::app : std::ios::trunc);
ofs.open(path.c_str(), mode);
return ofs.is_open();
}
// 关闭文件输出(可重复调用)
void FileSink::close()
{
if (ofs.is_open()) ofs.close();
}
// 查询是否已打开
bool FileSink::isOpen() const
{
return ofs.is_open();
}
// 写入一整行
// 难点:
// - 写入后若启用 rotateBytes需要及时检测文件大小是否到阈值
void FileSink::writeLine(const std::string& line)
{
if (!ofs.is_open()) return;
ofs << line;
if (rotateBytes > 0) rotateIfNeeded();
}
// flush 文件缓冲
void FileSink::flush()
{
if (ofs.is_open()) ofs.flush();
}
// 滚动文件
// 难点:
// 1) tellp() 返回的是当前写指针位置,通常可近似视为文件大小
// 2) 时间戳用于文件名时需要做字符清洗,避免出现不友好字符
// 3) rename 行为与权限/占用有关,失败时需要保证不崩溃(此处选择“尽力而为”)
bool FileSink::rotateIfNeeded()
{
if (!ofs.is_open() || rotateBytes == 0) return false;
const std::streampos pos = ofs.tellp();
if (pos < 0) return false;
const std::size_t size = static_cast<std::size_t>(pos);
if (size < rotateBytes) return false;
ofs.flush();
ofs.close();
// xxx.log -> xxx.log.YYYYmmdd_HHMMSS
// 说明:
// - makeTimestampLocal 形如 "2026-01-09 12:34:56"
// - 文件名中把 '-' ' ' ':' 替换为 '_',只保留数字与 '_',降低环境差异
const std::string ts = SxLogger::makeTimestampLocal();
std::string safeTs;
safeTs.reserve(ts.size());
for (char ch : ts)
{
if (ch >= '0' && ch <= '9') safeTs.push_back(ch);
else if (ch == '-' || ch == ' ' || ch == ':') safeTs.push_back('_');
}
if (safeTs.empty()) safeTs = "rotated";
const std::string rotated = filePath + "." + safeTs;
std::rename(filePath.c_str(), rotated.c_str());
// 重新打开新文件
// 注意: 这里用 append=false确保新文件从空开始
return open(filePath, false);
}
// -------- SxLogger --------
// 设置 Windows 控制台 codepage只执行一次
// 难点:
// - 只影响终端解释输出字节的方式,不影响源码文件编码
// - 使用 once_flag 避免重复 system 调用造成噪声与性能浪费
//
// 注意:
// - 下面原注释写“切到 UTF-8”但实际命令是 chcp 936GBK
// - 为避免改动你原注释,这里补充说明事实,保持行为不变
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 实际是设置为 CP936GBK
// - 如果你的终端本身是 UTF-8 环境,调用它可能反而改变显示行为
// - 该函数建议只在“明确需要 GBK 控制台输出”的场景调用
// 尝试让 C/C++ 运行库按 UTF-8 工作(对部分流输出有帮助)
// std::setlocale(LC_ALL, ".UTF8");
});
#endif
}
// 获取单例
// 难点:
// - 作为全局入口,初始化必须线程安全
// - C++11 起函数内静态对象初始化由标准保证线程安全
SxLogger& SxLogger::Get()
{
static SxLogger inst;
return inst;
}
// 构造:设置默认语言
SxLogger::SxLogger()
: lang(SxLogLanguage::ZhCN)
{
}
// 设置最低输出级别
void SxLogger::setMinLevel(SxLogLevel level)
{
std::lock_guard<std::mutex> lock(mtx);
cfg.minLevel = level;
}
// 获取最低输出级别
SxLogLevel SxLogger::getMinLevel() const
{
std::lock_guard<std::mutex> lock(mtx);
return cfg.minLevel;
}
// 设置语言
// 难点:
// - 语言只影响 SX_T 的字符串选择
// - 这里用 atomic relaxed避免频繁加锁
void SxLogger::setLanguage(SxLogLanguage l)
{
lang.store(l, std::memory_order_relaxed);
}
// 获取语言
SxLogLanguage SxLogger::getLanguage() const
{
return lang.load(std::memory_order_relaxed);
}
// 设置 Tag 过滤
// 难点:
// - 当前实现是 vector<string> 线性匹配,适合 tag 数量不大
// - 若未来 tag 很多,可考虑 unordered_set 优化(但会增加依赖与复杂度)
void SxLogger::setTagFilter(SxTagFilterMode mode, const std::vector<std::string>& tags)
{
std::lock_guard<std::mutex> lock(mtx);
cfg.tagFilterMode = mode;
cfg.tagList = tags;
}
// 清空 Tag 过滤
void SxLogger::clearTagFilter()
{
std::lock_guard<std::mutex> lock(mtx);
cfg.tagFilterMode = SxTagFilterMode::None;
cfg.tagList.clear();
}
// 开关控制台输出
// 难点:
// - ConsoleSink 持有 ostream 引用,不管理其生命周期
void SxLogger::enableConsole(bool enable)
{
std::lock_guard<std::mutex> lock(mtx);
if (enable)
{
if (!consoleSink) consoleSink.reset(new ConsoleSink(std::cout));
}
else
{
consoleSink.reset();
}
}
// 开启文件输出
// 难点:
// - enableFile 成功与否决定 cfg.fileEnabled
// - 需要把 rotateBytes 同步到 FileSink
bool SxLogger::enableFile(const std::string& path, bool append, std::size_t rotateBytes_)
{
std::lock_guard<std::mutex> lock(mtx);
if (!fileSink) fileSink.reset(new FileSink());
fileSink->setRotateBytes(rotateBytes_);
const bool ok = fileSink->open(path, append);
cfg.fileEnabled = ok;
cfg.filePath = path;
cfg.fileAppend = append;
cfg.rotateBytes = rotateBytes_;
return ok;
}
// 关闭文件输出
void SxLogger::disableFile()
{
std::lock_guard<std::mutex> lock(mtx);
if (fileSink) fileSink->close();
cfg.fileEnabled = false;
}
// 获取配置副本
// 难点:
// - 返回副本避免外部拿到内部引用后绕过锁修改
SxLogConfig SxLogger::getConfigCopy() const
{
std::lock_guard<std::mutex> lock(mtx);
return cfg;
}
// 设置配置(整体替换)
void SxLogger::setConfig(const SxLogConfig& c)
{
std::lock_guard<std::mutex> lock(mtx);
cfg = c;
}
// 级别转字符串
const char* SxLogger::levelToString(SxLogLevel level)
{
switch (level)
{
case SxLogLevel::Trace: return "TRACE";
case SxLogLevel::Debug: return "DEBUG";
case SxLogLevel::Info: return "INFO ";
case SxLogLevel::Warn: return "WARN ";
case SxLogLevel::Error: return "ERROR";
case SxLogLevel::Fatal: return "FATAL";
default: return "OFF ";
}
}
// 判断 tag 是否允许输出
// 难点:
// - 精确匹配 tag 字符串
// - tag==nullptr 时默认允许,避免“无 tag 日志被误杀”
bool SxLogger::tagAllowed(const SxLogConfig& c, const char* tag)
{
if (c.tagFilterMode == SxTagFilterMode::None) return true;
if (!tag) return true;
bool found = false;
for (const auto& t : c.tagList)
{
if (t == tag) { found = true; break; }
}
if (c.tagFilterMode == SxTagFilterMode::Whitelist) return found;
if (c.tagFilterMode == SxTagFilterMode::Blacklist) return !found;
return true;
}
// 快速判定是否需要输出(宏短路依赖)
// 难点:
// 1) 必须无副作用:返回 false 时调用端不会构造对象也不会拼接
// 2) 过滤维度要完整级别、tag、sink 是否启用
// 3) 当前实现加锁保证 cfg 与 sink 状态一致;代价是高频路径会有锁开销
bool SxLogger::shouldLog(SxLogLevel level, const char* tag) const
{
std::lock_guard<std::mutex> lock(mtx);
if (cfg.minLevel == SxLogLevel::Off) return false;
if (level < cfg.minLevel) return false;
if (!tagAllowed(cfg, tag)) return false;
if (!consoleSink && !cfg.fileEnabled) return false;
return true;
}
// 生成本地时间戳字符串
// 难点:
// - Windows 与 POSIX 的线程安全 localtime API 不同
std::string SxLogger::makeTimestampLocal()
{
using namespace std::chrono;
const auto now = system_clock::now();
const std::time_t t = system_clock::to_time_t(now);
std::tm tmv{};
#if defined(_WIN32)
localtime_s(&tmv, &t);
#else
localtime_r(&t, &tmv);
#endif
std::ostringstream oss;
oss << std::put_time(&tmv, "%Y-%m-%d %H:%M:%S");
return oss.str();
}
// 拼接日志前缀(调用方已持锁)
// 难点:
// - 前缀拼接必须与配置项严格对应,且尽量避免多余开销
// - showSource 会输出 (file:line func),对定位时序问题很有价值
std::string SxLogger::formatPrefixUnlocked(
const SxLogConfig& c,
SxLogLevel level,
const char* tag,
const char* file,
int line,
const char* func) const
{
std::ostringstream oss;
if (c.showTimestamp) oss << "[" << makeTimestampLocal() << "] ";
if (c.showLevel) oss << "[" << levelToString(level) << "] ";
if (c.showTag && tag) oss << "[" << tag << "] ";
if (c.showThreadId)
{
oss << "[T:" << std::this_thread::get_id() << "] ";
}
if (c.showSource && file && func)
{
oss << "(" << file << ":" << line << " " << func << ") ";
}
return oss.str();
}
// 统一输出出口
// 难点:
// 1) 行级一致性:必须把 prefix + msg + "\n" 当作整体写入
// 2) 线程安全:持锁写入可避免不同线程日志互相穿插
// 3) 避免重入:在持锁期间不要再调用 SX_LOG...(会导致死锁)
void SxLogger::logLine(
SxLogLevel level,
const char* tag,
const char* file,
int line,
const char* func,
const std::string& msg)
{
std::lock_guard<std::mutex> lock(mtx);
if (cfg.minLevel == SxLogLevel::Off) return;
if (level < cfg.minLevel) return;
if (!tagAllowed(cfg, tag)) return;
const std::string prefix = formatPrefixUnlocked(cfg, level, tag, file, line, func);
const std::string lineText = prefix + msg + "\n";
if (consoleSink) consoleSink->writeLine(lineText);
if (cfg.fileEnabled && fileSink && fileSink->isOpen())
{
fileSink->writeLine(lineText);
}
if (cfg.autoFlush)
{
if (consoleSink) consoleSink->flush();
if (cfg.fileEnabled && fileSink) fileSink->flush();
}
}
// -------- SxLogLine --------
// 构造:只记录元信息
SxLogLine::SxLogLine(SxLogLevel level, const char* tag, const char* file, int line, const char* func)
: lvl(level), tg(tag), srcFile(file), srcLine(line), srcFunc(func)
{
}
// 析构:提交输出
// 难点:
// - 这是 RAII 设计的核心:保证语句结束时日志自动落地
// - 也要求调用端不要把临时对象跨语句保存(宏用法本身也不支持那样做)
SxLogLine::~SxLogLine()
{
SxLogger::Get().logLine(lvl, tg, srcFile, srcLine, srcFunc, ss.str());
}
// -------- SxLogScope --------
// 构造:按需启用计时
// 难点:
// - 只有 shouldLog 为 true 才记录起点,避免在未输出场景做无意义计时
SxLogScope::SxLogScope(SxLogLevel level, const char* tag, const char* file, int line, const char* func, const char* name)
: lvl(level), tg(tag), srcFile(file), srcLine(line), srcFunc(func), scopeName(name)
{
enabled = SxLogger::Get().shouldLog(lvl, tg);
if (enabled) t0 = std::chrono::steady_clock::now();
}
// 析构:输出耗时
// 难点:
// - steady_clock 用于衡量耗时,避免系统时间调整造成跳变
SxLogScope::~SxLogScope()
{
if (!enabled) return;
const auto t1 = std::chrono::steady_clock::now();
const auto us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
std::ostringstream oss;
oss << "SCOPE " << (scopeName ? scopeName : "") << " cost=" << us << "us";
SxLogger::Get().logLine(lvl, tg, srcFile, srcLine, srcFunc, oss.str());
}
} // namespace StellarX

View File

@@ -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;
@@ -250,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);
@@ -404,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();
} }
} }

View File

@@ -1,4 +1,5 @@
#include "Table.h" #include "Table.h"
#include "SxLog.h"
// 绘制表格的当前页 // 绘制表格的当前页
// 使用双循环绘制行和列,考虑分页偏移 // 使用双循环绘制行和列,考虑分页偏移
void Table::drawTable() void Table::drawTable()
@@ -159,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);
} }
@@ -394,13 +410,8 @@ void Table::draw()
restBackground(); restBackground();
// 绘制表头 // 绘制表头
//dX = x; //if (!headers.empty())
//dY = y;
if(isNeedDrawHeaders)
{
drawHeader(); drawHeader();
this->isNeedDrawHeaders = false;
}
// 绘制当前页 // 绘制当前页
drawTable(); drawTable();
// 绘制页码标签 // 绘制页码标签
@@ -438,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("新增Dataid=", "appendRow: id=") << id
<< SX_T(" 本行列数=", " cols=") << (int)data.size()
<< SX_T(" 数据总行数=", " totalRows=") << (int)this->data.size()
<< SX_T(" 总页数=", " totalPages=") << totalPages;
} }
void Table::setData(std::initializer_list<std::vector<std::string>> data) void Table::setData(std::initializer_list<std::vector<std::string>> data)
{ {
for (auto lis : data) for (auto lis : data)
@@ -473,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("新增Dataid=", "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)
@@ -542,6 +571,8 @@ void Table::clearHeaders()
isNeedDrawHeaders = true; // 标记需要重新绘制表头 isNeedDrawHeaders = true; // 标记需要重新绘制表头
isNeedButtonAndPageNum = true;// 标记需要重新计算翻页按钮和页码信息 isNeedButtonAndPageNum = true;// 标记需要重新计算翻页按钮和页码信息
dirty = true; dirty = true;
SX_LOGI("Table") << SX_T("清除表头id=","clearHeaders: id=" )<< id;
} }
void Table::clearData() void Table::clearData()
@@ -552,6 +583,7 @@ void Table::clearData()
isNeedCellSize = true; // 标记需要重新计算单元格尺寸 isNeedCellSize = true; // 标记需要重新计算单元格尺寸
isNeedButtonAndPageNum = true;// 标记需要重新计算翻页按钮和页码信息 isNeedButtonAndPageNum = true;// 标记需要重新计算翻页按钮和页码信息
dirty = true; dirty = true;
SX_LOGI("Table") << SX_T("清除表格数据id=","clearData: id=") << id;
} }
void Table::resetTable() void Table::resetTable()

View File

@@ -1,5 +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)
@@ -76,6 +77,8 @@ void TextBox::draw()
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;
@@ -86,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 + 1, "输入框", 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)
@@ -111,22 +119,36 @@ bool TextBox::handleEvent(const ExMessage& msg)
else if (StellarX::TextBoxmode::PASSWORD_MODE == mode) else if (StellarX::TextBoxmode::PASSWORD_MODE == mode)
{ {
char* temp = new char[maxCharLen + 1]; char* temp = new char[maxCharLen + 1];
// 不记录明文,只记录长度变化
dirty = InputBox(temp, (int)maxCharLen + 1, "输入框\n不可见输入,覆盖即可", NULL, NULL, NULL, NULL, false); dirty = InputBox(temp, (int)maxCharLen + 1, "输入框\n不可见输入,覆盖即可", NULL, NULL, NULL, NULL, false);
if (dirty) text = temp; if (dirty) text = temp;
delete[] temp; delete[] temp;
temp = nullptr;
consume = true; 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;
@@ -179,7 +201,6 @@ 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

View File

@@ -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)
@@ -445,6 +487,9 @@ int Window::runEventLoop()
// —— 统一收口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);
@@ -523,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; // 收口完成,清标志
} }
@@ -608,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;
} }
@@ -661,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("执行 pumpResizeIfNeededneedResizeDirty=",
"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);
@@ -743,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");
} }
} }