Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd4588731b | |||
| 0522bffa1b | |||
| 49ecb65514 | |||
| 0a9e290d42 | |||
| a29b4371fb | |||
| d62d8a90d6 |
+74
-13
@@ -1,16 +1,77 @@
|
||||
.vs/
|
||||
x64/
|
||||
.codex-temp/
|
||||
*.obj
|
||||
# 编译生成文件
|
||||
*.exe
|
||||
*.pdb
|
||||
*.ilk
|
||||
*.iobj
|
||||
*.ipdb
|
||||
*.tlog/
|
||||
*.log
|
||||
*.recipe
|
||||
*.idb
|
||||
*.obj
|
||||
*.pdb
|
||||
*.o
|
||||
*.a
|
||||
*.lib
|
||||
*.dll
|
||||
*.exp
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# 构建目录
|
||||
build/
|
||||
bin/
|
||||
obj/
|
||||
Debug/
|
||||
Release/
|
||||
x64/
|
||||
x86/
|
||||
Win32/
|
||||
|
||||
# Visual Studio 文件
|
||||
.vs/
|
||||
*.sln
|
||||
*.vcxproj
|
||||
*.vcxproj.filters
|
||||
*.vcxproj.user
|
||||
*.suo
|
||||
*.ncb
|
||||
*.user
|
||||
*.sdf
|
||||
ipch/
|
||||
*.aps
|
||||
*.res
|
||||
*.tlog
|
||||
*.lastbuildstate
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
*.idb
|
||||
*.pch
|
||||
|
||||
# CMake 生成文件
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
cmake_install.cmake
|
||||
Makefile
|
||||
*.cmake
|
||||
*.cmake.*
|
||||
CTestTestfile.cmake
|
||||
Testing/
|
||||
_deps/
|
||||
|
||||
# 文档生成
|
||||
docs/html/
|
||||
docs/latex/
|
||||
docs/man/
|
||||
docs/rtf/
|
||||
docs/xml/
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
*.log
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# EasyX 临时文件
|
||||
*.jpg
|
||||
*.bmp
|
||||
*.png
|
||||
*.tga
|
||||
|
||||
# 其他
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
-791
@@ -1,791 +0,0 @@
|
||||
#include "Button.h"
|
||||
#include "SxLog.h"
|
||||
#include "Window.h"
|
||||
|
||||
Button::Button(int x, int y, int width, int height, const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape)
|
||||
: Control(x, y, width, height)
|
||||
{
|
||||
initButton(text, mode, shape, RGB(202, 255, 255), RGB(171, 196, 220), RGB(255, 255, 0));
|
||||
}
|
||||
|
||||
Button::Button(int x, int y, int width, int height, const std::string text, COLORREF ct, COLORREF cf, StellarX::ButtonMode mode, StellarX::ControlShape shape)
|
||||
: Control(x, y, width, height)
|
||||
{
|
||||
initButton(text, mode, shape, ct, cf, RGB(255, 255, 0));
|
||||
}
|
||||
|
||||
Button::Button(int x, int y, int width, int height, const std::string text, COLORREF ct, COLORREF cf, COLORREF ch, StellarX::ButtonMode mode, StellarX::ControlShape shape)
|
||||
: Control(x, y, width, height)
|
||||
{
|
||||
initButton(text, mode, shape, ct, cf, ch);
|
||||
}
|
||||
// ====== GBK/MBCS 安全:字符边界与省略号裁切 ======
|
||||
static inline int gbk_char_len(const std::string& s, size_t i)
|
||||
{
|
||||
unsigned char b = (unsigned char)s[i];
|
||||
if (b <= 0x7F) return 1; // ASCII
|
||||
if (b >= 0x81 && b <= 0xFE && i + 1 < s.size())
|
||||
{
|
||||
unsigned char b2 = (unsigned char)s[i + 1];
|
||||
if (b2 >= 0x40 && b2 <= 0xFE && b2 != 0x7F) return 2; // 合法双字节
|
||||
}
|
||||
return 1; // 容错
|
||||
}
|
||||
|
||||
static inline void rtrim_spaces_gbk(std::string& s)
|
||||
{
|
||||
while (!s.empty() && s.back() == ' ') s.pop_back(); // ASCII 空格
|
||||
while (s.size() >= 2)
|
||||
{ // 全角空格 A1 A1
|
||||
unsigned char a = (unsigned char)s[s.size() - 2];
|
||||
unsigned char b = (unsigned char)s[s.size() - 1];
|
||||
if (a == 0xA1 && b == 0xA1) s.resize(s.size() - 2);
|
||||
else break;
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool is_ascii_only(const std::string& s)
|
||||
{
|
||||
for (unsigned char c : s) if (c > 0x7F) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool is_word_boundary_char(unsigned char c)
|
||||
{
|
||||
return c == ' ' || c == '-' || c == '_' || c == '/' || c == '\\' || c == '.' || c == ':';
|
||||
}
|
||||
|
||||
// 英文优先策略:优先在“词边界”回退,再退化到逐字符;省略号为 "..."
|
||||
static std::string ellipsize_ascii_pref(const std::string& text, int maxW)
|
||||
{
|
||||
if (maxW <= 0) return "";
|
||||
if (textwidth(LPCTSTR(text.c_str())) <= maxW) return text;
|
||||
|
||||
const std::string ell = "...";
|
||||
int ellW = textwidth(LPCTSTR(ell.c_str()));
|
||||
if (ellW > maxW)
|
||||
{ // 连 ... 都放不下
|
||||
std::string e = ell;
|
||||
while (!e.empty() && textwidth(LPCTSTR(e.c_str())) > maxW) e.pop_back();
|
||||
return e; // 可能是 ".."、"." 或 ""
|
||||
}
|
||||
const int limit = maxW - ellW;
|
||||
|
||||
// 先找到能放下的最长前缀
|
||||
size_t i = 0, lastFit = 0;
|
||||
while (i < text.size())
|
||||
{
|
||||
int clen = gbk_char_len(text, i);
|
||||
size_t j = text.size() < i + (size_t)clen ? text.size() : i + (size_t)clen;
|
||||
int w = textwidth(LPCTSTR(text.substr(0, j).c_str()));
|
||||
if (w <= limit) { lastFit = j; i = j; }
|
||||
else break;
|
||||
}
|
||||
if (lastFit == 0) return ell;
|
||||
|
||||
// 在已适配前缀范围内,向左找最近的词边界
|
||||
size_t cutPos = lastFit;
|
||||
for (size_t k = lastFit; k > 0; --k)
|
||||
{
|
||||
unsigned char c = (unsigned char)text[k - 1];
|
||||
if (c <= 0x7F && is_word_boundary_char(c)) { cutPos = k - 1; break; }
|
||||
}
|
||||
|
||||
std::string head = text.substr(0, cutPos);
|
||||
rtrim_spaces_gbk(head);
|
||||
head += ell;
|
||||
return head;
|
||||
}
|
||||
|
||||
// 中文优先策略:严格逐“字符”(1/2字节)回退;省略号用全角 "…"
|
||||
static std::string ellipsize_cjk_pref(const std::string& text, int maxW, const char* ellipsis = "…")
|
||||
{
|
||||
if (maxW <= 0) return "";
|
||||
if (textwidth(LPCTSTR(text.c_str())) <= maxW) return text;
|
||||
|
||||
std::string ell = ellipsis ? ellipsis : "…";
|
||||
int ellW = textwidth(LPCTSTR(ell.c_str()));
|
||||
if (ellW > maxW)
|
||||
{ // 连省略号都放不下
|
||||
std::string e = ell;
|
||||
while (!e.empty() && textwidth(LPCTSTR(e.c_str())) > maxW) e.pop_back();
|
||||
return e;
|
||||
}
|
||||
const int limit = maxW - ellW;
|
||||
|
||||
size_t i = 0, lastFit = 0;
|
||||
while (i < text.size())
|
||||
{
|
||||
int clen = gbk_char_len(text, i);
|
||||
size_t j = text.size() < i + (size_t)clen ? text.size() : i + (size_t)clen;
|
||||
int w = textwidth(LPCTSTR(text.substr(0, j).c_str()));
|
||||
if (w <= limit) { lastFit = j; i = j; }
|
||||
else break;
|
||||
}
|
||||
if (lastFit == 0) return ell;
|
||||
|
||||
std::string head = text.substr(0, lastFit);
|
||||
rtrim_spaces_gbk(head);
|
||||
head += ell;
|
||||
return head;
|
||||
}
|
||||
|
||||
void Button::setTooltipStyle(COLORREF text, COLORREF bk, bool transparent)
|
||||
{
|
||||
tipLabel.textStyle.color = text;
|
||||
tipLabel.setTextBkColor(bk);
|
||||
tipLabel.setTextdisap(transparent);
|
||||
}
|
||||
|
||||
void Button::setTooltipTextsForToggle(const std::string& onText, const std::string& offText)
|
||||
{
|
||||
tipTextOn = onText;
|
||||
tipTextOff = offText;
|
||||
tipUserOverride = true;
|
||||
}
|
||||
|
||||
void Button::initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch)
|
||||
{
|
||||
this->id = "Button";
|
||||
this->text = text;
|
||||
this->mode = mode;
|
||||
this->shape = shape;
|
||||
this->buttonTrueColor = ct;
|
||||
this->buttonFalseColor = cf;
|
||||
this->buttonHoverColor = ch;
|
||||
this->click = false;
|
||||
this->hover = false;
|
||||
|
||||
// === Tooltip 默认:文本=按钮文本;白底黑字;不透明;用当前按钮字体样式 ===
|
||||
tipTextClick = tipTextOn = tipTextOff = this->text;
|
||||
tipLabel.setText(tipTextClick);
|
||||
tipLabel.textStyle.color = (RGB(167, 170, 172));
|
||||
tipLabel.setTextBkColor(RGB(255, 255, 255));
|
||||
tipLabel.setTextdisap(false);
|
||||
tipLabel.textStyle = this->textStyle; // 复用按钮字体样式
|
||||
}
|
||||
|
||||
Button::~Button() = default;
|
||||
|
||||
void Button::draw()
|
||||
{
|
||||
if (!dirty || !show)return;
|
||||
|
||||
//保存当前样式和颜色
|
||||
saveStyle();
|
||||
StellarX::ControlText drawTextStyle = textStyle;
|
||||
|
||||
if (StellarX::ButtonMode::DISABLED == mode) //设置禁用按钮色
|
||||
{
|
||||
setfillcolor(DISABLEDCOLOUR);
|
||||
// 禁用态删除线只属于本次绘制效果,不回写用户公开 textStyle。
|
||||
drawTextStyle.bStrikeOut = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 点击状态优先级最高,然后是悬停状态,最后是默认状态
|
||||
COLORREF col = click ? buttonTrueColor : (hover ? buttonHoverColor : buttonFalseColor);
|
||||
setfillcolor(col);
|
||||
}
|
||||
//
|
||||
//设置字体背景色透明
|
||||
setbkmode(TRANSPARENT);
|
||||
//边框颜色
|
||||
setlinecolor(buttonBorderColor);
|
||||
|
||||
//设置字体颜色
|
||||
settextcolor(drawTextStyle.color);
|
||||
//设置字体样式
|
||||
settextstyle(drawTextStyle.nHeight, drawTextStyle.nWidth, drawTextStyle.lpszFace,
|
||||
drawTextStyle.nEscapement, drawTextStyle.nOrientation, drawTextStyle.nWeight,
|
||||
drawTextStyle.bItalic, drawTextStyle.bUnderline, drawTextStyle.bStrikeOut);
|
||||
|
||||
if (needCutText)
|
||||
cutButtonText();
|
||||
|
||||
//获取字符串像素高度和宽度
|
||||
if ((this->oldtext_width != this->text_width || this->oldtext_height != this->text_height)
|
||||
|| (-1 == oldtext_width && oldtext_height == -1))
|
||||
{
|
||||
if (isUseCutText)
|
||||
{
|
||||
this->oldtext_width = this->text_width = textwidth(LPCTSTR(this->cutText.c_str()));
|
||||
this->oldtext_height = this->text_height = textheight(LPCTSTR(this->cutText.c_str()));
|
||||
}
|
||||
else
|
||||
{
|
||||
this->oldtext_width = this->text_width = textwidth(LPCTSTR(this->text.c_str()));
|
||||
this->oldtext_height = this->text_height = textheight(LPCTSTR(this->text.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
//设置按钮填充模式
|
||||
setfillstyle((int)buttonFillMode, (int)buttonFillIma, buttonFileIMAGE.get());
|
||||
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage)
|
||||
saveBackground(this->x, this->y, (this->width + bordWith), (this->height + bordHeight));
|
||||
// 恢复背景(清除旧内容)
|
||||
restBackground();
|
||||
//根据按钮形状绘制
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE://有边框填充矩形
|
||||
fillrectangle(x, y, x + width, y + height);
|
||||
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
|
||||
: outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::B_RECTANGLE://无边框填充矩形
|
||||
solidrectangle(x, y, x + width, y + height);
|
||||
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
|
||||
: outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE://有边框填充圆角矩形
|
||||
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);
|
||||
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
|
||||
: outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE://无边框填充圆角矩形
|
||||
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);
|
||||
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
|
||||
: outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::CIRCLE://有边框填充圆形
|
||||
fillcircle(x + width / 2, y + height / 2, min(width, height) / 2);
|
||||
isUseCutText ? outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(cutText.c_str()))
|
||||
: outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::B_CIRCLE://无边框填充圆形
|
||||
solidcircle(x + width / 2, y + height / 2, min(width, height) / 2);
|
||||
isUseCutText ? outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(cutText.c_str()))
|
||||
: outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::ELLIPSE://有边框填充椭圆
|
||||
fillellipse(x, y, x + width, y + height);
|
||||
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
|
||||
: outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::B_ELLIPSE://无边框填充椭圆
|
||||
solidellipse(x, y, x + width, y + height);
|
||||
isUseCutText ? outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(cutText.c_str()))
|
||||
: outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
}
|
||||
|
||||
restoreStyle();//恢复默认字体样式和颜色
|
||||
dirty = false; //标记按钮不需要重绘
|
||||
|
||||
if (tipEnabled && tipVisible)
|
||||
tipLabel.draw();
|
||||
}
|
||||
// 处理鼠标事件,检测点击和悬停状态
|
||||
// 根据按钮模式和形状进行不同的处理
|
||||
bool Button::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
if (!show)
|
||||
return false;
|
||||
resetEventVisualChanged();
|
||||
|
||||
bool oldHover = hover;// 注意:只在状态变化时记录,避免 WM_MOUSEMOVE 刷屏
|
||||
bool oldClick = click;
|
||||
const bool oldTipVisible = tipVisible;
|
||||
|
||||
bool consume = false;//是否消耗事件
|
||||
const bool isMouseMessage =
|
||||
msg.message == WM_MOUSEMOVE ||
|
||||
msg.message == WM_LBUTTONDOWN ||
|
||||
msg.message == WM_LBUTTONUP;
|
||||
// 记录鼠标位置(用于tip定位)
|
||||
if (isMouseMessage)
|
||||
{
|
||||
lastMouseX = msg.x;
|
||||
lastMouseY = msg.y;
|
||||
}
|
||||
// 检测悬停状态(根据不同形状)
|
||||
if (isMouseMessage)
|
||||
{
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
hover = (msg.x > x && msg.x < (x + width) && msg.y > y && msg.y < (y + height));
|
||||
break;
|
||||
case StellarX::ControlShape::CIRCLE:
|
||||
case StellarX::ControlShape::B_CIRCLE:
|
||||
hover = isMouseInCircle(msg.x, msg.y, x + width / 2, y + height / 2, min(width, height) / 2);
|
||||
break;
|
||||
case StellarX::ControlShape::ELLIPSE:
|
||||
case StellarX::ControlShape::B_ELLIPSE:
|
||||
hover = isMouseInEllipse(msg.x, msg.y, x, y, x + width, y + height);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 处理鼠标点击事件
|
||||
if (msg.message == WM_LBUTTONDOWN && hover && mode != StellarX::ButtonMode::DISABLED)
|
||||
{
|
||||
if (mode == StellarX::ButtonMode::NORMAL)
|
||||
{
|
||||
click = true;
|
||||
SX_LOGD("Button") << SX_T("被点击: ","lbtn - down:")<< "id = " << id <<" text = "<<text << " mode = " << (int)mode;
|
||||
|
||||
dirty = true;
|
||||
consume = true;
|
||||
}
|
||||
else if (mode == StellarX::ButtonMode::TOGGLE)
|
||||
{
|
||||
// TOGGLE模式在鼠标释放时处理
|
||||
}
|
||||
}
|
||||
// NORMAL 模式:鼠标在按钮上释放时才触发点击回调,如果移出区域则取消点击状态。
|
||||
// TOGGLE 模式:在释放时切换状态,并触发相应的开/关回调。
|
||||
else if (msg.message == WM_LBUTTONUP && hover && mode != StellarX::ButtonMode::DISABLED)
|
||||
{
|
||||
hideTooltip(); // 隐藏悬停提示
|
||||
if (mode == StellarX::ButtonMode::NORMAL && click)
|
||||
{
|
||||
if (onClickCallback) onClickCallback();
|
||||
SX_LOGI("Button") << "click: id=" << id << " (NORMAL) callback=" << (onClickCallback ? "Y" : "N");
|
||||
|
||||
click = false;
|
||||
dirty = true;
|
||||
consume = true;
|
||||
hideTooltip();
|
||||
}
|
||||
else if (mode == StellarX::ButtonMode::TOGGLE)
|
||||
{
|
||||
click = !click;
|
||||
if (click && onToggleOnCallback) onToggleOnCallback();
|
||||
else if (!click && onToggleOffCallback) onToggleOffCallback();
|
||||
SX_LOGI("Button") << "toggle: id=" << id
|
||||
<< " " << (oldClick ? 1 : 0) << "->" << (click ? 1 : 0)
|
||||
<< " onCb=" << (onToggleOnCallback ? "Y" : "N")
|
||||
<< " offCb=" << (onToggleOffCallback ? "Y" : "N");
|
||||
|
||||
dirty = true;
|
||||
consume = true;
|
||||
refreshTooltipTextForState();
|
||||
hideTooltip();
|
||||
}
|
||||
}
|
||||
// 处理鼠标移出区域的情况
|
||||
|
||||
else if (msg.message == WM_MOUSEMOVE)
|
||||
{
|
||||
if (!hover && mode == StellarX::ButtonMode::NORMAL && click)
|
||||
{
|
||||
click = false;
|
||||
dirty = true;
|
||||
}
|
||||
else if (hover != oldHover)
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (tipEnabled)
|
||||
{
|
||||
if (hover && !oldHover)
|
||||
{
|
||||
// 刚刚进入悬停:开计时,暂不显示
|
||||
tipHoverTick = GetTickCount64();
|
||||
tipVisible = false;
|
||||
}
|
||||
if (!hover && oldHover)
|
||||
{
|
||||
// 刚移出:立即隐藏
|
||||
hideTooltip();
|
||||
}
|
||||
if (hover && !tipVisible)
|
||||
{
|
||||
// 到点就显示
|
||||
if (GetTickCount64() - tipHoverTick >= (ULONGLONG)tipDelayMs)
|
||||
{
|
||||
tipVisible = true;
|
||||
|
||||
// 定位(跟随鼠标 or 相对按钮)
|
||||
int tipX = tipFollowCursor ? (lastMouseX + tipOffsetX) : lastMouseX;
|
||||
int tipY = tipFollowCursor ? (lastMouseY + tipOffsetY) : y + height;
|
||||
// 设置文本(用户可能动态改了提示文本
|
||||
if (tipUserOverride)
|
||||
{
|
||||
if (mode == StellarX::ButtonMode::NORMAL)
|
||||
tipLabel.setText(tipTextClick);
|
||||
else if (mode == StellarX::ButtonMode::TOGGLE)
|
||||
tipLabel.setText(click ? tipTextOn : tipTextOff);
|
||||
}
|
||||
else
|
||||
if (mode == StellarX::ButtonMode::TOGGLE)
|
||||
tipLabel.setText(click ? tipTextOn : tipTextOff);
|
||||
// 设置位置
|
||||
tipLabel.setX(tipX);
|
||||
tipLabel.setY(tipY);
|
||||
// 标记需要绘制
|
||||
tipLabel.setDirty(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果状态发生变化,标记需要重绘
|
||||
const bool stateChanged = (hover != oldHover || click != oldClick);
|
||||
if (stateChanged)
|
||||
dirty = true;
|
||||
const bool tipVisibilityChanged = (tipVisible != oldTipVisible);
|
||||
if (tipVisibilityChanged)
|
||||
dirty = true;
|
||||
|
||||
// 事件吞噬规则:
|
||||
// - 鼠标移动:只有“当前命中按钮”时才吞掉,避免前一个按钮在清 hover 时截断消息,
|
||||
// 导致后一个真正命中的按钮收不到 WM_MOUSEMOVE。
|
||||
// 这里的前提是:父容器会保证“没有拿到真实消息的后续兄弟”还能走一遍
|
||||
// clearTransientMouseState(),从而清掉旧 hover / tooltip。
|
||||
// - 鼠标按下/抬起:命中按钮区域时吞掉,避免点击穿透到底层控件。
|
||||
if (msg.message == WM_MOUSEMOVE)
|
||||
{
|
||||
if (hover)
|
||||
consume = true;
|
||||
}
|
||||
else if ((msg.message == WM_LBUTTONDOWN || msg.message == WM_LBUTTONUP) && hover)
|
||||
{
|
||||
consume = true;
|
||||
}
|
||||
markEventVisualChanged(stateChanged || tipVisibilityChanged);
|
||||
|
||||
// 如果需要重绘,立即执行
|
||||
if (dirty)
|
||||
requestRepaint(parent);
|
||||
|
||||
return consume;
|
||||
}
|
||||
|
||||
bool Button::clearTransientMouseState()
|
||||
{
|
||||
if (!show)
|
||||
return false;
|
||||
|
||||
bool hoverChanged = false;
|
||||
bool normalClickChanged = false;
|
||||
bool bodyVisualChanged = false;
|
||||
bool tooltipChanged = false;
|
||||
|
||||
if (hover)
|
||||
{
|
||||
hover = false;
|
||||
hoverChanged = true;
|
||||
bodyVisualChanged = true;
|
||||
}
|
||||
|
||||
// NORMAL 模式下的按下态只属于一次鼠标交互过程;
|
||||
// 如果本次 WM_MOUSEMOVE 已经被同层更上层的兄弟控件消费,
|
||||
// 当前按钮就不应继续保留这笔临时按下视觉。
|
||||
if (mode == StellarX::ButtonMode::NORMAL && click)
|
||||
{
|
||||
click = false;
|
||||
normalClickChanged = true;
|
||||
bodyVisualChanged = true;
|
||||
}
|
||||
|
||||
if (tipVisible)
|
||||
{
|
||||
hideTooltip();
|
||||
tooltipChanged = true;
|
||||
}
|
||||
|
||||
const bool stateChanged = bodyVisualChanged || tooltipChanged;
|
||||
if (!stateChanged)
|
||||
return false;
|
||||
|
||||
SX_LOG_TRACE("Event")
|
||||
<< SX_T("清理按钮鼠标临时状态:id=", "clear button transient mouse state: id=")
|
||||
<< id
|
||||
<< SX_T(" hover=", " hover=") << (hoverChanged ? 1 : 0)
|
||||
<< SX_T(" tooltip=", " tooltip=") << (tooltipChanged ? 1 : 0)
|
||||
<< SX_T(" normalClick=", " normalClick=") << (normalClickChanged ? 1 : 0);
|
||||
|
||||
markEventVisualChanged(true);
|
||||
|
||||
// 只有按钮本体视觉真正变化时,才请求按钮自身区域重绘;
|
||||
// 若只是 Tooltip 消失,hideTooltip() 已经通过回贴快照清掉了悬浮层区域,
|
||||
// 不再额外重绘按钮本体,避免高频鼠标移动时的无意义重画。
|
||||
if (bodyVisualChanged)
|
||||
{
|
||||
dirty = true;
|
||||
requestRepaint(parent);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
RECT Button::getManagedRepaintCoverageRect() const
|
||||
{
|
||||
RECT coverage = getBoundsRect();
|
||||
|
||||
if (!tipEnabled || !tipVisible)
|
||||
return coverage;
|
||||
|
||||
const RECT tipRect = tipLabel.getManagedRepaintCoverageRect();
|
||||
coverage.left = (std::min)(coverage.left, tipRect.left);
|
||||
coverage.top = (std::min)(coverage.top, tipRect.top);
|
||||
coverage.right = (std::max)(coverage.right, tipRect.right);
|
||||
coverage.bottom = (std::max)(coverage.bottom, tipRect.bottom);
|
||||
return coverage;
|
||||
}
|
||||
|
||||
RECT Button::getManagedRepaintPersistentCoverageRect() const
|
||||
{
|
||||
// Tooltip 属于临时浮层,只参与完整 coverage 和上层 overlay 判断;
|
||||
// 兄弟控件是否需要作废背景快照,只应由按钮本体这类持久绘制范围决定。
|
||||
return getBoundsRect();
|
||||
}
|
||||
|
||||
void Button::setOnClickListener(std::function<void()> callback)
|
||||
{
|
||||
this->onClickCallback = std::move(callback);
|
||||
}
|
||||
|
||||
void Button::setOnToggleOnListener(std::function<void()> callback)
|
||||
{
|
||||
this->onToggleOnCallback = std::move(callback);
|
||||
}
|
||||
void Button::setOnToggleOffListener(std::function<void()> callback)
|
||||
{
|
||||
this->onToggleOffCallback = std::move(callback);
|
||||
}
|
||||
|
||||
void Button::setbuttonMode(StellarX::ButtonMode mode)
|
||||
{
|
||||
//取值范围参考 buttMode的枚举注释
|
||||
this->mode = mode;
|
||||
dirty = true; // 标记需要重绘
|
||||
}
|
||||
|
||||
void Button::setROUND_RECTANGLEwidth(int width)
|
||||
{
|
||||
rouRectangleSize.ROUND_RECTANGLEwidth = width;
|
||||
this->dirty = true; // 标记需要重绘
|
||||
}
|
||||
|
||||
void Button::setROUND_RECTANGLEheight(int height)
|
||||
{
|
||||
rouRectangleSize.ROUND_RECTANGLEheight = height;
|
||||
this->dirty = true; // 标记需要重绘
|
||||
}
|
||||
|
||||
bool Button::isClicked() const
|
||||
{
|
||||
return this->click;
|
||||
}
|
||||
|
||||
void Button::setFillMode(StellarX::FillMode mode)
|
||||
{
|
||||
this->buttonFillMode = mode;
|
||||
this->dirty = true; // 标记需要重绘
|
||||
}
|
||||
|
||||
void Button::setFillIma(StellarX::FillStyle ima)
|
||||
{
|
||||
buttonFillIma = ima;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Button::setFillIma(std::string imaNAme)
|
||||
{
|
||||
if (buttonFileIMAGE)
|
||||
{
|
||||
buttonFileIMAGE.reset();
|
||||
}
|
||||
buttonFileIMAGE = std::make_unique<IMAGE>();
|
||||
loadimage(buttonFileIMAGE.get(), imaNAme.c_str(), width, height);
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Button::setButtonBorder(COLORREF Border)
|
||||
{
|
||||
buttonBorderColor = Border;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Button::setButtonFalseColor(COLORREF color)
|
||||
{
|
||||
this->buttonFalseColor = color;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Button::setButtonText(const char* text)
|
||||
{
|
||||
this->text = std::string(text);
|
||||
this->text_width = textwidth(LPCTSTR(this->text.c_str()));
|
||||
this->text_height = textheight(LPCTSTR(this->text.c_str()));
|
||||
this->dirty = true;
|
||||
this->needCutText = true;
|
||||
if (!tipUserOverride)
|
||||
tipTextClick = tipTextOn = tipTextOff = text;
|
||||
}
|
||||
|
||||
void Button::setButtonText(std::string text)
|
||||
{
|
||||
this->text = text;
|
||||
this->text_width = textwidth(LPCTSTR(this->text.c_str()));
|
||||
this->text_height = textheight(LPCTSTR(this->text.c_str()));
|
||||
this->dirty = true; // 标记需要重绘
|
||||
this->needCutText = true;
|
||||
if (!tipUserOverride)
|
||||
tipTextClick = tipTextOn = tipTextOff = text;
|
||||
}
|
||||
|
||||
void Button::setButtonShape(StellarX::ControlShape shape)
|
||||
{
|
||||
this->shape = shape;
|
||||
this->dirty = true;
|
||||
this->needCutText = true;
|
||||
}
|
||||
|
||||
//允许通过外部函数修改按钮的点击状态,并执行相应的回调函数
|
||||
void Button::setButtonClick(BOOL click)
|
||||
{
|
||||
const bool targetClick = (click != FALSE);
|
||||
// TOGGLE 状态若没有发生变化,就不应重复触发 onToggleOn/OffCallback。
|
||||
// 否则像 TabControl 这种“外部再次激活已激活页签”的场景,
|
||||
// 会把页面显示/隐藏、快照失效、重绘链整条重复执行一遍,最终破坏可见页的背景恢复语义。
|
||||
if (mode == StellarX::ButtonMode::TOGGLE && this->click == targetClick)
|
||||
return;
|
||||
|
||||
this->click = targetClick;
|
||||
|
||||
if (mode == StellarX::ButtonMode::NORMAL && click)
|
||||
{
|
||||
if (onClickCallback) onClickCallback();
|
||||
dirty = true;
|
||||
hideTooltip();
|
||||
}
|
||||
else if (mode == StellarX::ButtonMode::TOGGLE)
|
||||
{
|
||||
if (click && onToggleOnCallback) onToggleOnCallback();
|
||||
else if (!click && onToggleOffCallback) onToggleOffCallback();
|
||||
dirty = true;
|
||||
refreshTooltipTextForState();
|
||||
hideTooltip();
|
||||
}
|
||||
if (dirty)
|
||||
requestRepaint(parent);
|
||||
}
|
||||
|
||||
std::string Button::getButtonText() const
|
||||
{
|
||||
return this->text;
|
||||
}
|
||||
|
||||
const char* Button::getButtonText_c() const
|
||||
{
|
||||
return this->text.c_str();
|
||||
}
|
||||
|
||||
StellarX::ButtonMode Button::getButtonMode() const
|
||||
{
|
||||
return this->mode;
|
||||
}
|
||||
|
||||
StellarX::ControlShape Button::getButtonShape() const
|
||||
{
|
||||
return this->shape;
|
||||
}
|
||||
|
||||
StellarX::FillMode Button::getFillMode() const
|
||||
{
|
||||
return this->buttonFillMode;
|
||||
}
|
||||
|
||||
StellarX::FillStyle Button::getFillIma() const
|
||||
{
|
||||
return this->buttonFillIma;
|
||||
}
|
||||
|
||||
IMAGE* Button::getFillImaImage() const
|
||||
{
|
||||
return this->buttonFileIMAGE.get();
|
||||
}
|
||||
|
||||
COLORREF Button::getButtonBorder() const
|
||||
{
|
||||
return this->buttonBorderColor;
|
||||
}
|
||||
|
||||
COLORREF Button::getButtonTextColor() const
|
||||
{
|
||||
return this->textStyle.color;
|
||||
}
|
||||
|
||||
StellarX::ControlText Button::getButtonTextStyle() const
|
||||
{
|
||||
return this->textStyle;
|
||||
}
|
||||
|
||||
bool Button::isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius)
|
||||
{
|
||||
double dis = sqrt(pow(mouseX - x, 2) + pow(mouseY - y, 2));
|
||||
if (dis <= radius)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Button::isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height)
|
||||
{
|
||||
int centerX = (x + width) / 2;
|
||||
int centerY = (y + height) / 2;
|
||||
int majorAxis = (width - x) / 2;
|
||||
int minorAxis = (height - y) / 2;
|
||||
double dx = mouseX - centerX;
|
||||
double dy = mouseY - centerY;
|
||||
double normalizedDistance = (dx * dx) / (majorAxis * majorAxis) + (dy * dy) / (minorAxis * minorAxis);
|
||||
|
||||
// 判断鼠标是否在椭圆内
|
||||
if (normalizedDistance <= 1.0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
void Button::cutButtonText()
|
||||
{
|
||||
const int contentW = 1 > this->width - 2 * padX ? 1 : this->width - 2 * padX;
|
||||
// 放得下:不截断,直接用原文
|
||||
if (textwidth(LPCTSTR(this->text.c_str())) <= contentW) {
|
||||
isUseCutText = false;
|
||||
needCutText = false;
|
||||
cutText.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// 放不下:按语言偏好裁切(ASCII→词边界;CJK→逐字符,不撕裂双字节)
|
||||
if (is_ascii_only(this->text))
|
||||
{
|
||||
cutText = ellipsize_ascii_pref(this->text, contentW); // "..."
|
||||
}
|
||||
else
|
||||
{
|
||||
cutText = ellipsize_cjk_pref(this->text, contentW, "…"); // 全角省略号
|
||||
}
|
||||
isUseCutText = true;
|
||||
needCutText = false;
|
||||
}
|
||||
|
||||
void Button::hideTooltip()
|
||||
{
|
||||
if (tipVisible)
|
||||
{
|
||||
tipVisible = false;
|
||||
// Tooltip 是 Button 的内置浮层,不属于独立控件树节点。
|
||||
// 因此在隐藏时应直接回贴它自己的背景快照并作废,
|
||||
// 不能仅仅作废快照,否则当本轮只重绘按钮本体区域时,
|
||||
// Tooltip 占用的那块屏幕可能无人擦除,最终表现为“鼠标移开后提示框残留”。
|
||||
tipLabel.hide(); // 还原快照 + 作废快照,立即清掉 Tooltip 自身绘制区域
|
||||
tipHoverTick = GetTickCount64(); // 重置计时基线
|
||||
}
|
||||
}
|
||||
|
||||
void Button::refreshTooltipTextForState()
|
||||
{
|
||||
if (tipUserOverride) return; // 用户显式设置过 tipText,保持不变
|
||||
if (mode == StellarX::ButtonMode::NORMAL)
|
||||
tipLabel.setText(tipTextClick);
|
||||
else if (mode == StellarX::ButtonMode::TOGGLE)
|
||||
tipLabel.setText(click ? tipTextOn : tipTextOff);
|
||||
}
|
||||
@@ -1,196 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* @类: Button
|
||||
* @摘要: 多功能按钮控件,支持多种状态和样式
|
||||
* @描述:
|
||||
* 提供完整的按钮功能,包括普通点击、切换模式、禁用状态。
|
||||
* 支持多种形状(矩形、圆形、椭圆等)和丰富的视觉样式。
|
||||
* 通过回调函数机制实现灵活的交互逻辑。
|
||||
*
|
||||
* @特性:
|
||||
* - 支持三种工作模式:普通、切换、禁用
|
||||
* - 八种几何形状,各有边框和无边框版本
|
||||
* - 自定义颜色(默认、悬停、点击状态)
|
||||
* - 多种填充模式(纯色、图案、图像)
|
||||
* - 完整的鼠标事件处理(点击、悬停、移出)
|
||||
*
|
||||
* @使用场景: 作为主要交互控件,用于触发动作或表示状态
|
||||
* @所属框架: 星垣(StellarX) GUI框架
|
||||
* @作者: 我在人间做废物
|
||||
******************************************************************************/
|
||||
#pragma once
|
||||
#include "Control.h"
|
||||
#include"label.h"
|
||||
|
||||
#define DISABLEDCOLOUR RGB(96, 96, 96) //禁用状态颜色
|
||||
#define TEXTMARGINS_X 6
|
||||
#define TEXTMARGINS_Y 4
|
||||
constexpr int bordWith = 1; //边框宽度,用于快照恢复时的偏移计算
|
||||
constexpr int bordHeight = 1; //边框高度,用于快照恢复时的偏移计算
|
||||
|
||||
class Button : public Control
|
||||
{
|
||||
std::string text; // 按钮上的文字
|
||||
bool click; // 是否被点击
|
||||
bool hover; // 是否被悬停
|
||||
|
||||
std::string cutText; // 切割后的文本
|
||||
bool needCutText = true; // 是否需要切割文本
|
||||
bool isUseCutText = false; // 是否使用切割文本
|
||||
int padX = TEXTMARGINS_X; // 文本最小左右内边距
|
||||
int padY = TEXTMARGINS_Y; // 文本最小上下内边距
|
||||
|
||||
COLORREF buttonTrueColor; // 按钮被点击后的颜色
|
||||
COLORREF buttonFalseColor; // 按钮未被点击的颜色
|
||||
COLORREF buttonHoverColor; // 按钮被鼠标悬停的颜色
|
||||
COLORREF buttonBorderColor = RGB(0, 0, 0);// 按钮边框颜色
|
||||
|
||||
StellarX::ButtonMode mode; // 按钮模式
|
||||
StellarX::ControlShape shape; // 按钮形状
|
||||
|
||||
StellarX::FillMode buttonFillMode = StellarX::FillMode::Solid; //按钮填充模式
|
||||
StellarX::FillStyle buttonFillIma = StellarX::FillStyle::BDiagonal; //按钮填充图案
|
||||
std::unique_ptr<IMAGE> buttonFileIMAGE; //按钮填充图像
|
||||
|
||||
std::function<void()> onClickCallback; //回调函数
|
||||
std::function<void()> onToggleOnCallback; //TOGGLE模式下的回调函数
|
||||
std::function<void()> onToggleOffCallback; //TOGGLE模式下的回调函数
|
||||
|
||||
StellarX::ControlText oldStyle = textStyle; // 按钮文字样式
|
||||
int oldtext_width = -1;
|
||||
int oldtext_height = -1;
|
||||
int text_width = 0;
|
||||
int text_height = 0;
|
||||
|
||||
// === Tooltip ===
|
||||
bool tipEnabled = false; // 是否启用
|
||||
bool tipVisible = false; // 当前是否显示
|
||||
bool tipFollowCursor = false; // 是否跟随鼠标
|
||||
bool tipUserOverride = false; // 是否用户自定义了tip文本
|
||||
int tipDelayMs = 1000; // 延时(毫秒)
|
||||
int tipOffsetX = 12; // 相对鼠标偏移
|
||||
int tipOffsetY = 18;
|
||||
ULONGLONG tipHoverTick = 0; // 开始悬停的时间戳
|
||||
int lastMouseX = 0; // 最新鼠标位置(用于定位)
|
||||
int lastMouseY = 0;
|
||||
|
||||
std::string tipTextClick; // NORMAL 模式下用
|
||||
std::string tipTextOn; // click==true 时用
|
||||
std::string tipTextOff; // click==false 时用
|
||||
Label tipLabel; // 直接复用Label作为提示
|
||||
|
||||
public:
|
||||
StellarX::ControlText textStyle; // 按钮文字样式
|
||||
|
||||
public:
|
||||
|
||||
//默认按钮颜色
|
||||
Button(int x, int y, int width, int height, const std::string text,
|
||||
StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
|
||||
//自定义按钮未被点击和被点击颜色
|
||||
Button(int x, int y, int width, int height, const std::string text,
|
||||
COLORREF ct, COLORREF cf, StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL,
|
||||
StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
|
||||
//自定义按钮颜色和悬停颜色
|
||||
Button(int x, int y, int width, int height, const std::string text,
|
||||
COLORREF ct, COLORREF cf, COLORREF ch,
|
||||
StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
|
||||
//析构函数 释放图形指针内存
|
||||
~Button();
|
||||
|
||||
//绘制按钮
|
||||
void draw() override;
|
||||
//按钮事件处理
|
||||
bool handleEvent(const ExMessage& msg) override;
|
||||
// 清理按钮的鼠标瞬时状态;用于父容器在 WM_MOUSEMOVE 短路后补做 hover/tooltip 收口。
|
||||
bool clearTransientMouseState() override;
|
||||
// Tooltip 可见时,按钮实际写像素范围不再等于按钮本体,需要把 Tooltip 矩形并入 coverage。
|
||||
RECT getManagedRepaintCoverageRect() const override;
|
||||
// Button 的持久绘制范围只包含本体;Tooltip 是临时浮层,不能污染兄弟控件背景快照。
|
||||
RECT getManagedRepaintPersistentCoverageRect() const override;
|
||||
|
||||
// 设置 NORMAL 模式下的点击回调
|
||||
void setOnClickListener(std::function<void()> callback);
|
||||
// 设置 TOGGLE 模式下切换到打开状态时的回调
|
||||
void setOnToggleOnListener(std::function<void()> callback);
|
||||
// 设置 TOGGLE 模式下切换到关闭状态时的回调
|
||||
void setOnToggleOffListener(std::function<void()> callback);
|
||||
// 设置按钮模式(NORMAL / TOGGLE / DISABLED)
|
||||
void setbuttonMode(StellarX::ButtonMode mode);
|
||||
// 设置圆角矩形的圆角宽度
|
||||
void setROUND_RECTANGLEwidth(int width);
|
||||
// 设置圆角矩形的圆角高度
|
||||
void setROUND_RECTANGLEheight(int height);
|
||||
// 设置按钮填充模式
|
||||
void setFillMode(StellarX::FillMode mode);
|
||||
// 设置按钮图案填充样式
|
||||
void setFillIma(StellarX::FillStyle ima);
|
||||
// 设置按钮图像填充资源
|
||||
void setFillIma(std::string imaName);
|
||||
// 设置按钮边框颜色
|
||||
void setButtonBorder(COLORREF Border);
|
||||
// 设置按钮默认态颜色
|
||||
void setButtonFalseColor(COLORREF color);
|
||||
// 设置按钮文本(char* 重载)
|
||||
void setButtonText(const char* text);
|
||||
// 设置按钮文本(std::string 重载)
|
||||
void setButtonText(std::string text);
|
||||
// 设置按钮几何形状
|
||||
void setButtonShape(StellarX::ControlShape shape);
|
||||
// 直接设置按钮点击状态;NORMAL 设置为 true 时触发 onClick,TOGGLE 仅在状态变化时触发 on/off 回调
|
||||
void setButtonClick(BOOL click);
|
||||
|
||||
// 查询按钮当前是否处于点击/选中状态
|
||||
bool isClicked() const;
|
||||
|
||||
// 获取按钮文本
|
||||
std::string getButtonText() const;
|
||||
// 获取按钮文本的 C 字符串视图
|
||||
const char* getButtonText_c() const;
|
||||
// 获取按钮模式
|
||||
StellarX::ButtonMode getButtonMode() const;
|
||||
// 获取按钮形状
|
||||
StellarX::ControlShape getButtonShape() const;
|
||||
// 获取按钮填充模式
|
||||
StellarX::FillMode getFillMode() const;
|
||||
// 获取按钮图案填充样式
|
||||
StellarX::FillStyle getFillIma() const;
|
||||
// 获取按钮图像填充资源
|
||||
IMAGE* getFillImaImage() const;
|
||||
// 获取按钮边框颜色
|
||||
COLORREF getButtonBorder() const;
|
||||
// 获取按钮文字颜色
|
||||
COLORREF getButtonTextColor() const;
|
||||
// 获取按钮文字样式
|
||||
StellarX::ControlText getButtonTextStyle() const;
|
||||
public:
|
||||
// === Tooltip API===
|
||||
// 开关 Tooltip;关闭时只修改内部启用/显示状态,不负责智能选位
|
||||
void enableTooltip(bool on) { tipEnabled = on; if (!on) tipVisible = false; }
|
||||
// 设置 Tooltip 延时(毫秒)
|
||||
void setTooltipDelay(int ms) { tipDelayMs = (ms < 0 ? 0 : ms); }
|
||||
// 设置 Tooltip 是否跟随鼠标
|
||||
void setTooltipFollowCursor(bool on) { tipFollowCursor = on; }
|
||||
// 设置 Tooltip 偏移量;当前仅在 setTooltipFollowCursor(true) 时参与定位
|
||||
void setTooltipOffset(int dx, int dy) { tipOffsetX = dx; tipOffsetY = dy; }
|
||||
// 设置 Tooltip 的文字、背景和透明样式
|
||||
void setTooltipStyle(COLORREF text, COLORREF bk, bool transparent);
|
||||
// 设置 NORMAL 模式下 Tooltip 文本
|
||||
void setTooltipText(const std::string& s) { tipTextClick = s; tipUserOverride = true; }
|
||||
// 设置 TOGGLE 模式开/关两种状态下的 Tooltip 文本
|
||||
void setTooltipTextsForToggle(const std::string& onText, const std::string& offText);
|
||||
private:
|
||||
// 初始化按钮内部状态、颜色和 Tooltip 默认样式
|
||||
void initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch);
|
||||
// 判断鼠标是否在圆形按钮内
|
||||
bool isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius);
|
||||
// 判断鼠标是否在椭圆按钮内
|
||||
bool isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height);
|
||||
//获取对话框类型
|
||||
bool model() const override { return false; }
|
||||
// 按当前按钮宽度和语言特征裁剪文本
|
||||
void cutButtonText();
|
||||
// 统一隐藏 Tooltip 并恢复其背景快照
|
||||
void hideTooltip();
|
||||
// 根据当前 click 状态选择 Tooltip 文案
|
||||
void refreshTooltipTextForState();
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
# 更新日志
|
||||
|
||||
StellarX 项目所有显著的变化都将被记录在这个文件中。
|
||||
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||
|
||||
## [1.0.0] - 2025-09-08
|
||||
|
||||
### 发布摘要
|
||||
首个稳定版本 (Stable Release)
|
||||
|
||||
### 新增
|
||||
|
||||
- StellarX 框架的第一个稳定版本
|
||||
- 完整的控件库:按钮、标签、文本框、画布、表格和窗口
|
||||
- 基于 CMake 的构建系统
|
||||
- 详细的文档和示例代码
|
||||
- **明确声明:本框架专为 Windows 平台设计**
|
||||
|
||||
### Released
|
||||
- **首次发布预编译的二进制库文件**,方便用户快速集成,无需从源码编译。
|
||||
- 提供的发布包包括:
|
||||
- `StellarX-v1.0.0-x64-Windows-msvc-x64.zip`
|
||||
- **编译环境**: Visual Studio 2022 (MSVC v143)
|
||||
- **架构**: x64 (64位)
|
||||
- **运行时库**: `/MD`
|
||||
- **构建模式**: `Release | Debug`
|
||||
- **内容**: 包含所有必要的头文件(`include/`)和静态库文件(``lib/StellarX-Debug.lib StellarX-Release.lib`)
|
||||
|
||||
### 核心特性
|
||||
- 模块化设计,遵循 SOLID 原则
|
||||
- 统一的控件接口(draw() 和 handleEvent())
|
||||
- 支持多种控件形状和样式
|
||||
- 自定义事件处理回调
|
||||
- 轻量级设计,无外部依赖
|
||||
|
||||
## [0.1.0] - 2025-08-15
|
||||
### 新增
|
||||
- 初始项目结构和核心架构
|
||||
- Control 基类和基本事件处理系统
|
||||
- 基础示例和文档设置
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(StellarX VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
# 设置C++标准
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# Windows特定设置
|
||||
if(WIN32)
|
||||
add_definitions(-DWIN32 -D_WINDOWS)
|
||||
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
|
||||
# 设置Windows子系统
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS")
|
||||
endif()
|
||||
|
||||
# 设置输出目录
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||
|
||||
# 包含目录
|
||||
include_directories(include)
|
||||
|
||||
# Windows API库
|
||||
if(WIN32)
|
||||
find_library(GDI32_LIBRARY gdi32)
|
||||
find_library(USER32_LIBRARY user32)
|
||||
find_library(KERNEL32_LIBRARY kernel32)
|
||||
set(WIN32_LIBS ${GDI32_LIBRARY} ${USER32_LIBRARY} ${KERNEL32_LIBRARY})
|
||||
endif()
|
||||
|
||||
# 创建库
|
||||
file(GLOB_RECURSE SOURCES "src/*.cpp")
|
||||
add_library(StellarX STATIC ${SOURCES})
|
||||
|
||||
# 链接Windows库
|
||||
if(WIN32)
|
||||
target_link_libraries(StellarX ${WIN32_LIBS})
|
||||
endif()
|
||||
|
||||
# 设置库属性
|
||||
set_target_properties(StellarX PROPERTIES
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION 1
|
||||
PUBLIC_HEADER "include/StellarX/StellarX.h"
|
||||
OUTPUT_NAME "StellarX"
|
||||
)
|
||||
|
||||
# 安装规则
|
||||
install(DIRECTORY include/StellarX DESTINATION include)
|
||||
install(TARGETS StellarX
|
||||
ARCHIVE DESTINATION lib
|
||||
LIBRARY DESTINATION lib
|
||||
RUNTIME DESTINATION bin
|
||||
PUBLIC_HEADER DESTINATION include/StellarX
|
||||
)
|
||||
|
||||
# 示例程序
|
||||
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/examples)
|
||||
file(GLOB EXAMPLE_SOURCES "examples/*.cpp")
|
||||
foreach(example_source ${EXAMPLE_SOURCES})
|
||||
get_filename_component(example_name ${example_source} NAME_WE)
|
||||
add_executable(${example_name} ${example_source})
|
||||
target_link_libraries(${example_name} StellarX ${WIN32_LIBS})
|
||||
|
||||
# 设置Windows子系统
|
||||
if(WIN32)
|
||||
set_target_properties(${example_name} PROPERTIES
|
||||
LINK_FLAGS "/SUBSYSTEM:WINDOWS"
|
||||
)
|
||||
endif()
|
||||
|
||||
# 安装示例
|
||||
install(TARGETS ${example_name} DESTINATION bin)
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# 文档生成选项
|
||||
option(BUILD_DOCS "Build documentation" OFF)
|
||||
if(BUILD_DOCS)
|
||||
find_package(Doxygen)
|
||||
if(Doxygen_FOUND)
|
||||
set(DOXYGEN_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/docs)
|
||||
set(DOXYGEN_USE_MDFILE_AS_MAINPAGE ${CMAKE_CURRENT_SOURCE_DIR}/README.md)
|
||||
set(DOXYGEN_PROJECT_NAME "StellarX GUI Framework")
|
||||
set(DOXYGEN_PROJECT_NUMBER ${PROJECT_VERSION})
|
||||
set(DOXYGEN_PROJECT_BRIEF "A lightweight, modular C++ GUI framework for Windows")
|
||||
set(DOXYGEN_INPUT ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
set(DOXYGEN_RECURSIVE YES)
|
||||
set(DOXYGEN_EXTRACT_ALL YES)
|
||||
set(DOXYGEN_EXTRACT_PRIVATE YES)
|
||||
set(DOXYGEN_EXTRACT_STATIC YES)
|
||||
set(DOXYGEN_SOURCE_BROWSER YES)
|
||||
set(DOXYGEN_GENERATE_TREEVIEW YES)
|
||||
set(DOXYGEN_HAVE_DOT YES)
|
||||
set(DOXYGEN_CALL_GRAPH YES)
|
||||
set(DOXYGEN_CALLER_GRAPH YES)
|
||||
|
||||
doxygen_add_docs(
|
||||
docs
|
||||
${DOXYGEN_INPUT}
|
||||
COMMENT "Generate HTML documentation"
|
||||
)
|
||||
|
||||
install(DIRECTORY ${DOXYGEN_OUTPUT_DIRECTORY}/html DESTINATION docs)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# 打包配置
|
||||
include(InstallRequiredSystemLibraries)
|
||||
set(CPACK_PACKAGE_NAME ${PROJECT_NAME})
|
||||
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
|
||||
set(CPACK_PACKAGE_DESCRIPTION "A lightweight, modular C++ GUI framework for Windows")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "星垣 (StellarX) - A lightweight Windows GUI framework")
|
||||
set(CPACK_PACKAGE_VENDOR "StellarX Development Team")
|
||||
set(CPACK_PACKAGE_CONTACT "contact@stellarx.dev")
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
|
||||
set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "StellarX")
|
||||
set(CPACK_NSIS_MODIFY_PATH ON)
|
||||
|
||||
if(WIN32)
|
||||
set(CPACK_GENERATOR "ZIP;NSIS")
|
||||
else()
|
||||
set(CPACK_GENERATOR "ZIP")
|
||||
endif()
|
||||
|
||||
include(CPack)
|
||||
@@ -0,0 +1,93 @@
|
||||
# 为 StellarX 做贡献
|
||||
|
||||
感谢您有兴趣为 StellarX 做出贡献!本文档提供了贡献的指南和步骤。
|
||||
|
||||
StellarX 是一个为 **Windows 平台**打造的 C++ GUI 框架,基于 EasyX 图形库。
|
||||
|
||||
## 开发环境设置
|
||||
|
||||
1. 安装 Visual Studio 2019 或更高版本
|
||||
2. 安装对应版本的 EasyX 图形库
|
||||
3. 安装 CMake 3.12 或更高版本
|
||||
4. 克隆项目仓库
|
||||
5. 使用 CMake 生成解决方案并编译
|
||||
|
||||
## 如何贡献
|
||||
|
||||
### 报告错误
|
||||
1. 检查 [Issues](../../issues) 看看这个错误是否已经被报告过。
|
||||
2. 如果没有,请创建一个新的 Issue。
|
||||
3. 使用 "Bug 报告" 模板。
|
||||
4. 提供**清晰的标题和描述**。
|
||||
5. 包括相关的代码片段、截图或重现问题的步骤。
|
||||
|
||||
### 建议功能增强
|
||||
1. 检查现有的 Issue 中是否有你的想法。
|
||||
2. 使用 "功能请求" 模板创建一个新的 Issue。
|
||||
3. 清晰地描述新功能,并解释它为何有用。
|
||||
|
||||
### 提交代码更改 (拉取请求 Pull Requests)
|
||||
1. **Fork** GitHub 上的仓库。
|
||||
2. **Clone** 你 fork 的仓库到本地机器。
|
||||
3. 为你的功能或错误修复创建一个**新分支** (`git checkout -b my-feature-branch`)。
|
||||
4. **进行你的更改**。确保你的代码遵循项目的风格(见下文)。
|
||||
5. **提交你的更改**,并附上清晰描述性的提交信息。
|
||||
6. **Push** 你的分支到你 fork 的 GitHub 仓库 (`git push origin my-feature-branch`)。
|
||||
7. 针对原始 StellarX 仓库的 `main` 分支发起一个**拉取请求 (Pull Request)**。
|
||||
|
||||
## 代码风格指南
|
||||
|
||||
* 遵循项目中现有的代码格式和命名约定。
|
||||
* 为变量、函数和类使用有意义的名称。
|
||||
* 在必要时注释你的代码,特别是复杂的逻辑。
|
||||
* 确保你的代码编译时没有警告。
|
||||
* 彻底测试你的更改。
|
||||
* 使用 **4个空格** 进行缩进(不要使用制表符)
|
||||
- 类名使用 **PascalCase**(如 `ClassName`)
|
||||
- 函数和变量使用 **camelCase**(如 `functionName`, `variableName`)
|
||||
- 常量使用 **UPPER_CASE**(如 `CONSTANT_VALUE`)
|
||||
- 成员变量以 **m_** 前缀(如 `m_memberVariable`)
|
||||
- 控件属性使用有意义的名称,避免缩写
|
||||
- 为所有公共接口添加详细的注释
|
||||
- 遵循 RAII 原则管理资源
|
||||
|
||||
## 示例代码风格
|
||||
|
||||
```c++
|
||||
|
||||
// 好的示例
|
||||
class MyControl : public Control {
|
||||
public:
|
||||
MyControl(int x, int y, int width, int height)
|
||||
: Control(x, y, width, height), m_isActive(false) {}
|
||||
|
||||
void draw() override {
|
||||
// 绘制逻辑
|
||||
}
|
||||
|
||||
private:
|
||||
bool m_isActive;
|
||||
};
|
||||
|
||||
// 不好的示例
|
||||
class my_control: public Control{
|
||||
public:
|
||||
my_control(int x,int y,int w,int h):Control(x,y,w,h),active(false){}
|
||||
void Draw() override{
|
||||
// 绘制逻辑
|
||||
}
|
||||
private:
|
||||
bool active;
|
||||
};
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
请遵循项目的目录结构:
|
||||
- 头文件放在 `include/StellarX/` 目录
|
||||
- 实现文件放在 `src/` 目录
|
||||
- 示例代码放在 `examples/` 目录
|
||||
|
||||
## 问题?
|
||||
|
||||
如果你有任何问题,可以随时开一个 Issue 或联系维护者。
|
||||
-575
@@ -1,575 +0,0 @@
|
||||
#include "Canvas.h"
|
||||
#include "SxLog.h"
|
||||
#include "Window.h"
|
||||
|
||||
static bool SxIsNoisyMsg(UINT m)
|
||||
{
|
||||
return m == WM_MOUSEMOVE;
|
||||
}
|
||||
|
||||
static const char* SxCanvasMsgName(UINT m)
|
||||
{
|
||||
switch (m)
|
||||
{
|
||||
case WM_MOUSEMOVE: return "WM_MOUSEMOVE";
|
||||
case WM_LBUTTONDOWN: return "WM_LBUTTONDOWN";
|
||||
case WM_LBUTTONUP: return "WM_LBUTTONUP";
|
||||
case WM_KEYDOWN: return "WM_KEYDOWN";
|
||||
case WM_KEYUP: return "WM_KEYUP";
|
||||
default: return "WM_UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
enum class SxCanvasOverlayRedrawMode
|
||||
{
|
||||
None,
|
||||
RefreshSnapshot
|
||||
};
|
||||
|
||||
bool SxCanvasRectValid(const RECT& rc)
|
||||
{
|
||||
return rc.right > rc.left && rc.bottom > rc.top;
|
||||
}
|
||||
|
||||
bool SxCanvasRectsIntersect(const RECT& a, const RECT& b)
|
||||
{
|
||||
return a.left < b.right && a.right > b.left &&
|
||||
a.top < b.bottom && a.bottom > b.top;
|
||||
}
|
||||
|
||||
RECT SxCanvasUnionRect(const RECT& a, const RECT& b)
|
||||
{
|
||||
RECT out{};
|
||||
out.left = (std::min)(a.left, b.left);
|
||||
out.top = (std::min)(a.top, b.top);
|
||||
out.right = (std::max)(a.right, b.right);
|
||||
out.bottom = (std::max)(a.bottom, b.bottom);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
Canvas::Canvas()
|
||||
:Control(0, 0, 100, 100)
|
||||
{
|
||||
this->id = "Canvas";
|
||||
// Canvas 是通用容器,当前阶段显式允许双轴 Stretch。
|
||||
this->layoutCapability.allowStretchX = true;
|
||||
this->layoutCapability.allowStretchY = true;
|
||||
}
|
||||
|
||||
Canvas::Canvas(int x, int y, int width, int height)
|
||||
:Control(x, y, width, height)
|
||||
{
|
||||
this->id = "Canvas";
|
||||
this->layoutCapability.allowStretchX = true;
|
||||
this->layoutCapability.allowStretchY = true;
|
||||
}
|
||||
|
||||
void Canvas::relayoutManagedChildren()
|
||||
{
|
||||
// Canvas 负责子控件从“父局部设计矩形”到“当前世界矩形”的转换。
|
||||
// 当 Canvas 自己的位置或尺寸变化后,所有受它管理的子控件都要重新走一次统一解算。
|
||||
for (auto& child : controls)
|
||||
{
|
||||
const StellarX::ResolvedLayoutRect rect =
|
||||
child->resolveLayoutRect(localWidth, localHeight, x, y, width, height);
|
||||
child->applyResolvedLayoutRect(rect);
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::setX(int x)
|
||||
{
|
||||
// 公开 setter 在 Canvas 上不能再视为“单纯改自己的 x”:
|
||||
// 一旦容器移动,子控件的世界坐标也必须整体重算。
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
relayoutManagedChildren();
|
||||
onWindowResize();
|
||||
}
|
||||
|
||||
void Canvas::setY(int y)
|
||||
{
|
||||
applyRuntimeRectDirect(this->x, y, width, height);
|
||||
relayoutManagedChildren();
|
||||
onWindowResize();
|
||||
}
|
||||
|
||||
void Canvas::setWidth(int width)
|
||||
{
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
relayoutManagedChildren();
|
||||
onWindowResize();
|
||||
}
|
||||
|
||||
void Canvas::setHeight(int height)
|
||||
{
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
relayoutManagedChildren();
|
||||
onWindowResize();
|
||||
}
|
||||
|
||||
void Canvas::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
|
||||
{
|
||||
// 统一解算器已经给出当前运行态世界矩形;
|
||||
// Canvas 在应用自身矩形后,还必须继续刷新全部子控件。
|
||||
applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height);
|
||||
relayoutManagedChildren();
|
||||
}
|
||||
|
||||
void Canvas::clearAllControls()
|
||||
{
|
||||
controls.clear();
|
||||
}
|
||||
|
||||
void Canvas::draw()
|
||||
{
|
||||
if (!dirty || !show)
|
||||
{
|
||||
for (auto& control : controls)
|
||||
if (auto c = dynamic_cast<Table*>(control.get()))
|
||||
c->draw();
|
||||
return;
|
||||
}
|
||||
saveStyle();
|
||||
setlinecolor(canvasBorderClor);//设置线色
|
||||
if (StellarX::FillMode::Null != canvasFillMode)
|
||||
setfillcolor(canvasBkClor);//设置填充色
|
||||
setfillstyle((int)canvasFillMode);//设置填充模式
|
||||
setlinestyle((int)canvasLineStyle, canvaslinewidth);
|
||||
|
||||
// 在绘制画布之前,先恢复并更新背景快照:
|
||||
// 1. 如果已有快照,则先回贴旧快照以清除之前的内容。
|
||||
// 2. 当坐标或尺寸变化,或缓存图像无效时,丢弃旧快照并重新抓取新的背景。
|
||||
int margin = canvaslinewidth > 1 ? canvaslinewidth : 1;
|
||||
if (hasSnap)
|
||||
{
|
||||
// 恢复旧快照,清除上一次绘制
|
||||
restBackground();
|
||||
// 如果位置或尺寸变了,或没有有效缓存,则重新抓取
|
||||
if (!saveBkImage || saveBkX != this->x - margin || saveBkY != this->y - margin || saveWidth != this->width + margin * 2 || saveHeight != this->height + margin * 2)
|
||||
{
|
||||
invalidateBackgroundSnapshot();
|
||||
saveBackground(this->x - margin, this->y - margin, this->width + margin * 2, this->height + margin * 2);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 首次绘制或没有快照时直接抓取背景
|
||||
saveBackground(this->x - margin, this->y - margin, this->width + margin * 2, this->height + margin * 2);
|
||||
}
|
||||
// 再次恢复最新快照,确保绘制区域干净
|
||||
restBackground();
|
||||
//根据画布形状绘制
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
fillrectangle(x, y, x + width, y + height);//有边框填充矩形
|
||||
break;
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
solidrectangle(x, y, x + width, y + height);//无边框填充矩形
|
||||
break;
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//有边框填充圆角矩形
|
||||
break;
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//无边框填充圆角矩形
|
||||
break;
|
||||
}
|
||||
// 绘制所有子控件
|
||||
for (auto& control : controls)
|
||||
{
|
||||
control->setDirty(true);
|
||||
control->draw();
|
||||
}
|
||||
|
||||
restoreStyle();
|
||||
dirty = false; //标记画布不需要重绘
|
||||
}
|
||||
|
||||
bool Canvas::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
if (!show) return false;
|
||||
resetEventVisualChanged();
|
||||
|
||||
bool consumed = false;
|
||||
bool anyDirty = false;
|
||||
bool anyVisualChanged = false;
|
||||
Control* firstConsumer = nullptr;
|
||||
|
||||
if (msg.message == WM_MOUSEMOVE)
|
||||
{
|
||||
// WM_MOUSEMOVE 需要特殊处理:
|
||||
// - 第一个命中的兄弟分支收到真实消息;
|
||||
// - 后续兄弟不再重新命中,只清理旧 hover / tooltip 等临时状态。
|
||||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||||
{
|
||||
Control* c = it->get();
|
||||
if (!consumed)
|
||||
{
|
||||
bool cConsumed = c->handleEvent(msg);
|
||||
if (c->didEventAffectVisual()) anyVisualChanged = true;
|
||||
if (cConsumed)
|
||||
{
|
||||
firstConsumer = c;
|
||||
consumed = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 后续兄弟只走临时状态清理,不会再进入自己的 handleEvent()。
|
||||
// Tooltip 隐藏会先回贴旧快照,再改变 coverage;因此必须先保存旧覆盖范围,
|
||||
// 避免登记重绘时丢失旧 Tooltip 区域,导致上层 overlay 补画判断不完整。
|
||||
const RECT previousCoverage = c->getManagedRepaintCoverageRect();
|
||||
if (c->clearTransientMouseState())
|
||||
{
|
||||
if (Window* host = getHostWindow())
|
||||
host->requestManagedRepaint(c, previousCoverage);
|
||||
anyVisualChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||||
{
|
||||
Control* c = it->get();
|
||||
bool cConsumed = c->handleEvent(msg);
|
||||
|
||||
if (c->isDirty()) anyDirty = true;
|
||||
if (c->didEventAffectVisual()) anyVisualChanged = true;
|
||||
|
||||
if (cConsumed)
|
||||
{
|
||||
firstConsumer = c;
|
||||
consumed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstConsumer && !SxIsNoisyMsg(msg.message))
|
||||
{
|
||||
SX_LOGD("Event") << SX_T("Canvas 消耗消息: ","Canvas consumed: ")
|
||||
<< SxCanvasMsgName(msg.message)
|
||||
<< SX_T(" 子控件 id=", " childId=") << firstConsumer->getId();
|
||||
}
|
||||
|
||||
if (anyDirty)
|
||||
{
|
||||
// 只要任一子控件因本次事件进入 dirty,就把这笔重绘继续向上汇报。
|
||||
// 在托管模式下,这不会立即绘制,而是登记为 Canvas 对应的重绘 root。
|
||||
if (!SxIsNoisyMsg(msg.message))
|
||||
SX_LOGD("Dirty") << SX_T("Canvas检测有控件为脏状态 -> 请求重绘, ","Canvas anyDirty -> requestRepaint, ")<<"id = " << id;
|
||||
requestRepaint(parent);
|
||||
}
|
||||
markEventVisualChanged(anyVisualChanged);
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
bool Canvas::clearTransientMouseState()
|
||||
{
|
||||
bool changed = false;
|
||||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||||
{
|
||||
Control* child = it->get();
|
||||
if (!child->IsVisible())
|
||||
continue;
|
||||
if (child->clearTransientMouseState())
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
|
||||
void Canvas::addControl(std::unique_ptr<Control> control)
|
||||
{
|
||||
control->setParent(this);
|
||||
// 新子控件加入容器时,立刻按“当前容器运行态矩形”解算一次,
|
||||
// 避免后续第一次 draw / resize 前 world 坐标仍停留在设计态。
|
||||
const StellarX::ResolvedLayoutRect rect =
|
||||
control->resolveLayoutRect(localWidth, localHeight, this->x, this->y, this->width, this->height);
|
||||
control->applyResolvedLayoutRect(rect);
|
||||
SX_LOGI("Canvas")
|
||||
<< SX_T("添加子控件:父=Canvas 子id=", "addControl: parent=Canvas childId=")
|
||||
<< control->getId()
|
||||
<< SX_T(" 相对坐标=(", " local=(")
|
||||
<< control->getLocalX() << "," << control->getLocalY()
|
||||
<< SX_T(") 绝对坐标=(", ") abs=(")
|
||||
<< control->getX() << "," << control->getY()
|
||||
<< ")";
|
||||
|
||||
|
||||
controls.push_back(std::move(control));
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Canvas::setShape(StellarX::ControlShape shape)
|
||||
{
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
this->shape = shape;
|
||||
dirty = true;
|
||||
break;
|
||||
case StellarX::ControlShape::CIRCLE:
|
||||
case StellarX::ControlShape::B_CIRCLE:
|
||||
case StellarX::ControlShape::ELLIPSE:
|
||||
case StellarX::ControlShape::B_ELLIPSE:
|
||||
this->shape = StellarX::ControlShape::RECTANGLE;
|
||||
dirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::setCanvasfillMode(StellarX::FillMode mode)
|
||||
{
|
||||
this->canvasFillMode = mode;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Canvas::setBorderColor(COLORREF color)
|
||||
{
|
||||
this->canvasBorderClor = color;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Canvas::setCanvasBkColor(COLORREF color)
|
||||
{
|
||||
this->canvasBkClor = color;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Canvas::setCanvasLineStyle(StellarX::LineStyle style)
|
||||
{
|
||||
this->canvasLineStyle = style;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Canvas::setLinewidth(int width)
|
||||
{
|
||||
this->canvaslinewidth = width;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Canvas::setIsVisible(bool visible)
|
||||
{
|
||||
this->show = visible;
|
||||
dirty = true;
|
||||
for (auto& control : controls)
|
||||
{
|
||||
control->setIsVisible(visible);
|
||||
}
|
||||
if (!visible)
|
||||
discardBackground();
|
||||
}
|
||||
|
||||
void Canvas::setDirty(bool dirty)
|
||||
{
|
||||
this->dirty = dirty;
|
||||
for (auto& control : controls)
|
||||
control->setDirty(dirty);
|
||||
}
|
||||
|
||||
void Canvas::onWindowResize()
|
||||
{
|
||||
// resize 语义已收口:
|
||||
// Canvas 不再在这里重新解算布局,只负责丢快照、标脏,并向子控件传播“环境已变化”。
|
||||
Control::onWindowResize();
|
||||
for (auto& child : controls)
|
||||
child->onWindowResize();
|
||||
}
|
||||
|
||||
void Canvas::requestRepaint(Control* parent)
|
||||
{
|
||||
if (shouldDeferManagedRepaint())
|
||||
{
|
||||
// 托管路径:由 Window 统一决定这次是否只重画本 Canvas,还是升级为补画 Dialog / 整体场景。
|
||||
if (auto* host = getHostWindow())
|
||||
host->requestManagedRepaint(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this == parent)
|
||||
{
|
||||
if (!show)
|
||||
return;
|
||||
|
||||
// 关键护栏:
|
||||
// - Canvas 自己是脏的 / 没有快照 / 缓存图为空
|
||||
// => 禁止局部重绘,直接升级为一次完整 draw(先把 dirty 置真,避免 draw() 早退)
|
||||
if (dirty || !hasSnap || !saveBkImage)
|
||||
{
|
||||
SX_LOG_TRACE("Dirty")
|
||||
<< SX_T("Canvas 局部重绘降级为全量重绘: id=", "Canvas partial->full draw: id=")
|
||||
<< id
|
||||
<< " dirty=" << (dirty ? 1 : 0)
|
||||
<< " hasSnap=" << (hasSnap ? 1 : 0);
|
||||
|
||||
this->dirty = true;
|
||||
this->draw();
|
||||
return;
|
||||
}
|
||||
|
||||
SX_LOG_TRACE("Dirty") << SX_T("Canvas 请求局部重绘:id=", "Canvas::requestRepaint(partial): id=") << id;
|
||||
|
||||
RECT paintCoverage{};
|
||||
bool hasPaintCoverage = false;
|
||||
RECT persistentCoverage{};
|
||||
bool hasPersistentCoverage = false;
|
||||
auto commitManagedChild = [&](Control* child, SxCanvasOverlayRedrawMode overlayMode)
|
||||
{
|
||||
if (!child || !child->IsVisible())
|
||||
return;
|
||||
|
||||
const bool directDirty = child->isDirty();
|
||||
const bool subtreeDirty = child->hasManagedDirtySubtree();
|
||||
|
||||
if (overlayMode == SxCanvasOverlayRedrawMode::RefreshSnapshot)
|
||||
{
|
||||
// overlay 补画必须先作废旧快照:
|
||||
// 下层兄弟的持久内容刚刚已经写过像素,若继续沿用旧快照,会把旧背景再贴回来。
|
||||
child->invalidateBackgroundSnapshot();
|
||||
child->setDirty(true);
|
||||
child->draw();
|
||||
}
|
||||
else if (directDirty)
|
||||
{
|
||||
child->draw();
|
||||
}
|
||||
else if (subtreeDirty)
|
||||
{
|
||||
// 这次真正脏的是“child 下面的子树”,而不是 child 自身。
|
||||
// 例如嵌套 Canvas 中,第二层/第三层按钮脏了,但第一层 Canvas 自己并不 dirty。
|
||||
// 这里必须把这条直接子分支提交下去,否则深层按钮状态永远没有机会被真正画出来。
|
||||
child->commitManagedRepaint();
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const RECT childPaintRect = child->getManagedRepaintCoverageRect();
|
||||
if (!hasPaintCoverage)
|
||||
{
|
||||
paintCoverage = childPaintRect;
|
||||
hasPaintCoverage = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
paintCoverage = SxCanvasUnionRect(paintCoverage, childPaintRect);
|
||||
}
|
||||
|
||||
const RECT childPersistentRect = child->getManagedRepaintPersistentCoverageRect();
|
||||
if (!hasPersistentCoverage)
|
||||
{
|
||||
persistentCoverage = childPersistentRect;
|
||||
hasPersistentCoverage = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
persistentCoverage = SxCanvasUnionRect(persistentCoverage, childPersistentRect);
|
||||
}
|
||||
};
|
||||
|
||||
for (auto& control : controls)
|
||||
{
|
||||
Control* child = control.get();
|
||||
if (!child->IsVisible())
|
||||
continue;
|
||||
|
||||
if (child->hasManagedDirtySubtree())
|
||||
{
|
||||
commitManagedChild(child, SxCanvasOverlayRedrawMode::None);
|
||||
}
|
||||
else if (hasPaintCoverage && SxCanvasRectsIntersect(child->getManagedRepaintCoverageRect(), paintCoverage))
|
||||
{
|
||||
// 位于本次累计 coverage 上方、且发生相交的兄弟控件,需要补画回最上层。
|
||||
// 但只有下层“持久内容”影响到它时,才允许作废并重新抓背景快照;
|
||||
// 如果只是被 Tooltip 等临时浮层覆盖,则跳过兄弟补画,避免透明控件回贴旧快照擦掉 Tooltip。
|
||||
const bool persistentHit = hasPersistentCoverage &&
|
||||
SxCanvasRectsIntersect(child->getManagedRepaintPersistentCoverageRect(), persistentCoverage);
|
||||
if (persistentHit)
|
||||
commitManagedChild(child, SxCanvasOverlayRedrawMode::RefreshSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SX_LOG_TRACE("Dirty") << SX_T("Canvas 请求根级重绘:id=", "Canvas::requestRepaint(root): id=") << id;
|
||||
onRequestRepaintAsRoot();
|
||||
}
|
||||
|
||||
bool Canvas::hasManagedDirtySubtree() const
|
||||
{
|
||||
if (dirty)
|
||||
return true;
|
||||
|
||||
for (const auto& child : controls)
|
||||
{
|
||||
if (!child->IsVisible())
|
||||
continue;
|
||||
if (child->hasManagedDirtySubtree())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT Canvas::getManagedRepaintCoverageRect() const
|
||||
{
|
||||
// Canvas::draw() 会先写自身背景,再强制绘制全部可见子控件。
|
||||
// 因此它的实际 coverage 不能只看本体矩形,还要把可见子控件 coverage 递归并入。
|
||||
RECT coverage = getBoundsRect();
|
||||
for (const auto& child : controls)
|
||||
{
|
||||
if (!child->IsVisible())
|
||||
continue;
|
||||
coverage = SxCanvasUnionRect(coverage, child->getManagedRepaintCoverageRect());
|
||||
}
|
||||
return coverage;
|
||||
}
|
||||
|
||||
RECT Canvas::getManagedRepaintPersistentCoverageRect() const
|
||||
{
|
||||
// 持久 coverage 只描述会进入背景快照语义的范围。
|
||||
// 子控件 Tooltip 等临时浮层不会被并入,避免兄弟控件补画时抓到临时像素。
|
||||
RECT coverage = getBoundsRect();
|
||||
for (const auto& child : controls)
|
||||
{
|
||||
if (!child->IsVisible())
|
||||
continue;
|
||||
coverage = SxCanvasUnionRect(coverage, child->getManagedRepaintPersistentCoverageRect());
|
||||
}
|
||||
return coverage;
|
||||
}
|
||||
|
||||
bool Canvas::canCommitManagedPartialRepaint() const
|
||||
{
|
||||
// Canvas 只有在“自己本体不脏 + 仍持有有效背景快照”时,
|
||||
// 才能安全地做局部提交(即只更新内部脏子控件)。
|
||||
return show && !dirty && hasValidBackgroundSnapshot();
|
||||
}
|
||||
|
||||
void Canvas::commitManagedRepaint()
|
||||
{
|
||||
if (!show)
|
||||
return;
|
||||
|
||||
if (canCommitManagedPartialRepaint())
|
||||
{
|
||||
// 快照完好:沿用 Canvas 自己已有的局部重绘逻辑。
|
||||
requestRepaint(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// 自身已经脏了,或快照失效:必须升级为整 Canvas 重画。
|
||||
this->dirty = true;
|
||||
onRequestRepaintAsRoot();
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* @类: Canvas
|
||||
* @摘要: 画布容器控件,用于分组和管理子控件
|
||||
* @描述:
|
||||
* 作为其他控件的父容器,提供统一的背景和边框样式。
|
||||
* 负责将事件传递给子控件并管理它们的绘制顺序。
|
||||
* 在托管重绘模式下,Canvas 通常作为一组子控件的安全重绘 root。
|
||||
*
|
||||
* @特性:
|
||||
* - 支持四种矩形形状(普通、圆角,各有边框和无边框版本)
|
||||
* - 可自定义填充模式、边框颜色和背景颜色
|
||||
* - 自动管理子控件的生命周期和事件传递
|
||||
* - 支持嵌套容器结构
|
||||
*
|
||||
* @使用场景: 用于分组相关控件、实现复杂布局或作为对话框基础
|
||||
* @所属框架: 星垣(StellarX) GUI框架
|
||||
* @作者: 我在人间做废物
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
#include "Control.h"
|
||||
#include"Table.h"
|
||||
|
||||
class Canvas : public Control
|
||||
{
|
||||
protected:
|
||||
std::vector<std::unique_ptr<Control>> controls;
|
||||
|
||||
StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE; //容器形状
|
||||
StellarX::FillMode canvasFillMode = StellarX::FillMode::Solid; //容器填充模式
|
||||
StellarX::LineStyle canvasLineStyle = StellarX::LineStyle::Solid; //线型
|
||||
int canvaslinewidth = 1; //线宽
|
||||
|
||||
COLORREF canvasBorderClor = RGB(0, 0, 0); //边框颜色
|
||||
COLORREF canvasBkClor = RGB(255, 255, 255); //背景颜色
|
||||
|
||||
// 清除所有子控件
|
||||
void clearAllControls();
|
||||
public:
|
||||
// 默认构造:创建一个基础矩形容器
|
||||
Canvas();
|
||||
// 指定初始位置和尺寸构造容器
|
||||
Canvas(int x, int y, int width, int height);
|
||||
~Canvas() {}
|
||||
|
||||
// 修改容器运行态 X,并同步重算全部受管理子控件的世界坐标
|
||||
void setX(int x)override;
|
||||
// 修改容器运行态 Y,并同步重算全部受管理子控件的世界坐标
|
||||
void setY(int y)override;
|
||||
// 修改容器运行态宽度,并同步重算全部受管理子控件的世界坐标
|
||||
void setWidth(int width) override;
|
||||
// 修改容器运行态高度,并同步重算全部受管理子控件的世界坐标
|
||||
void setHeight(int height) override;
|
||||
|
||||
//绘制容器及其子控件
|
||||
void draw() override;
|
||||
bool handleEvent(const ExMessage& msg) override;
|
||||
// 递归清理子树中的 hover / tooltip / 临时按下态,不把 Canvas 自己升级为整块重绘源。
|
||||
bool clearTransientMouseState() override;
|
||||
// 添加一个由 Canvas 托管的子控件
|
||||
void addControl(std::unique_ptr<Control> control);
|
||||
//设置容器样式
|
||||
void setShape(StellarX::ControlShape shape);
|
||||
//设置容器填充模式
|
||||
void setCanvasfillMode(StellarX::FillMode mode);
|
||||
//设置容器边框颜色
|
||||
void setBorderColor(COLORREF color);
|
||||
//设置填充颜色
|
||||
void setCanvasBkColor(COLORREF color);
|
||||
//设置线形
|
||||
void setCanvasLineStyle(StellarX::LineStyle style);
|
||||
//设置线段宽度
|
||||
void setLinewidth(int width);
|
||||
//设置不可见后传递给子控件重写
|
||||
void setIsVisible(bool visible) override;
|
||||
void setDirty(bool dirty) override;
|
||||
void onWindowResize() override;
|
||||
// 托管模式下登记为 root;非托管模式下走局部或根级重绘
|
||||
void requestRepaint(Control* parent)override;
|
||||
bool hasManagedDirtySubtree() const override;
|
||||
RECT getManagedRepaintCoverageRect() const override;
|
||||
RECT getManagedRepaintPersistentCoverageRect() const override;
|
||||
// 判断当前 Canvas 是否可安全做局部提交
|
||||
bool canCommitManagedPartialRepaint() const override;
|
||||
// 托管收口阶段执行 Canvas 的真正重绘
|
||||
void commitManagedRepaint() override;
|
||||
// 获取直接子控件列表
|
||||
std::vector<std::unique_ptr<Control>>& getControls() { return controls; }
|
||||
protected:
|
||||
// 统一解算后,按当前运行态矩形把所有受管理子控件重新映射到新的世界坐标。
|
||||
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
|
||||
private:
|
||||
// 容器自己的几何变化后,需要统一刷新所有子控件的运行态矩形。
|
||||
void relayoutManagedChildren();
|
||||
//用来检查对话框是否模态,此控件不做实现
|
||||
bool model() const override { return false; };
|
||||
};
|
||||
-667
@@ -1,667 +0,0 @@
|
||||
#include "Control.h"
|
||||
#include "SxLog.h"
|
||||
#include<assert.h>
|
||||
#include "Window.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace
|
||||
{
|
||||
// 单轴解算后的临时结果:只描述“在父局部坐标系中的位置和尺寸”。
|
||||
struct LayoutAxisResult
|
||||
{
|
||||
int pos = 0;
|
||||
int size = 0;
|
||||
};
|
||||
|
||||
int ClampNonNegative(int value)
|
||||
{
|
||||
return value < 0 ? 0 : value;
|
||||
}
|
||||
|
||||
int RoundDiv(long long numerator, long long denominator)
|
||||
{
|
||||
if (denominator == 0)
|
||||
return 0;
|
||||
if (numerator >= 0)
|
||||
return static_cast<int>((numerator + denominator / 2) / denominator);
|
||||
return -static_cast<int>(((-numerator) + denominator / 2) / denominator);
|
||||
}
|
||||
|
||||
// 旧接口兼容层:
|
||||
// 将 setAnchor(a1, a2) 的双锚点输入映射为单轴 LayoutSpec。
|
||||
// 这里只负责“从旧输入翻译成新模型”,不直接参与后续求解。
|
||||
StellarX::AxisLayoutSpec BuildLegacyAxisSpec(StellarX::Anchor anchor1, StellarX::Anchor anchor2,
|
||||
StellarX::Anchor startAnchor, StellarX::Anchor endAnchor)
|
||||
{
|
||||
StellarX::AxisLayoutSpec spec{};
|
||||
spec.anchorStart = (anchor1 == startAnchor || anchor2 == startAnchor);
|
||||
spec.anchorEnd = (anchor1 == endAnchor || anchor2 == endAnchor);
|
||||
|
||||
if (spec.anchorStart && spec.anchorEnd)
|
||||
{
|
||||
spec.sizePolicy = StellarX::AxisSizePolicy::Stretch;
|
||||
spec.alignPolicy = StellarX::AxisAlignPolicy::Start;
|
||||
}
|
||||
else if (spec.anchorEnd)
|
||||
{
|
||||
spec.sizePolicy = StellarX::AxisSizePolicy::FixedSize;
|
||||
spec.alignPolicy = StellarX::AxisAlignPolicy::End;
|
||||
}
|
||||
else
|
||||
{
|
||||
spec.sizePolicy = StellarX::AxisSizePolicy::FixedSize;
|
||||
spec.alignPolicy = StellarX::AxisAlignPolicy::Start;
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
// 单轴统一解算器:
|
||||
// 输入设计态父尺寸、当前父尺寸、子控件设计态位置/尺寸,以及当前轴策略,
|
||||
// 输出该轴在“父局部坐标系”下的运行态位置与尺寸。
|
||||
LayoutAxisResult ResolveAxis(int parentDesignSize, int parentCurrentSize, int childDesignPos, int childDesignSize,
|
||||
const StellarX::AxisLayoutSpec& spec, bool allowStretch)
|
||||
{
|
||||
LayoutAxisResult result{};
|
||||
const int startMargin = childDesignPos;
|
||||
const int endMargin = parentDesignSize - (childDesignPos + childDesignSize);
|
||||
|
||||
const bool wantsStretch = spec.sizePolicy == StellarX::AxisSizePolicy::Stretch;
|
||||
const bool canStretch = allowStretch && spec.anchorStart && spec.anchorEnd;
|
||||
|
||||
if (wantsStretch && canStretch)
|
||||
{
|
||||
// 双边锚定 + Stretch:
|
||||
// 保持设计态中的起边距和终边距,让尺寸随父容器变化。
|
||||
result.pos = startMargin;
|
||||
result.size = ClampNonNegative(parentCurrentSize - startMargin - endMargin);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 其余情况一律按固定尺寸处理;即便调用方请求 Stretch,
|
||||
// 只要锚点条件或控件能力边界不满足,也会在这里自然降级为 FixedSize。
|
||||
result.size = ClampNonNegative(childDesignSize);
|
||||
|
||||
if (spec.anchorStart && !spec.anchorEnd)
|
||||
{
|
||||
// 仅锚定起边:保持起边距,尺寸不变。
|
||||
result.pos = startMargin;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!spec.anchorStart && spec.anchorEnd)
|
||||
{
|
||||
// 仅锚定终边:保持终边距,尺寸不变。
|
||||
result.pos = parentCurrentSize - endMargin - result.size;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (spec.anchorStart && spec.anchorEnd)
|
||||
{
|
||||
// 双边锚定但尺寸固定:
|
||||
// 位置由 alignPolicy 决定,用于表达“只位移、不拉伸”的场景。
|
||||
switch (spec.alignPolicy)
|
||||
{
|
||||
case StellarX::AxisAlignPolicy::End:
|
||||
result.pos = parentCurrentSize - endMargin - result.size;
|
||||
break;
|
||||
case StellarX::AxisAlignPolicy::Center:
|
||||
// 保持相对父容器中心的偏移关系。
|
||||
result.pos = childDesignPos + (parentCurrentSize - parentDesignSize) / 2;
|
||||
break;
|
||||
case StellarX::AxisAlignPolicy::Proportional:
|
||||
{
|
||||
// 保持设计态中的相对位置比例。
|
||||
// 注意:这里只调整位置,不改变尺寸。
|
||||
const int designTravel = parentDesignSize - result.size;
|
||||
const int currentTravel = parentCurrentSize - result.size;
|
||||
if (designTravel <= 0 || currentTravel <= 0)
|
||||
result.pos = startMargin;
|
||||
else
|
||||
result.pos = RoundDiv(static_cast<long long>(startMargin) * currentTravel, designTravel);
|
||||
break;
|
||||
}
|
||||
case StellarX::AxisAlignPolicy::Start:
|
||||
default:
|
||||
result.pos = startMargin;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 无锚点:退回设计态位置,尺寸保持设计值。
|
||||
result.pos = childDesignPos;
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* SxAxisName(bool horizontal)
|
||||
{
|
||||
return horizontal ? "X" : "Y";
|
||||
}
|
||||
|
||||
void LogAxisDowngradeIfNeeded(const std::string& id, bool horizontal,
|
||||
const StellarX::AxisLayoutSpec& spec, bool allowStretch)
|
||||
{
|
||||
if (spec.sizePolicy != StellarX::AxisSizePolicy::Stretch)
|
||||
return;
|
||||
|
||||
if (!spec.anchorStart || !spec.anchorEnd)
|
||||
{
|
||||
SX_LOGD("Layout")
|
||||
<< SX_T("布局降级:id=", "layout downgrade: id=") << id
|
||||
<< SX_T(" axis=", " axis=") << SxAxisName(horizontal)
|
||||
<< SX_T(" 原因=Stretch 但未同时锚定起止边", " reason=stretch without dual anchors");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allowStretch)
|
||||
{
|
||||
SX_LOGD("Layout")
|
||||
<< SX_T("能力边界拦截:id=", "layout capability intercept: id=") << id
|
||||
<< SX_T(" axis=", " axis=") << SxAxisName(horizontal)
|
||||
<< SX_T(" 原因=当前控件禁止该轴 Stretch", " reason=stretch disabled by capability");
|
||||
}
|
||||
}
|
||||
|
||||
void ActivateExplicitLayoutSpecMode(StellarX::LayoutMode& mode)
|
||||
{
|
||||
// 新公开布局 API 明确属于“按边约束解算”的语义层。
|
||||
// 一旦调用,说明外部希望控件进入统一锚定布局模型,
|
||||
// 因此直接切换到 AnchorToEdges,避免出现“策略已设但 layoutMode 仍是 Fixed”的半失效状态。
|
||||
mode = StellarX::LayoutMode::AnchorToEdges;
|
||||
}
|
||||
}
|
||||
|
||||
StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text)
|
||||
{
|
||||
{
|
||||
nHeight = text.nHeight;
|
||||
nWidth = text.nWidth;
|
||||
lpszFace = text.lpszFace;
|
||||
color = text.color;
|
||||
nEscapement = text.nEscapement;
|
||||
nOrientation = text.nOrientation;
|
||||
nWeight = text.nWeight;
|
||||
bItalic = text.bItalic;
|
||||
bUnderline = text.bUnderline;
|
||||
bStrikeOut = text.bStrikeOut;
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
|
||||
bool StellarX::ControlText::operator!=(const ControlText& text)
|
||||
{
|
||||
if(nHeight != text.nHeight)
|
||||
return true;
|
||||
else if (nWidth != text.nWidth)
|
||||
return true;
|
||||
else if (lpszFace != text.lpszFace)
|
||||
return true;
|
||||
else if (color != text.color)
|
||||
return true;
|
||||
else if (nEscapement != text.nEscapement)
|
||||
return true;
|
||||
else if (nOrientation != text.nOrientation)
|
||||
return true;
|
||||
else if (nWeight != text.nWeight)
|
||||
return true;
|
||||
else if (bItalic != text.bItalic)
|
||||
return true;
|
||||
else if (bUnderline != text.bUnderline)
|
||||
return true;
|
||||
else if (bStrikeOut != text.bStrikeOut)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
void Control::setIsVisible(bool show)
|
||||
{
|
||||
SX_LOGD("Control") << SX_T("重置可见状态: id=", "setIsVisible: id=")
|
||||
<< id
|
||||
<< " show=" << (show ? 1 : 0);
|
||||
|
||||
if (this->show == show)
|
||||
return;
|
||||
|
||||
this->show = show;
|
||||
this->dirty = true;
|
||||
|
||||
if (!show)
|
||||
{
|
||||
// 隐藏:擦除自己在屏幕上的内容,并释放快照
|
||||
discardBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示:不在这里 requestRepaint(避免父容器快照未就绪时子控件抢跑 draw,污染快照)
|
||||
// 仅向上标脏,让事件收口阶段由容器统一重绘。
|
||||
if (parent)
|
||||
parent->setDirty(true);
|
||||
}
|
||||
|
||||
void Control::onWindowResize()
|
||||
{
|
||||
SX_LOGD("Layout") << SX_T("尺寸变化:id=", "onWindowResize: id=") << id
|
||||
<< SX_T(" -> 丢背景快照 + 标脏", " -> discardSnap + dirty");
|
||||
|
||||
// 自己:丢快照 + 标脏
|
||||
invalidateBackgroundSnapshot();
|
||||
setDirty(true);
|
||||
}
|
||||
void Control::setLayoutMode(StellarX::LayoutMode layoutMode_)
|
||||
{
|
||||
this->layoutMode = layoutMode_;
|
||||
}
|
||||
void Control::setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2)
|
||||
{
|
||||
this->anchor_1 = anchor_1;
|
||||
this->anchor_2 = anchor_2;
|
||||
// 旧 API 只作为兼容输入层存在:
|
||||
// 这里把历史上的 anchor_1 / anchor_2 映射为新的水平/垂直轴布局规格,
|
||||
// 后续统一解算全部以 layoutSpec 为准。
|
||||
this->layoutSpec.horizontal = BuildLegacyAxisSpec(anchor_1, anchor_2, StellarX::Anchor::Left, StellarX::Anchor::Right);
|
||||
this->layoutSpec.vertical = BuildLegacyAxisSpec(anchor_1, anchor_2, StellarX::Anchor::Top, StellarX::Anchor::Bottom);
|
||||
}
|
||||
void Control::setHorizontalLayoutSpec(const StellarX::AxisLayoutSpec& spec)
|
||||
{
|
||||
ActivateExplicitLayoutSpecMode(this->layoutMode);
|
||||
this->layoutSpec.horizontal = spec;
|
||||
}
|
||||
|
||||
void Control::setVerticalLayoutSpec(const StellarX::AxisLayoutSpec& spec)
|
||||
{
|
||||
ActivateExplicitLayoutSpecMode(this->layoutMode);
|
||||
this->layoutSpec.vertical = spec;
|
||||
}
|
||||
|
||||
void Control::setHorizontalAnchors(bool left, bool right)
|
||||
{
|
||||
ActivateExplicitLayoutSpecMode(this->layoutMode);
|
||||
this->layoutSpec.horizontal.anchorStart = left;
|
||||
this->layoutSpec.horizontal.anchorEnd = right;
|
||||
}
|
||||
|
||||
void Control::setVerticalAnchors(bool top, bool bottom)
|
||||
{
|
||||
ActivateExplicitLayoutSpecMode(this->layoutMode);
|
||||
this->layoutSpec.vertical.anchorStart = top;
|
||||
this->layoutSpec.vertical.anchorEnd = bottom;
|
||||
}
|
||||
|
||||
void Control::setHorizontalSizePolicy(StellarX::AxisSizePolicy policy)
|
||||
{
|
||||
ActivateExplicitLayoutSpecMode(this->layoutMode);
|
||||
this->layoutSpec.horizontal.sizePolicy = policy;
|
||||
}
|
||||
|
||||
void Control::setVerticalSizePolicy(StellarX::AxisSizePolicy policy)
|
||||
{
|
||||
ActivateExplicitLayoutSpecMode(this->layoutMode);
|
||||
this->layoutSpec.vertical.sizePolicy = policy;
|
||||
}
|
||||
|
||||
void Control::setHorizontalAlignPolicy(StellarX::AxisAlignPolicy policy)
|
||||
{
|
||||
ActivateExplicitLayoutSpecMode(this->layoutMode);
|
||||
this->layoutSpec.horizontal.alignPolicy = policy;
|
||||
}
|
||||
|
||||
void Control::setVerticalAlignPolicy(StellarX::AxisAlignPolicy policy)
|
||||
{
|
||||
ActivateExplicitLayoutSpecMode(this->layoutMode);
|
||||
this->layoutSpec.vertical.alignPolicy = policy;
|
||||
}
|
||||
|
||||
StellarX::Anchor Control::getAnchor_1() const
|
||||
{
|
||||
return this->anchor_1;
|
||||
}
|
||||
StellarX::Anchor Control::getAnchor_2() const
|
||||
{
|
||||
return this->anchor_2;
|
||||
}
|
||||
StellarX::LayoutMode Control::getLayoutMode() const
|
||||
{
|
||||
return this->layoutMode;
|
||||
}
|
||||
StellarX::AxisLayoutSpec Control::getHorizontalLayoutSpec() const
|
||||
{
|
||||
return layoutSpec.horizontal;
|
||||
}
|
||||
|
||||
StellarX::AxisLayoutSpec Control::getVerticalLayoutSpec() const
|
||||
{
|
||||
return layoutSpec.vertical;
|
||||
}
|
||||
|
||||
const StellarX::LayoutSpec& Control::getLayoutSpec() const
|
||||
{
|
||||
return layoutSpec;
|
||||
}
|
||||
|
||||
const StellarX::LayoutCapability& Control::getLayoutCapability() const
|
||||
{
|
||||
return layoutCapability;
|
||||
}
|
||||
|
||||
void Control::setX(int x)
|
||||
{
|
||||
this->x = x;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Control::setY(int y)
|
||||
{
|
||||
this->y = y;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Control::setWidth(int width)
|
||||
{
|
||||
this->width = width;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Control::setHeight(int height)
|
||||
{
|
||||
this->height = height;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
bool Control::clearTransientMouseState()
|
||||
{
|
||||
// 基类默认没有 hover / tooltip / 临时按下态等鼠标瞬时状态。
|
||||
// 具体控件若需要在“未收到本次 WM_MOUSEMOVE”时做清理,重写此接口即可。
|
||||
return false;
|
||||
}
|
||||
|
||||
void Control::commitCurrentGeometryAsDesignRect()
|
||||
{
|
||||
// 该接口是“显式提交新的设计基线”的唯一入口之一。
|
||||
// 普通布局解算、父容器重排、窗口 resize 均不得自动回写 local*,
|
||||
// 否则会导致设计基线漂移,后续解算越来越不稳定。
|
||||
SX_LOGD("Layout")
|
||||
<< SX_T("提交设计基线:id=", "commit design rect: id=") << id
|
||||
<< SX_T(" world=(", " world=(") << x << "," << y << "," << width << "," << height << ")"
|
||||
<< SX_T(" parentLocalBefore=(", " parentLocalBefore=(")
|
||||
<< localx << "," << localy << "," << localWidth << "," << localHeight << ")";
|
||||
localx = parent ? (x - parent->getX()) : x;
|
||||
localy = parent ? (y - parent->getY()) : y;
|
||||
localWidth = width;
|
||||
localHeight = height;
|
||||
SX_LOGD("Layout")
|
||||
<< SX_T("提交设计基线完成:id=", "commit design rect done: id=") << id
|
||||
<< SX_T(" parentLocalAfter=(", " parentLocalAfter=(")
|
||||
<< localx << "," << localy << "," << localWidth << "," << localHeight << ")";
|
||||
}
|
||||
|
||||
StellarX::ResolvedLayoutRect Control::resolveLayoutRect(int parentDesignW, int parentDesignH,
|
||||
int parentWorldX, int parentWorldY, int parentCurrentW, int parentCurrentH) const
|
||||
{
|
||||
StellarX::ResolvedLayoutRect rect{};
|
||||
|
||||
if (layoutMode != StellarX::LayoutMode::AnchorToEdges)
|
||||
{
|
||||
// 非锚点布局模式:直接使用设计态矩形,再映射到当前世界坐标。
|
||||
rect.localX = localx;
|
||||
rect.localY = localy;
|
||||
rect.width = localWidth;
|
||||
rect.height = localHeight;
|
||||
rect.worldX = parentWorldX + rect.localX;
|
||||
rect.worldY = parentWorldY + rect.localY;
|
||||
return rect;
|
||||
}
|
||||
|
||||
// 第 1 层:先在父局部坐标系内分别解算水平轴和垂直轴。
|
||||
LogAxisDowngradeIfNeeded(id, true, layoutSpec.horizontal, layoutCapability.allowStretchX);
|
||||
LogAxisDowngradeIfNeeded(id, false, layoutSpec.vertical, layoutCapability.allowStretchY);
|
||||
const LayoutAxisResult horizontal = ResolveAxis(parentDesignW, parentCurrentW, localx, localWidth,
|
||||
layoutSpec.horizontal, layoutCapability.allowStretchX);
|
||||
const LayoutAxisResult vertical = ResolveAxis(parentDesignH, parentCurrentH, localy, localHeight,
|
||||
layoutSpec.vertical, layoutCapability.allowStretchY);
|
||||
|
||||
// 第 2 层:把父局部矩形映射到世界坐标。
|
||||
rect.localX = horizontal.pos;
|
||||
rect.localY = vertical.pos;
|
||||
rect.width = horizontal.size;
|
||||
rect.height = vertical.size;
|
||||
rect.worldX = parentWorldX + rect.localX;
|
||||
rect.worldY = parentWorldY + rect.localY;
|
||||
return rect;
|
||||
}
|
||||
|
||||
void Control::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
|
||||
{
|
||||
// 默认实现只应用运行态世界矩形。
|
||||
// 若某个控件还需要在应用后继续刷新内部布局,可重写此函数。
|
||||
applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height);
|
||||
}
|
||||
|
||||
void Control::applyRuntimeRectDirect(int worldX, int worldY, int width, int height)
|
||||
{
|
||||
// 最底层运行态赋值入口:
|
||||
// 只修改当前世界坐标和运行态尺寸,不触碰设计基线 local*。
|
||||
this->x = worldX;
|
||||
this->y = worldY;
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
dirty = true;
|
||||
}
|
||||
// 保存当前的绘图状态(字体、颜色、线型等)
|
||||
// 在控件绘制前调用,确保不会影响全局绘图状态
|
||||
void Control::saveStyle()
|
||||
{
|
||||
|
||||
gettextstyle(¤tFont); // 获取当前字体样式
|
||||
currentColor = gettextcolor(); // 获取当前字体颜色
|
||||
currentBorderColor = getlinecolor(); //保存当前边框颜色
|
||||
getlinestyle(¤tLineStyle); //保存当前线型
|
||||
currentBkColor = getfillcolor(); //保存当前填充色
|
||||
}
|
||||
// 恢复之前保存的绘图状态
|
||||
// 在控件绘制完成后调用,恢复全局绘图状态
|
||||
void Control::restoreStyle()
|
||||
{
|
||||
settextstyle(¤tFont); // 恢复默认字体样式
|
||||
settextcolor(currentColor); // 恢复默认字体颜色
|
||||
setfillcolor(currentBkColor);
|
||||
setlinestyle(¤tLineStyle);
|
||||
setlinecolor(currentBorderColor);
|
||||
setfillstyle(BS_SOLID);//恢复填充
|
||||
}
|
||||
|
||||
void Control::requestRepaint(Control* parent)
|
||||
{
|
||||
if (shouldDeferManagedRepaint())
|
||||
{
|
||||
// 托管路径:当前正在 Window 的事件分发阶段,不能立即绘制;
|
||||
// 这里只登记 source,真正的 root 选择由 Window 在 requestManagedRepaint 中完成。
|
||||
if (auto* host = getHostWindow())
|
||||
host->requestManagedRepaint(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// 说明:
|
||||
// - 常规路径:子控件调用 requestRepaint(this->parent),然后 parent 负责局部重绘(Canvas/TabControl override)
|
||||
// - 兜底路径:如果某个“容器控件”没 override requestRepaint,就会出现 parent==this 的递归风险
|
||||
// 此时我们改为向更上层冒泡,直到根重绘。
|
||||
if (parent == this)
|
||||
{
|
||||
SX_LOGW("Dirty")
|
||||
<< SX_T("requestRepaint(默认容器兜底):id=", "requestRepaint(default-container-fallback): id=")
|
||||
<< id
|
||||
<< SX_T(",parent==this,向上层 parent 继续冒泡", " parent==this, bubble to upper parent");
|
||||
|
||||
if (this->parent) this->parent->requestRepaint(this->parent);
|
||||
else onRequestRepaintAsRoot();
|
||||
return;
|
||||
}
|
||||
|
||||
SX_LOG_TRACE("Dirty") << SX_T("请求重绘:id=","requestRepaint: id=") << id << " parent=" << (parent ? parent->getId() : "null");
|
||||
|
||||
if (parent) parent->requestRepaint(parent); // 交给容器处理(容器可局部重绘)
|
||||
else onRequestRepaintAsRoot(); // 根兜底
|
||||
}
|
||||
|
||||
void Control::onRequestRepaintAsRoot()
|
||||
{
|
||||
if (shouldDeferManagedRepaint())
|
||||
{
|
||||
// 即使已经冒泡到 root,只要还在托管分发期,也不能直接绘制;
|
||||
// 仍然回到 Window 做统一提交。
|
||||
if (auto* host = getHostWindow())
|
||||
host->requestManagedRepaint(this);
|
||||
return;
|
||||
}
|
||||
|
||||
SX_LOG_TRACE("Dirty")
|
||||
<< SX_T("触发根重绘:id=", "onRequestRepaintAsRoot: id=") << id
|
||||
<< SX_T("(从根节点开始重画)", " (root repaint)");
|
||||
|
||||
|
||||
discardBackground();
|
||||
setDirty(true);
|
||||
draw(); // 只有“无父”时才允许立即画,不会被谁覆盖
|
||||
}
|
||||
|
||||
bool Control::shouldDeferManagedRepaint() const
|
||||
{
|
||||
Window* host = getHostWindow();
|
||||
return host && host->isManagedDispatchActive();
|
||||
}
|
||||
|
||||
// 获取宿主 Window:
|
||||
// - 顶层控件由 Window/addDialog 直接注入;
|
||||
// - 子控件没有直接注入时,沿 parent 链向上回溯即可。
|
||||
Window* Control::getHostWindow() const
|
||||
{
|
||||
if (hostWindow)
|
||||
return hostWindow;
|
||||
return parent ? parent->getHostWindow() : nullptr;
|
||||
}
|
||||
|
||||
// 托管重绘 root 选择规则:
|
||||
// - 对于直接挂在 Window 下的控件,root 就是它自己;
|
||||
// - 对于嵌套在 Canvas/TabControl/Dialog 内的子控件,沿 parent 向上找到与宿主 Window 同属一棵树的最上层控件。
|
||||
Control* Control::getManagedRepaintRoot()
|
||||
{
|
||||
Control* root = this;
|
||||
Window* host = getHostWindow();
|
||||
while (root->parent && root->parent->getHostWindow() == host)
|
||||
root = root->parent;
|
||||
return root;
|
||||
}
|
||||
|
||||
Control* Control::getManagedRepaintDirectBranch(Control* root)
|
||||
{
|
||||
if (!root)
|
||||
return nullptr;
|
||||
|
||||
Control* branch = this;
|
||||
while (branch->parent && branch->parent != root)
|
||||
branch = branch->parent;
|
||||
return branch;
|
||||
}
|
||||
|
||||
RECT Control::getBoundsRect() const
|
||||
{
|
||||
RECT rc{};
|
||||
rc.left = x;
|
||||
rc.top = y;
|
||||
rc.right = x + width;
|
||||
rc.bottom = y + height;
|
||||
return rc;
|
||||
}
|
||||
|
||||
RECT Control::getManagedRepaintCoverageRect() const
|
||||
{
|
||||
// 基类默认认为“实际绘制 coverage == 控件本体矩形”。
|
||||
// 复合控件或带浮层的控件会 override,把附加绘制区域一并并入。
|
||||
return getBoundsRect();
|
||||
}
|
||||
|
||||
RECT Control::getManagedRepaintPersistentCoverageRect() const
|
||||
{
|
||||
// 基类默认认为持久绘制范围等于控件本体。
|
||||
// 只有 Tooltip 这类临时浮层需要从持久范围中剔除。
|
||||
return getBoundsRect();
|
||||
}
|
||||
|
||||
bool Control::canCommitManagedPartialRepaint() const
|
||||
{
|
||||
// 基类默认不承诺自己能安全做局部提交;
|
||||
// 只有 Canvas / TabControl / Dialog 这类“拥有完整背景语义”的 root 才会 override 为 true。
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Control::hasManagedDirtySubtree() const
|
||||
{
|
||||
// 基类只感知自身是否为脏;
|
||||
// 容器类会 override 为“自身或其受管理子树是否有脏内容”。
|
||||
return dirty;
|
||||
}
|
||||
|
||||
void Control::commitManagedRepaint()
|
||||
{
|
||||
if (!show)
|
||||
return;
|
||||
// 基类兜底:如果没有更具体的容器实现,就按根级重绘处理。
|
||||
if (dirty)
|
||||
onRequestRepaintAsRoot();
|
||||
}
|
||||
|
||||
void Control::saveBackground(int x, int y, int w, int h)
|
||||
{
|
||||
|
||||
if (w <= 0 || h <= 0) return;
|
||||
saveBkX = x; saveBkY = y; saveWidth = w; saveHeight = h;
|
||||
if (saveBkImage)
|
||||
{
|
||||
//尺寸变了才重建,避免反复 new/delete
|
||||
if (saveBkImage->getwidth() != w || saveBkImage->getheight() != h)
|
||||
{
|
||||
SX_LOGD("Snap") <<SX_T("重新保存背景快照:id=", "saveBackground rebuild: id=") << id << " size=(" << w << "x" << h << ")";
|
||||
|
||||
saveBkImage.reset();
|
||||
}
|
||||
}
|
||||
else
|
||||
SX_LOGD("Snap") << SX_T("保存背景快照:id=", "saveBackground rebuild: id=") << id << " size=(" << w << "x" << h << ")";
|
||||
if (!saveBkImage) saveBkImage = std::make_unique<IMAGE>(w, h);
|
||||
|
||||
SetWorkingImage(nullptr); // ★抓屏幕
|
||||
getimage(saveBkImage.get(), x, y, w, h);
|
||||
hasSnap = true;
|
||||
}
|
||||
|
||||
void Control::restBackground()
|
||||
{
|
||||
if (!hasSnap || !saveBkImage) return;
|
||||
// 直接回贴屏幕(与抓取一致)
|
||||
SetWorkingImage(nullptr);
|
||||
putimage(saveBkX, saveBkY, saveBkImage.get());
|
||||
}
|
||||
|
||||
void Control::discardBackground()
|
||||
{
|
||||
if (saveBkImage)
|
||||
{
|
||||
restBackground();
|
||||
SX_LOGD("Snap") << SX_T("丢弃背景快照:id=","discardBackground: id=") << id << " hasSnap=" << (hasSnap ? 1 : 0);
|
||||
saveBkImage.reset();
|
||||
}
|
||||
hasSnap = false; saveWidth = saveHeight = 0;
|
||||
}
|
||||
|
||||
void Control::invalidateBackgroundSnapshot()
|
||||
{
|
||||
if (saveBkImage)
|
||||
{
|
||||
SX_LOGD("Snap") << SX_T("作废背景快照:id=", "invalidateBackgroundSnapshot: id=") << id
|
||||
<< " hasSnap=" << (hasSnap ? 1 : 0);
|
||||
saveBkImage.reset();
|
||||
}
|
||||
hasSnap = false;
|
||||
saveBkX = saveBkY = 0;
|
||||
saveWidth = saveHeight = 0;
|
||||
}
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* @类: Control
|
||||
* @摘要: 所有控件的抽象基类,定义通用接口和基础功能
|
||||
* @描述:
|
||||
* 提供控件的基本属性和方法,包括位置、尺寸、重绘标记等。
|
||||
* 实现绘图状态保存和恢复机制,确保控件绘制不影响全局状态。
|
||||
* 同时提供“事件阶段登记、收口阶段统一提交”的托管重绘基础接口。
|
||||
*
|
||||
* @特性:
|
||||
* - 定义控件基本属性(坐标、尺寸、脏标记)
|
||||
* - 提供绘图状态管理(saveStyle/restoreStyle)
|
||||
* - 声明纯虚接口(draw、handleEvent等)
|
||||
* - 禁止移动语义,禁止拷贝语义
|
||||
*
|
||||
* @使用场景: 作为所有具体控件类的基类,不直接实例化
|
||||
* @所属框架: 星垣(StellarX) GUI框架
|
||||
* @作者: 我在人间做废物
|
||||
******************************************************************************/
|
||||
#pragma once
|
||||
#ifndef _WIN32_WINNT
|
||||
#define _WIN32_WINNT 0x0600
|
||||
#endif
|
||||
#ifndef WINVER
|
||||
#define WINVER _WIN32_WINNT
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <easyx.h>
|
||||
#undef MessageBox
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include "CoreTypes.h"
|
||||
|
||||
class Window;
|
||||
|
||||
class Control
|
||||
{
|
||||
friend class Window;
|
||||
friend class Canvas;
|
||||
protected:
|
||||
std::string id; // 控件ID
|
||||
int localx, x, localy, y; // local* 为设计态父局部坐标,x/y 为运行态世界坐标
|
||||
int localWidth, width, localHeight, height; // local* 为设计态尺寸,width/height 为运行态尺寸
|
||||
Control* parent = nullptr; // 父控件
|
||||
Window* hostWindow = nullptr; // 宿主窗口(顶层由 Window 注入,子控件可沿 parent 回溯)
|
||||
bool dirty = true; // 是否重绘
|
||||
bool show = true; // 是否显示
|
||||
bool eventVisualChanged = false; // 最近一次 handleEvent 是否真的引发了视觉变化(用于上层判断是否需要登记重绘)
|
||||
|
||||
/* == 布局模式 == */
|
||||
StellarX::LayoutMode layoutMode = StellarX::LayoutMode::Fixed; // 布局模式
|
||||
StellarX::Anchor anchor_1 = StellarX::Anchor::Top; // 旧版兼容锚点 1
|
||||
StellarX::Anchor anchor_2 = StellarX::Anchor::Right; // 旧版兼容锚点 2
|
||||
// 新布局模型:内部统一使用“水平轴 + 垂直轴”的规格描述,不再直接依赖旧锚点求解。
|
||||
StellarX::LayoutSpec layoutSpec{
|
||||
{ false, true, StellarX::AxisSizePolicy::FixedSize, StellarX::AxisAlignPolicy::End },
|
||||
{ true, false, StellarX::AxisSizePolicy::FixedSize, StellarX::AxisAlignPolicy::Start }
|
||||
};
|
||||
// 控件能力边界:用于限制某些控件在特定轴上是否允许 Stretch。
|
||||
StellarX::LayoutCapability layoutCapability{};
|
||||
|
||||
/* == 背景快照 == */
|
||||
std::unique_ptr<IMAGE> saveBkImage;
|
||||
int saveBkX = 0, saveBkY = 0; // 快照保存起始坐标
|
||||
int saveWidth = 0, saveHeight = 0; // 快照保存尺寸
|
||||
bool hasSnap = false; // 当前是否持有有效快照
|
||||
|
||||
StellarX::RouRectangle rouRectangleSize; // 圆角矩形椭圆宽度和高度
|
||||
|
||||
LOGFONT currentFont{}; // 保存当前字体样式和颜色
|
||||
COLORREF currentColor{};
|
||||
COLORREF currentBkColor{}; // 保存当前填充色
|
||||
COLORREF currentBorderColor{}; // 边框颜色
|
||||
LINESTYLE currentLineStyle{}; // 保存当前线型
|
||||
|
||||
Control(const Control&) = delete;
|
||||
Control& operator=(const Control&) = delete;
|
||||
Control(Control&&) = delete;
|
||||
Control& operator=(Control&&) = delete;
|
||||
|
||||
Control() : localx(0), x(0), localy(0), y(0), localWidth(100), width(100), height(100), localHeight(100) {}
|
||||
Control(int x, int y, int width, int height)
|
||||
: localx(x), x(x), localy(y), y(y), localWidth(width), width(width), height(height), localHeight(height) {
|
||||
}
|
||||
public:
|
||||
|
||||
virtual ~Control()
|
||||
{
|
||||
discardBackground();
|
||||
}
|
||||
protected:
|
||||
// 向上请求重绘:普通路径交给父容器,托管路径则登记到 Window
|
||||
virtual void requestRepaint(Control* parent);
|
||||
// 根控件/无父时触发重绘
|
||||
virtual void onRequestRepaintAsRoot();
|
||||
// 当前是否处于 Window 托管分发阶段;若为真,则不应立即画
|
||||
bool shouldDeferManagedRepaint() const;
|
||||
protected:
|
||||
//保存背景快照
|
||||
virtual void saveBackground(int x, int y, int w, int h);
|
||||
// putimage 回屏
|
||||
virtual void restBackground();
|
||||
// 回贴旧背景并释放快照
|
||||
void discardBackground();
|
||||
public:
|
||||
// 仅作废快照,不回贴旧背景
|
||||
void invalidateBackgroundSnapshot();
|
||||
//“纯作废快照 + 标脏”,不再在 resize 路径里回贴旧背景
|
||||
virtual void onWindowResize();
|
||||
// 获取位置和尺寸
|
||||
int getX() const { return x; }
|
||||
int getY() const { return y; }
|
||||
int getWidth() const { return width; }
|
||||
int getHeight() const { return height; }
|
||||
int getRight() const { return x + width; }
|
||||
int getBottom() const { return y + height; }
|
||||
|
||||
int getLocalX() const { return localx; }
|
||||
int getLocalY() const { return localy; }
|
||||
int getLocalWidth() const { return localWidth; }
|
||||
int getLocalHeight() const { return localHeight; }
|
||||
int getLocalRight() const { return localx + localWidth; }
|
||||
int getLocalBottom() const { return localy + localHeight; }
|
||||
|
||||
// 公开几何 setter:
|
||||
// - 面向用户使用
|
||||
// - 默认只修改运行态矩形
|
||||
// - 不允许自动回写设计基线 local*
|
||||
// 若某个复合控件需要在 setter 后维护自己拥有的运行态子树一致性,
|
||||
// 由该控件在 override 中自行处理,但仍不得越权承担统一布局器职责。
|
||||
virtual void setX(int x);
|
||||
virtual void setY(int y);
|
||||
virtual void setWidth(int width);
|
||||
virtual void setHeight(int height);
|
||||
public:
|
||||
|
||||
virtual void draw() = 0;
|
||||
virtual bool handleEvent(const ExMessage& msg) = 0;//返回true代表事件已消费
|
||||
// 轻量清理接口:仅用于“没有机会收到本次 WM_MOUSEMOVE”的兄弟分支,
|
||||
// 清掉 hover / tooltip / 临时按下态等鼠标瞬时状态。
|
||||
// 该接口不是事件分发入口,不参与新的命中判断,也不允许回写设计基线。
|
||||
virtual bool clearTransientMouseState();
|
||||
//设置是否显示
|
||||
virtual void setIsVisible(bool show);
|
||||
//设置父容器指针
|
||||
void setParent(Control* parent) { this->parent = parent; }
|
||||
//设置宿主窗口(通常仅由顶层 Window/对话框注入)
|
||||
virtual void setHostWindow(Window* host) { this->hostWindow = host; }
|
||||
Window* getHostWindow() const; // 获取宿主 Window;子控件可沿 parent 向上回溯
|
||||
RECT getBoundsRect() const; // 获取当前控件外接矩形,用于覆盖/相交判断
|
||||
// 获取本控件“本轮实际可能写到屏幕上的覆盖范围”。
|
||||
// 默认等于控件本体矩形;像 Button 这类会额外绘制 Tooltip 的控件,可 override 后扩大范围。
|
||||
// 托管重绘 coverage、overlay 相交判断统一走这个接口,而不再默认使用控件本体 bounds。
|
||||
virtual RECT getManagedRepaintCoverageRect() const;
|
||||
// 获取会影响后续控件背景快照的“持久绘制范围”。
|
||||
// Tooltip 等临时浮层不应进入该范围,避免上层兄弟补画时把临时浮层抓进背景快照。
|
||||
virtual RECT getManagedRepaintPersistentCoverageRect() const;
|
||||
Control* getManagedRepaintRoot(); // 找到本控件对应的托管重绘 root
|
||||
Control* getManagedRepaintDirectBranch(Control* root); // 找到“root 下面承接本次脏变化的直接子分支”
|
||||
bool hasValidBackgroundSnapshot() const { return hasSnap && saveBkImage != nullptr; } // 当前是否持有可用于局部恢复的快照
|
||||
virtual bool hasManagedDirtySubtree() const; // 当前控件或其受管理子树中是否存在待提交的脏内容
|
||||
virtual bool canCommitManagedPartialRepaint() const; // 当前 root 是否可安全做“局部提交”而非整 root 重画
|
||||
virtual void commitManagedRepaint(); // 托管收口阶段真正执行绘制的入口
|
||||
//设置是否重绘
|
||||
virtual void setDirty(bool dirty) { this->dirty = dirty; }
|
||||
//检查控件是否可见
|
||||
bool IsVisible() const { return show; };
|
||||
//获取控件id
|
||||
std::string getId() const { return id; }
|
||||
//检查是否为脏
|
||||
bool isDirty() { return dirty; }
|
||||
//获取控件最近一次事件处理是否引发了视觉变化
|
||||
bool didEventAffectVisual() const { return eventVisualChanged; }
|
||||
//用来检查对话框是否模态,其他控件不用实现
|
||||
virtual bool model()const = 0;
|
||||
//布局
|
||||
// 设置旧版布局模式(兼容入口)
|
||||
void setLayoutMode(StellarX::LayoutMode layoutMode_);
|
||||
// 设置旧版双锚点输入,并映射到内部统一 LayoutSpec
|
||||
void setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2);
|
||||
// 直接设置水平轴布局规格。
|
||||
// 调用该接口后会自动切换到 AnchorToEdges 布局模式;
|
||||
// 这是新布局模型的公开入口,后设置者会覆盖旧 setAnchor() 对水平轴的映射结果。
|
||||
void setHorizontalLayoutSpec(const StellarX::AxisLayoutSpec& spec);
|
||||
// 直接设置垂直轴布局规格。
|
||||
// 调用该接口后会自动切换到 AnchorToEdges 布局模式;
|
||||
// 这是新布局模型的公开入口,后设置者会覆盖旧 setAnchor() 对垂直轴的映射结果。
|
||||
void setVerticalLayoutSpec(const StellarX::AxisLayoutSpec& spec);
|
||||
// 设置水平轴锚定边集合(left / right)。
|
||||
void setHorizontalAnchors(bool left, bool right);
|
||||
// 设置垂直轴锚定边集合(top / bottom)。
|
||||
void setVerticalAnchors(bool top, bool bottom);
|
||||
// 设置水平轴尺寸策略(Stretch / FixedSize)。
|
||||
void setHorizontalSizePolicy(StellarX::AxisSizePolicy policy);
|
||||
// 设置垂直轴尺寸策略(Stretch / FixedSize)。
|
||||
void setVerticalSizePolicy(StellarX::AxisSizePolicy policy);
|
||||
// 设置水平轴固定尺寸位移策略(Start / End / Center / Proportional)。
|
||||
void setHorizontalAlignPolicy(StellarX::AxisAlignPolicy policy);
|
||||
// 设置垂直轴固定尺寸位移策略(Start / End / Center / Proportional)。
|
||||
void setVerticalAlignPolicy(StellarX::AxisAlignPolicy policy);
|
||||
// 获取旧版锚点 1(兼容读取入口)
|
||||
StellarX::Anchor getAnchor_1() const;
|
||||
// 获取旧版锚点 2(兼容读取入口)
|
||||
StellarX::Anchor getAnchor_2() const;
|
||||
// 获取旧版布局模式
|
||||
StellarX::LayoutMode getLayoutMode() const;
|
||||
// 获取水平轴布局规格;返回的是当前生效的新模型状态,不要求可逆回旧 anchor 语义。
|
||||
StellarX::AxisLayoutSpec getHorizontalLayoutSpec() const;
|
||||
// 获取垂直轴布局规格;返回的是当前生效的新模型状态,不要求可逆回旧 anchor 语义。
|
||||
StellarX::AxisLayoutSpec getVerticalLayoutSpec() const;
|
||||
// 获取内部统一布局规格;供 Window / Canvas 等统一解算入口使用。
|
||||
const StellarX::LayoutSpec& getLayoutSpec() const;
|
||||
// 获取控件能力边界;用于判断某个轴是否允许 Stretch。
|
||||
const StellarX::LayoutCapability& getLayoutCapability() const;
|
||||
// 显式将当前运行态矩形提交为新的设计基线。
|
||||
// 这是“运行态 -> 设计态”的受控入口之一:
|
||||
// - 普通 resize / 重排过程中不得自动调用
|
||||
// - 公开 setter 也不得隐式调用
|
||||
// - 仅用于少数需要把当前结果固化为新设计基线的场景
|
||||
void commitCurrentGeometryAsDesignRect();
|
||||
protected:
|
||||
// 第 1 层:根据父设计尺寸、父当前尺寸和本控件设计矩形,解算出当前运行态局部矩形。
|
||||
StellarX::ResolvedLayoutRect resolveLayoutRect(int parentDesignW, int parentDesignH,
|
||||
int parentWorldX, int parentWorldY, int parentCurrentW, int parentCurrentH) const;
|
||||
// 内部受控路径:把统一解算结果应用到运行态矩形。不要把公开 setter 当成纯赋值接口来复用。
|
||||
virtual void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect);
|
||||
// 最底层运行态赋值入口:仅修改运行态矩形,不回写 local*。
|
||||
void applyRuntimeRectDirect(int worldX, int worldY, int width, int height);
|
||||
void saveStyle();
|
||||
void restoreStyle();
|
||||
void resetEventVisualChanged() { eventVisualChanged = false; }
|
||||
void markEventVisualChanged(bool changed = true) { eventVisualChanged = changed; }
|
||||
};
|
||||
-488
@@ -1,488 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* @文件: CoreTypes.h
|
||||
* @摘要: 星垣(StellarX)框架核心类型定义文件
|
||||
* @描述:
|
||||
* 集中定义框架中使用的所有枚举类型和结构体,确保类型一致性。
|
||||
* 这是框架的类型系统基础,所有控件都依赖于此文件中定义的类型。
|
||||
*
|
||||
* @作者: 我在人间做废物
|
||||
* @日期: September 2025
|
||||
******************************************************************************/
|
||||
#pragma once
|
||||
|
||||
#include "easyx.h"
|
||||
|
||||
/**
|
||||
* @命名空间: StellarX
|
||||
*
|
||||
* @详细说明:
|
||||
* 集中定义框架中使用的所有枚举类型和结构体,确保类型一致性。
|
||||
* 这是框架的类型系统基础,所有控件都依赖于此文件中定义的类型
|
||||
*
|
||||
* @使用示例:
|
||||
* StellarX::FillStyle::Horizontal - 填充样式
|
||||
*
|
||||
* @备注:
|
||||
* 不用单独包含此头文件,已在StellarX.h中包含,包含唯一对外头文件即可
|
||||
*/
|
||||
namespace StellarX
|
||||
{
|
||||
/**
|
||||
* @枚举类名称: FillStyle
|
||||
* @功能描述: 用来定义控件填充图案的枚举类
|
||||
*
|
||||
* @详细说明:
|
||||
* 根据此枚举类可以自定义控件填充图案
|
||||
* 可以在控件初始化时设置填充图案
|
||||
* 根据具体情况选择不同的填充图案
|
||||
* 默认填充图案为水平线
|
||||
*
|
||||
* @取值说明:
|
||||
* Horizontal - 水平线
|
||||
* Vertical - 垂直线
|
||||
* FDiagonal - 反斜线
|
||||
* BDiagonal - 正斜线
|
||||
* Cross - 十字
|
||||
* DiagCross - 网格
|
||||
*
|
||||
* @使用示例:
|
||||
* FillStyle var = FillStyle::Horizontal;
|
||||
*
|
||||
* @备注:
|
||||
* 此枚举类仅支持图案填充模式
|
||||
*/
|
||||
enum class FillStyle
|
||||
{
|
||||
Horizontal = HS_HORIZONTAL, // 水平线
|
||||
Vertical = HS_VERTICAL, // 垂直线
|
||||
FDiagonal = HS_FDIAGONAL, // 反斜线
|
||||
BDiagonal = HS_BDIAGONAL, // 正斜线
|
||||
Cross = HS_CROSS, // 十字
|
||||
DiagCross = HS_DIAGCROSS // 网格
|
||||
};
|
||||
|
||||
/**
|
||||
* @枚举类名称: FillMode
|
||||
* @功能描述: 用来定义控件填充模式的枚举类
|
||||
*
|
||||
* @详细说明:
|
||||
* 根据此枚举类可以自定义控件填充模式
|
||||
* 可以在控件初始化时设置填充模式
|
||||
* 根据具体情况选择不同的填充模式
|
||||
* 默认填充模式为固实填充
|
||||
*
|
||||
* @取值说明:
|
||||
* Solid - 固实填充
|
||||
* Null - 不填充
|
||||
* Hatched - 图案填充
|
||||
* Pattern - 自定义图案填充
|
||||
* DibPattern - 自定义图像填充
|
||||
*
|
||||
* @使用示例:
|
||||
* FillMode var = FillMode::Solid;
|
||||
*/
|
||||
enum class FillMode
|
||||
{
|
||||
Solid = BS_SOLID, //固实填充
|
||||
Null = BS_NULL, // 不填充
|
||||
Hatched = BS_HATCHED, // 图案填充
|
||||
Pattern = BS_PATTERN, // 自定义图案填充
|
||||
DibPattern = BS_DIBPATTERN // 自定义图像填充
|
||||
};
|
||||
|
||||
/**
|
||||
* @枚举类名称: LineStyle
|
||||
* @功能描述: 此枚举类用来定义控件边框线型
|
||||
*
|
||||
* @详细说明:
|
||||
* 根据此枚举类可以自定义控件边框线型
|
||||
* 可以在控件初始化时设置边框线型
|
||||
* 根据具体情况选择不同的线型
|
||||
* 默认线型为实线
|
||||
*
|
||||
* @取值说明:
|
||||
* Solid // 实线
|
||||
* Dash // 虚线
|
||||
* Dot // 点线
|
||||
* DashDot // 点划线
|
||||
* DashDotDot // 双点划线
|
||||
* Null // 无线
|
||||
*
|
||||
* @使用示例:
|
||||
* LineStyle var = LineStyle::Solid;
|
||||
*/
|
||||
enum class LineStyle
|
||||
{
|
||||
Solid = PS_SOLID, // 实线
|
||||
Dash = PS_DASH, // 虚线
|
||||
Dot = PS_DOT, // 点线
|
||||
DashDot = PS_DASHDOT, // 点划线
|
||||
DashDotDot = PS_DASHDOTDOT, // 双点划线
|
||||
Null = PS_NULL // 无线
|
||||
};
|
||||
|
||||
/**
|
||||
* @结构体名称: ControlText
|
||||
* @功能描述: 控件字体样式 可以自定义不同的样式
|
||||
*
|
||||
* @详细说明:
|
||||
* 主要使用的场景为:需要修改或想自定义控件字体大小,字体样式,颜色等
|
||||
*
|
||||
* @成员说明:
|
||||
* int nHeight = 0; - 字体高度
|
||||
* int nWidth = 0; - 字体宽度 如果为0则自适应
|
||||
* LPCTSTR lpszFace = "宋体"; - 字体名称
|
||||
* COLORREF color = RGB(0, 0, 0); - 字体颜色
|
||||
* int nEscapement = 0; - 字符串旋转角度
|
||||
* int nOrientation = 0; - 字符旋转角度
|
||||
* int nWeight = 0; - 字体粗细 范围0~1000 0表示默认
|
||||
* bool bItalic = false; - 是否斜体
|
||||
* bool bUnderline = false; - 是否下划线
|
||||
* bool bStrikeOut = false; - 是否删除线
|
||||
*/
|
||||
struct ControlText
|
||||
{
|
||||
int nHeight = 0; //- 字体高度
|
||||
int nWidth = 0; //- 字体宽度 如果为0则自适应
|
||||
LPCTSTR lpszFace = "微软雅黑"; //- 字体名称
|
||||
COLORREF color = RGB(0, 0, 0); //- 字体颜色
|
||||
int nEscapement = 0; //- 字符串旋转角度
|
||||
int nOrientation = 0; //- 字符旋转角度
|
||||
int nWeight = 0; //- 字体粗细 范围0~1000 0表示默认
|
||||
bool bItalic = false; //- 是否斜体
|
||||
bool bUnderline = false; //- 是否下划线
|
||||
bool bStrikeOut = false; //- 是否删除线
|
||||
|
||||
bool operator!=(const ControlText& text);
|
||||
ControlText& operator=(const ControlText& text);
|
||||
};
|
||||
|
||||
/**
|
||||
* @枚举名称: ControlShape
|
||||
* @功能描述: 枚举控件的不同几何样式
|
||||
*
|
||||
* @详细说明:
|
||||
* 定义了四种(有无边框算一种)不同的几何样式,可以根据具体需求
|
||||
* 自定义控件的形状。
|
||||
*
|
||||
* @取值说明:
|
||||
* RECTANGLE = 1, //有边框矩形
|
||||
* B_RECTANGLE, //无边框矩形
|
||||
* ROUND_RECTANGLE, //有边框圆角矩形
|
||||
* B_ROUND_RECTANGLE, //无边框圆角矩形
|
||||
* CIRCLE, //有边框圆形
|
||||
* B_CIRCLE, //无边框圆形
|
||||
* ELLIPSE, //有边框椭圆
|
||||
* B_ELLIPSE //无边框椭圆
|
||||
*
|
||||
* @使用示例:
|
||||
* ControlShape shape = ControlShape::ELLIPSE;
|
||||
*
|
||||
* @备注:
|
||||
* 按钮类支持所有形状,部分控件只支持部分形状,具体请参考控件类。
|
||||
*/
|
||||
enum class ControlShape
|
||||
{
|
||||
RECTANGLE = 1, //有边框矩形
|
||||
B_RECTANGLE, //无边框矩形
|
||||
ROUND_RECTANGLE, //有边框圆角矩形
|
||||
B_ROUND_RECTANGLE, //无边框圆角矩形
|
||||
CIRCLE, //有边框圆形
|
||||
B_CIRCLE, //无边框圆形
|
||||
ELLIPSE, //有边框椭圆
|
||||
B_ELLIPSE //无边框椭圆
|
||||
};
|
||||
|
||||
/**
|
||||
* @枚举类名称: TextBoxmode
|
||||
* @功能描述: 定义了文本框的三种模式
|
||||
*
|
||||
* @详细说明:
|
||||
* 需要限制文本框是否接受用户输入时使用
|
||||
*
|
||||
* @取值说明:
|
||||
* INPUT_MODE, // 用户可输入模式
|
||||
* READONLY_MODE // 只读模式
|
||||
* PASSWORD_MODE // 密码模式
|
||||
*/
|
||||
enum class TextBoxmode
|
||||
{
|
||||
INPUT_MODE, // 用户可输入模式
|
||||
READONLY_MODE, // 只读模式
|
||||
PASSWORD_MODE // 密码模式
|
||||
};
|
||||
|
||||
/**
|
||||
* @枚举名称: ButtonMode
|
||||
* @功能描述: 定义按钮的工作模式
|
||||
*
|
||||
* @详细说明:
|
||||
* 根据按钮的工作模式,按钮可以有不同的行为。
|
||||
* 用户可以在具体情况下设置按钮的工作模式。
|
||||
*
|
||||
* @取值说明:
|
||||
* NORMAL = 1, - 普通模式,点击后触发回调,但不会保持状态。
|
||||
* TOGGLE, - 切换模式,点击后会在选中和未选中之间切换,触发不同的回调函数。
|
||||
* DISABLED - 禁用模式,按钮不可点击,显示为灰色,文本显示删除线。
|
||||
*
|
||||
* @使用示例:
|
||||
* ButtonMode mode = ButtonMode::NORMAL;
|
||||
*/
|
||||
enum class ButtonMode
|
||||
{
|
||||
NORMAL = 1, //普通模式
|
||||
TOGGLE, //切换模式
|
||||
DISABLED //禁用模式
|
||||
};
|
||||
|
||||
/**
|
||||
* @结构体名称: RouRectangle
|
||||
* @功能描述: 定义了控件圆角矩形样式时圆角的椭圆尺寸
|
||||
*
|
||||
* @详细说明:
|
||||
* 需要修改控件圆角矩形样式时的圆角椭圆。
|
||||
*
|
||||
* @成员说明:
|
||||
* int ROUND_RECTANGLEwidth = 20; //构成圆角矩形的圆角的椭圆的宽度。
|
||||
* int ROUND_RECTANGLEheight = 20; //构成圆角矩形的圆角的椭圆的高度。
|
||||
*/
|
||||
struct RouRectangle
|
||||
{
|
||||
int ROUND_RECTANGLEwidth = 20; //构成圆角矩形的圆角的椭圆的宽度。
|
||||
int ROUND_RECTANGLEheight = 20; //构成圆角矩形的圆角的椭圆的高度。
|
||||
};
|
||||
|
||||
// 消息框类型
|
||||
enum class MessageBoxType
|
||||
{
|
||||
OK, // 只有确定按钮
|
||||
OKCancel, // 确定和取消按钮
|
||||
YesNo, // 是和否按钮
|
||||
YesNoCancel, // 是、否和取消按钮
|
||||
RetryCancel, // 重试和取消按钮
|
||||
AbortRetryIgnore, // 中止、重试和忽略按钮
|
||||
};
|
||||
|
||||
// 消息框返回值
|
||||
enum class MessageBoxResult
|
||||
{
|
||||
OK = 1, // 确定按钮
|
||||
Cancel = 2, // 取消按钮
|
||||
Yes = 6, // 是按钮
|
||||
No = 7, // 否按钮
|
||||
Abort = 3, // 中止按钮
|
||||
Retry = 4, // 重试按钮
|
||||
Ignore = 5 // 忽略按钮
|
||||
};
|
||||
#if 0 //布局管理器相关 —待实现—
|
||||
/*
|
||||
*
|
||||
*@枚举名称: LayoutKind
|
||||
* @功能描述 : 定义布局管理类型
|
||||
*
|
||||
*@详细说明 :
|
||||
* 根据布局管理类型,控件可以有不同的布局方式。
|
||||
* 用户可以在具体情况下设置布局管理类型。
|
||||
*
|
||||
* @取值说明 :
|
||||
* Absolute:不管,保持子控件自己的坐标(向后兼容)。
|
||||
* HBox: 水平方向排队;支持固定宽、权重分配、对齐、拉伸。
|
||||
* VBox: 竖直方向排队;同上。
|
||||
* Grid(网格):按行列摆;支持固定/自适应/权重行列;支持跨行/跨列;单元内对齐/拉伸。
|
||||
*
|
||||
*/
|
||||
// 布局类型
|
||||
enum class LayoutKind
|
||||
{
|
||||
Absolute = 1,
|
||||
HBox,
|
||||
VBox,
|
||||
Grid,
|
||||
Flow,
|
||||
Stack
|
||||
};
|
||||
|
||||
// 布局参数
|
||||
struct LayoutParams
|
||||
{
|
||||
// 边距左、右、上、下
|
||||
int marginL = 0, marginR = 0, marginT = 0, marginB = 0;
|
||||
// 固定尺寸(>=0 强制;-1 用控件当前尺寸)
|
||||
int fixedW = -1, fixedH = -1;
|
||||
// 主轴权重(HBox=宽度、VBox=高度、Grid见下)
|
||||
float weight = 0.f;
|
||||
// 对齐(非拉伸时生效)
|
||||
enum Align { Start = 0, Center = 1, End = 2, Stretch = 3 };
|
||||
int alignX = Start; // HBox: 次轴=Y;VBox: 次轴=X;Grid: 单元内
|
||||
int alignY = Start; // Grid :控制单元内垂直(HBox / VBox通常只用 alignX)
|
||||
// Grid 专用(可先不做)
|
||||
int gridRow = 0, gridCol = 0, rowSpan = 1, colSpan = 1;
|
||||
// Flow 专用(可先不做)
|
||||
int flowBreak = 0; // 1=强制换行
|
||||
};
|
||||
#endif
|
||||
|
||||
/*
|
||||
* @枚举名称: TabPlacement
|
||||
* @功能描述: 定义了选项卡页签的不同位置
|
||||
*
|
||||
* @详细说明:
|
||||
* 根据选项卡页签的位置,选项卡页签可以有不同的布局方式。
|
||||
*
|
||||
* @成员说明:
|
||||
* Top, - 选项卡页签位于顶部
|
||||
* Bottom, - 选项卡页签位于底部
|
||||
* Left, - 选项卡页签位于左侧
|
||||
* Right - 选项卡页签位于右侧
|
||||
*
|
||||
* @使用示例:
|
||||
* TabPlacement placement = TabPlacement::Top;
|
||||
*/
|
||||
enum class TabPlacement
|
||||
{
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right
|
||||
};
|
||||
/*
|
||||
* @枚举名称: LayoutMode
|
||||
* @功能描述: 定义了两种布局模式
|
||||
*
|
||||
* @详细说明:
|
||||
* 根据不同模式,在窗口拉伸时采用不同的布局策略
|
||||
*
|
||||
* @成员说明:
|
||||
* Fixed, - 固定布局
|
||||
* AnchorToEdges - 锚定布局
|
||||
*
|
||||
* @使用示例:
|
||||
* LayoutMode mode = LayoutMode::Fixed;
|
||||
*/
|
||||
enum class LayoutMode
|
||||
{
|
||||
Fixed,
|
||||
AnchorToEdges
|
||||
};
|
||||
/*
|
||||
* @枚举名称: Anchor
|
||||
* @功能描述: 定义了控件相对于窗口锚定的位置
|
||||
*
|
||||
* @详细说明:
|
||||
* 根据不同的锚定位置,有不同的拉伸策略
|
||||
*
|
||||
* @成员说明:
|
||||
* Top, - 锚定上边,控件上边与窗口上侧距离保持不变
|
||||
* Bottom, - 锚定底边,控件底边与窗口底边距离保持不变
|
||||
* Left, - 锚定左边,控件左边与窗口左侧距离保持不变
|
||||
* Right - 锚定右边,控件上边与窗口右侧距离保持不变
|
||||
*
|
||||
* @使用示例:
|
||||
* Anchor a = Anchor::Top;
|
||||
*/
|
||||
enum class Anchor
|
||||
{
|
||||
NoAnchor = 0,
|
||||
Left = 1,
|
||||
Right,
|
||||
Top,
|
||||
Bottom
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
* @枚举: AxisSizePolicy
|
||||
* @摘要: 单轴尺寸策略,决定该轴是否允许因父容器变化而拉伸
|
||||
* @详细说明:
|
||||
* 该枚举只决定“尺寸是否变化”,不决定“位置如何求解”。
|
||||
* 若需要在固定尺寸下决定控件如何位移,应结合 AxisAlignPolicy 一起使用。
|
||||
*
|
||||
* @成员说明:
|
||||
* Stretch - 允许尺寸随父容器变化而变化
|
||||
* FixedSize - 尺寸保持设计态大小,仅位置按锚点/位移策略变化
|
||||
******************************************************************************/
|
||||
enum class AxisSizePolicy
|
||||
{
|
||||
Stretch,
|
||||
FixedSize
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
* @枚举: AxisAlignPolicy
|
||||
* @摘要: 单轴固定尺寸时的位置策略
|
||||
* @详细说明:
|
||||
* 该策略只在“该轴不拉伸”时生效,用于决定控件在父容器变化后如何重新定位。
|
||||
* 其中 Proportional 只用于位置求解,不参与尺寸缩放。
|
||||
*
|
||||
* @成员说明:
|
||||
* Start - 保持起边距离
|
||||
* End - 保持终边距离
|
||||
* Center - 保持相对父容器中心的偏移关系
|
||||
* Proportional - 保持设计态中的相对位置比例
|
||||
******************************************************************************/
|
||||
enum class AxisAlignPolicy
|
||||
{
|
||||
Start,
|
||||
End,
|
||||
Center,
|
||||
Proportional
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
* @结构体: AxisLayoutSpec
|
||||
* @摘要: 描述单个坐标轴上的锚点与解算策略
|
||||
* @详细说明:
|
||||
* 一个控件完整布局由“水平轴 + 垂直轴”两份 AxisLayoutSpec 组成。
|
||||
* anchorStart / anchorEnd 只表达当前轴是否锚定起止边;
|
||||
* sizePolicy 决定该轴是否允许拉伸;
|
||||
* alignPolicy 则用于固定尺寸时的位置求解。
|
||||
******************************************************************************/
|
||||
struct AxisLayoutSpec
|
||||
{
|
||||
bool anchorStart = false;
|
||||
bool anchorEnd = false;
|
||||
AxisSizePolicy sizePolicy = AxisSizePolicy::FixedSize;
|
||||
AxisAlignPolicy alignPolicy = AxisAlignPolicy::Start;
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
* @结构体: LayoutSpec
|
||||
* @摘要: 控件完整布局规格,由水平轴和垂直轴两部分组成
|
||||
******************************************************************************/
|
||||
struct LayoutSpec
|
||||
{
|
||||
AxisLayoutSpec horizontal{};
|
||||
AxisLayoutSpec vertical{};
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
* @结构体: LayoutCapability
|
||||
* @摘要: 控件能力边界,声明控件在哪些轴上允许 Stretch
|
||||
* @详细说明:
|
||||
* 规则表负责定义“某种组合如何解算”,LayoutCapability 负责声明
|
||||
* “该控件是否允许采用这种组合”。例如 Table 当前阶段仅允许 X 轴拉伸,
|
||||
* 因此 allowStretchY 会被显式关闭。
|
||||
******************************************************************************/
|
||||
struct LayoutCapability
|
||||
{
|
||||
bool allowStretchX = true;
|
||||
bool allowStretchY = true;
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
* @结构体: ResolvedLayoutRect
|
||||
* @摘要: 统一布局解算后的运行态矩形结果
|
||||
* @详细说明:
|
||||
* localX/localY 表示解算完成后的“父局部坐标”;
|
||||
* worldX/worldY 表示映射到窗口世界坐标后的最终绘制位置;
|
||||
* width/height 为运行态尺寸。
|
||||
******************************************************************************/
|
||||
struct ResolvedLayoutRect
|
||||
{
|
||||
int localX = 0;
|
||||
int localY = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int worldX = 0;
|
||||
int worldY = 0;
|
||||
};
|
||||
}
|
||||
-773
@@ -1,773 +0,0 @@
|
||||
#include "Dialog.h"
|
||||
#include "SxLog.h"
|
||||
|
||||
Dialog::Dialog(Window& h, std::string text, std::string message, StellarX::MessageBoxType type, bool modal)
|
||||
: Canvas(), message(message), type(type), modal(modal), hWnd(h), titleText(text)
|
||||
{
|
||||
this->id = "Dialog";
|
||||
// Dialog 当前阶段只做“居中 + 自管内部 chrome”,
|
||||
// 不参与外部 Stretch;宿主窗口变化时只重新居中。
|
||||
this->layoutCapability.allowStretchX = false;
|
||||
this->layoutCapability.allowStretchY = false;
|
||||
setHostWindow(&hWnd);
|
||||
show = false;
|
||||
}
|
||||
|
||||
Dialog::~Dialog()
|
||||
{
|
||||
}
|
||||
|
||||
void Dialog::draw()
|
||||
{
|
||||
if (!show)
|
||||
{
|
||||
// 如果对话框不可见且需要清理,执行清理
|
||||
if (pendingCleanup && !isCleaning)
|
||||
{
|
||||
performDelayedCleanup();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 如果需要初始化,则执行初始化
|
||||
if (needsInitialization && show)
|
||||
{
|
||||
initDialogSize();
|
||||
needsInitialization = false;
|
||||
}
|
||||
|
||||
if (dirty && show)
|
||||
{
|
||||
// 保存当前绘图状态
|
||||
saveStyle();
|
||||
|
||||
Canvas::setBorderColor(this->borderColor);
|
||||
Canvas::setLinewidth(BorderWidth);
|
||||
Canvas::setCanvasBkColor(this->backgroundColor);
|
||||
Canvas::setShape(StellarX::ControlShape::ROUND_RECTANGLE);
|
||||
|
||||
Canvas::draw();
|
||||
|
||||
//绘制消息文本
|
||||
settextcolor(textStyle.color);
|
||||
setbkmode(TRANSPARENT);
|
||||
|
||||
//设置字体样式
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
|
||||
outtextxy(x + 5, y + 5, LPCTSTR(titleText.c_str()));
|
||||
|
||||
int ty = y + closeButtonHeight + titleToTextMargin; // 文本起始Y坐标
|
||||
for (auto& line : lines)
|
||||
{
|
||||
int tx = this->x + ((this->width - textwidth(line.c_str())) / 2); // 文本起始X坐标
|
||||
outtextxy(tx, ty, LPCTSTR(line.c_str()));
|
||||
ty = ty + textheight(LPCTSTR(line.c_str())) + 5; // 每行文本高度加5像素间距
|
||||
}
|
||||
|
||||
// 恢复绘图状态
|
||||
restoreStyle();
|
||||
|
||||
dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Dialog::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
bool consume = false;
|
||||
resetEventVisualChanged();
|
||||
if (!show)
|
||||
{
|
||||
if (pendingCleanup && !isCleaning)
|
||||
{
|
||||
performDelayedCleanup();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果正在清理或标记为待清理,则不处理事件
|
||||
if (pendingCleanup || isCleaning)
|
||||
return false;
|
||||
|
||||
const bool isMouseMessage =
|
||||
msg.message == WM_MOUSEMOVE ||
|
||||
msg.message == WM_LBUTTONDOWN ||
|
||||
msg.message == WM_LBUTTONUP;
|
||||
const bool insideDialog =
|
||||
msg.x >= x && msg.x <= x + width &&
|
||||
msg.y >= y && msg.y <= y + height;
|
||||
|
||||
// 模态对话框区域外鼠标事件不允许落到底层背景。
|
||||
if (modal && isMouseMessage && !insideDialog)
|
||||
{
|
||||
if (msg.message == WM_LBUTTONUP)
|
||||
std::cout << "\a" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 将事件传递给子控件处理
|
||||
if (!consume)
|
||||
consume = Canvas::handleEvent(msg);
|
||||
|
||||
// 对话框矩形范围内的鼠标事件一律由对话框吞掉,避免穿透到底层控件。
|
||||
if (isMouseMessage && insideDialog)
|
||||
consume = true;
|
||||
|
||||
// 每次事件处理后检查是否需要执行延迟清理
|
||||
if (pendingCleanup && !isCleaning)
|
||||
performDelayedCleanup();
|
||||
return consume;
|
||||
}
|
||||
|
||||
void Dialog::SetTitle(const std::string& title)
|
||||
{
|
||||
this->titleText = title;
|
||||
invalidateLayout(true);
|
||||
}
|
||||
|
||||
void Dialog::SetMessage(const std::string& message)
|
||||
{
|
||||
this->message = message;
|
||||
invalidateLayout(true);
|
||||
}
|
||||
|
||||
void Dialog::SetType(StellarX::MessageBoxType type)
|
||||
{
|
||||
this->type = type;
|
||||
invalidateLayout(true);
|
||||
}
|
||||
|
||||
void Dialog::SetModal(bool modal)
|
||||
{
|
||||
this->modal = modal;
|
||||
}
|
||||
|
||||
void Dialog::SetResult(StellarX::MessageBoxResult result)
|
||||
{
|
||||
this->result = result;
|
||||
}
|
||||
|
||||
StellarX::MessageBoxResult Dialog::GetResult() const
|
||||
{
|
||||
return this->result;
|
||||
}
|
||||
|
||||
bool Dialog::model() const
|
||||
{
|
||||
return modal;
|
||||
}
|
||||
|
||||
void Dialog::Show()
|
||||
{
|
||||
if (pendingCleanup)
|
||||
performDelayedCleanup();
|
||||
SX_LOGI("Dialog") << SX_T("对话框弹出:是否模态=","Dialog::Show: modal=") << (modal ? 1 : 0);
|
||||
|
||||
show = true;
|
||||
dirty = true;
|
||||
needsInitialization = true;
|
||||
|
||||
hWnd.dialogOpen = true;// 通知窗口有对话框打开
|
||||
|
||||
if (modal)
|
||||
{
|
||||
// 模态对话框需要阻塞当前线程直到对话框关闭
|
||||
// 记录当前窗口客户区尺寸,供轮询对比
|
||||
RECT rc0;
|
||||
GetClientRect(hWnd.getHwnd(), &rc0);
|
||||
int lastW = rc0.right - rc0.left;
|
||||
int lastH = rc0.bottom - rc0.top;
|
||||
|
||||
while (show)
|
||||
{
|
||||
// ① 轮询窗口尺寸(不依赖 WM_SIZE)
|
||||
RECT rc;
|
||||
GetClientRect(hWnd.getHwnd(), &rc);
|
||||
const int cw = rc.right - rc.left;
|
||||
const int ch = rc.bottom - rc.top;
|
||||
|
||||
if (cw != lastW || ch != lastH)
|
||||
{
|
||||
lastW = cw;
|
||||
lastH = ch;
|
||||
SX_LOGD("Resize") <<SX_T("模态对话框检测到窗口大小变化:(", "Modal dialog detected window size change: (") << cw << "x" << ch << ")";
|
||||
|
||||
// 通知父窗口:有新尺寸 → 标记 needResizeDirty
|
||||
hWnd.scheduleResizeFromModal(cw, ch);
|
||||
|
||||
// 立即统一收口:父窗重绘 背景+普通控件(不会画到这只模态)
|
||||
hWnd.pumpResizeIfNeeded();
|
||||
|
||||
// 这只模态只重新居中,不参与拉伸;背景快照需要在新位置重抓。
|
||||
recenterInHostWindow();
|
||||
}
|
||||
|
||||
// ② 处理这只对话框的鼠标/键盘(沿用原来 EX_MOUSE | EX_KEY)
|
||||
ExMessage msg;
|
||||
if (peekmessage(&msg, EX_MOUSE | EX_KEY))
|
||||
{
|
||||
handleEvent(msg);
|
||||
if (!show)
|
||||
break;
|
||||
}
|
||||
|
||||
// ③ 最后一笔:只画这只模态,保证永远在最上层
|
||||
if (dirty)
|
||||
{
|
||||
BeginBatchDraw();
|
||||
this->draw(); // 注意:不要 requestRepaint(parent),只画自己
|
||||
EndBatchDraw();
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
Sleep(10);
|
||||
}
|
||||
|
||||
if (pendingCleanup && !isCleaning)
|
||||
performDelayedCleanup();
|
||||
}
|
||||
else
|
||||
// 非模态对话框只需标记为可见,由主循环处理
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Dialog::Close()
|
||||
{
|
||||
if (!show) return;
|
||||
|
||||
show = false;
|
||||
dirty = true;
|
||||
pendingCleanup = true; // 只标记需要清理,不立即执行
|
||||
|
||||
// 工厂模式下非模态触发回调 返回结果
|
||||
if (resultCallback && !modal)
|
||||
resultCallback(this->result);
|
||||
}
|
||||
|
||||
void Dialog::recenterInHostWindow()
|
||||
{
|
||||
if (!show)
|
||||
return;
|
||||
|
||||
// 尚未完成首次初始化时,保持“延迟初始化”语义,由首次 draw 统一创建子控件。
|
||||
if (needsInitialization || width <= 0 || height <= 0)
|
||||
{
|
||||
dirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const int newX = (hWnd.getWidth() - width) / 2;
|
||||
const int newY = (hWnd.getHeight() - height) / 2;
|
||||
invalidateBackgroundSnapshot();
|
||||
|
||||
x = newX;
|
||||
y = newY;
|
||||
rebuildChrome();
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Dialog::initButtons()
|
||||
{
|
||||
switch (this->type)
|
||||
{
|
||||
case StellarX::MessageBoxType::OK: // 只有确定按钮
|
||||
{
|
||||
auto okbutton = createDialogButton((this->x + (this->width - (functionButtonWidth * buttonNum + buttonMargin * (buttonNum - 1))) / 2),
|
||||
((this->y + (this->height - buttonAreaHeight)) + (buttonAreaHeight - functionButtonHeight) / 2),
|
||||
"确定"
|
||||
);
|
||||
okbutton->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::OK);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close(); });
|
||||
|
||||
okbutton->textStyle = this->textStyle;
|
||||
|
||||
this->addControl(std::move(okbutton));
|
||||
}
|
||||
break;
|
||||
case StellarX::MessageBoxType::OKCancel: // 确定和取消按钮
|
||||
{
|
||||
auto okButton = createDialogButton(
|
||||
(this->x + (this->width - (functionButtonWidth * buttonNum + buttonMargin * (buttonNum - 1))) / 2),
|
||||
((this->y + (this->height - buttonAreaHeight)) + (buttonAreaHeight - functionButtonHeight) / 2),
|
||||
"确定"
|
||||
);
|
||||
okButton->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::OK);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close(); });
|
||||
|
||||
auto cancelButton = createDialogButton(
|
||||
(okButton.get()->getX() + okButton.get()->getWidth() + buttonMargin),
|
||||
okButton.get()->getY(),
|
||||
"取消"
|
||||
);
|
||||
cancelButton->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::Cancel);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close(); });
|
||||
|
||||
okButton->textStyle = this->textStyle;
|
||||
cancelButton->textStyle = this->textStyle;
|
||||
|
||||
this->addControl(std::move(okButton));
|
||||
this->addControl(std::move(cancelButton));
|
||||
}
|
||||
break;
|
||||
case StellarX::MessageBoxType::YesNo: // 是和否按钮
|
||||
{
|
||||
auto yesButton = createDialogButton(
|
||||
(this->x + (this->width - (functionButtonWidth * buttonNum + buttonMargin * (buttonNum - 1))) / 2),
|
||||
((this->y + (this->height - buttonAreaHeight)) + (buttonAreaHeight - functionButtonHeight) / 2),
|
||||
"是"
|
||||
);
|
||||
yesButton->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::Yes);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close(); });
|
||||
|
||||
auto noButton = createDialogButton(
|
||||
(yesButton.get()->getX() + yesButton.get()->getWidth() + buttonMargin),
|
||||
yesButton.get()->getY(),
|
||||
"否"
|
||||
);
|
||||
noButton->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::No);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close(); });
|
||||
|
||||
yesButton->textStyle = this->textStyle;
|
||||
noButton->textStyle = this->textStyle;
|
||||
|
||||
this->addControl(std::move(yesButton));
|
||||
this->addControl(std::move(noButton));
|
||||
}
|
||||
break;
|
||||
case StellarX::MessageBoxType::YesNoCancel: // 是、否和取消按钮
|
||||
{
|
||||
auto yesButton = createDialogButton(
|
||||
(this->x + (this->width - (functionButtonWidth * buttonNum + buttonMargin * (buttonNum - 1))) / 2),
|
||||
((this->y + (this->height - buttonAreaHeight)) + (buttonAreaHeight - functionButtonHeight) / 2),
|
||||
"是"
|
||||
);
|
||||
yesButton->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::Yes);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close(); });
|
||||
|
||||
auto noButton = createDialogButton(
|
||||
yesButton.get()->getX() + yesButton.get()->getWidth() + buttonMargin,
|
||||
yesButton.get()->getY(),
|
||||
"否"
|
||||
);
|
||||
noButton->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::No);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close(); });
|
||||
|
||||
auto cancelButton = createDialogButton(
|
||||
noButton.get()->getX() + noButton.get()->getWidth() + buttonMargin,
|
||||
noButton.get()->getY(),
|
||||
"取消"
|
||||
);
|
||||
cancelButton->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::Cancel);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close(); });
|
||||
|
||||
yesButton->textStyle = this->textStyle;
|
||||
noButton->textStyle = this->textStyle;
|
||||
cancelButton->textStyle = this->textStyle;
|
||||
|
||||
this->addControl(std::move(yesButton));
|
||||
this->addControl(std::move(noButton));
|
||||
this->addControl(std::move(cancelButton));
|
||||
}
|
||||
break;
|
||||
case StellarX::MessageBoxType::RetryCancel: // 重试和取消按钮
|
||||
{
|
||||
auto retryButton = createDialogButton(
|
||||
(this->x + (this->width - (functionButtonWidth * buttonNum + buttonMargin * (buttonNum - 1))) / 2),
|
||||
((this->y + (this->height - buttonAreaHeight)) + (buttonAreaHeight - functionButtonHeight) / 2),
|
||||
"重试"
|
||||
);
|
||||
retryButton->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::Retry);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close(); });
|
||||
|
||||
auto cancelButton = createDialogButton(
|
||||
retryButton.get()->getX() + retryButton.get()->getWidth() + buttonMargin,
|
||||
retryButton.get()->getY(),
|
||||
"取消"
|
||||
);
|
||||
cancelButton->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::Cancel);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close(); });
|
||||
|
||||
retryButton->textStyle = this->textStyle;
|
||||
cancelButton->textStyle = this->textStyle;
|
||||
|
||||
this->addControl(std::move(retryButton));
|
||||
this->addControl(std::move(cancelButton));
|
||||
}
|
||||
break;
|
||||
case StellarX::MessageBoxType::AbortRetryIgnore: // 中止、重试和忽略按钮
|
||||
{
|
||||
auto abortButton = createDialogButton(
|
||||
(this->x + (this->width - (functionButtonWidth * buttonNum + buttonMargin * (buttonNum - 1))) / 2),
|
||||
((this->y + (this->height - buttonAreaHeight)) + (buttonAreaHeight - functionButtonHeight) / 2),
|
||||
"中止"
|
||||
);
|
||||
abortButton->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::Abort);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close();
|
||||
});
|
||||
auto retryButton = createDialogButton(
|
||||
abortButton.get()->getX() + abortButton.get()->getWidth() + buttonMargin,
|
||||
abortButton.get()->getY(),
|
||||
"重试"
|
||||
);
|
||||
retryButton->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::Retry);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close();
|
||||
});
|
||||
auto ignoreButton = createDialogButton(
|
||||
retryButton.get()->getX() + retryButton.get()->getWidth() + buttonMargin,
|
||||
retryButton.get()->getY(),
|
||||
"忽略"
|
||||
);
|
||||
ignoreButton.get()->setOnClickListener([this]()
|
||||
{
|
||||
this->SetResult(StellarX::MessageBoxResult::Ignore);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close();
|
||||
});
|
||||
|
||||
abortButton->textStyle = this->textStyle;
|
||||
retryButton->textStyle = this->textStyle;
|
||||
ignoreButton->textStyle = this->textStyle;
|
||||
|
||||
this->addControl(std::move(abortButton));
|
||||
this->addControl(std::move(retryButton));
|
||||
this->addControl(std::move(ignoreButton));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Dialog::initCloseButton()
|
||||
{
|
||||
//初始化关闭按钮
|
||||
auto but = std::make_unique<Button>
|
||||
(
|
||||
(this->x + this->width - closeButtonWidth) - 3, (this->y + 3), closeButtonWidth - 1, closeButtonHeight,
|
||||
"X", // 按钮文本
|
||||
RGB(255, 0, 0), // 按钮被点击颜色
|
||||
this->canvasBkClor, // 按钮背景颜色
|
||||
RGB(255, 0, 0), // 按钮被悬停颜色
|
||||
StellarX::ButtonMode::NORMAL,
|
||||
StellarX::ControlShape::B_RECTANGLE
|
||||
);
|
||||
but.get()->setButtonFalseColor(this->backgroundColor);
|
||||
but.get()->enableTooltip(false);
|
||||
but->setOnClickListener([this]() {
|
||||
this->SetResult(StellarX::MessageBoxResult::Cancel);
|
||||
this->hWnd.dialogClose = true;
|
||||
this->Close(); });
|
||||
|
||||
this->closeButton = but.get();
|
||||
this->addControl(std::move(but));
|
||||
}
|
||||
|
||||
void Dialog::splitMessageLines()
|
||||
{
|
||||
lines.clear(); // 先清空现有的行
|
||||
|
||||
std::string currentLine;
|
||||
for (size_t i = 0; i < message.length(); i++) {
|
||||
// 处理 换行符 \r\n \n \r
|
||||
const bool hasNext = (i + 1 < message.length());
|
||||
const bool isLineBreak = (message[i] == '\r' || message[i] == '\n');
|
||||
const bool isCrLf = hasNext && message[i] == '\r' && message[i + 1] == '\n';
|
||||
if (isLineBreak || isCrLf)
|
||||
{
|
||||
if (!currentLine.empty()) {
|
||||
lines.push_back(currentLine);
|
||||
currentLine.clear();
|
||||
}
|
||||
|
||||
if (isCrLf)
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
currentLine += message[i];
|
||||
}
|
||||
|
||||
// 添加最后一行(如果有内容)
|
||||
if (!currentLine.empty())
|
||||
{
|
||||
lines.push_back(currentLine);
|
||||
}
|
||||
|
||||
// 如果消息为空,至少添加一个空行
|
||||
if (lines.empty())
|
||||
{
|
||||
lines.push_back("");
|
||||
}
|
||||
}
|
||||
|
||||
void Dialog::getTextSize()
|
||||
{
|
||||
saveStyle();
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
this->textHeight = 0;
|
||||
this->textWidth = 0;
|
||||
for (auto& text : lines)
|
||||
{
|
||||
int w = textwidth(LPCTSTR(text.c_str()));
|
||||
int h = textheight(LPCTSTR(text.c_str()));
|
||||
if (this->textHeight < h)
|
||||
this->textHeight = h;
|
||||
if (this->textWidth < w)
|
||||
this->textWidth = w;
|
||||
}
|
||||
|
||||
restoreStyle();
|
||||
}
|
||||
|
||||
void Dialog::invalidateLayout(bool clearChildren)
|
||||
{
|
||||
if (clearChildren)
|
||||
clearControls();
|
||||
|
||||
this->textWidth = 0;
|
||||
this->textHeight = 0;
|
||||
this->buttonNum = 0;
|
||||
this->needsInitialization = true;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Dialog::rebuildChrome()
|
||||
{
|
||||
clearControls();
|
||||
initButtons();
|
||||
initCloseButton();
|
||||
}
|
||||
|
||||
// 计算逻辑:对话框宽度取【文本区域最大宽度】和【按钮区域总宽度】中的较大值。
|
||||
// 对话框高度 = 标题栏 + 文本区 + 按钮区 + 各种间距。
|
||||
void Dialog::initDialogSize()
|
||||
{
|
||||
this->textStyle.nHeight = 20;
|
||||
splitMessageLines(); // 分割消息行
|
||||
getTextSize(); // 获取文本最大尺寸
|
||||
|
||||
// 获取功能按钮数量
|
||||
switch (this->type)
|
||||
{
|
||||
case StellarX::MessageBoxType::OK: // 只有确定按钮
|
||||
buttonNum = 1;
|
||||
break;
|
||||
case StellarX::MessageBoxType::OKCancel: // 确定和取消按钮
|
||||
case StellarX::MessageBoxType::YesNo: // 是和否按钮
|
||||
case StellarX::MessageBoxType::RetryCancel: // 重试和取消按钮
|
||||
buttonNum = 2;
|
||||
break;
|
||||
case StellarX::MessageBoxType::YesNoCancel: // 是、否和取消按钮
|
||||
case StellarX::MessageBoxType::AbortRetryIgnore: // 中止、重试和忽略按钮
|
||||
buttonNum = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
// 计算按钮区域宽度
|
||||
int buttonAreaWidth = buttonNum * functionButtonWidth +
|
||||
(buttonNum > 0 ? (buttonNum + 1) * buttonMargin : 0);
|
||||
|
||||
// 计算文本区域宽度(包括边距)
|
||||
int textAreaWidth = textWidth + textToBorderMargin * 2;
|
||||
saveStyle();
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
int titleAreaWidth = textwidth(LPCTSTR(titleText.c_str())) + textToBorderMargin * 2 + closeButtonWidth + buttonMargin;
|
||||
restoreStyle();
|
||||
|
||||
// 对话框宽度取文本、标题和按钮区域中的较大值,并确保最小宽度
|
||||
this->width = buttonAreaWidth > textAreaWidth ? buttonAreaWidth : textAreaWidth;
|
||||
if (titleAreaWidth > this->width)
|
||||
this->width = titleAreaWidth;
|
||||
this->width = this->width > 200 ? this->width : 200;
|
||||
|
||||
// 计算对话框高度
|
||||
// 高度 = 标题栏高度 + 文本区域高度 + 按钮区域高度 + 间距
|
||||
int textAreaHeight = textHeight * (int)lines.size() + 5 * ((int)lines.size() - 1); // 文本行高+行间距
|
||||
this->height = closeButtonHeight + // 标题栏高度
|
||||
titleToTextMargin + // 标题到文本的间距
|
||||
textAreaHeight + // 文本区域高度
|
||||
buttonAreaHeight; // 按钮区域高度
|
||||
|
||||
// 居中定位对话框
|
||||
this->x = (hWnd.getWidth() - this->width) / 2;
|
||||
this->y = (hWnd.getHeight() - this->height) / 2;
|
||||
|
||||
rebuildChrome();
|
||||
}
|
||||
|
||||
void Dialog::addControl(std::unique_ptr<Control> control)
|
||||
{
|
||||
control->setParent(this);
|
||||
controls.push_back(std::move(control));
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// 延迟清理策略:由于对话框绘制时保存了背景快照,必须在对话框隐藏后、
|
||||
// 所有控件析构前恢复背景,否则会导致背景图像被错误覆盖。
|
||||
// 此方法在对话框不可见且被标记为待清理时由 draw() 或 handleEvent() 调用。
|
||||
void Dialog::performDelayedCleanup()
|
||||
{
|
||||
if (isCleaning) return;
|
||||
|
||||
isCleaning = true;
|
||||
|
||||
auto& c = hWnd.getControls();
|
||||
for (auto& control : c)
|
||||
control->setDirty(true);
|
||||
|
||||
controls.clear();
|
||||
|
||||
// 重置指针
|
||||
closeButton = nullptr;
|
||||
// 释放背景图像资源
|
||||
if (saveBkImage && hasSnap)
|
||||
{
|
||||
restBackground();
|
||||
FlushBatchDraw();
|
||||
invalidateBackgroundSnapshot();
|
||||
}
|
||||
if (!(saveBkImage && hasSnap))
|
||||
{
|
||||
// 没有背景快照:强制一次完整重绘,立即擦掉残影
|
||||
hWnd.pumpResizeIfNeeded(); // 如果正好有尺寸标志,顺便统一收口
|
||||
// 即使没有尺寸变化,也重绘一帧
|
||||
BeginBatchDraw();
|
||||
// 背景
|
||||
if (hWnd.getBkImage() && !hWnd.getBkImageFile().empty())
|
||||
putimage(0, 0, hWnd.getBkImage());
|
||||
else { setbkcolor(hWnd.getBkcolor()); cleardevice(); }
|
||||
// 所有普通控件
|
||||
for (auto& c : hWnd.getControls()) c->draw();
|
||||
// 其他对话框(this 已经 show=false,会早退不绘)
|
||||
EndBatchDraw();
|
||||
FlushBatchDraw();
|
||||
}
|
||||
// 重置状态
|
||||
needsInitialization = true;
|
||||
pendingCleanup = false;
|
||||
isCleaning = false;
|
||||
}
|
||||
|
||||
void Dialog::SetResultCallback(std::function<void(StellarX::MessageBoxResult)> cb)
|
||||
{
|
||||
resultCallback = std::move(cb);
|
||||
}
|
||||
|
||||
std::string Dialog::GetCaption() const
|
||||
{
|
||||
return titleText;
|
||||
}
|
||||
|
||||
std::string Dialog::GetText() const
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
void Dialog::clearControls()
|
||||
{
|
||||
for (auto& control : controls)
|
||||
control->invalidateBackgroundSnapshot();
|
||||
|
||||
controls.clear();
|
||||
// 重置按钮指针
|
||||
closeButton = nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<Button> Dialog::createDialogButton(int x, int y, const std::string& text)
|
||||
{
|
||||
auto btn = std::make_unique<Button>(
|
||||
x, y, functionButtonWidth, functionButtonHeight,
|
||||
text,
|
||||
buttonTrueColor, // 点击色
|
||||
buttonFalseColor, // 背景色
|
||||
buttonHoverColor, // 悬停色
|
||||
StellarX::ButtonMode::NORMAL,
|
||||
StellarX::ControlShape::RECTANGLE
|
||||
);
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
void Dialog::requestRepaint(Control* parent)
|
||||
{
|
||||
if (shouldDeferManagedRepaint())
|
||||
{
|
||||
// 非模态 Dialog 在 Window 主循环中也走托管提交;
|
||||
// 这样底层控件和对话框的绘制顺序由 Window 统一收口控制。
|
||||
if (auto* host = getHostWindow())
|
||||
host->requestManagedRepaint(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this == parent)
|
||||
{
|
||||
for (auto& control : controls)
|
||||
if (control->isDirty() && control->IsVisible())
|
||||
control->draw();
|
||||
}
|
||||
else
|
||||
onRequestRepaintAsRoot();
|
||||
}
|
||||
|
||||
bool Dialog::canCommitManagedPartialRepaint() const
|
||||
{
|
||||
// Dialog 只有在“自身底板不脏 + 仍持有有效背景快照”时,
|
||||
// 才能安全地只更新内部按钮,而不重画整个对话框底板。
|
||||
return show && !dirty && hasValidBackgroundSnapshot();
|
||||
}
|
||||
|
||||
void Dialog::commitManagedRepaint()
|
||||
{
|
||||
if (!show)
|
||||
return;
|
||||
|
||||
if (canCommitManagedPartialRepaint())
|
||||
{
|
||||
// 背景快照完好:沿用 Dialog 自己已有的局部重绘路径。
|
||||
requestRepaint(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// 对话框底板本身已脏,或快照失效:必须整 Dialog 重画。
|
||||
this->dirty = true;
|
||||
onRequestRepaintAsRoot();
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* @类: Dialog
|
||||
* @摘要: 模态和非模态对话框控件,提供丰富的消息框功能
|
||||
* @描述:
|
||||
* 实现完整的对话框功能,支持多种按钮组合和异步结果回调。
|
||||
* 自动处理布局、背景保存恢复和生命周期管理。
|
||||
* 在窗口托管重绘模式下,Dialog 自身也是一个独立的重绘 root。
|
||||
*
|
||||
* @特性:
|
||||
* - 支持六种标准消息框类型(OK、YesNo、YesNoCancel等)
|
||||
* - 模态和非模态两种工作模式
|
||||
* - 自动文本换行和尺寸计算
|
||||
* - 背景保存和恢复机制
|
||||
* - 工厂模式下的去重检测
|
||||
*
|
||||
* @使用场景: 用户提示、确认操作、输入请求等交互场景
|
||||
* @所属框架: 星垣(StellarX) GUI框架
|
||||
* @作者: 我在人间做废物
|
||||
******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
#include"StellarX.h"
|
||||
|
||||
#define closeButtonWidth 30 //关闭按钮宽度
|
||||
#define closeButtonHeight 20 //关闭按钮高度 同时作为对话框标题栏高度
|
||||
#define functionButtonWidth 70 //按钮宽度
|
||||
#define functionButtonHeight 30 //按钮高度
|
||||
#define buttonMargin 10 //按钮间距
|
||||
|
||||
#define buttonAreaHeight 50 //按钮区域高度
|
||||
#define titleToTextMargin 10 //标题到文本的距离
|
||||
#define textToBorderMargin 10 //文本到边框的距离
|
||||
#define BorderWidth 3 //边框宽度
|
||||
class Dialog : public Canvas
|
||||
{
|
||||
friend class Window;
|
||||
|
||||
Window& hWnd; //窗口引用
|
||||
|
||||
int textWidth = 0; //文本宽度
|
||||
int textHeight = 0; //文本高度
|
||||
int buttonNum = 0; // 按钮数量
|
||||
|
||||
StellarX::MessageBoxType type = StellarX::MessageBoxType::OK; //对话框类型
|
||||
std::string titleText = "提示"; //标题文本
|
||||
|
||||
std::string message; //提示信息
|
||||
std::vector<std::string> lines; //消息内容按行分割
|
||||
|
||||
bool needsInitialization = true; // 是否需要根据当前内容重新初始化布局和内部按钮
|
||||
bool modal = true; //是否模态
|
||||
|
||||
COLORREF backgroundColor = RGB(240, 240, 240); //背景颜色
|
||||
COLORREF borderColor = RGB(100, 100, 100); //边框颜色
|
||||
|
||||
COLORREF buttonTrueColor = RGB(211, 190, 190); //按钮被点击颜色
|
||||
COLORREF buttonFalseColor = RGB(215, 215, 215); //按钮未被点击颜色
|
||||
COLORREF buttonHoverColor = RGB(224, 224, 224); //按钮悬浮颜色
|
||||
|
||||
Button* closeButton = nullptr; //关闭按钮
|
||||
|
||||
StellarX::MessageBoxResult result = StellarX::MessageBoxResult::Cancel; // 对话框结果
|
||||
|
||||
bool isCleaning = false; //是否正在清理
|
||||
bool pendingCleanup = false; //延迟清理
|
||||
public:
|
||||
StellarX::ControlText textStyle; // 字体样式
|
||||
// 在事件安全点执行延迟清理;用于非模态关闭后统一回收
|
||||
void performDelayedCleanup();
|
||||
// 非模态工厂调用时用于回传结果
|
||||
std::function<void(StellarX::MessageBoxResult)> resultCallback;
|
||||
// 设置非模态结果回调
|
||||
void SetResultCallback(std::function<void(StellarX::MessageBoxResult)> cb);
|
||||
// 获取标题文本,用于非模态去重
|
||||
std::string GetCaption() const;
|
||||
// 获取正文文本,用于非模态去重
|
||||
std::string GetText() const;
|
||||
|
||||
public:
|
||||
// 构造对话框;modal=false 时按非模态浮层工作
|
||||
Dialog(Window& hWnd, std::string text, std::string message = "对话框", StellarX::MessageBoxType type = StellarX::MessageBoxType::OK, bool modal = true);
|
||||
~Dialog();
|
||||
//绘制对话框
|
||||
void draw() override;
|
||||
//事件处理
|
||||
bool handleEvent(const ExMessage& msg) override;
|
||||
// 设置标题
|
||||
void SetTitle(const std::string& title);
|
||||
// 设置消息内容
|
||||
void SetMessage(const std::string& message);
|
||||
// 设置对话框类型
|
||||
void SetType(StellarX::MessageBoxType type);
|
||||
// 设置模态属性
|
||||
void SetModal(bool modal);
|
||||
// 设置对话框结果
|
||||
void SetResult(StellarX::MessageBoxResult result);
|
||||
// 获取对话框结果
|
||||
StellarX::MessageBoxResult GetResult() const;
|
||||
|
||||
// 返回当前对话框是否为模态
|
||||
bool model() const override;
|
||||
|
||||
// 显示对话框
|
||||
void Show();
|
||||
// 关闭对话框
|
||||
void Close();
|
||||
// 宿主窗口变化时仅重新居中,不拉伸 Dialog 自身
|
||||
void recenterInHostWindow();
|
||||
|
||||
private:
|
||||
// 初始化按钮
|
||||
void initButtons();
|
||||
// 初始化关闭按钮
|
||||
void initCloseButton();
|
||||
// 按行分割消息内容
|
||||
void splitMessageLines();
|
||||
// 获取文本大小
|
||||
void getTextSize();
|
||||
// 标记需要重新布局并按需清空旧控件
|
||||
void invalidateLayout(bool clearChildren);
|
||||
//初始化对话框尺寸
|
||||
void initDialogSize();
|
||||
// 依据当前 Dialog 的 x/y/width/height 重新创建标题和按钮
|
||||
void rebuildChrome();
|
||||
// 禁止走普通 Canvas 添加路径;Dialog 内部控件由自身 chrome 重建逻辑统一维护
|
||||
void addControl(std::unique_ptr<Control> control);
|
||||
bool canCommitManagedPartialRepaint() const override; // 判断当前 Dialog 是否可安全做局部提交
|
||||
void commitManagedRepaint() override; // 托管收口阶段执行 Dialog 的真正重绘
|
||||
|
||||
// 清除所有控件
|
||||
void clearControls();
|
||||
// 创建标准对话框功能按钮
|
||||
std::unique_ptr<Button> createDialogButton(int x, int y, const std::string& text);
|
||||
void requestRepaint(Control* parent) override; // 托管模式下登记为 Dialog root;非托管模式下立即更新内部按钮
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
# Doxyfile 1.9.1
|
||||
|
||||
# 项目相关配置
|
||||
DOXYFILE_ENCODING = UTF-8
|
||||
PROJECT_NAME = "星垣 (StellarX) GUI Framework"
|
||||
PROJECT_NUMBER = 1.0.0
|
||||
PROJECT_BRIEF = "A lightweight, modular C++ GUI framework for Windows"
|
||||
PROJECT_LOGO =
|
||||
OUTPUT_DIRECTORY = docs
|
||||
CREATE_SUBDIRS = NO
|
||||
ALLOW_UNICODE_NAMES = YES
|
||||
OUTPUT_LANGUAGE = English
|
||||
BRIEF_MEMBER_DESC = YES
|
||||
REPEAT_BRIEF = YES
|
||||
ABBREVIATE_BRIEF =
|
||||
ALWAYS_DETAILED_SEC = NO
|
||||
INLINE_INHERITED_MEMB = NO
|
||||
FULL_PATH_NAMES = YES
|
||||
STRIP_FROM_PATH =
|
||||
STRIP_FROM_INC_PATH =
|
||||
SHORT_NAMES = NO
|
||||
JAVADOC_AUTOBRIEF = YES
|
||||
JAVADOC_BANNER = NO
|
||||
QT_AUTOBRIEF = NO
|
||||
MULTILINE_CPP_IS_BRIEF = NO
|
||||
INHERIT_DOCS = YES
|
||||
SEPARATE_MEMBER_PAGES = NO
|
||||
TAB_SIZE = 4
|
||||
ALIASES =
|
||||
OPTIMIZE_OUTPUT_FOR_C = NO
|
||||
OPTIMIZE_OUTPUT_JAVA = NO
|
||||
OPTIMIZE_FOR_FORTRAN = NO
|
||||
OPTIMIZE_OUTPUT_VHDL = NO
|
||||
EXTENSION_MAPPING =
|
||||
MARKDOWN_SUPPORT = YES
|
||||
|
||||
# 构建类型
|
||||
EXTRACT_ALL = YES
|
||||
EXTRACT_PRIVATE = YES
|
||||
EXTRACT_PACKAGE = YES
|
||||
EXTRACT_STATIC = YES
|
||||
EXTRACT_LOCAL_CLASSES = YES
|
||||
EXTRACT_LOCAL_METHODS = NO
|
||||
EXTRACT_ANON_NSPACES = NO
|
||||
HIDE_UNDOC_MEMBERS = NO
|
||||
HIDE_UNDOC_CLASSES = NO
|
||||
HIDE_FRIEND_COMPOUNDS = NO
|
||||
HIDE_IN_BODY_DOCS = NO
|
||||
INTERNAL_DOCS = NO
|
||||
CASE_SENSE_NAMES = NO
|
||||
HIDE_SCOPE_NAMES = NO
|
||||
HIDE_COMPOUND_REFERENCE= NO
|
||||
SHOW_INCLUDE_FILES = YES
|
||||
SHOW_GROUPED_MEMB_INC = NO
|
||||
FORCE_LOCAL_INCLUDES = NO
|
||||
INLINE_INFO = YES
|
||||
SORT_MEMBER_DOCS = YES
|
||||
SORT_BRIEF_DOCS = NO
|
||||
SORT_MEMBERS_CTOR_1ST = NO
|
||||
SORT_GROUP_NAMES = YES
|
||||
SORT_BY_SCOPE_NAME = YES
|
||||
STRICT_PROTO_MATCHING = NO
|
||||
GENERATE_TODOLIST = YES
|
||||
GENERATE_TESTLIST = YES
|
||||
GENERATE_BUGLIST = YES
|
||||
GENERATE_DEPRECATEDLIST= YES
|
||||
MAX_INITIALIZER_LINES = 30
|
||||
SHOW_USED_FILES = YES
|
||||
SHOW_FILES = YES
|
||||
SHOW_NAMESPACES = YES
|
||||
FILE_VERSION_FILTER =
|
||||
LAYOUT_FILE =
|
||||
CITE_BIB_FILES =
|
||||
|
||||
# 输入配置
|
||||
INPUT = include src README.md
|
||||
INPUT_ENCODING = UTF-8
|
||||
FILE_PATTERNS = *.h *.hpp *.cpp *.c *.cc *.cxx *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.idl *.ddl *.odl *.cs *.php *.php3 *.inc *.m *.mm *.dox *.py *.f90 *.f *.for *.vhd *.vhdl *.md
|
||||
RECURSIVE = YES
|
||||
EXCLUDE =
|
||||
EXCLUDE_SYMLINKS = NO
|
||||
EXCLUDE_PATTERNS =
|
||||
EXCLUDE_SYMBOLS =
|
||||
EXAMPLE_PATH = examples
|
||||
EXAMPLE_PATTERNS = *
|
||||
EXAMPLE_RECURSIVE = NO
|
||||
IMAGE_PATH =
|
||||
INPUT_FILTER =
|
||||
FILTER_PATTERNS =
|
||||
FILTER_SOURCE_FILES = NO
|
||||
FILTER_SOURCE_PATTERNS =
|
||||
USE_MDFILE_AS_MAINPAGE = README.md
|
||||
|
||||
# 源代码浏览配置
|
||||
SOURCE_BROWSER = YES
|
||||
INLINE_SOURCES = NO
|
||||
STRIP_CODE_COMMENTS = YES
|
||||
REFERENCED_BY_RELATION = YES
|
||||
REFERENCES_RELATION = YES
|
||||
REFERENCES_LINK_SOURCE = YES
|
||||
SOURCE_TOOLTIPS = YES
|
||||
USE_HTAGS = NO
|
||||
VERBATIM_HEADERS = YES
|
||||
|
||||
# HTML输出配置
|
||||
GENERATE_HTML = YES
|
||||
HTML_OUTPUT = html
|
||||
HTML_FILE_EXTENSION = .html
|
||||
HTML_HEADER =
|
||||
HTML_FOOTER =
|
||||
HTML_STYLESHEET =
|
||||
HTML_EXTRA_STYLESHEET =
|
||||
HTML_EXTRA_FILES =
|
||||
HTML_COLORSTYLE_HUE = 220
|
||||
HTML_COLORSTYLE_SAT = 100
|
||||
HTML_COLORSTYLE_GAMMA = 80
|
||||
HTML_TIMESTAMP = YES
|
||||
HTML_DYNAMIC_SECTIONS = NO
|
||||
HTML_INDEX_NUM_ENTRIES = 100
|
||||
GENERATE_DOCSET = NO
|
||||
DOCSET_FEEDNAME = "Doxygen generated docs"
|
||||
DOCSET_BUNDLE_ID = org.doxygen.Project
|
||||
DOCSET_PUBLISHER_ID = org.doxygen.Publisher
|
||||
DOCSET_PUBLISHER_NAME = Publisher
|
||||
GENERATE_HTMLHELP = NO
|
||||
GENERATE_CHI = NO
|
||||
GENERATE_QHP = NO
|
||||
GENERATE_ECLIPSEHELP = NO
|
||||
DISABLE_INDEX = NO
|
||||
ENUM_VALUES_PER_LINE = 4
|
||||
GENERATE_TREEVIEW = YES
|
||||
TREEVIEW_WIDTH = 250
|
||||
EXT_LINKS_IN_WINDOW = NO
|
||||
FORMULA_FONTSIZE = 10
|
||||
FORMULA_TRANSPARENT = YES
|
||||
USE_MATHJAX = NO
|
||||
MATHJAX_FORMAT = HTML-CSS
|
||||
MATHJAX_RELPATH =
|
||||
MATHJAX_EXTENSIONS =
|
||||
MATHJAX_CODEFILE =
|
||||
SEARCHENGINE = YES
|
||||
SERVER_BASED_SEARCH = NO
|
||||
EXTERNAL_SEARCH = NO
|
||||
SEARCHDATA_FILE = searchdata.xml
|
||||
EXTERNAL_SEARCH_ID =
|
||||
EXTRA_SEARCH_MAPPINGS =
|
||||
|
||||
# LaTeX输出配置
|
||||
GENERATE_LATEX = NO
|
||||
|
||||
# RTF输出配置
|
||||
GENERATE_RTF = NO
|
||||
|
||||
# Man page输出配置
|
||||
GENERATE_MAN = NO
|
||||
|
||||
# XML输出配置
|
||||
GENERATE_XML = NO
|
||||
|
||||
# DOCBOOK输出配置
|
||||
GENERATE_DOCBOOK = NO
|
||||
|
||||
# 自动生成配置
|
||||
GENERATE_AUTOGEN_DEF = NO
|
||||
|
||||
# 配置生成器
|
||||
ENABLE_PREPROCESSING = YES
|
||||
MACRO_EXPANSION = YES
|
||||
EXPAND_ONLY_PREDEF = NO
|
||||
SEARCH_INCLUDES = YES
|
||||
INCLUDE_PATH =
|
||||
INCLUDE_FILE_PATTERNS =
|
||||
PREDEFINED = WIN32=1 \
|
||||
_WINDOWS=1 \
|
||||
UNICODE=1 \
|
||||
_UNICODE=1
|
||||
EXPAND_AS_DEFINED =
|
||||
SKIP_FUNCTION_MACROS = YES
|
||||
|
||||
# 外部引用
|
||||
TAGFILES =
|
||||
GENERATE_TAGFILE =
|
||||
ALLEXTERNALS = NO
|
||||
EXTERNAL_GROUPS = YES
|
||||
EXTERNAL_PAGES = YES
|
||||
|
||||
# 图表生成
|
||||
CLASS_DIAGRAMS = YES
|
||||
MSCGEN_PATH =
|
||||
DIA_PATH =
|
||||
HIDE_UNDOC_RELATIONS = YES
|
||||
HAVE_DOT = YES
|
||||
DOT_NUM_THREADS = 0
|
||||
DOT_FONTNAME = Helvetica
|
||||
DOT_FONTSIZE = 10
|
||||
DOT_FONTPATH =
|
||||
CLASS_GRAPH = YES
|
||||
COLLABORATION_GRAPH = YES
|
||||
GROUP_GRAPHS = YES
|
||||
UML_LOOK = NO
|
||||
UML_LIMIT_NUM_FIELDS = 10
|
||||
TEMPLATE_RELATIONS = YES
|
||||
INCLUDE_GRAPH = YES
|
||||
INCLUDED_BY_GRAPH = YES
|
||||
CALL_GRAPH = YES
|
||||
CALLER_GRAPH = YES
|
||||
GRAPHICAL_HIERARCHY = YES
|
||||
DIRECTORY_GRAPH = YES
|
||||
DOT_IMAGE_FORMAT = png
|
||||
INTERACTIVE_SVG = NO
|
||||
DOT_PATH =
|
||||
DOTFILE_DIRS =
|
||||
MSCFILE_DIRS =
|
||||
DOT_GRAPH_MAX_NODES = 50
|
||||
MAX_DOT_GRAPH_DEPTH = 0
|
||||
DOT_TRANSPARENT = YES
|
||||
DOT_MULTI_TARGETS = NO
|
||||
GENERATE_LEGEND = YES
|
||||
DOT_CLEANUP = YES
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 我在人间做废物
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,194 +0,0 @@
|
||||
#include "Label.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Label 的内容尺寸刷新不依赖 EasyX 当前绘图上下文,
|
||||
// 直接使用 GDI 依据“文本 + 字体样式”测量宽高。
|
||||
// 这样无论是文本变化、字体样式变化,还是控件在窗口正式绘制前进入 dirty,
|
||||
// 都可以先把运行态尺寸收口好,而不必等到 draw() 再决定几何。
|
||||
void MeasureLabelText(const std::string& text, const StellarX::ControlText& style, int& width, int& height)
|
||||
{
|
||||
width = 0;
|
||||
height = 0;
|
||||
|
||||
HDC hdc = CreateCompatibleDC(nullptr);
|
||||
if (!hdc)
|
||||
return;
|
||||
|
||||
LOGFONTA fontDesc{};
|
||||
fontDesc.lfHeight = style.nHeight;
|
||||
fontDesc.lfWidth = style.nWidth;
|
||||
fontDesc.lfEscapement = style.nEscapement;
|
||||
fontDesc.lfOrientation = style.nOrientation;
|
||||
fontDesc.lfWeight = style.nWeight;
|
||||
fontDesc.lfItalic = style.bItalic ? TRUE : FALSE;
|
||||
fontDesc.lfUnderline = style.bUnderline ? TRUE : FALSE;
|
||||
fontDesc.lfStrikeOut = style.bStrikeOut ? TRUE : FALSE;
|
||||
fontDesc.lfCharSet = DEFAULT_CHARSET;
|
||||
fontDesc.lfOutPrecision = OUT_DEFAULT_PRECIS;
|
||||
fontDesc.lfClipPrecision = CLIP_DEFAULT_PRECIS;
|
||||
fontDesc.lfQuality = DEFAULT_QUALITY;
|
||||
fontDesc.lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE;
|
||||
lstrcpynA(fontDesc.lfFaceName, style.lpszFace ? style.lpszFace : "微软雅黑", LF_FACESIZE);
|
||||
|
||||
HFONT font = CreateFontIndirectA(&fontDesc);
|
||||
HFONT oldFont = nullptr;
|
||||
if (font)
|
||||
oldFont = (HFONT)SelectObject(hdc, font);
|
||||
|
||||
if (text.empty())
|
||||
{
|
||||
TEXTMETRICA tm{};
|
||||
if (GetTextMetricsA(hdc, &tm))
|
||||
height = tm.tmHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
SIZE size{};
|
||||
if (GetTextExtentPoint32A(hdc, text.c_str(), (int)text.size(), &size))
|
||||
{
|
||||
width = size.cx;
|
||||
height = size.cy;
|
||||
}
|
||||
if (height == 0)
|
||||
{
|
||||
TEXTMETRICA tm{};
|
||||
if (GetTextMetricsA(hdc, &tm))
|
||||
height = tm.tmHeight;
|
||||
}
|
||||
}
|
||||
|
||||
if (font)
|
||||
{
|
||||
if (oldFont)
|
||||
SelectObject(hdc, oldFont);
|
||||
DeleteObject(font);
|
||||
}
|
||||
DeleteDC(hdc);
|
||||
}
|
||||
}
|
||||
|
||||
Label::Label()
|
||||
:Control(0, 0, 0, 0)
|
||||
{
|
||||
this->id = "Label";
|
||||
// Label 当前阶段明确走“内容驱动尺寸”语义:
|
||||
// 外部布局器只安置位置,不通过 Stretch 改写它的宽高。
|
||||
this->layoutCapability.allowStretchX = false;
|
||||
this->layoutCapability.allowStretchY = false;
|
||||
this->text = "默认标签";
|
||||
textStyle.color = RGB(0, 0, 0);
|
||||
textBkColor = RGB(255, 255, 255);; //默认白色背景
|
||||
}
|
||||
|
||||
Label::Label(int x, int y, std::string text, COLORREF textcolor, COLORREF bkColor)
|
||||
:Control(x, y, 0, 0)
|
||||
{
|
||||
this->id = "Label";
|
||||
this->layoutCapability.allowStretchX = false;
|
||||
this->layoutCapability.allowStretchY = false;
|
||||
this->text = text;
|
||||
textStyle.color = textcolor;
|
||||
textBkColor = bkColor; //默认白色背景
|
||||
}
|
||||
|
||||
void Label::refreshContentDrivenRuntimeGeometry()
|
||||
{
|
||||
// Label 的尺寸来源于“当前文本 + 当前字体样式”。
|
||||
// 这里只更新运行态 width/height,不自动回写 localWidth/localHeight,
|
||||
// 避免内容变化把设计基线偷偷带偏。
|
||||
if (contentMeasureValid &&
|
||||
lastMeasuredText == text &&
|
||||
!(lastMeasuredStyle != textStyle))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int newWidth = 0;
|
||||
int newHeight = 0;
|
||||
MeasureLabelText(text, textStyle, newWidth, newHeight);
|
||||
|
||||
const bool sizeChanged = (newWidth != this->width) || (newHeight != this->height);
|
||||
if (sizeChanged && hasSnap)
|
||||
{
|
||||
// 运行态尺寸变化时,先恢复旧背景,避免文本缩短后旧像素残留在屏幕上。
|
||||
discardBackground();
|
||||
}
|
||||
|
||||
this->width = newWidth;
|
||||
this->height = newHeight;
|
||||
lastMeasuredText = text;
|
||||
lastMeasuredStyle = textStyle;
|
||||
contentMeasureValid = true;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Label::draw()
|
||||
{
|
||||
if (dirty && show)
|
||||
{
|
||||
saveStyle();
|
||||
if (textBkDisap)
|
||||
setbkmode(TRANSPARENT); //设置背景透明
|
||||
else
|
||||
{
|
||||
setbkmode(OPAQUE); //设置背景不透明
|
||||
setbkcolor(textBkColor); //设置背景颜色
|
||||
}
|
||||
settextcolor(textStyle.color);
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut); //设置字体样式
|
||||
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage)
|
||||
saveBackground(this->x, this->y, this->width, this->height);
|
||||
// 恢复背景(清除旧内容)
|
||||
restBackground();
|
||||
outtextxy(x, y, LPCTSTR(text.c_str()));
|
||||
restoreStyle();
|
||||
dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Label::hide()
|
||||
{
|
||||
discardBackground(); // 还原并释放快照
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
void Label::setDirty(bool dirty)
|
||||
{
|
||||
if (dirty)
|
||||
{
|
||||
// 只要 Label 进入脏状态,就在绘制前把内容驱动尺寸同步到运行态。
|
||||
// 这样文本变化和字体样式变化都会在 draw() 之前完成几何刷新。
|
||||
refreshContentDrivenRuntimeGeometry();
|
||||
}
|
||||
this->dirty = dirty;
|
||||
}
|
||||
|
||||
void Label::setTextdisap(bool key)
|
||||
{
|
||||
textBkDisap = key;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Label::setTextBkColor(COLORREF color)
|
||||
{
|
||||
textBkColor = color;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Label::setText(std::string text)
|
||||
{
|
||||
this->text = text;
|
||||
refreshContentDrivenRuntimeGeometry();
|
||||
}
|
||||
|
||||
void Label::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
|
||||
{
|
||||
// 通用布局器给出的 rect.width/rect.height 只代表“外部几何解算结果”。
|
||||
// Label 当前阶段的语义是内容驱动尺寸,因此这里只接位置,不接外部宽高,
|
||||
// 宽高继续由 refreshContentDrivenRuntimeGeometry() 按文本和字体样式刷新。
|
||||
refreshContentDrivenRuntimeGeometry();
|
||||
applyRuntimeRectDirect(rect.worldX, rect.worldY, this->width, this->height);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* @类: Label
|
||||
* @摘要: 简单文本标签控件,用于显示静态文本
|
||||
* @描述:
|
||||
* 提供基本的文本显示功能,支持透明背景和自定义样式。
|
||||
* 不支持用户交互,专注于文本呈现。
|
||||
*
|
||||
* @特性:
|
||||
* - 支持背景透明/不透明模式
|
||||
* - 完整的文本样式控制(字体、颜色、效果)
|
||||
* - 自动适应文本内容
|
||||
* - 轻量级无事件处理开销
|
||||
*
|
||||
* @使用场景: 显示说明文字、标题、状态信息等静态内容
|
||||
* @所属框架: 星垣(StellarX) GUI框架
|
||||
* @作者: 我在人间做废物
|
||||
******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
#include "Control.h"
|
||||
|
||||
class Label : public Control
|
||||
{
|
||||
std::string text; //标签文本
|
||||
COLORREF textBkColor; //标签背景颜色
|
||||
bool textBkDisap = false; //标签背景是否透明
|
||||
std::string lastMeasuredText; // 最近一次参与尺寸测量的文本
|
||||
StellarX::ControlText lastMeasuredStyle; // 最近一次参与尺寸测量的字体样式
|
||||
bool contentMeasureValid = false; // 当前内容尺寸缓存是否有效
|
||||
|
||||
//标签事件处理(标签无事件)不实现具体代码
|
||||
bool handleEvent(const ExMessage& msg) override { return false; }
|
||||
//用来检查对话框是否模态,此控件不做实现
|
||||
bool model() const override { return false; };
|
||||
// 根据当前文本和字体样式刷新运行态尺寸。
|
||||
// 这是 Label 的内容驱动几何入口:
|
||||
// - 覆盖文本变化
|
||||
// - 覆盖字体样式变化
|
||||
// - 只更新运行态宽高,不自动提交设计基线 local*
|
||||
void refreshContentDrivenRuntimeGeometry();
|
||||
public:
|
||||
// 标签字体样式保持公开,便于用户直接改字段。
|
||||
// 但直接修改 textStyle 后,必须显式调用 setDirty(true),
|
||||
// 才会触发内容驱动尺寸刷新并让新的运行态宽高生效。
|
||||
StellarX::ControlText textStyle; //标签文本样式
|
||||
public:
|
||||
// 默认构造:创建一个内容驱动尺寸的标签
|
||||
Label();
|
||||
// 指定位置、文本和基础样式构造标签
|
||||
Label(int x, int y, std::string text = "标签", COLORREF textcolor = BLACK, COLORREF bkColor = RGB(255, 255, 255));
|
||||
|
||||
// 绘制标签文本
|
||||
void draw() override;
|
||||
//用于“隐藏提示框”时调用(还原并释放快照)
|
||||
void hide();
|
||||
// Label 的内容尺寸由文本和字体样式共同决定。
|
||||
// 因此只要进入 dirty 状态,就在绘制前先刷新一次运行态尺寸,
|
||||
// 避免 draw() 阶段继续承担新的几何决策。
|
||||
void setDirty(bool dirty) override;
|
||||
// 设置标签背景是否透明
|
||||
void setTextdisap(bool key);
|
||||
// 设置标签背景颜色
|
||||
void setTextBkColor(COLORREF color);
|
||||
// 设置标签文本,并触发内容驱动尺寸刷新
|
||||
void setText(std::string text);
|
||||
protected:
|
||||
// 统一布局器只负责安置 Label 的运行态位置;
|
||||
// 宽高仍由 Label 自己的内容驱动路径决定。
|
||||
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
#include "MessageBox.h"
|
||||
#include "SxLog.h"
|
||||
namespace StellarX
|
||||
{
|
||||
MessageBoxResult MessageBox::showModal(Window& wnd, const std::string& text, const std::string& caption,
|
||||
MessageBoxType type)
|
||||
{
|
||||
Dialog dlg(wnd, caption, text, type, true); // 模态
|
||||
SX_LOGI("MessageBox") << "show: Message=" << dlg.GetText()
|
||||
<< " modal=" << (dlg.model() ? 1 : 0);
|
||||
|
||||
dlg.Show();
|
||||
return dlg.GetResult();
|
||||
}
|
||||
|
||||
void MessageBox::showAsync(Window& wnd, const std::string& text, const std::string& caption, MessageBoxType type,
|
||||
std::function<void(MessageBoxResult)> onResult)
|
||||
{
|
||||
//去重,如果窗口内已有相同的对话框被触发,则不再创建
|
||||
if (wnd.hasNonModalDialogWithCaption(caption, text))
|
||||
{
|
||||
std::cout << "\a" << std::endl;
|
||||
return;
|
||||
}
|
||||
auto dlg = std::make_unique<Dialog>(wnd, caption, text,
|
||||
type, false); // 非模态
|
||||
SX_LOGI("MessageBox") << "show: Message=" << dlg->GetText()
|
||||
<< " modal=" << (dlg->model() ? 1 : 0);
|
||||
Dialog* dlgPtr = dlg.get();
|
||||
// 设置回调
|
||||
if (onResult)
|
||||
dlgPtr->SetResultCallback(std::move(onResult));
|
||||
// 交给 Window 管理生命周期
|
||||
wnd.addDialog(std::move(dlg));
|
||||
dlgPtr->Show();
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* @类: MessageBox
|
||||
* @摘要: 消息框工厂类,提供简化的对话框创建接口
|
||||
* @描述:
|
||||
* 封装对话框的创建和显示逻辑,提供静态方法供快速调用。
|
||||
* 支持模态阻塞和非模态回调两种使用方式。
|
||||
*
|
||||
* @特性:
|
||||
* - 静态方法调用,无需实例化
|
||||
* - 自动处理模态和非模态的逻辑差异
|
||||
* - 集成到窗口的对话框管理系统中
|
||||
* - 提供去重机制防止重复对话框
|
||||
*
|
||||
* @使用场景: 快速创建标准消息框,减少样板代码
|
||||
* @所属框架: 星垣(StellarX) GUI框架
|
||||
* @作者: 我在人间做废物
|
||||
******************************************************************************/
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include "CoreTypes.h"
|
||||
#include "Dialog.h"
|
||||
#include "Window.h"
|
||||
|
||||
namespace StellarX
|
||||
{
|
||||
class MessageBox
|
||||
{
|
||||
public:
|
||||
// 模态:阻塞直到关闭,返回结果
|
||||
// 直接创建并显示模态消息框,直到用户关闭后返回结果
|
||||
static MessageBoxResult showModal(Window& wnd,
|
||||
const std::string& text,
|
||||
const std::string& caption = "提示",
|
||||
MessageBoxType type = MessageBoxType::OK);
|
||||
|
||||
// 非模态:立即返回,通过回调异步获取结果
|
||||
// 创建并显示非模态消息框,关闭时通过回调返回结果
|
||||
static void showAsync(Window& wnd,
|
||||
const std::string& text,
|
||||
const std::string& caption = "提示",
|
||||
MessageBoxType type = MessageBoxType::OK,
|
||||
std::function<void(MessageBoxResult)> onResult = nullptr);
|
||||
};
|
||||
} // namespace StellarX
|
||||
@@ -0,0 +1,343 @@
|
||||
# 星垣 (StellarX) GUI Framework
|
||||
|
||||
 <!-- 新增版本徽章 -->
|
||||
 <!-- 新增下载徽章 -->
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
> **「繁星为界,轻若尘埃」** —— 一个为Windows平台打造的、极致轻量级、高度模块化的C++原生GUI框架。
|
||||
|
||||
`星垣 (StellarX)` 诞生于对现代GUI框架"过度臃肿"的反抗。它拒绝动辄数百MB的依赖、漫长的编译时间和复杂的学习曲线,选择回归本质:用最精简的代码、最清晰的架构和最高的效率,解决桌面应用开发的核心需求。
|
||||
|
||||
它是一个**纯粹的教学级、工具级框架**,旨在让开发者深入理解GUI原理,并快速构建轻量级Windows工具。
|
||||
|
||||
---
|
||||
|
||||
## 📦 项目结构与设计哲学
|
||||
|
||||
星垣框架采用经典的**面向对象**和**模块化**设计,项目结构清晰规范:
|
||||
|
||||
```
|
||||
StellarX/
|
||||
├── include/ # 头文件目录
|
||||
│ └── StellarX/ # 框架头文件
|
||||
│ ├── StellarX.h # 主包含头文件 - 一键引入整个框架
|
||||
│ ├── CoreTypes.h # ★ 核心 ★ - 所有枚举、结构体的唯一定义源
|
||||
│ ├── Control.h # 抽象基类 - 定义所有控件的统一接口
|
||||
│ ├── Button.h # 按钮控件
|
||||
│ ├── Window.h # 窗口管理
|
||||
│ ├── Label.h # 标签控件
|
||||
│ ├── TextBox.h # 文本框控件
|
||||
│ ├── Canvas.h # 画布容器
|
||||
│ └── Table.h # 表格控件
|
||||
├── src/ # 源文件目录
|
||||
│ ├── Control.cpp
|
||||
│ ├── Button.cpp
|
||||
│ ├── Window.cpp
|
||||
│ ├── Label.cpp
|
||||
│ ├── TextBox.cpp
|
||||
│ ├── Canvas.cpp
|
||||
│ └── Table.cpp
|
||||
├── examples/ # 示例代码目录
|
||||
│ └── demo.cpp # 基础演示
|
||||
├── docs/ # 文档目录
|
||||
│ └── CODE_OF_CONDUCT.md # 行为准则
|
||||
├── CMakeLists.txt # CMake 构建配置
|
||||
├── CONTRIBUTING.md # 贡献指南
|
||||
├── CHANGELOG.md # 更新日志
|
||||
├── Doxyfile # Doxygen 配置
|
||||
├── LICENSE # MIT 许可证
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
**设计理念:**
|
||||
1. **单一职责原则 (SRP)**: 每个类/文件只负责一件事。
|
||||
2. **依赖倒置原则 (DIP)**: 高层模块(如`Window`)不依赖低层模块(如`Button`),二者都依赖其抽象(`Control`)。
|
||||
3. **开闭原则 (OCP)**: 通过继承`Control`基类,可以轻松扩展新的控件,而无需修改现有代码。
|
||||
4. **一致性**: 所有控件共享统一的`draw()`和`handleEvent()`接口。
|
||||
|
||||
## 🚀 核心特性
|
||||
|
||||
- **极致的轻量级**: 核心库编译后仅 ~1.2MB,无任何外部依赖。生成的应用程序小巧玲珑。
|
||||
- **清晰的模块化架构**: 使用`CoreTypes.h`统一管理所有类型,消除重复定义,极大提升可维护性。
|
||||
- **原生C++性能**: 直接基于EasyX和Win32 API,提供接近原生的执行效率,内存占用极低(通常<10MB)。
|
||||
- **丰富的控件库**: 提供按钮、标签、文本框、表格、画布等常用控件,满足基本桌面应用需求。
|
||||
- **高度可定制化**: 从控件颜色、形状(矩形、圆角、圆形、椭圆)到填充模式、字体样式,均有详尽枚举支持,可轻松定制。
|
||||
- **简洁直观的API**: 采用经典的面向对象设计,代码即文档,学习成本极低。
|
||||
- **标准项目结构**: 采用标准的include/src分离结构,支持CMake构建,易于集成和使用。
|
||||
|
||||
## ⚡ 快速开始(5分钟上手)
|
||||
|
||||
> **🎯 最新版本下载**
|
||||
> 从 [GitHub Releases](https://github.com/Ysm-04/StellarX/releases/latest) 下载预编译的库文件和头文件,即可快速集成到你的项目中。
|
||||
|
||||
### 环境要求
|
||||
|
||||
- **操作系统**: Windows 10 或更高版本
|
||||
- **编译器**: 支持C++17的编译器 (如: **Visual Studio 2019+**)
|
||||
- **图形库**: [EasyX](https://easyx.cn/) (2022版本或更高,安装时请选择与您编译器匹配的版本)
|
||||
- **构建工具**: CMake 3.12+ (可选,推荐使用)
|
||||
|
||||
### 安装 EasyX
|
||||
1. 访问 [EasyX 官网](https://easyx.cn/) 下载最新版本
|
||||
2. 运行安装程序,选择与您的 Visual Studio 版本匹配的版本
|
||||
3. 安装完成后,无需额外配置,星垣框架会自动链接 EasyX
|
||||
|
||||
### 方法一:使用CMake构建(推荐)
|
||||
|
||||
1. **克隆项目**:
|
||||
```bash
|
||||
git clone https://github.com/Ysm-04/StellarX.git
|
||||
cd StellarX
|
||||
```
|
||||
|
||||
2. **生成构建系统**:
|
||||
```bash
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
```
|
||||
|
||||
3. **编译项目**:
|
||||
```bash
|
||||
cmake --build .
|
||||
```
|
||||
|
||||
4. **运行示例**:
|
||||
```bash
|
||||
./examples/Demo
|
||||
```
|
||||
|
||||
### 方法二:手动集成到现有项目
|
||||
|
||||
1. **将include和src目录复制**到您的项目中
|
||||
2. **配置包含路径**,确保编译器可以找到`include/StellarX/`目录
|
||||
3. **将所有.cpp文件**添加到您的项目中编译
|
||||
|
||||
### 创建你的第一个星垣应用
|
||||
|
||||
```cpp
|
||||
// 只需包含这一个头文件即可使用所有功能
|
||||
#include "StellarX.h"
|
||||
|
||||
// 程序入口点(请使用WinMain以获得更好的兼容性)
|
||||
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd) {
|
||||
|
||||
// 1. 创建一个640x480的窗口,背景为白色,标题为"我的应用"
|
||||
Window mainWindow(640, 480, 0, RGB(255, 255, 255), "我的第一个星垣应用");
|
||||
|
||||
// 2. 创建一个按钮 (使用智能指针管理)
|
||||
auto myButton = std::make_unique<Button>(
|
||||
250, 200, 140, 40, // x, y, 宽度, 高度
|
||||
"点击我", // 按钮文本
|
||||
StellarX::ButtonMode::NORMAL,
|
||||
StellarX::ControlShape::ROUND_RECTANGLE
|
||||
);
|
||||
|
||||
// 3. 为按钮设置点击事件(使用Lambda表达式)
|
||||
myButton->setOnClickListener([]() {
|
||||
MessageBox(nullptr, "Hello, 星垣!", "问候", MB_OK | MB_ICONINFORMATION);
|
||||
});
|
||||
|
||||
// 4. (可选)设置按钮样式
|
||||
myButton->textStyle.nHeight = 20;
|
||||
myButton->textStyle.color = RGB(0, 0, 128); // 深蓝色文字
|
||||
myButton->setButtonBorder(RGB(0, 128, 255)); // 蓝色边框
|
||||
|
||||
// 5. 将按钮添加到窗口
|
||||
mainWindow.addControl(std::move(myButton));
|
||||
|
||||
// 6. 绘制窗口
|
||||
mainWindow.draw();
|
||||
|
||||
// 7. 进入消息循环,等待用户交互
|
||||
mainWindow.runEventLoop();
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
5. **编译并运行!** 您将看到一个带有蓝色圆角按钮的窗口,点击它将会弹出消息框。
|
||||
|
||||
## 📚 核心类型详解 (`CoreTypes.h`)
|
||||
|
||||
星垣框架的所有视觉和行为属性都通过`CoreTypes.h`中定义的精美枚举和结构体来控制。
|
||||
|
||||
### 枚举类型 (Enums)
|
||||
|
||||
| 枚举类型 | 描述 | 常用值 |
|
||||
| :----------------- | :----------- | :----------------------------------------------------------- |
|
||||
| **`ControlShape`** | 控件几何形状 | `RECTANGLE`, `B_RECTANGLE`, `ROUND_RECTANGLE`, `CIRCLE`, `ELLIPSE`等 |
|
||||
| **`ButtonMode`** | 按钮行为模式 | `NORMAL`(普通), `TOGGLE`(切换), `DISABLED`(禁用) |
|
||||
| **`TextBoxMode`** | 文本框模式 | `INPUT_MODE`(输入), `READONLY_MODE`(只读) |
|
||||
| **`FillMode`** | 图形填充模式 | `SOLID`(实心), `NULL`(空心), `HATCHED`(图案)等 |
|
||||
| **`FillStyle`** | 图案填充样式 | `HORIZONTAL`(水平线), `CROSS`(十字线)等 |
|
||||
| **`LineStyle`** | 边框线型 | `SOLID`(实线), `DASH`(虚线), `DOT`(点线)等 |
|
||||
|
||||
### 结构体 (Structs)
|
||||
|
||||
| 结构体 | 描述 |
|
||||
| :---------------- | :----------------------------------------------------------- |
|
||||
| **`ControlText`** | 封装了所有文本样式属性,包括字体、大小、颜色、粗体、斜体、下划线、删除线等。 |
|
||||
|
||||
**使用示例:**
|
||||
```cpp
|
||||
// 创建一个复杂的文本样式
|
||||
StellarX::ControlText myStyle;
|
||||
myStyle.nHeight = 25; // 字体高度
|
||||
myStyle.lpszFace = _T("微软雅黑"); // 字体
|
||||
myStyle.color = RGB(255, 0, 0); // 红色
|
||||
myStyle.nWeight = FW_BOLD; // 粗体
|
||||
myStyle.bUnderline = true; // 下划线
|
||||
|
||||
// 应用于控件
|
||||
myLabel->textStyle = myStyle;
|
||||
myButton->textStyle = myStyle;
|
||||
```
|
||||
|
||||
## 🧩 控件库大全
|
||||
|
||||
### 1. 基础控件
|
||||
|
||||
| 控件 | 头文件 | 描述 | 关键特性 |
|
||||
| :---------- | :---------- | :------------ | :------------------------------------------------------ |
|
||||
| **Button** | `Button.h` | 多功能按钮 | 支持多种模式/形状/状态,可设置悬停/点击颜色,自定义回调 |
|
||||
| **Label** | `Label.h` | 文本标签 | 支持背景透明/不透明,自定义字体样式 |
|
||||
| **TextBox** | `TextBox.h` | 输入框/显示框 | 支持输入和只读模式,集成EasyX的`InputBox` |
|
||||
|
||||
### 2. 容器控件
|
||||
|
||||
| 控件 | 头文件 | 描述 |
|
||||
| :--------- | :--------- | :------------------------------------------------------- |
|
||||
| **Canvas** | `Canvas.h` | 容器控件,可作为其他控件的父容器,支持自定义边框和背景。 |
|
||||
| **Window** | `Window.h` | 顶级窗口,所有控件的最终容器,负责消息循环和调度。 |
|
||||
|
||||
### 3. 高级控件
|
||||
|
||||
| 控件 | 头文件 | 描述 | 关键特性 |
|
||||
| :-------- | :-------- | :------- | :----------------------------------------------------------- |
|
||||
| **Table** | `Table.h` | 数据表格 | **框架功能亮点**,支持分页显示、自定义表头和数据、自动计算列宽、翻页按钮。 |
|
||||
|
||||
**表格控件示例:**
|
||||
```cpp
|
||||
// 创建一个表格
|
||||
auto myTable = std::make_unique<StellarX::Table>(50, 50);
|
||||
|
||||
// 设置表头
|
||||
myTable->setHeaders({ "ID", "姓名", "年龄", "职业" });
|
||||
|
||||
// 添加数据行
|
||||
myTable->addDataRow({ "1", "张三", "25", "工程师" });
|
||||
myTable->addDataRow({ "2", "李四", "30", "设计师" });
|
||||
myTable->addDataRow({ "3", "王五", "28", "产品经理" });
|
||||
|
||||
// 设置每页显示2行
|
||||
myTable->setRowsPerPage(2);
|
||||
|
||||
// 设置表格样式
|
||||
myTable->textStyle.nHeight = 16;
|
||||
myTable->setTableBorder(RGB(50, 50, 50));
|
||||
myTable->setTableBackground(RGB(240, 240, 240));
|
||||
|
||||
// 添加到窗口
|
||||
mainWindow.addControl(std::move(myTable));
|
||||
```
|
||||
|
||||
## 🔧 高级主题与最佳实践
|
||||
|
||||
### 1. 自定义控件
|
||||
您可以通过继承`Control`基类来创建自定义控件。只需实现`draw()`和`handleEvent()`两个纯虚函数即可。
|
||||
|
||||
```cpp
|
||||
class MyCustomControl : public StellarX::Control {
|
||||
public:
|
||||
MyCustomControl(int x, int y) : Control(x, y, 100, 100) {}
|
||||
void draw() override {
|
||||
// 您的自定义绘制逻辑
|
||||
setfillcolor(RGB(255, 100, 100));
|
||||
fillrectangle(x, y, x + width, y + height);
|
||||
}
|
||||
void handleEvent(const ExMessage& msg) override {
|
||||
// 您的自定义事件处理逻辑
|
||||
if (msg.message == WM_LBUTTONDOWN && isInControl(msg.x, msg.y)) {
|
||||
// 处理点击
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 布局管理
|
||||
当前版本星垣主要采用**绝对定位**。对于简单布局,您可以通过计算坐标来实现。对于复杂布局,可以考虑:
|
||||
- 在`Canvas`中嵌套控件,实现相对定位。
|
||||
- 自行实现简单的流式布局或网格布局管理器。
|
||||
|
||||
### 3. 性能优化
|
||||
- **脏矩形渲染**: 框架内部已实现,控件状态改变时`dirty=true`,仅在需要时重绘。
|
||||
- **图像资源**: 使用`IMAGE`对象加载图片后,可重复使用,避免多次加载。
|
||||
- **减少循环内操作**: 在`draw()`和`handleEvent()`中避免进行重型计算。
|
||||
|
||||
## ⚠️ 重要限制与适用场景
|
||||
|
||||
**星垣框架的设计目标是轻便、清晰和教学价值,因此它明确** **不适用于** **以下场景:**
|
||||
|
||||
- **高性能游戏或复杂动画**: 渲染基于EasyX的CPU软件渲染,性能有限。
|
||||
- **需要高DPI缩放的应用**: 对高DPI显示器的支持有限,界面可能显示不正确。
|
||||
- **需要无障碍功能的应用**: 未提供对屏幕阅读器等辅助技术的支持。
|
||||
- **跨平台应用**: 深度依赖Windows API和EasyX,无法直接在Linux/macOS上运行。
|
||||
- **复杂的商业软件前端**: 缺乏高级控件(如树形图、富文本框、选项卡、高级列表等)和成熟的自动布局管理器。
|
||||
|
||||
**如果您需要开发上述类型的应用,请考虑使用以下成熟方案:**
|
||||
- **Qt**: 功能极其强大,跨平台,适合大型商业应用。
|
||||
- **wxWidgets**: 原生外观,跨平台。
|
||||
- **Dear ImGui**: 即时模式GUI,非常适合工具和调试界面。
|
||||
- **Web技术栈 (Electron/CEF)**: 适合需要Web技术的场景。
|
||||
|
||||
## 📜 许可证 (License)
|
||||
|
||||
本项目采用 **MIT 许可证**。
|
||||
|
||||
您可以自由地:
|
||||
- 使用、复制、修改、合并、出版发行、散布、再授权及销售本框架的副本。
|
||||
- 将其用于私人或商业项目。
|
||||
|
||||
唯一要求是:
|
||||
- 请在您的项目中保留原始的版权声明。
|
||||
|
||||
详见项目根目录的 [LICENSE](LICENSE) 文件。
|
||||
|
||||
## 👥 贡献指南 (Contributing)
|
||||
|
||||
我们欢迎任何形式的贡献!如果您想为星垣框架添砖加瓦,请阅读以下指南:
|
||||
|
||||
1. **代码风格**: 请遵循现有的Google C++规范风格(使用空格缩进,大括号换行等)。
|
||||
2. **新增功能**: 必须提供**示例代码**,并更新本README文档的相关部分。
|
||||
3. **提交PR**: 请确保您的代码在提交前已经过测试,并描述清楚您的更改内容和动机。
|
||||
4. **问题反馈**: 如果您发现了Bug或有新的想法,欢迎在GitHub提交Issue。
|
||||
|
||||
详细贡献指南请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
||||
|
||||
## 🙏 致谢 (Acknowledgements)
|
||||
|
||||
- 感谢 [EasyX Graphics Library](https://easyx.cn/) 为这个项目提供了简单易用的图形基础,使得用C++教学GUI编程成为可能。
|
||||
- 感谢所有追求**简洁、高效、清晰**编码理念的开发者,你们是"星垣"诞生的灵感源泉。
|
||||
|
||||
---
|
||||
|
||||
**星辰大海,代码为舟。**
|
||||
|
||||
愿 `星垣 (StellarX)` 能成为您探索GUI世界的一颗可靠基石,无论是用于学习、教学还是创造实用的工具。
|
||||
|
||||
## 📞 支持与反馈
|
||||
|
||||
如果您在使用过程中遇到问题或有任何建议:
|
||||
1. 查看 [示例代码](examples/) 获取使用参考
|
||||
2. 查阅 [更新日志](CHANGELOG.md) 了解最新变化
|
||||
3. 在GitHub仓库提交Issue反馈问题
|
||||
|
||||
---
|
||||
*星垣框架 - 轻若尘埃,繁星为界*
|
||||
@@ -1,443 +0,0 @@
|
||||
#include "SxLog.h"
|
||||
#include <cstdlib>
|
||||
#include <clocale>
|
||||
|
||||
/********************************************************************************
|
||||
* @文件: SxLog.cpp
|
||||
* @摘要: StellarX 日志系统实现(过滤/格式化/输出/文件滚动/RAII提交/作用域计时)
|
||||
* @描述:
|
||||
* 该实现文件主要包含 4 个关键点:
|
||||
* 1) FileSink: 文件打开、写入、flush 与按阈值滚动
|
||||
* 2) SxLogger: shouldLog 过滤、formatPrefix 前缀拼接、logLine 统一输出出口
|
||||
* 3) SxLogLine: 析构提交(RAII)确保“一条语句输出一整行”
|
||||
* 4) SxLogScope: 按需启用计时,析构输出耗时
|
||||
*
|
||||
* @实现难点提示:
|
||||
* - shouldLog 必须“零副作用”,否则宏短路会带来不可预测行为
|
||||
* - logLine 是统一出口,必须保证行级一致性,且避免在持锁状态下递归打日志
|
||||
* - 文件滚动要处理文件名安全性与跨平台 rename 行为差异
|
||||
* - 时间戳生成需要兼容 Windows 与 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 调用造成噪声与性能浪费
|
||||
//
|
||||
void SxLogger::setGBK()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
static std::once_flag once;
|
||||
std::call_once(once, []() {
|
||||
// 切到chcp 936(GBK),避免中文日志在 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,416 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
/********************************************************************************
|
||||
* @文件: SxLog.h
|
||||
* @摘要: StellarX 日志系统对外接口定义(控制台/文件输出 + 级别过滤 + Tag过滤 + 中英文选择)
|
||||
* @描述:
|
||||
* 该日志系统采用“宏 + RAII(析构提交)”的方式实现:
|
||||
* - 调用端通过 SX_LOGD/SX_LOGI... 写日志
|
||||
* - 宏内部先 shouldLog 短路过滤,未命中时不构造对象、不拼接字符串
|
||||
* - 命中时构造 SxLogLine,使用 operator<< 拼接内容
|
||||
* - 语句结束时 SxLogLine 析构,统一提交到 SxLogger::logLine 输出
|
||||
*
|
||||
* 输出通道(Sink)目前提供:
|
||||
* - ConsoleSink: 写入 std::cout(不走 WinAPI 调试输出通道)
|
||||
* - FileSink: 写入文件,支持按字节阈值滚动
|
||||
*
|
||||
* @特性:
|
||||
* - 日志级别:Trace/Debug/Info/Warn/Error/Fatal/Off
|
||||
* - Tag 过滤:None/Whitelist/Blacklist
|
||||
* - 可选前缀:时间戳/级别/Tag/线程ID/源码位置
|
||||
* - 中英文选择:SX_T(zh, en) / setLanguage
|
||||
* - 文件滚动:rotateBytes > 0 时按阈值滚动
|
||||
*
|
||||
* @使用场景:
|
||||
* - 排查重绘链路、脏标记传播、Tab 切换、Table 数据刷新等时序问题
|
||||
* - 输出可复现日志,配合回归验证
|
||||
*
|
||||
* @注意:
|
||||
* - SX_T 仅做“字符串选择”,不做编码转换
|
||||
* - 控制台显示是否乱码由“终端 codepage/字体/环境”决定
|
||||
* - 该头文件只声明接口,实现位于 SxLog.cpp
|
||||
*
|
||||
* @所属框架: 星垣(StellarX) GUI框架
|
||||
* @作者: 我在人间做废物
|
||||
********************************************************************************/
|
||||
|
||||
// SxLog.h - header-only interface (implementation in SxLog.cpp)
|
||||
// Pure standard library: std::cout and optional file sink.
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#ifndef SX_LOG_ENABLE
|
||||
#define SX_LOG_ENABLE 1
|
||||
#endif
|
||||
|
||||
namespace StellarX
|
||||
{
|
||||
/* ========================= 日志级别 ========================= */
|
||||
// 说明:
|
||||
// - minLevel 表示最低输出级别,小于 minLevel 的日志会被 shouldLog 直接过滤
|
||||
// - Off 表示全局关闭
|
||||
enum class SxLogLevel : int
|
||||
{
|
||||
Trace = 0, // 最细粒度:高频路径追踪(谨慎开启)
|
||||
Debug = 1, // 调试信息:状态变化/关键分支
|
||||
Info = 2, // 业务信息:关键流程节点
|
||||
Warn = 3, // 警告:非致命但异常的情况
|
||||
Error = 4, // 错误:功能失败、需要关注
|
||||
Fatal = 5, // 致命:通常意味着无法继续运行
|
||||
Off = 6 // 关闭全部日志
|
||||
};
|
||||
|
||||
/* ========================= 语言选择 ========================= */
|
||||
// 说明:仅用于 SX_T 选择输出哪一段文本,不做编码转换
|
||||
enum class SxLogLanguage : int
|
||||
{
|
||||
ZhCN = 0, // 中文
|
||||
EnUS = 1 // 英文
|
||||
};
|
||||
|
||||
/* ========================= Tag 过滤模式 ========================= */
|
||||
// None : 不过滤,全部输出
|
||||
// Whitelist : 只输出 tagList 中包含的 tag
|
||||
// Blacklist : 输出除 tagList 以外的 tag
|
||||
enum class SxTagFilterMode : int
|
||||
{
|
||||
None = 0,
|
||||
Whitelist = 1,
|
||||
Blacklist = 2
|
||||
};
|
||||
|
||||
/* ========================= 日志配置 ========================= */
|
||||
// 说明:SxLogger 内部持有该配置,shouldLog 与 logLine 都依赖它
|
||||
struct SxLogConfig
|
||||
{
|
||||
SxLogLevel minLevel = SxLogLevel::Info; // 最低输出级别
|
||||
|
||||
bool showTimestamp = true; // 是否输出时间戳前缀
|
||||
bool showLevel = true; // 是否输出级别前缀
|
||||
bool showTag = true; // 是否输出 tag 前缀
|
||||
bool showThreadId = false; // 是否输出线程ID(排查并发时开启)
|
||||
bool showSource = false; // 是否输出源码位置(file:line func)
|
||||
bool autoFlush = true; // 每行写完是否 flush(排查问题更稳,性能略差)
|
||||
|
||||
SxTagFilterMode tagFilterMode = SxTagFilterMode::None; // Tag 过滤模式
|
||||
std::vector<std::string> tagList; // Tag 列表(白名单/黑名单)
|
||||
|
||||
bool fileEnabled = false; // 文件输出是否启用(enableFile 成功才为 true)
|
||||
std::string filePath; // 文件路径
|
||||
bool fileAppend = true; // 是否追加写入
|
||||
std::size_t rotateBytes = 0; // 滚动阈值(0 表示不滚动)
|
||||
};
|
||||
|
||||
/* ========================= Sink 接口 ========================= */
|
||||
// 说明:
|
||||
// - Sink 负责“把完整的一行日志写到某个地方”
|
||||
// - SxLogger 负责过滤/格式化/分发
|
||||
class ILogSink
|
||||
{
|
||||
public:
|
||||
virtual ~ILogSink() = default;
|
||||
|
||||
// 返回 Sink 名称,用于调试识别(例如 "console"/"file")
|
||||
virtual const char* name() const = 0;
|
||||
|
||||
// 写入一整行(调用方保证 line 已包含换行或按约定追加换行)
|
||||
virtual void writeLine(const std::string& line) = 0;
|
||||
|
||||
// 刷新缓冲(可选实现)
|
||||
virtual void flush() {}
|
||||
};
|
||||
|
||||
/* ========================= 控制台输出 Sink ========================= */
|
||||
// 作用:把日志写入指定输出流(默认用 std::cout)
|
||||
class ConsoleSink : public ILogSink
|
||||
{
|
||||
public:
|
||||
// 绑定一个输出流引用(常见用法:std::cout)
|
||||
explicit ConsoleSink(std::ostream& os) : out(os) {}
|
||||
|
||||
const char* name() const override { return "console"; }
|
||||
|
||||
// 写入一行(不自动追加换行,换行由上层统一拼接)
|
||||
void writeLine(const std::string& line) override { out << line; }
|
||||
|
||||
// 立即 flush(当 autoFlush=true 时由 SxLogger 调用)
|
||||
void flush() override { out.flush(); }
|
||||
|
||||
private:
|
||||
std::ostream& out; // 输出流引用(不负责生命周期)
|
||||
};
|
||||
|
||||
/* ========================= 文件输出 Sink ========================= */
|
||||
// 作用:把日志写入文件,支持按字节阈值滚动
|
||||
class FileSink : public ILogSink
|
||||
{
|
||||
public:
|
||||
FileSink() = default;
|
||||
|
||||
const char* name() const override { return "file"; }
|
||||
|
||||
// 打开文件
|
||||
// path : 文件路径
|
||||
// append : true 追加写;false 清空重写
|
||||
bool open(const std::string& path, bool append);
|
||||
|
||||
// 关闭文件(安全可重复调用)
|
||||
void close();
|
||||
|
||||
// 查询文件是否处于打开状态
|
||||
bool isOpen() const;
|
||||
|
||||
// 设置滚动阈值(字节)
|
||||
// bytes = 0 表示不滚动
|
||||
void setRotateBytes(std::size_t bytes) { rotateBytes = bytes; }
|
||||
|
||||
// 写入一行,并在需要时触发滚动
|
||||
void writeLine(const std::string& line) override;
|
||||
|
||||
// flush 文件缓冲
|
||||
void flush() override;
|
||||
|
||||
private:
|
||||
// 检查并执行滚动
|
||||
// 返回值:是否发生滚动(或是否重新打开)
|
||||
bool rotateIfNeeded();
|
||||
|
||||
std::ofstream ofs; // 文件输出流
|
||||
std::string filePath; // 当前文件路径
|
||||
bool appendMode = true; // 是否追加模式(用于 reopen)
|
||||
std::size_t rotateBytes = 0; // 滚动阈值
|
||||
};
|
||||
|
||||
/* ========================= 日志中心 SxLogger ========================= */
|
||||
// 作用:
|
||||
// - 保存配置(SxLogConfig)
|
||||
// - 过滤(level/tag/sink enabled)
|
||||
// - 格式化前缀(时间/级别/tag/线程/源码位置)
|
||||
// - 分发到 console/file 等 sink
|
||||
class SxLogger
|
||||
{
|
||||
public:
|
||||
// 仅用于 Windows 控制台:把 codepage 切到 GBK,解决中文乱码。
|
||||
// 不使用 WinAPI:内部通过 system("chcp 936") 实现
|
||||
// 注意:这只影响终端解释输出字节的方式,不影响源码文件编码
|
||||
static void setGBK();
|
||||
|
||||
// 获取全局单例
|
||||
// 说明:函数内静态对象,C++11 起保证线程安全初始化
|
||||
static SxLogger& Get();
|
||||
|
||||
// 设置最低输出级别
|
||||
void setMinLevel(SxLogLevel level);
|
||||
|
||||
// 获取最低输出级别
|
||||
SxLogLevel getMinLevel() const;
|
||||
|
||||
// 设置语言(用于 SX_T 选择)
|
||||
void setLanguage(SxLogLanguage lang);
|
||||
|
||||
// 获取当前语言
|
||||
SxLogLanguage getLanguage() const;
|
||||
|
||||
// 设置 Tag 过滤
|
||||
// mode: None/Whitelist/Blacklist
|
||||
// tags: 过滤列表(精确匹配)
|
||||
void setTagFilter(SxTagFilterMode mode, const std::vector<std::string>& tags);
|
||||
|
||||
// 清空 Tag 过滤(恢复 None)
|
||||
void clearTagFilter();
|
||||
|
||||
// 开关控制台输出
|
||||
void enableConsole(bool enable);
|
||||
|
||||
// 开启文件输出
|
||||
// path : 文件路径
|
||||
// append : 追加写/清空写
|
||||
// rotateBytes: 滚动阈值(0 不滚动)
|
||||
// 返回值:是否打开成功
|
||||
bool enableFile(const std::string& path, bool append = true, std::size_t rotateBytes = 0);
|
||||
|
||||
// 关闭文件输出(不影响控制台输出)
|
||||
void disableFile();
|
||||
|
||||
// 快速判定是否需要输出(宏层面的短路依赖它)
|
||||
// 说明:
|
||||
// - shouldLog 一定要“副作用为 0”
|
||||
// - 若返回 false,调用端不会创建 SxLogLine,也不会拼接字符串
|
||||
bool shouldLog(SxLogLevel level, const char* tag) const;
|
||||
|
||||
// 输出一条完整日志
|
||||
// 说明:这是统一出口,SxLogLine 析构最终会走到这里
|
||||
void logLine(
|
||||
SxLogLevel level,
|
||||
const char* tag,
|
||||
const char* file,
|
||||
int line,
|
||||
const char* func,
|
||||
const std::string& msg);
|
||||
|
||||
// 获取配置副本(避免外部直接改内部 cfg)
|
||||
SxLogConfig getConfigCopy() const;
|
||||
|
||||
// 批量设置配置(整体替换)
|
||||
void setConfig(const SxLogConfig& cfg);
|
||||
|
||||
// 工具:把级别转为字符串(用于前缀)
|
||||
static const char* levelToString(SxLogLevel level);
|
||||
|
||||
// 工具:生成本地时间戳字符串(用于前缀与文件滚动名)
|
||||
static std::string makeTimestampLocal();
|
||||
|
||||
private:
|
||||
SxLogger();
|
||||
|
||||
// 判断 tag 是否允许输出(根据 Tag 过滤模式与 tagList)
|
||||
static bool tagAllowed(const SxLogConfig& cfg, const char* tag);
|
||||
|
||||
// 生成前缀(调用方需已持有锁)
|
||||
std::string formatPrefixUnlocked(
|
||||
const SxLogConfig& cfg,
|
||||
SxLogLevel level,
|
||||
const char* tag,
|
||||
const char* file,
|
||||
int line,
|
||||
const char* func) const;
|
||||
|
||||
mutable std::mutex mtx; // 保护 cfg 与 sink 写入,确保多线程行级一致性
|
||||
SxLogConfig cfg; // 当前配置
|
||||
std::atomic<SxLogLanguage> lang; // 语言开关(仅影响 SX_T 选择)
|
||||
|
||||
std::unique_ptr<ConsoleSink> consoleSink; // 控制台 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
|
||||
-676
@@ -1,676 +0,0 @@
|
||||
#include "TabControl.h"
|
||||
#include "SxLog.h"
|
||||
#include "Window.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
enum class SxTabOverlayRedrawMode
|
||||
{
|
||||
None,
|
||||
RefreshSnapshot
|
||||
};
|
||||
|
||||
bool SxTabRectsIntersect(const RECT& a, const RECT& b)
|
||||
{
|
||||
return a.left < b.right && a.right > b.left &&
|
||||
a.top < b.bottom && a.bottom > b.top;
|
||||
}
|
||||
|
||||
RECT SxTabUnionRect(const RECT& a, const RECT& b)
|
||||
{
|
||||
RECT out{};
|
||||
out.left = (std::min)(a.left, b.left);
|
||||
out.top = (std::min)(a.top, b.top);
|
||||
out.right = (std::max)(a.right, b.right);
|
||||
out.bottom = (std::max)(a.bottom, b.bottom);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
inline void TabControl::initTabBar()
|
||||
{
|
||||
if (controls.empty())return;
|
||||
int butW = max(this->width / (int)controls.size(), BUTMINWIDTH);
|
||||
int butH = max(this->height / (int)controls.size(), BUTMINHEIGHT);
|
||||
|
||||
if (this->tabPlacement == StellarX::TabPlacement::Top || this->tabPlacement == StellarX::TabPlacement::Bottom)
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.first->setHeight(tabBarHeight);
|
||||
c.first->setWidth(butW);
|
||||
}
|
||||
else if (this->tabPlacement == StellarX::TabPlacement::Left || this->tabPlacement == StellarX::TabPlacement::Right)
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.first->setHeight(butH);
|
||||
c.first->setWidth(tabBarHeight);
|
||||
}
|
||||
int i = 0;
|
||||
switch (this->tabPlacement)
|
||||
{
|
||||
case StellarX::TabPlacement::Top:
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.first->setX(this->x + i * butW);
|
||||
c.first->setY(this->y);
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
case StellarX::TabPlacement::Bottom:
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.first->setX(this->x + i * butW);
|
||||
c.first->setY(this->y + this->height - tabBarHeight);
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
case StellarX::TabPlacement::Left:
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.first->setX(this->x);
|
||||
c.first->setY(this->y + i * butH);
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
case StellarX::TabPlacement::Right:
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.first->setX(this->x + this->width - tabBarHeight);
|
||||
c.first->setY(this->y + i * butH);
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
inline void TabControl::initTabPage()
|
||||
{
|
||||
if (controls.empty())return;
|
||||
// TabControl 内部页签页仍然保留专用布局:
|
||||
// 这里负责把“当前选项卡容器矩形”拆分成页签栏和页面区,
|
||||
// 不把这部分细节下放到通用布局解算器里。
|
||||
// 但页面内容子树的世界坐标映射职责,继续归页面 Canvas 自己管理。
|
||||
// 因此这里不再手工遍历页内子控件做 setX/setY,
|
||||
// 只安置每个页面 Canvas 的矩形,让 Canvas 通过自身 relayoutManagedChildren() 收口。
|
||||
switch (this->tabPlacement)
|
||||
{
|
||||
case StellarX::TabPlacement::Top:
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.second->setX(this->x);
|
||||
c.second->setY(this->y + tabBarHeight);
|
||||
c.second->setWidth(this->width);
|
||||
c.second->setHeight(this->height - tabBarHeight);
|
||||
}
|
||||
break;
|
||||
case StellarX::TabPlacement::Bottom:
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.second->setX(this->x);
|
||||
c.second->setY(this->y);
|
||||
c.second->setWidth(this->width);
|
||||
c.second->setHeight(this->height - tabBarHeight);
|
||||
}
|
||||
break;
|
||||
case StellarX::TabPlacement::Left:
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.second->setX(this->x + tabBarHeight);
|
||||
c.second->setY(this->y);
|
||||
c.second->setWidth(this->width - tabBarHeight);
|
||||
c.second->setHeight(this->height);
|
||||
}
|
||||
break;
|
||||
case StellarX::TabPlacement::Right:
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.second->setX(this->x);
|
||||
c.second->setY(this->y);
|
||||
c.second->setWidth(this->width - tabBarHeight);
|
||||
c.second->setHeight(this->height);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TabControl::refreshRuntimeLayout()
|
||||
{
|
||||
// 这是 TabControl 的内部专用布局入口:
|
||||
// 外层先通过统一解算得到 TabControl 自身矩形,
|
||||
// 再由这里继续安置页签按钮和页面区。
|
||||
initTabBar();
|
||||
initTabPage();
|
||||
}
|
||||
|
||||
TabControl::TabControl() :Canvas()
|
||||
{
|
||||
this->id = "TabControl";
|
||||
// TabControl 作为外层容器,当前阶段显式允许双轴 Stretch;
|
||||
// 内部页签栏和页面区仍由自己的专用布局逻辑管理。
|
||||
this->layoutCapability.allowStretchX = true;
|
||||
this->layoutCapability.allowStretchY = true;
|
||||
}
|
||||
|
||||
TabControl::TabControl(int x, int y, int width, int height)
|
||||
: Canvas(x, y, width, height)
|
||||
{
|
||||
this->id = "TabControl";
|
||||
this->layoutCapability.allowStretchX = true;
|
||||
this->layoutCapability.allowStretchY = true;
|
||||
}
|
||||
|
||||
TabControl::~TabControl()
|
||||
{
|
||||
}
|
||||
|
||||
void TabControl::setX(int x)
|
||||
{
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
refreshRuntimeLayout();
|
||||
onWindowResize();
|
||||
}
|
||||
|
||||
void TabControl::setY(int y)
|
||||
{
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
refreshRuntimeLayout();
|
||||
onWindowResize();
|
||||
}
|
||||
|
||||
void TabControl::setWidth(int width)
|
||||
{
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
refreshRuntimeLayout();
|
||||
onWindowResize();
|
||||
}
|
||||
|
||||
void TabControl::setHeight(int height)
|
||||
{
|
||||
applyRuntimeRectDirect(x, y, width, height);
|
||||
refreshRuntimeLayout();
|
||||
onWindowResize();
|
||||
}
|
||||
|
||||
void TabControl::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
|
||||
{
|
||||
// TabControl 作为外层控件接入统一解算;
|
||||
// 但页签栏和页面区仍由自身专用逻辑继续排布。
|
||||
applyRuntimeRectDirect(rect.worldX, rect.worldY, rect.width, rect.height);
|
||||
refreshRuntimeLayout();
|
||||
}
|
||||
|
||||
void TabControl::draw()
|
||||
{
|
||||
if (!dirty || !show)return;
|
||||
// 绘制画布背景和基本形状及其子画布控件
|
||||
Canvas::draw();
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.second->setDirty(true);
|
||||
c.second->draw();
|
||||
}
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.first->setDirty(true);
|
||||
c.first->draw();
|
||||
}
|
||||
|
||||
// 首次绘制时处理默认激活页签
|
||||
if (IsFirstDraw)
|
||||
{
|
||||
if (defaultActivation >= 0 && defaultActivation < (int)controls.size())
|
||||
controls[defaultActivation].first->setButtonClick(true);
|
||||
else if (defaultActivation >= (int)controls.size())//索引越界则激活最后一个
|
||||
controls[controls.size() - 1].first->setButtonClick(true);
|
||||
IsFirstDraw = false;//避免重复处理
|
||||
}
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
bool TabControl::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
if (!show)return false;
|
||||
resetEventVisualChanged();
|
||||
bool consume = false;
|
||||
bool anyVisualChanged = false;
|
||||
|
||||
// TabControl 的同层页签按钮/页面区事件分发顺序,
|
||||
// 与 Window / Canvas 保持一致:按倒序处理,后加入者视为更靠上层。
|
||||
if (msg.message == WM_MOUSEMOVE)
|
||||
{
|
||||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||||
{
|
||||
if (!consume)
|
||||
{
|
||||
if (it->first->handleEvent(msg))
|
||||
{
|
||||
consume = true;
|
||||
}
|
||||
if (it->first->didEventAffectVisual())
|
||||
anyVisualChanged = true;
|
||||
}
|
||||
else if (it->first->clearTransientMouseState())
|
||||
{
|
||||
anyVisualChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||||
{
|
||||
if (!it->second->IsVisible())
|
||||
continue;
|
||||
|
||||
if (!consume)
|
||||
{
|
||||
if (it->second->handleEvent(msg))
|
||||
{
|
||||
consume = true;
|
||||
}
|
||||
if (it->second->didEventAffectVisual())
|
||||
anyVisualChanged = true;
|
||||
}
|
||||
else if (it->second->clearTransientMouseState())
|
||||
{
|
||||
anyVisualChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||||
if (it->first->handleEvent(msg))
|
||||
{
|
||||
consume = true;
|
||||
break;
|
||||
}
|
||||
if (!consume)
|
||||
{
|
||||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||||
if (it->second->IsVisible())
|
||||
if (it->second->handleEvent(msg))
|
||||
{
|
||||
consume = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (dirty)
|
||||
requestRepaint(parent);
|
||||
markEventVisualChanged(anyVisualChanged || dirty);
|
||||
return consume;
|
||||
}
|
||||
|
||||
bool TabControl::clearTransientMouseState()
|
||||
{
|
||||
bool changed = false;
|
||||
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
|
||||
{
|
||||
if (it->first->IsVisible() && it->first->clearTransientMouseState())
|
||||
changed = true;
|
||||
|
||||
if (it->second->IsVisible() && it->second->clearTransientMouseState())
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void TabControl::add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>&& control)
|
||||
{
|
||||
controls.push_back(std::move(control));
|
||||
initTabBar();
|
||||
initTabPage();
|
||||
size_t idx = controls.size() - 1;
|
||||
controls[idx].first->setParent(this);
|
||||
controls[idx].first->enableTooltip(true);
|
||||
controls[idx].first->setbuttonMode(StellarX::ButtonMode::TOGGLE);
|
||||
|
||||
controls[idx].first->setOnToggleOnListener([this, idx]()
|
||||
{
|
||||
int prevIdx = -1;
|
||||
for (size_t i = 0; i < controls.size(); ++i)
|
||||
{
|
||||
if (controls[i].second->IsVisible())
|
||||
{
|
||||
prevIdx = (int)i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (auto& tab : controls)
|
||||
{
|
||||
if (tab.first->getButtonText() != controls[idx].first->getButtonText() && tab.first->isClicked())
|
||||
tab.first->setButtonClick(false);
|
||||
}
|
||||
|
||||
|
||||
SX_LOGI("Tab") << SX_T("激活选项卡:","activate tab: ") << prevIdx << "->" << (int)idx
|
||||
<< " text=" << controls[idx].first->getButtonText();
|
||||
controls[idx].second->onWindowResize();
|
||||
controls[idx].second->setIsVisible(true);
|
||||
dirty = true;
|
||||
|
||||
|
||||
});
|
||||
controls[idx].first->setOnToggleOffListener([this, idx]()
|
||||
{
|
||||
SX_LOGI("Tab") << SX_T("关闭选项卡:id=","deactivate tab: idx=") << (int)idx
|
||||
<< " text=" << controls[idx].first->getButtonText();
|
||||
|
||||
controls[idx].second->setIsVisible(false);
|
||||
dirty = true;
|
||||
});
|
||||
controls[idx].second->setParent(this);
|
||||
controls[idx].second->setLinewidth(canvaslinewidth);
|
||||
controls[idx].second->setIsVisible(false);
|
||||
}
|
||||
|
||||
void TabControl::add(std::string tabText, std::unique_ptr<Control> control)
|
||||
{
|
||||
control->setDirty(true);
|
||||
for (auto& tab : controls)
|
||||
{
|
||||
if (tab.first->getButtonText() == tabText)
|
||||
{
|
||||
control->setParent(tab.second.get());
|
||||
control->setIsVisible(tab.second->IsVisible());
|
||||
tab.second->addControl(std::move(control));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabControl::setTabPlacement(StellarX::TabPlacement placement)
|
||||
{
|
||||
this->tabPlacement = placement;
|
||||
setDirty(true);
|
||||
initTabBar();
|
||||
initTabPage();
|
||||
}
|
||||
|
||||
void TabControl::setTabBarHeight(int height)
|
||||
{
|
||||
tabBarHeight = height;
|
||||
setDirty(true);
|
||||
initTabBar();
|
||||
initTabPage();
|
||||
}
|
||||
|
||||
void TabControl::setIsVisible(bool visible)
|
||||
{
|
||||
// 先让基类 Canvas 处理自己的回贴/丢快照逻辑
|
||||
Canvas::setIsVisible(visible);
|
||||
for (auto& tab : controls)
|
||||
{
|
||||
if(true == visible)
|
||||
{
|
||||
tab.first->setIsVisible(visible);
|
||||
//页也要跟着关/开,否则它们会保留旧的 saveBkImage
|
||||
if (tab.first->isClicked())
|
||||
tab.second->setIsVisible(true);
|
||||
else
|
||||
tab.second->setIsVisible(false);
|
||||
tab.second->setDirty(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
tab.first->setIsVisible(visible);
|
||||
tab.second->setIsVisible(visible);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TabControl::onWindowResize()
|
||||
{
|
||||
// 本轮不再在 onWindowResize 中重做页签布局,
|
||||
// 这里只负责失效快照、标脏,并把 resize 语义向页签按钮和页面传递。
|
||||
Control::onWindowResize();
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.first->onWindowResize();
|
||||
c.second->onWindowResize();
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
int TabControl::getActiveIndex() const
|
||||
{
|
||||
int idx = -1;
|
||||
for (auto& c : controls)
|
||||
{
|
||||
idx++;
|
||||
if (c.first->isClicked())
|
||||
return idx;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void TabControl::setActiveIndex(int idx)
|
||||
{
|
||||
if (IsFirstDraw)
|
||||
defaultActivation = idx;
|
||||
else
|
||||
{
|
||||
// 外部重复激活“已经处于激活状态”的页签时,不应再把整条 onToggleOn 链重跑一遍。
|
||||
// 否则当前可见页面会重复执行 onWindowResize()/setIsVisible(true),
|
||||
// 对页内像 Table 这种会越出页面边界绘制的控件,容易把快照链再次扰乱,留下残影。
|
||||
if (idx >= 0 && idx < controls.size())
|
||||
{
|
||||
if (getActiveIndex() == idx)
|
||||
return;
|
||||
controls[idx].first->setButtonClick(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int TabControl::count() const
|
||||
{
|
||||
return (int)controls.size();
|
||||
}
|
||||
|
||||
int TabControl::indexOf(const std::string& tabText) const
|
||||
{
|
||||
int idx = -1;
|
||||
for (auto& c : controls)
|
||||
{
|
||||
idx++;
|
||||
if (c.first->getButtonText() == tabText)
|
||||
return idx;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void TabControl::setDirty(bool dirty)
|
||||
{
|
||||
this->dirty = dirty;
|
||||
for (auto& c : controls)
|
||||
{
|
||||
c.first->setDirty(dirty);
|
||||
c.second->setDirty(dirty);
|
||||
}
|
||||
}
|
||||
|
||||
void TabControl::requestRepaint(Control* parent)
|
||||
{
|
||||
if (shouldDeferManagedRepaint())
|
||||
{
|
||||
// 托管路径:TabControl 作为“页签栏 + 当前页面”的统一重绘 root 登记到 Window。
|
||||
if (auto* host = getHostWindow())
|
||||
host->requestManagedRepaint(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this == parent)
|
||||
{
|
||||
RECT paintCoverage{};
|
||||
bool hasPaintCoverage = false;
|
||||
RECT persistentCoverage{};
|
||||
bool hasPersistentCoverage = false;
|
||||
auto commitTabUnit = [&](Control* unit, SxTabOverlayRedrawMode overlayMode)
|
||||
{
|
||||
if (!unit || !unit->IsVisible())
|
||||
return;
|
||||
|
||||
const bool directDirty = unit->isDirty();
|
||||
const bool subtreeDirty = unit->hasManagedDirtySubtree();
|
||||
|
||||
if (overlayMode == SxTabOverlayRedrawMode::RefreshSnapshot)
|
||||
{
|
||||
// 下层单元的持久内容已经写过像素,上层页签/页面作为 overlay 补画时,
|
||||
// 必须先丢掉旧快照,重新抓取当前背景后再画,否则会把旧背景再贴回来。
|
||||
unit->invalidateBackgroundSnapshot();
|
||||
unit->setDirty(true);
|
||||
unit->draw();
|
||||
}
|
||||
else if (directDirty)
|
||||
{
|
||||
unit->draw();
|
||||
}
|
||||
else if (subtreeDirty)
|
||||
{
|
||||
unit->commitManagedRepaint();
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const RECT paintRect = unit->getManagedRepaintCoverageRect();
|
||||
if (!hasPaintCoverage)
|
||||
{
|
||||
paintCoverage = paintRect;
|
||||
hasPaintCoverage = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
paintCoverage = SxTabUnionRect(paintCoverage, paintRect);
|
||||
}
|
||||
|
||||
const RECT persistentRect = unit->getManagedRepaintPersistentCoverageRect();
|
||||
if (!hasPersistentCoverage)
|
||||
{
|
||||
persistentCoverage = persistentRect;
|
||||
hasPersistentCoverage = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
persistentCoverage = SxTabUnionRect(persistentCoverage, persistentRect);
|
||||
}
|
||||
};
|
||||
|
||||
// 局部重绘必须和 draw() 维持同一套顺序:
|
||||
// 先页面,再页签按钮。
|
||||
// 否则页签按钮上的 Tooltip 会被后画的页面盖掉,
|
||||
// 表现为“有页面打开时 Tooltip 看不到,所有页关闭时才正常”。
|
||||
for (auto& control : controls)
|
||||
{
|
||||
Control* page = control.second.get();
|
||||
if (!page->IsVisible())
|
||||
continue;
|
||||
|
||||
if (page->hasManagedDirtySubtree())
|
||||
{
|
||||
commitTabUnit(page, SxTabOverlayRedrawMode::None);
|
||||
}
|
||||
else if (hasPaintCoverage && SxTabRectsIntersect(page->getManagedRepaintCoverageRect(), paintCoverage))
|
||||
{
|
||||
const bool persistentHit = hasPersistentCoverage &&
|
||||
SxTabRectsIntersect(page->getManagedRepaintPersistentCoverageRect(), persistentCoverage);
|
||||
if (persistentHit)
|
||||
commitTabUnit(page, SxTabOverlayRedrawMode::RefreshSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& control : controls)
|
||||
{
|
||||
Control* button = control.first.get();
|
||||
if (!button->IsVisible())
|
||||
continue;
|
||||
|
||||
if (button->hasManagedDirtySubtree())
|
||||
{
|
||||
commitTabUnit(button, SxTabOverlayRedrawMode::None);
|
||||
}
|
||||
else if (hasPaintCoverage && SxTabRectsIntersect(button->getManagedRepaintCoverageRect(), paintCoverage))
|
||||
{
|
||||
const bool persistentHit = hasPersistentCoverage &&
|
||||
SxTabRectsIntersect(button->getManagedRepaintPersistentCoverageRect(), persistentCoverage);
|
||||
if (persistentHit)
|
||||
commitTabUnit(button, SxTabOverlayRedrawMode::RefreshSnapshot);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
onRequestRepaintAsRoot();
|
||||
}
|
||||
|
||||
bool TabControl::hasManagedDirtySubtree() const
|
||||
{
|
||||
if (dirty)
|
||||
return true;
|
||||
|
||||
for (const auto& control : controls)
|
||||
{
|
||||
if (control.first->IsVisible() && control.first->hasManagedDirtySubtree())
|
||||
return true;
|
||||
if (control.second->IsVisible() && control.second->hasManagedDirtySubtree())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT TabControl::getManagedRepaintCoverageRect() const
|
||||
{
|
||||
// TabControl 的 draw() 会写自身背景、全部页签按钮以及全部页面。
|
||||
// 因此 coverage 也必须按“页签按钮 + 页面”递归并集,避免内部 Tooltip 等附加绘制区域被漏算。
|
||||
RECT coverage = getBoundsRect();
|
||||
for (const auto& control : controls)
|
||||
{
|
||||
if (control.first->IsVisible())
|
||||
coverage = SxTabUnionRect(coverage, control.first->getManagedRepaintCoverageRect());
|
||||
if (control.second->IsVisible())
|
||||
coverage = SxTabUnionRect(coverage, control.second->getManagedRepaintCoverageRect());
|
||||
}
|
||||
return coverage;
|
||||
}
|
||||
|
||||
RECT TabControl::getManagedRepaintPersistentCoverageRect() const
|
||||
{
|
||||
// 持久 coverage 排除页签按钮 Tooltip 等临时浮层,
|
||||
// 用于判断上层页签/页面补画时是否允许刷新背景快照。
|
||||
RECT coverage = getBoundsRect();
|
||||
for (const auto& control : controls)
|
||||
{
|
||||
if (control.first->IsVisible())
|
||||
coverage = SxTabUnionRect(coverage, control.first->getManagedRepaintPersistentCoverageRect());
|
||||
if (control.second->IsVisible())
|
||||
coverage = SxTabUnionRect(coverage, control.second->getManagedRepaintPersistentCoverageRect());
|
||||
}
|
||||
return coverage;
|
||||
}
|
||||
|
||||
bool TabControl::canCommitManagedPartialRepaint() const
|
||||
{
|
||||
// TabControl 只有在自己本体不脏且背景快照有效时,才允许只更新脏页签/脏页面。
|
||||
return show && !dirty && hasValidBackgroundSnapshot();
|
||||
}
|
||||
|
||||
void TabControl::commitManagedRepaint()
|
||||
{
|
||||
if (!show)
|
||||
return;
|
||||
|
||||
if (canCommitManagedPartialRepaint())
|
||||
{
|
||||
// 页签栏和页面基线都还有效:沿用原有局部重绘逻辑。
|
||||
requestRepaint(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// 自身布局或背景已经变化:升级为整 TabControl 重画。
|
||||
this->dirty = true;
|
||||
onRequestRepaintAsRoot();
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* @类: TabControl
|
||||
* @摘要: 选项卡容器控件,管理“页签按钮 + 对应页面(Canvas)”
|
||||
* @描述:
|
||||
* 提供页签栏布局(上/下/左/右)、选中切换、页内容区域定位;
|
||||
* 与 Button 一起工作,支持窗口大小变化、可见性联动与脏区重绘。
|
||||
* 在托管重绘模式下,TabControl 作为“页签栏 + 当前页面”的统一重绘 root。
|
||||
*
|
||||
* @特性:
|
||||
* - 页签栏四向排列(Top / Bottom / Left / Right)
|
||||
* - 一键添加“页签+页”或为指定页添加子控件
|
||||
* - 获取/设置当前激活页签索引
|
||||
* - 自适应窗口变化,重算页签与页面区域
|
||||
* - 与 Button 的 TOGGLE 模式联动显示/隐藏页面
|
||||
*
|
||||
* @使用场景: 在同一区域内承载多张页面,使用页签进行快速切换
|
||||
* @所属框架: 星垣(StellarX) GUI框架
|
||||
* @作者: 我在人间做废物
|
||||
******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
#include "CoreTypes.h"
|
||||
#include "Button.h"
|
||||
#include "Canvas.h"
|
||||
#define BUTMINHEIGHT 15 //页签按钮最小尺寸,过小会导致显示问题
|
||||
#define BUTMINWIDTH 30 //页签按钮最小尺寸,过小会导致显示问题
|
||||
class TabControl :public Canvas
|
||||
{
|
||||
int tabBarHeight = BUTMINWIDTH; //页签栏高度
|
||||
bool IsFirstDraw = true; //首次绘制标记
|
||||
int defaultActivation = -1; //默认激活页签索引
|
||||
StellarX::TabPlacement tabPlacement = StellarX::TabPlacement::Top; //页签排列方式
|
||||
std::vector<std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>> controls; //页签/页列表
|
||||
|
||||
private:
|
||||
using Canvas::addControl; // 禁止外部误用
|
||||
void addControl(std::unique_ptr<Control>) = delete; // 精准禁用该重载
|
||||
private:
|
||||
// 初始化页签按钮位置和尺寸
|
||||
inline void initTabBar();
|
||||
inline void initTabPage();
|
||||
// 统一刷新 TabControl 当前运行态下的页签栏和页面区布局。
|
||||
void refreshRuntimeLayout();
|
||||
public:
|
||||
// 默认构造:创建一个空 TabControl
|
||||
TabControl();
|
||||
// 指定初始位置和尺寸构造 TabControl
|
||||
TabControl(int x, int y, int width, int height);
|
||||
~TabControl();
|
||||
|
||||
//重写位置设置以适应页签和页面布局
|
||||
void setX(int x)override;
|
||||
void setY(int y)override;
|
||||
void setWidth(int width) override;
|
||||
void setHeight(int height) override;
|
||||
|
||||
void draw() override;
|
||||
bool handleEvent(const ExMessage& msg) override;
|
||||
// 只转发清理页签按钮和当前可见页面中的鼠标临时状态,不升级为整块 TabControl 重绘。
|
||||
bool clearTransientMouseState() override;
|
||||
|
||||
// 添加一个“页签按钮 + 页面 Canvas”组合
|
||||
void add(std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>>&& control);
|
||||
// 按页签文本把控件添加到对应页面
|
||||
void add(std::string tabText, std::unique_ptr<Control> control);
|
||||
//设置页签位置
|
||||
void setTabPlacement(StellarX::TabPlacement placement);
|
||||
//设置页签栏高度 两侧排列时为宽度
|
||||
void setTabBarHeight(int height);
|
||||
//设置不可见后传递给子控件重写
|
||||
void setIsVisible(bool visible) override;
|
||||
void onWindowResize() override;
|
||||
//获取当前激活页签索引
|
||||
int getActiveIndex() const;
|
||||
// 设置当前激活页签索引;若已是当前激活页则直接返回
|
||||
void setActiveIndex(int idx);
|
||||
// 获取页签数量
|
||||
int count() const;
|
||||
// 通过页签文本查找索引
|
||||
int indexOf(const std::string& tabText) const;
|
||||
//设置脏区并请求重绘
|
||||
void setDirty(bool dirty) override;
|
||||
//请求父控件重绘
|
||||
void requestRepaint(Control* parent)override; // 托管模式下登记为 root;非托管模式下局部更新脏按钮/脏页面
|
||||
bool hasManagedDirtySubtree() const override;
|
||||
RECT getManagedRepaintCoverageRect() const override;
|
||||
RECT getManagedRepaintPersistentCoverageRect() const override;
|
||||
bool canCommitManagedPartialRepaint() const override; // 判断当前 TabControl 是否可安全做局部提交
|
||||
void commitManagedRepaint() override; // 托管收口阶段执行 TabControl 的真正重绘
|
||||
protected:
|
||||
// 外层统一解算后,TabControl 需要同步刷新其内部页签栏和页面区。
|
||||
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
|
||||
};
|
||||
@@ -1,889 +0,0 @@
|
||||
#include "Table.h"
|
||||
#include "SxLog.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::vector<std::string> NormalizeTableRow(std::vector<std::string> row, size_t headerCount)
|
||||
{
|
||||
if (headerCount == 0)
|
||||
return row;
|
||||
|
||||
if (row.size() > headerCount)
|
||||
row.resize(headerCount);
|
||||
else if (row.size() < headerCount)
|
||||
row.resize(headerCount, "");
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
int CalculateTotalPages(size_t rowCount, int rowsPerPage)
|
||||
{
|
||||
if (rowsPerPage < 1)
|
||||
rowsPerPage = 1;
|
||||
|
||||
const int total = static_cast<int>((rowCount + static_cast<size_t>(rowsPerPage) - 1) / static_cast<size_t>(rowsPerPage));
|
||||
return total > 0 ? total : 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制表格的当前页
|
||||
// 使用双循环绘制行和列,考虑分页偏移
|
||||
void Table::drawTable()
|
||||
{
|
||||
if (lineHeights.empty() || colWidths.empty())
|
||||
return;
|
||||
|
||||
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
|
||||
|
||||
// 表体从“表头之下”开始
|
||||
dX = x + border;
|
||||
dY = y + border + lineHeights.at(0) + TABLE_HEADER_EXTRA; // 表头高度
|
||||
uY = dY + lineHeights.at(0) + TABLE_ROW_EXTRA;
|
||||
|
||||
size_t startRow = (currentPage - 1) * rowsPerPage;
|
||||
size_t endRow = startRow + (size_t)rowsPerPage < data.size() ? startRow + (size_t)rowsPerPage : data.size();
|
||||
|
||||
for (size_t i = startRow; i < endRow; ++i)
|
||||
{
|
||||
for (size_t j = 0; j < data[i].size(); ++j)
|
||||
{
|
||||
uX = dX + colWidths.at(j) + TABLE_COL_GAP;
|
||||
fillrectangle(dX, dY, uX, uY);
|
||||
outtextxy(dX + TABLE_PAD_X, dY + TABLE_PAD_Y, LPCTSTR(data[i][j].c_str()));
|
||||
dX += colWidths.at(j) + TABLE_COL_GAP;
|
||||
}
|
||||
dX = x + border;
|
||||
dY = uY;
|
||||
uY = dY + lineHeights.at(0) + TABLE_ROW_EXTRA;
|
||||
}
|
||||
}
|
||||
|
||||
void Table::drawHeader()
|
||||
{
|
||||
if (headers.empty() || lineHeights.empty() || colWidths.empty())
|
||||
return;
|
||||
|
||||
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
|
||||
// 内容区原点 = x+border, y+border
|
||||
dX = x + border;
|
||||
dY = y + border;
|
||||
uY = dY + lineHeights.at(0) + TABLE_HEADER_EXTRA;
|
||||
|
||||
for (size_t i = 0; i < headers.size(); i++)
|
||||
{
|
||||
uX = dX + colWidths.at(i) + TABLE_COL_GAP; // 注意这里是 +20,和表体一致
|
||||
fillrectangle(dX, dY, uX, uY);
|
||||
outtextxy(dX + TABLE_PAD_X, dY + TABLE_PAD_Y, LPCTSTR(headers[i].c_str()));
|
||||
dX += colWidths.at(i) + TABLE_COL_GAP; // 列间距 20
|
||||
}
|
||||
}
|
||||
// 遍历所有数据单元和表头,计算每列的最大宽度和每行的最大高度,
|
||||
// 为后续绘制表格单元格提供尺寸依据。此计算在数据变更时自动触发。
|
||||
void Table::initTextWaH()
|
||||
{
|
||||
// 和绘制一致的单元内边距
|
||||
const int padX = TABLE_PAD_X; // 左右 padding
|
||||
const int padY = TABLE_PAD_Y; // 上下 padding
|
||||
const int colGap = TABLE_COL_GAP; // 列间距
|
||||
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
|
||||
size_t maxCols = headers.size();
|
||||
for (const auto& row : data)
|
||||
if (row.size() > maxCols)
|
||||
maxCols = row.size();
|
||||
|
||||
// 统计每列最大文本宽 & 每列最大行高(包含数据 + 表头)
|
||||
colWidths.assign(maxCols, 0);
|
||||
lineHeights.assign(maxCols, 0);
|
||||
|
||||
// 先看数据
|
||||
for (size_t i = 0; i < data.size(); ++i)
|
||||
{
|
||||
for (size_t j = 0; j < data[i].size(); ++j)
|
||||
{
|
||||
const int w = textwidth(LPCTSTR(data[i][j].c_str()));
|
||||
const int h = textheight(LPCTSTR(data[i][j].c_str()));
|
||||
if (w > colWidths[j]) colWidths[j] = w;
|
||||
if (h > lineHeights[j]) lineHeights[j] = h;
|
||||
}
|
||||
}
|
||||
// 再用表头更新(谁大取谁)
|
||||
for (size_t j = 0; j < headers.size(); ++j)
|
||||
{
|
||||
const int w = textwidth(LPCTSTR(headers[j].c_str()));
|
||||
const int h = textheight(LPCTSTR(headers[j].c_str()));
|
||||
if (w > colWidths[j]) colWidths[j] = w;
|
||||
if (h > lineHeights[j]) lineHeights[j] = h;
|
||||
}
|
||||
|
||||
// 用“所有列的最大行高”作为一行的基准高度
|
||||
int maxLineH = 0;
|
||||
|
||||
for (int h : lineHeights)
|
||||
if (h > maxLineH)
|
||||
maxLineH = h;
|
||||
if (maxLineH == 0)
|
||||
maxLineH = textheight(LPCTSTR("A"));
|
||||
if (rowsPerPage < 1)
|
||||
rowsPerPage = 1;
|
||||
|
||||
// 列宽包含左右 padding:在计算完最大文本宽度后,加上 2*padX 作为单元格内边距
|
||||
for (size_t j = 0; j < colWidths.size(); ++j) {
|
||||
colWidths[j] += 2 * padX;
|
||||
}
|
||||
|
||||
// 表内容总宽 = Σ(列宽 + 列间距)
|
||||
int contentW = 0;
|
||||
for (size_t j = 0; j < colWidths.size(); ++j)
|
||||
contentW += colWidths[j] + colGap;
|
||||
|
||||
// 表头高 & 行高(与 drawHeader/drawTable 内部一致:+上下 padding)
|
||||
const int headerH = maxLineH + 2 * padY;
|
||||
const int rowH = maxLineH + 2 * padY;
|
||||
const int rowsH = rowH * rowsPerPage;
|
||||
|
||||
// 页脚:
|
||||
const int pageTextH = textheight(LPCTSTR(pageNumtext.c_str()));
|
||||
const int btnTextH = textheight(LPCTSTR("上一页"));
|
||||
const int btnPadV = TABLE_BTN_TEXT_PAD_V;
|
||||
const int btnH = btnTextH + 2 * btnPadV;
|
||||
const int footerPad = TABLE_FOOTER_PAD;
|
||||
const int footerH = (pageTextH > btnH ? pageTextH : btnH) + footerPad;
|
||||
|
||||
// 最终表宽/高:内容 + 对称边框
|
||||
this->width = contentW + (border << 1);
|
||||
this->height = headerH + rowsH + footerH + (border << 1);
|
||||
// 这是 Table 自身受控的“结构尺寸 -> 设计基线”刷新点:
|
||||
// 表格总宽高本来就由表头、表体、页脚和分页按钮共同决定,
|
||||
// 因此这里允许同步 localWidth/localHeight,作为后续锚点解算参考。
|
||||
const bool baselineChanged = (this->localWidth != this->width) || (this->localHeight != this->height);
|
||||
this->localWidth = this->width;
|
||||
this->localHeight = this->height;
|
||||
if (baselineChanged)
|
||||
{
|
||||
SX_LOGD("Layout")
|
||||
<< SX_T("Table 刷新结构设计基线:id=", "Table refresh structural design rect: id=") << id
|
||||
<< SX_T(" size=(", " size=(") << this->width << "," << this->height << ")";
|
||||
}
|
||||
}
|
||||
|
||||
void Table::initButton()
|
||||
{
|
||||
const int gap = TABLE_BTN_GAP;
|
||||
const int padH = TABLE_BTN_PAD_H;
|
||||
const int padV = TABLE_BTN_PAD_V; // 按钮垂直内边距
|
||||
|
||||
int pageW = textwidth(LPCTSTR(pageNumtext.c_str()));
|
||||
int lblH = textheight(LPCTSTR(pageNumtext.c_str()));
|
||||
|
||||
// 统一按钮尺寸(用按钮文字自身宽高 + padding)
|
||||
int prevW = textwidth(LPCTSTR(TABLE_STR_PREV)) + padH * 2;
|
||||
int nextW = textwidth(LPCTSTR(TABLE_STR_NEXT)) + padH * 2;
|
||||
int btnH = lblH + padV * 2;
|
||||
|
||||
// 基于“页码标签”的矩形来摆放:
|
||||
// prev 在页码左侧 gap 处;next 在右侧 gap 处;Y 对齐 pY
|
||||
int prevX = pX - gap - prevW;
|
||||
int nextX = pX + pageW + gap;
|
||||
int btnY = pY; // 和页码同一基线
|
||||
|
||||
if (!prevButton)
|
||||
prevButton = std::make_unique<Button>(prevX, btnY, prevW, btnH, TABLE_STR_PREV, RGB(0, 0, 0), RGB(255, 255, 255));
|
||||
else
|
||||
{
|
||||
prevButton->setX(prevX);
|
||||
prevButton->setY(btnY);
|
||||
}
|
||||
|
||||
if (!nextButton)
|
||||
nextButton = std::make_unique<Button>(nextX, btnY, nextW, btnH, TABLE_STR_NEXT, RGB(0, 0, 0), RGB(255, 255, 255));
|
||||
else
|
||||
{
|
||||
nextButton->setX(nextX);
|
||||
nextButton->setY(btnY);
|
||||
}
|
||||
|
||||
prevButton->setParent(this);
|
||||
nextButton->setParent(this);
|
||||
|
||||
prevButton->textStyle = this->textStyle;
|
||||
nextButton->textStyle = this->textStyle;
|
||||
prevButton->setFillMode(tableFillMode);
|
||||
nextButton->setFillMode(tableFillMode);
|
||||
|
||||
prevButton->setOnClickListener([this]()
|
||||
{
|
||||
int oldPage = currentPage;
|
||||
if (currentPage > 1)
|
||||
{
|
||||
--currentPage;
|
||||
SX_LOGI("Table")
|
||||
<< SX_T("翻页:id=", "page change: id=") << id
|
||||
<< " " << oldPage << "->" << currentPage
|
||||
<< SX_T(" 总页数=", " total=") << totalPages
|
||||
<< SX_T(" 行数=", " rows=") << (int)data.size();
|
||||
|
||||
|
||||
dirty = true;
|
||||
if (pageNum) pageNum->setDirty(true);
|
||||
}
|
||||
});
|
||||
nextButton->setOnClickListener([this]()
|
||||
{
|
||||
int oldPage = currentPage;
|
||||
if (currentPage < totalPages)
|
||||
{
|
||||
++currentPage;
|
||||
SX_LOGI("Table")
|
||||
<< SX_T("翻页:id=", "page change: id=") << id
|
||||
<< " " << oldPage << "->" << currentPage
|
||||
<< SX_T(" 总页数=", " total=") << totalPages
|
||||
<< SX_T(" 行数=", " rows=") << (int)data.size();
|
||||
|
||||
dirty = true;
|
||||
if (pageNum) pageNum->setDirty(true);
|
||||
}
|
||||
});
|
||||
isNeedButtonAndPageNum = false;
|
||||
}
|
||||
|
||||
void Table::initPageNum()
|
||||
{
|
||||
// 统一坐标系
|
||||
const int border = tableBorderWidth > 0 ? tableBorderWidth : 0;
|
||||
const int baseH = lineHeights.empty() ? 0 : lineHeights.at(0);
|
||||
const int headerH = baseH + TABLE_HEADER_EXTRA;
|
||||
const int rowsH = baseH * rowsPerPage + rowsPerPage * TABLE_ROW_EXTRA;
|
||||
|
||||
// 内容宽度 = sum(colWidths + 20);initTextWaH() 已把 this->width += 2*border
|
||||
// 因此 contentW = this->width - 2*border 更稳妥
|
||||
const int contentW = this->width - (border << 1);
|
||||
|
||||
// 页脚顶部位置(表头 + 可视数据区 之后)
|
||||
pY = y + border + headerH + rowsH + TABLE_FOOTER_BLANK; // +8 顶部留白
|
||||
|
||||
// 按理来说 x + (this->width - textW) / 2;就可以
|
||||
// 但是在绘制时,发现控件偏右,因此减去40
|
||||
int textW = textwidth(LPCTSTR(pageNumtext.c_str()));
|
||||
pX = x + TABLE_PAGE_TEXT_OFFSET_X + (this->width - textW) / 2;
|
||||
|
||||
if (!pageNum)
|
||||
pageNum = std::make_unique<Label>(pX, pY, pageNumtext);
|
||||
else
|
||||
{
|
||||
pageNum->setX(pX);
|
||||
pageNum->setY(pY);
|
||||
}
|
||||
|
||||
pageNum->setParent(this);
|
||||
|
||||
pageNum->textStyle = this->textStyle;
|
||||
if (StellarX::FillMode::Null == tableFillMode)
|
||||
pageNum->setTextdisap(true); // 透明文本
|
||||
}
|
||||
|
||||
void Table::drawPageNum()
|
||||
{
|
||||
pageNumtext = "第";
|
||||
pageNumtext += std::to_string(currentPage);
|
||||
pageNumtext += "页/共";
|
||||
pageNumtext += std::to_string(totalPages);
|
||||
pageNumtext += "页";
|
||||
if (nullptr == pageNum || isNeedButtonAndPageNum)
|
||||
initPageNum();
|
||||
pageNum->setText(pageNumtext);
|
||||
pageNum->textStyle = this->textStyle;
|
||||
if (StellarX::FillMode::Null == tableFillMode)
|
||||
pageNum->setTextdisap(true);
|
||||
// Table 每次整表重绘前都会先回贴自己的背景快照,
|
||||
// 页码区域会被一起清空。即便页码文本本身没变化,也必须强制把 Label 重新设脏,
|
||||
// 否则 Label::draw() 会因为 dirty=false 直接跳过,导致页码在表格重绘后消失。
|
||||
pageNum->setDirty(true);
|
||||
pageNum->draw();
|
||||
}
|
||||
|
||||
void Table::drawButton()
|
||||
{
|
||||
if ((nullptr == prevButton || nullptr == nextButton) || isNeedButtonAndPageNum)
|
||||
initButton();
|
||||
|
||||
this->prevButton->textStyle = this->textStyle;
|
||||
this->nextButton->textStyle = this->textStyle;
|
||||
this->prevButton->setFillMode(tableFillMode);
|
||||
this->nextButton->setFillMode(tableFillMode);
|
||||
this->prevButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
|
||||
this->nextButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
|
||||
this->prevButton->setDirty(true);
|
||||
this->nextButton->setDirty(true);
|
||||
prevButton->draw();
|
||||
nextButton->draw();
|
||||
}
|
||||
|
||||
void Table::setX(int x)
|
||||
{
|
||||
this->x = x;
|
||||
isNeedButtonAndPageNum = true;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Table::setY(int y)
|
||||
{
|
||||
this->y = y;
|
||||
isNeedButtonAndPageNum = true;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Table::setWidth(int width)
|
||||
{
|
||||
// 调整列宽以匹配新的表格总宽度。不修改 localWidth,避免累计误差。
|
||||
// 当 width 与当前 width 不同时,根据差值平均分配到各列,余数依次累加/扣减。
|
||||
const int ncols = static_cast<int>(colWidths.size());
|
||||
if (ncols <= 0) {
|
||||
this->width = width;
|
||||
isNeedButtonAndPageNum = true;
|
||||
return;
|
||||
}
|
||||
int diff = width - this->width;
|
||||
// 基础增量:整除部分
|
||||
int baseChange = diff / ncols;
|
||||
int remainder = diff % ncols;
|
||||
for (int i = 0; i < ncols; ++i) {
|
||||
int change = baseChange;
|
||||
if (remainder > 0) {
|
||||
change += 1;
|
||||
remainder -= 1;
|
||||
}
|
||||
else if (remainder < 0) {
|
||||
change -= 1;
|
||||
remainder += 1;
|
||||
}
|
||||
int newWidth = colWidths[i] + change;
|
||||
// 限制最小宽度为 1,防止出现负值
|
||||
if (newWidth < 1) newWidth = 1;
|
||||
colWidths[i] = newWidth;
|
||||
}
|
||||
this->width = width;
|
||||
// 需要重新布局页脚元素
|
||||
isNeedButtonAndPageNum = true;
|
||||
}
|
||||
|
||||
void Table::setHeight(int height)
|
||||
{
|
||||
// 当前阶段 Table 明确不支持纵向 Stretch。
|
||||
// 高度链路依赖表头、表体、页脚、按钮和页码计算,
|
||||
// 因此这里保持空实现,避免被通用布局层错误拉高/压缩。
|
||||
}
|
||||
|
||||
Table::Table(int x, int y)
|
||||
:Control(x, y, 0, 0)
|
||||
{
|
||||
this->id = "Table";
|
||||
// Table 当前正式能力边界:
|
||||
// 仅允许 X 轴 Stretch,Y 轴固定尺寸。
|
||||
this->layoutCapability.allowStretchX = true;
|
||||
this->layoutCapability.allowStretchY = false;
|
||||
}
|
||||
|
||||
Table::~Table() = default;
|
||||
|
||||
void Table::applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect)
|
||||
{
|
||||
// Table 不能像普通控件那样直接写入 width/height:
|
||||
// 它的 setWidth() 内部会联动列宽与页脚布局,因此这里必须复用原有 setter 副作用。
|
||||
// 同时 Table 的 Y Fixed 是当前版本的正式行为边界:
|
||||
// 即便统一解算器给出了新的 rect.height,这里也不会接收该高度。
|
||||
if (this->x != rect.worldX)
|
||||
setX(rect.worldX);
|
||||
if (this->y != rect.worldY)
|
||||
setY(rect.worldY);
|
||||
if (this->width != rect.width)
|
||||
setWidth(rect.width);
|
||||
if (this->height != rect.height)
|
||||
{
|
||||
SX_LOGD("Layout")
|
||||
<< SX_T("Table 忽略 Y 轴运行态高度写入:id=", "Table ignore runtime Y resize: id=")
|
||||
<< id
|
||||
<< SX_T(" currentH=", " currentH=") << this->height
|
||||
<< SX_T(" requestedH=", " requestedH=") << rect.height;
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Table::draw()
|
||||
{
|
||||
//在这里先初始化保证翻页按钮不为空
|
||||
// 在一些容器中,Table不会被立即绘制可能导致事件事件传递时触发空指针警报
|
||||
// 由于单元格初始化依赖字体数据所以先设置一次字体样式
|
||||
// 先保存当前绘图状态
|
||||
saveStyle();
|
||||
|
||||
// 设置表格样式
|
||||
setfillcolor(tableBkClor);
|
||||
setlinecolor(tableBorderClor);
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
settextcolor(textStyle.color);
|
||||
setlinestyle((int)tableLineStyle, tableBorderWidth);
|
||||
setfillstyle((int)tableFillMode);
|
||||
// 是否需要计算单元格尺寸
|
||||
if (isNeedCellSize)
|
||||
{
|
||||
initTextWaH();
|
||||
isNeedCellSize = false;
|
||||
}
|
||||
restoreStyle();
|
||||
if (this->dirty && this->show)
|
||||
{
|
||||
// 先保存当前绘图状态
|
||||
saveStyle();
|
||||
|
||||
// 设置表格样式
|
||||
setfillcolor(tableBkClor);
|
||||
setlinecolor(tableBorderClor);
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
settextcolor(textStyle.color);
|
||||
setlinestyle((int)tableLineStyle, tableBorderWidth);
|
||||
setfillstyle((int)tableFillMode);
|
||||
setbkmode(TRANSPARENT);
|
||||
|
||||
if (isNeedDrawHeaders)
|
||||
{
|
||||
// 重新设置表格样式
|
||||
setfillcolor(tableBkClor);
|
||||
setlinecolor(tableBorderClor);
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
settextcolor(textStyle.color);
|
||||
setlinestyle((int)tableLineStyle, tableBorderWidth);
|
||||
setfillstyle((int)tableFillMode);
|
||||
setbkmode(TRANSPARENT);
|
||||
}
|
||||
// 在绘制前先恢复并更新背景快照:
|
||||
// 如果已有快照且尺寸发生变化,先恢复旧快照以清除上一次绘制,然后丢弃旧快照再重新抓取新的区域。
|
||||
if (hasSnap)
|
||||
{
|
||||
// 始终先恢复旧背景,清除上一帧内容
|
||||
restBackground();
|
||||
// 当尺寸变化或缓存图像无效时,需要重新截图
|
||||
if (!saveBkImage || saveWidth != this->width || saveHeight != this->height)
|
||||
{
|
||||
invalidateBackgroundSnapshot();
|
||||
saveBackground(this->x, this->y, this->width, this->height);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 首次绘制时无背景缓存,直接抓取
|
||||
saveBackground(this->x, this->y, this->width, this->height);
|
||||
}
|
||||
// 恢复最新的背景,保证绘制区域干净
|
||||
restBackground();
|
||||
// 绘制表头
|
||||
|
||||
//if (!headers.empty())
|
||||
drawHeader();
|
||||
// 绘制当前页
|
||||
drawTable();
|
||||
// 绘制页码标签
|
||||
drawPageNum();
|
||||
|
||||
// 绘制翻页按钮
|
||||
if (this->isShowPageButton)
|
||||
drawButton();
|
||||
|
||||
// 恢复绘图状态
|
||||
restoreStyle();
|
||||
dirty = false; // 标记不需要重绘
|
||||
}
|
||||
}
|
||||
|
||||
bool Table::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
if (!show)return false;
|
||||
resetEventVisualChanged();
|
||||
bool consume = false;
|
||||
bool anyVisualChanged = false;
|
||||
if (!this->isShowPageButton)
|
||||
return consume;
|
||||
|
||||
if (msg.message == WM_MOUSEMOVE)
|
||||
{
|
||||
// Table 内部分页按钮也按“后绘制者优先”的倒序语义处理。
|
||||
// drawButton() 里是 prev 再 next,因此这里先 next 再 prev。
|
||||
if (nextButton)
|
||||
{
|
||||
if (!consume)
|
||||
{
|
||||
consume = nextButton->handleEvent(msg);
|
||||
if (nextButton->didEventAffectVisual())
|
||||
anyVisualChanged = true;
|
||||
}
|
||||
else if (nextButton->clearTransientMouseState())
|
||||
{
|
||||
anyVisualChanged = true;
|
||||
}
|
||||
}
|
||||
if (prevButton)
|
||||
{
|
||||
if (!consume)
|
||||
{
|
||||
consume = prevButton->handleEvent(msg);
|
||||
if (prevButton->didEventAffectVisual())
|
||||
anyVisualChanged = true;
|
||||
}
|
||||
else if (prevButton->clearTransientMouseState())
|
||||
{
|
||||
anyVisualChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 点击类消息仍保持“后绘制者优先”的倒序命中语义。
|
||||
// 但和 WM_MOUSEMOVE 一样,分页按钮的视觉状态变化必须向上提升为 Table 重绘,
|
||||
// 否则按钮内部已经变成 hover/click,父级却不知道需要重画整张 Table,
|
||||
// 就会出现“状态实际变了,但屏幕上直到下一次翻页才看见”的现象。
|
||||
if (nextButton)
|
||||
{
|
||||
consume = nextButton->handleEvent(msg);
|
||||
if (nextButton->didEventAffectVisual())
|
||||
anyVisualChanged = true;
|
||||
}
|
||||
if (prevButton && !consume)
|
||||
{
|
||||
consume = prevButton->handleEvent(msg);
|
||||
if (prevButton->didEventAffectVisual())
|
||||
anyVisualChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Table 当前还没有像 TabControl 那样把分页按钮做成可独立提交的内部绘制单元。
|
||||
// 因此只要内部按钮的 hover / click / leave 造成视觉变化,就必须把整张 Table 设脏,
|
||||
// 让父容器通过现有的 Table::draw() 链把页脚区域正确重画出来。
|
||||
if (anyVisualChanged)
|
||||
dirty = true;
|
||||
|
||||
if (dirty)
|
||||
requestRepaint(parent);
|
||||
markEventVisualChanged(anyVisualChanged || dirty);
|
||||
return consume;
|
||||
}
|
||||
|
||||
bool Table::clearTransientMouseState()
|
||||
{
|
||||
bool changed = false;
|
||||
if (nextButton && nextButton->clearTransientMouseState())
|
||||
changed = true;
|
||||
if (prevButton && prevButton->clearTransientMouseState())
|
||||
changed = true;
|
||||
return changed;
|
||||
}
|
||||
|
||||
RECT Table::getManagedRepaintCoverageRect() const
|
||||
{
|
||||
RECT coverage = getBoundsRect();
|
||||
|
||||
if (pageNum && pageNum->IsVisible())
|
||||
{
|
||||
const RECT pageRect = pageNum->getManagedRepaintCoverageRect();
|
||||
coverage.left = (std::min)(coverage.left, pageRect.left);
|
||||
coverage.top = (std::min)(coverage.top, pageRect.top);
|
||||
coverage.right = (std::max)(coverage.right, pageRect.right);
|
||||
coverage.bottom = (std::max)(coverage.bottom, pageRect.bottom);
|
||||
}
|
||||
if (prevButton && prevButton->IsVisible())
|
||||
{
|
||||
const RECT prevRect = prevButton->getManagedRepaintCoverageRect();
|
||||
coverage.left = (std::min)(coverage.left, prevRect.left);
|
||||
coverage.top = (std::min)(coverage.top, prevRect.top);
|
||||
coverage.right = (std::max)(coverage.right, prevRect.right);
|
||||
coverage.bottom = (std::max)(coverage.bottom, prevRect.bottom);
|
||||
}
|
||||
if (nextButton && nextButton->IsVisible())
|
||||
{
|
||||
const RECT nextRect = nextButton->getManagedRepaintCoverageRect();
|
||||
coverage.left = (std::min)(coverage.left, nextRect.left);
|
||||
coverage.top = (std::min)(coverage.top, nextRect.top);
|
||||
coverage.right = (std::max)(coverage.right, nextRect.right);
|
||||
coverage.bottom = (std::max)(coverage.bottom, nextRect.bottom);
|
||||
}
|
||||
|
||||
return coverage;
|
||||
}
|
||||
|
||||
RECT Table::getManagedRepaintPersistentCoverageRect() const
|
||||
{
|
||||
// Table 当前仍按整表绘制保证正确性;分页按钮 Tooltip 属于临时浮层,
|
||||
// 不能进入持久 coverage,否则可能污染外层兄弟控件快照。
|
||||
RECT coverage = getBoundsRect();
|
||||
|
||||
if (pageNum && pageNum->IsVisible())
|
||||
{
|
||||
const RECT pageRect = pageNum->getManagedRepaintPersistentCoverageRect();
|
||||
coverage.left = (std::min)(coverage.left, pageRect.left);
|
||||
coverage.top = (std::min)(coverage.top, pageRect.top);
|
||||
coverage.right = (std::max)(coverage.right, pageRect.right);
|
||||
coverage.bottom = (std::max)(coverage.bottom, pageRect.bottom);
|
||||
}
|
||||
if (prevButton && prevButton->IsVisible())
|
||||
{
|
||||
const RECT prevRect = prevButton->getManagedRepaintPersistentCoverageRect();
|
||||
coverage.left = (std::min)(coverage.left, prevRect.left);
|
||||
coverage.top = (std::min)(coverage.top, prevRect.top);
|
||||
coverage.right = (std::max)(coverage.right, prevRect.right);
|
||||
coverage.bottom = (std::max)(coverage.bottom, prevRect.bottom);
|
||||
}
|
||||
if (nextButton && nextButton->IsVisible())
|
||||
{
|
||||
const RECT nextRect = nextButton->getManagedRepaintPersistentCoverageRect();
|
||||
coverage.left = (std::min)(coverage.left, nextRect.left);
|
||||
coverage.top = (std::min)(coverage.top, nextRect.top);
|
||||
coverage.right = (std::max)(coverage.right, nextRect.right);
|
||||
coverage.bottom = (std::max)(coverage.bottom, nextRect.bottom);
|
||||
}
|
||||
|
||||
return coverage;
|
||||
}
|
||||
|
||||
void Table::setHeaders(std::initializer_list<std::string> headers)
|
||||
{
|
||||
this->headers.clear();
|
||||
for (auto& lis : headers)
|
||||
this->headers.push_back(lis);
|
||||
|
||||
if (!this->headers.empty())
|
||||
{
|
||||
for (auto& row : this->data)
|
||||
row = NormalizeTableRow(std::move(row), this->headers.size());
|
||||
}
|
||||
|
||||
totalPages = CalculateTotalPages(this->data.size(), rowsPerPage);
|
||||
if (currentPage > totalPages)
|
||||
currentPage = totalPages;
|
||||
|
||||
SX_LOGI("Table") << SX_T("设置表头:id=","setHeaders: id=") << id << SX_T("总数="," count=") << (int)this->headers.size();
|
||||
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
||||
isNeedDrawHeaders = true; // 标记需要重新绘制表头
|
||||
isNeedButtonAndPageNum = true;
|
||||
dirty = true;
|
||||
|
||||
}
|
||||
|
||||
void Table::setData(std::vector<std::string> data)
|
||||
{
|
||||
data = NormalizeTableRow(std::move(data), headers.size());
|
||||
|
||||
this->data.push_back(data);
|
||||
|
||||
totalPages = CalculateTotalPages(this->data.size(), rowsPerPage);
|
||||
if (currentPage > totalPages)
|
||||
currentPage = totalPages;
|
||||
|
||||
isNeedCellSize = true;
|
||||
isNeedButtonAndPageNum = true;
|
||||
dirty = true;
|
||||
|
||||
SX_LOGI("Table")
|
||||
<< SX_T("新增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)
|
||||
{
|
||||
int addedRows = 0;
|
||||
for (auto row : data)
|
||||
{
|
||||
this->data.push_back(NormalizeTableRow(std::move(row), headers.size()));
|
||||
++addedRows;
|
||||
}
|
||||
|
||||
totalPages = CalculateTotalPages(this->data.size(), rowsPerPage);
|
||||
if (currentPage > totalPages)
|
||||
currentPage = totalPages;
|
||||
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
||||
isNeedButtonAndPageNum = true;
|
||||
dirty = true;
|
||||
SX_LOGI("Table")
|
||||
<< SX_T("新增多行Data:id=", "appendRows: id=") << id
|
||||
<< SX_T(" 新增行数=", " addedRows=") << addedRows
|
||||
<< SX_T(" 数据总行数=", " totalRows=") << (int)this->data.size()
|
||||
<< SX_T(" 总页数=", " totalPages=") << totalPages;
|
||||
|
||||
|
||||
}
|
||||
|
||||
void Table::setRowsPerPage(int rows)
|
||||
{
|
||||
this->rowsPerPage = rows;
|
||||
if (this->rowsPerPage < 1)
|
||||
this->rowsPerPage = 1;
|
||||
totalPages = CalculateTotalPages(data.size(), rowsPerPage);
|
||||
if (currentPage > totalPages)
|
||||
currentPage = totalPages;
|
||||
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
||||
isNeedButtonAndPageNum = true;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Table::showPageButton(bool isShow)
|
||||
{
|
||||
this->isShowPageButton = isShow;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Table::setTableBorder(COLORREF color)
|
||||
{
|
||||
this->tableBorderClor = color;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Table::setTableBk(COLORREF color)
|
||||
{
|
||||
this->tableBkClor = color;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Table::setTableFillMode(StellarX::FillMode mode)
|
||||
{
|
||||
if (StellarX::FillMode::Solid == mode || StellarX::FillMode::Null == mode)
|
||||
this->tableFillMode = mode;
|
||||
else
|
||||
this->tableFillMode = StellarX::FillMode::Solid;
|
||||
if (this->prevButton && this->nextButton && this->pageNum)
|
||||
{
|
||||
this->prevButton->textStyle = this->textStyle;
|
||||
this->nextButton->textStyle = this->textStyle;
|
||||
this->prevButton->setFillMode(tableFillMode);
|
||||
this->nextButton->setFillMode(tableFillMode);
|
||||
if (StellarX::FillMode::Null == tableFillMode)
|
||||
pageNum->setTextdisap(true);
|
||||
this->prevButton->setDirty(true);
|
||||
this->nextButton->setDirty(true);
|
||||
}
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Table::setTableLineStyle(StellarX::LineStyle style)
|
||||
{
|
||||
this->tableLineStyle = style;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Table::setTableBorderWidth(int width)
|
||||
{
|
||||
this->tableBorderWidth = width;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void Table::clearHeaders()
|
||||
{
|
||||
this->headers.clear();
|
||||
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
||||
isNeedDrawHeaders = true; // 标记需要重新绘制表头
|
||||
isNeedButtonAndPageNum = true;// 标记需要重新计算翻页按钮和页码信息
|
||||
dirty = true;
|
||||
SX_LOGI("Table") << SX_T("清除表头:id=","clearHeaders: id=" )<< id;
|
||||
|
||||
}
|
||||
|
||||
void Table::clearData()
|
||||
{
|
||||
this->data.clear();
|
||||
this->currentPage = 1;
|
||||
this->totalPages = CalculateTotalPages(this->data.size(), rowsPerPage);
|
||||
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
||||
isNeedButtonAndPageNum = true;// 标记需要重新计算翻页按钮和页码信息
|
||||
dirty = true;
|
||||
SX_LOGI("Table") << SX_T("清除表格数据:id=","clearData: id=") << id;
|
||||
}
|
||||
|
||||
void Table::resetTable()
|
||||
{
|
||||
clearHeaders();
|
||||
clearData();
|
||||
}
|
||||
|
||||
void Table::onWindowResize()
|
||||
{
|
||||
Control::onWindowResize(); // 先处理自己
|
||||
if (this->prevButton && this->nextButton && this->pageNum)
|
||||
{
|
||||
prevButton->onWindowResize();
|
||||
nextButton->onWindowResize();
|
||||
pageNum->onWindowResize();
|
||||
}
|
||||
}
|
||||
|
||||
int Table::getCurrentPage() const
|
||||
{
|
||||
return this->currentPage;
|
||||
}
|
||||
|
||||
int Table::getTotalPages() const
|
||||
{
|
||||
return this->totalPages;
|
||||
}
|
||||
|
||||
int Table::getRowsPerPage() const
|
||||
{
|
||||
return this->rowsPerPage;
|
||||
}
|
||||
|
||||
bool Table::getShowPageButton() const
|
||||
{
|
||||
return this->isShowPageButton;
|
||||
}
|
||||
|
||||
COLORREF Table::getTableBorder() const
|
||||
{
|
||||
return this->tableBorderClor;
|
||||
}
|
||||
|
||||
COLORREF Table::getTableBk() const
|
||||
{
|
||||
return this->tableBkClor;
|
||||
}
|
||||
|
||||
StellarX::FillMode Table::getTableFillMode() const
|
||||
{
|
||||
return this->tableFillMode;
|
||||
}
|
||||
|
||||
StellarX::LineStyle Table::getTableLineStyle() const
|
||||
{
|
||||
return this->tableLineStyle;
|
||||
}
|
||||
|
||||
std::vector<std::string> Table::getHeaders() const
|
||||
{
|
||||
return this->headers;
|
||||
}
|
||||
|
||||
std::vector<std::vector<std::string>> Table::getData() const
|
||||
{
|
||||
return this->data;
|
||||
}
|
||||
|
||||
int Table::getTableBorderWidth() const
|
||||
{
|
||||
return this->tableBorderWidth;
|
||||
}
|
||||
|
||||
int Table::getTableWidth() const
|
||||
{
|
||||
int temp = 0;
|
||||
for (auto& w : colWidths)
|
||||
temp += w;
|
||||
return temp;
|
||||
}
|
||||
|
||||
int Table::getTableHeight() const
|
||||
{
|
||||
return this->height;
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* @类: Table
|
||||
* @摘要: 高级表格控件,支持分页显示和大数据量展示
|
||||
* @描述:
|
||||
* 提供完整的数据表格功能,包括表头、数据行、分页导航等。
|
||||
* 自动计算列宽行高,支持自定义样式和交互。
|
||||
*
|
||||
* @特性:
|
||||
* - 自动分页和页码计算
|
||||
* - 可配置的每页行数
|
||||
* - 自定义边框样式和填充模式
|
||||
* - 翻页按钮和页码显示
|
||||
* - 背景缓存优化渲染性能
|
||||
*
|
||||
* @使用场景: 数据展示、报表生成、记录浏览等表格需求
|
||||
* @所属框架: 星垣(StellarX) GUI框架
|
||||
* @作者: 我在人间做废物
|
||||
******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
#include "Control.h"
|
||||
#include "Button.h"
|
||||
#include "Label.h"
|
||||
|
||||
// === Table metrics (layout) ===
|
||||
#define TABLE_PAD_X 10 // 单元格左右内边距
|
||||
#define TABLE_PAD_Y 5 // 单元格上下内边距
|
||||
#define TABLE_COL_GAP 20 // 列间距(列与列之间)
|
||||
#define TABLE_HEADER_EXTRA 10 // 表头额外高度(若不想复用 pad 计算)
|
||||
#define TABLE_ROW_EXTRA 10 // 行额外高度(同上;或直接用 2*TABLE_PAD_Y)
|
||||
#define TABLE_BTN_GAP 12 // 页码与按钮的水平间距
|
||||
#define TABLE_BTN_PAD_H 12 // 按钮水平 padding
|
||||
#define TABLE_BTN_PAD_V 0 // 按钮垂直 padding(initButton)
|
||||
#define TABLE_BTN_TEXT_PAD_V 8 // 计算页脚高度时的按钮文字垂直 padding(initTextWaH)
|
||||
#define TABLE_FOOTER_PAD 16 // 页脚额外高度(底部留白)
|
||||
#define TABLE_FOOTER_BLANK 8 // 页脚顶部留白
|
||||
#define TABLE_PAGE_TEXT_OFFSET_X (-40) // 页码文本的临时水平修正
|
||||
|
||||
// === Table defaults (theme) ===
|
||||
#define TABLE_DEFAULT_ROWS_PER_PAGE 5
|
||||
#define TABLE_DEFAULT_BORDER_WIDTH 1
|
||||
#define TABLE_DEFAULT_BORDER_COLOR RGB(0,0,0)
|
||||
#define TABLE_DEFAULT_BG_COLOR RGB(255,255,255)
|
||||
|
||||
// === Strings (i18n ready) ===
|
||||
#define TABLE_STR_PREV "上一页"
|
||||
#define TABLE_STR_NEXT "下一页"
|
||||
#define TABLE_STR_PAGE_PREFIX "第"
|
||||
#define TABLE_STR_PAGE_MID "页/共"
|
||||
#define TABLE_STR_PAGE_SUFFIX "页"
|
||||
|
||||
class Table :public Control
|
||||
{
|
||||
private:
|
||||
std::vector<std::vector<std::string>> data; // 表格数据
|
||||
std::vector<std::string> headers; // 表格表头
|
||||
std::string pageNumtext = "页码标签"; // 页码标签文本
|
||||
|
||||
int tableBorderWidth = TABLE_DEFAULT_BORDER_WIDTH; // 边框宽度
|
||||
|
||||
std::vector<int> colWidths; // 每列的宽度
|
||||
std::vector<int> lineHeights; // 每行的高度
|
||||
|
||||
int rowsPerPage = TABLE_DEFAULT_ROWS_PER_PAGE; // 每页显示的行数
|
||||
int currentPage = 1; // 当前页码
|
||||
int totalPages = 1; // 总页数
|
||||
|
||||
bool isShowPageButton = true; // 是否显示翻页按钮
|
||||
bool isNeedDrawHeaders = true; // 是否需要绘制表头(暂时废弃,单做保留,后期优化可能用到)
|
||||
bool isNeedCellSize = true; // 是否需要计算单元格尺寸
|
||||
bool isNeedButtonAndPageNum = true; // 是否需要计算翻页按钮和页码信息
|
||||
|
||||
std::unique_ptr<Button> prevButton; // 上一页按钮
|
||||
std::unique_ptr<Button> nextButton; // 下一页按钮
|
||||
std::unique_ptr<Label> pageNum; //页码文本
|
||||
|
||||
int dX = x, dY = y; // 单元格的开始坐标
|
||||
int uX = x, uY = y; // 单元格的结束坐标
|
||||
|
||||
int pX = 0; //标签左上角坐标
|
||||
int pY = 0; //标签左上角坐标
|
||||
|
||||
StellarX::FillMode tableFillMode = StellarX::FillMode::Solid; //填充模式
|
||||
StellarX::LineStyle tableLineStyle = StellarX::LineStyle::Solid; // 线型
|
||||
COLORREF tableBorderClor = TABLE_DEFAULT_BORDER_COLOR; // 表格边框颜色
|
||||
COLORREF tableBkClor = TABLE_DEFAULT_BG_COLOR; // 表格背景颜色
|
||||
|
||||
void initTextWaH(); //初始化文本像素宽度和高度
|
||||
void initButton(); //初始化翻页按钮
|
||||
void initPageNum(); //初始化页码标签
|
||||
|
||||
void drawTable(); //绘制当前页
|
||||
void drawHeader(); //绘制表头
|
||||
void drawPageNum(); //绘制页码信息
|
||||
void drawButton(); //绘制翻页按钮
|
||||
// 统一解算后的内部应用入口:
|
||||
// Table 需要复用自己已有的 setWidth/setX 等副作用逻辑,因此单独接管应用过程。
|
||||
void applyResolvedLayoutRect(const StellarX::ResolvedLayoutRect& rect) override;
|
||||
private:
|
||||
//用来检查对话框是否模态,此控件不做实现
|
||||
bool model() const override { return false; };
|
||||
public:
|
||||
StellarX::ControlText textStyle; // 文本样式
|
||||
// 修改表格运行态 X,并同步内部页脚元素位置
|
||||
void setX(int x) override;
|
||||
// 修改表格运行态 Y,并同步内部页脚元素位置
|
||||
void setY(int y) override;
|
||||
// 修改表格运行态宽度;当前支持横向拉伸
|
||||
void setWidth(int width) override;
|
||||
// 修改表格运行态高度;当前版本作为行为边界明确忽略纵向 Stretch
|
||||
void setHeight(int height) override;
|
||||
public:
|
||||
// 构造一个基础表格,后续通过表头和数据初始化内容
|
||||
Table(int x, int y);
|
||||
~Table();
|
||||
|
||||
// 绘制表格
|
||||
void draw() override;
|
||||
//事件处理
|
||||
bool handleEvent(const ExMessage& msg) override;
|
||||
// 只清理分页按钮的 hover / tooltip / 临时按下态,不触发表格整体重绘。
|
||||
bool clearTransientMouseState() override;
|
||||
// Table 重绘时会一并绘制页码 Label 和分页按钮,coverage 需要把这些内部绘制单元并入。
|
||||
RECT getManagedRepaintCoverageRect() const override;
|
||||
// 持久 coverage 排除分页按钮 Tooltip 等临时浮层。
|
||||
RECT getManagedRepaintPersistentCoverageRect() const override;
|
||||
|
||||
// 设置表头
|
||||
void setHeaders(std::initializer_list<std::string> headers);
|
||||
// 设置表格数据(单行追加)
|
||||
void setData(std::vector<std::string> data);
|
||||
// 设置表格数据(多行追加);如需覆盖请先 clearData() 或 resetTable()
|
||||
void setData(std::initializer_list<std::vector<std::string>> data);
|
||||
//设置每页显示的行数
|
||||
void setRowsPerPage(int rows);
|
||||
//设置是否显示翻页按钮
|
||||
void showPageButton(bool isShow);
|
||||
//设置表格边框颜色
|
||||
void setTableBorder(COLORREF color);
|
||||
//设置表格背景颜色
|
||||
void setTableBk(COLORREF color);
|
||||
//设置填充模式
|
||||
void setTableFillMode(StellarX::FillMode mode);
|
||||
//设置线型
|
||||
void setTableLineStyle(StellarX::LineStyle style);
|
||||
//设置边框宽度
|
||||
void setTableBorderWidth(int width);
|
||||
//清空表头
|
||||
void clearHeaders();
|
||||
//清空表格数据
|
||||
void clearData();
|
||||
//清空表头和数据
|
||||
void resetTable();
|
||||
//窗口变化丢快照+标脏
|
||||
void onWindowResize() override;
|
||||
|
||||
//************************** 获取属性 *****************************/
|
||||
|
||||
//获取当前页码
|
||||
int getCurrentPage() const;
|
||||
//获取总页数
|
||||
int getTotalPages() const;
|
||||
//获取每页显示的行数
|
||||
int getRowsPerPage() const;
|
||||
//获取是否显示翻页按钮
|
||||
bool getShowPageButton() const;
|
||||
//获取表格边框颜色
|
||||
COLORREF getTableBorder() const;
|
||||
//获取表格背景颜色
|
||||
COLORREF getTableBk() const;
|
||||
//获取填充模式
|
||||
StellarX::FillMode getTableFillMode() const;
|
||||
//获取线型
|
||||
StellarX::LineStyle getTableLineStyle() const;
|
||||
//获取表头
|
||||
std::vector<std::string> getHeaders() const;
|
||||
//获取表格数据
|
||||
std::vector<std::vector<std::string>> getData() const;
|
||||
//获取表格边框宽度
|
||||
int getTableBorderWidth() const;
|
||||
//获取表格尺寸
|
||||
int getTableWidth() const;
|
||||
int getTableHeight() const;
|
||||
};
|
||||
-344
@@ -1,344 +0,0 @@
|
||||
// TextBox.cpp
|
||||
#include "TextBox.h"
|
||||
#include "SxLog.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
int SxTextBoxMbcCharLen(const std::string& s, size_t i)
|
||||
{
|
||||
unsigned char b = static_cast<unsigned char>(s[i]);
|
||||
if (b <= 0x7F)
|
||||
return 1;
|
||||
|
||||
if (b >= 0x81 && b <= 0xFE && i + 1 < s.size())
|
||||
{
|
||||
unsigned char b2 = static_cast<unsigned char>(s[i + 1]);
|
||||
if (b2 >= 0x40 && b2 <= 0xFE && b2 != 0x7F)
|
||||
return 2;
|
||||
}
|
||||
|
||||
// 非法或不完整字节按单字节容错,避免死循环。
|
||||
return 1;
|
||||
}
|
||||
|
||||
void SxTextBoxTrimTrailingSpaces(std::string& s)
|
||||
{
|
||||
while (!s.empty() && s.back() == ' ')
|
||||
s.pop_back();
|
||||
|
||||
while (s.size() >= 2)
|
||||
{
|
||||
unsigned char a = static_cast<unsigned char>(s[s.size() - 2]);
|
||||
unsigned char b = static_cast<unsigned char>(s[s.size() - 1]);
|
||||
if (a == 0xA1 && b == 0xA1)
|
||||
s.resize(s.size() - 2);
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string SxTextBoxTruncateByMbcBoundary(const std::string& text, size_t maxBytes)
|
||||
{
|
||||
size_t i = 0;
|
||||
size_t lastSafe = 0;
|
||||
while (i < text.size())
|
||||
{
|
||||
const int charLen = SxTextBoxMbcCharLen(text, i);
|
||||
const size_t next = i + static_cast<size_t>(charLen);
|
||||
if (next > maxBytes || next > text.size())
|
||||
break;
|
||||
lastSafe = next;
|
||||
i = next;
|
||||
}
|
||||
return text.substr(0, lastSafe);
|
||||
}
|
||||
|
||||
std::string SxTextBoxEllipsizeMbc(const std::string& text, int maxWidth)
|
||||
{
|
||||
if (maxWidth <= 0)
|
||||
return "";
|
||||
if (textwidth(LPCTSTR(text.c_str())) <= maxWidth)
|
||||
return text;
|
||||
|
||||
const std::string ellipsis = "...";
|
||||
const int ellipsisWidth = textwidth(LPCTSTR(ellipsis.c_str()));
|
||||
if (ellipsisWidth > maxWidth)
|
||||
{
|
||||
std::string clippedEllipsis = ellipsis;
|
||||
while (!clippedEllipsis.empty() && textwidth(LPCTSTR(clippedEllipsis.c_str())) > maxWidth)
|
||||
clippedEllipsis.pop_back();
|
||||
return clippedEllipsis;
|
||||
}
|
||||
|
||||
const int contentLimit = maxWidth - ellipsisWidth;
|
||||
size_t i = 0;
|
||||
size_t lastFit = 0;
|
||||
while (i < text.size())
|
||||
{
|
||||
const int charLen = SxTextBoxMbcCharLen(text, i);
|
||||
const size_t next = i + static_cast<size_t>(charLen);
|
||||
if (next > text.size())
|
||||
break;
|
||||
|
||||
const std::string candidate = text.substr(0, next);
|
||||
if (textwidth(LPCTSTR(candidate.c_str())) <= contentLimit)
|
||||
{
|
||||
lastFit = next;
|
||||
i = next;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastFit == 0)
|
||||
return ellipsis;
|
||||
|
||||
std::string head = text.substr(0, lastFit);
|
||||
SxTextBoxTrimTrailingSpaces(head);
|
||||
return head + ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
TextBox::TextBox(int x, int y, int width, int height, std::string text, StellarX::TextBoxmode mode, StellarX::ControlShape shape)
|
||||
:Control(x, y, width, height), text(text), mode(mode), shape(shape)
|
||||
{
|
||||
this->id = "TextBox";
|
||||
// TextBox 当前阶段的默认布局策略:
|
||||
// - 水平方向允许通过锚点语义拉伸
|
||||
// - 垂直方向当前实现保持固定尺寸,不在本轮引入随高度变化的视觉自适应
|
||||
this->layoutCapability.allowStretchX = true;
|
||||
this->layoutCapability.allowStretchY = false;
|
||||
}
|
||||
|
||||
void TextBox::draw()
|
||||
{
|
||||
if (dirty && show)
|
||||
{
|
||||
saveStyle();
|
||||
setfillcolor(textBoxBkClor);
|
||||
setlinecolor(textBoxBorderClor);
|
||||
StellarX::ControlText drawStyle = textStyle;
|
||||
if (drawStyle.nHeight > height)
|
||||
drawStyle.nHeight = height;
|
||||
if (drawStyle.nWidth > width)
|
||||
drawStyle.nWidth = width;
|
||||
settextstyle(drawStyle.nHeight, drawStyle.nWidth, drawStyle.lpszFace,
|
||||
drawStyle.nEscapement, drawStyle.nOrientation, drawStyle.nWeight,
|
||||
drawStyle.bItalic, drawStyle.bUnderline, drawStyle.bStrikeOut);
|
||||
|
||||
settextcolor(drawStyle.color);
|
||||
setbkmode(TRANSPARENT);
|
||||
|
||||
int text_width = 0;
|
||||
int text_height = 0;
|
||||
std::string pwdText;
|
||||
std::string displayText; // 用于显示的文本(可能被截断)
|
||||
|
||||
if (StellarX::TextBoxmode::PASSWORD_MODE == mode)
|
||||
{
|
||||
for (size_t i = 0; i < text.size(); ++i)
|
||||
pwdText += '*';
|
||||
displayText = pwdText;
|
||||
}
|
||||
else
|
||||
{
|
||||
displayText = text;
|
||||
}
|
||||
|
||||
// 计算可用宽度(留出左右边距)
|
||||
int availableWidth = width - 20; // 左右各10像素边距
|
||||
|
||||
// 截断文本以适应可用宽度
|
||||
int currentWidth = textwidth(LPCTSTR(displayText.c_str()));
|
||||
if (currentWidth > availableWidth && availableWidth > 0)
|
||||
{
|
||||
// 按 GBK/MBCS 字符边界裁切,避免中文文本被截断到半个字节。
|
||||
displayText = SxTextBoxEllipsizeMbc(displayText, availableWidth);
|
||||
currentWidth = textwidth(LPCTSTR(displayText.c_str()));
|
||||
}
|
||||
|
||||
text_width = currentWidth;
|
||||
text_height = textheight(LPCTSTR(displayText.c_str()));
|
||||
|
||||
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage)
|
||||
saveBackground(this->x, this->y, this->width, this->height);
|
||||
// 恢复背景(清除旧内容)
|
||||
restBackground();
|
||||
//根据形状绘制
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
fillrectangle(x, y, x + width, y + height);//有边框填充矩形
|
||||
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
solidrectangle(x, y, x + width, y + height);//无边框填充矩形
|
||||
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//有边框填充圆角矩形
|
||||
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//无边框填充圆角矩形
|
||||
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str()));
|
||||
break;
|
||||
}
|
||||
restoreStyle();
|
||||
dirty = false; //标记不需要重绘
|
||||
}
|
||||
}
|
||||
|
||||
bool TextBox::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
if (!show) return false;
|
||||
resetEventVisualChanged();
|
||||
|
||||
bool hover = false;
|
||||
bool oldClick = click;
|
||||
bool consume = false;
|
||||
const bool isMouseMessage =
|
||||
msg.message == WM_MOUSEMOVE ||
|
||||
msg.message == WM_LBUTTONDOWN ||
|
||||
msg.message == WM_LBUTTONUP;
|
||||
|
||||
if (isMouseMessage)
|
||||
{
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
hover = (msg.x > x && msg.x < (x + width) && msg.y > y && msg.y < (y + height));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hover && msg.message == WM_LBUTTONUP)
|
||||
{
|
||||
click = true;
|
||||
|
||||
const size_t oldLen = text.size();
|
||||
SX_LOGI("TextBox") << SX_T("激活:id=","activate: id=") << id << " mode=" << (int)mode << " oldLen=" << oldLen;
|
||||
|
||||
if (StellarX::TextBoxmode::INPUT_MODE == mode)
|
||||
{
|
||||
std::vector<char> temp(maxCharLen + 1, '\0');
|
||||
dirty = InputBox(temp.data(), (int)maxCharLen + 1, "输入框", NULL, text.c_str(), NULL, NULL, false);
|
||||
if (dirty) text = temp.data();
|
||||
consume = true;
|
||||
}
|
||||
else if (StellarX::TextBoxmode::READONLY_MODE == mode)
|
||||
{
|
||||
dirty = false;
|
||||
InputBox(NULL, (int)maxCharLen, "输出框(输入无效!)", NULL, text.c_str(), NULL, NULL, false);
|
||||
consume = true;
|
||||
}
|
||||
else if (StellarX::TextBoxmode::PASSWORD_MODE == mode)
|
||||
{
|
||||
std::vector<char> temp(maxCharLen + 1, '\0');
|
||||
// 不记录明文,只记录长度变化
|
||||
dirty = InputBox(temp.data(), (int)maxCharLen + 1, "输入框\n不可见输入,覆盖即可", NULL, NULL, NULL, NULL, false);
|
||||
if (dirty) text = temp.data();
|
||||
consume = true;
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
{
|
||||
SX_LOGI("TextBox") << SX_T("文本已更改: id=","text changed: id=") << id
|
||||
<< " oldLen=" << oldLen << " newLen=" << text.size();
|
||||
}
|
||||
else
|
||||
{
|
||||
SX_LOGD("TextBox") << SX_T("文本无变化:id=","no change: id=") << id;
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
requestRepaint(parent);
|
||||
markEventVisualChanged(dirty);
|
||||
|
||||
if (click)
|
||||
click = false;
|
||||
|
||||
return consume;
|
||||
}
|
||||
|
||||
|
||||
void TextBox::setMode(StellarX::TextBoxmode mode)
|
||||
{
|
||||
this->mode = mode;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void TextBox::setMaxCharLen(size_t len)
|
||||
{
|
||||
if (len > 0)
|
||||
maxCharLen = len;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void TextBox::setTextBoxshape(StellarX::ControlShape shape)
|
||||
{
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
this->shape = shape;
|
||||
this->dirty = true;
|
||||
break;
|
||||
case StellarX::ControlShape::CIRCLE:
|
||||
case StellarX::ControlShape::B_CIRCLE:
|
||||
case StellarX::ControlShape::ELLIPSE:
|
||||
case StellarX::ControlShape::B_ELLIPSE:
|
||||
this->shape = StellarX::ControlShape::RECTANGLE;
|
||||
this->dirty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TextBox::setTextBoxBorder(COLORREF color)
|
||||
{
|
||||
textBoxBorderClor = color;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void TextBox::setTextBoxBk(COLORREF color)
|
||||
{
|
||||
textBoxBkClor = color;
|
||||
this->dirty = true;
|
||||
}
|
||||
|
||||
void TextBox::setText(std::string text)
|
||||
{
|
||||
if(text == this->text)
|
||||
return; // 文本未改变,无需更新和重绘
|
||||
if (text.size() > maxCharLen)
|
||||
text = SxTextBoxTruncateByMbcBoundary(text, maxCharLen);
|
||||
this->text = text;
|
||||
this->dirty = true; // 标记需要重绘,不论是否窗口图形上下文是否已初始化,等第一次绘制时由窗口真正调用 draw() 来重绘显示文本
|
||||
|
||||
//有父控件时请求父控件重绘,无父控件时直接重绘,确保文本更新后界面正确刷新显示
|
||||
if (nullptr != parent)
|
||||
{
|
||||
//通过hasSnap是否持有有效快照,判断控件是否已经绘制过,避免在控件未绘制前/窗口图形上下文未初始化调用draw()导致的错误
|
||||
if (hasSnap)
|
||||
requestRepaint(parent);
|
||||
}
|
||||
else
|
||||
if (hasSnap)
|
||||
draw();
|
||||
|
||||
}
|
||||
|
||||
std::string TextBox::getText() const
|
||||
{
|
||||
return this->text;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* @类: TextBox
|
||||
* @摘要: 文本框控件,支持输入和只读两种模式
|
||||
* @描述:
|
||||
* 提供文本输入和显示功能,集成EasyX的InputBox用于数据输入。
|
||||
* 支持有限的形状样式和视觉定制。
|
||||
*
|
||||
* @特性:
|
||||
* - 两种工作模式:输入模式和只读模式
|
||||
* - 最大字符长度限制
|
||||
* - 集成系统输入框简化文本输入
|
||||
* - 支持四种矩形形状变体
|
||||
*
|
||||
* @使用场景: 数据输入、文本显示、表单字段等
|
||||
* @所属框架: 星垣(StellarX) GUI框架
|
||||
* @作者: 我在人间做废物
|
||||
******************************************************************************/
|
||||
#pragma once
|
||||
#include "Control.h"
|
||||
|
||||
class TextBox : public Control
|
||||
{
|
||||
std::string text; //文本
|
||||
StellarX::TextBoxmode mode; //模式
|
||||
StellarX::ControlShape shape; //形状
|
||||
bool click = false; //是否点击
|
||||
size_t maxCharLen = 10;//最大字符长度
|
||||
COLORREF textBoxBkClor = RGB(255, 255, 255); //背景颜色
|
||||
COLORREF textBoxBorderClor = RGB(0, 0, 0); //边框颜色
|
||||
|
||||
public:
|
||||
StellarX::ControlText textStyle; //文本样式
|
||||
|
||||
// 构造文本框;支持输入模式和只读模式
|
||||
TextBox(int x, int y, int width, int height, std::string text = "", StellarX::TextBoxmode mode = StellarX::TextBoxmode::INPUT_MODE, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
|
||||
// 绘制文本框
|
||||
void draw() override;
|
||||
// 处理输入框鼠标交互和文本录入
|
||||
bool handleEvent(const ExMessage& msg) override;
|
||||
// 设置文本框模式
|
||||
void setMode(StellarX::TextBoxmode mode);
|
||||
// 设置可输入最大长度;当前按 std::string 字节长度限制,不等同于 Unicode 字符数
|
||||
void setMaxCharLen(size_t len);
|
||||
// 设置文本框形状;仅支持矩形/圆角矩形,圆形和椭圆会回退为 RECTANGLE
|
||||
void setTextBoxshape(StellarX::ControlShape shape);
|
||||
// 设置边框颜色
|
||||
void setTextBoxBorder(COLORREF color);
|
||||
// 设置背景颜色
|
||||
void setTextBoxBk(COLORREF color);
|
||||
// 设置当前文本
|
||||
void setText(std::string text);
|
||||
|
||||
// 获取当前文本
|
||||
std::string getText() const;
|
||||
|
||||
private:
|
||||
//用来检查对话框是否模态,此控件不做实现
|
||||
bool model() const override { return false; };
|
||||
};
|
||||
-1261
File diff suppressed because it is too large
Load Diff
@@ -1,144 +0,0 @@
|
||||
/**
|
||||
* Window(头文件)
|
||||
*
|
||||
* 设计目标:
|
||||
* - 提供一个基于 Win32 + EasyX 的“可拉伸且稳定不抖”的窗口容器。
|
||||
* - 通过消息过程子类化(WndProcThunk)接管关键消息(WM_SIZING/WM_SIZE/...)。
|
||||
* - 将“几何变化记录(pendingW/H)”与“统一收口重绘(needResizeDirty)”解耦。
|
||||
* - 在事件分发阶段只改状态并登记重绘 root,在事件尾统一提交,减少分离绘制造成的闪烁。
|
||||
*
|
||||
* 关键点(与 .cpp 中实现对应):
|
||||
* - isSizing:处于交互拉伸阶段时,冻结重绘;松手后统一重绘,防止抖动。
|
||||
* - WM_SIZING:只做“最小尺寸夹紧”,不回滚矩形、不做对齐;把其余交给系统。
|
||||
* - WM_GETMINMAXINFO:按最小“客户区”换算到“窗口矩形”,提供系统层最小轨迹值。
|
||||
* - runEventLoop:输入事件先分发给对话框/控件,控件只登记重绘请求;真正绘制在分发或 resize 收口时统一处理。
|
||||
*/
|
||||
//fuck windows
|
||||
//fuck win32
|
||||
//fuck xiaomi
|
||||
#pragma once
|
||||
|
||||
#include "Control.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <windows.h>
|
||||
|
||||
class Window
|
||||
{
|
||||
// —— 尺寸状态 ——(绘制尺寸与待应用尺寸分离;收口时一次性更新)
|
||||
int width; // 当前有效宽(已应用到画布/控件的客户区宽)
|
||||
int height; // 当前有效高(已应用到画布/控件的客户区高)
|
||||
int localwidth; // 基准宽(创建时的宽度)
|
||||
int localheight; // 基准高(创建是的高度)
|
||||
int pendingW; // 待应用宽(WM_SIZE/拉伸中记录用)
|
||||
int pendingH; // 待应用高
|
||||
int minClientW; // 业务设定的最小客户区宽(用于 GETMINMAXINFO 与 SIZING 夹紧)
|
||||
int minClientH; // 业务设定的最小客户区高
|
||||
int windowMode = NULL; // EasyX 初始化模式(EX_SHOWCONSOLE/EX_TOPMOST/...)
|
||||
bool needResizeDirty = false; // 统一收口重绘标志(置位后在事件环末尾处理)
|
||||
bool isSizing = false; // 是否处于拖拽阶段(ENTER/EXIT SIZEMOVE 切换)
|
||||
|
||||
// —— 原生窗口句柄与子类化钩子 ——(子类化 EasyX 的窗口过程以拦截关键消息)
|
||||
HWND hWnd = NULL; // EasyX 初始化后的窗口句柄
|
||||
WNDPROC oldWndProc = nullptr; // 保存旧过程(CallWindowProc 回落)
|
||||
bool procHooked = false; // 避免重复子类化
|
||||
static LRESULT CALLBACK WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l); // 静态过程分发到 this
|
||||
|
||||
// —— 绘制相关 ——(是否使用合成双缓冲、窗口标题、背景等)
|
||||
bool useComposited = true; // 是否启用 WS_EX_COMPOSITED(部分机器可能增加一帧观感延迟)
|
||||
std::string headline; // 窗口标题文本
|
||||
COLORREF wBkcolor = BLACK; // 纯色背景(无背景图时使用)
|
||||
std::unique_ptr<IMAGE> background; // 背景图对象指针(存在时优先绘制)
|
||||
std::string bkImageFile; // 背景图文件路径(loadimage 用)
|
||||
|
||||
// —— 控件/对话框 ——(容器内的普通控件与非模态对话框)
|
||||
std::vector<std::unique_ptr<Control>> controls; // 普通顶层控件;绘制顺序也决定层级顺序
|
||||
std::vector<std::unique_ptr<Control>> dialogs; // 非模态对话框;始终位于普通控件之上
|
||||
bool managedDispatchActive = false; // 事件分发期:控件只改状态并登记重绘,不立即画
|
||||
bool managedSceneDirty = false; // 当前分发轮次是否已经登记了至少一笔托管重绘请求
|
||||
struct ManagedRepaintItem // 托管重绘项:记录由哪个控件发起、需要重绘的根控件和覆盖范围(用于后续判断哪些对话框需要补画)
|
||||
{
|
||||
Control* root = nullptr; // 顶层重绘根(直接挂在 Window 下的控件,或 Dialog 自身)
|
||||
RECT coverage{}; // 本轮脏区覆盖范围;用于判断哪些上层 Dialog 需要补画
|
||||
};
|
||||
std::vector<ManagedRepaintItem> managedRepaintItems; // 本轮事件分发累计的重绘项
|
||||
|
||||
public:
|
||||
bool dialogClose = false; // 项目内使用的状态位,对话框关闭标志
|
||||
mutable bool dialogOpen = false; // 项目内使用的状态位,对话框打开标志
|
||||
|
||||
// —— 构造/析构 ——(仅初始化成员;实际样式与子类化在 draw() 中完成)
|
||||
// 创建纯色背景窗口
|
||||
Window(int width, int height, int mode);
|
||||
// 创建指定背景色窗口
|
||||
Window(int width, int height, int mode, COLORREF bkcloc);
|
||||
// 创建带标题与背景色的窗口
|
||||
Window(int width, int height, int mode, COLORREF bkcloc, std::string headline);
|
||||
// 析构窗口并释放背景、控件与子类化资源
|
||||
~Window();
|
||||
|
||||
// —— 绘制与事件循环 ——(draw* 完成一次全量绘制;runEventLoop 驱动事件与统一收口)
|
||||
void draw(); // 以纯色背景执行一次全量绘制
|
||||
void draw(std::string pImgFile); // 以背景图执行一次全量绘制
|
||||
int runEventLoop(); // 主事件循环(PeekMessage + 统一收口重绘)
|
||||
|
||||
// —— 背景/标题设置 ——(更换背景、背景色与标题;立即触发一次批量绘制)
|
||||
void setBkImage(std::string pImgFile); // 设置窗口背景图
|
||||
void setBkcolor(COLORREF c); // 设置窗口纯色背景
|
||||
void setHeadline(std::string headline); // 设置窗口标题
|
||||
|
||||
// —— 控件/对话框管理 ——(添加到容器,或做存在性判断)
|
||||
void addControl(std::unique_ptr<Control> control); // 添加普通顶层控件
|
||||
void addDialog(std::unique_ptr<Control> dialogs); // 添加非模态对话框
|
||||
bool hasNonModalDialogWithCaption(const std::string& caption, const std::string& message) const; // 检查是否已有同 caption/message 的非模态对话框
|
||||
|
||||
// —— 访问器 ——(只读接口,供外部查询当前窗口/标题/背景等)
|
||||
HWND getHwnd() const; // 获取窗口句柄
|
||||
int getWidth() const; // 获取当前有效客户区宽度
|
||||
int getHeight() const; // 获取当前有效客户区高度
|
||||
int getPendingWidth() const; // 获取待应用宽度
|
||||
int getPendingHeight() const; // 获取待应用高度
|
||||
std::string getHeadline() const; // 获取窗口标题
|
||||
COLORREF getBkcolor() const; // 获取纯色背景
|
||||
IMAGE* getBkImage() const; // 获取背景图对象
|
||||
std::string getBkImageFile() const; // 获取背景图路径
|
||||
std::vector<std::unique_ptr<Control>>& getControls(); // 获取普通顶层控件列表
|
||||
|
||||
// —— 尺寸调整 / 托管重绘 ——(事件阶段登记,收口阶段提交)
|
||||
|
||||
// 执行一次 resize 收口 + 统一重绘
|
||||
void pumpResizeIfNeeded();
|
||||
// 供模态 Dialog 上报宿主窗口尺寸变化
|
||||
void scheduleResizeFromModal(int w, int h);
|
||||
// 当前是否处于“事件只改状态,不立即画”的阶段
|
||||
bool isManagedDispatchActive() const;
|
||||
// 记录一笔由 source 发起的托管重绘请求
|
||||
void requestManagedRepaint(Control* source);
|
||||
// 记录一笔托管重绘请求,并显式并入状态清理前的旧覆盖范围
|
||||
void requestManagedRepaint(Control* source, const RECT& previousCoverage);
|
||||
// 在事件收口阶段提交本轮登记的 root 重绘
|
||||
void flushManagedRepaint();
|
||||
private:
|
||||
void adaptiveLayout(std::unique_ptr<Control>& c, const int finalH, const int finalW);
|
||||
// resize / 初次绘制 / 对话框开关这类全局场景的整场景重绘
|
||||
void redrawScene(bool forceControlsDirty, bool forceDialogsDirty);
|
||||
void drawWindowBackground();
|
||||
// 合成 WM_MOUSEMOVE,用于同步底层 hover 状态
|
||||
void dispatchSyntheticMouseMoveToControls(short x, short y);
|
||||
enum class OverlayMouseSyncMode
|
||||
{
|
||||
ClearBehindOverlay, // overlay 吞掉鼠标移动:只清理底层 hover / tooltip,不重新命中底层控件
|
||||
RestoreAtCursor // overlay 关闭:按当前鼠标位置刷新底层 hover 状态
|
||||
};
|
||||
// overlay 层级变化后,同步底层鼠标瞬时状态;内部仍复用既有 synthetic WM_MOUSEMOVE 路径
|
||||
void syncMouseStateAfterOverlayChanged(OverlayMouseSyncMode mode);
|
||||
// 在事件安全点清理已关闭的非模态 Dialog,避免 dialogs 容器长期积累无效对象
|
||||
void sweepClosedDialogs();
|
||||
// 事件回调可能改脏其他 root;flush 前补收集这些跨 root 脏子树
|
||||
void collectDirtyRootsForManagedRepaint();
|
||||
// 清空本轮托管重绘登记
|
||||
void clearManagedRepaintState();
|
||||
// 找出需要补画到最上层的对话框
|
||||
void collectManagedDialogOverlays(Control* repaintRoot, const RECT& coverage, std::vector<Control*>& overlays);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
# 贡献者公约行为准则
|
||||
|
||||
## 我们的承诺
|
||||
|
||||
为了营造一个开放和受欢迎的环境,我们作为贡献者和维护者承诺:无论年龄、体型、身体健全与否、民族、性征、性别认同与表达、经验水平、教育程度、社会地位、国籍、个人外貌、种族、宗教信仰、性取向,我们的项目和社区对每一个人都不存在歧视。
|
||||
|
||||
## 我们的标准
|
||||
|
||||
有助于为我们社区创造积极环境的行为包括但不限于:
|
||||
|
||||
* 对他人的体贴和尊重
|
||||
* 尊重不同的观点和经历
|
||||
* 优雅地接受建设性批评
|
||||
* 关注对社区最有利的事情
|
||||
* 对其他社区成员表现出同理心
|
||||
|
||||
......
|
||||
|
||||
## 执行
|
||||
|
||||
虐待、骚扰或其他不可接受行为的实例可通过项目维护团队进行报告。所有投诉都将被认真、迅速地审查和调查,并将得出被认为在情况下必要和适当的回应。
|
||||
|
||||
......
|
||||
|
||||
有关此行为准则的常见问题解答,请参阅 https://www.contributor-covenant.org/faq。翻译版本可在 https://www.contributor-covenant.org/translations 获取。
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @file demo.cpp
|
||||
* @brief 一个简单的演示程序,展示 StellarX GUI 框架的基本用法。
|
||||
* @description 创建一个带有按钮的窗口,点击按钮会改变其文本。
|
||||
*/
|
||||
|
||||
#include <StellarX/StellarX.h>
|
||||
#include <iostream>
|
||||
|
||||
int main()
|
||||
{
|
||||
// 创建一个窗口 (Windows平台)
|
||||
Window mainWindow(800, 600, NULL, RGB(240, 240, 240), "StellarX 演示程序");
|
||||
|
||||
// 创建一个按钮
|
||||
auto myButton = std::make_unique<Button>(300, 250, 200, 80, "点击我!", StellarX::ButtonMode::NORMAL, StellarX::ControlShape::ROUND_RECTANGLE);
|
||||
|
||||
// 为按钮点击事件设置一个回调函数
|
||||
myButton->setOnClickListener([&myButton]() {
|
||||
std::cout << "按钮被点击了!" << std::endl;
|
||||
// 点击后改变按钮文本作为视觉反馈
|
||||
static bool 已切换 = false;
|
||||
if (已切换) {
|
||||
myButton->setButtonText("点击我!");
|
||||
}
|
||||
else {
|
||||
myButton->setButtonText("被点过了!");
|
||||
}
|
||||
已切换 = !已切换;
|
||||
});
|
||||
|
||||
// 将按钮添加到窗口
|
||||
mainWindow.addControl(std::move(myButton));
|
||||
|
||||
// 绘制窗口并运行事件循环 (Windows消息循环)
|
||||
mainWindow.draw();
|
||||
mainWindow.runEventLoop();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.13.35828.75 d17.13
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "imGui-easyX", "imGui-easyX.vcxproj", "{FD33B55B-FD97-4087-8B95-6BF8285AAE73}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Debug|x64.Build.0 = Debug|x64
|
||||
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Debug|x86.Build.0 = Debug|Win32
|
||||
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x64.ActiveCfg = Release|x64
|
||||
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x64.Build.0 = Release|x64
|
||||
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x86.ActiveCfg = Release|Win32
|
||||
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x86.Build.0 = Release|Win32
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {8E5EB082-F5AD-4E8A-8809-29A47F920B2B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,174 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Button.h" />
|
||||
<ClInclude Include="Canvas.h" />
|
||||
<ClInclude Include="Control.h" />
|
||||
<ClInclude Include="CoreTypes.h" />
|
||||
<ClInclude Include="Dialog.h" />
|
||||
<ClInclude Include="label.h" />
|
||||
<ClInclude Include="MessageBox.h" />
|
||||
<ClInclude Include="StellarX.h" />
|
||||
<ClInclude Include="SxLog.h" />
|
||||
<ClInclude Include="TabControl.h" />
|
||||
<ClInclude Include="table.h" />
|
||||
<ClInclude Include="textBox.h" />
|
||||
<ClInclude Include="window.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Button.cpp" />
|
||||
<ClCompile Include="Canvas.cpp" />
|
||||
<ClCompile Include="Control.cpp" />
|
||||
<ClCompile Include="Dialog.cpp" />
|
||||
<ClCompile Include="label.cpp" />
|
||||
<ClCompile Include="MessageBox.cpp" />
|
||||
<ClCompile Include="SxLog.cpp" />
|
||||
<ClCompile Include="TabControl.cpp" />
|
||||
<ClCompile Include="table.cpp" />
|
||||
<ClCompile Include="textBox.cpp" />
|
||||
<ClCompile Include="window.cpp" />
|
||||
<ClCompile Include="z-testDome.cpp" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{fd33b55b-fd97-4087-8b95-6bf8285aae73}</ProjectGuid>
|
||||
<RootNamespace>imGuieasyX</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>MultiByte</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<PreprocessorDefinitions>
|
||||
</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<PrecompiledHeaderFile />
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<PreprocessorDefinitions>
|
||||
</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<PrecompiledHeaderFile />
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<PreprocessorDefinitions>
|
||||
</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<PrecompiledHeaderFile />
|
||||
<LanguageStandard_C>stdc17</LanguageStandard_C>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>false</SDLCheck>
|
||||
<PreprocessorDefinitions>
|
||||
</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<PrecompiledHeaderFile />
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -1,96 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="源文件">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="头文件">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="资源文件">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="Control.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Button.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="window.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="label.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="textBox.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Canvas.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="table.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="StellarX.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="CoreTypes.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="Dialog.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="MessageBox.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="TabControl.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="SxLog.h">
|
||||
<Filter>头文件</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Control.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Button.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="z-testDome.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="window.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="label.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="textBox.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Canvas.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="table.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="Dialog.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="MessageBox.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="TabControl.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="SxLog.cpp">
|
||||
<Filter>源文件</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<ShowAllFiles>false</ShowAllFiles>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,146 @@
|
||||
/*******************************************************************************
|
||||
* @文件: Button.h
|
||||
* @摘要: 按钮控件类
|
||||
* @描述:
|
||||
* 提供多种样式和行为的按钮控件,支持点击、悬停、切换等状态。
|
||||
* 继承自Control基类,是框架的核心交互组件之一。
|
||||
*
|
||||
* @所属框架: 星垣(StellarX) GUI框架
|
||||
* @作者: 我在人间做废物
|
||||
* @日期: September 2025
|
||||
******************************************************************************/
|
||||
#pragma once
|
||||
#include "Control.h"
|
||||
|
||||
class Button : public Control
|
||||
{
|
||||
|
||||
std::string text; // 按钮上的文字
|
||||
bool click; // 是否被点击
|
||||
bool hover; // 是否被悬停
|
||||
bool dirty = true; // 是否重绘
|
||||
|
||||
COLORREF buttonTrueColor; // 按钮被点击后的颜色
|
||||
COLORREF buttonFalseColor; // 按钮未被点击的颜色
|
||||
COLORREF buttonHoverColor; // 按钮被鼠标悬停的颜色
|
||||
COLORREF buttonBorderColor = RGB(0,0,0);// 按钮边框颜色
|
||||
|
||||
StellarX::ButtonMode mode; // 按钮模式
|
||||
StellarX::ControlShape shape; // 按钮形状
|
||||
|
||||
int buttonFillMode = BS_SOLID; //按钮填充模式
|
||||
StellarX::FillStyle buttonFillIma = StellarX::FillStyle::BDiagonal; //按钮填充图案
|
||||
IMAGE* buttonFileIMAGE = nullptr; //按钮填充图像
|
||||
|
||||
|
||||
|
||||
std::function<void()> onClickCallback; //回调函数
|
||||
std::function<void()> onToggleOnCallback; //TOGGLE模式下的回调函数
|
||||
std::function<void()> onToggleOffCallback; //TOGGLE模式下的回调函数
|
||||
|
||||
StellarX::ControlText oldStyle = textStyle; // 按钮文字样式
|
||||
int oldtext_width = -1;
|
||||
int oldtext_height = -1;
|
||||
int text_width = 0;
|
||||
int text_height = 0;
|
||||
public:
|
||||
StellarX::ControlText textStyle; // 按钮文字样式
|
||||
|
||||
public:
|
||||
/*************************************************************************/
|
||||
/********************************构造函数*********************************/
|
||||
/*************************************************************************/
|
||||
|
||||
//默认按钮颜色
|
||||
Button(int x, int y, int width, int height, const std::string text,
|
||||
StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
|
||||
//自定义按钮未被点击和被点击颜色
|
||||
Button(int x, int y, int width, int height, const std::string text,
|
||||
COLORREF ct, COLORREF cf, StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL,
|
||||
StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
|
||||
//自定义按钮颜色和悬停颜色
|
||||
Button(int x, int y, int width, int height, const std::string text,
|
||||
COLORREF ct, COLORREF cf,COLORREF ch,
|
||||
StellarX::ButtonMode mode = StellarX::ButtonMode::NORMAL, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
|
||||
//析构函数 释放图形指针内存
|
||||
~Button();
|
||||
|
||||
/*************************************************************************/
|
||||
/********************************Set方法**********************************/
|
||||
/*************************************************************************/
|
||||
//绘制按钮
|
||||
void draw() override;
|
||||
//按钮事件处理
|
||||
|
||||
void handleEvent(const ExMessage& msg) override;
|
||||
|
||||
//设置回调函数
|
||||
//************************************
|
||||
// 名称: setOnClickListener | setOnToggleOnListener | setOnToggleOffListener
|
||||
// 全名: Button::setOnClickListener
|
||||
// 访问: public
|
||||
// 返回类型: void
|
||||
// Parameter: const std::function<> & & callback 设置回调函数 传入回调函数名即可,不需要传入(),不需要传入参数,不需要传入返回值
|
||||
// 如果要传入参数,可以使用lambda表达式
|
||||
//************************************
|
||||
void setOnClickListener(const std::function<void()>&& callback);
|
||||
//设置TOGGLE模式下被点击的回调函数
|
||||
void setOnToggleOnListener(const std::function<void()>&& callback);
|
||||
//设置TOGGLE模式下取消点击的回调函数
|
||||
void setOnToggleOffListener(const std::function<void()>&& callback);
|
||||
//设置按钮模式
|
||||
void setbuttonMode(StellarX::ButtonMode mode);
|
||||
//设置圆角矩形椭圆宽度
|
||||
int setROUND_RECTANGLEwidth(int width);
|
||||
//设置圆角矩形椭圆高度
|
||||
int setROUND_RECTANGLEheight(int height);
|
||||
//设置按钮填充模式
|
||||
void setFillMode(int mode);
|
||||
//设置按钮填充图案
|
||||
void setFillIma(StellarX::FillStyle ima);
|
||||
//设置按钮填充图像
|
||||
void setFillIma(std::string imaName);
|
||||
//设置按钮边框颜色
|
||||
void setButtonBorder(COLORREF Border);
|
||||
//设置按钮文本
|
||||
void setButtonText(const char* text);
|
||||
void setButtonText(std::string text);
|
||||
//设置按钮形状
|
||||
void setButtonShape(StellarX::ControlShape shape);
|
||||
|
||||
//判断按钮是否被点击
|
||||
bool isClicked() const;
|
||||
|
||||
/*************************************************************************/
|
||||
/********************************Get方法**********************************/
|
||||
/*************************************************************************/
|
||||
|
||||
//获取按钮文字
|
||||
std::string getButtonText() const;
|
||||
const char* getButtonText_c() const;
|
||||
//获取按钮模式
|
||||
StellarX::ButtonMode getButtonMode() const;
|
||||
//获取按钮形状
|
||||
StellarX::ControlShape getButtonShape() const;
|
||||
//获取按钮填充模式
|
||||
int getFillMode() const;
|
||||
//获取按钮填充图案
|
||||
StellarX::FillStyle getFillIma() const;
|
||||
//获取按钮填充图像
|
||||
IMAGE* getFillImaImage() const;
|
||||
//获取按钮边框颜色
|
||||
COLORREF getButtonBorder() const;
|
||||
//获取按钮文字颜色
|
||||
COLORREF getButtonTextColor() const;
|
||||
//获取按钮文字样式
|
||||
StellarX::ControlText getButtonTextStyle() const;
|
||||
|
||||
private:
|
||||
//初始化按钮
|
||||
void initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch);
|
||||
//判断鼠标是否在圆形按钮内
|
||||
bool isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius);
|
||||
//判断鼠标是否在椭圆按钮内
|
||||
bool isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
#include "Control.h"
|
||||
// 画布容器控件,可以作为其他控件的父容器
|
||||
// 功能:
|
||||
// - 包含和管理子控件
|
||||
// - 将事件传递给子控件
|
||||
// - 提供统一的背景和边框
|
||||
// 使用场景: 用于分组相关控件或实现复杂布局
|
||||
|
||||
class Canvas : public Control
|
||||
{
|
||||
private:
|
||||
std::vector<std::unique_ptr<Control>> controls;
|
||||
|
||||
StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE; //容器形状
|
||||
|
||||
StellarX::FillMode canvasFillMode = StellarX::FillMode::Solid; //容器填充模式
|
||||
StellarX::LineStyle canvasLineStyle = StellarX::LineStyle::Solid; //线型
|
||||
int canvaslinewidth = 1; //线宽
|
||||
|
||||
COLORREF canvasBorderClor = RGB(0, 0, 0);//边框颜色
|
||||
COLORREF canvasBkClor = RGB(255,255,255); //背景颜色
|
||||
|
||||
public:
|
||||
Canvas(int x, int y, int width, int height);
|
||||
~Canvas() {}
|
||||
//绘制容器及其子控件
|
||||
void draw() override;
|
||||
void handleEvent(const ExMessage& msg) override;
|
||||
//添加控件
|
||||
void addControl(std::unique_ptr<Control> control);
|
||||
|
||||
//设置容器样式
|
||||
void setShape(StellarX::ControlShape shape);
|
||||
//设置容器填充模式
|
||||
void setCanvasfillMode(StellarX::FillMode mode);
|
||||
//设置容器边框颜色
|
||||
void setBorderColor(COLORREF color);
|
||||
//设置填充颜色
|
||||
void setCanvasBkColor(COLORREF color);
|
||||
//设置线形
|
||||
void setCanvasLineStyle(StellarX::LineStyle style);
|
||||
//设置线段宽度
|
||||
void setLinewidth(int width);
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
/*********************************************************************
|
||||
* \文件: Control.h
|
||||
* \描述: 控件基类,所有控件都继承自此类。
|
||||
* 提供了控件的一些基本属性和方法。
|
||||
*
|
||||
* \作者: 我在人间做废物
|
||||
* \日期: September 2025
|
||||
*********************************************************************/
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <easyx.h>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include "CoreTypes.h"
|
||||
|
||||
class Control
|
||||
{
|
||||
protected:
|
||||
int x, y; // 左上角坐标
|
||||
int width, height; // 控件尺寸
|
||||
|
||||
StellarX::RouRectangle rouRectangleSize; // 圆角矩形椭圆宽度和高度
|
||||
|
||||
LOGFONT currentFont; // 保存当前字体样式和颜色
|
||||
COLORREF currentColor = 0;
|
||||
COLORREF currentBkColor = 0; // 保存当前填充色
|
||||
COLORREF currentBorderColor = 0; // 边框颜色
|
||||
LINESTYLE* currentLineStyle = new LINESTYLE; // 保存当前线型
|
||||
|
||||
public:
|
||||
Control(const Control&) = delete;
|
||||
Control& operator=(const Control&) = delete;
|
||||
Control(Control&&) = default;
|
||||
Control& operator=(Control&&) = default;
|
||||
|
||||
Control() : x(0), y(0), width(100), height(100) {}
|
||||
Control(int x, int y, int width, int height)
|
||||
: x(x), y(y), width(width), height(height) {
|
||||
}
|
||||
|
||||
virtual ~Control() {
|
||||
delete currentLineStyle;
|
||||
currentLineStyle = nullptr;
|
||||
}
|
||||
|
||||
// 获取位置和尺寸
|
||||
int getX() const { return x; }
|
||||
int getY() const { return y; }
|
||||
int getWidth() const { return width; }
|
||||
int getHeight() const { return height; }
|
||||
int getRight() const { return x + width; }
|
||||
int getBottom() const { return y + height; }
|
||||
|
||||
virtual void draw() = 0;
|
||||
virtual void handleEvent(const ExMessage& msg) = 0;
|
||||
|
||||
protected:
|
||||
void saveStyle();
|
||||
void restoreStyle();
|
||||
};
|
||||
@@ -0,0 +1,248 @@
|
||||
#pragma once
|
||||
/*******************************************************************************
|
||||
* @文件: CoreTypes.h
|
||||
* @摘要: 星垣框架核心类型定义文件
|
||||
* @描述:
|
||||
* 集中定义框架中使用的所有枚举类型和结构体,确保类型一致性。
|
||||
* 这是框架的类型系统基础,所有控件都依赖于此文件中定义的类型。
|
||||
*
|
||||
* @作者: 我在人间做废物
|
||||
* @日期: September 2025
|
||||
******************************************************************************/
|
||||
#include"easyX.h"
|
||||
namespace StellarX
|
||||
{
|
||||
/**
|
||||
* @枚举类名称: hatchStyle
|
||||
* @功能描述: 用来定义控件填充图案的枚举类
|
||||
*
|
||||
* @详细说明:
|
||||
* 根据此枚举类可以自定义控件填充图案
|
||||
* 可以在控件初始化时设置填充图案
|
||||
* 根据具体情况选择不同的填充图案
|
||||
* 默认填充图案为水平线
|
||||
*
|
||||
* @取值说明:
|
||||
* Horizontal - 水平线
|
||||
* Vertical - 垂直线
|
||||
* FDiagonal - 反斜线
|
||||
* BDiagonal - 正斜线
|
||||
* Cross - 十字
|
||||
* DiagCross - 网格
|
||||
*
|
||||
* @使用示例:
|
||||
* // 示例代码展示如何使用此枚举类
|
||||
* hatchStyle var = hatchStyle::Horizontal;
|
||||
*
|
||||
* @备注:
|
||||
* 此枚举类仅支持图案填充模式
|
||||
* 枚举类在使用时,需要使用::进行调用,还要注意大小写
|
||||
*/
|
||||
enum class FillStyle {
|
||||
Horizontal = HS_HORIZONTAL, // 水平线
|
||||
Vertical = HS_VERTICAL, // 垂直线
|
||||
FDiagonal = HS_FDIAGONAL, // 反斜线
|
||||
BDiagonal = HS_BDIAGONAL, // 正斜线
|
||||
Cross = HS_CROSS, // 十字
|
||||
DiagCross = HS_DIAGCROSS // 网格
|
||||
};
|
||||
/**
|
||||
* @枚举类名称: FillMode
|
||||
* @功能描述: 用来定义控件填充模式的枚举类
|
||||
*
|
||||
* @详细说明:
|
||||
* 根据此枚举类可以自定义控件填充模式
|
||||
* 可以在控件初始化时设置填充模式
|
||||
* 根据具体情况选择不同的填充模式
|
||||
* 默认填充模式为固实填充
|
||||
*
|
||||
* @取值说明:
|
||||
* Solid - 固实填充
|
||||
* Null - 不填充
|
||||
* Hatched - 图案填充
|
||||
* Pattern - 自定义图案填充
|
||||
* DibPattern - 自定义图像填充
|
||||
*
|
||||
* @使用示例:
|
||||
* // 示例代码展示如何使用此枚举类
|
||||
* FillMode var = FillMode::Solid;
|
||||
*
|
||||
* @备注:
|
||||
* 枚举类在使用时,需要使用::进行调用,还要注意大小写
|
||||
*/
|
||||
enum class FillMode
|
||||
{
|
||||
Solid = BS_SOLID, //固实填充
|
||||
Null = BS_NULL, // 不填充
|
||||
Hatched = BS_HATCHED, // 图案填充
|
||||
Pattern = BS_PATTERN, // 自定义图案填充
|
||||
DibPattern = BS_DIBPATTERN // 自定义图像填充
|
||||
};
|
||||
/**
|
||||
* @枚举类名称: linStyle
|
||||
* @功能描述: 此枚举类用来定义控件边框线型
|
||||
*
|
||||
* @详细说明:
|
||||
* 根据此枚举类可以自定义控件边框线型
|
||||
* 可以在控件初始化时设置边框线型
|
||||
* 根据具体情况选择不同的线型
|
||||
* 默认线型为实线
|
||||
*
|
||||
* @取值说明:
|
||||
* Solid // 实线
|
||||
* Dash // 虚线
|
||||
* Dot // 点线
|
||||
* DashDot // 点划线
|
||||
* DashDotDot // 双点划线
|
||||
* Null // 无线
|
||||
*
|
||||
* @使用示例:
|
||||
* // 示例代码展示如何使用此枚举类
|
||||
* LineStyle var = LineStyle::Solid;
|
||||
*
|
||||
* @备注:
|
||||
* 枚举类在使用时,需要使用::进行调用,还要注意大小写
|
||||
*/
|
||||
enum class LineStyle {
|
||||
Solid = PS_SOLID, // 实线
|
||||
Dash = PS_DASH, // 虚线
|
||||
Dot = PS_DOT, // 点线
|
||||
DashDot = PS_DASHDOT, // 点划线
|
||||
DashDotDot = PS_DASHDOTDOT, // 双点划线
|
||||
Null = PS_NULL // 无线
|
||||
};
|
||||
|
||||
/**
|
||||
* @结构体名称: ControlText
|
||||
* @功能描述: 控件字体样式 可以自定义不同的样式
|
||||
*
|
||||
* @详细说明:
|
||||
* 主要使用的场景为:需要修改或想自定义控件字体大小,字体样式,颜色等
|
||||
*
|
||||
* @成员说明:
|
||||
* int nHeight = 0; - 字体高度
|
||||
* int nWidth = 0; - 字体宽度 如果为0则自适应
|
||||
* LPCTSTR lpszFace = "宋体"; - 字体名称
|
||||
* COLORREF color = RGB(0, 0, 0); - 字体颜色
|
||||
* int nEscapement = 0; - 字符串旋转角度
|
||||
* int nOrientation = 0; - 字符旋转角度
|
||||
* int nWeight = 0; - 字体粗细 范围0~1000 0表示默认
|
||||
* bool bItalic = false; - 是否斜体
|
||||
* bool bUnderline = false; - 是否下划线
|
||||
* bool bStrikeOut = false; - 是否删除线
|
||||
|
||||
* bool operator!=(const ControlText& text);
|
||||
* ControlText& operator=(const ControlText& text
|
||||
*/
|
||||
struct ControlText
|
||||
{
|
||||
int nHeight = 0; //- 字体高度
|
||||
int nWidth = 0; //- 字体宽度 如果为0则自适应
|
||||
LPCTSTR lpszFace = "宋体"; //- 字体名称
|
||||
COLORREF color = RGB(0, 0, 0); //- 字体颜色
|
||||
int nEscapement = 0; //- 字符串旋转角度
|
||||
int nOrientation = 0; //- 字符旋转角度
|
||||
int nWeight = 0; //- 字体粗细 范围0~1000 0表示默认
|
||||
bool bItalic = false; //- 是否斜体
|
||||
bool bUnderline = false; //- 是否下划线
|
||||
bool bStrikeOut = false; //- 是否删除线
|
||||
|
||||
bool operator!=(const ControlText& text);
|
||||
ControlText& operator=(const ControlText& text);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @枚举名称: ControlShape
|
||||
* @功能描述: 枚举控件的不同几何样式
|
||||
*
|
||||
* @详细说明:
|
||||
* 定义了四种(有无边框算一种)不同的几何样式,可以根据具体需求
|
||||
* 自定义控件的形状。
|
||||
*
|
||||
* @取值说明:
|
||||
* RECTANGLE = 1, //有边框矩形
|
||||
* B_RECTANGLE, //无边框矩形
|
||||
|
||||
* ROUND_RECTANGLE, //有边框圆角矩形
|
||||
* B_ROUND_RECTANGLE, //无边框圆角矩形
|
||||
|
||||
* CIRCLE, //有边框圆形
|
||||
* B_CIRCLE, //无边框圆形
|
||||
|
||||
* ELLIPSE, //有边框椭圆
|
||||
* B_ELLIPSE //无边框椭圆
|
||||
*
|
||||
* @使用示例:
|
||||
* ControlShape shape = ELLIPSE;
|
||||
*
|
||||
* @备注:
|
||||
* 按钮类支持所有形状,部分控件只支持部分形状,具体请参考控件类。
|
||||
*/
|
||||
enum class ControlShape
|
||||
{
|
||||
RECTANGLE = 1, //有边框矩形
|
||||
B_RECTANGLE, //无边框矩形
|
||||
|
||||
ROUND_RECTANGLE, //有边框圆角矩形
|
||||
B_ROUND_RECTANGLE, //无边框圆角矩形
|
||||
|
||||
CIRCLE, //有边框圆形
|
||||
B_CIRCLE, //无边框圆形
|
||||
|
||||
ELLIPSE, //有边框椭圆
|
||||
B_ELLIPSE //无边框椭圆
|
||||
};
|
||||
/**
|
||||
* @枚举类名称: TextBoxmode
|
||||
* @功能描述: 定义了文本框的两种模式
|
||||
*
|
||||
* @详细说明:
|
||||
* 需要限制文本框是否接受用户输入时使用
|
||||
*
|
||||
* @取值说明:
|
||||
* INPUT_MODE, // 用户可输入模式
|
||||
* READONLY_MODE // 只读模式
|
||||
*
|
||||
* @使用示例:
|
||||
* // 示例代码展示如何使用此枚举类
|
||||
* StellarX::TextBoxmode var = EnumClassName::VALUE1;
|
||||
*
|
||||
* @备注:
|
||||
* 枚举类的特性、与普通枚举的区别
|
||||
*/
|
||||
enum class TextBoxmode
|
||||
{
|
||||
INPUT_MODE, // 用户可输入模式
|
||||
READONLY_MODE // 只读模式
|
||||
};
|
||||
|
||||
/**
|
||||
* @枚举名称: ButtonMode
|
||||
* @功能描述: brief
|
||||
*
|
||||
* @详细说明:
|
||||
* 根据按钮的工作模式,按钮可以有不同的行为。
|
||||
* 用户可以在具体情况下设置按钮的工作模式。
|
||||
*
|
||||
* @取值说明:
|
||||
* NORMAL = 1, - 普通模式,点击后触发回调,但不会保持状态。
|
||||
* TOGGLE, - 切换模式,点击后会在选中和未选中之间切换,触发不同的回调函数。
|
||||
* DISABLED - 禁用模式,按钮不可点击,显示为灰色,文本显示删除线。
|
||||
*
|
||||
* @使用示例:
|
||||
* Button b1(100, 100, 120, 120, "测试按钮", RGB(128, 0, 0), RGB(255, 9, 9));
|
||||
*
|
||||
*/
|
||||
enum class ButtonMode
|
||||
{
|
||||
NORMAL = 1, //普通模式,点击后触发回调,但不会保持状态。
|
||||
TOGGLE, //切换模式,点击后会在选中和未选中之间切换,触发不同的回调函数。
|
||||
DISABLED //禁用模式,按钮不可点击,显示为灰色,文本显示删除线。
|
||||
};
|
||||
struct RouRectangle
|
||||
{
|
||||
int ROUND_RECTANGLEwidth = 20; //构成圆角矩形的圆角的椭圆的宽度。
|
||||
int ROUND_RECTANGLEheight = 20; //构成圆角矩形的圆角的椭圆的高度。
|
||||
};
|
||||
};
|
||||
@@ -1,19 +1,17 @@
|
||||
/*******************************************************************************
|
||||
* @文件: StellarX.h
|
||||
* @摘要: 星垣(StellarX) GUI框架 - 主包含头文件
|
||||
* @版本: v3.1.0
|
||||
* @版本: v1.0.0
|
||||
* @描述:
|
||||
* 一个为Windows平台打造的轻量级、模块化C++ GUI框架。
|
||||
* 基于EasyX图形库,提供简洁易用的API和丰富的控件。
|
||||
*
|
||||
* 通过包含此单一头文件,即可使用框架的所有功能。
|
||||
* 内部头文件包含顺序由框架维护,用户代码不应依赖该顺序。
|
||||
* 内部包含顺序经过精心设计,确保所有依赖关系正确解析。
|
||||
*
|
||||
* @作者: 我在人间做废物
|
||||
* @邮箱: [3150131407@qq.com] | [ysm3150131407@gmail.com]
|
||||
* @官网:https://stellarx-gui.top/
|
||||
* @邮箱: [3150131407@qq.com][]
|
||||
* @仓库: https://github.com/Ysm-04/StellarX
|
||||
* @博客:https://blog.stellarx-gui.top/
|
||||
*
|
||||
* @许可证: MIT License
|
||||
* @版权: Copyright (c) 2025 我在人间做废物
|
||||
@@ -21,25 +19,19 @@
|
||||
* @使用说明:
|
||||
* 只需包含此文件即可使用框架所有功能。
|
||||
* 示例: #include "StellarX.h"
|
||||
* @包含模块:
|
||||
* CoreTypes.h / SxLog.h / Control.h / Canvas.h / Window.h
|
||||
* Button.h / Label.h / TextBox.h / Table.h
|
||||
* Dialog.h / MessageBox.h / TabControl.h
|
||||
* @包含顺序:
|
||||
* 1. CoreTypes.h - 基础类型定义
|
||||
* 2. Control.h - 控件基类
|
||||
* 3. 其他具体控件头文件
|
||||
******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreTypes.h"
|
||||
#include "SxLog.h"
|
||||
#include "Control.h"
|
||||
#include"Canvas.h"
|
||||
#include"Window.h"
|
||||
#include"Button.h"
|
||||
#include"Window.h"
|
||||
#include"Label.h"
|
||||
#include"TextBox.h"
|
||||
#include"Canvas.h"
|
||||
#include"Table.h"
|
||||
#include"Dialog.h"
|
||||
#include"MessageBox.h"
|
||||
#include"TabControl.h"
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
#include "Control.h"
|
||||
// 标签控件,用于显示文本
|
||||
// 特点:
|
||||
// - 支持背景透明/不透明模式
|
||||
// - 不支持用户交互(无事件处理)
|
||||
// - 自动适应文本内容大小
|
||||
|
||||
class Label : public Control
|
||||
{
|
||||
std::string text; //标签文本
|
||||
COLORREF textColor; //标签文本颜色
|
||||
COLORREF textBkColor; //标签背景颜色
|
||||
bool textBkDisap = false; //标签背景是否透明
|
||||
|
||||
//标签事件处理(标签无事件)不实现具体代码
|
||||
void handleEvent(const ExMessage& msg) override {}
|
||||
public:
|
||||
StellarX::ControlText textStyle; //标签文本样式
|
||||
public:
|
||||
Label();
|
||||
Label(int x, int y, std::string text = "标签",COLORREF textcolor = BLACK, COLORREF bkColor= RGB(255,255,255));
|
||||
//绘标签
|
||||
void draw() override;
|
||||
//设置标签背景是否透明
|
||||
void setTextdisap(bool key);
|
||||
//设置标签文本颜色
|
||||
void setTextColor(COLORREF color);
|
||||
//设置标签背景颜色
|
||||
void setTextBkColor(COLORREF color);
|
||||
//设置标签文本
|
||||
void setText(std::string text);
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*******************************************************************************
|
||||
* @文件: Table.h
|
||||
* @摘要: 高级表格控件,支持分页显示
|
||||
* @描述:
|
||||
* 提供完整的数据表格功能,包括表头、数据行、分页和导航按钮。
|
||||
* 自动计算列宽和行高,支持自定义样式
|
||||
*
|
||||
* @实现机制:
|
||||
* 1. 使用二维向量存储数据
|
||||
* 2. 通过分页算法计算显示范围
|
||||
* 3. 使用内部按钮和标签实现分页UI
|
||||
* 4. 通过背景缓存优化渲染性能
|
||||
*
|
||||
* @所属框架: 星垣(StellarX) GUI框架
|
||||
* @作者: 我在人间做废物
|
||||
* @日期: September 2025
|
||||
******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
#include "Control.h"
|
||||
#include "Button.h"
|
||||
#include "Label.h"
|
||||
|
||||
class Table :public Control
|
||||
{
|
||||
private:
|
||||
std::vector<std::vector<std::string>> data; // 表格数据
|
||||
std::vector<std::string> headers; // 表格表头
|
||||
std::string pageNumtext = "页码标签"; // 页码标签文本
|
||||
|
||||
int tableBorderWidth = 1; // 边框宽度
|
||||
|
||||
std::vector<int> colWidths; // 每列的宽度
|
||||
std::vector<int> lineHeights; // 每行的高度
|
||||
|
||||
IMAGE* saveBkImage = nullptr;
|
||||
|
||||
int rowsPerPage = 5; // 每页显示的行数
|
||||
int currentPage = 1; // 当前页码
|
||||
int totalPages = 1; // 总页数
|
||||
|
||||
bool isShowPageButton = true; // 是否显示翻页按钮
|
||||
bool dirty = true; // 是否需要重绘
|
||||
bool isNeedDrawHeaders = true; // 是否需要绘制表头
|
||||
bool isNeedCellSize = true; // 是否需要计算单元格尺寸
|
||||
|
||||
Button* prevButton = nullptr; // 上一页按钮
|
||||
Button* nextButton = nullptr; // 下一页按钮
|
||||
Label* pageNum = nullptr; //页码文本
|
||||
|
||||
int dX = x, dY = y; // 单元格的开始坐标
|
||||
int uX = x, uY = y; // 单元格的结束坐标
|
||||
|
||||
int pX = 0; //标签左上角坐标
|
||||
int pY = 0; //标签左上角坐标
|
||||
|
||||
StellarX::FillMode tableFillMode = StellarX::FillMode::Solid; //填充模式
|
||||
StellarX::LineStyle tableLineStyle = StellarX::LineStyle::Solid; // 线型
|
||||
COLORREF tableBorderClor = RGB(0, 0, 0); // 表格边框颜色
|
||||
COLORREF tableBkClor = RGB(255, 255, 255); // 表格背景颜色
|
||||
|
||||
void initTextWaH(); //初始化文本像素宽度和高度
|
||||
void initButton(); //初始化翻页按钮
|
||||
void initPageNum(); //初始化页码标签
|
||||
|
||||
void drawTable(); //绘制当前页
|
||||
void drawHeader(); //绘制表头
|
||||
void drawPageNum(); //绘制页码信息
|
||||
void drawButton(); //绘制翻页按钮
|
||||
public:
|
||||
StellarX::ControlText textStyle; // 文本样式
|
||||
|
||||
public:
|
||||
Table(int x, int y);
|
||||
~Table();
|
||||
|
||||
// 绘制表格
|
||||
void draw() override;
|
||||
//事件处理
|
||||
void handleEvent(const ExMessage& msg) override;
|
||||
|
||||
//************************** 设置属性 *****************************/
|
||||
|
||||
//设置表头
|
||||
void setHeaders(std::initializer_list<std::string> headers);
|
||||
//设置表格数据
|
||||
void setData(const std::vector<std::string>& data);
|
||||
void setData(const std::initializer_list<std::vector<std::string>>& data);
|
||||
//设置每页显示的行数
|
||||
void setRowsPerPage(int rows);
|
||||
//设置是否显示翻页按钮
|
||||
void showPageButton(bool isShow);
|
||||
//设置表格边框颜色
|
||||
void setTableBorder(COLORREF color);
|
||||
//设置表格背景颜色
|
||||
void setTableBk(COLORREF color);
|
||||
//设置填充模式
|
||||
void setTableFillMode(StellarX::FillMode mode);
|
||||
//设置线型
|
||||
void setTableLineStyle(StellarX::LineStyle style);
|
||||
//设置边框宽度
|
||||
void setTableBorderWidth(int width);
|
||||
|
||||
//************************** 获取属性 *****************************/
|
||||
|
||||
//获取当前页码
|
||||
int getCurrentPage() const;
|
||||
//获取总页数
|
||||
int getTotalPages() const;
|
||||
//获取每页显示的行数
|
||||
int getRowsPerPage() const;
|
||||
//获取是否显示翻页按钮
|
||||
bool getShowPageButton() const;
|
||||
//获取表格边框颜色
|
||||
COLORREF getTableBorder() const;
|
||||
//获取表格背景颜色
|
||||
COLORREF getTableBk() const;
|
||||
//获取填充模式
|
||||
StellarX::FillMode getTableFillMode() const;
|
||||
//获取线型
|
||||
StellarX::LineStyle getTableLineStyle() const;
|
||||
//获取表头
|
||||
std::vector<std::string> getHeaders () const;
|
||||
//获取表格数据
|
||||
std::vector<std::vector<std::string>> getData() const;
|
||||
//获取表格边框宽度
|
||||
int getTableBorderWidth() const;
|
||||
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
#include "Control.h"
|
||||
|
||||
// 文本框控件,支持输入和只读两种模式
|
||||
// 特殊说明:
|
||||
// - 在INPUT_MODE下点击会调用EasyX的InputBox
|
||||
// - 在READONLY_MODE下点击会显示信息对话框
|
||||
// - 最大字符长度受maxCharLen限制
|
||||
|
||||
class TextBox : public Control
|
||||
{
|
||||
std::string text; //文本
|
||||
StellarX::TextBoxmode mode; //模式
|
||||
StellarX::ControlShape shape; //形状
|
||||
bool dirty = true; //是否重绘
|
||||
bool click = false; //是否点击
|
||||
int maxCharLen = 10;//最大字符长度
|
||||
COLORREF textBoxBkClor = RGB(255, 255, 255); //背景颜色
|
||||
COLORREF textBoxBorderClor = RGB(0,0,0); //边框颜色
|
||||
|
||||
public:
|
||||
StellarX::ControlText textStyle; //文本样式
|
||||
|
||||
TextBox(int x, int y, int width, int height, std::string text = "", StellarX::TextBoxmode mode = StellarX::TextBoxmode::INPUT_MODE, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
|
||||
void draw() override;
|
||||
void handleEvent(const ExMessage& msg) override;
|
||||
//设置模式
|
||||
|
||||
void setMode(StellarX::TextBoxmode mode);
|
||||
//设置可输入最大字符长度
|
||||
void setMaxCharLen(int len);
|
||||
//设置形状
|
||||
void setTextBoxshape(StellarX::ControlShape shape);
|
||||
//设置边框颜色
|
||||
void setTextBoxBorder(COLORREF color);
|
||||
//设置背景颜色
|
||||
void setTextBoxBk(COLORREF color);
|
||||
|
||||
//获取文本
|
||||
std::string getText() const;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
#include "Control.h"
|
||||
//窗口模式
|
||||
//#define EX_DBLCLKS 1 - 在绘图窗口中支持鼠标双击事件。
|
||||
//#define EX_NOCLOSE 2 - 禁用绘图窗口的关闭按钮。
|
||||
//#define EX_NOMINIMIZE 3 - 禁用绘图窗口的最小化按钮。
|
||||
//#define EX_SHOWCONSOLE 4 - 显示控制台窗口。
|
||||
/*******************************************************************************
|
||||
* @类: Window
|
||||
* @摘要: 应用程序主窗口类
|
||||
* @描述:
|
||||
* 负责创建和管理应用程序的主窗口,是所有控件的根容器。
|
||||
* 处理消息循环、事件分发和窗口生命周期管理。
|
||||
*
|
||||
* @重要说明:
|
||||
* - 使用 initgraph() 创建窗口
|
||||
* - 使用 BeginBatchDraw()/EndBatchDraw() 实现双缓冲
|
||||
* - 使用 getmessage() 处理消息循环
|
||||
******************************************************************************/
|
||||
class Window
|
||||
{
|
||||
int width; //窗口宽度
|
||||
int height; //窗口高度
|
||||
int windowMode = NULL; //窗口模式
|
||||
HWND hWnd = NULL; //窗口句柄
|
||||
std::string headline; //窗口标题
|
||||
COLORREF wBkcolor = BLACK; //窗口背景
|
||||
IMAGE* background = nullptr; //窗口背景图片
|
||||
std::vector<std::unique_ptr<Control>> controls; //控件管理
|
||||
|
||||
public:
|
||||
Window(int width, int height, int mode);
|
||||
Window(int width, int height, int mode, COLORREF bkcloc);
|
||||
Window(int width, int height, int mode , COLORREF bkcloc, std::string headline = "窗口");
|
||||
~Window();
|
||||
//绘制窗口
|
||||
void draw();
|
||||
void draw(std::string pImgFile);
|
||||
//事件循环
|
||||
void runEventLoop();
|
||||
//设置窗口背景图片
|
||||
|
||||
void setBkImage(std::string pImgFile);
|
||||
//设置窗口背景颜色
|
||||
void setBkcolor(COLORREF c);
|
||||
//设置窗口标题
|
||||
void setHeadline(std::string headline);
|
||||
//添加控件
|
||||
void addControl(std::unique_ptr<Control> control);
|
||||
|
||||
};
|
||||
|
||||
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
#include "StellarX/Button.h"
|
||||
|
||||
Button::Button(int x, int y, int width, int height, const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape)
|
||||
: Control(x, y, width, height)
|
||||
{
|
||||
initButton(text, mode, shape, RGB(202, 255, 255), RGB(171, 196, 220), RGB(255, 255, 0));
|
||||
}
|
||||
|
||||
Button::Button(int x, int y, int width, int height, const std::string text, COLORREF ct, COLORREF cf, StellarX::ButtonMode mode, StellarX::ControlShape shape)
|
||||
: Control(x, y, width, height)
|
||||
{
|
||||
initButton(text, mode, shape, ct, cf, RGB(255, 255, 0));
|
||||
}
|
||||
|
||||
Button::Button(int x, int y, int width, int height, const std::string text, COLORREF ct, COLORREF cf, COLORREF ch, StellarX::ButtonMode mode, StellarX::ControlShape shape)
|
||||
: Control(x, y, width, height)
|
||||
{
|
||||
initButton(text, mode, shape, ct, cf, ch);
|
||||
}
|
||||
|
||||
void Button::initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch)
|
||||
{
|
||||
this->text = text;
|
||||
this->mode = mode;
|
||||
this->shape = shape;
|
||||
this->buttonTrueColor = ct;
|
||||
this->buttonFalseColor = cf;
|
||||
this->buttonHoverColor = ch;
|
||||
this->click = false;
|
||||
this->hover = false;
|
||||
}
|
||||
|
||||
|
||||
Button::~Button()
|
||||
{
|
||||
if (buttonFileIMAGE)
|
||||
delete buttonFileIMAGE;
|
||||
buttonFileIMAGE = nullptr;
|
||||
}
|
||||
|
||||
void Button::draw()
|
||||
{
|
||||
if (dirty)
|
||||
{
|
||||
//保存当前样式和颜色
|
||||
saveStyle();
|
||||
|
||||
if (StellarX::ButtonMode::DISABLED == mode) //设置禁用按钮色
|
||||
{
|
||||
setfillcolor(RGB(96, 96, 96));
|
||||
textStyle.bStrikeOut = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 确保点击状态的颜色正确显示
|
||||
// 点击状态优先级最高,然后是悬停状态,最后是默认状态
|
||||
if (click)
|
||||
setfillcolor(buttonTrueColor);
|
||||
else if (hover)
|
||||
setfillcolor(buttonHoverColor);
|
||||
else
|
||||
setfillcolor(buttonFalseColor);
|
||||
}
|
||||
|
||||
//设置字体背景色透明
|
||||
setbkmode(TRANSPARENT);
|
||||
//边框颜色
|
||||
setlinecolor(buttonBorderColor);
|
||||
if (this->textStyle != oldStyle)
|
||||
{
|
||||
//设置字体颜色
|
||||
settextcolor(textStyle.color);
|
||||
|
||||
//设置字体样式
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut); //设置字体样式
|
||||
}
|
||||
//设置按钮填充模式
|
||||
setfillstyle(buttonFillMode, (int)buttonFillIma, buttonFileIMAGE);
|
||||
|
||||
//获取字符串像素高度和宽度
|
||||
if ((this->oldtext_width != this->text_width || this->oldtext_height != this->text_height)
|
||||
|| (-1 == oldtext_width && oldtext_height == -1))
|
||||
{
|
||||
this->oldtext_width = this->text_width = textwidth(LPCTSTR(this->text.c_str()));
|
||||
this->oldtext_height = this->text_height = textheight(LPCTSTR(this->text.c_str()));
|
||||
}
|
||||
|
||||
|
||||
//根据按钮形状绘制
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
fillrectangle(x, y, x + width, y + height);//有边框填充矩形
|
||||
outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
solidrectangle(x, y, x + width, y + height);//无边框填充矩形
|
||||
outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//有边框填充圆角矩形
|
||||
outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//无边框填充圆角矩形
|
||||
outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::CIRCLE:
|
||||
fillcircle(x + width / 2, y + height / 2, min(width, height) / 2);//有边框填充圆形
|
||||
outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::B_CIRCLE:
|
||||
solidcircle(x + width / 2, y + height / 2, min(width, height) / 2);//无边框填充圆形
|
||||
outtextxy(x + width / 2 - text_width / 2, y + height / 2 - text_height / 2, LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::ELLIPSE:
|
||||
fillellipse(x, y, x + width, y + height);//有边框填充椭圆
|
||||
outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::B_ELLIPSE:
|
||||
solidellipse(x, y, x + width, y + height);//无边框填充椭圆
|
||||
outtextxy((x + (width - text_width) / 2), (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
}
|
||||
|
||||
restoreStyle();//恢复默认字体样式和颜色
|
||||
|
||||
dirty = false; //标记按钮不需要重绘
|
||||
}
|
||||
}
|
||||
// 处理鼠标事件,检测点击和悬停状态
|
||||
// 根据按钮模式和形状进行不同的处理
|
||||
void Button::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
bool oldHover = hover;
|
||||
bool oldClick = click;
|
||||
|
||||
// 检测悬停状态(根据不同形状)
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
hover = (msg.x > x && msg.x < (x + width) && msg.y > y && msg.y < (y + height));
|
||||
break;
|
||||
case StellarX::ControlShape::CIRCLE:
|
||||
case StellarX::ControlShape::B_CIRCLE:
|
||||
hover = isMouseInCircle(msg.x, msg.y, x + width / 2, y + height / 2, min(width, height) / 2);
|
||||
break;
|
||||
case StellarX::ControlShape::ELLIPSE:
|
||||
case StellarX::ControlShape::B_ELLIPSE:
|
||||
hover = isMouseInEllipse(msg.x, msg.y, x, y, x + width, y + height);
|
||||
break;
|
||||
}
|
||||
|
||||
// 处理鼠标点击事件
|
||||
if (msg.message == WM_LBUTTONDOWN && hover && mode != StellarX::ButtonMode::DISABLED)
|
||||
{
|
||||
if (mode == StellarX::ButtonMode::NORMAL)
|
||||
{
|
||||
click = true;
|
||||
dirty = true;
|
||||
}
|
||||
else if (mode == StellarX::ButtonMode::TOGGLE)
|
||||
{
|
||||
// TOGGLE模式在鼠标释放时处理
|
||||
}
|
||||
}
|
||||
// 处理鼠标释放事件
|
||||
else if (msg.message == WM_LBUTTONUP && hover && mode != StellarX::ButtonMode::DISABLED)
|
||||
{
|
||||
if (mode == StellarX::ButtonMode::NORMAL && click)
|
||||
{
|
||||
if (onClickCallback) onClickCallback();
|
||||
click = false;
|
||||
dirty = true;
|
||||
// 使用新的flushmessage函数刷新消息队列:cite[2]:cite[3]
|
||||
flushmessage(EX_MOUSE | EX_KEY);
|
||||
}
|
||||
else if (mode == StellarX::ButtonMode::TOGGLE)
|
||||
{
|
||||
click = !click;
|
||||
if (click && onToggleOnCallback) onToggleOnCallback();
|
||||
else if (!click && onToggleOffCallback) onToggleOffCallback();
|
||||
dirty = true;
|
||||
// 使用新的flushmessage函数刷新消息队列:cite[2]:cite[3]
|
||||
flushmessage(EX_MOUSE | EX_KEY);
|
||||
}
|
||||
}
|
||||
// 处理鼠标移出区域的情况
|
||||
else if (msg.message == WM_MOUSEMOVE)
|
||||
{
|
||||
if (!hover && mode == StellarX::ButtonMode::NORMAL && click)
|
||||
{
|
||||
click = false;
|
||||
dirty = true;
|
||||
}
|
||||
else if (hover != oldHover)
|
||||
{
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果状态发生变化,标记需要重绘
|
||||
if (hover != oldHover || click != oldClick)
|
||||
{
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// 如果需要重绘,立即执行
|
||||
if (dirty)
|
||||
{
|
||||
draw();
|
||||
}
|
||||
}
|
||||
|
||||
void Button::setOnClickListener(const std::function<void()>&& callback)
|
||||
{
|
||||
this->onClickCallback = callback;
|
||||
}
|
||||
|
||||
void Button::setOnToggleOnListener(const std::function<void()>&& callback)
|
||||
{
|
||||
this->onToggleOnCallback = callback;
|
||||
}
|
||||
void Button::setOnToggleOffListener(const std::function<void()>&& callback)
|
||||
{
|
||||
this->onToggleOffCallback = callback;
|
||||
}
|
||||
|
||||
void Button::setbuttonMode(StellarX::ButtonMode mode)
|
||||
{
|
||||
//取值范围参考 buttMode的枚举注释
|
||||
this->mode = mode;
|
||||
}
|
||||
|
||||
int Button::setROUND_RECTANGLEwidth(int width)
|
||||
{
|
||||
return rouRectangleSize.ROUND_RECTANGLEwidth = width;
|
||||
}
|
||||
|
||||
int Button::setROUND_RECTANGLEheight(int height)
|
||||
{
|
||||
return rouRectangleSize.ROUND_RECTANGLEheight = height;
|
||||
}
|
||||
|
||||
bool Button::isClicked() const
|
||||
{
|
||||
return this->click;
|
||||
}
|
||||
|
||||
void Button::setFillMode(int mode)
|
||||
{
|
||||
buttonFillMode = mode;
|
||||
}
|
||||
|
||||
void Button::setFillIma(StellarX::FillStyle ima)
|
||||
{
|
||||
buttonFillIma = ima;
|
||||
}
|
||||
|
||||
void Button::setFillIma(std::string imaNAme)
|
||||
{
|
||||
buttonFileIMAGE = new IMAGE;
|
||||
loadimage(buttonFileIMAGE, imaNAme.c_str(),width-x,height-y);
|
||||
}
|
||||
|
||||
|
||||
void Button::setButtonBorder(COLORREF Border)
|
||||
{
|
||||
buttonBorderColor = Border;
|
||||
}
|
||||
|
||||
void Button::setButtonText(const char* text)
|
||||
{
|
||||
this->text = std::string(text);
|
||||
this->text_width = textwidth(LPCTSTR(this->text.c_str()));
|
||||
this->text_height = textheight(LPCTSTR(this->text.c_str()));
|
||||
}
|
||||
|
||||
void Button::setButtonText(std::string text)
|
||||
{
|
||||
this->text = text;
|
||||
this->text_width = textwidth(LPCTSTR(this->text.c_str()));
|
||||
this->text_height = textheight(LPCTSTR(this->text.c_str()));
|
||||
this->dirty = true; // 标记需要重绘
|
||||
}
|
||||
|
||||
void Button::setButtonShape(StellarX::ControlShape shape)
|
||||
{
|
||||
this->shape = shape;
|
||||
}
|
||||
|
||||
|
||||
std::string Button::getButtonText() const
|
||||
{
|
||||
return this->text;
|
||||
}
|
||||
|
||||
const char* Button::getButtonText_c() const
|
||||
{
|
||||
return this->text.c_str();
|
||||
}
|
||||
|
||||
StellarX::ButtonMode Button::getButtonMode() const
|
||||
{
|
||||
return this->mode;
|
||||
}
|
||||
|
||||
StellarX::ControlShape Button::getButtonShape() const
|
||||
{
|
||||
return this->shape;
|
||||
}
|
||||
|
||||
int Button::getFillMode() const
|
||||
{
|
||||
return this->buttonFillMode;
|
||||
}
|
||||
|
||||
StellarX::FillStyle Button::getFillIma() const
|
||||
{
|
||||
return this->buttonFillIma;
|
||||
}
|
||||
|
||||
IMAGE* Button::getFillImaImage() const
|
||||
{
|
||||
return this->buttonFileIMAGE;
|
||||
}
|
||||
|
||||
COLORREF Button::getButtonBorder() const
|
||||
{
|
||||
return this->buttonBorderColor;
|
||||
}
|
||||
|
||||
COLORREF Button::getButtonTextColor() const
|
||||
{
|
||||
return this->textStyle.color;
|
||||
}
|
||||
|
||||
StellarX::ControlText Button::getButtonTextStyle() const
|
||||
{
|
||||
return this->textStyle;
|
||||
}
|
||||
|
||||
|
||||
bool Button::isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius)
|
||||
{
|
||||
double dis = sqrt(pow(mouseX - x, 2) + pow(mouseY - y, 2));
|
||||
if (dis <= radius)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Button::isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height)
|
||||
{
|
||||
int centerX = (x + width) / 2;
|
||||
int centerY = (y + height) / 2;
|
||||
int majorAxis = (width - x) / 2;
|
||||
int minorAxis = (height - y) / 2;
|
||||
double dx = mouseX - centerX;
|
||||
double dy = mouseY - centerY;
|
||||
double normalizedDistance = (dx * dx) / (majorAxis * majorAxis) + (dy * dy) / (minorAxis * minorAxis);
|
||||
|
||||
// 判断鼠标是否在椭圆内
|
||||
if (normalizedDistance <= 1.0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
#include "StellarX/Canvas.h"
|
||||
|
||||
Canvas::Canvas(int x, int y, int width, int height)
|
||||
:Control(x, y, width, height) {}
|
||||
|
||||
void Canvas::draw()
|
||||
{
|
||||
saveStyle();
|
||||
|
||||
setlinecolor(canvasBorderClor);//设置线色
|
||||
setfillcolor(canvasBkClor);//设置填充色
|
||||
setfillstyle((int)canvasFillMode);//设置填充模式
|
||||
setlinestyle((int)canvasLineStyle, canvaslinewidth);
|
||||
|
||||
//根据画布形状绘制
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
fillrectangle(x,y,x+width,y+height);//有边框填充矩形
|
||||
break;
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
solidrectangle(x, y, x + width, y + height);//无边框填充矩形
|
||||
break;
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//有边框填充圆角矩形
|
||||
break;
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//无边框填充圆角矩形
|
||||
break;
|
||||
}
|
||||
// 绘制所有子控件
|
||||
for (auto& control : controls)
|
||||
control->draw();
|
||||
|
||||
|
||||
restoreStyle();
|
||||
}
|
||||
|
||||
void Canvas::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
for (auto& control : controls) {
|
||||
control->handleEvent(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::addControl(std::unique_ptr<Control> control)
|
||||
{
|
||||
controls.push_back(std::move(control));
|
||||
}
|
||||
|
||||
void Canvas::setShape(StellarX::ControlShape shape)
|
||||
{
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
this->shape = shape;
|
||||
break;
|
||||
case StellarX::ControlShape::CIRCLE:
|
||||
case StellarX::ControlShape::B_CIRCLE:
|
||||
case StellarX::ControlShape::ELLIPSE:
|
||||
case StellarX::ControlShape::B_ELLIPSE:
|
||||
this->shape = StellarX::ControlShape::RECTANGLE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Canvas::setCanvasfillMode(StellarX::FillMode mode)
|
||||
{
|
||||
this->canvasFillMode = mode;
|
||||
}
|
||||
|
||||
void Canvas::setBorderColor(COLORREF color)
|
||||
{
|
||||
this->canvasBorderClor = color;
|
||||
}
|
||||
|
||||
void Canvas::setCanvasBkColor(COLORREF color)
|
||||
{
|
||||
this->canvasBkClor = color;
|
||||
}
|
||||
|
||||
void Canvas::setCanvasLineStyle(StellarX::LineStyle style)
|
||||
{
|
||||
this->canvasLineStyle = style;
|
||||
}
|
||||
|
||||
|
||||
void Canvas::setLinewidth(int width)
|
||||
{
|
||||
this->canvaslinewidth = width;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#include "StellarX/Control.h"
|
||||
|
||||
StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text)
|
||||
{
|
||||
{
|
||||
nHeight = text.nHeight;
|
||||
nWidth = text.nWidth;
|
||||
lpszFace = text.lpszFace;
|
||||
color = text.color;
|
||||
nEscapement = text.nEscapement;
|
||||
nOrientation = text.nOrientation;
|
||||
nWeight = text.nWeight;
|
||||
bItalic = text.bItalic;
|
||||
bUnderline = text.bUnderline;
|
||||
bStrikeOut = text.bStrikeOut;
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
|
||||
bool StellarX::ControlText::operator!=(const ControlText& text)
|
||||
{
|
||||
if(nHeight != text.nHeight)
|
||||
return true;
|
||||
else if (nWidth != text.nWidth)
|
||||
return true;
|
||||
else if (lpszFace != text.lpszFace)
|
||||
return true;
|
||||
else if (color != text.color)
|
||||
return true;
|
||||
else if (nEscapement != text.nEscapement)
|
||||
return true;
|
||||
else if (nOrientation != text.nOrientation)
|
||||
return true;
|
||||
else if (nWeight != text.nWeight)
|
||||
return true;
|
||||
else if (bItalic != text.bItalic)
|
||||
return true;
|
||||
else if (bUnderline != text.bUnderline)
|
||||
return true;
|
||||
else if (bStrikeOut != text.bStrikeOut)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
// 保存当前的绘图状态(字体、颜色、线型等)
|
||||
// 在控件绘制前调用,确保不会影响全局绘图状态
|
||||
void Control::saveStyle()
|
||||
{
|
||||
gettextstyle(¤tFont); // 获取当前字体样式
|
||||
currentColor = gettextcolor(); // 获取当前字体颜色
|
||||
currentBorderColor = getlinecolor(); //保存当前边框颜色
|
||||
getlinestyle(currentLineStyle); //保存当前线型
|
||||
currentBkColor = getfillcolor(); //保存当前填充色
|
||||
}
|
||||
// 恢复之前保存的绘图状态
|
||||
// 在控件绘制完成后调用,恢复全局绘图状态
|
||||
void Control::restoreStyle()
|
||||
{
|
||||
settextstyle(¤tFont); // 恢复默认字体样式
|
||||
settextcolor(currentColor); // 恢复默认字体颜色
|
||||
setfillcolor(currentBkColor);
|
||||
setlinestyle(currentLineStyle);
|
||||
setlinecolor(currentBorderColor);
|
||||
setfillstyle(BS_SOLID);//恢复填充
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#include "StellarX/Label.h"
|
||||
|
||||
Label::Label()
|
||||
:Control(0, 0, 0, 0)
|
||||
{
|
||||
this->text = "默认标签";
|
||||
textColor = RGB(0,0,0);
|
||||
textBkColor = RGB(255, 255, 255);; //默认白色背景
|
||||
}
|
||||
|
||||
Label::Label(int x, int y, std::string text, COLORREF textcolor, COLORREF bkColor)
|
||||
:Control(x, y, 0, 0)
|
||||
{
|
||||
this->text = text;
|
||||
textColor = textcolor;
|
||||
textBkColor = bkColor; //默认白色背景
|
||||
}
|
||||
|
||||
void Label::draw()
|
||||
{
|
||||
saveStyle();
|
||||
if (textBkDisap)
|
||||
setbkmode(TRANSPARENT); //设置背景透明
|
||||
else
|
||||
{
|
||||
setbkmode(OPAQUE); //设置背景不透明
|
||||
setbkcolor(textBkColor); //设置背景颜色
|
||||
}
|
||||
settextcolor(textColor);
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut); //设置字体样式
|
||||
outtextxy(x,y, LPCTSTR(text.c_str()));
|
||||
restoreStyle();
|
||||
}
|
||||
|
||||
void Label::setTextdisap(bool key)
|
||||
{
|
||||
textBkDisap = key;
|
||||
}
|
||||
|
||||
void Label::setTextColor(COLORREF color)
|
||||
{
|
||||
textColor = color;
|
||||
}
|
||||
|
||||
void Label::setTextBkColor(COLORREF color)
|
||||
{
|
||||
textBkColor = color;
|
||||
}
|
||||
|
||||
void Label::setText(std::string text)
|
||||
{
|
||||
this->text = text;
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
#include "StellarX/Table.h"
|
||||
// 绘制表格的当前页
|
||||
// 使用双循环绘制行和列,考虑分页偏移
|
||||
void Table::drawTable()
|
||||
{
|
||||
dX = x;
|
||||
dY = uY;
|
||||
uY = dY + lineHeights.at(0) + 10;
|
||||
|
||||
for (int i = (currentPage * rowsPerPage - rowsPerPage); i < (currentPage*rowsPerPage) && i < data.size(); i++)
|
||||
{
|
||||
for (int j = 0; j < data[i].size(); j++)
|
||||
{
|
||||
uX = dX + colWidths.at(j) + 20;
|
||||
fillrectangle(dX, dY, uX, uY);
|
||||
outtextxy(dX + 10, dY + 5, LPCTSTR(data[i][j].c_str()));
|
||||
dX += this->colWidths.at(j) + 20;
|
||||
}
|
||||
dX = x;
|
||||
dY = uY;
|
||||
uY = dY + lineHeights.at(0) + 10;
|
||||
|
||||
}
|
||||
uY = y + lineHeights.at(0) + 10;
|
||||
}
|
||||
|
||||
void Table::drawHeader()
|
||||
{
|
||||
|
||||
uY = dY + lineHeights.at(0) + 10;
|
||||
for(int i = 0; i < headers.size(); i++)
|
||||
{
|
||||
uX = dX + colWidths.at(i) + 20;
|
||||
fillrectangle(dX, dY, uX, uY);
|
||||
outtextxy(dX + 10, dY + 5, LPCTSTR(headers[i].c_str()));
|
||||
dX += this->colWidths.at(i) + 20;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
// 初始化文本宽度和高度计算
|
||||
// 遍历所有数据和表头,计算每列的最大宽度和行高
|
||||
// 此方法在数据变更时自动调用
|
||||
void Table::initTextWaH()
|
||||
{
|
||||
this->colWidths.resize(this->headers.size());
|
||||
this->lineHeights.resize(this->headers.size());
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
//计算数据尺寸
|
||||
for (int i = 0; i < data.size(); i++)
|
||||
{
|
||||
for (int j = 0; j < data[i].size(); j++)
|
||||
{
|
||||
width = textwidth(LPCTSTR(data[i][j].c_str()));
|
||||
height = textheight(LPCTSTR(data[i][j].c_str()));
|
||||
if (width > this->colWidths.at(j))
|
||||
this->colWidths.at(j) = width;
|
||||
if (height > this->lineHeights[j])
|
||||
this->lineHeights.at(j) = height;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < this->headers.size(); i++)
|
||||
{
|
||||
width = textwidth(LPCTSTR(headers[i].c_str()));
|
||||
height = textheight(LPCTSTR(headers[i].c_str()));
|
||||
if (width > this->colWidths.at(i))
|
||||
this->colWidths.at(i) = width;
|
||||
if (height > this->lineHeights[i])
|
||||
this->lineHeights.at(i) = height;
|
||||
}
|
||||
|
||||
// 计算表格总宽度和高度
|
||||
this->width = 0;
|
||||
for (int i = 0; i < colWidths.size(); i++)
|
||||
this->width += colWidths.at(i) + 20;
|
||||
LINESTYLE currentStyle;
|
||||
|
||||
this->width += tableBorderWidth;
|
||||
|
||||
this->height = lineHeights.at(0) * (rowsPerPage + 1) + rowsPerPage * 10+20 ; // 表头+数据行+页码区域
|
||||
|
||||
// 如果背景图像不存在或尺寸不匹配,创建或重新创建
|
||||
if (saveBkImage == nullptr) {
|
||||
saveBkImage = new IMAGE(width, height);
|
||||
}
|
||||
else if (saveBkImage->getwidth() != width || saveBkImage->getheight() != height) {
|
||||
delete saveBkImage;
|
||||
saveBkImage = new IMAGE(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
void Table::initButton()
|
||||
{
|
||||
int x1, x2;
|
||||
int y1, y2;
|
||||
x1 = pX - 70;
|
||||
x2 = pX + textwidth(LPCTSTR(pageNumtext.c_str())) + 10;
|
||||
y1 = y2 = pY;
|
||||
this->prevButton = new Button(x1, y1, 60, textheight(LPCTSTR(pageNumtext.c_str())), "上一页", RGB(0, 0, 0), RGB(255, 255, 255));
|
||||
this->nextButton = new Button(x2, y2, 60, textheight(LPCTSTR(pageNumtext.c_str())), "下一页", RGB(0, 0, 0), RGB(255, 255, 255));
|
||||
prevButton->setOnClickListener([this]()
|
||||
{if (this->currentPage > 1)
|
||||
{
|
||||
this->currentPage--;
|
||||
this->dirty = true;
|
||||
this->draw();
|
||||
} });
|
||||
|
||||
nextButton->setOnClickListener([this]()
|
||||
{if (this->currentPage < (this->totalPages))
|
||||
{
|
||||
this->currentPage++;
|
||||
this->dirty = true;
|
||||
this->draw();
|
||||
}});
|
||||
}
|
||||
|
||||
void Table::initPageNum()
|
||||
{
|
||||
if (0 == pY)
|
||||
pY = uY + lineHeights.at(0) * rowsPerPage + rowsPerPage * 10+10;
|
||||
for (int i = 0; i < colWidths.size(); i++)
|
||||
this->pX += colWidths.at(i) + 20;
|
||||
this->pX -= textwidth(LPCTSTR(pageNumtext.c_str()));
|
||||
this->pX /= 2;
|
||||
this->pX += x;
|
||||
this->pageNum = new Label(this->pX, pY, pageNumtext);
|
||||
//pageNum->setTextdisap(true);
|
||||
pageNum->textStyle = this->textStyle;
|
||||
}
|
||||
|
||||
void Table::drawPageNum()
|
||||
{
|
||||
if (nullptr == pageNum)
|
||||
initPageNum();
|
||||
pageNumtext = std::to_string(currentPage);
|
||||
pageNumtext += "页/第";
|
||||
pageNumtext += std::to_string(totalPages);
|
||||
pageNumtext += "页";
|
||||
pageNum->setText(pageNumtext);
|
||||
pageNum->draw();
|
||||
|
||||
}
|
||||
|
||||
void Table::drawButton()
|
||||
{
|
||||
if (nullptr == prevButton || nullptr == nextButton)
|
||||
initButton();
|
||||
prevButton->draw();
|
||||
nextButton->draw();
|
||||
|
||||
}
|
||||
|
||||
Table::Table(int x, int y)
|
||||
:Control(x, y, 0,0)
|
||||
{
|
||||
//this->saveBkImage = new IMAGE(this->width,this->height);
|
||||
}
|
||||
|
||||
Table::~Table()
|
||||
{
|
||||
if (this->prevButton)
|
||||
delete this->prevButton;
|
||||
if (this->nextButton)
|
||||
delete this->nextButton;
|
||||
if (this->pageNum)
|
||||
delete this->pageNum;
|
||||
if (this->saveBkImage)
|
||||
delete this->saveBkImage;
|
||||
this->prevButton = nullptr;
|
||||
this->nextButton = nullptr;
|
||||
this->pageNum = nullptr;
|
||||
this->saveBkImage = nullptr;
|
||||
}
|
||||
|
||||
void Table::draw()
|
||||
{
|
||||
if (this->dirty)
|
||||
{
|
||||
// 先保存当前绘图状态
|
||||
saveStyle();
|
||||
|
||||
// 设置表格样式
|
||||
setfillcolor(tableBkClor);
|
||||
setlinecolor(tableBorderClor);
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
settextcolor(textStyle.color);
|
||||
setlinestyle((int)tableLineStyle, tableBorderWidth);
|
||||
setfillstyle((int)tableFillMode);
|
||||
setbkmode(TRANSPARENT);
|
||||
|
||||
// 是否需要计算单元格尺寸
|
||||
if (isNeedCellSize)
|
||||
{
|
||||
initTextWaH();
|
||||
isNeedCellSize = false;
|
||||
}
|
||||
|
||||
// 在绘制表格之前捕获背景
|
||||
// 只有在第一次绘制或者尺寸变化时才需要重新捕获背景
|
||||
static bool firstDraw = true;
|
||||
if (firstDraw || isNeedDrawHeaders) {
|
||||
// 确保在绘制任何表格内容之前捕获背景
|
||||
if (saveBkImage) {
|
||||
// 临时恢复样式,确保捕获正确的背景
|
||||
restoreStyle();
|
||||
if(tableBorderWidth>1)
|
||||
getimage(saveBkImage, this->x- tableBorderWidth, this->y- tableBorderWidth, this->width+ tableBorderWidth, this->height+ tableBorderWidth);
|
||||
else
|
||||
getimage(saveBkImage, this->x, this->y, this->width, this->height);
|
||||
saveStyle(); // 重新保存样式
|
||||
|
||||
// 重新设置表格样式
|
||||
setfillcolor(tableBkClor);
|
||||
setlinecolor(tableBorderClor);
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
settextcolor(textStyle.color);
|
||||
setlinestyle((int)tableLineStyle, tableBorderWidth);
|
||||
setfillstyle((int)tableFillMode);
|
||||
setbkmode(TRANSPARENT);
|
||||
}
|
||||
firstDraw = false;
|
||||
}
|
||||
|
||||
// 恢复背景(清除旧内容)
|
||||
if (saveBkImage) {
|
||||
if (tableBorderWidth > 1)
|
||||
putimage(this->x - tableBorderWidth, this->y - tableBorderWidth, saveBkImage);
|
||||
else
|
||||
putimage(this->x,this->y,this->saveBkImage);
|
||||
}
|
||||
|
||||
// 绘制表头
|
||||
|
||||
dX = x;
|
||||
dY = y;
|
||||
drawHeader();
|
||||
this->isNeedDrawHeaders = false;
|
||||
|
||||
|
||||
// 绘制当前页
|
||||
drawTable();
|
||||
|
||||
// 绘制页码标签
|
||||
drawPageNum();
|
||||
|
||||
// 绘制翻页按钮
|
||||
if (this->isShowPageButton)
|
||||
drawButton();
|
||||
|
||||
// 恢复绘图状态
|
||||
restoreStyle();
|
||||
dirty = false; // 标记不需要重绘
|
||||
}
|
||||
}
|
||||
|
||||
void Table::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
if(!this->isShowPageButton)
|
||||
return;
|
||||
else
|
||||
{
|
||||
prevButton->handleEvent(msg);
|
||||
nextButton->handleEvent(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void Table::setHeaders(std::initializer_list<std::string> headers)
|
||||
{
|
||||
this->headers.clear();
|
||||
for (auto lis : headers)
|
||||
this->headers.push_back(lis);
|
||||
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
||||
isNeedDrawHeaders = true; // 标记需要重新绘制表头
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Table::setData(const std::vector<std::string>& data)
|
||||
{
|
||||
this->data.push_back(data);
|
||||
totalPages = (this->data.size() + rowsPerPage - 1) / rowsPerPage;
|
||||
if (totalPages < 1)
|
||||
totalPages = 1;
|
||||
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Table::setData(const std::initializer_list<std::vector<std::string>>& data)
|
||||
{
|
||||
for (auto lis : data)
|
||||
this->data.push_back(lis);
|
||||
totalPages = (this->data.size() + rowsPerPage - 1) / rowsPerPage;
|
||||
if (totalPages < 1)
|
||||
totalPages = 1;
|
||||
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Table::setRowsPerPage(int rows)
|
||||
{
|
||||
this->rowsPerPage = rows;
|
||||
totalPages = (data.size() + rowsPerPage - 1) / rowsPerPage;
|
||||
if (totalPages < 1)
|
||||
totalPages = 1;
|
||||
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void Table::showPageButton(bool isShow)
|
||||
{
|
||||
this->isShowPageButton = isShow;
|
||||
}
|
||||
|
||||
void Table::setTableBorder(COLORREF color)
|
||||
{
|
||||
this->tableBorderClor = color;
|
||||
}
|
||||
|
||||
void Table::setTableBk(COLORREF color)
|
||||
{
|
||||
this->tableBkClor = color;
|
||||
}
|
||||
|
||||
void Table::setTableFillMode(StellarX::FillMode mode)
|
||||
{
|
||||
if (StellarX::FillMode::Solid == mode || StellarX::FillMode::Null == mode)
|
||||
this->tableFillMode = mode;
|
||||
else
|
||||
this->tableFillMode = StellarX::FillMode::Solid;
|
||||
}
|
||||
|
||||
void Table::setTableLineStyle(StellarX::LineStyle style)
|
||||
{
|
||||
this->tableLineStyle = style;
|
||||
}
|
||||
|
||||
void Table::setTableBorderWidth(int width)
|
||||
{
|
||||
this->tableBorderWidth = width;
|
||||
}
|
||||
|
||||
int Table::getCurrentPage() const
|
||||
{
|
||||
return this->currentPage;
|
||||
}
|
||||
|
||||
int Table::getTotalPages() const
|
||||
{
|
||||
return this->totalPages;;
|
||||
}
|
||||
|
||||
int Table::getRowsPerPage() const
|
||||
{
|
||||
return this->rowsPerPage;
|
||||
}
|
||||
|
||||
bool Table::getShowPageButton() const
|
||||
{
|
||||
return this->isShowPageButton;
|
||||
}
|
||||
|
||||
COLORREF Table::getTableBorder() const
|
||||
{
|
||||
return this->tableBorderClor;
|
||||
}
|
||||
|
||||
COLORREF Table::getTableBk() const
|
||||
{
|
||||
return this->tableBkClor;
|
||||
}
|
||||
|
||||
StellarX::FillMode Table::getTableFillMode() const
|
||||
{
|
||||
return this->tableFillMode;
|
||||
}
|
||||
|
||||
StellarX::LineStyle Table::getTableLineStyle() const
|
||||
{
|
||||
return this->tableLineStyle;
|
||||
}
|
||||
|
||||
std::vector<std::string> Table::getHeaders() const
|
||||
{
|
||||
return this->headers;
|
||||
}
|
||||
|
||||
std::vector<std::vector<std::string>> Table::getData() const
|
||||
{
|
||||
return this->data;
|
||||
}
|
||||
|
||||
int Table::getTableBorderWidth() const
|
||||
{
|
||||
return this->tableBorderWidth;
|
||||
}
|
||||
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
// TextBox.cpp
|
||||
#include "StellarX/TextBox.h"
|
||||
|
||||
|
||||
TextBox::TextBox(int x, int y, int width, int height, std::string text, StellarX::TextBoxmode mode, StellarX::ControlShape shape)
|
||||
:Control(x,y,width,height),text(text), mode(mode), shape(shape)
|
||||
{
|
||||
}
|
||||
|
||||
void TextBox::draw()
|
||||
{
|
||||
if(dirty)
|
||||
{
|
||||
saveStyle();
|
||||
setfillcolor(textBoxBkClor);
|
||||
setlinecolor(textBoxBorderClor);
|
||||
if (textStyle.nHeight > height)
|
||||
textStyle.nHeight = height;
|
||||
if (textStyle.nWidth > width)
|
||||
textStyle.nWidth = width;
|
||||
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
|
||||
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
|
||||
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
|
||||
|
||||
settextcolor(textStyle.color);
|
||||
setbkmode(TRANSPARENT);
|
||||
int text_width = textwidth(LPCTSTR(text.c_str()));
|
||||
int text_height = textheight(LPCTSTR(text.c_str()));
|
||||
|
||||
|
||||
//根据形状绘制
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
fillrectangle(x,y,x+width,y+height);//有边框填充矩形
|
||||
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
solidrectangle(x, y, x + width, y + height);//无边框填充矩形
|
||||
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//有边框填充圆角矩形
|
||||
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//无边框填充圆角矩形
|
||||
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str()));
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
restoreStyle();
|
||||
dirty = false; //标记不需要重绘
|
||||
}
|
||||
|
||||
void TextBox::handleEvent(const ExMessage& msg)
|
||||
{
|
||||
bool hover = false;
|
||||
bool oldClick = click;
|
||||
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
hover = (msg.x > x && msg.x < (x + width) && msg.y > y && msg.y < (y + height));//判断鼠标是否在矩形按钮内
|
||||
break;
|
||||
}
|
||||
if (hover && msg.message == WM_LBUTTONUP)
|
||||
{
|
||||
click = true;
|
||||
if(StellarX::TextBoxmode::INPUT_MODE == mode)
|
||||
dirty = InputBox(LPTSTR(text.c_str()), maxCharLen,"输入框",NULL,text.c_str(), NULL ,NULL,false);
|
||||
else if (StellarX::TextBoxmode::READONLY_MODE == mode)
|
||||
{
|
||||
dirty = false;
|
||||
InputBox(NULL, maxCharLen, "输出框(输入无效!)", NULL, text.c_str(), NULL, NULL, false);
|
||||
}
|
||||
flushmessage(EX_MOUSE | EX_KEY);
|
||||
}
|
||||
if (dirty)
|
||||
draw();
|
||||
|
||||
if (click)
|
||||
click = false;
|
||||
}
|
||||
|
||||
void TextBox::setMode(StellarX::TextBoxmode mode)
|
||||
{
|
||||
this->mode = mode;
|
||||
}
|
||||
|
||||
void TextBox::setMaxCharLen(int len)
|
||||
{
|
||||
if (len > 0)
|
||||
maxCharLen = len;
|
||||
}
|
||||
|
||||
void TextBox::setTextBoxshape(StellarX::ControlShape shape)
|
||||
{
|
||||
switch (shape)
|
||||
{
|
||||
case StellarX::ControlShape::RECTANGLE:
|
||||
case StellarX::ControlShape::B_RECTANGLE:
|
||||
case StellarX::ControlShape::ROUND_RECTANGLE:
|
||||
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
||||
this->shape = shape;
|
||||
break;
|
||||
case StellarX::ControlShape::CIRCLE:
|
||||
case StellarX::ControlShape::B_CIRCLE:
|
||||
case StellarX::ControlShape::ELLIPSE:
|
||||
case StellarX::ControlShape::B_ELLIPSE:
|
||||
this->shape = StellarX::ControlShape::RECTANGLE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void TextBox::setTextBoxBorder(COLORREF color)
|
||||
{
|
||||
textBoxBorderClor = color;
|
||||
}
|
||||
|
||||
void TextBox::setTextBoxBk(COLORREF color)
|
||||
{
|
||||
textBoxBkClor = color;
|
||||
}
|
||||
|
||||
std::string TextBox::getText() const
|
||||
{
|
||||
return this->text;
|
||||
}
|
||||
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
#include "StellarX/Window.h"
|
||||
|
||||
Window::Window(int width, int height, int mode)
|
||||
{
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
this->windowMode = mode;
|
||||
}
|
||||
|
||||
Window::Window(int width, int height, int mode, COLORREF bkcloc)
|
||||
{
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
this->windowMode = mode;
|
||||
this->wBkcolor = bkcloc;
|
||||
}
|
||||
|
||||
Window::Window(int width, int height, int mode, COLORREF bkcloc, std::string headline)
|
||||
{
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
this->windowMode = mode;
|
||||
this->wBkcolor = bkcloc;
|
||||
this->headline = headline;
|
||||
}
|
||||
|
||||
Window::~Window()
|
||||
{
|
||||
if (background)
|
||||
delete background;
|
||||
background = nullptr;
|
||||
closegraph(); // 确保关闭图形上下文
|
||||
}
|
||||
|
||||
void Window::draw()
|
||||
{
|
||||
hWnd = initgraph(width, height, windowMode);
|
||||
SetWindowText(hWnd,headline.c_str());
|
||||
setbkcolor(wBkcolor);
|
||||
cleardevice();
|
||||
BeginBatchDraw(); // 开始批量绘制
|
||||
// 绘制所有子控件
|
||||
for (auto& control : controls)
|
||||
control->draw();
|
||||
EndBatchDraw(); // 结束批量绘制
|
||||
}
|
||||
// 使用背景图片绘制窗口
|
||||
// @参数 pImgFile: 图片文件路径,支持常见图片格式
|
||||
// @备注: 会拉伸图片以适应窗口尺寸
|
||||
void Window::draw(std::string pImgFile)
|
||||
{
|
||||
this->background = new IMAGE(width, height);
|
||||
hWnd = initgraph(width, height, windowMode);
|
||||
SetWindowText(hWnd, headline.c_str());
|
||||
loadimage(background, pImgFile.c_str(), width, height, true);
|
||||
putimage(0,0, background);
|
||||
|
||||
// 绘制所有子控件
|
||||
BeginBatchDraw(); // 开始批量绘制
|
||||
for (auto& control : controls)
|
||||
control->draw();
|
||||
EndBatchDraw(); // 结束批量绘制
|
||||
|
||||
}
|
||||
// 运行主事件循环,处理用户输入和窗口消息
|
||||
// 此方法会阻塞直到窗口关闭
|
||||
void Window::runEventLoop()
|
||||
{
|
||||
ExMessage msg;
|
||||
bool running = true;
|
||||
while (running) {
|
||||
msg = getmessage(EX_MOUSE | EX_KEY);
|
||||
if (msg.message == WM_CLOSE) {
|
||||
running = false;
|
||||
continue;
|
||||
}
|
||||
for (auto& c : controls)
|
||||
c->handleEvent(msg);
|
||||
flushmessage(EX_MOUSE |EX_KEY |EX_CHAR|EX_WINDOW);
|
||||
Sleep(10);
|
||||
}
|
||||
}
|
||||
|
||||
void Window::setBkImage(std::string pImgFile)
|
||||
{
|
||||
if(nullptr == background)
|
||||
this->background = new IMAGE;
|
||||
else
|
||||
delete background;
|
||||
this->background = new IMAGE;
|
||||
loadimage(background, pImgFile.c_str(), width, height, true);
|
||||
putimage(0, 0, background);
|
||||
}
|
||||
|
||||
void Window::setBkcolor(COLORREF c)
|
||||
{
|
||||
wBkcolor = c;
|
||||
}
|
||||
|
||||
void Window::setHeadline(std::string headline)
|
||||
{
|
||||
this->headline = headline;
|
||||
SetWindowText(this->hWnd, headline.c_str());
|
||||
}
|
||||
|
||||
void Window::addControl(std::unique_ptr<Control> control)
|
||||
{
|
||||
this->controls.push_back(std::move(control));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-1674
File diff suppressed because it is too large
Load Diff
@@ -1,137 +0,0 @@
|
||||
# 修改总览-20260409
|
||||
|
||||
## 说明
|
||||
|
||||
- 本目录用于记录本轮协作中对框架做过的修改。
|
||||
- 记录范围覆盖:
|
||||
- 早期未建 Git 基线前的修改
|
||||
- 已提交到仓库的阶段性修改
|
||||
- 当前工作区中尚未提交的第二阶段重绘架构与注释整理
|
||||
- 其中相对最原始版本、但未落到 Git 基线的那部分内容,依据协作过程与当前代码状态回溯整理。
|
||||
|
||||
## 阶段概览
|
||||
|
||||
1. 初始基线
|
||||
- 提交:`dde570a`
|
||||
- 含义:建立仓库后的第一版基线
|
||||
|
||||
2. 第一阶段:基础治理与确定性问题修复
|
||||
- 主要内容:
|
||||
- 资源所有权收紧
|
||||
- 析构顺序修正
|
||||
- `Table / Label / Dialog / Button / TextBox` 等基础问题修复
|
||||
|
||||
3. 第二阶段:快照与对话框行为收口
|
||||
- 提交:`4a6e153`
|
||||
- 提交:`7f8431a`
|
||||
- 主要内容:
|
||||
- 快照“作废”和“回贴”语义拆分
|
||||
- `Dialog` 在窗口变化时只重新居中
|
||||
- 模态/非模态对话框的残影、穿透、关闭后 hover 清理等问题修复
|
||||
|
||||
4. 第三阶段:统一提交重绘(第一版)
|
||||
- 提交:`b07a4ec`
|
||||
- 主要内容:
|
||||
- 将事件阶段的“改状态”与绘制阶段的“提交”分离
|
||||
- 窗口开始托管控件在事件分发期间的重绘请求
|
||||
|
||||
5. 第四阶段:统一提交重绘(第二版,当前工作区)
|
||||
- 状态:未提交
|
||||
- 主要内容:
|
||||
- 从“整场景统一重绘”推进到“按 root 登记并选择性补画对话框”
|
||||
- 为 `Canvas / TabControl / Dialog` 建立托管重绘提交语义
|
||||
- 补充相关注释
|
||||
|
||||
6. 测试与文档
|
||||
- 主要内容:
|
||||
- `KEY == 4` 综合回归用例
|
||||
- 日志降噪
|
||||
- 模板体系与开发记录目录建立
|
||||
|
||||
## 记录索引
|
||||
|
||||
### BUG
|
||||
|
||||
- [BUG-20260409-0001 对话框重绘、快照残留与遮挡交互异常](./BUG/BUG-20260409-0001-对话框重绘快照与遮挡交互异常.md)
|
||||
- [BUG-20260409-0002 基础控件生命周期与边界条件问题](./BUG/BUG-20260409-0002-基础控件生命周期与边界条件问题.md)
|
||||
|
||||
### Fix
|
||||
|
||||
- [Fix-BUG-20260409-0001 对话框重绘、快照残留与遮挡交互异常](./Fix/Fix-BUG-20260409-0001-对话框重绘快照与遮挡交互异常.md)
|
||||
- [Fix-BUG-20260409-0002 基础控件生命周期与边界条件问题](./Fix/Fix-BUG-20260409-0002-基础控件生命周期与边界条件问题.md)
|
||||
|
||||
### 功能变更
|
||||
|
||||
- [Feature-20260409-0001 基础资源所有权与生命周期收口](./功能变更/Feature-20260409-0001-基础资源所有权与生命周期收口.md)
|
||||
- [Feature-20260409-0002 Dialog 与 MessageBox 行为调整](./功能变更/Feature-20260409-0002-Dialog与MessageBox行为调整.md)
|
||||
- [Feature-20260409-0003 输入事件、hover 与遮挡交互调整](./功能变更/Feature-20260409-0003-输入事件与遮挡交互调整.md)
|
||||
- [Feature-20260409-0004 测试用例与可观测性调整](./功能变更/Feature-20260409-0004-测试用例与可观测性调整.md)
|
||||
- [Feature-20260409-0005 开发记录与模板体系整理](./功能变更/Feature-20260409-0005-开发记录与模板体系整理.md)
|
||||
|
||||
### 模块
|
||||
|
||||
- [Module-20260409-0001 Window 托管重绘与覆盖合成机制](./模块/Module-20260409-0001-Window托管重绘与覆盖合成机制.md)
|
||||
|
||||
## 覆盖关系
|
||||
|
||||
### 核心代码文件覆盖
|
||||
|
||||
- `Window.h / Window.cpp`
|
||||
- 覆盖于:
|
||||
- `BUG-20260409-0001`
|
||||
- `Fix-BUG-20260409-0001`
|
||||
- `Feature-20260409-0002`
|
||||
- `Feature-20260409-0003`
|
||||
- `Module-20260409-0001`
|
||||
|
||||
- `Control.h / Control.cpp`
|
||||
- 覆盖于:
|
||||
- `BUG-20260409-0002`
|
||||
- `Fix-BUG-20260409-0002`
|
||||
- `Feature-20260409-0001`
|
||||
- `Module-20260409-0001`
|
||||
|
||||
- `Canvas.h / Canvas.cpp`
|
||||
- 覆盖于:
|
||||
- `BUG-20260409-0001`
|
||||
- `Fix-BUG-20260409-0001`
|
||||
- `Feature-20260409-0003`
|
||||
- `Module-20260409-0001`
|
||||
|
||||
- `TabControl.h / TabControl.cpp`
|
||||
- 覆盖于:
|
||||
- `Feature-20260409-0003`
|
||||
- `Feature-20260409-0004`
|
||||
- `Module-20260409-0001`
|
||||
|
||||
- `Dialog.h / Dialog.cpp`
|
||||
- 覆盖于:
|
||||
- `BUG-20260409-0001`
|
||||
- `Fix-BUG-20260409-0001`
|
||||
- `Feature-20260409-0002`
|
||||
- `Module-20260409-0001`
|
||||
|
||||
- `Button.cpp / TextBox.cpp`
|
||||
- 覆盖于:
|
||||
- `BUG-20260409-0001`
|
||||
- `Fix-BUG-20260409-0001`
|
||||
- `Feature-20260409-0003`
|
||||
|
||||
- `Table.cpp / Table.h / Label.cpp / MessageBox.cpp`
|
||||
- 覆盖于:
|
||||
- `BUG-20260409-0002`
|
||||
- `Fix-BUG-20260409-0002`
|
||||
- `Feature-20260409-0001`
|
||||
- `Feature-20260409-0002`
|
||||
|
||||
- `z-testDome.cpp`
|
||||
- 覆盖于:
|
||||
- `Feature-20260409-0004`
|
||||
|
||||
## 当前状态
|
||||
|
||||
- 当前工作区在 `77a8fe5` 之后还有未提交修改。
|
||||
- 这些未提交修改主要是:
|
||||
- 第二阶段托管重绘
|
||||
- 注释补充与校正
|
||||
- 记录与模板体系整理
|
||||
@@ -1,55 +0,0 @@
|
||||
# 补充记录-20260409-窗口最大化收口保护
|
||||
|
||||
## 说明
|
||||
|
||||
- 本文件用于补充记录 2026-04-09 当天新增的窗口最大化收口问题。
|
||||
- 对应修改发生在 `f567369` 快照之后,当前仍位于工作区未提交状态。
|
||||
|
||||
## 问题概述
|
||||
|
||||
- 现象:
|
||||
- 某些窗口初始尺寸下,直接点击最大化会出现黑背景、控件残影。
|
||||
- 若先手动拖拽窗口略微放大,再最大化,则问题消失。
|
||||
- 影响用例:
|
||||
- `KEY == 1`
|
||||
- `KEY == 2`
|
||||
- 不受影响用例:
|
||||
- `KEY == 3`
|
||||
- `KEY == 4`
|
||||
|
||||
## 根因
|
||||
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 的 `runEventLoop()` resize 收口中存在一段历史“跨度保护”。
|
||||
- 该保护把“宽高变化超过 1000”视为异常帧,直接终止收口:
|
||||
- 不再执行 `adaptiveLayout`
|
||||
- 不再执行 `Resize`
|
||||
- 不再执行 `redrawScene(true, true)`
|
||||
- 因此新暴露区域不会被完整刷新,最终表现为黑背景与残影。
|
||||
|
||||
## 本次新增记录
|
||||
|
||||
- BUG:
|
||||
- [`BUG-20260409-0003 直接最大化触发收口保护导致黑背景与残影`](./BUG/BUG-20260409-0003-直接最大化触发收口保护导致黑背景与残影.md)
|
||||
- Fix:
|
||||
- [`Fix-BUG-20260409-0003 直接最大化触发收口保护导致黑背景与残影`](./Fix/Fix-BUG-20260409-0003-直接最大化触发收口保护导致黑背景与残影.md)
|
||||
- 功能变更:
|
||||
- [`Feature-20260409-0006 窗口最大化收口保护调整`](./功能变更/Feature-20260409-0006-窗口最大化收口保护调整.md)
|
||||
|
||||
## 修改摘要
|
||||
|
||||
- 将“跨度过大即跳过”改为“仅非法尺寸才跳过”。
|
||||
- 非法尺寸标准:
|
||||
- `actualWidth <= 0`
|
||||
- `actualHeight <= 0`
|
||||
- `actualWidth > max(10000, virtualScreenWidth * 2)`
|
||||
- `actualHeight > max(10000, virtualScreenHeight * 2)`
|
||||
- 对“大跨度但合法”的 resize 保留 `DEBUG` 日志,但继续执行完整收口。
|
||||
- 顺手删除了 `Window.cpp` 中未使用的 `ExMessage mm;` 遗留变量。
|
||||
|
||||
## 当前状态
|
||||
|
||||
- 代码修改:已完成,待提交
|
||||
- 文档记录:已补充
|
||||
- 验证状态:
|
||||
- 已完成源码级编译验证
|
||||
- 待用户做 GUI 回归
|
||||
@@ -1,73 +0,0 @@
|
||||
# BUG-20260409-0001
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260409-0001
|
||||
- 标题: 对话框重绘、快照残留与遮挡交互异常
|
||||
- 状态:已修复 / 待持续回归
|
||||
- 严重性:S2
|
||||
- 优先级:P0
|
||||
- 模块:Window / Dialog / Canvas / Button / TextBox
|
||||
- 版本 / 分支:`master`
|
||||
- 环境:Windows + EasyX + VS2022
|
||||
- 发现人:协作过程静态审查与测试用例回归
|
||||
- 关联 Fix ID:Fix-BUG-20260409-0001
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象:
|
||||
- 非模态对话框打开后,底层被遮挡控件仍可能收到鼠标事件。
|
||||
- 模态或非模态对话框在窗口尺寸变化后,可能出现背景快照残留。
|
||||
- 模态对话框标题区域曾出现抓到底层背景的问题。
|
||||
- 对话框关闭后,底层按钮 hover 状态可能不能立即恢复。
|
||||
- 多个按钮快速 hover 切换时,对话框补画链容易闪烁。
|
||||
- 影响范围:
|
||||
- 对话框交互正确性
|
||||
- 重绘层级稳定性
|
||||
- hover 与点击行为一致性
|
||||
- 期望结果:
|
||||
- 对话框始终位于正确层级。
|
||||
- 被对话框覆盖的底层控件不会误交互。
|
||||
- resize / 打开 / 关闭过程中无残影、无错位。
|
||||
- 实际结果:
|
||||
- 曾出现穿透、残影、错误快照、关闭后 hover 残留、频繁补画闪烁等问题。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:存在非模态或模态对话框,且下层有可交互控件
|
||||
- 复现步骤:
|
||||
|
||||
1. 打开包含按钮、Canvas、TabControl 的测试窗口。
|
||||
2. 弹出非模态或模态对话框。
|
||||
3. 在对话框遮挡区域附近快速移动鼠标,或拖动窗口大小。
|
||||
|
||||
- 复现概率:高概率 / 偶现(不同问题不同)
|
||||
- 最小复现 Demo:`z-testDome.cpp` 中 `KEY == 2 / 3 / 4`
|
||||
- 证据:日志、静态推演、用户回归观察
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置:
|
||||
- `Dialog::handleEvent`
|
||||
- `Dialog::draw / recenterInHostWindow / clearControls`
|
||||
- `Window::runEventLoop / pumpResizeIfNeeded / flushManagedRepaint`
|
||||
- `Button::handleEvent`
|
||||
- 触发条件:
|
||||
- 对话框覆盖底层控件
|
||||
- 窗口 resize
|
||||
- hover 高频切换
|
||||
- 对话框打开/关闭后的层级切换
|
||||
- 相关线索:
|
||||
- “快照作废”和“回贴旧背景”语义混用
|
||||
- 事件阶段与绘制阶段原先分离不彻底
|
||||
- 非模态对话框区域内鼠标事件吞掉后,底层控件拿不到离开消息
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间:本轮协作初期
|
||||
- 最后更新时间:2026-04-09
|
||||
- 修复版本:[当前工作区]
|
||||
- 验证版本:[当前工作区]
|
||||
- 备注:快速划过多个按钮时一帧内偶发双高亮的问题暂未彻底消除,属于已接受的底层限制
|
||||
@@ -1,71 +0,0 @@
|
||||
# BUG-20260409-0002
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260409-0002
|
||||
- 标题: 基础控件生命周期与边界条件问题
|
||||
- 状态:已修复 / 待持续回归
|
||||
- 严重性:S2
|
||||
- 优先级:P1
|
||||
- 模块:Control / Window / Table / Label / Button / TextBox / MessageBox
|
||||
- 版本 / 分支:`master`
|
||||
- 环境:Windows + EasyX + VS2022
|
||||
- 发现人:静态审查
|
||||
- 关联 Fix ID:Fix-BUG-20260409-0002
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象:
|
||||
- `Window` 析构时可能先关闭图形环境,再让控件析构访问绘图接口。
|
||||
- `Table` 在空表头、空数据、列数不一致时存在越界或空状态问题。
|
||||
- `Label::setText()` 后尺寸与背景快照不能及时同步。
|
||||
- `MessageBox` 外层曾直接干预 `Dialog` 初始化流程。
|
||||
- 一些资源所有权和析构职责不够清晰。
|
||||
- 影响范围:
|
||||
- 基础稳定性
|
||||
- 边界输入正确性
|
||||
- 维护成本
|
||||
- 期望结果:
|
||||
- 生命周期安全、边界输入可防御、接口语义清晰
|
||||
- 实际结果:
|
||||
- 存在潜在崩溃、越界、状态不一致和维护负担
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:特定表格输入、动态修改文本、窗口关闭、MessageBox 使用链
|
||||
- 复现步骤:
|
||||
|
||||
1. 静态审查相关代码路径。
|
||||
2. 构造空表头、长于表头的数据行、动态变更 Label 文本。
|
||||
3. 观察关闭窗口或销毁相关对象时的析构顺序。
|
||||
|
||||
- 复现概率:高概率 / 必现(不同问题不同)
|
||||
- 最小复现 Demo:[可选]
|
||||
- 证据:静态分析、日志、编译验证
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置:
|
||||
- `Window::~Window`
|
||||
- `Control` 析构与背景快照恢复链
|
||||
- `Table::initTextWaH / setData / drawHeader / drawTable`
|
||||
- `Label::setText`
|
||||
- `MessageBox::showAsync / showModal`
|
||||
- 触发条件:
|
||||
- 边界数据输入
|
||||
- 动态文本变化
|
||||
- 对象销毁 / 窗口关闭
|
||||
- 相关线索:
|
||||
- 所有权层次混乱
|
||||
- 空状态防御不足
|
||||
- 接口暴露超出真实职责
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间:本轮协作初期
|
||||
- 最后更新时间:2026-04-09
|
||||
- 修复版本:[当前工作区]
|
||||
- 验证版本:[当前工作区]
|
||||
- 备注:字符集相关问题暂未处理,仍按 MBCS 假设工作
|
||||
@@ -1,68 +0,0 @@
|
||||
# BUG-20260409-0003
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260409-0003
|
||||
- 标题: 直接最大化触发收口保护,导致黑背景与控件残影
|
||||
- 状态:已修复 / 待回归验证
|
||||
- 严重性:S2
|
||||
- 优先级:P0
|
||||
- 模块:Window / Resize / Scene Redraw
|
||||
- 版本 / 分支:`master`
|
||||
- 环境:Windows 桌面 + EasyX + VS2022
|
||||
- 发现人:用户反馈
|
||||
- 关联 Fix ID:`Fix-BUG-20260409-0003`
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象:
|
||||
- 某些用例在窗口初始大小下直接点击最大化后,新暴露出的区域显示为黑色背景。
|
||||
- 已有控件可能出现残留、残影或旧内容未被覆盖。
|
||||
- 若先手动拖动边框略微放大,再执行最大化,则问题消失。
|
||||
- 影响范围:
|
||||
- 顶层窗口 resize 收口
|
||||
- 背景完整重绘
|
||||
- 控件 resize 后的快照/标脏收口
|
||||
- 期望结果:
|
||||
- 无论是拖拽 resize 还是直接最大化,只要 `WM_SIZE` 进入收口流程,都应完成一次完整 redraw。
|
||||
- 实际结果:
|
||||
- 某些“大跨度但合法”的最大化尺寸变化,会在收口中途被直接跳过,导致背景和控件未完成刷新。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 复现条件:
|
||||
- 初始窗口尺寸与最大化后的客户区尺寸跨度较大。
|
||||
- 典型用例:
|
||||
1. `KEY == 1`:`1200x400 -> 2160x1417`
|
||||
2. `KEY == 2`:`700x510 -> 2160x1417`
|
||||
3. `KEY == 3 / 4`:初始尺寸较大时不复现
|
||||
- 复现概率:高
|
||||
- 证据:
|
||||
- `WM_SIZE:待处理=(2160x1417), isSizing=0`
|
||||
- `调整窗口尺寸开始:width=... height=...`
|
||||
- `尺寸调整被跨度保护跳过:old=(...) new=(...) diff=(...)`
|
||||
- 异常日志中缺失 `尺寸调整已完成`
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置:
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 的 `runEventLoop()` resize 收口逻辑
|
||||
- 可疑代码特征:
|
||||
- 使用 `abs(finalW - width) > 1000 || abs(finalH - height) > 1000` 作为跳过条件
|
||||
- 命中后直接:
|
||||
- `needResizeDirty = false`
|
||||
- `continue`
|
||||
- 根因判断:
|
||||
- 该分支把“跨度较大的合法最大化”误判为异常帧
|
||||
- 导致后续 `Resize / adaptiveLayout / redrawScene / done log` 全部被跳过
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间:2026-04-09
|
||||
- 最后更新时间:2026-04-09
|
||||
- 修复版本:当前工作区
|
||||
- 验证版本:待用户回归
|
||||
- 备注:
|
||||
- 当前证据表明问题与 DPI 不是同一级根因,更直接的是跨度阈值误杀。
|
||||
@@ -1,68 +0,0 @@
|
||||
# BUG-20260410-0004
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: `BUG-20260410-0004`
|
||||
- 标题: 按钮 Tooltip 显示后,鼠标移出按钮区域提示框不消失
|
||||
- 状态:已修复
|
||||
- 严重性:S3
|
||||
- 优先级:P1
|
||||
- 模块: Button / Tooltip / 托管重绘
|
||||
- 版本 / 分支: 当前工作区 / 下一版本开发中
|
||||
- 环境: Windows + EasyX,本地 GUI Demo
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: `Fix-BUG-20260410-0004`
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象:
|
||||
- 按钮启用 Tooltip 后,提示框成功显示。
|
||||
- 鼠标离开按钮区域后,Tooltip 仍残留在屏幕上,不会及时消失。
|
||||
- 影响范围:
|
||||
- 所有启用了 Tooltip 的 `Button`
|
||||
- 典型场景包括 `KEY == 4` 的综合回归用例
|
||||
- 期望结果:
|
||||
- 鼠标离开按钮后,Tooltip 应立即回贴并消失,不留下残影
|
||||
- 实际结果:
|
||||
- Tooltip 逻辑状态已切换为隐藏,但屏幕上的提示框区域未被及时擦除
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:
|
||||
- 使用启用 Tooltip 的按钮
|
||||
- 复现步骤:
|
||||
|
||||
1. 运行带 Tooltip 按钮的 Demo
|
||||
2. 将鼠标悬停在按钮上,等待 Tooltip 显示
|
||||
3. 将鼠标移出按钮区域,观察 Tooltip 是否立即消失
|
||||
|
||||
- 复现概率:高概率
|
||||
- 最小复现 Demo:
|
||||
- `z-testDome.cpp` 中 `KEY == 4`
|
||||
- 证据:现象观察 + 代码链路分析
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置:
|
||||
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp)
|
||||
- `Button::hideTooltip()`
|
||||
- 触发条件:
|
||||
- Tooltip 已经显示
|
||||
- 鼠标离开按钮,进入 Tooltip 隐藏分支
|
||||
- 相关线索:
|
||||
- Tooltip 不是独立控件树成员,而是 `Button::draw()` 末尾补画的内置浮层
|
||||
- 隐藏时若只作废快照而不回贴 Tooltip 自己的背景,按钮本体重绘并不会覆盖 Tooltip 占用区域
|
||||
- 最近相关改动:
|
||||
- 事件改状态 / Window 托管重绘收口
|
||||
- 布局系统第一阶段重构
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-04-10
|
||||
- 最后更新时间: 2026-04-10
|
||||
- 修复版本: 待定
|
||||
- 验证版本: 当前工作区
|
||||
- 备注:
|
||||
- 本文档仅记录问题本身,具体修复方案见关联 Fix 文档
|
||||
@@ -1,73 +0,0 @@
|
||||
# BUG-20260415-0005
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260415-0005
|
||||
- 标题: 局部重绘未补画上层兄弟导致遮挡错误
|
||||
- 状态:已修复
|
||||
- 严重性:S2
|
||||
- 优先级:P1
|
||||
- 模块: Window / Canvas / TabControl 局部重绘与合成
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows + EasyX
|
||||
- 发现人: 用户回归测试
|
||||
- 关联 Fix ID:[可选]
|
||||
- `Fix-BUG-20260415-0005`
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象:
|
||||
- 下层控件发生局部重绘后,位于其上方且与 coverage 相交的兄弟控件没有被补画回来。
|
||||
- 表现为上层 `Canvas / Dialog / 页 / 页签按钮` 被“切掉”或被下层重新盖住。
|
||||
- 影响范围:
|
||||
- 顶层普通控件之间
|
||||
- `Canvas` 直接子控件之间
|
||||
- `TabControl` 页签按钮与页面之间
|
||||
- 期望结果:
|
||||
- 任意局部重绘提交后,父容器应按实际绘制顺序把 coverage 上方相交的兄弟重新合成回来。
|
||||
- 实际结果:
|
||||
- 仅 dirty root 或 dirty child 被重画,上层兄弟未恢复。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选]
|
||||
- 使用 `KEY5` 测试场景
|
||||
- 复现步骤:
|
||||
|
||||
1. 进入 `KEY5`
|
||||
2. 触发下层控件局部重绘,例如 `Table` 翻页或相交子控件 hover / click
|
||||
3. 观察上层相交兄弟区域
|
||||
|
||||
- 复现概率:高概率
|
||||
- 最小复现 Demo:[可选]
|
||||
- `KEY5` 中 `Table` 与顶层粉色浮层重叠场景
|
||||
- 同父 `Canvas` 兄弟相交场景
|
||||
- 证据:截图 / 日志 / 调用栈 / 录屏 / 断点观察
|
||||
- 断点观察显示 dirty root 已提交,但上层 sibling 未重新执行 draw
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置:
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
|
||||
- 触发条件:
|
||||
- 局部重绘提交写入了下层像素,但没有做 overlay 兄弟补画
|
||||
- 相关线索:
|
||||
- 该问题不依赖“是否回贴背景快照”,直接重绘同样会破坏上层合成结果
|
||||
- `Dialog` 之前之所以需要补画,本质上是同一类问题
|
||||
- 最近相关改动:[可选]
|
||||
- 第二阶段开始引入更严格的局部重绘与 overlay 收口后,问题被系统性暴露
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-04-15
|
||||
- 最后更新时间: 2026-04-15
|
||||
- 修复版本:[可选]
|
||||
- 当前工作区
|
||||
- 验证版本:[可选]
|
||||
- 当前工作区
|
||||
- 备注:[可选]
|
||||
- 后续仍需继续关注 `Dialog` 旧 synthetic move 机制与新模型的统一。
|
||||
@@ -1,53 +0,0 @@
|
||||
# BUG-20260415-0006
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260415-0006
|
||||
- 标题: 托管局部重绘未正确提交脏子树导致嵌套 Canvas 按钮状态不刷新
|
||||
- 状态:已修复
|
||||
- 严重性:S2
|
||||
- 优先级:P1
|
||||
- 模块: 重绘 / 托管局部提交 / Canvas 嵌套
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows / EasyX / KEY5 回归场景
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID:Fix-BUG-20260415-0006
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: KEY5 中第二层、第三层 Canvas 内按钮可正常触发 hover / click 回调,但视觉状态不刷新。
|
||||
- 影响范围: 嵌套 Canvas 内的深层按钮、页内嵌套容器按钮、所有依赖托管局部重绘的脏后代场景。
|
||||
- 期望结果: 只要深层按钮状态变化,最外层托管 root 应能正确提交对应脏子树并把视觉结果画出来。
|
||||
- 实际结果: 日志显示事件和回调已执行,但屏幕上不出现 hover / press / release 反馈,直到触发更大范围重绘才会补出来。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件: 使用 KEY5 场景,启用三层 Canvas 嵌套回归区。
|
||||
- 复现步骤:
|
||||
|
||||
1. 打开 KEY5。
|
||||
2. 将鼠标移到第二层或第三层 Canvas 内的按钮上,观察 hover。
|
||||
3. 点击按钮并观察按下、松开状态。
|
||||
|
||||
- 复现概率:必现
|
||||
- 最小复现 Demo:KEY5 A 区三层 Canvas 嵌套
|
||||
- 证据:日志显示按钮回调已执行,但按钮视觉状态不刷新
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: Canvas 托管局部重绘提交链、Window 托管重绘登记 coverage
|
||||
- 触发条件: root 的直接子控件本身不 dirty,但其下存在 dirty descendant
|
||||
- 相关线索:
|
||||
- 托管局部提交只认直接 dirty child
|
||||
- 深层按钮状态变化未提升到 root 下直接脏分支
|
||||
- 最近相关改动:第二阶段布局与托管重绘收口
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-04-15
|
||||
- 最后更新时间: 2026-04-15
|
||||
- 修复版本:当前工作区
|
||||
- 验证版本:KEY5 编译级验证通过,待用户手测
|
||||
- 备注:该问题已按机制修复,不再使用临时整容器重绘补丁
|
||||
@@ -1,53 +0,0 @@
|
||||
# BUG-20260415-0007
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260415-0007
|
||||
- 标题: 实际绘制 coverage 低估导致 Tooltip 与 overlay 补画漏算
|
||||
- 状态:已修复
|
||||
- 严重性:S2
|
||||
- 优先级:P1
|
||||
- 模块: Tooltip / overlay 重组 / 托管 coverage
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows / EasyX / KEY5 回归场景
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID:Fix-BUG-20260415-0007
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: C 米区容器内按钮 Tooltip 会画到 D 橙区上层;类似问题也可能出现在带附加绘制区域的其他控件上。
|
||||
- 影响范围: Tooltip、页签按钮 Tooltip、内部按钮与浮层、overlay 补画依赖 coverage 的所有链路。
|
||||
- 期望结果: overlay 补画应按“实际写到屏幕上的区域”判断,而不是只按控件本体 bounds。
|
||||
- 实际结果: 按钮本体未与上层控件相交时,Tooltip 超出部分不会触发 overlay 补画,导致 Tooltip 压到不应被压住的上层控件之上。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件: 使用 KEY5,保证 C 米区按钮 Tooltip 会伸进 D 橙区。
|
||||
- 复现步骤:
|
||||
|
||||
1. 打开 KEY5。
|
||||
2. 将鼠标移到 C 米区容器内按钮上,触发 Tooltip。
|
||||
3. 观察 Tooltip 与 D 橙区的遮挡关系。
|
||||
|
||||
- 复现概率:必现
|
||||
- 最小复现 Demo:KEY5 C / D 交界区
|
||||
- 证据:Tooltip 可见区域超出按钮 bounds,但 overlay 补画未跟上
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: Button Tooltip coverage、Window/Canvas/TabControl 的 coverage 计算链
|
||||
- 触发条件: 控件本体 bounds 未与 overlay 相交,但附加绘制区域(例如 Tooltip)已伸入 overlay 区
|
||||
- 相关线索:
|
||||
- 之前的 coverage 默认等于 `getBoundsRect()`
|
||||
- `Button` 的 Tooltip 由 `tipLabel` 额外绘制,不属于按钮本体矩形
|
||||
- 最近相关改动:顶层 overlay 传递式补画机制已补齐,但 coverage 仍按本体矩形计算
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-04-15
|
||||
- 最后更新时间: 2026-04-15
|
||||
- 修复版本:当前工作区
|
||||
- 验证版本:KEY5 编译级验证通过,待用户手测
|
||||
- 备注:Tooltip 智能选位明确后置,本次只修根因链路
|
||||
@@ -1,68 +0,0 @@
|
||||
# BUG-20260415-0008
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260415-0008
|
||||
- 标题: TabControl 页签层级与重复激活链路导致 Tooltip 和残影异常
|
||||
- 状态:已修复
|
||||
- 严重性:S2
|
||||
- 优先级:P1
|
||||
- 模块: TabControl / 页签绘制 / 外部激活页签
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows / EasyX / KEY1 / KEY5
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID:Fix-BUG-20260415-0008
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象:
|
||||
- 有任意页签打开时,页签按钮 Tooltip 无法正常显示,只有全部页签关闭时正常。
|
||||
- KEY1 中页签 1 的表格超出 TabControl 区域,外部按钮重复激活同一页签后,再切页或关闭页签会留下超出部分残影。
|
||||
- 影响范围: TabControl 页签按钮 Tooltip、外部 `setActiveIndex()` 重复调用、页内超出页面边界绘制的控件。
|
||||
- 期望结果:
|
||||
- 页签按钮应始终位于页面之上,Tooltip 正常显示。
|
||||
- 外部重复激活已激活页签不应破坏当前可见页面的快照链。
|
||||
- 实际结果:
|
||||
- 页签按钮 Tooltip 会被页面盖掉。
|
||||
- 外部重复激活同一页签后,页内超出页面区域的 Table 在切页/关页时会残留。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:
|
||||
- KEY5:存在可见页面和页签按钮 Tooltip
|
||||
- KEY1:页签 1 内的 Table 长于 TabControl 页面区域,存在外部按钮 `test`
|
||||
- 复现步骤:
|
||||
|
||||
1. 在 KEY5 中打开任意页签,将鼠标移到页签按钮上触发 Tooltip。
|
||||
2. 观察 Tooltip 是否被页面盖掉。
|
||||
3. 在 KEY1 中点击一次 `test` 激活页签 1。
|
||||
4. 再点击一次 `test`,然后切页或关闭页签 1。
|
||||
|
||||
- 复现概率:必现
|
||||
- 最小复现 Demo:KEY5 TabControl 区、KEY1 页签 1 + 外部 `test` 按钮
|
||||
- 证据:Tooltip 被页面覆盖;重复激活后切页/关页出现残影
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置:
|
||||
- `TabControl::draw()`
|
||||
- `TabControl::requestRepaint()`
|
||||
- `TabControl::setActiveIndex()`
|
||||
- `Button::setButtonClick()`
|
||||
- 触发条件:
|
||||
- 页签按钮先画、页面后画
|
||||
- `TOGGLE` 同状态重复 set 仍触发 onToggleOn
|
||||
- 相关线索:
|
||||
- 页面绘制层级高于页签按钮时,页签 Tooltip 会被后画的页面盖掉
|
||||
- 重复激活已激活页签会重复执行页面 `onWindowResize()` / `setIsVisible(true)` 链
|
||||
- 最近相关改动:TabControl overlay / Tooltip 回归专项修复
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-04-15
|
||||
- 最后更新时间: 2026-04-15
|
||||
- 修复版本:当前工作区
|
||||
- 验证版本:KEY1 / KEY5 编译级验证通过,待用户手测
|
||||
- 备注:本次只修页签层级和重复激活链路,不扩到 TabControl 其它内部布局语义
|
||||
@@ -1,51 +0,0 @@
|
||||
# BUG-20260511-0009
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260511-0009
|
||||
- 标题: TabControl::indexOf 未命中时返回最后索引
|
||||
- 状态:已修复
|
||||
- 严重性:S3
|
||||
- 优先级:P2
|
||||
- 模块: TabControl
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows / EasyX / z-testDome 回归场景
|
||||
- 发现人: Codex 巡检
|
||||
- 关联 Fix ID: Fix-BUG-20260511-0009
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: `TabControl::indexOf(const std::string& tabText) const` 在未找到目标页签时返回循环后的最后索引。
|
||||
- 影响范围: 依赖 `indexOf()` 判断页签是否存在的调用方可能把“不存在”误判为最后一个页签。
|
||||
- 期望结果: 找到匹配页签时返回对应索引;未找到时返回 `-1`。
|
||||
- 实际结果: 未找到时返回最后一个页签的索引。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选]
|
||||
- 复现步骤:
|
||||
|
||||
1. 构造包含至少一个页签的 `TabControl`。
|
||||
2. 调用 `indexOf()` 查询不存在的页签文本。
|
||||
3. 观察返回值。
|
||||
|
||||
- 复现概率:必现
|
||||
- 最小复现 Demo:[可选]
|
||||
- 证据:源码巡检可证
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: `TabControl.cpp::indexOf`
|
||||
- 触发条件: 查询文本不存在。
|
||||
- 相关线索: 函数名和预期语义要求“不存在返回 -1”,但旧实现返回 `idx`。
|
||||
- 最近相关改动:[可选]
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-05-11
|
||||
- 最后更新时间: 2026-05-11
|
||||
- 修复版本: 当前工作区
|
||||
- 验证版本: KEY1 ~ KEY6 编译验证通过
|
||||
- 备注:[可选]
|
||||
@@ -1,52 +0,0 @@
|
||||
# BUG-20260511-0010
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260511-0010
|
||||
- 标题: 局部提交内部 coverage 扩张未反馈导致 Dialog 漏补
|
||||
- 状态:已修复
|
||||
- 严重性:S2
|
||||
- 优先级:P1
|
||||
- 模块: Window / Canvas / Dialog / 托管重绘
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows / EasyX / KEY6
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: Fix-BUG-20260511-0010
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: KEY6 中非模态 Dialog 打开后,触发旁边按钮 Tooltip,某些位置会导致下层按钮重绘覆盖 Dialog。
|
||||
- 影响范围: 可局部提交的 root 在内部补画兄弟控件后,如果实际写屏范围扩大,Window 可能仍按初始 coverage 判断上层 overlay。
|
||||
- 期望结果: 下层 root 内部扩张后的实际写屏区域能触发上层 Dialog 补画。
|
||||
- 实际结果: Window 使用初始 coverage,导致 Dialog overlay 补画漏判。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选] 打开 KEY6,触发非模态 `AbortRetryIgnore` 对话框。
|
||||
- 复现步骤:
|
||||
|
||||
1. 最大化窗口。
|
||||
2. 打开 KEY6 非模态 `AbortRetryIgnore` 对话框。
|
||||
3. 在 `RetryCancel` 按钮特定区域触发 Tooltip。
|
||||
4. 观察非模态 Dialog 是否被下层按钮局部重绘覆盖。
|
||||
|
||||
- 复现概率:高概率
|
||||
- 最小复现 Demo:KEY6
|
||||
- 证据:临时 `OverlayTemp` 日志显示 Canvas 内部 coverage 从 `(250,512,490,583)` 扩张到 `(250,512,700,583)`,但 Window 仍用旧 working coverage 判断 Dialog,相交结果为 `hit=0`。
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: `Window::flushManagedRepaint()` 与 `Canvas::requestRepaint(this)` 的 coverage 交接。
|
||||
- 触发条件: root 支持局部提交,内部补画后序兄弟导致实际 coverage 扩张。
|
||||
- 相关线索: Dialog 区域与内部最终 coverage 相交,但与 Window 初始 working coverage 不相交。
|
||||
- 最近相关改动:[可选] 托管局部重绘与 overlay 补画收口。
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-05-11
|
||||
- 最后更新时间: 2026-05-11
|
||||
- 修复版本: 当前工作区
|
||||
- 验证版本: KEY6 编译通过,待用户手测
|
||||
- 备注:[可选] 精确 actual coverage 回传作为下版本优化项。
|
||||
@@ -1,51 +0,0 @@
|
||||
# BUG-20260511-0011
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260511-0011
|
||||
- 标题: 跨 root 回调改脏未同轮提交导致 Label 延迟刷新
|
||||
- 状态:已修复
|
||||
- 严重性:S3
|
||||
- 优先级:P1
|
||||
- 模块: Window / 托管重绘 / Label / TabControl
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows / EasyX / KEY6
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: Fix-BUG-20260511-0011
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: KEY6 中点击 Right 页签内按钮后,A 区状态 Label 不立即刷新,需要下一次左键消息才显示。
|
||||
- 影响范围: 事件回调修改了另一个顶层 root 下的控件状态时,目标 root 可能不会进入本轮托管重绘队列。
|
||||
- 期望结果: 回调中改脏的跨 root 控件能在同一轮事件尾刷新。
|
||||
- 实际结果: 只有当前事件分发链所在 root 被提交,其他 root 的 dirty 子树滞留到下一次事件。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选] 打开 KEY6,激活右侧页签第一页。
|
||||
- 复现步骤:
|
||||
|
||||
1. 点击 Right 页签中的“页内按钮”。
|
||||
2. 观察 A 区状态 Label。
|
||||
3. 再点击空白处或其他控件。
|
||||
|
||||
- 复现概率:必现
|
||||
- 最小复现 Demo:KEY6
|
||||
- 证据:`Label::setText()` 只标脏目标 Label,不主动把其所属 `shapePanel` root 登记到 Window。
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: `Window` 事件尾托管重绘收口。
|
||||
- 触发条件: 当前事件 root 与被回调修改的目标 root 不同。
|
||||
- 相关线索: A 区 Label 在下一次左键消息后才刷新,说明 dirty 状态存在但未同轮提交。
|
||||
- 最近相关改动:[可选] 托管重绘统一收口。
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-05-11
|
||||
- 最后更新时间: 2026-05-11
|
||||
- 修复版本: 当前工作区
|
||||
- 验证版本: KEY6 编译通过,待用户手测
|
||||
- 备注:[可选]
|
||||
@@ -1,54 +0,0 @@
|
||||
# BUG-20260511-0012
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: BUG-20260511-0012
|
||||
- 标题: Tooltip 临时浮层污染或擦除兄弟控件快照
|
||||
- 状态:已修复
|
||||
- 严重性:S2
|
||||
- 优先级:P1
|
||||
- 模块: Tooltip / Canvas / TabControl / 托管重绘 / 背景快照
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows / EasyX / KEY6
|
||||
- 发现人: 用户
|
||||
- 关联 Fix ID: Fix-BUG-20260511-0012
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象: KEY6 A 区禁用按钮触发 Tooltip 后,如果 Tooltip 位于状态 Label 下方,状态 Label 可能捕获 Tooltip 到背景快照,或用透明 Label 的旧快照擦掉 Tooltip 一部分。
|
||||
- 影响范围: 同一容器内,绘制顺序在 Tooltip 触发控件之后、且与 Tooltip coverage 相交的兄弟控件。
|
||||
- 期望结果: Tooltip 作为临时浮层显示,不应污染兄弟控件背景快照,也不应被透明 Label 的旧快照擦除。
|
||||
- 实际结果: 旧逻辑把 Tooltip coverage 与持久绘制 coverage 混用,导致兄弟控件作废快照并重抓,或保留快照重画时擦掉 Tooltip。
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选] 打开 KEY6。
|
||||
- 复现步骤:
|
||||
|
||||
1. 将鼠标移到 A 区禁用按钮上触发 Tooltip。
|
||||
2. 让 Tooltip 显示区域位于状态 Label 下方。
|
||||
3. 观察状态 Label 区域是否覆盖 / 擦除 Tooltip。
|
||||
4. 触发下一次重绘后观察是否有 Tooltip 残留。
|
||||
|
||||
- 复现概率:高概率
|
||||
- 最小复现 Demo:KEY6 A 区
|
||||
- 证据:`Label::draw()` 即使透明也会先 `restBackground()`;如果被临时浮层触发补画,会回贴旧快照。
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置: `Canvas::requestRepaint(this)` / `TabControl::requestRepaint(this)` 的局部 overlay 补画策略。
|
||||
- 触发条件: 下层 Button Tooltip 的完整 coverage 与后序兄弟控件相交。
|
||||
- 相关线索:
|
||||
- `Button::getManagedRepaintCoverageRect()` 包含 Tooltip。
|
||||
- Canvas 旧逻辑用同一 coverage 同时决定“是否补画兄弟”和“是否刷新兄弟背景快照”。
|
||||
- 最近相关改动:[可选] Tooltip coverage 纳入托管重绘链。
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间: 2026-05-11
|
||||
- 最后更新时间: 2026-05-11
|
||||
- 修复版本: 当前工作区
|
||||
- 验证版本: KEY1 ~ KEY6 编译通过,待用户手测
|
||||
- 备注:[可选]
|
||||
@@ -1,93 +0,0 @@
|
||||
# Fix-BUG-20260409-0001
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260409-0001
|
||||
- 关联 BUG ID: BUG-20260409-0001
|
||||
- 修复目标: 收紧对话框快照、遮挡、事件分发和统一提交重绘链
|
||||
- 状态:已完成 / 待持续回归
|
||||
- 负责人:Codex 协作修改
|
||||
- 分支 / 版本:`master`
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因:
|
||||
- 旧实现中“作废快照”和“回贴旧背景”混在一起,resize 时容易把旧画面贴回屏幕。
|
||||
- 非模态对话框与底层控件之间缺少明确的事件阻断和 hover 清理机制。
|
||||
- 重绘长期依赖控件即时局部绘制,导致底层先画、对话框后补,容易闪烁或层级错乱。
|
||||
- 标题曾作为独立 `Label` 存在,导致额外一层背景快照,时序敏感。
|
||||
- 触发条件:
|
||||
- 非模态遮挡下的 hover / click
|
||||
- 模态或非模态 resize
|
||||
- 对话框打开 / 关闭
|
||||
- 为什么之前没发现:
|
||||
- 问题主要集中在“高频交互 + 对话框遮挡 + resize”组合路径
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路:
|
||||
- 拆分快照语义
|
||||
- 收紧对话框事件消费
|
||||
- 让 `Dialog` 在窗口变化时只重新居中
|
||||
- 将标题绘制并入 `Dialog`
|
||||
- 把重绘推进到窗口统一收口
|
||||
- 关键改动:
|
||||
- `discardBackground()` 与 `invalidateBackgroundSnapshot()` 分离
|
||||
- `Dialog` 标题改为直接绘制,不再使用独立 `Label`
|
||||
- `Dialog::handleEvent()` 对自身区域内鼠标事件统一吞掉
|
||||
- 对话框关闭及遮挡场景补发合成 `WM_MOUSEMOVE`,清理底层 hover
|
||||
- `Window` 引入托管重绘,先重绘受影响 root,再补画相交对话框
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `Window.h / Window.cpp`
|
||||
- `Dialog.h / Dialog.cpp`
|
||||
- `Control.h / Control.cpp`
|
||||
- `Canvas.cpp`
|
||||
- `TabControl.cpp`
|
||||
- `Button.cpp`
|
||||
- `TextBox.cpp`
|
||||
- 影响的 API / 行为:
|
||||
- `Dialog` 在窗口变化时的语义固定为“只重新居中,不拉伸”
|
||||
- 非模态对话框覆盖区域内不再允许鼠标事件穿透到底层
|
||||
- 关键约束 / 不变量:
|
||||
- 对话框始终绘制在普通控件之上
|
||||
- 托管分发期间控件不直接提交绘制
|
||||
- resize 和对话框开关仍走整场景重绘兜底
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围:
|
||||
- 所有包含 `Dialog`、按钮 hover、遮挡重绘的场景
|
||||
- 兼容性影响:有(行为更严格,底层控件不再穿透响应)
|
||||
- 行为变化:有(对话框 resize 只居中;遮挡区域 hover 不再透传)
|
||||
- 性能影响:有正向变化(减少无意义整场景补画);同时在某些兜底场景仍保留整场景重绘
|
||||
- 回归风险:
|
||||
- 托管重绘 root 选择不当可能导致嵌套容器局部重绘异常
|
||||
- 需持续回归 `Canvas / TabControl / Table / Dialog` 组合场景
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 在 `KEY == 2 / 4` 中打开非模态对话框,快速在遮挡区域附近 hover 和点击。
|
||||
2. 在 `KEY == 3 / 4` 中打开模态对话框并拖动窗口大小。
|
||||
3. 关闭对话框后观察底层按钮 hover 恢复。
|
||||
|
||||
- 验证结果:
|
||||
- 穿透、残影、关闭后 hover 不恢复等主问题已压住。
|
||||
- 回归检查:
|
||||
- 保留对“快速划过多个按钮时偶发一帧双高亮”的已知限制记录。
|
||||
- 验证证据:
|
||||
- 静态推演 + 多轮编译验证 + 用户回归反馈
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit:
|
||||
- `4a6e153`
|
||||
- `7f8431a`
|
||||
- `b07a4ec`
|
||||
- 当前工作区未提交阶段
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:该修复跨多个阶段逐步收敛,不是一次性完成
|
||||
@@ -1,88 +0,0 @@
|
||||
# Fix-BUG-20260409-0002
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260409-0002
|
||||
- 关联 BUG ID: BUG-20260409-0002
|
||||
- 修复目标: 收紧资源所有权,补齐基础边界防御,修正生命周期与接口语义问题
|
||||
- 状态:已完成 / 待持续回归
|
||||
- 负责人:Codex 协作修改
|
||||
- 分支 / 版本:`master`
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因:
|
||||
- 部分对象持有关系不清晰,析构顺序对 EasyX 上下文敏感。
|
||||
- `Table / Label` 等基础控件对空状态和动态变化处理不足。
|
||||
- `MessageBox` 曾通过历史接口越权干预 `Dialog` 初始化。
|
||||
- 触发条件:
|
||||
- 析构时机
|
||||
- 空数据 / 列数不一致
|
||||
- 动态变更文本
|
||||
- 为什么之前没发现:
|
||||
- 很多问题集中在边界输入和静态结构设计,而非主路径功能
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路:
|
||||
- 收紧资源所有权
|
||||
- 明确析构顺序
|
||||
- 给空状态和列数不一致加防御
|
||||
- 让接口更贴合真实职责
|
||||
- 关键改动:
|
||||
- `Window` 析构先清空控件树,再 `closegraph()`
|
||||
- `Control / Window / Button / Table` 中一批资源改为明确所有权
|
||||
- `Table` 增加空状态、列数规范化、分页下限保护、表高计算
|
||||
- `Label::setText()` 重新计算尺寸并处理旧快照
|
||||
- `MessageBox` 移除外层 `setInitialization(true)` 调用
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `Control.h / Control.cpp`
|
||||
- `Window.h / Window.cpp`
|
||||
- `Button.h / Button.cpp`
|
||||
- `Table.h / Table.cpp`
|
||||
- `Label.cpp`
|
||||
- `MessageBox.cpp`
|
||||
- `TextBox.cpp`
|
||||
- 影响的 API / 行为:
|
||||
- `Button` 回调 setter 语义收正常
|
||||
- `MessageBox` 不再依赖外层强制初始化 `Dialog`
|
||||
- 关键约束 / 不变量:
|
||||
- 宿主图形上下文必须在控件销毁后再关闭
|
||||
- 表格行数据必须与表头列数对齐
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围:
|
||||
- 基础控件稳定性与维护性
|
||||
- 兼容性影响:低
|
||||
- 行为变化:有(边界输入会被规范化,不再放任异常状态继续传播)
|
||||
- 性能影响:无明显负担
|
||||
- 回归风险:
|
||||
- 主要集中在表格布局与分页按钮链路
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `Window / Control / Table / Label / MessageBox / TextBox` 相关文件。
|
||||
2. 逻辑推演空表头、长行数据、动态文本变化和窗口关闭链路。
|
||||
3. 在现有测试用例中回归表格和消息框基本功能。
|
||||
|
||||
- 验证结果:
|
||||
- 已消除确定性越界和明显生命周期风险。
|
||||
- 回归检查:
|
||||
- 字符集相关问题暂不在本次修复范围内。
|
||||
- 验证证据:
|
||||
- 静态审查 + 编译通过
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit:
|
||||
- `dde570a` 之后的早期工作区修改
|
||||
- `4a6e153`
|
||||
- `7f8431a`
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:这部分修复多为基础治理,部分早期修改发生在仓库初始化之前
|
||||
@@ -1,80 +0,0 @@
|
||||
# Fix-BUG-20260409-0003
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: `Fix-BUG-20260409-0003`
|
||||
- 关联 BUG ID: `BUG-20260409-0003`
|
||||
- 修复目标: 取消对合法大跨度最大化的误拦截,仅保留非法尺寸保护
|
||||
- 状态:已完成 / 待用户回归
|
||||
- 负责人:Codex 协作修改
|
||||
- 分支 / 版本:`master`
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 旧逻辑问题:
|
||||
- `runEventLoop()` 的 resize 收口中存在“跨度保护”:
|
||||
- `abs(finalW - width) > 1000 || abs(finalH - height) > 1000`
|
||||
- 命中后直接跳过整个收口流程。
|
||||
- 为什么会出问题:
|
||||
- 该判断关注的是“变化跨度”,而不是“尺寸是否合法”。
|
||||
- 在大屏、窗口初始尺寸较小、直接最大化等正常场景下,跨度超过 `1000` 很常见。
|
||||
- 为什么之前未必稳定暴露:
|
||||
- 当初始尺寸较大时,最大化跨度较小,不会命中阈值。
|
||||
- 先手动拉大一点后再最大化,也会避开该阈值。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路:
|
||||
- 删除“跨度过大即跳过”的保护语义。
|
||||
- 改成“仅非法客户区尺寸才跳过”。
|
||||
- 保留关键 `DEBUG` 日志,方便追踪极端尺寸帧。
|
||||
- 关键改动:
|
||||
- 删除旧的 `>1000` 误拦截分支。
|
||||
- 引入虚拟桌面尺寸:
|
||||
- `SM_CXVIRTUALSCREEN`
|
||||
- `SM_CYVIRTUALSCREEN`
|
||||
- 仅在以下情况下跳过本次收口:
|
||||
- `actualWidth <= 0`
|
||||
- `actualHeight <= 0`
|
||||
- `actualWidth > max(10000, virtualScreenWidth * 2)`
|
||||
- `actualHeight > max(10000, virtualScreenHeight * 2)`
|
||||
- 对“大跨度但合法”的 resize 增加 `DEBUG` 日志,但继续执行收口。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `Window.cpp`
|
||||
- `Window::runEventLoop()`
|
||||
- 输出日志:
|
||||
- `尺寸调整被非法尺寸保护跳过`
|
||||
- `检测到大跨度尺寸调整,继续执行收口`
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 对外行为变化:
|
||||
- 有。直接最大化不再被 `>1000` 阈值拦截。
|
||||
- 兼容性影响:
|
||||
- 正向兼容。正常拖拽 resize、正常最大化都仍然走原有收口流程。
|
||||
- 风险:
|
||||
- 如果系统偶发产生一次极端异常客户区尺寸,可能会更多依赖当前“非法尺寸保护”而不是原来的跨度阈值。
|
||||
- 但相较于误杀合法最大化,这个风险更合理。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证方式:
|
||||
1. 在 `>1000` 旧保护分支中先加 `DEBUG` 日志,确认 `KEY == 1 / 2` 命中。
|
||||
2. 替换为非法尺寸保护后,编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)。
|
||||
3. 按 `KEY == 1 ~ 4` 做逻辑推演。
|
||||
- 理论结果:
|
||||
- `KEY == 1 / 2`:现在应打印“大跨度但继续执行收口”日志,并继续完成 `尺寸调整已完成`。
|
||||
- `KEY == 3 / 4`:原本正常,行为保持一致。
|
||||
- 备注:
|
||||
- 当前这轮无法直接替用户跑 GUI 自动化,只能做逻辑推演和源码级编译验证。
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit:
|
||||
- `f567369`:修改前快照
|
||||
- 当前工作区:已替换跨度保护,待提交
|
||||
- 发布版本:待定
|
||||
- 备注:
|
||||
- 同轮顺手删除了 `Window.cpp` 中一个未使用的 `ExMessage mm;` 遗留变量。
|
||||
@@ -1,88 +0,0 @@
|
||||
# Fix-BUG-20260410-0004
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: `Fix-BUG-20260410-0004`
|
||||
- 关联 BUG ID: `BUG-20260410-0004`
|
||||
- 修复目标: 让按钮 Tooltip 在鼠标移出后立即回贴并消失,不再残留在屏幕上
|
||||
- 状态:已验证
|
||||
- 负责人: Codex 协作修改
|
||||
- 分支 / 版本: 当前工作区 / 下一版本开发中
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因:
|
||||
- Tooltip 由 `Button` 在绘制末尾以内置浮层方式直接补画,不属于独立控件树节点。
|
||||
- 隐藏路径里根据托管分发状态分叉处理,在托管分发阶段仅调用 `invalidateBackgroundSnapshot()`,只作废 Tooltip 快照,不执行回贴。
|
||||
- 当本轮后续重绘只覆盖按钮本体区域时,Tooltip 占用的屏幕区域不会被擦除,因此表现为 Tooltip 留在屏幕上。
|
||||
- 触发条件:
|
||||
- Tooltip 已显示
|
||||
- 鼠标移出按钮区域,进入 `hideTooltip()` 分支
|
||||
- 为什么之前没发现:
|
||||
- 之前更关注 Tooltip 的显示、Hover 切换和对话框遮挡链路
|
||||
- “逻辑状态已隐藏,但屏幕区域无人回贴”的问题只有在当前托管重绘路径下才更稳定暴露
|
||||
- 关键证据:
|
||||
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp) 中 `hideTooltip()` 在托管分发阶段只作废快照,不回贴 Tooltip 自身区域
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路:
|
||||
- Tooltip 既然是 `Button` 的内置浮层,就按“内置浮层”语义处理隐藏逻辑
|
||||
- 隐藏时直接调用 Tooltip 现有接口回贴背景快照并作废,不再区分托管分发阶段
|
||||
- 关键改动:
|
||||
- 简化 `Button::hideTooltip()` 分支
|
||||
- 只要 `tipVisible == true`,统一调用 `tipLabel.hide()`
|
||||
- 保留 `tipHoverTick` 重置逻辑
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp)
|
||||
- `Button::hideTooltip()`
|
||||
- 影响的 API / 行为:
|
||||
- 无公开 API 变化
|
||||
- 内部行为调整为“Tooltip 隐藏时总是立即回贴其自身区域”
|
||||
- 关键约束 / 不变量:
|
||||
- Tooltip 继续作为 `Button` 内置浮层存在
|
||||
- 不引入新的 Tooltip 控件树托管协议
|
||||
- 不扩大 `Window` 托管重绘 coverage
|
||||
- 回滚点 / 开关:
|
||||
- 若后续需要回退,只需恢复 `Button::hideTooltip()` 的旧分支逻辑
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围:
|
||||
- 启用 Tooltip 的 `Button`
|
||||
- `KEY == 4` 等包含 Tooltip 的回归场景
|
||||
- 兼容性影响:无
|
||||
- 行为变化:有(Tooltip 隐藏时由“部分路径只作废快照”改为“统一立即回贴并作废”)
|
||||
- 性能影响:无
|
||||
- 回归风险:
|
||||
- Tooltip 隐藏时会发生一次即时局部回贴
|
||||
- 由于 Tooltip 本身就是按钮内置浮层,这个行为与其显示方式一致,风险相对可控
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 修改 [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp) 中 `Button::hideTooltip()` 的 Tooltip 隐藏分支
|
||||
2. 编译 `Button.cpp`
|
||||
3. 编译 `z-testDome.cpp` 并指定 `KEY=4`,确认 Tooltip 相关 demo 入口可正常编译
|
||||
|
||||
- 验证结果:
|
||||
- 代码已完成修改
|
||||
- 源码级编译验证通过
|
||||
- 基于当前代码链路推演,Tooltip 离开按钮后会立即回贴自身区域,不再依赖按钮本体重绘覆盖 Tooltip 区域
|
||||
- 回归检查:
|
||||
- 未做 GUI 手动交互回归
|
||||
- 需用户进一步确认实际 Tooltip 行为
|
||||
- 验证证据:
|
||||
- `Button.cpp` 编译通过
|
||||
- `z-testDome.cpp /DKEY=4` 编译通过
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 当前工作区未提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:
|
||||
- 本次修复未改动 Tooltip 对外接口,只调整内部隐藏路径
|
||||
@@ -1,83 +0,0 @@
|
||||
# Fix-BUG-20260415-0005
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260415-0005
|
||||
- 关联 BUG ID: BUG-20260415-0005
|
||||
- 修复目标: 收口局部重绘提交后的 overlay 兄弟补画机制
|
||||
- 状态:已完成
|
||||
- 负责人: Codex 协作修改
|
||||
- 分支 / 版本: 当前工作区
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因:
|
||||
- 局部重绘提交只重画了 dirty root 或 dirty child,没有按父容器真实绘制顺序把 coverage 上方相交的兄弟补画回来。
|
||||
- 触发条件:
|
||||
- 下层区域写入像素后,上层 sibling 与本次 coverage 相交。
|
||||
- 为什么之前没发现:[可选]
|
||||
- 旧场景多集中在对话框覆盖链,普通 sibling overlay 问题没有被系统化回归。
|
||||
- 关键证据:[可选]
|
||||
- `KEY5` 中 `Table` 翻页覆盖顶层粉色浮层可稳定复现。
|
||||
- 同父 `Canvas` 相交子控件在局部重绘后会出现相同症状。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路:
|
||||
- 不做控件之间的长期联动标记。
|
||||
- 改为由父容器在局部重绘提交后,按真实绘制顺序动态补画 coverage 上方相交的直接绘制单元。
|
||||
- 关键改动:
|
||||
- `Window`:补画普通顶层控件 overlay,而不再只补 `Dialog`
|
||||
- `Canvas`:补画 coverage 上方相交的直接子控件
|
||||
- `TabControl`:按自己的真实绘制顺序补画页签按钮与页面
|
||||
- overlay 补画前统一 `invalidateBackgroundSnapshot()`,避免旧快照反贴旧背景
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
|
||||
- 影响的 API / 行为:[可选]
|
||||
- 无对外 API 变化
|
||||
- 局部重绘行为更严格遵守视觉合成顺序
|
||||
- 关键约束 / 不变量:[可选]
|
||||
- 父容器只处理自己的直接绘制单元
|
||||
- overlay 补画顺序必须与实际 `draw()` 顺序一致
|
||||
- 不升级为整窗 / 整容器重绘,除非父容器自身快照或 dirty 条件不满足
|
||||
- 回滚点 / 开关:[可选]
|
||||
- 无单独开关,回滚需回退相关局部重绘提交逻辑
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围:
|
||||
- `Window / Canvas / TabControl` 的局部重绘提交路径
|
||||
- 兼容性影响:无
|
||||
- 行为变化:有(局部重绘后会补画上层 overlay)
|
||||
- 性能影响:有(增加必要的 overlay 补画),但仍显著小于整窗 / 整容器重绘
|
||||
- 回归风险:
|
||||
- 若 coverage 计算不准,仍可能遗漏或多画
|
||||
- `TabControl` 局部重绘顺序必须和真实 `draw()` 顺序保持一致
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)、[`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)、[`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
|
||||
2. 编译 [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp) `KEY=5`
|
||||
3. 回归 `Table` 翻页、相交 `Canvas`、`TabControl` 页签/页面遮挡场景
|
||||
|
||||
- 验证结果:
|
||||
- 编译级验证通过
|
||||
- GUI 手动回归需继续在本机确认
|
||||
- 回归检查:[可选]
|
||||
- `Table` 分页按钮与页码链已同步收口
|
||||
- 验证证据:[可选]
|
||||
- `KEY5` 已新增更明确的 overlay 专项场景
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 未提交(当前工作区)
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
- `Dialog` 旧 synthetic move 逻辑本轮保留,后续可再评估是否纳入通用清理模型。
|
||||
@@ -1,69 +0,0 @@
|
||||
# Fix-BUG-20260415-0006
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260415-0006
|
||||
- 关联 BUG ID: BUG-20260415-0006
|
||||
- 修复目标: 让托管局部重绘能够正确提交嵌套容器中的脏后代
|
||||
- 状态:已完成
|
||||
- 负责人: Codex
|
||||
- 分支 / 版本: 当前工作区
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: 托管局部重绘提交只识别 root 的直接 dirty child,不识别“自己不脏,但下面有 dirty descendant”的直接子分支。
|
||||
- 触发条件: 深层按钮状态变化后,叶子控件已 dirty,但中间层 Canvas 自己未 dirty。
|
||||
- 为什么之前没发现: 第一阶段主要覆盖顶层和浅层容器,三层嵌套专项场景在 KEY5 重构后才稳定暴露。
|
||||
- 关键证据:
|
||||
- 按钮 hover / click 回调正常执行
|
||||
- 日志正常,但视觉状态直到更大重绘才补出来
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: 把托管重绘提交从“只认直接 dirty child”改成“识别 dirty descendant,并提升到 root 下直接脏分支提交”。
|
||||
- 关键改动:
|
||||
- 新增 `hasManagedDirtySubtree()`
|
||||
- 新增 `getManagedRepaintDirectBranch(root)`
|
||||
- `Canvas / TabControl` 局部提交时,直接子单元若拥有 dirty descendant,也继续 `commitManagedRepaint()`
|
||||
- `Window::requestManagedRepaint()` 的 coverage 从最深叶子提升到 root 下直接脏分支
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `Control.h / Control.cpp`
|
||||
- `Canvas.cpp`
|
||||
- `TabControl.cpp`
|
||||
- `Window.cpp`
|
||||
- 影响的 API / 行为:无公开 API 变化
|
||||
- 关键约束 / 不变量:
|
||||
- dirty descendant 不能再被中间层漏掉
|
||||
- 托管局部提交顺序仍需保持与真实绘制顺序一致
|
||||
- 回滚点 / 开关:无
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: 嵌套 Canvas、页内嵌套容器、深层按钮 hover / click / tooltip 状态刷新
|
||||
- 兼容性影响:无
|
||||
- 行为变化:有(深层按钮视觉状态会及时刷新)
|
||||
- 性能影响:有(coverage 更保守,局部补画可能略增)
|
||||
- 回归风险:
|
||||
- 局部提交 coverage 扩大后,上层 overlay 补画次数可能增加
|
||||
- 需重点回归嵌套容器与顶层 overlay 场景
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `Control.cpp / Canvas.cpp / TabControl.cpp / Window.cpp / z-testDome.cpp`
|
||||
2. 使用 KEY5 三层嵌套区作为主回归入口
|
||||
3. 检查深层按钮 hover / press / release 是否能即时刷新
|
||||
|
||||
- 验证结果: 编译通过;逻辑推演闭合;GUI 需用户本机手测
|
||||
- 回归检查:KEY5 三层嵌套、页内嵌套、overlay 补画主线
|
||||
- 验证证据:编译级验证通过
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 未单独提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:该修复为机制修正,不是 KEY5 专项补丁
|
||||
@@ -1,71 +0,0 @@
|
||||
# Fix-BUG-20260415-0007
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260415-0007
|
||||
- 关联 BUG ID: BUG-20260415-0007
|
||||
- 修复目标: 让托管 coverage 与 overlay 补画基于实际绘制范围,而非控件本体 bounds
|
||||
- 状态:已完成
|
||||
- 负责人: Codex
|
||||
- 分支 / 版本: 当前工作区
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: `Window / Canvas / TabControl` 的 coverage 计算链默认使用 `getBoundsRect()`,没有把 Tooltip 这类附加绘制区域纳入。
|
||||
- 触发条件: 控件本体未与 overlay 相交,但附加绘制区域超出本体并写入更上层区域。
|
||||
- 为什么之前没发现: 第一轮主要解决普通控件和顶层 overlay;Tooltip 属于本体外附加绘制,直到 C / D 交界回归才稳定暴露。
|
||||
- 关键证据:
|
||||
- 顶层按钮 Tooltip 正常,但容器内按钮 Tooltip 压到 D 橙区
|
||||
- 根因与按钮本体 bounds 无关,与 Tooltip 超出范围有关
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: 引入“实际绘制 coverage”接口,让托管重绘与局部提交统一走真实覆盖范围。
|
||||
- 关键改动:
|
||||
- 在 `Control` 增加 `getManagedRepaintCoverageRect()`
|
||||
- `Button` 在 Tooltip 可见时返回“按钮矩形 + Tooltip 矩形”的并集
|
||||
- `Canvas / TabControl / Table` 将可见内部绘制单元 coverage 递归并入
|
||||
- `Window::requestManagedRepaint()` 和顶层 overlay 重组改为走 coverage 接口
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `Control.h / Control.cpp`
|
||||
- `Button.h / Button.cpp`
|
||||
- `Canvas.h / Canvas.cpp`
|
||||
- `TabControl.h / TabControl.cpp`
|
||||
- `Table.h / Table.cpp`
|
||||
- `Window.cpp`
|
||||
- 影响的 API / 行为:新增内部 virtual 接口,无公开 API 变化
|
||||
- 关键约束 / 不变量:
|
||||
- Tooltip 智能选位不在本次修复范围内
|
||||
- overlay 补画继续按父容器真实绘制顺序工作
|
||||
- 回滚点 / 开关:无
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: Tooltip、overlay 补画、复杂容器内附加绘制区域
|
||||
- 兼容性影响:无
|
||||
- 行为变化:有(Tooltip 与 overlay 层级恢复正确)
|
||||
- 性能影响:有(coverage 更保守,可能触发更多 overlay 补画)
|
||||
- 回归风险:
|
||||
- coverage 扩大后顶层/容器层局部提交会多画一些 overlay
|
||||
- 需重点回归 Tooltip 与多跳 overlay 场景
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `Control.cpp / Button.cpp / Canvas.cpp / TabControl.cpp / Table.cpp / Window.cpp / z-testDome.cpp`
|
||||
2. 在 KEY5 中验证 C 米区容器内按钮 Tooltip 与 D 橙区的遮挡关系
|
||||
3. 回归顶层按钮 Tooltip、页签按钮 Tooltip、浮层按钮 Tooltip
|
||||
|
||||
- 验证结果: 编译通过;逻辑推演闭合;GUI 需用户本机手测
|
||||
- 回归检查:KEY5 C/D 交界区、TabControl 页签、三层嵌套按钮 Tooltip
|
||||
- 验证证据:编译级验证通过
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 未单独提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:Tooltip 智能选位明确延期,本次只修“coverage 正确性”
|
||||
@@ -1,75 +0,0 @@
|
||||
# Fix-BUG-20260415-0008
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260415-0008
|
||||
- 关联 BUG ID: BUG-20260415-0008
|
||||
- 修复目标: 修正 TabControl 页签层级和重复激活已激活页签的回调链
|
||||
- 状态:已完成
|
||||
- 负责人: Codex
|
||||
- 分支 / 版本: 当前工作区
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因:
|
||||
- `TabControl::draw()` 与局部重绘顺序为“先页签按钮,后页面”,导致页签 Tooltip 被页面盖掉。
|
||||
- `Button::setButtonClick()` 对 `TOGGLE` 没有做“同状态短路”,外部重复激活同一页签会重复触发 `onToggleOnCallback`。
|
||||
- `TabControl::setActiveIndex()` 也没有对“目标已是当前激活页”做保护,导致当前可见页的 `onWindowResize()` / `setIsVisible(true)` 链被重复执行。
|
||||
- 触发条件:
|
||||
- 任意页打开时触发页签 Tooltip
|
||||
- 外部对已激活页签再次 `setActiveIndex()`
|
||||
- 为什么之前没发现: KEY1 外部按钮重复激活场景和 KEY5 页签 Tooltip 场景是后续专项回归才补出来的。
|
||||
- 关键证据:
|
||||
- 所有页关闭时页签 Tooltip 正常,说明问题在页面层级覆盖
|
||||
- 只点一次外部激活正常,重复激活后残影出现,说明问题在重复回调链
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路:
|
||||
- 把 TabControl 绘制/局部重绘顺序改为“先页面、后页签按钮”
|
||||
- 给 `TOGGLE` 状态设置和 `setActiveIndex()` 增加同状态短路
|
||||
- 关键改动:
|
||||
- `TabControl::draw()` 改成先画页面、后画页签按钮
|
||||
- `TabControl::requestRepaint(this)` 的局部提交顺序同步调整
|
||||
- `Button::setButtonClick()` 在 `TOGGLE` 同状态时直接返回
|
||||
- `TabControl::setActiveIndex()` 若目标已是当前激活页则直接返回
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `TabControl.cpp`
|
||||
- `Button.cpp`
|
||||
- 影响的 API / 行为:
|
||||
- 重复设置相同 `TOGGLE` 状态不再重复触发回调
|
||||
- 关键约束 / 不变量:
|
||||
- 页签按钮视觉层级应始终高于页面
|
||||
- 外部重复激活同一页签不应重新走页面显示链
|
||||
- 回滚点 / 开关:无
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: TabControl 页签 Tooltip、外部页签激活、页内超出页面边界的复合控件
|
||||
- 兼容性影响:有(收紧了 TOGGLE 同状态重复 set 的回调语义)
|
||||
- 行为变化:有(页签 Tooltip 层级恢复正确;重复激活同一页签不再重复触发回调)
|
||||
- 性能影响:无
|
||||
- 回归风险:
|
||||
- 如有外部代码依赖“同状态重复 set 也要重复触发回调”,该行为将被收掉
|
||||
- 需重点回归 KEY1 和 KEY5 的 TabControl 场景
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `Button.cpp / TabControl.cpp`
|
||||
2. 编译 `z-testDome.cpp` 的 `KEY=1`
|
||||
3. 回归 `KEY5` 页签 Tooltip 与 `KEY1` 外部按钮重复激活页签场景
|
||||
|
||||
- 验证结果: 编译通过;逻辑推演闭合;GUI 需用户本机手测
|
||||
- 回归检查:KEY1 页签 1 表格超出区、KEY5 页签 Tooltip
|
||||
- 验证证据:编译级验证通过
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 未单独提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:此修复不包含 TabControl 其它内部布局专题
|
||||
@@ -1,55 +0,0 @@
|
||||
# Fix-BUG-20260511-0009
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260511-0009
|
||||
- 关联 BUG ID: BUG-20260511-0009
|
||||
- 修复目标: 保证 `TabControl::indexOf()` 未命中时返回 `-1`
|
||||
- 状态:已完成
|
||||
- 负责人: Codex
|
||||
- 分支 / 版本: 当前工作区
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: `indexOf()` 循环结束后直接返回 `idx`,没有显式处理未命中分支。
|
||||
- 触发条件: 查询不存在的页签文本。
|
||||
- 为什么之前没发现:[可选] 当前测试更多关注页签切换行为,未覆盖未命中返回值。
|
||||
- 关键证据:[可选] `TabControl.cpp` 旧实现未命中路径返回最后索引。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: 未找到匹配项时显式返回 `-1`。
|
||||
- 关键改动: 将 `TabControl::indexOf()` 末尾返回值从 `idx` 改为 `-1`。
|
||||
- 涉及文件 / 类 / 函数: `TabControl.cpp::TabControl::indexOf`
|
||||
- 影响的 API / 行为:[可选] 行为修正;符合函数语义。
|
||||
- 关键约束 / 不变量:[可选] 找到时仍返回真实索引。
|
||||
- 回滚点 / 开关:[可选] 无
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: 仅影响 `indexOf()` 未命中返回值。
|
||||
- 兼容性影响:无
|
||||
- 行为变化:有(未命中从最后索引修正为 `-1`)
|
||||
- 性能影响:无
|
||||
- 回归风险: 低
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `TabControl.cpp`。
|
||||
2. 编译 `z-testDome.cpp` 的 `KEY1 ~ KEY6`。
|
||||
3. 静态确认未命中路径返回 `-1`。
|
||||
|
||||
- 验证结果: 编译通过
|
||||
- 回归检查:[可选] KEY1 ~ KEY6 编译级回归通过
|
||||
- 验证证据:[可选]
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 未提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
@@ -1,57 +0,0 @@
|
||||
# Fix-BUG-20260511-0010
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260511-0010
|
||||
- 关联 BUG ID: BUG-20260511-0010
|
||||
- 修复目标: 避免局部 root 内部 coverage 扩张后漏补 Dialog / 上层 overlay
|
||||
- 状态:已完成
|
||||
- 负责人: Codex
|
||||
- 分支 / 版本: 当前工作区
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: `commitManagedRepaint()` 只执行绘制,不返回本轮实际写屏 coverage;Window 仍按登记时的初始 coverage 进行上层补画判断。
|
||||
- 触发条件: Canvas / TabControl 内部局部提交中补画了后序兄弟控件,实际写屏范围大于初始 source coverage。
|
||||
- 为什么之前没发现:[可选] 需要 Dialog、Tooltip、后序兄弟补画三者同时满足。
|
||||
- 关键证据:[可选] 临时日志中 `Canvas partial end coverage` 已与 Dialog 相交,但 `dialog overlay check` 使用旧 working coverage 结果为不相交。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: 在 Window 层进行保守兜底。root 局部提交完成后,如果 root 仍支持局部提交,则将 root 当前 managed coverage 并入上层补画判断 coverage。
|
||||
- 关键改动:
|
||||
- `Window::flushManagedRepaint()` 在 `root->commitManagedRepaint()` 后合并 `root->getManagedRepaintCoverageRect()`。
|
||||
- 只扩大上层补画判断,不改变 root 自身局部提交策略。
|
||||
- 涉及文件 / 类 / 函数: `Window.cpp::flushManagedRepaint`
|
||||
- 影响的 API / 行为:[可选] 无公开 API 变化。
|
||||
- 关键约束 / 不变量:[可选] Dialog 仍保持顶层 overlay 语义。
|
||||
- 回滚点 / 开关:[可选] 回退该 coverage 合并逻辑。
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: 托管重绘后的上层普通控件 / Dialog 补画判断。
|
||||
- 兼容性影响:无
|
||||
- 行为变化:有(Dialog / 上层控件补画更保守)
|
||||
- 性能影响:有(可能多补画少量 overlay)
|
||||
- 回归风险: 中低;主要风险是局部 root 大范围覆盖时上层补画次数增加。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `Window.cpp / Canvas.cpp`。
|
||||
2. 编译 `z-testDome.cpp /DKEY=6`。
|
||||
3. 编译 `z-testDome.cpp` 的 `KEY1 ~ KEY6`。
|
||||
|
||||
- 验证结果: 编译通过;临时日志已删除
|
||||
- 回归检查:[可选] KEY6 非模态 Dialog + Tooltip 覆盖链待用户手测
|
||||
- 验证证据:[可选]
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 未提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注: 下版本建议让 `commitManagedRepaint()` 返回 actual coverage,替代 root 全覆盖兜底。
|
||||
@@ -1,60 +0,0 @@
|
||||
# Fix-BUG-20260511-0011
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260511-0011
|
||||
- 关联 BUG ID: BUG-20260511-0011
|
||||
- 修复目标: 事件尾补收集跨 root dirty 子树,保证同轮托管提交
|
||||
- 状态:已完成
|
||||
- 负责人: Codex
|
||||
- 分支 / 版本: 当前工作区
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: `Window::requestManagedRepaint()` 只登记当前事件链触发的 source/root;回调里直接修改其他 root 的控件时,该 root 只变 dirty,没有进入 `managedRepaintItems`。
|
||||
- 触发条件: 点击一个 root 中的控件,回调修改另一个 root 下的 Label / 控件状态。
|
||||
- 为什么之前没发现:[可选] 需要跨 root 回调才能稳定暴露。
|
||||
- 关键证据:[可选] 下一次鼠标消息后 Label 才刷新,说明目标 root dirty 状态滞留。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: 在普通输入事件收口阶段、`flushManagedRepaint()` 前扫描所有顶层普通控件和可见 Dialog,发现未登记但存在 dirty 子树的 root,则补登记为托管重绘项。
|
||||
- 关键改动:
|
||||
- 新增 `Window::collectDirtyRootsForManagedRepaint()`。
|
||||
- `runEventLoop()` 在 `flushManagedRepaint()` 前调用该函数。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `Window.h`
|
||||
- `Window.cpp::collectDirtyRootsForManagedRepaint`
|
||||
- `Window.cpp::runEventLoop`
|
||||
- 影响的 API / 行为:[可选] 无公开 API 变化。
|
||||
- 关键约束 / 不变量:[可选] 只处理可见 root;已登记 root 不重复登记。
|
||||
- 回滚点 / 开关:[可选] 移除事件尾补收集调用。
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: 普通输入事件尾的托管重绘收口。
|
||||
- 兼容性影响:无
|
||||
- 行为变化:有(跨 root dirty 会更及时刷新)
|
||||
- 性能影响:有(事件尾多一次顶层 dirty 子树扫描)
|
||||
- 回归风险: 中;如果存在历史残留 dirty,可能更早暴露重绘问题。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `Window.cpp`。
|
||||
2. 编译 `z-testDome.cpp /DKEY=6`。
|
||||
3. 编译 `z-testDome.cpp` 的 `KEY1 ~ KEY6`。
|
||||
|
||||
- 验证结果: 编译通过
|
||||
- 回归检查:[可选] KEY6 Right 页内按钮更新 A 区状态 Label 待用户手测
|
||||
- 验证证据:[可选]
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 未提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
@@ -1,68 +0,0 @@
|
||||
# Fix-BUG-20260511-0012
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID: Fix-BUG-20260511-0012
|
||||
- 关联 BUG ID: BUG-20260511-0012
|
||||
- 修复目标: 区分 Tooltip 临时浮层 coverage 与会污染背景快照的持久 coverage
|
||||
- 状态:已完成
|
||||
- 负责人: Codex
|
||||
- 分支 / 版本: 当前工作区
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因: 托管重绘只有一类 coverage,既用于完整绘制范围判断,也用于兄弟控件背景快照刷新判断。Tooltip 属于临时浮层,不应进入后者。
|
||||
- 触发条件: Tooltip coverage 覆盖到后序兄弟控件,兄弟控件被局部 overlay 补画。
|
||||
- 为什么之前没发现:[可选] 需要 Tooltip 与透明 Label 或带快照兄弟控件重叠。
|
||||
- 关键证据:[可选] 透明 Label 已设置 `setTextdisap(true)`,但 `draw()` 仍会先回贴背景快照。
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路: 拆分完整 coverage 与持久 coverage。Tooltip 进入完整 coverage,用于 Window/Dialog overlay 判断;但不进入持久 coverage,避免兄弟控件刷新快照时捕获 Tooltip。
|
||||
- 关键改动:
|
||||
- `Control` 新增内部虚函数 `getManagedRepaintPersistentCoverageRect()`。
|
||||
- `Button` 的完整 coverage 包含 Tooltip,持久 coverage 仅包含按钮本体。
|
||||
- `Canvas / TabControl / Table` 递归合并子控件持久 coverage。
|
||||
- `Canvas / TabControl` 局部提交中,只有命中持久 coverage 的兄弟控件才作废快照并补画。
|
||||
- 只命中 Tooltip 等临时 coverage 的兄弟控件不再补画,避免透明控件回贴旧快照擦掉 Tooltip。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `Control.h / Control.cpp`
|
||||
- `Button.h / Button.cpp`
|
||||
- `Canvas.h / Canvas.cpp`
|
||||
- `TabControl.h / TabControl.cpp`
|
||||
- `Table.h / Table.cpp`
|
||||
- 影响的 API / 行为:[可选] 新增内部 virtual 接口,无公开用户 API 变化。
|
||||
- 关键约束 / 不变量:[可选] Tooltip 智能选位仍不在本轮范围内。
|
||||
- 回滚点 / 开关:[可选] 回退持久 coverage 拆分逻辑。
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围: Tooltip、Canvas / TabControl 局部 overlay 补画、背景快照捕获。
|
||||
- 兼容性影响:无
|
||||
- 行为变化:有(Tooltip 作为临时浮层时,不再触发普通兄弟控件补画)
|
||||
- 性能影响:轻微(多维护一组矩形并集和相交判断)
|
||||
- 回归风险:
|
||||
- 如果某控件持久 coverage 定义过小,可能漏补兄弟快照。
|
||||
- Tooltip 会短暂显示在同容器后序普通控件上方,这是本轮有意选择。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `Control.cpp / Button.cpp / Canvas.cpp / TabControl.cpp / Table.cpp / Window.cpp`。
|
||||
2. 编译 `z-testDome.cpp` 的 `KEY1 ~ KEY6`。
|
||||
3. 手测 KEY6 A 区禁用按钮 Tooltip 与状态 Label 交叠。
|
||||
4. 手测 TabControl 页签 / 页面内 Tooltip。
|
||||
|
||||
- 验证结果: 编译通过;GUI 行为待用户手测
|
||||
- 回归检查:[可选] KEY6 A 区、KEY5 Tooltip/overlay、KEY4 Dialog overlay
|
||||
- 验证证据:[可选]
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit: 未提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注: 下版本可进一步引入 actual coverage 返回值和正式 coverage 诊断日志。
|
||||
@@ -1,73 +0,0 @@
|
||||
# 功能变更 ID: Feature-20260409-0001
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260409-0001
|
||||
- 标题: 基础资源所有权与生命周期收口
|
||||
- 状态:已完成
|
||||
- 类型:修改
|
||||
- 级别:L2 中等
|
||||
- 模块:Control / Window / Button / Table / Label
|
||||
- 版本 / 分支:`master`
|
||||
- 环境:Windows + EasyX + VS2022
|
||||
- 负责人:Codex 协作修改
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- 初始版本中一部分资源持有关系不明确,析构顺序和边界输入处理存在隐患。
|
||||
- 目标:
|
||||
- 提高基础稳定性与维护性。
|
||||
- 不做什么:[可选]
|
||||
- 不处理字符集体系切换问题。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要:
|
||||
- 收紧资源所有权,调整析构顺序,补齐基础控件边界防御。
|
||||
- 新增项:[可选]
|
||||
- `Control` 提供更明确的快照有效性查询和宿主窗口回溯能力。
|
||||
- 修改项:[可选]
|
||||
- `Window` 析构顺序调整
|
||||
- `Table` 行数据规范化、空状态防御、页数保护
|
||||
- `Label` 动态文本变化处理
|
||||
- 删除 / 废弃项:[可选]
|
||||
- `MessageBox` 外层强制 `Dialog` 初始化的做法被移除
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- `Control.h / Control.cpp`
|
||||
- `Window.h / Window.cpp`
|
||||
- `Button.h / Button.cpp`
|
||||
- `Table.h / Table.cpp`
|
||||
- `Label.cpp`
|
||||
- `MessageBox.cpp`
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- `Button` 回调设置接口语义更正常
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前:
|
||||
- 生命周期和边界输入更依赖使用者自觉,异常输入容易直接进入绘制或析构链。
|
||||
- 变更后:
|
||||
- 基础控件会主动规范化一部分输入,生命周期顺序更安全。
|
||||
- 兼容性说明:兼容
|
||||
- 迁移说明:[可选]
|
||||
- 无强制迁移要求
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 静态审查 + 编译验证 + 现有测试用例回归
|
||||
- 验证结果:
|
||||
- 主要基础问题已收敛
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- `BUG-20260409-0002`
|
||||
- `Fix-BUG-20260409-0002`
|
||||
- Commit:
|
||||
- 早期工作区修改 + `4a6e153` + `7f8431a`
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
- 其中一部分修改早于仓库基线
|
||||
@@ -1,71 +0,0 @@
|
||||
# 功能变更 ID: Feature-20260409-0002
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260409-0002
|
||||
- 标题: Dialog 与 MessageBox 行为调整
|
||||
- 状态:已完成
|
||||
- 类型:修改
|
||||
- 级别:L2 中等
|
||||
- 模块:Dialog / MessageBox / Window
|
||||
- 版本 / 分支:`master`
|
||||
- 环境:Windows + EasyX + VS2022
|
||||
- 负责人:Codex 协作修改
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- 初始版本中 `Dialog` 的初始化、resize、标题绘制和 `MessageBox` 外层调用关系较松散。
|
||||
- 目标:
|
||||
- 明确 `Dialog` 行为边界,让其在 resize、显示、关闭、标题更新等场景更稳定。
|
||||
- 不做什么:[可选]
|
||||
- 暂不重命名 `setInitialization()` 这类历史接口。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要:
|
||||
- `Dialog` 在窗口变化时只重新居中;标题直接由 `Dialog` 自身绘制;`MessageBox` 不再外层强制初始化。
|
||||
- 新增项:[可选]
|
||||
- `Dialog::recenterInHostWindow()`
|
||||
- 修改项:[可选]
|
||||
- `SetTitle / SetMessage / SetType` 触发重新布局
|
||||
- `Dialog` 标题不再使用独立 `Label`
|
||||
- `Dialog` 在模态和非模态场景中的重绘语义被收紧
|
||||
- 删除 / 废弃项:[可选]
|
||||
- `MessageBox::showAsync/showModal()` 中对 `setInitialization(true)` 的直接调用已移除
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- `Dialog.h / Dialog.cpp`
|
||||
- `MessageBox.cpp`
|
||||
- `Window.cpp`
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- `Dialog` 在窗口变化时的对外可观察行为改为“只居中,不拉伸”
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前:
|
||||
- `Dialog` resize 时可能重建行为与快照行为混杂,标题区域额外依赖子控件快照。
|
||||
- 变更后:
|
||||
- `Dialog` 尺寸由内容决定;窗口变化只重新居中;标题直接绘制,减少一层快照时序问题。
|
||||
- 兼容性说明:部分兼容
|
||||
- 迁移说明:[可选]
|
||||
- 如果外部逻辑曾假设 `Dialog` 会跟随窗口拉伸,现在需要改按“仅居中”理解
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- `KEY == 2 / 3 / 4` 相关场景静态推演与回归
|
||||
- 验证结果:
|
||||
- 标题更新、消息框初始化链、resize 后位置语义更稳定
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- `BUG-20260409-0001`
|
||||
- `Fix-BUG-20260409-0001`
|
||||
- Commit:
|
||||
- `4a6e153`
|
||||
- `7f8431a`
|
||||
- 当前工作区部分未提交整理
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
@@ -1,75 +0,0 @@
|
||||
# 功能变更 ID: Feature-20260409-0003
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260409-0003
|
||||
- 标题: 输入事件、hover 与遮挡交互调整
|
||||
- 状态:已完成
|
||||
- 类型:修改
|
||||
- 级别:L2 中等
|
||||
- 模块:Window / Canvas / Button / TextBox / Dialog / TabControl
|
||||
- 版本 / 分支:`master`
|
||||
- 环境:Windows + EasyX + VS2022
|
||||
- 负责人:Codex 协作修改
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- 初始版本中,hover、点击、遮挡与重叠控件之间的事件传递不够稳定。
|
||||
- 目标:
|
||||
- 建立更一致的鼠标事件消费规则,减少穿透、残留 hover 和无意义重绘。
|
||||
- 不做什么:[可选]
|
||||
- 暂不彻底解决“快速划过多按钮偶发一帧双高亮”的底层限制问题。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要:
|
||||
- 收紧 `Button / TextBox / Dialog` 的事件消费语义,并补上对话框关闭后及遮挡场景下的 hover 清理。
|
||||
- 新增项:[可选]
|
||||
- `Window::dispatchSyntheticMouseMoveToControls(...)`
|
||||
- 修改项:[可选]
|
||||
- `Button` 只在合理命中路径上吞掉鼠标事件
|
||||
- `TextBox` 纯 hover 不再处理
|
||||
- 非模态对话框覆盖区域内阻断底层穿透
|
||||
- 对话框关闭后补发合成鼠标移动同步 hover
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 大量 hover DEBUG 日志已降噪
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- `Window.cpp`
|
||||
- `Canvas.cpp`
|
||||
- `Button.cpp`
|
||||
- `TextBox.cpp`
|
||||
- `Dialog.cpp`
|
||||
- `TabControl.cpp`
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 无新增公开 API,主要是行为语义变化
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前:
|
||||
- 遮挡区域 hover/click 可能穿透;对话框关闭后底层 hover 可能残留。
|
||||
- 变更后:
|
||||
- 交互更偏向“命中即消费,关闭后同步清理 hover,遮挡区域不透传”。
|
||||
- 兼容性说明:部分兼容
|
||||
- 迁移说明:[可选]
|
||||
- 若旧代码依赖非模态对话框下方控件还能收到事件,这种行为已被收紧
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- `KEY == 2 / 3 / 4` 中对按钮、Tab、Table 分页、对话框关闭链做回归
|
||||
- 验证结果:
|
||||
- 主要交互一致性问题已收敛
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- `BUG-20260409-0001`
|
||||
- `Fix-BUG-20260409-0001`
|
||||
- Commit:
|
||||
- `7f8431a`
|
||||
- `b07a4ec`
|
||||
- 当前工作区未提交阶段
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
@@ -1,69 +0,0 @@
|
||||
# 功能变更 ID: Feature-20260409-0004
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260409-0004
|
||||
- 标题: 测试用例与可观测性调整
|
||||
- 状态:已完成
|
||||
- 类型:修改
|
||||
- 级别:L1 轻量
|
||||
- 模块:z-testDome / 日志
|
||||
- 版本 / 分支:`master`
|
||||
- 环境:Windows + EasyX + VS2022
|
||||
- 负责人:Codex 协作修改
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- 对话框、遮挡、hover、分页和页签的组合问题需要更有针对性的回归入口。
|
||||
- 目标:
|
||||
- 增加一组综合用例,并降低 DEBUG 日志噪音。
|
||||
- 不做什么:[可选]
|
||||
- 不改变原有 `KEY == 1 / 2 / 3` 的默认运行逻辑
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要:
|
||||
- 新增 `KEY == 4` 的综合回归用例,覆盖对话框遮挡、Tab、Table、模态 resize 等场景。
|
||||
- 新增项:[可选]
|
||||
- `KEY == 4` 测试分支
|
||||
- 修改项:[可选]
|
||||
- hover 相关日志降噪
|
||||
- 事件日志补充更清晰的事件名
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 无
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- `z-testDome.cpp`
|
||||
- `Window.cpp`
|
||||
- `Canvas.cpp`
|
||||
- `Button.cpp`
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 无
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前:
|
||||
- 回归更多依赖手工拼场景,hover 日志容易刷屏。
|
||||
- 变更后:
|
||||
- 有固定综合回归入口,日志更适合看关键路径。
|
||||
- 兼容性说明:兼容
|
||||
- 迁移说明:[可选]
|
||||
- 测试 `KEY == 4` 需手动切换宏值
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 编译 `z-testDome.cpp`
|
||||
- 静态推演 `KEY == 4` 控件创建、显示、对话框触发和关闭链
|
||||
- 验证结果:
|
||||
- 当前默认 `KEY == 2` 未被带坏,`KEY == 4` 逻辑链自洽
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- 可作为 `BUG-20260409-0001` 相关问题的回归入口
|
||||
- Commit:
|
||||
- 当前工作区未提交阶段
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
@@ -1,69 +0,0 @@
|
||||
# 功能变更 ID: Feature-20260409-0005
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260409-0005
|
||||
- 标题: 开发记录与模板体系整理
|
||||
- 状态:已完成
|
||||
- 类型:修改
|
||||
- 级别:L1 轻量
|
||||
- 模块:文档模板 / 开发记录
|
||||
- 版本 / 分支:`master`
|
||||
- 环境:本地工作区
|
||||
- 负责人:Codex 协作修改
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- 需要对本轮协作中的大量修改建立可持续维护的记录体系。
|
||||
- 目标:
|
||||
- 将“问题、修复、功能变化、模块设计”这四类信息分层记录,避免后续继续靠聊天上下文回忆。
|
||||
- 不做什么:[可选]
|
||||
- 不把模板设计成覆盖所有场景的一份大而全文档
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要:
|
||||
- 重写四个文档模板,修正编码内容,并建立统一的开发记录目录。
|
||||
- 新增项:[可选]
|
||||
- `开发记录/BUG`
|
||||
- `开发记录/Fix`
|
||||
- `开发记录/功能变更`
|
||||
- `开发记录/模块`
|
||||
- 修改项:[可选]
|
||||
- `文档模板` 目录下四个模板重写并重新划分职责
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 原模板中“功能变更”和“模块说明”职责重叠的写法已废弃
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- `文档模板/*.md`
|
||||
- `开发记录/*.md`
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 无
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前:
|
||||
- 模板存在乱码,功能变更和模块说明边界不清,小改动记录成本过高。
|
||||
- 变更后:
|
||||
- 模板职责明确,记录粒度更合理,可以持续沉淀协作历史。
|
||||
- 兼容性说明:兼容
|
||||
- 迁移说明:[可选]
|
||||
- 后续新增记录优先按新模板写,不再沿用旧模板内容
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 读取模板文件确认编码与内容正常
|
||||
- 创建第一批记录文件验证模板可用
|
||||
- 验证结果:
|
||||
- 模板可正常使用,第一批记录已按新体系落地
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- 无
|
||||
- Commit:
|
||||
- 当前工作区未提交阶段
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
@@ -1,54 +0,0 @@
|
||||
# Feature-20260409-0006
|
||||
|
||||
> 适用场景:记录小到中等规模的接口、行为、默认值、交互和内部机制变化。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: `Feature-20260409-0006`
|
||||
- 标题: 窗口最大化收口保护从跨度阈值改为非法尺寸判断
|
||||
- 类型:修改
|
||||
- 级别:L2
|
||||
- 模块:Window / Resize
|
||||
- 状态:已完成 / 待用户回归
|
||||
- 关联:
|
||||
- `BUG-20260409-0003`
|
||||
- `Fix-BUG-20260409-0003`
|
||||
|
||||
## 背景
|
||||
|
||||
- 旧实现把“大跨度尺寸变化”视为异常帧。
|
||||
- 在初始窗口较小、显示器较大时,正常最大化会被误伤。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 删除旧的跨度保护语义:
|
||||
- 不再以 `abs(finalW - width)` 或 `abs(finalH - height)` 决定是否跳过。
|
||||
- 新增非法尺寸保护语义:
|
||||
- 仅拦截 `<= 0` 的客户区尺寸
|
||||
- 仅拦截明显超出虚拟桌面合理范围的客户区尺寸
|
||||
- 新增两类调试日志:
|
||||
- 非法尺寸跳过日志
|
||||
- 大跨度但继续执行日志
|
||||
|
||||
## 对外影响
|
||||
|
||||
- 直接最大化的行为更稳定。
|
||||
- 之前会黑背景/残影的场景,现在理论上应恢复为正常收口。
|
||||
|
||||
## 兼容性
|
||||
|
||||
- 向后兼容。
|
||||
- 不涉及公开 API 变化。
|
||||
|
||||
## 验证方式
|
||||
|
||||
- 用户回归 `KEY == 1 ~ 4`
|
||||
- 日志检查是否继续打印 `尺寸调整已完成`
|
||||
- 单文件编译验证 `Window.cpp`
|
||||
|
||||
## 落地信息
|
||||
|
||||
- 涉及文件:
|
||||
- `Window.cpp`
|
||||
- 落地状态:
|
||||
- 当前工作区已完成,待提交
|
||||
@@ -1,78 +0,0 @@
|
||||
# 功能变更 ID: Feature-20260415-0008
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260415-0008
|
||||
- 标题: KEY5 第二阶段专项回归场景增强
|
||||
- 状态:已完成
|
||||
- 类型:修改
|
||||
- 级别:L2 中等
|
||||
- 模块: 测试用例 / 回归场景
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows + EasyX
|
||||
- 负责人: Codex 协作修改
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- 旧版 `KEY5` 已能覆盖第一阶段布局主线,但场景层次不够集中,说明文字偏长,部分区域排版紧。
|
||||
- 第二阶段引入了内容驱动、overlay 补画、容器职责边界等新验证点,旧 `KEY5` 覆盖不完整。
|
||||
- 目标:
|
||||
- 把 `KEY5` 重构成第二阶段专项回归入口。
|
||||
- 增加三层 `Canvas` 嵌套、通用 overlay、跨容器 hover 清理、`Label` 内容驱动规则验证。
|
||||
- 不做什么:[可选]
|
||||
- 不追求视觉美化。
|
||||
- 不把测试用例变成完整 demo 页。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要:
|
||||
- 重构 `KEY5` 的区域布局与说明文案。
|
||||
- 引入三层嵌套 `Canvas` 场景。
|
||||
- 增加 `Window / Canvas / TabControl / Table` 四类关键回归点。
|
||||
- 新增项:[可选]
|
||||
- 三层 `Canvas` 嵌套与多层锚点链回归。
|
||||
- `Label` 文本变化 / 样式变化按钮。
|
||||
- 跨容器相邻按钮 hover/tooltip 回归点。
|
||||
- 顶层浮层覆盖 `Table` 的 overlay 回归点。
|
||||
- 修改项:[可选]
|
||||
- 缩短说明 `Label` 文案,减少普通排版重叠。
|
||||
- 重新组织分区顺序,提升单区可读性。
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 删除过长、重复的说明文案。
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 无
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前:
|
||||
- `KEY5` 主要覆盖第一阶段布局规则,层次和交互链回归点相对分散。
|
||||
- 说明文字较长,部分区域排版偏挤。
|
||||
- 变更后:
|
||||
- `KEY5` 成为第二阶段主回归入口。
|
||||
- 三层嵌套、overlay、跨容器 hover、`Label` 内容驱动、`Table` 分页与页码都能直接观察。
|
||||
- 兼容性说明:兼容
|
||||
- 迁移说明:[可选]
|
||||
- 仍然通过 `#define KEY 5` 进入,无额外迁移。
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 编译 `z-testDome.cpp /DKEY=5`
|
||||
- 手动观察窗口 resize、hover、tooltip、overlay、分页和 `Label` 变化
|
||||
- 验证结果:
|
||||
- 编译级验证通过
|
||||
- GUI 手动验证待本机执行
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- `BUG-20260415-0005`
|
||||
- `Fix-BUG-20260415-0005`
|
||||
- Commit: 未提交(当前工作区)
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
- `Label` 样式变化验证明确依赖 `setDirty(true)` 使用约定。
|
||||
@@ -1,87 +0,0 @@
|
||||
# 功能变更 ID: Feature-20260416-0009
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260416-0009
|
||||
- 标题: KEY2 使用新布局 API 重构与联动刷新
|
||||
- 状态:开发中
|
||||
- 类型:修改
|
||||
- 级别:L2 中等
|
||||
- 模块: 测试用例 / Layout API 迁移样例
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows + EasyX
|
||||
- 负责人: Codex 协作修改
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- `KEY2` 原有布局主要依赖旧 `setAnchor(...)` 语义,无法清晰表达“固定尺寸 + 比例位移”。
|
||||
- 32 位位号标签与位按钮分别按各自尺寸参与比例位移,窗口拉伸后会发生错位。
|
||||
- 位按钮点击后,下面的十六进制、十进制与二进制显示区刷新链散落在多个回调中,维护成本高且容易漏更新。
|
||||
- 目标:
|
||||
- 把 `KEY2` 改成新布局 API 的首个完整迁移样例。
|
||||
- 修复位标签与位按钮 resize 后错位问题。
|
||||
- 让位按钮、位操作按钮、配置区按钮共用一条统一的显示刷新链。
|
||||
- 不做什么:[可选]
|
||||
- 不重构 `KEY1 / KEY5`
|
||||
- 不引入 `KEY2` 以外的新功能
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要:
|
||||
- 重构 `KEY2` 位选择区为“每一位一个小 Canvas 单元”。
|
||||
- 使用新布局 API 重排 `KEY2` 的选择区、功能区、数值显示区、二进制显示区与配置区。
|
||||
- 抽出统一的 bit 快照、应用、显示刷新链,收口位按钮与功能按钮的联动。
|
||||
- 新增项:[可选]
|
||||
- `KEY2` 内部的布局辅助策略函数
|
||||
- `snapshotBits / applyBits / refreshDisplaysWithBits` 这类统一刷新路径
|
||||
- 修改项:[可选]
|
||||
- 32 位位标签与位按钮的组织方式
|
||||
- 顶层区域的拉伸策略
|
||||
- 一键置 0 / 一键置 1 / 左移 / 右移 / 位取反 / signed toggle 的显示刷新方式
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 废弃旧的“位标签和位按钮分别做比例位移”的选择区组织方式
|
||||
- 废弃散在多个回调中的局部显示刷新
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 无框架级 API 变化,属于测试用例迁移
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前:
|
||||
- 32 位位标签与位按钮在窗口拉伸后会发生错位。
|
||||
- 位按钮点击后,显示区更新链不统一,后续维护难度高。
|
||||
- `KEY2` 的多个区域仍依赖旧锚点表达,窗口拉伸时不够稳定。
|
||||
- 变更后:
|
||||
- 每一位作为独立单元整体位移,位标签与位按钮不再分离漂移。
|
||||
- 位按钮、一键置 0/1、位取反、左移、右移、signed toggle 均通过统一刷新链更新显示区。
|
||||
- 顶层五个区域在窗口拉伸后继续铺满可用空间,内部小控件保持固定尺寸。
|
||||
- 兼容性说明:兼容
|
||||
- 迁移说明:[可选]
|
||||
- 这是新布局 API 的示例迁移,不影响其它 `KEY` 入口。
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 编译 `z-testDome.cpp /DKEY=2`
|
||||
- 编译 `z-testDome.cpp /DKEY=5`
|
||||
- 手动验证 `KEY2`:
|
||||
- 窗口横向拉伸
|
||||
- 位按钮点击
|
||||
- 位取反 / 左移 / 右移
|
||||
- 一键置 0 / 一键置 1
|
||||
- 有符号 / 无符号切换
|
||||
- 验证结果:
|
||||
- 编译级验证通过。
|
||||
- GUI 手动验证待本机继续确认。
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- 无单独 BUG 记录,本次属于布局 API 落地后的示例迁移
|
||||
- Commit:
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
- 当前功能区采用左/中/右固定宽度面板布局;窗口变宽时可稳定铺满,若后续需要进一步适配极窄窗口,再考虑把功能区做成等比分栏。
|
||||
@@ -1,156 +0,0 @@
|
||||
# 功能变更 ID: Feature-20260418-0010
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260418-0010
|
||||
- 标题: 当前阶段测试用例矩阵与定位
|
||||
- 状态:已完成
|
||||
- 类型:修改
|
||||
- 级别:L1 轻量
|
||||
- 模块: 测试用例 / 回归策略
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows + EasyX
|
||||
- 负责人: Codex 协作修改
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- 第二阶段主线、布局策略公开 API、`KEY2` 迁移以及近期重绘 / tooltip / overlay / `TabControl` / `Table` 修复已基本收口。
|
||||
- 当前项目中同时存在 `KEY1 ~ KEY5` 多组测试入口,但各自负责覆盖什么、哪些是主集、哪些是专项补充,若不显式记录,后续容易继续依赖聊天上下文记忆。
|
||||
- 目标:
|
||||
- 把当前阶段各测试入口的职责、覆盖范围和预期行为正式写成测试矩阵。
|
||||
- 明确本阶段建议主回归集与补充专项。
|
||||
- 不做什么:[可选]
|
||||
- 不新增测试逻辑。
|
||||
- 不修改运行时代码。
|
||||
- 不把测试矩阵扩成自动化测试体系。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要:
|
||||
- 新增当前阶段测试矩阵记录。
|
||||
- 同步 `KEY1 ~ KEY5` 的阶段定位与行为预期。
|
||||
- 新增项:[可选]
|
||||
- 当前阶段建议主回归集:`KEY1 + KEY2 + KEY5`
|
||||
- Dialog / MessageBox 补充专项:`KEY4`
|
||||
- 修改项:[可选]
|
||||
- 无代码修改,仅补充测试约定与阶段定位。
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 无
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 无
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前:
|
||||
- `KEY1 ~ KEY5` 的职责划分主要依赖源码注释和聊天上下文。
|
||||
- 很难快速判断“当前阶段主回归集”与“专项补充集”分别是什么。
|
||||
- 变更后:
|
||||
- 每个 `KEY` 入口的阶段定位、覆盖重点和行为预期都有正式记录。
|
||||
- 可以按矩阵组织回归,而不再只靠记忆。
|
||||
- 兼容性说明:兼容
|
||||
- 迁移说明:[可选]
|
||||
- 无需迁移,属于当前阶段测试约定落地。
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 对照当前 [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp) 中 `KEY1 ~ KEY5` 的实际结构与近期修复范围进行人工核对。
|
||||
- 验证结果:
|
||||
- 当前矩阵与现有测试入口和阶段主线一致。
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- [Module-20260415-0004-布局系统第二阶段验收与封口.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260415-0004-布局系统第二阶段验收与封口.md)
|
||||
- [Module-20260416-0005-布局策略公开API落地.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260416-0005-布局策略公开API落地.md)
|
||||
- Commit:
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
- resize 过程中若开启高频 console 日志,可能出现一帧视觉延迟;当前判断属于调试态 I/O 现象,不按 bug 处理。
|
||||
|
||||
## 当前阶段测试矩阵
|
||||
|
||||
### KEY1:旧页签链路 + Table 超界残留回归
|
||||
|
||||
- 覆盖目标:
|
||||
- `TabControl` 重复激活同一页签
|
||||
- 切页 / 关页后的快照与清理链
|
||||
- 页内容超出页范围时的残影与恢复
|
||||
- 当前预期:
|
||||
- 外部重复激活当前页签,不再扰动当前页快照链。
|
||||
- 页签 1 中 `Table` 超出页区域的部分,在切页或关闭页签时不会残留。
|
||||
- 正常全量重绘后,不应再出现额外残影或层级错乱。
|
||||
- 阶段定位:
|
||||
- 当前主回归集之一。
|
||||
|
||||
### KEY2:公开布局 API 首个迁移样例
|
||||
|
||||
- 覆盖目标:
|
||||
- 新公开布局 API 的真实使用
|
||||
- 位选择区容器化重构
|
||||
- 显示区统一刷新链
|
||||
- 顶层区块拉伸铺满
|
||||
- 当前预期:
|
||||
- 32 位选择区中的“位号 + 位按钮”以单元整体位移,窗口拉伸后保持对齐。
|
||||
- 位按钮点击、位取反、左移、右移、一键置 0/1、签名切换,会统一刷新十六进制、十进制、上次值、本次值和 `initData`。
|
||||
- 顶层五个区块在正常拉伸 / 最大化后继续铺满窗口。
|
||||
- 当前更偏重横向铺满验证;极窄窗口下功能区进一步自适应,不作为本阶段硬指标。
|
||||
- 阶段定位:
|
||||
- 当前主回归集之一。
|
||||
- 同时是新布局 API 的首个迁移示例。
|
||||
|
||||
### KEY3:旧业务大场景保留用例
|
||||
|
||||
- 覆盖目标:
|
||||
- 保留老业务页示例
|
||||
- 观察新主线是否把旧业务场景直接打坏
|
||||
- 当前预期:
|
||||
- 登录页、`TabControl`、旧业务页基本行为保持可运行。
|
||||
- 不要求覆盖新布局 API、overlay、tooltip 或第二阶段专项行为。
|
||||
- 阶段定位:
|
||||
- 保留用例,不纳入当前主回归集。
|
||||
|
||||
### KEY4:Dialog / MessageBox 专项回归
|
||||
|
||||
- 覆盖目标:
|
||||
- 模态 / 非模态 `Dialog`
|
||||
- 遮挡交互
|
||||
- 关闭后 hover 恢复
|
||||
- 拖拽 resize
|
||||
- 当前预期:
|
||||
- 非模态 `Dialog` 遮挡底层按钮时,不应出现 hover / tooltip 穿透或残留。
|
||||
- 模态 `Dialog` 打开后拖拽窗口,标题、关闭按钮和底层恢复链保持稳定。
|
||||
- 对话框关闭后,底层按钮 hover 能及时恢复。
|
||||
- 阶段定位:
|
||||
- 当前补充专项,不纳入每轮主回归集。
|
||||
|
||||
### KEY5:第二阶段专项主回归
|
||||
|
||||
- 覆盖目标:
|
||||
- 三层 `Canvas` 嵌套
|
||||
- 跨容器 hover / tooltip
|
||||
- overlay 补画与 coverage 链
|
||||
- `TabControl`
|
||||
- `Table`
|
||||
- 页码与分页按钮
|
||||
- 当前预期:
|
||||
- 三层 `Canvas` 嵌套下,深层按钮 hover / press / release / tooltip 都能正确刷新。
|
||||
- `Window / Canvas / TabControl` 的 overlay 补画与 coverage 链闭合,不再出现遮挡错层。
|
||||
- `TabControl` 页签按钮与页面层级正确;页签 tooltip 不会再被页面盖掉。
|
||||
- `Table` 分页按钮、页码标签、与上层浮层相交时的重绘链保持正确。
|
||||
- 阶段定位:
|
||||
- 当前主回归集之一。
|
||||
- 第二阶段及重绘主线的核心专项入口。
|
||||
|
||||
## 当前建议
|
||||
|
||||
- 当前阶段建议主回归集:
|
||||
- `KEY1 + KEY2 + KEY5`
|
||||
- 当前补充专项:
|
||||
- `KEY4`
|
||||
- 暂不纳入每轮主集:
|
||||
- `KEY3`
|
||||
@@ -1,295 +0,0 @@
|
||||
# 功能变更 ID: Feature-20260509-0011
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260509-0011
|
||||
- 标题: 布局 API 使用说明与旧接口覆盖规则
|
||||
- 状态:已验证
|
||||
- 类型:新增
|
||||
- 级别:L2 中等
|
||||
- 模块: Layout API / Control
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows + EasyX
|
||||
- 负责人: Codex 协作修改
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- 当前布局系统内部已经以 `LayoutSpec` 为统一解算依据。
|
||||
- 旧接口 `setLayoutMode(...)` / `setAnchor(...)` 仍然保留,但只能表达有限的双锚点语义。
|
||||
- 新增公开布局 API 后,需要明确用户应如何设置轴向锚点、尺寸策略和固定尺寸位移策略。
|
||||
- 目标:
|
||||
- 说明新布局 API 的职责和使用方式。
|
||||
- 明确新旧接口混用时的覆盖规则。
|
||||
- 给出常见布局组合和 `KEY2` 迁移场景中的推荐写法。
|
||||
- 不做什么:[可选]
|
||||
- 不开放 `LayoutCapability` 的普通外部写入接口。
|
||||
- 不修改 `Table` 当前版本 `Y Fixed` 的能力边界。
|
||||
- 不处理 Tooltip 智能选位、`Table` 内部局部重绘、`Dialog` synthetic move 统一等后置项。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要:
|
||||
- 增加布局 API 使用说明文档。
|
||||
- 明确新接口与旧接口的覆盖关系。
|
||||
- 汇总常见布局模式的代码写法。
|
||||
- 新增项:[可选]
|
||||
- 新公开接口说明
|
||||
- 旧接口映射说明
|
||||
- 混用覆盖规则
|
||||
- 常见写法示例
|
||||
- 调试态日志说明
|
||||
- 修改项:[可选]
|
||||
- 无运行时代码修改,本文件只记录使用说明。
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 无
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- `setHorizontalLayoutSpec(...)`
|
||||
- `setVerticalLayoutSpec(...)`
|
||||
- `setHorizontalAnchors(...)`
|
||||
- `setVerticalAnchors(...)`
|
||||
- `setHorizontalSizePolicy(...)`
|
||||
- `setVerticalSizePolicy(...)`
|
||||
- `setHorizontalAlignPolicy(...)`
|
||||
- `setVerticalAlignPolicy(...)`
|
||||
- `getHorizontalLayoutSpec()`
|
||||
- `getVerticalLayoutSpec()`
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前:
|
||||
- 用户主要通过 `setLayoutMode(...)` 与 `setAnchor(...)` 设置布局。
|
||||
- 固定尺寸 + 居中、固定尺寸 + 比例位移等组合无法通过旧接口清晰表达。
|
||||
- 旧双锚点在某些测试场景中会被迫承担 Stretch 语义,导致固定按钮和内容驱动标签在 resize 后错位。
|
||||
- 变更后:
|
||||
- 用户可按水平轴 / 垂直轴分别设置锚点集合、尺寸策略和固定尺寸位移策略。
|
||||
- 旧接口仍可用,但只作为兼容输入层。
|
||||
- 新旧接口混用时,最后调用的接口覆盖对应轴的布局规格。
|
||||
- 兼容性说明:兼容
|
||||
- 迁移说明:[可选]
|
||||
- 新代码建议优先使用新布局 API。
|
||||
- 旧代码可以继续使用 `setAnchor(...)`,但复杂布局应逐步迁移到新接口。
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 编译 `Control.cpp`
|
||||
- 编译 `z-testDome.cpp /DKEY=2`
|
||||
- 编译 `z-testDome.cpp /DKEY=5`
|
||||
- 手测 `KEY1 / KEY2 / KEY5`
|
||||
- 验证结果:
|
||||
- 编译级验证通过。
|
||||
- 用户手测当前暂未发现问题。
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- [Feature-20260416-0009-KEY2-使用新布局API重构与联动刷新.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/功能变更/Feature-20260416-0009-KEY2-使用新布局API重构与联动刷新.md)
|
||||
- [Feature-20260418-0010-当前阶段测试用例矩阵与定位.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/功能变更/Feature-20260418-0010-当前阶段测试用例矩阵与定位.md)
|
||||
- Commit:
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
- resize 收口阶段若开启高频 console 日志,可能出现一帧视觉延迟;当前按调试态 I/O 现象记录,不作为 bug 处理。
|
||||
|
||||
## API 总览
|
||||
|
||||
### 完整轴向规格接口
|
||||
|
||||
```cpp
|
||||
void setHorizontalLayoutSpec(const StellarX::AxisLayoutSpec& spec);
|
||||
void setVerticalLayoutSpec(const StellarX::AxisLayoutSpec& spec);
|
||||
```
|
||||
|
||||
- 直接覆盖对应轴的完整布局规格。
|
||||
- 调用后自动切换到 `StellarX::LayoutMode::AnchorToEdges`。
|
||||
- 适合调用方已经准备好完整 `AxisLayoutSpec` 的场景。
|
||||
|
||||
### 锚点集合接口
|
||||
|
||||
```cpp
|
||||
void setHorizontalAnchors(bool left, bool right);
|
||||
void setVerticalAnchors(bool top, bool bottom);
|
||||
```
|
||||
|
||||
- 水平轴对应 `left / right`。
|
||||
- 垂直轴对应 `top / bottom`。
|
||||
- 只修改对应轴的锚点集合,不主动改变尺寸策略或位移策略。
|
||||
|
||||
### 尺寸策略接口
|
||||
|
||||
```cpp
|
||||
void setHorizontalSizePolicy(StellarX::AxisSizePolicy policy);
|
||||
void setVerticalSizePolicy(StellarX::AxisSizePolicy policy);
|
||||
```
|
||||
|
||||
- `Stretch`:允许该轴尺寸随父容器变化。
|
||||
- `FixedSize`:该轴尺寸保持设计态尺寸。
|
||||
- 控件能力边界仍然生效。例如 `Table` 当前版本不允许 `Y Stretch`,即使用户设置垂直 `Stretch`,也会被能力边界降级。
|
||||
|
||||
### 固定尺寸位移策略接口
|
||||
|
||||
```cpp
|
||||
void setHorizontalAlignPolicy(StellarX::AxisAlignPolicy policy);
|
||||
void setVerticalAlignPolicy(StellarX::AxisAlignPolicy policy);
|
||||
```
|
||||
|
||||
- 只在 `FixedSize` 语义下决定位置。
|
||||
- 不负责改变尺寸。
|
||||
- 常用值:
|
||||
- `Start`:保持起边关系,例如左固定、上固定。
|
||||
- `End`:保持终边关系,例如右固定、下固定。
|
||||
- `Center`:保持居中关系。
|
||||
- `Proportional`:保持设计态相对位置比例,适合固定尺寸控件在可变宽容器中按比例移动。
|
||||
|
||||
### 读取接口
|
||||
|
||||
```cpp
|
||||
StellarX::AxisLayoutSpec getHorizontalLayoutSpec() const;
|
||||
StellarX::AxisLayoutSpec getVerticalLayoutSpec() const;
|
||||
```
|
||||
|
||||
- 返回当前生效的新模型轴向规格。
|
||||
- 不保证能完整逆映射回旧 `anchor_1 / anchor_2` 语义。
|
||||
|
||||
## 新旧接口覆盖规则
|
||||
|
||||
### 旧接口仍然保留
|
||||
|
||||
```cpp
|
||||
void setLayoutMode(StellarX::LayoutMode layoutMode);
|
||||
void setAnchor(StellarX::Anchor anchor1, StellarX::Anchor anchor2);
|
||||
```
|
||||
|
||||
- 旧接口是兼容入口。
|
||||
- `setAnchor(...)` 内部会把旧双锚点映射到新的 `layoutSpec.horizontal` 与 `layoutSpec.vertical`。
|
||||
- 旧接口无法表达所有新模型状态,例如固定尺寸 + 比例位移、固定尺寸 + 居中等。
|
||||
|
||||
### 后调用者生效
|
||||
|
||||
规则:
|
||||
|
||||
- 先调用 `setAnchor(...)`,再调用新 API,则新 API 覆盖对应轴规格。
|
||||
- 先调用新 API,再调用 `setAnchor(...)`,则旧接口重新覆盖水平轴和垂直轴规格。
|
||||
- 新 API 只覆盖自己负责的轴或字段。例如 `setHorizontalSizePolicy(...)` 只修改水平轴尺寸策略,不修改垂直轴。
|
||||
|
||||
示例:
|
||||
|
||||
```cpp
|
||||
button->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
|
||||
button->setAnchor(StellarX::Anchor::Left, StellarX::Anchor::Right);
|
||||
|
||||
// 覆盖水平轴:改为固定尺寸 + 居中,不再沿用旧接口映射出的 Stretch。
|
||||
button->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
|
||||
button->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Center);
|
||||
```
|
||||
|
||||
### 新 API 自动切换布局模式
|
||||
|
||||
所有新布局 setter 都会自动将 `layoutMode` 切换为 `AnchorToEdges`。
|
||||
因此新代码通常不需要再额外写:
|
||||
|
||||
```cpp
|
||||
control->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
|
||||
```
|
||||
|
||||
## 常见布局写法
|
||||
|
||||
### 左固定
|
||||
|
||||
```cpp
|
||||
control->setHorizontalAnchors(true, false);
|
||||
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
|
||||
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Start);
|
||||
```
|
||||
|
||||
### 右固定
|
||||
|
||||
```cpp
|
||||
control->setHorizontalAnchors(false, true);
|
||||
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
|
||||
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::End);
|
||||
```
|
||||
|
||||
### 水平拉伸
|
||||
|
||||
```cpp
|
||||
control->setHorizontalAnchors(true, true);
|
||||
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::Stretch);
|
||||
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Start);
|
||||
```
|
||||
|
||||
### 固定尺寸居中
|
||||
|
||||
```cpp
|
||||
control->setHorizontalAnchors(true, true);
|
||||
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
|
||||
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Center);
|
||||
```
|
||||
|
||||
### 固定尺寸比例位移
|
||||
|
||||
```cpp
|
||||
control->setHorizontalAnchors(true, true);
|
||||
control->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
|
||||
control->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Proportional);
|
||||
```
|
||||
|
||||
适用场景:
|
||||
|
||||
- 一组固定尺寸按钮需要随着父容器变宽按比例展开。
|
||||
- `KEY2` 中每个 bit 单元就是该策略的典型使用方式。
|
||||
|
||||
## KEY2 迁移要点
|
||||
|
||||
`KEY2` 原问题不是单纯参数错误,而是布局单元拆分不合理:
|
||||
|
||||
- 位号 `Label` 是内容驱动尺寸。
|
||||
- 位按钮是固定尺寸。
|
||||
- 二者分别使用比例位移时,因为自身尺寸不同,resize 后会出现错位。
|
||||
|
||||
当前迁移方式:
|
||||
|
||||
- 每一位创建一个小 `Canvas` 单元。
|
||||
- 单元内部固定放置位号 `Label` 与位按钮。
|
||||
- 单元自身使用 `FixedSize + Proportional`。
|
||||
- 这样移动的是整个 bit 单元,而不是标签和按钮分别移动。
|
||||
|
||||
示意写法:
|
||||
|
||||
```cpp
|
||||
auto cell = std::make_unique<Canvas>(cellX, 0, 32, 50);
|
||||
cell->setHorizontalAnchors(true, true);
|
||||
cell->setHorizontalSizePolicy(StellarX::AxisSizePolicy::FixedSize);
|
||||
cell->setHorizontalAlignPolicy(StellarX::AxisAlignPolicy::Proportional);
|
||||
```
|
||||
|
||||
## 能力边界说明
|
||||
|
||||
布局 API 表达的是“调用方希望如何布局”,但控件能力边界仍然优先。
|
||||
|
||||
当前关键边界:
|
||||
|
||||
- `Canvas`:`X / Y` 均允许 Stretch。
|
||||
- `TabControl`:外层允许 Stretch,内部页签栏 / 页面区仍由自身管理。
|
||||
- `Table`:当前版本 `X Stretch / Y Fixed`,这是当前实现边界,不是永久产品结论。
|
||||
- `TextBox`:默认更适合 `X Stretch / Y FixedSize`。
|
||||
- `Label`:默认固定或内容驱动刷新运行态尺寸,不建议做双轴 Stretch。
|
||||
- `Dialog`:只居中,不参与父级拉伸。
|
||||
|
||||
如果用户设置的策略超出控件能力边界,统一解算层会按现有降级规则处理,并保留必要日志。
|
||||
|
||||
## 调试态说明
|
||||
|
||||
resize / 最大化 / 还原过程中,如果开启大量 console 日志,可能出现一帧视觉延迟或短暂残影。
|
||||
当前判断原因是日志系统同步写控制台且默认逐行 flush,导致主线程 resize 收口绘制被 I/O 拖慢。
|
||||
|
||||
处理建议:
|
||||
|
||||
- 常规手测可关闭 console 日志。
|
||||
- 需要日志时可优先输出到文件。
|
||||
- 当前不作为布局或重绘 bug 处理,后续可在官网/API 文档中作为调试说明记录。
|
||||
@@ -1,105 +0,0 @@
|
||||
# 功能变更 ID: Feature-20260511-0012
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID: Feature-20260511-0012
|
||||
- 标题: 发布前托管重绘与 KEY6 封版收口
|
||||
- 状态:已完成
|
||||
- 类型:修改
|
||||
- 级别:L3 重大
|
||||
- 模块: Window / Canvas / TabControl / Tooltip / Dialog / 测试用例
|
||||
- 版本 / 分支: 当前工作区
|
||||
- 环境: Windows / EasyX
|
||||
- 负责人: Codex
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- 发布前封版阶段需要稳定现有布局、托管重绘、Dialog / overlay / hover 状态同步链路。
|
||||
- KEY6 补充覆盖后暴露了非模态 Dialog、Tooltip、跨 root dirty、透明 Label 快照等组合问题。
|
||||
- 目标:
|
||||
- 修复确定性小问题。
|
||||
- 收口 Dialog / overlay 后鼠标状态同步语义。
|
||||
- 让 KEY6 覆盖已实现但原主回归未覆盖的分支。
|
||||
- 修复托管局部重绘中 coverage 和背景快照语义混用导致的问题。
|
||||
- 不做什么:[可选]
|
||||
- 不实现焦点系统。
|
||||
- 不实现键盘事件系统。
|
||||
- 不做 Tooltip 智能选位。
|
||||
- 不做 Table 纵向 Stretch。
|
||||
- 不实现 Table 内部局部重绘体系。
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要:
|
||||
- 修复 `TabControl::indexOf()` 未命中返回值。
|
||||
- 抽取 Dialog / overlay 后鼠标状态同步辅助函数。
|
||||
- 新增 KEY6 分支覆盖用例。
|
||||
- `Window` 增加局部 root 提交后 overlay coverage 保守兜底。
|
||||
- `Window` 事件尾补收集跨 root dirty 子树。
|
||||
- 托管重绘拆分完整 coverage 与持久 coverage,避免 Tooltip 污染背景快照。
|
||||
- 新增项:[可选]
|
||||
- `Window::syncMouseStateAfterOverlayChanged(...)`
|
||||
- `Window::collectDirtyRootsForManagedRepaint()`
|
||||
- `Control::getManagedRepaintPersistentCoverageRect()`
|
||||
- KEY6 测试入口
|
||||
- 修改项:[可选]
|
||||
- `Canvas / TabControl` 局部 overlay 补画策略。
|
||||
- `Button` Tooltip coverage 语义。
|
||||
- `Table` 托管 coverage 语义补充。
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 无
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- `Window.h / Window.cpp`
|
||||
- `Control.h / Control.cpp`
|
||||
- `Button.h / Button.cpp`
|
||||
- `Canvas.h / Canvas.cpp`
|
||||
- `TabControl.h / TabControl.cpp`
|
||||
- `Table.h / Table.cpp`
|
||||
- `z-testDome.cpp`
|
||||
- 对外 API / 属性变化:[可选]
|
||||
- 无公开用户 API 变化。
|
||||
- 新增接口均为内部托管重绘语义。
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前:
|
||||
- `indexOf()` 未命中返回最后索引。
|
||||
- Dialog 关闭 / 非模态 Dialog 吞鼠标移动后的 synthetic move 逻辑散落在 `runEventLoop()`。
|
||||
- 局部 root 内部 coverage 扩张后,Window 可能漏补 Dialog。
|
||||
- 跨 root 回调改脏后,目标 root 可能等下一次事件才刷新。
|
||||
- Tooltip 临时浮层与持久 coverage 混用,可能污染或擦除兄弟控件快照。
|
||||
- 变更后:
|
||||
- `indexOf()` 未命中返回 `-1`。
|
||||
- overlay 后鼠标状态同步由语义函数集中处理。
|
||||
- Window 对可局部提交 root 的上层补画判断更保守。
|
||||
- 事件尾会补收集未登记但已 dirty 的 root。
|
||||
- Tooltip 进入完整 coverage,但不进入持久 coverage;只命中临时浮层的兄弟控件不再补画。
|
||||
- 兼容性说明:兼容
|
||||
- 迁移说明:[可选]
|
||||
- 用户代码无需迁移。
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 编译核心变更文件。
|
||||
- 编译 `z-testDome.cpp` 的 `KEY1 ~ KEY6`。
|
||||
- 根据用户手测反馈修正 KEY6 Tooltip / Dialog / Label 组合问题。
|
||||
- 验证结果:
|
||||
- `Control.cpp / Button.cpp / Canvas.cpp / TabControl.cpp / Table.cpp / Window.cpp` 编译通过。
|
||||
- `KEY1 ~ KEY6` 编译通过。
|
||||
- GUI 行为需继续由用户在本机手测确认。
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- BUG-20260511-0009 / Fix-BUG-20260511-0009
|
||||
- BUG-20260511-0010 / Fix-BUG-20260511-0010
|
||||
- BUG-20260511-0011 / Fix-BUG-20260511-0011
|
||||
- BUG-20260511-0012 / Fix-BUG-20260511-0012
|
||||
- Commit: 未提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
- 下版本建议引入 `commitManagedRepaint()` actual coverage 返回值,替代当前 root coverage 保守兜底。
|
||||
- 下版本建议建立正式 coverage 诊断日志开关,避免再临时散加日志。
|
||||
@@ -1,134 +0,0 @@
|
||||
# 新增功能模块 / 模块重构
|
||||
|
||||
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
|
||||
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- 模块 ID:Module-20260409-0001
|
||||
- 模块名称:Window 托管重绘与覆盖合成机制
|
||||
- 状态:已完成 / 待持续回归
|
||||
- 类型:架构演进
|
||||
- 所属系统 / 子系统:Window / Control / Dialog 重绘链
|
||||
- 版本 / 分支:`master`
|
||||
- 环境:Windows + EasyX + VS2022
|
||||
- 负责人:Codex 协作修改
|
||||
|
||||
## 背景与目标
|
||||
|
||||
- 背景:
|
||||
- 初始框架以控件局部即时重绘为主,非模态对话框与底层控件重叠时,容易出现底层先画、对话框后补的层级问题。
|
||||
- 当前痛点:
|
||||
- 对话框遮挡下的 hover / click / tooltip / 分页等交互容易闪烁
|
||||
- 底层控件和上层对话框在事件里分离绘制,顺序不可控
|
||||
- 目标:
|
||||
- 将“事件改状态”和“绘制提交”拆开,由 `Window` 在事件尾统一提交重绘。
|
||||
- 非目标:[可选]
|
||||
- 暂不做真正像素级脏区裁剪
|
||||
- 暂不统一布局系统
|
||||
|
||||
## 模块边界
|
||||
|
||||
- 职责:
|
||||
- 在 `Window` 主循环里托管控件重绘请求
|
||||
- 选择安全的重绘 root
|
||||
- 在提交阶段维护普通控件与非模态 `Dialog` 的正确层级
|
||||
- 不负责什么:
|
||||
- 模态 `Dialog` 自己的内部事件循环
|
||||
- 布局语义统一
|
||||
- 字符集问题
|
||||
- 外部依赖:
|
||||
- EasyX 绘制接口
|
||||
- 各控件自身的快照恢复能力
|
||||
- 对外能力 / API:
|
||||
- `Window::requestManagedRepaint(Control* source)`
|
||||
- `Window::flushManagedRepaint()`
|
||||
- `Control::getManagedRepaintRoot()`
|
||||
- `Control::commitManagedRepaint()`
|
||||
- 关键数据 / 状态:
|
||||
- `managedDispatchActive`
|
||||
- `managedSceneDirty`
|
||||
- `managedRepaintItems`
|
||||
|
||||
## 设计说明
|
||||
|
||||
- 核心流程:
|
||||
1. `Window::runEventLoop()` 开始输入分发前,进入 `managedDispatchActive = true`
|
||||
2. 控件在 `handleEvent()` 中只改状态并通过 `requestRepaint()` 上报
|
||||
3. `Window::requestManagedRepaint(source)` 把 source 转换成 `root + coverage`
|
||||
4. 事件收口时 `flushManagedRepaint()` 按层级提交 root
|
||||
5. 最后补画和脏区相交的非模态 `Dialog`
|
||||
- 关键对象 / 类关系:
|
||||
- `Window`:重绘调度中心
|
||||
- `Control`:提供宿主窗口回溯与 root 选择语义
|
||||
- `Canvas / TabControl / Dialog`:作为安全重绘 root 的主要容器
|
||||
- 生命周期:
|
||||
- 托管重绘项只存在于当前分发轮次
|
||||
- `clearManagedRepaintState()` 在 flush、resize 收口、对话框开关后的整场景重绘后清空
|
||||
- 事件 / 渲染 / 数据流:
|
||||
- 事件:Dialog 优先,普通控件其次
|
||||
- 渲染:受影响普通 root 先画,Dialog 后补
|
||||
- 数据流:leaf control -> requestRepaint -> Window registry -> flush
|
||||
- 关键不变量:
|
||||
- 分发期不直接提交托管重绘
|
||||
- 顶层普通控件的提交顺序必须服从 `Window::controls` 的 z-order
|
||||
- 对话框始终绘制在普通控件之上
|
||||
- 降级 / 回退策略:[可选]
|
||||
- `resize`、对话框打开/关闭等全局变化仍走整场景重绘
|
||||
|
||||
## 实现与影响
|
||||
|
||||
- 关键实现点:
|
||||
- `Control` 增加 `hostWindow`、`getManagedRepaintRoot()`、`commitManagedRepaint()`
|
||||
- `Window` 增加 `ManagedRepaintItem` 队列
|
||||
- `Canvas / TabControl / Dialog` 提供局部可提交与升级为整 root 重画的判断
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- `Window.h / Window.cpp`
|
||||
- `Control.h / Control.cpp`
|
||||
- `Canvas.h / Canvas.cpp`
|
||||
- `TabControl.h / TabControl.cpp`
|
||||
- `Dialog.h / Dialog.cpp`
|
||||
- 兼容性影响:
|
||||
- 行为层面更严格,底层控件不再依赖旧的“即时局部重绘”副作用
|
||||
- 性能影响:
|
||||
- 相比整场景重绘更细
|
||||
- 相比纯即时局部重绘更稳定
|
||||
- 在全局变化场景仍保留整场景兜底
|
||||
- 风险点:
|
||||
- root 选择错误会导致局部重绘不完整
|
||||
- 嵌套容器组合场景需持续回归
|
||||
|
||||
## 测试与验证
|
||||
|
||||
- 测试范围:
|
||||
- 非模态遮挡下的按钮 hover / click
|
||||
- `Canvas / TabControl / Table` 组合场景
|
||||
- 模态与非模态对话框 coexist 场景
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `Control / Canvas / TabControl / Dialog / Window` 相关源文件。
|
||||
2. 回归 `KEY == 2 / 3 / 4` 的对话框与遮挡场景。
|
||||
3. 检查对话框关闭、resize、hover 清理和局部重绘顺序。
|
||||
|
||||
- 验证结果:
|
||||
- 第二阶段架构已落地,核心编译通过,主问题链路已被压住。
|
||||
- 已知限制 / 遗留问题:[可选]
|
||||
- 快速划过多个按钮时偶发一帧双高亮,当前接受为底层限制
|
||||
- 仍未推进到真正像素级脏区裁剪
|
||||
|
||||
## 落地信息
|
||||
|
||||
- 关联功能变更 ID:[可选]
|
||||
- `Feature-20260409-0002`
|
||||
- `Feature-20260409-0003`
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- `BUG-20260409-0001`
|
||||
- `Fix-BUG-20260409-0001`
|
||||
- Commit:
|
||||
- `b07a4ec`
|
||||
- `77a8fe5`
|
||||
- 当前工作区未提交阶段
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 相关文档:[可选]
|
||||
- `00-修改总览-20260409.md`
|
||||
@@ -1,161 +0,0 @@
|
||||
# 新增功能模块 / 模块重构
|
||||
|
||||
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
|
||||
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- 模块 ID:`Module-20260410-0002`
|
||||
- 模块名称: 锚点与布局系统第一阶段重构
|
||||
- 状态:已验证
|
||||
- 类型:模块重构
|
||||
- 所属系统 / 子系统: GUI 框架 / Layout
|
||||
- 版本 / 分支: 当前工作区 / 下一版本开发中
|
||||
- 环境: Windows + EasyX
|
||||
- 负责人: Codex 协作修改
|
||||
|
||||
## 背景与目标
|
||||
|
||||
- 背景:
|
||||
- 原有锚点系统仍依赖 `anchor_1 / anchor_2`
|
||||
- `Window::adaptiveLayout()` 与 `Canvas::onWindowResize()` 长期并存两套布局语义
|
||||
- `Canvas` 布局层存在 `Table` 外部特判
|
||||
- `TabControl` 外层布局与内部布局耦合较深
|
||||
- 当前痛点:
|
||||
- 双锚点表达能力不足,难以覆盖更完整的边集合语义
|
||||
- 顶层窗口与容器子控件的解算规则不统一,维护成本高
|
||||
- 特殊控件能力边界没有收回自身语义层
|
||||
- 缺少针对布局系统的专项回归用例
|
||||
- 目标:
|
||||
- 建立统一的布局数据模型与统一解算入口
|
||||
- 正式区分设计态矩形与运行态矩形
|
||||
- 保留旧 API 的兼容输入能力
|
||||
- 将 `Table` 的当前能力边界收回控件自身
|
||||
- 增加布局专项回归用例 `KEY == 5`
|
||||
- 非目标:
|
||||
- 不做字体随控件缩放
|
||||
- 不做 `Table` 纵向拉伸
|
||||
- 不重构重绘系统
|
||||
|
||||
## 模块边界
|
||||
|
||||
- 职责:
|
||||
- 提供统一的布局规格描述
|
||||
- 统一顶层窗口与容器子控件的几何解算
|
||||
- 在保持旧 API 可用的前提下,将内部布局实现迁移到新模型
|
||||
- 通过控件能力边界约束非法或暂不支持的拉伸组合
|
||||
- 不负责什么:
|
||||
- 字体缩放与内容排版自适应
|
||||
- `Dialog` 内部布局语义重构
|
||||
- `Table` 纵向拉伸能力
|
||||
- 外部依赖:
|
||||
- EasyX 绘制环境
|
||||
- 现有 `Control / Window / Canvas / TabControl / Table` 控件体系
|
||||
- 对外能力 / API:
|
||||
- 继续保留 `setLayoutMode(...)`
|
||||
- 继续保留 `setAnchor(a1, a2)`
|
||||
- 当前阶段新增能力主要用于内部统一实现,不额外新增用户层 API
|
||||
- 关键数据 / 状态:
|
||||
- `localx / localy / localWidth / localHeight`
|
||||
- `x / y / width / height`
|
||||
- `LayoutSpec`
|
||||
- `LayoutCapability`
|
||||
- `ResolvedLayoutRect`
|
||||
|
||||
## 设计说明
|
||||
|
||||
- 核心流程:
|
||||
- 先在父局部坐标系内按水平轴 / 垂直轴独立解算
|
||||
- 再将局部矩形映射为世界坐标矩形
|
||||
- 由控件内部受控路径应用运行态矩形
|
||||
- 关键对象 / 类关系:
|
||||
- [`Control`](D:/programming/imGUI-easyX/imGui-easyX/Control.h) 作为统一布局规格与基础解算入口
|
||||
- [`Window`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 负责顶层控件统一收口
|
||||
- [`Canvas`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp) 负责容器子控件重映射
|
||||
- [`TabControl`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp) 外层接入统一解算,内部页签栏 / 页面区仍自管
|
||||
- [`Table`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp) 通过 `LayoutCapability` 显式禁止 `Y` 轴 Stretch
|
||||
- 生命周期:
|
||||
- 设计态矩形 `local*` 在普通 resize / 重排过程中不自动回写
|
||||
- 运行态矩形由统一解算器产出,再通过内部路径应用
|
||||
- 若确需同步设计基线,只能显式调用 `commitCurrentGeometryAsDesignRect()`
|
||||
- 事件 / 渲染 / 数据流:
|
||||
- 事件阶段只改状态,不直接扩散成多套布局公式
|
||||
- `Window` 与 `Canvas` 共用 `resolveLayoutRect()`
|
||||
- `onWindowResize()` 收口为“快照失效 + 标脏 + 必要传播”,不再承担布局求解
|
||||
- 关键不变量:
|
||||
- `local*` 始终表示设计态父局部矩形
|
||||
- `x / y / width / height` 始终表示运行态绘制矩形
|
||||
- 旧 API 只作兼容输入层,不再作为内部解算依据
|
||||
- `Table` 当前阶段只允许 `X` 轴 Stretch
|
||||
- 降级 / 回退策略:
|
||||
- 对不满足能力边界的拉伸请求,自然降级为固定尺寸位移
|
||||
- 旧接口输入通过映射层退回到新模型的有限子集
|
||||
|
||||
## 实现与影响
|
||||
|
||||
- 关键实现点:
|
||||
- 引入 `AxisSizePolicy / AxisAlignPolicy / AxisLayoutSpec / LayoutSpec / LayoutCapability / ResolvedLayoutRect`
|
||||
- 在 `Control` 中增加统一解算与内部受控应用路径
|
||||
- 将 `Window::adaptiveLayout()` 改为统一解算入口
|
||||
- 将 `Canvas` 子控件布局从旧比例缩放逻辑切换为统一解算
|
||||
- 将 `TabControl` 外层接入统一解算,同时保留内部页签系统专用布局
|
||||
- 将 `Table` 的 `Y` 轴固定能力边界收回控件自身
|
||||
- 在 `z-testDome.cpp` 增加 `KEY == 5` 布局专项回归
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- [`CoreTypes.h`](D:/programming/imGUI-easyX/imGui-easyX/CoreTypes.h)
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
|
||||
- [`Canvas.h`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.h)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
|
||||
- [`TabControl.h`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.h)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
|
||||
- [`Table.h`](D:/programming/imGUI-easyX/imGui-easyX/Table.h)
|
||||
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- 兼容性影响:
|
||||
- 向后兼容旧锚点 API
|
||||
- 当前阶段未删除旧 getter / setter
|
||||
- 性能影响:
|
||||
- 无明显新增性能负担
|
||||
- 主要是布局求解路径从多处分散逻辑收口为统一函数
|
||||
- 风险点:
|
||||
- 若某些控件运行态尺寸变化后未明确同步设计基线,后续 resize 仍可能出现“回到旧设计态”的现象
|
||||
- `TabControl` 的外层统一解算与内部专用布局之间存在边界风险
|
||||
- `Table` 纵向仍为固定尺寸,后续若扩展能力需单独立项
|
||||
|
||||
## 测试与验证
|
||||
|
||||
- 测试范围:
|
||||
- 顶层窗口 resize
|
||||
- `Canvas` 嵌套布局
|
||||
- `TabControl` 外层布局接入
|
||||
- `Table` 横向拉伸与纵向固定
|
||||
- `KEY == 5` 布局专项回归
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `Control.cpp / Canvas.cpp / Table.cpp / TabControl.cpp / Window.cpp`
|
||||
2. 编译 `z-testDome.cpp /DKEY=2`
|
||||
3. 编译 `z-testDome.cpp /DKEY=5`
|
||||
|
||||
- 验证结果:
|
||||
- 源码级编译验证通过
|
||||
- `KEY == 5` 已补齐布局专项回归用例
|
||||
- GUI 交互仍需用户本机手动确认
|
||||
- 已知限制 / 遗留问题:
|
||||
- 本轮不包含字体缩放
|
||||
- 本轮不包含 `Table` 纵向拉伸
|
||||
- Tooltip 问题已另外拆分为独立 `BUG / Fix`
|
||||
|
||||
## 落地信息
|
||||
|
||||
- 关联功能变更 ID:[可选]
|
||||
- 关联 BUG / Fix:
|
||||
- `BUG-20260410-0004`
|
||||
- `Fix-BUG-20260410-0004`
|
||||
- Commit: 当前工作区未提交
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 相关文档:
|
||||
- [`BUG-20260410-0004-按钮Tooltip移出后不消失.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/BUG/BUG-20260410-0004-按钮Tooltip移出后不消失.md)
|
||||
- [`Fix-BUG-20260410-0004-按钮Tooltip移出后不消失.md`](D:/programming/imGUI-easyX/imGui-easyX/开发记录/Fix/Fix-BUG-20260410-0004-按钮Tooltip移出后不消失.md)
|
||||
@@ -1,157 +0,0 @@
|
||||
# 新增功能模块 / 模块重构
|
||||
|
||||
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
|
||||
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- 模块 ID:Module-20260415-0003
|
||||
- 模块名称: 布局系统第二阶段收口
|
||||
- 状态:已完成
|
||||
- 类型:模块重构
|
||||
- 所属系统 / 子系统: GUI 框架 / Layout
|
||||
- 版本 / 分支: 当前工作区 / 下一版本开发中
|
||||
- 环境: Windows + EasyX
|
||||
- 负责人: Codex 协作修改
|
||||
|
||||
## 背景与目标
|
||||
|
||||
- 背景:
|
||||
- 第一阶段已经完成统一布局链路打通,但几何写入口、内容驱动控件规则、复合控件职责边界仍有残留混用。
|
||||
- `Label` 曾在 `draw()` 阶段临时决定尺寸,`TabControl` 和 `Canvas` 在子树几何映射上也存在职责重叠。
|
||||
- `WM_MOUSEMOVE` 的容器分发和局部重绘合成,在复杂遮挡场景下容易暴露 hover/tooltip 与 overlay 残留问题。
|
||||
- 当前痛点:
|
||||
- 几何语义还不够制度化,后续继续演进布局系统时容易再次混回“draw 阶段改几何”。
|
||||
- 内容驱动控件和复合控件没有完全落成可解释的边界。
|
||||
- 局部重绘与上层兄弟补画机制不完整时,会直接破坏遮挡正确性。
|
||||
- 目标:
|
||||
- 收口几何写入口语义,明确公开 setter、统一布局应用路径、内容驱动路径、显式设计基线提交。
|
||||
- 收口 `Label / Table` 的内容驱动规则与设计基线边界。
|
||||
- 收口 `TabControl / Canvas` 的职责边界,不重写页签系统,但消除页内子控件手工回填。
|
||||
- 建立轻量级鼠标瞬时状态清理路径和 overlay 补画机制,保证 hover / tooltip / 局部重绘链正确。
|
||||
- 非目标:[可选]
|
||||
- 不做字体缩放。
|
||||
- 不做 `Table` 纵向拉伸。
|
||||
- 不做 `Dialog` 旧 synthetic move 机制统一。
|
||||
- 不做 `Table` 内部局部重绘体系。
|
||||
|
||||
## 模块边界
|
||||
|
||||
- 职责:
|
||||
- 定义并收口布局系统第二阶段的运行态 / 设计态几何语义。
|
||||
- 为当前主线控件显式写出能力边界和默认策略。
|
||||
- 收口局部重绘下的 overlay 补画规则。
|
||||
- 不负责什么:
|
||||
- 不扩展新的布局表达能力。
|
||||
- 不处理字体、图标、DPI 自适应。
|
||||
- 不把所有控件都改造成内容驱动或局部重绘型复合控件。
|
||||
- 外部依赖:
|
||||
- EasyX 绘制环境
|
||||
- 现有 `Control / Window / Canvas / TabControl / Table / Label / Button` 体系
|
||||
- 对外能力 / API:
|
||||
- 保留 `setLayoutMode(...)`
|
||||
- 保留 `setAnchor(a1, a2)`
|
||||
- 保留 `commitCurrentGeometryAsDesignRect()`
|
||||
- `Label::textStyle` 继续保持公开,但要求样式修改后显式 `setDirty(true)`
|
||||
- 关键数据 / 状态:
|
||||
- `localx / localy / localWidth / localHeight`
|
||||
- `x / y / width / height`
|
||||
- `LayoutSpec / LayoutCapability / ResolvedLayoutRect`
|
||||
- `eventVisualChanged`
|
||||
|
||||
## 设计说明
|
||||
|
||||
- 核心流程:
|
||||
- 先在父局部坐标系内完成统一布局解算。
|
||||
- 再由内部受控路径把运行态矩形应用到控件。
|
||||
- 内容驱动控件在自己的受控路径里刷新运行态尺寸。
|
||||
- 局部重绘提交后,由父容器按实际绘制顺序补画 coverage 上方的 overlay 兄弟。
|
||||
- 关键对象 / 类关系:
|
||||
- [`Control`](D:/programming/imGUI-easyX/imGui-easyX/Control.h):几何语义、解算入口、设计基线提交入口。
|
||||
- [`Label`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp):内容驱动尺寸,`draw()` 只消费运行态矩形。
|
||||
- [`Canvas`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp):管理直接子控件的世界坐标映射与局部 overlay 补画。
|
||||
- [`TabControl`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp):外层接统一解算,内部页签栏 / 页面区继续自管。
|
||||
- [`Table`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp):当前版本 `X Stretch / Y Fixed`,并保留内部受控结构尺寸基线刷新。
|
||||
- [`Window`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp):顶层托管重绘收口与 overlay 兄弟补画。
|
||||
- 生命周期:
|
||||
- 普通 resize / 父容器重排不自动回写 `local*`。
|
||||
- 显式设计基线提交只通过 `commitCurrentGeometryAsDesignRect()` 或控件内部受控结构刷新点发生。
|
||||
- 内容驱动尺寸刷新优先发生在 `draw()` 之前。
|
||||
- 事件 / 渲染 / 数据流:[按模块类型填写]
|
||||
- `WM_MOUSEMOVE`:第一个命中的兄弟收到真实消息,后续兄弟只清理瞬时鼠标状态。
|
||||
- 局部重绘:先画本次 dirty 单元,再补画 coverage 上方相交 overlay。
|
||||
- 运行态几何:统一解算或内容驱动路径写入;设计基线不自动漂移。
|
||||
- 关键不变量:
|
||||
- `local*` 始终表示设计态父局部矩形。
|
||||
- `x / y / width / height` 始终表示运行态绘制矩形。
|
||||
- `draw()` 不再承担新的几何决策入口。
|
||||
- `Table` 当前版本 `Y Fixed` 是实现边界,不是永久产品结论。
|
||||
- 降级 / 回退策略:[可选]
|
||||
- Stretch 条件不满足时自然降级为固定尺寸位置策略。
|
||||
- 控件能力边界禁止 Stretch 时,通过日志输出拦截原因。
|
||||
|
||||
## 实现与影响
|
||||
|
||||
- 关键实现点:
|
||||
- 在 [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp) 增加布局降级日志、能力边界拦截日志、显式设计基线提交日志。
|
||||
- 在 [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp) 收口内容驱动尺寸刷新,并显式关闭双轴 Stretch。
|
||||
- 在 [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)、[`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)、[`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp) 收口 overlay 补画。
|
||||
- 在 [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp) 收口 `Y Fixed`、分页按钮视觉链与页码重绘。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
|
||||
- [`Label.h`](D:/programming/imGUI-easyX/imGui-easyX/Label.h)
|
||||
- [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
|
||||
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp)
|
||||
- [`TextBox.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TextBox.cpp)
|
||||
- [`Dialog.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Dialog.cpp)
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
|
||||
- 兼容性影响:
|
||||
- 对旧锚点 API 保持兼容。
|
||||
- `Label::textStyle` 仍为公开字段,但使用约定更严格。
|
||||
- 性能影响:
|
||||
- `WM_MOUSEMOVE` 和 overlay 补画路径增加了必要的状态清理与补画,但避免了整窗级重绘。
|
||||
- `Table` 分页按钮视觉变化当前仍提升为整表重绘,颗粒度偏粗,但正确性优先。
|
||||
- 风险点:
|
||||
- `Dialog` 旧 synthetic move 机制仍与新清理模型并存。
|
||||
- `Table` 内部局部重绘体系尚未建立,分页区仍偏重。
|
||||
|
||||
## 测试与验证
|
||||
|
||||
- 测试范围:
|
||||
- 顶层 resize
|
||||
- 三层 `Canvas` 嵌套
|
||||
- `TabControl` 外层 resize 与页内稳定性
|
||||
- overlay 补画
|
||||
- `Table` 横向拉伸、分页按钮、页码重绘
|
||||
- `Label` 文本 / 字体样式变化
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `Control.cpp / Label.cpp / Canvas.cpp / TabControl.cpp / Table.cpp / TextBox.cpp / Dialog.cpp`
|
||||
2. 编译 `z-testDome.cpp /DKEY=5`
|
||||
3. 手动回归 `KEY5` 的 hover、tooltip、overlay、分页与页码场景
|
||||
|
||||
- 验证结果:
|
||||
- 编译级验证通过。
|
||||
- 手动 GUI 回归依赖本机继续执行。
|
||||
- 已知限制 / 遗留问题:[可选]
|
||||
- `Dialog` 旧 synthetic move 机制暂未统一。
|
||||
- `Table` 尚未引入内部局部重绘模型。
|
||||
- 全项目所有控件的能力边界总审计未做。
|
||||
|
||||
## 落地信息
|
||||
|
||||
- 关联功能变更 ID:[可选]
|
||||
- `Feature-20260415-0008`
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- `BUG-20260415-0005`
|
||||
- `Fix-BUG-20260415-0005`
|
||||
- Commit: 未提交(当前工作区)
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 相关文档:[可选]
|
||||
- [Feature-20260415-0008-KEY5-第二阶段专项回归场景增强.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/功能变更/Feature-20260415-0008-KEY5-第二阶段专项回归场景增强.md)
|
||||
- [BUG-20260415-0005-局部重绘未补画上层兄弟导致遮挡错误.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/BUG/BUG-20260415-0005-局部重绘未补画上层兄弟导致遮挡错误.md)
|
||||
- [Fix-BUG-20260415-0005-局部重绘补画上层兄弟修复.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/Fix/Fix-BUG-20260415-0005-局部重绘补画上层兄弟修复.md)
|
||||
@@ -1,177 +0,0 @@
|
||||
# 新增功能模块 / 模块重构
|
||||
|
||||
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
|
||||
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- 模块 ID:Module-20260415-0004
|
||||
- 模块名称: 布局系统第二阶段验收与封口
|
||||
- 状态:已验证
|
||||
- 类型:模块重构
|
||||
- 所属系统 / 子系统: GUI 框架 / Layout
|
||||
- 版本 / 分支: 当前工作区 / 下一版本开发中
|
||||
- 环境: Windows + EasyX
|
||||
- 负责人: Codex 协作修改
|
||||
|
||||
## 背景与目标
|
||||
|
||||
- 背景:
|
||||
- 第一阶段已完成统一锚点模型和统一解算入口。
|
||||
- 第二阶段继续围绕“几何所有权收口 + 内容驱动规则收口”推进,实现几何写入口分层、内容驱动控件规则收口、局部重绘与 overlay 补画链修复。
|
||||
- 在 `KEY1 / KEY5` 回归过程中,又暴露出脏子树提交、coverage 低估、TabControl 层级顺序等一组重绘链问题,需要一并收口后才能视为阶段稳定。
|
||||
- 当前痛点:
|
||||
- 缺少一份明确的阶段验收记录,难以区分“本阶段完成项”和“明确延期项”。
|
||||
- 当前主线 bug 虽已基于测试用例修复,但如果不做封口记录,后续继续推进下一主题时容易重复回头整理。
|
||||
- 目标:
|
||||
- 明确第二阶段已经完成的主线改造和已验证的 bug 修复。
|
||||
- 明确当前接受的边界与明确延期的技术债。
|
||||
- 为后续“公开布局 API + 旧 demo 迁移”提供稳定起点。
|
||||
- 非目标:[可选]
|
||||
- 本记录不新增运行时代码逻辑。
|
||||
- 本记录不覆盖未来的 Tooltip 智能选位、`Table` 纵向拉伸、`Dialog` synthetic move 统一改造。
|
||||
|
||||
## 模块边界
|
||||
|
||||
- 职责:
|
||||
- 汇总布局系统第二阶段的最终语义收口结果。
|
||||
- 记录本阶段已完成的运行态 / 设计态几何规则、局部重绘提交规则与 overlay 补画规则。
|
||||
- 明确已知延期项与后续主题边界。
|
||||
- 不负责什么:
|
||||
- 不扩展新的布局表达能力。
|
||||
- 不重写 `Dialog` 旧 synthetic move 机制。
|
||||
- 不实现 `Table` 内部局部重绘体系。
|
||||
- 外部依赖:
|
||||
- `Control / Window / Canvas / TabControl / Table / Label / Button`
|
||||
- EasyX 绘制与消息循环环境
|
||||
- 对外能力 / API:
|
||||
- 保持旧 API:`setLayoutMode(...)`、`setAnchor(...)`
|
||||
- 保持显式设计基线提交入口:`commitCurrentGeometryAsDesignRect()`
|
||||
- 保持 `Label::textStyle` 公开,但要求样式修改后手动 `setDirty(true)`
|
||||
- 关键数据 / 状态:
|
||||
- `localx / localy / localWidth / localHeight`
|
||||
- `x / y / width / height`
|
||||
- `LayoutSpec / LayoutCapability / ResolvedLayoutRect`
|
||||
- `eventVisualChanged / dirty / coverage / overlay`
|
||||
|
||||
## 设计说明
|
||||
|
||||
- 核心流程:
|
||||
- 几何变化先在父局部坐标系内统一解算,再通过内部受控路径应用到运行态矩形。
|
||||
- 内容驱动控件在自身受控路径中刷新运行态尺寸;设计基线不得在普通布局过程中自动漂移。
|
||||
- 托管局部重绘按“脏子树提交 -> 直接分支 coverage -> 传递式 overlay 补画”收口,确保嵌套容器、Tooltip、上层兄弟遮挡链闭合。
|
||||
- 关键对象 / 类关系:
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h):统一布局解算、设计基线提交、托管重绘底座
|
||||
- [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp):内容驱动尺寸刷新前移,`draw()` 只消费运行态矩形
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp):直接子树映射、脏子树提交、局部 overlay 补画
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp):外层统一布局,内部页签栏/页面区自管,绘制顺序与局部提交顺序统一
|
||||
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp):当前版本 `X Stretch / Y Fixed`、分页按钮视觉与页码重绘修复
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp):顶层托管重绘、传递式 overlay 补画、顶层 coverage 收口
|
||||
- 生命周期:
|
||||
- 普通 resize / 重排不自动回写 `local*`
|
||||
- 公开 setter 只写运行态,不隐式提交设计基线
|
||||
- 显式设计基线提交只通过 `commitCurrentGeometryAsDesignRect()` 或控件内部受控结构刷新触发
|
||||
- 事件 / 渲染 / 数据流:[按模块类型填写]
|
||||
- `WM_MOUSEMOVE`:真实命中分支处理事件,后续兄弟仅清理鼠标瞬时状态
|
||||
- 托管局部重绘:先提交 dirty root / dirty branch,再按 coverage 传递式补画 overlay
|
||||
- Tooltip / 扩展绘制 coverage:通过 `getManagedRepaintCoverageRect()` 纳入托管 coverage 计算
|
||||
- 关键不变量:
|
||||
- `local*` 只表示设计态父局部矩形
|
||||
- `x / y / width / height` 只表示运行态绘制矩形
|
||||
- `draw()` 不再承担新的几何决策入口
|
||||
- `Table` 当前版本 `Y Fixed` 是实现边界,不是永久产品结论
|
||||
- 降级 / 回退策略:[可选]
|
||||
- Stretch 不满足条件时降级为固定尺寸位移策略,并输出最小必要日志
|
||||
- 控件能力边界拦截 Stretch 请求时,通过日志说明被拦截的轴和原因
|
||||
|
||||
## 实现与影响
|
||||
|
||||
- 关键实现点:
|
||||
- 收口公开 setter、统一布局应用路径、内容驱动路径、显式设计基线提交四类几何写入口
|
||||
- 修复 `WM_MOUSEMOVE` 短路后 hover / tooltip 无法及时清理的问题
|
||||
- 修复局部重绘只认直接 dirty child、不认 dirty descendant 的链路缺口
|
||||
- 修复 coverage 低估导致 Tooltip / overlay 漏补画的问题
|
||||
- 修复 Tab 页签按钮与页面绘制顺序不一致导致 Tooltip 被页面覆盖的问题
|
||||
- 修复重复激活已激活页签时的残影 / 快照链扰动问题
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
|
||||
- [`Label.h`](D:/programming/imGUI-easyX/imGui-easyX/Label.h)
|
||||
- [`Label.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Label.cpp)
|
||||
- [`Button.h`](D:/programming/imGUI-easyX/imGui-easyX/Button.h)
|
||||
- [`Button.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Button.cpp)
|
||||
- [`Canvas.h`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.h)
|
||||
- [`Canvas.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Canvas.cpp)
|
||||
- [`TabControl.h`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.h)
|
||||
- [`TabControl.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TabControl.cpp)
|
||||
- [`Table.h`](D:/programming/imGUI-easyX/imGui-easyX/Table.h)
|
||||
- [`Table.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Table.cpp)
|
||||
- [`TextBox.h`](D:/programming/imGUI-easyX/imGui-easyX/TextBox.h)
|
||||
- [`TextBox.cpp`](D:/programming/imGUI-easyX/imGui-easyX/TextBox.cpp)
|
||||
- [`Dialog.h`](D:/programming/imGUI-easyX/imGui-easyX/Dialog.h)
|
||||
- [`Dialog.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Dialog.cpp)
|
||||
- [`Window.h`](D:/programming/imGUI-easyX/imGui-easyX/Window.h)
|
||||
- [`Window.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Window.cpp)
|
||||
- [`MessageBox.h`](D:/programming/imGUI-easyX/imGui-easyX/MessageBox.h)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- 兼容性影响:
|
||||
- 旧锚点 API 仍可用,内部实现已完全转到新布局模型
|
||||
- `Label::textStyle` 仍为公开字段,但要求样式修改后显式 `setDirty(true)`
|
||||
- `TabControl::setActiveIndex()` 和 `Button::setButtonClick()` 对重复同状态调用新增短路,不再重复触发链路
|
||||
- 性能影响:
|
||||
- 局部重绘 coverage 和 overlay 补画会更保守,补画次数可能略增
|
||||
- 相比整窗 / 整容器强制重绘,这仍是更合理的正确性与性能折中
|
||||
- `Table` 分页按钮视觉变化当前仍提升为整张 `Table` 重绘,颗粒度偏粗但正确性优先
|
||||
- 风险点:
|
||||
- `Dialog` 旧 synthetic move 机制仍与新清理模型并存
|
||||
- `Table` 尚未拥有自己的内部局部重绘体系
|
||||
- 公开 `AxisSizePolicy / AxisAlignPolicy` API 仍未开放,旧 demo 仍受旧入口表达能力限制
|
||||
|
||||
## 测试与验证
|
||||
|
||||
- 测试范围:
|
||||
- `KEY1`:页签重复激活、表格超出页范围残影回归
|
||||
- `KEY5`:三层 `Canvas` 嵌套、跨容器 hover / tooltip、overlay 补画、`TabControl`、`Table`、页码与分页按钮
|
||||
- 核心源码编译级验证:布局主线、重绘主线、Tab / Table / Label / TextBox / Dialog 主线
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `Control.cpp / Button.cpp / Label.cpp / Canvas.cpp / TabControl.cpp / Table.cpp / TextBox.cpp / Dialog.cpp / Window.cpp`
|
||||
2. 编译 `z-testDome.cpp /DKEY=1`
|
||||
3. 编译 `z-testDome.cpp /DKEY=5`
|
||||
4. 手动回归 `KEY1 / KEY5` 中的 tooltip、overlay、页签、分页、三层嵌套和跨容器按钮场景
|
||||
|
||||
- 验证结果:
|
||||
- 编译级验证通过
|
||||
- 基于当前 `KEY1 / KEY5` 用例回归,已知 bug 已修复
|
||||
- GUI 手动回归依赖本机继续确认,当前结论基于现有测试反馈成立
|
||||
- 已知限制 / 遗留问题:[可选]
|
||||
- `Dialog` 旧 synthetic `WM_MOUSEMOVE` 机制尚未统一到新模型
|
||||
- `Table` 内部局部重绘体系尚未实现
|
||||
- Tooltip 智能选位明确后置
|
||||
- 公开 `AxisSizePolicy / AxisAlignPolicy` API 尚未开放,`KEY2` 等旧场景仍受旧 anchor 语义限制
|
||||
|
||||
## 落地信息
|
||||
|
||||
- 关联功能变更 ID:[可选]
|
||||
- `Feature-20260415-0008`
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- `BUG-20260415-0005`
|
||||
- `Fix-BUG-20260415-0005`
|
||||
- `BUG-20260415-0006`
|
||||
- `Fix-BUG-20260415-0006`
|
||||
- `BUG-20260415-0007`
|
||||
- `Fix-BUG-20260415-0007`
|
||||
- `BUG-20260415-0008`
|
||||
- `Fix-BUG-20260415-0008`
|
||||
- Commit: 未提交(当前工作区)
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 相关文档:[可选]
|
||||
- [Module-20260415-0003-布局系统第二阶段收口.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260415-0003-布局系统第二阶段收口.md)
|
||||
- [Feature-20260415-0008-KEY5-第二阶段专项回归场景增强.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/功能变更/Feature-20260415-0008-KEY5-第二阶段专项回归场景增强.md)
|
||||
- [BUG-20260415-0006-托管局部重绘未正确提交脏子树导致嵌套Canvas按钮状态不刷新.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/BUG/BUG-20260415-0006-托管局部重绘未正确提交脏子树导致嵌套Canvas按钮状态不刷新.md)
|
||||
- [Fix-BUG-20260415-0006-托管局部重绘脏子树提交链修复.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/Fix/Fix-BUG-20260415-0006-托管局部重绘脏子树提交链修复.md)
|
||||
- [BUG-20260415-0007-实际绘制coverage低估导致Tooltip与overlay补画漏算.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/BUG/BUG-20260415-0007-实际绘制coverage低估导致Tooltip与overlay补画漏算.md)
|
||||
- [Fix-BUG-20260415-0007-实际绘制coverage与overlay补画链修复.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/Fix/Fix-BUG-20260415-0007-实际绘制coverage与overlay补画链修复.md)
|
||||
- [BUG-20260415-0008-TabControl页签层级与重复激活链路导致Tooltip和残影异常.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/BUG/BUG-20260415-0008-TabControl页签层级与重复激活链路导致Tooltip和残影异常.md)
|
||||
- [Fix-BUG-20260415-0008-TabControl页签层级与重复激活链路修复.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/Fix/Fix-BUG-20260415-0008-TabControl页签层级与重复激活链路修复.md)
|
||||
@@ -1,146 +0,0 @@
|
||||
# 新增功能模块 / 模块重构
|
||||
|
||||
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
|
||||
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- 模块 ID:Module-20260416-0005
|
||||
- 模块名称: 布局策略公开 API 落地
|
||||
- 状态:开发中
|
||||
- 类型:架构演进
|
||||
- 所属系统 / 子系统: GUI 框架 / Layout API
|
||||
- 版本 / 分支: 当前工作区 / 下一开发阶段
|
||||
- 环境: Windows + EasyX
|
||||
- 负责人: Codex 协作修改
|
||||
|
||||
## 背景与目标
|
||||
|
||||
- 背景:
|
||||
- 第一阶段与第二阶段已经把内部布局模型收口到 `LayoutSpec / LayoutCapability / AxisSizePolicy / AxisAlignPolicy`。
|
||||
- 外部调用层仍主要依赖 `setLayoutMode(...)` 与 `setAnchor(...)`,无法直接表达“固定尺寸 + 比例位移”“固定尺寸 + 居中”这类语义。
|
||||
- `KEY2` 顶部位选择区就是典型例子:旧双锚点语义不足,导致按钮与标签 resize 后错位,且难以继续扩展。
|
||||
- 当前痛点:
|
||||
- 外部用户无法直接设置 `AxisSizePolicy / AxisAlignPolicy`。
|
||||
- 旧 demo 继续依赖旧锚点入口,验证成本高,迁移路径不清晰。
|
||||
- 新旧 API 混用规则若不写清,后续容易再次回到“旧 anchor 硬凑新布局”的状态。
|
||||
- 目标:
|
||||
- 将当前内部布局策略正式开放为公开 API。
|
||||
- 明确新旧 API 混用规则与对外可见语义。
|
||||
- 用 `KEY2` 作为新布局 API 的首个迁移样例。
|
||||
- 非目标:[可选]
|
||||
- 不统一 `Dialog` 旧 synthetic `WM_MOUSEMOVE` 机制。
|
||||
- 不实现 `Table` 内部局部重绘体系。
|
||||
- 不实现 Tooltip 智能选位。
|
||||
- 不扩展 `Table` 纵向拉伸和字体缩放。
|
||||
|
||||
## 模块边界
|
||||
|
||||
- 职责:
|
||||
- 对外开放按轴设置布局规格的最小 API。
|
||||
- 固化新旧 API 的混用规则与默认行为。
|
||||
- 提供首个迁移样例,验证新 API 可表达旧场景中“旧 anchor 无法清晰表达”的布局。
|
||||
- 不负责什么:
|
||||
- 不开放 `LayoutCapability` 的通用外部修改接口。
|
||||
- 不改变控件内部硬能力边界。
|
||||
- 不把所有旧 demo 一次性全部迁完。
|
||||
- 外部依赖:
|
||||
- `Control / Canvas / TabControl / Table / Label / TextBox`
|
||||
- `z-testDome.cpp`
|
||||
- 对外能力 / API:
|
||||
- `setHorizontalLayoutSpec(...)`
|
||||
- `setVerticalLayoutSpec(...)`
|
||||
- `setHorizontalAnchors(...)`
|
||||
- `setVerticalAnchors(...)`
|
||||
- `setHorizontalSizePolicy(...)`
|
||||
- `setVerticalSizePolicy(...)`
|
||||
- `setHorizontalAlignPolicy(...)`
|
||||
- `setVerticalAlignPolicy(...)`
|
||||
- `getHorizontalLayoutSpec()`
|
||||
- `getVerticalLayoutSpec()`
|
||||
- 关键数据 / 状态:
|
||||
- `layoutSpec`
|
||||
- `layoutMode`
|
||||
- `layoutCapability`
|
||||
- `localx / localy / localWidth / localHeight`
|
||||
- `x / y / width / height`
|
||||
|
||||
## 设计说明
|
||||
|
||||
- 核心流程:
|
||||
- 外部通过新 API 直接写入水平轴 / 垂直轴布局规格。
|
||||
- 新 API 被调用后,控件自动切到 `AnchorToEdges` 布局模式。
|
||||
- 运行时统一解算继续以 `layoutSpec` 为唯一依据。
|
||||
- 关键对象 / 类关系:
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h):公开布局策略 API 的对外入口。
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp):新 API 的实现、新旧 API 映射共存规则。
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp):`KEY2` 迁移示例与验证入口。
|
||||
- 生命周期:
|
||||
- 新 API 只影响运行态布局策略,不自动提交设计基线。
|
||||
- 设计基线仍由显式提交路径或控件内部受控路径维护。
|
||||
- 事件 / 渲染 / 数据流:[按模块类型填写]
|
||||
- 新 API 不改变事件分发与局部重绘主链。
|
||||
- 只是改变布局规格输入层与解算参数来源。
|
||||
- 关键不变量:
|
||||
- 新旧 API 混用时,后调用者生效。
|
||||
- 旧 API 仍保留兼容,但只覆盖新模型的有限子集。
|
||||
- 公开布局 API 不得自动回写 `local*`。
|
||||
- 降级 / 回退策略:[可选]
|
||||
- 非法组合继续沿用现有降级规则,并输出最小必要日志。
|
||||
- 若后续回退,只需回退 `Control` 中新增 API 与 `KEY2` 迁移代码,不影响第二阶段主线。
|
||||
|
||||
## 实现与影响
|
||||
|
||||
- 关键实现点:
|
||||
- 在 `Control` 层开放最小够用的布局策略 API。
|
||||
- 新 API 调用后自动切换到 `AnchorToEdges`。
|
||||
- 保留 `setAnchor(...)` 与旧 getter,不破坏兼容路径。
|
||||
- 以 `KEY2` 位选择区、功能区、显示区和配置区作为迁移样例,验证新 API 的可用性。
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- [`Control.h`](D:/programming/imGUI-easyX/imGui-easyX/Control.h)
|
||||
- [`Control.cpp`](D:/programming/imGUI-easyX/imGui-easyX/Control.cpp)
|
||||
- [`z-testDome.cpp`](D:/programming/imGUI-easyX/imGui-easyX/z-testDome.cpp)
|
||||
- 兼容性影响:
|
||||
- 兼容旧 `setAnchor(...)` 调用。
|
||||
- 新 API 为新增能力,不影响现有二进制接口使用方式。
|
||||
- 混用时以最后一次设置为准。
|
||||
- 性能影响:
|
||||
- 布局解算主逻辑不变,性能影响可忽略。
|
||||
- `KEY2` 迁移后控件层次变多,但仍处于测试用例范围。
|
||||
- 风险点:
|
||||
- `KEY2` 迁移后可能暴露旧 demo 中原本被旧锚点语义掩盖的布局问题。
|
||||
- 若后续没有补使用说明,外部调用仍可能继续滥用旧锚点入口。
|
||||
|
||||
## 测试与验证
|
||||
|
||||
- 测试范围:
|
||||
- `Control` 新公开布局 API 编译级验证
|
||||
- `KEY2` 迁移后的布局与显示联动
|
||||
- `KEY5` 回归,确认新 API 未破坏第二阶段主线
|
||||
- 验证步骤:
|
||||
|
||||
1. 编译 `Control.cpp`
|
||||
2. 编译 `z-testDome.cpp /DKEY=2`
|
||||
3. 编译 `z-testDome.cpp /DKEY=5`
|
||||
4. 手动回归 `KEY2` 位选择区、功能区、显示区与配置区的 resize 和联动行为
|
||||
|
||||
- 验证结果:
|
||||
- 编译级验证通过。
|
||||
- GUI 手动回归待本机继续确认。
|
||||
- 已知限制 / 遗留问题:[可选]
|
||||
- `Dialog` 旧 synthetic `WM_MOUSEMOVE` 机制暂未统一。
|
||||
- `Table` 内部局部重绘体系暂未实现。
|
||||
- resize 过程中若开启高频 console 日志,可能出现一帧视觉延迟;当前判断属于调试态 I/O 现象,不作为 bug 处理,后续可在官网或 API 文档中注明。
|
||||
|
||||
## 落地信息
|
||||
|
||||
- 关联功能变更 ID:[可选]
|
||||
- `Feature-20260416-0009`
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- [Module-20260415-0004-布局系统第二阶段验收与封口.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260415-0004-布局系统第二阶段验收与封口.md)
|
||||
- Commit:
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 相关文档:[可选]
|
||||
- [Module-20260410-0002-锚点与布局系统第一阶段重构.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260410-0002-锚点与布局系统第一阶段重构.md)
|
||||
- [Module-20260415-0004-布局系统第二阶段验收与封口.md](D:/programming/imGUI-easyX/imGui-easyX/开发记录/模块/Module-20260415-0004-布局系统第二阶段验收与封口.md)
|
||||
@@ -1,51 +0,0 @@
|
||||
# BUG-YYYYMMDD-XXXX
|
||||
|
||||
> 适用场景:记录问题本身,不展开完整修复方案。修复内容写入对应的 Fix 文档。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID:
|
||||
- 标题:
|
||||
- 状态:待修复 / 修复中 / 已修复 / 已验证 / 已关闭
|
||||
- 严重性:S1 / S2 / S3 / S4
|
||||
- 优先级:P0 / P1 / P2 / P3
|
||||
- 模块:
|
||||
- 版本 / 分支:
|
||||
- 环境:
|
||||
- 发现人:
|
||||
- 关联 Fix ID:[可选]
|
||||
|
||||
## 问题描述
|
||||
|
||||
- 现象:
|
||||
- 影响范围:
|
||||
- 期望结果:
|
||||
- 实际结果:
|
||||
|
||||
## 复现信息
|
||||
|
||||
- 前置条件:[可选]
|
||||
- 复现步骤:
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
- 复现概率:必现 / 高概率 / 偶现 / 低概率
|
||||
- 最小复现 Demo:[可选]
|
||||
- 证据:截图 / 日志 / 调用栈 / 录屏 / 断点观察
|
||||
|
||||
## 初步分析
|
||||
|
||||
- 疑似位置:
|
||||
- 触发条件:
|
||||
- 相关线索:
|
||||
- 最近相关改动:[可选]
|
||||
|
||||
## 跟踪信息
|
||||
|
||||
- 首次发现时间:
|
||||
- 最后更新时间:
|
||||
- 修复版本:[可选]
|
||||
- 验证版本:[可选]
|
||||
- 备注:[可选]
|
||||
@@ -1,55 +0,0 @@
|
||||
# Fix-BUG-YYYYMMDD-XXXX
|
||||
|
||||
> 适用场景:记录某个 BUG 的修复方案、影响评估、验证结果与落地信息。
|
||||
|
||||
## 关联信息
|
||||
|
||||
- Fix ID:
|
||||
- 关联 BUG ID:
|
||||
- 修复目标:
|
||||
- 状态:设计中 / 开发中 / 已完成 / 已验证 / 已回滚
|
||||
- 负责人:
|
||||
- 分支 / 版本:
|
||||
|
||||
## 根因分析
|
||||
|
||||
- 根因:
|
||||
- 触发条件:
|
||||
- 为什么之前没发现:[可选]
|
||||
- 关键证据:[可选]
|
||||
|
||||
## 修复方案
|
||||
|
||||
- 修复思路:
|
||||
- 关键改动:
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- 影响的 API / 行为:[可选]
|
||||
- 关键约束 / 不变量:[可选]
|
||||
- 回滚点 / 开关:[可选]
|
||||
|
||||
## 影响评估
|
||||
|
||||
- 影响范围:
|
||||
- 兼容性影响:无 / 有(说明)
|
||||
- 行为变化:无 / 有(说明)
|
||||
- 性能影响:无 / 有(说明)
|
||||
- 回归风险:
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 验证步骤:
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
- 验证结果:
|
||||
- 回归检查:[可选]
|
||||
- 验证证据:[可选]
|
||||
|
||||
## 落地信息
|
||||
|
||||
- Commit:
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
@@ -1,48 +0,0 @@
|
||||
# 功能变更 ID: Feature-YYYYMMDD-XXXXX
|
||||
|
||||
> 适用场景:记录小到中等规模的功能、接口、行为、默认值或配置变化。
|
||||
> 不适用场景:新增核心模块、重大模块重构、架构级设计,请使用“新增功能模块”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- ID:
|
||||
- 标题:
|
||||
- 状态:设计中 / 开发中 / 已完成 / 已验证 / 已发布
|
||||
- 类型:新增 / 修改 / 删除 / 废弃 / 重命名
|
||||
- 级别:L1 轻量 / L2 中等 / L3 重大
|
||||
- 模块:
|
||||
- 版本 / 分支:
|
||||
- 环境:
|
||||
- 负责人:
|
||||
|
||||
## 变更背景
|
||||
|
||||
- 背景:
|
||||
- 目标:
|
||||
- 不做什么:[可选]
|
||||
|
||||
## 变更内容
|
||||
|
||||
- 变更摘要:
|
||||
- 新增项:[可选]
|
||||
- 修改项:[可选]
|
||||
- 删除 / 废弃项:[可选]
|
||||
- 受影响的文件 / 类 / 函数:
|
||||
- 对外 API / 属性变化:[可选]
|
||||
|
||||
## 行为对照
|
||||
|
||||
- 变更前:
|
||||
- 变更后:
|
||||
- 兼容性说明:兼容 / 部分兼容 / 不兼容
|
||||
- 迁移说明:[可选]
|
||||
|
||||
## 验证与落地
|
||||
|
||||
- 验证方式:
|
||||
- 验证结果:
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- Commit:
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 备注:[可选]
|
||||
@@ -1,68 +0,0 @@
|
||||
# 新增功能模块 / 模块重构
|
||||
|
||||
> 适用场景:新增模块、重大模块重构、核心架构能力演进。
|
||||
> 不适用场景:小接口或轻量功能变更,请使用“功能变更”模板。
|
||||
|
||||
## 基本信息
|
||||
|
||||
- 模块 ID:Module-YYYYMMDD-XXXXX
|
||||
- 模块名称:
|
||||
- 状态:设计中 / 开发中 / 已完成 / 已验证 / 已发布
|
||||
- 类型:新增模块 / 模块重构 / 架构演进
|
||||
- 所属系统 / 子系统:
|
||||
- 版本 / 分支:
|
||||
- 环境:
|
||||
- 负责人:
|
||||
|
||||
## 背景与目标
|
||||
|
||||
- 背景:
|
||||
- 当前痛点:
|
||||
- 目标:
|
||||
- 非目标:[可选]
|
||||
|
||||
## 模块边界
|
||||
|
||||
- 职责:
|
||||
- 不负责什么:
|
||||
- 外部依赖:
|
||||
- 对外能力 / API:
|
||||
- 关键数据 / 状态:
|
||||
|
||||
## 设计说明
|
||||
|
||||
- 核心流程:
|
||||
- 关键对象 / 类关系:
|
||||
- 生命周期:
|
||||
- 事件 / 渲染 / 数据流:[按模块类型填写]
|
||||
- 关键不变量:
|
||||
- 降级 / 回退策略:[可选]
|
||||
|
||||
## 实现与影响
|
||||
|
||||
- 关键实现点:
|
||||
- 涉及文件 / 类 / 函数:
|
||||
- 兼容性影响:
|
||||
- 性能影响:
|
||||
- 风险点:
|
||||
|
||||
## 测试与验证
|
||||
|
||||
- 测试范围:
|
||||
- 验证步骤:
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
- 验证结果:
|
||||
- 已知限制 / 遗留问题:[可选]
|
||||
|
||||
## 落地信息
|
||||
|
||||
- 关联功能变更 ID:[可选]
|
||||
- 关联 BUG / Fix:[可选]
|
||||
- Commit:
|
||||
- PR:[可选]
|
||||
- 发布版本:[可选]
|
||||
- 相关文档:[可选]
|
||||
Reference in New Issue
Block a user