Initial baseline

This commit is contained in:
Codex
2026-03-22 18:45:47 +08:00
commit dde570ac3c
31 changed files with 7771 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
.vs/
x64/
.codex-temp/
*.obj
*.exe
*.pdb
*.ilk
*.iobj
*.ipdb
*.tlog/
*.log
*.recipe
*.idb
*.lastbuildstate
*.VC.db
*.VC.VC.opendb
+684
View File
@@ -0,0 +1,684 @@
#include "Button.h"
#include "SxLog.h"
Button::Button(int x, int y, int width, int height, const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape)
: 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();
if (StellarX::ButtonMode::DISABLED == mode) //设置禁用按钮色
{
setfillcolor(DISABLEDCOLOUR);
textStyle.bStrikeOut = true;
}
else
{
// 点击状态优先级最高,然后是悬停状态,最后是默认状态
COLORREF col = click ? buttonTrueColor : (hover ? buttonHoverColor : buttonFalseColor);
setfillcolor(col);
}
//
//设置字体背景色透明
setbkmode(TRANSPARENT);
//边框颜色
setlinecolor(buttonBorderColor);
//设置字体颜色
settextcolor(textStyle.color);
//设置字体样式
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
if (needCutText)
cutButtonText();
//获取字符串像素高度和宽度
if ((this->oldtext_width != this->text_width || this->oldtext_height != this->text_height)
|| (-1 == oldtext_width && oldtext_height == -1))
{
if (isUseCutText)
{
this->oldtext_width = this->text_width = textwidth(LPCTSTR(this->cutText.c_str()));
this->oldtext_height = this->text_height = textheight(LPCTSTR(this->cutText.c_str()));
}
else
{
this->oldtext_width = this->text_width = textwidth(LPCTSTR(this->text.c_str()));
this->oldtext_height = this->text_height = textheight(LPCTSTR(this->text.c_str()));
}
}
//设置按钮填充模式
setfillstyle((int)buttonFillMode, (int)buttonFillIma, buttonFileIMAGE.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; //标记按钮不需要重绘
}
// 处理鼠标事件,检测点击和悬停状态
// 根据按钮模式和形状进行不同的处理
bool Button::handleEvent(const ExMessage& msg)
{
if (!show)
return false;
bool oldHover = hover;// 注意:只在状态变化时记录,避免 WM_MOUSEMOVE 刷屏
bool oldClick = click;
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 (hover != oldHover)
{
SX_LOGD("Button") << SX_T("悬停变化: ","hover change: ") << "id=" << id
<< " text= " << text
<< " " << (oldHover ? 1 : 0) << "->" << (hover ? 1 : 0);
}
// 处理鼠标点击事件
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)
{
SX_LOGD("Button") << SX_T("提示信息显示: ","tooltip show:")<<" id = " << id <<SX_T("延时时间: ", " delayMs = ") << tipDelayMs;
tipVisible = true;
// 定位(跟随鼠标 or 相对按钮)
int tipX = tipFollowCursor ? (lastMouseX + tipOffsetX) : lastMouseX;
int tipY = tipFollowCursor ? (lastMouseY + tipOffsetY) : y + height;
// 设置文本(用户可能动态改了提示文本
if (tipUserOverride)
{
if (mode == StellarX::ButtonMode::NORMAL)
tipLabel.setText(tipTextClick);
else if (mode == StellarX::ButtonMode::TOGGLE)
tipLabel.setText(click ? tipTextOn : tipTextOff);
}
else
if (mode == StellarX::ButtonMode::TOGGLE)
tipLabel.setText(click ? tipTextOn : tipTextOff);
// 设置位置
tipLabel.setX(tipX);
tipLabel.setY(tipY);
// 标记需要绘制
tipLabel.setDirty(true);
}
}
}
// 如果状态发生变化,标记需要重绘
if (hover != oldHover || click != oldClick)
dirty = true;
// 如果需要重绘,立即执行
if (dirty)
requestRepaint(parent);
if (tipEnabled && tipVisible)
tipLabel.draw();
return consume;
}
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)
{
if (this->mode == StellarX::ButtonMode::DISABLED && mode != StellarX::ButtonMode::DISABLED)
textStyle.bStrikeOut = false;
//取值范围参考 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)
{
this->click = click;
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;
tipLabel.hide(); // 还原快照+作废,防止残影
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);
}
+187
View File
@@ -0,0 +1,187 @@
/*******************************************************************************
* @类: 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;
//设置回调函数
void setOnClickListener(std::function<void()> callback);
//设置TOGGLE模式下被点击的回调函数
void setOnToggleOnListener(std::function<void()> callback);
//设置TOGGLE模式下取消点击的回调函数
void setOnToggleOffListener(std::function<void()> callback);
//设置按钮模式
void setbuttonMode(StellarX::ButtonMode mode);
//设置圆角矩形椭圆宽度
void setROUND_RECTANGLEwidth(int width);
//设置圆角矩形椭圆高度
void setROUND_RECTANGLEheight(int height);
//设置按钮填充模式
void setFillMode(StellarX::FillMode mode);
//设置按钮填充图案
void setFillIma(StellarX::FillStyle ima);
//设置按钮填充图像
void setFillIma(std::string imaName);
//设置按钮边框颜色
void setButtonBorder(COLORREF Border);
//设置按钮未被点击颜色
void setButtonFalseColor(COLORREF color);
//设置按钮文本
void setButtonText(const char* text);
void setButtonText(std::string text);
//设置按钮形状
void setButtonShape(StellarX::ControlShape shape);
//设置按钮点击状态
void setButtonClick(BOOL click);
//判断按钮是否被点击
bool isClicked() const;
//获取按钮文字
std::string getButtonText() const;
const char* getButtonText_c() const;
//获取按钮模式
StellarX::ButtonMode getButtonMode() const;
//获取按钮形状
StellarX::ControlShape getButtonShape() const;
//获取按钮填充模式
StellarX::FillMode getFillMode() const;
//获取按钮填充图案
StellarX::FillStyle getFillIma() const;
//获取按钮填充图像
IMAGE* getFillImaImage() const;
//获取按钮边框颜色
COLORREF getButtonBorder() const;
//获取按钮文字颜色
COLORREF getButtonTextColor() const;
//获取按钮文字样式
StellarX::ControlText getButtonTextStyle() const;
public:
// === Tooltip API===
//设置是否启用提示框
void enableTooltip(bool on) { tipEnabled = on; if (!on) tipVisible = false; }
//设置提示框延时
void setTooltipDelay(int ms) { tipDelayMs = (ms < 0 ? 0 : ms); }
//设置提示框是否跟随鼠标
void setTooltipFollowCursor(bool on) { tipFollowCursor = on; }
//设置提示框位置偏移
void setTooltipOffset(int dx, int dy) { tipOffsetX = dx; tipOffsetY = dy; }
//设置提示框样式
void setTooltipStyle(COLORREF text, COLORREF bk, bool transparent);
//设置提示框文本
void setTooltipText(const std::string& s) { tipTextClick = s; tipUserOverride = true; }
void setTooltipTextsForToggle(const std::string& onText, const std::string& offText);
private:
//初始化按钮
void initButton(const std::string text, StellarX::ButtonMode mode, StellarX::ControlShape shape, COLORREF ct, COLORREF cf, COLORREF ch);
//判断鼠标是否在圆形按钮内
bool isMouseInCircle(int mouseX, int mouseY, int x, int y, int radius);
//判断鼠标是否在椭圆按钮内
bool isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height);
//获取对话框类型
bool model() const override { return false; }
//文本截断
void cutButtonText();
// 统一隐藏&恢复背景
void hideTooltip();
// 根据当前 click 状态选择文案
void refreshTooltipTextForState();
};
+450
View File
@@ -0,0 +1,450 @@
#include "Canvas.h"
#include "SxLog.h"
static bool SxIsNoisyMsg(UINT m)
{
return m == WM_MOUSEMOVE;
}
Canvas::Canvas()
:Control(0, 0, 100, 100)
{
this->id = "Canvas";
}
Canvas::Canvas(int x, int y, int width, int height)
:Control(x, y, width, height)
{
this->id = "Canvas";
}
void Canvas::setX(int x)
{
this->x = x;
for (auto& c : controls)
{
c->onWindowResize();
c->setX(c->getLocalX() + this->x);
}
dirty = true;
}
void Canvas::setY(int y)
{
this->y = y;
for (auto& c : controls)
{
c->onWindowResize();
c->setY(c->getLocalY() + this->y);
}
dirty = true;
}
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)
{
discardBackground();
saveBackground(this->x - margin, this->y - margin, this->width + margin * 2, this->height + margin * 2);
}
}
else
{
// 首次绘制或没有快照时直接抓取背景
saveBackground(this->x - margin, this->y - margin, this->width + margin * 2, this->height + margin * 2);
}
// 再次恢复最新快照,确保绘制区域干净
restBackground();
//根据画布形状绘制
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;
bool consumed = false;
bool anyDirty = false;
Control* firstConsumer = nullptr;
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
Control* c = it->get();
bool cConsumed = c->handleEvent(msg);
if (c->isDirty()) anyDirty = true;
if (cConsumed)
{
firstConsumer = c;
consumed = true;
break;
}
}
if (firstConsumer && !SxIsNoisyMsg(msg.message))
{
SX_LOGD("Event") << SX_T("Canvas 消耗消息: ","Canvas consumed: msg=") << msg.message
<< SX_T("子控件"," by child")<<" id=" << firstConsumer->getId();
}
if (anyDirty)
{
if (!SxIsNoisyMsg(msg.message))
SX_LOGD("Dirty") << SX_T("Canvas检测有控件为脏状态 -> 请求重绘, ","Canvas anyDirty -> requestRepaint, ")<<"id = " << id;
requestRepaint(parent);
}
return consumed;
}
void Canvas::addControl(std::unique_ptr<Control> control)
{
//坐标转化
control->setX(control->getLocalX() + this->x);
control->setY(control->getLocalY() + this->y);
control->setParent(this);
SX_LOGI("Canvas")
<< SX_T("添加子控件:父=Canvas 子id=", "addControl: parent=Canvas childId=")
<< control->getId()
<< SX_T(" 相对坐标=(", " local=(")
<< control->getLocalX() << "," << control->getLocalY()
<< SX_T(") 绝对坐标=(", ") abs=(")
<< control->getX() << "," << control->getY()
<< ")";
controls.push_back(std::move(control));
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)
this->updateBackground();
}
void Canvas::setDirty(bool dirty)
{
this->dirty = dirty;
for (auto& control : controls)
control->setDirty(dirty);
}
void Canvas::onWindowResize()
{
// 首先处理自身的快照等逻辑
Control::onWindowResize();
// 记录父容器原始尺寸(用于计算子控件的右/下边距)
int origParentW = this->localWidth;
int origParentH = this->localHeight;
// 当前容器的新尺寸
int finalW = this->width;
int finalH = this->height;
// 当前容器的新坐标(全局坐标)
int parentX = this->x;
int parentY = this->y;
// 调整每个子控件在 AnchorToEdges 模式下的位置与尺寸
for (auto& ch : controls)
{
// Only adjust when using anchor-to-edges layout
if (ch->getLayoutMode() == StellarX::LayoutMode::AnchorToEdges)
{
// Determine whether this child is a Table; tables keep their height constant
bool isTable = (dynamic_cast<Table*>(ch.get()) != nullptr);
// Unpack anchors
auto a1 = ch->getAnchor_1();
auto a2 = ch->getAnchor_2();
bool anchorLeft = (a1 == StellarX::Anchor::Left || a2 == StellarX::Anchor::Left);
bool anchorRight = (a1 == StellarX::Anchor::Right || a2 == StellarX::Anchor::Right);
bool anchorTop = (a1 == StellarX::Anchor::Top || a2 == StellarX::Anchor::Top);
bool anchorBottom = (a1 == StellarX::Anchor::Bottom || a2 == StellarX::Anchor::Bottom);
// If it's a table, treat as anchored left and right horizontally and anchored top vertically by default.
if (isTable)
{
anchorLeft = true;
anchorRight = true;
// If no explicit vertical anchor was provided, default to top.
if (!(anchorTop || anchorBottom))
{
anchorTop = true;
}
}
// Compute new X and width
int newX = ch->getX();
int newWidth = ch->getWidth();
if (anchorLeft && anchorRight)
{
// Scale horizontally relative to parent's size.
if (origParentW > 0)
{
// Maintain proportional position and size based on original local values.
double scaleW = static_cast<double>(finalW) / static_cast<double>(origParentW);
newX = parentX + static_cast<int>(ch->getLocalX() * scaleW + 0.5);
newWidth = static_cast<int>(ch->getLocalWidth() * scaleW + 0.5);
}
else
{
// Fallback: keep original
newX = parentX + ch->getLocalX();
newWidth = ch->getLocalWidth();
}
}
else if (anchorLeft && !anchorRight)
{
// Only left anchored: keep original width and left margin.
newWidth = ch->getLocalWidth();
newX = parentX + ch->getLocalX();
}
else if (!anchorLeft && anchorRight)
{
// Only right anchored: keep original width and right margin.
newWidth = ch->getLocalWidth();
int origRightDist = origParentW - (ch->getLocalX() + ch->getLocalWidth());
newX = parentX + finalW - origRightDist - newWidth;
}
else
{
// No horizontal anchor: position relative to parent's left and width unchanged.
newWidth = ch->getLocalWidth();
newX = parentX + ch->getLocalX();
}
ch->setX(newX);
ch->setWidth(newWidth);
// Compute new Y and height
int newY = ch->getY();
int newHeight = ch->getHeight();
if (isTable)
{
// Table: Height remains constant; adjust Y based on anchors.
newHeight = ch->getLocalHeight();
if (anchorTop && anchorBottom)
{
// If both top and bottom anchored, scale Y but keep height.
if (origParentH > 0)
{
double scaleH = static_cast<double>(finalH) / static_cast<double>(origParentH);
newY = parentY + static_cast<int>(ch->getLocalY() * scaleH + 0.5);
}
else
{
newY = parentY + ch->getLocalY();
}
}
else if (anchorTop && !anchorBottom)
{
// Top anchored only
newY = parentY + ch->getLocalY();
}
else if (!anchorTop && anchorBottom)
{
// Bottom anchored only
int origBottomDist = origParentH - (ch->getLocalY() + ch->getLocalHeight());
newY = parentY + finalH - origBottomDist - newHeight;
}
else
{
// No vertical anchor: default to top
newY = parentY + ch->getLocalY();
}
}
else
{
if (anchorTop && anchorBottom)
{
// Scale vertically relative to parent's size.
if (origParentH > 0)
{
double scaleH = static_cast<double>(finalH) / static_cast<double>(origParentH);
newY = parentY + static_cast<int>(ch->getLocalY() * scaleH + 0.5);
newHeight = static_cast<int>(ch->getLocalHeight() * scaleH + 0.5);
}
else
{
newY = parentY + ch->getLocalY();
newHeight = ch->getLocalHeight();
}
}
else if (anchorTop && !anchorBottom)
{
// Top anchored only: keep height constant
newHeight = ch->getLocalHeight();
newY = parentY + ch->getLocalY();
}
else if (!anchorTop && anchorBottom)
{
// Bottom anchored only: keep height and adjust Y relative to bottom
newHeight = ch->getLocalHeight();
int origBottomDist = origParentH - (ch->getLocalY() + ch->getLocalHeight());
newY = parentY + finalH - origBottomDist - newHeight;
}
else
{
// No vertical anchor: position relative to parent's top, height constant.
newHeight = ch->getLocalHeight();
newY = parentY + ch->getLocalY();
}
}
ch->setY(newY);
ch->setHeight(newHeight);
}
// Always forward the window resize event to the child (recursively).
ch->onWindowResize();
}
}
void Canvas::requestRepaint(Control* parent)
{
if (this == parent)
{
if (!show)
return;
// 关键护栏:
// - Canvas 自己是脏的 / 没有快照 / 缓存图为空
// => 禁止局部重绘,直接升级为一次完整 draw(先把 dirty 置真,避免 draw() 早退)
if (dirty || !hasSnap || !saveBkImage)
{
SX_LOGD("Dirty")
<< SX_T("Canvas 局部重绘降级为全量重绘: id=", "Canvas partial->full draw: id=")
<< id
<< " dirty=" << (dirty ? 1 : 0)
<< " hasSnap=" << (hasSnap ? 1 : 0);
this->dirty = true;
this->draw();
return;
}
SX_LOGD("Dirty") << SX_T("Canvas 请求局部重绘:id=", "Canvas::requestRepaint(partial): id=") << id;
for (auto& control : controls)
if (control->isDirty() && control->IsVisible())
control->draw();
return;
}
SX_LOGD("Dirty") << SX_T("Canvas 请求根级重绘:id=", "Canvas::requestRepaint(root): id=") << id;
onRequestRepaintAsRoot();
}
+73
View File
@@ -0,0 +1,73 @@
/*******************************************************************************
* @类: Canvas
* @摘要: 画布容器控件,用于分组和管理子控件
* @描述:
* 作为其他控件的父容器,提供统一的背景和边框样式。
* 负责将事件传递给子控件并管理它们的绘制顺序。
*
* @特性:
* - 支持四种矩形形状(普通、圆角,各有边框和无边框版本)
* - 可自定义填充模式、边框颜色和背景颜色
* - 自动管理子控件的生命周期和事件传递
* - 支持嵌套容器结构
*
* @使用场景: 用于分组相关控件、实现复杂布局或作为对话框基础
* @所属框架: 星垣(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() {}
void setX(int x)override;
void setY(int y)override;
//绘制容器及其子控件
void draw() override;
bool handleEvent(const ExMessage& msg) override;
//添加控件
void addControl(std::unique_ptr<Control> control);
//设置容器样式
void setShape(StellarX::ControlShape shape);
//设置容器填充模式
void setCanvasfillMode(StellarX::FillMode mode);
//设置容器边框颜色
void setBorderColor(COLORREF color);
//设置填充颜色
void setCanvasBkColor(COLORREF color);
//设置线形
void setCanvasLineStyle(StellarX::LineStyle style);
//设置线段宽度
void setLinewidth(int width);
//设置不可见后传递给子控件重写
void setIsVisible(bool visible) override;
void setDirty(bool dirty) override;
void onWindowResize() override;
void requestRepaint(Control* parent)override;
//获取子控件列表
std::vector<std::unique_ptr<Control>>& getControls() { return controls; }
private:
//用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; };
};
+207
View File
@@ -0,0 +1,207 @@
#include "Control.h"
#include "SxLog.h"
#include<assert.h>
StellarX::ControlText& StellarX::ControlText::operator=(const ControlText& text)
{
{
nHeight = text.nHeight;
nWidth = text.nWidth;
lpszFace = text.lpszFace;
color = text.color;
nEscapement = text.nEscapement;
nOrientation = text.nOrientation;
nWeight = text.nWeight;
bItalic = text.bItalic;
bUnderline = text.bUnderline;
bStrikeOut = text.bStrikeOut;
return *this;
}
}
bool StellarX::ControlText::operator!=(const ControlText& text)
{
if(nHeight != text.nHeight)
return true;
else if (nWidth != text.nWidth)
return true;
else if (lpszFace != text.lpszFace)
return true;
else if (color != text.color)
return true;
else if (nEscapement != text.nEscapement)
return true;
else if (nOrientation != text.nOrientation)
return true;
else if (nWeight != text.nWeight)
return true;
else if (bItalic != text.bItalic)
return true;
else if (bUnderline != text.bUnderline)
return true;
else if (bStrikeOut != text.bStrikeOut)
return true;
return false;
}
void Control::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)
{
// 隐藏:擦除自己在屏幕上的内容,并释放快照
this->updateBackground();
return;
}
// 显示:不在这里 requestRepaint(避免父容器快照未就绪时子控件抢跑 draw,污染快照)
// 仅向上标脏,让事件收口阶段由容器统一重绘。
if (parent)
parent->setDirty(true);
}
void Control::onWindowResize()
{
SX_LOGD("Layout") << SX_T("尺寸变化:id=", "onWindowResize: id=") << id
<< SX_T(" -> 丢背景快照 + 标脏", " -> discardSnap + dirty");
// 自己:丢快照 + 标脏
discardBackground();
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;
}
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;
}
// 保存当前的绘图状态(字体、颜色、线型等)
// 在控件绘制前调用,确保不会影响全局绘图状态
void Control::saveStyle()
{
gettextstyle(&currentFont); // 获取当前字体样式
currentColor = gettextcolor(); // 获取当前字体颜色
currentBorderColor = getlinecolor(); //保存当前边框颜色
getlinestyle(&currentLineStyle); //保存当前线型
currentBkColor = getfillcolor(); //保存当前填充色
}
// 恢复之前保存的绘图状态
// 在控件绘制完成后调用,恢复全局绘图状态
void Control::restoreStyle()
{
settextstyle(&currentFont); // 恢复默认字体样式
settextcolor(currentColor); // 恢复默认字体颜色
setfillcolor(currentBkColor);
setlinestyle(&currentLineStyle);
setlinecolor(currentBorderColor);
setfillstyle(BS_SOLID);//恢复填充
}
void Control::requestRepaint(Control* parent)
{
// 说明:
// - 常规路径:子控件调用 requestRepaint(this->parent),然后 parent 负责局部重绘(Canvas/TabControl override
// - 兜底路径:如果某个“容器控件”没 override requestRepaint,就会出现 parent==this 的递归风险
// 此时我们改为向更上层冒泡,直到根重绘。
if (parent == this)
{
SX_LOGW("Dirty")
<< SX_T("requestRepaint(默认容器兜底):id=", "requestRepaint(default-container-fallback): id=")
<< id
<< SX_T("parent==this,向上层 parent 继续冒泡", " parent==this, bubble to upper parent");
if (this->parent) this->parent->requestRepaint(this->parent);
else onRequestRepaintAsRoot();
return;
}
SX_LOGD("Dirty") << SX_T("请求重绘:id=","requestRepaint: id=") << id << " parent=" << (parent ? parent->getId() : "null");
if (parent) parent->requestRepaint(parent); // 交给容器处理(容器可局部重绘)
else onRequestRepaintAsRoot(); // 根兜底
}
void Control::onRequestRepaintAsRoot()
{
SX_LOGI("Dirty")
<< SX_T("触发根重绘:id=", "onRequestRepaintAsRoot: id=") << id
<< SX_T("(从根节点开始重画)", " (root repaint)");
discardBackground();
setDirty(true);
draw(); // 只有“无父”时才允许立即画,不会被谁覆盖
}
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::updateBackground()
{
restBackground();
discardBackground();
}
+142
View File
@@ -0,0 +1,142 @@
/*******************************************************************************
* @类: Control
* @摘要: 所有控件的抽象基类,定义通用接口和基础功能
* @描述:
* 提供控件的基本属性和方法,包括位置、尺寸、重绘标记等。
* 实现绘图状态保存和恢复机制,确保控件绘制不影响全局状态。
*
* @特性:
* - 定义控件基本属性(坐标、尺寸、脏标记)
* - 提供绘图状态管理(saveStyle/restoreStyle
* - 声明纯虚接口(draw、handleEvent等)
* - 禁止移动语义,禁止拷贝语义
*
* @使用场景: 作为所有具体控件类的基类,不直接实例化
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
#pragma once
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif
#ifndef WINVER
#define WINVER _WIN32_WINNT
#endif
#include <windows.h>
#include <vector>
#include <memory>
#include <easyx.h>
#undef MessageBox
#include <iostream>
#include <string>
#include <functional>
#include "CoreTypes.h"
class Control
{
protected:
std::string id; // 控件ID
int localx, x, localy, y; // 左上角坐标
int localWidth, width, localHeight, height; // 控件尺寸
Control* parent = nullptr; // 父控件
bool dirty = true; // 是否重绘
bool show = true; // 是否显示
/* == 布局模式 == */
StellarX::LayoutMode layoutMode = StellarX::LayoutMode::Fixed; // 布局模式
StellarX::Anchor anchor_1 = StellarX::Anchor::Top; // 锚点
StellarX::Anchor anchor_2 = StellarX::Anchor::Right; // 锚点
/* == 背景快照 == */
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:
//向上请求重绘
virtual void requestRepaint(Control* parent);
//根控件/无父时触发重绘
virtual void onRequestRepaintAsRoot();
protected:
//保存背景快照
virtual void saveBackground(int x, int y, int w, int h);
// putimage 回屏
virtual void restBackground();
// 释放快照(窗口重绘/尺寸变化后必须作废)
void discardBackground();
public:
//释放快照重新保存,在尺寸变化时更新背景快照避免尺寸变化导致显示错位
void updateBackground();
//窗口变化丢快照
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; }
virtual void setX(int x) { this->x = x; dirty = true; }
virtual void setY(int y) { this->y = y; dirty = true; }
virtual void setWidth(int width) { this->width = width; dirty = true; }
virtual void setHeight(int height) { this->height = height; dirty = true; }
public:
virtual void draw() = 0;
virtual bool handleEvent(const ExMessage& msg) = 0;//返回true代表事件已消费
//设置是否显示
virtual void setIsVisible(bool show);
//设置父容器指针
void setParent(Control* parent) { this->parent = parent; }
//设置是否重绘
virtual void setDirty(bool dirty) { this->dirty = dirty; }
//检查控件是否可见
bool IsVisible() const { return show; };
//获取控件id
std::string getId() const { return id; }
//检查是否为脏
bool isDirty() { return dirty; }
//用来检查对话框是否模态,其他控件不用实现
virtual bool model()const = 0;
//布局
void setLayoutMode(StellarX::LayoutMode layoutMode_);
void setAnchor(StellarX::Anchor anchor_1, StellarX::Anchor anchor_2);
StellarX::Anchor getAnchor_1() const;
StellarX::Anchor getAnchor_2() const;
StellarX::LayoutMode getLayoutMode() const;
protected:
void saveStyle();
void restoreStyle();
};
+391
View File
@@ -0,0 +1,391 @@
/*******************************************************************************
* @文件: CoreTypes.h
* @摘要: 星垣(StellarX)框架核心类型定义文件
* @描述:
* 集中定义框架中使用的所有枚举类型和结构体,确保类型一致性。
* 这是框架的类型系统基础,所有控件都依赖于此文件中定义的类型。
*
* @作者: 我在人间做废物
* @日期: September 2025
******************************************************************************/
#pragma once
#include "easyx.h"
/**
* @命名空间: StellarX
*
* @详细说明:
* 集中定义框架中使用的所有枚举类型和结构体,确保类型一致性。
* 这是框架的类型系统基础,所有控件都依赖于此文件中定义的类型
*
* @使用示例:
* StellarX::FillStyle::Horizontal - 填充样式
*
* @备注:
* 不用单独包含此头文件,已在StellarX.h中包含,包含唯一对外头文件即可
*/
namespace StellarX
{
/**
* @枚举类名称: FillStyle
* @功能描述: 用来定义控件填充图案的枚举类
*
* @详细说明:
* 根据此枚举类可以自定义控件填充图案
* 可以在控件初始化时设置填充图案
* 根据具体情况选择不同的填充图案
* 默认填充图案为水平线
*
* @取值说明:
* Horizontal - 水平线
* Vertical - 垂直线
* FDiagonal - 反斜线
* BDiagonal - 正斜线
* Cross - 十字
* DiagCross - 网格
*
* @使用示例:
* FillStyle var = FillStyle::Horizontal;
*
* @备注:
* 此枚举类仅支持图案填充模式
*/
enum class FillStyle
{
Horizontal = HS_HORIZONTAL, // 水平线
Vertical = HS_VERTICAL, // 垂直线
FDiagonal = HS_FDIAGONAL, // 反斜线
BDiagonal = HS_BDIAGONAL, // 正斜线
Cross = HS_CROSS, // 十字
DiagCross = HS_DIAGCROSS // 网格
};
/**
* @枚举类名称: FillMode
* @功能描述: 用来定义控件填充模式的枚举类
*
* @详细说明:
* 根据此枚举类可以自定义控件填充模式
* 可以在控件初始化时设置填充模式
* 根据具体情况选择不同的填充模式
* 默认填充模式为固实填充
*
* @取值说明:
* Solid - 固实填充
* Null - 不填充
* Hatched - 图案填充
* Pattern - 自定义图案填充
* DibPattern - 自定义图像填充
*
* @使用示例:
* FillMode var = FillMode::Solid;
*/
enum class FillMode
{
Solid = BS_SOLID, //固实填充
Null = BS_NULL, // 不填充
Hatched = BS_HATCHED, // 图案填充
Pattern = BS_PATTERN, // 自定义图案填充
DibPattern = BS_DIBPATTERN // 自定义图像填充
};
/**
* @枚举类名称: LineStyle
* @功能描述: 此枚举类用来定义控件边框线型
*
* @详细说明:
* 根据此枚举类可以自定义控件边框线型
* 可以在控件初始化时设置边框线型
* 根据具体情况选择不同的线型
* 默认线型为实线
*
* @取值说明:
* Solid // 实线
* Dash // 虚线
* Dot // 点线
* DashDot // 点划线
* DashDotDot // 双点划线
* Null // 无线
*
* @使用示例:
* LineStyle var = LineStyle::Solid;
*/
enum class LineStyle
{
Solid = PS_SOLID, // 实线
Dash = PS_DASH, // 虚线
Dot = PS_DOT, // 点线
DashDot = PS_DASHDOT, // 点划线
DashDotDot = PS_DASHDOTDOT, // 双点划线
Null = PS_NULL // 无线
};
/**
* @结构体名称: ControlText
* @功能描述: 控件字体样式 可以自定义不同的样式
*
* @详细说明:
* 主要使用的场景为:需要修改或想自定义控件字体大小,字体样式,颜色等
*
* @成员说明:
* int nHeight = 0; - 字体高度
* int nWidth = 0; - 字体宽度 如果为0则自适应
* LPCTSTR lpszFace = "宋体"; - 字体名称
* COLORREF color = RGB(0, 0, 0); - 字体颜色
* int nEscapement = 0; - 字符串旋转角度
* int nOrientation = 0; - 字符旋转角度
* int nWeight = 0; - 字体粗细 范围0~1000 0表示默认
* bool bItalic = false; - 是否斜体
* bool bUnderline = false; - 是否下划线
* bool bStrikeOut = false; - 是否删除线
*/
struct ControlText
{
int nHeight = 0; //- 字体高度
int nWidth = 0; //- 字体宽度 如果为0则自适应
LPCTSTR lpszFace = "微软雅黑"; //- 字体名称
COLORREF color = RGB(0, 0, 0); //- 字体颜色
int nEscapement = 0; //- 字符串旋转角度
int nOrientation = 0; //- 字符旋转角度
int nWeight = 0; //- 字体粗细 范围0~1000 0表示默认
bool bItalic = false; //- 是否斜体
bool bUnderline = false; //- 是否下划线
bool bStrikeOut = false; //- 是否删除线
bool operator!=(const ControlText& text);
ControlText& operator=(const ControlText& text);
};
/**
* @枚举名称: ControlShape
* @功能描述: 枚举控件的不同几何样式
*
* @详细说明:
* 定义了四种(有无边框算一种)不同的几何样式,可以根据具体需求
* 自定义控件的形状。
*
* @取值说明:
* RECTANGLE = 1, //有边框矩形
* B_RECTANGLE, //无边框矩形
* ROUND_RECTANGLE, //有边框圆角矩形
* B_ROUND_RECTANGLE, //无边框圆角矩形
* CIRCLE, //有边框圆形
* B_CIRCLE, //无边框圆形
* ELLIPSE, //有边框椭圆
* B_ELLIPSE //无边框椭圆
*
* @使用示例:
* ControlShape shape = ControlShape::ELLIPSE;
*
* @备注:
* 按钮类支持所有形状,部分控件只支持部分形状,具体请参考控件类。
*/
enum class ControlShape
{
RECTANGLE = 1, //有边框矩形
B_RECTANGLE, //无边框矩形
ROUND_RECTANGLE, //有边框圆角矩形
B_ROUND_RECTANGLE, //无边框圆角矩形
CIRCLE, //有边框圆形
B_CIRCLE, //无边框圆形
ELLIPSE, //有边框椭圆
B_ELLIPSE //无边框椭圆
};
/**
* @枚举类名称: TextBoxmode
* @功能描述: 定义了文本框的三种模式
*
* @详细说明:
* 需要限制文本框是否接受用户输入时使用
*
* @取值说明:
* INPUT_MODE, // 用户可输入模式
* READONLY_MODE // 只读模式
* PASSWORD_MODE // 密码模式
*/
enum class TextBoxmode
{
INPUT_MODE, // 用户可输入模式
READONLY_MODE, // 只读模式
PASSWORD_MODE // 密码模式
};
/**
* @枚举名称: ButtonMode
* @功能描述: 定义按钮的工作模式
*
* @详细说明:
* 根据按钮的工作模式,按钮可以有不同的行为。
* 用户可以在具体情况下设置按钮的工作模式。
*
* @取值说明:
* NORMAL = 1, - 普通模式,点击后触发回调,但不会保持状态。
* TOGGLE, - 切换模式,点击后会在选中和未选中之间切换,触发不同的回调函数。
* DISABLED - 禁用模式,按钮不可点击,显示为灰色,文本显示删除线。
*
* @使用示例:
* ButtonMode mode = ButtonMode::NORMAL;
*/
enum class ButtonMode
{
NORMAL = 1, //普通模式
TOGGLE, //切换模式
DISABLED //禁用模式
};
/**
* @结构体名称: RouRectangle
* @功能描述: 定义了控件圆角矩形样式时圆角的椭圆尺寸
*
* @详细说明:
* 需要修改控件圆角矩形样式时的圆角椭圆。
*
* @成员说明:
* int ROUND_RECTANGLEwidth = 20; //构成圆角矩形的圆角的椭圆的宽度。
* int ROUND_RECTANGLEheight = 20; //构成圆角矩形的圆角的椭圆的高度。
*/
struct RouRectangle
{
int ROUND_RECTANGLEwidth = 20; //构成圆角矩形的圆角的椭圆的宽度。
int ROUND_RECTANGLEheight = 20; //构成圆角矩形的圆角的椭圆的高度。
};
// 消息框类型
enum class MessageBoxType
{
OK, // 只有确定按钮
OKCancel, // 确定和取消按钮
YesNo, // 是和否按钮
YesNoCancel, // 是、否和取消按钮
RetryCancel, // 重试和取消按钮
AbortRetryIgnore, // 中止、重试和忽略按钮
};
// 消息框返回值
enum class MessageBoxResult
{
OK = 1, // 确定按钮
Cancel = 2, // 取消按钮
Yes = 6, // 是按钮
No = 7, // 否按钮
Abort = 3, // 中止按钮
Retry = 4, // 重试按钮
Ignore = 5 // 忽略按钮
};
#if 0 //布局管理器相关 —待实现—
/*
*
*@枚举名称: LayoutKind
* @功能描述 : 定义布局管理类型
*
*@详细说明 :
* 根据布局管理类型,控件可以有不同的布局方式。
* 用户可以在具体情况下设置布局管理类型。
*
* @取值说明 :
* Absolute:不管,保持子控件自己的坐标(向后兼容)。
* HBox: 水平方向排队;支持固定宽、权重分配、对齐、拉伸。
* VBox: 竖直方向排队;同上。
* Grid(网格):按行列摆;支持固定/自适应/权重行列;支持跨行/跨列;单元内对齐/拉伸。
*
*/
// 布局类型
enum class LayoutKind
{
Absolute = 1,
HBox,
VBox,
Grid,
Flow,
Stack
};
// 布局参数
struct LayoutParams
{
// 边距左、右、上、下
int marginL = 0, marginR = 0, marginT = 0, marginB = 0;
// 固定尺寸(>=0 强制;-1 用控件当前尺寸)
int fixedW = -1, fixedH = -1;
// 主轴权重(HBox=宽度、VBox=高度、Grid见下)
float weight = 0.f;
// 对齐(非拉伸时生效)
enum Align { Start = 0, Center = 1, End = 2, Stretch = 3 };
int alignX = Start; // HBox: 次轴=YVBox: 次轴=XGrid: 单元内
int alignY = Start; // Grid :控制单元内垂直(HBox / VBox通常只用 alignX
// Grid 专用(可先不做)
int gridRow = 0, gridCol = 0, rowSpan = 1, colSpan = 1;
// Flow 专用(可先不做)
int flowBreak = 0; // 1=强制换行
};
#endif
/*
* @枚举名称: TabPlacement
* @功能描述: 定义了选项卡页签的不同位置
*
* @详细说明:
* 根据选项卡页签的位置,选项卡页签可以有不同的布局方式。
*
* @成员说明:
* Top, - 选项卡页签位于顶部
* Bottom, - 选项卡页签位于底部
* Left, - 选项卡页签位于左侧
* Right - 选项卡页签位于右侧
*
* @使用示例:
* TabPlacement placement = TabPlacement::Top;
*/
enum class TabPlacement
{
Top,
Bottom,
Left,
Right
};
/*
* @枚举名称: LayoutMode
* @功能描述: 定义了两种布局模式
*
* @详细说明:
* 根据不同模式,在窗口拉伸时采用不同的布局策略
*
* @成员说明:
* Fixed, - 固定布局
* AnchorToEdges - 锚定布局
*
* @使用示例:
* LayoutMode mode = LayoutMode::Fixed;
*/
enum class LayoutMode
{
Fixed,
AnchorToEdges
};
/*
* @枚举名称: Anchor
* @功能描述: 定义了控件相对于窗口锚定的位置
*
* @详细说明:
* 根据不同的锚定位置,有不同的拉伸策略
*
* @成员说明:
* Top, - 锚定上边,控件上边与窗口上侧距离保持不变
* Bottom, - 锚定底边,控件底边与窗口底边距离保持不变
* Left, - 锚定左边,控件左边与窗口左侧距离保持不变
* Right - 锚定右边,控件上边与窗口右侧距离保持不变
*
* @使用示例:
* Anchor a = Anchor::Top;
*/
enum class Anchor
{
NoAnchor = 0,
Left = 1,
Right,
Top,
Bottom
};
}
+722
View File
@@ -0,0 +1,722 @@
#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";
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);
//设置字体样式
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
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;
if (!show)
{
if (pendingCleanup && !isCleaning)
{
performDelayedCleanup();
}
return false;
}
// 如果正在清理或标记为待清理,则不处理事件
if (pendingCleanup || isCleaning)
return false;
// 模态对话框不允许点击外部区域
// 模态对话框:点击对话框外部区域时,发出提示音(\a)并吞噬该事件,不允许操作背景内容。
if (modal && msg.message == WM_LBUTTONUP &&
(msg.x < x || msg.x > x + width || msg.y < y || msg.y > y + height))
{
std::cout << "\a" << std::endl;
return true;
}
// 将事件传递给子控件处理
if (!consume)
consume = Canvas::handleEvent(msg);
// 每次事件处理后检查是否需要执行延迟清理
if (pendingCleanup && !isCleaning)
performDelayedCleanup();
return consume;
}
void Dialog::SetTitle(const std::string& title)
{
this->titleText = title;
if (this->title)
{
this->title->setText(title);
}
dirty = true;
}
void Dialog::SetMessage(const std::string& message)
{
this->message = message;
splitMessageLines();
getTextSize();
dirty = true;
}
void Dialog::SetType(StellarX::MessageBoxType type)
{
this->type = type;
// 重新初始化按钮
initButtons();
dirty = 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;
close = false;
shouldClose = false;
hWnd.dialogOpen = true;// 通知窗口有对话框打开
if (modal)
{
// 模态对话框需要阻塞当前线程直到对话框关闭
if (modal)
{
// 记录当前窗口客户区尺寸,供轮询对比
RECT rc0;
GetClientRect(hWnd.getHwnd(), &rc0);
int lastW = rc0.right - rc0.left;
int lastH = rc0.bottom - rc0.top;
while (show && !close)
{
// ① 轮询窗口尺寸(不依赖 WM_SIZE)
RECT rc;
GetClientRect(hWnd.getHwnd(), &rc);
const int cw = rc.right - rc.left;
const int ch = rc.bottom - rc.top;
if (cw != lastW || ch != lastH)
{
lastW = cw;
lastH = ch;
SX_LOGD("Resize") <<SX_T("模态对话框检测到窗口大小变化:(", "Modal dialog detected window size change: (") << cw << "x" << ch << ")";
// 通知父窗口:有新尺寸 → 标记 needResizeDirty
hWnd.scheduleResizeFromModal(cw, ch);
// 立即统一收口:父窗重绘 背景+普通控件(不会画到这只模态)
hWnd.pumpResizeIfNeeded();
// 这只模态在新尺寸下重建布局 / 重抓背景 → 本帧要画自己
setInitialization(true);
setDirty(true);
}
// ② 处理这只对话框的鼠标/键盘(沿用原来 EX_MOUSE | EX_KEY
ExMessage msg;
if (peekmessage(&msg, EX_MOUSE | EX_KEY))
{
handleEvent(msg);
if (shouldClose)
{
Close();
break;
}
}
// ③ 最后一笔:只画这只模态,保证永远在最上层
if (dirty)
{
BeginBatchDraw();
this->draw(); // 注意:不要 requestRepaint(parent),只画自己
EndBatchDraw();
dirty = false;
}
Sleep(10);
}
if (pendingCleanup && !isCleaning)
performDelayedCleanup();
}
else
{
// 非模态仍由主循环托管
dirty = true;
}
// 模态对话框关闭后执行清理
if (pendingCleanup && !isCleaning)
performDelayedCleanup();
}
else
// 非模态对话框只需标记为可见,由主循环处理
dirty = true;
}
void Dialog::Close()
{
if (!show) return;
show = false;
close = true;
dirty = true;
pendingCleanup = true; // 只标记需要清理,不立即执行
// 工厂模式下非模态触发回调 返回结果
if (resultCallback && !modal)
resultCallback(this->result);
}
void Dialog::setInitialization(bool init)
{
if (init)
{
initDialogSize();
saveBackground((x - BorderWidth), (y - BorderWidth), (width + 2 * BorderWidth), (height + 2 * BorderWidth));
this->dirty = true;
}
}
void Dialog::initButtons()
{
controls.clear();
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::initTitle()
{
auto titleLabel = std::make_unique<Label>(this->x + 5, this->y + 5, titleText, textStyle.color);
titleLabel->setTextdisap(true);
titleLabel->textStyle = this->textStyle;
this->title = titleLabel.get();
this->addControl(std::move(titleLabel));
}
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);
int tempHeight = 0;
int tempWidth = 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::initDialogSize()
{
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;
// 对话框宽度取两者中的较大值,并确保最小宽度
this->width = buttonAreaWidth > textAreaWidth ? buttonAreaWidth : textAreaWidth;
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;
//this->textStyle.nWidth = 10;
this->textStyle.nHeight = 20;
initButtons(); // 初始化按钮
initTitle(); // 初始化标题标签
initCloseButton(); // 初始化关闭按钮
}
void Dialog::addControl(std::unique_ptr<Control> control)
{
control->setParent(this);
controls.push_back(std::move(control));
dirty = true;
}
// 延迟清理策略:由于对话框绘制时保存了背景快照,必须在对话框隐藏后、
// 所有控件析构前恢复背景,否则会导致背景图像被错误覆盖。
// 此方法在对话框不可见且被标记为待清理时由 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;
title = nullptr;
// 释放背景图像资源
if (saveBkImage && hasSnap)
{
restBackground();
FlushBatchDraw();
discardBackground();
}
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;
shouldClose = 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()
{
controls.clear();
// 重置按钮指针
closeButton = nullptr;
title = 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 (this == parent)
{
for (auto& control : controls)
if (control->isDirty() && control->IsVisible())
control->draw();
}
else
onRequestRepaintAsRoot();
}
+129
View File
@@ -0,0 +1,129 @@
/*******************************************************************************
* @类: Dialog
* @摘要: 模态和非模态对话框控件,提供丰富的消息框功能
* @描述:
* 实现完整的对话框功能,支持多种按钮组合和异步结果回调。
* 自动处理布局、背景保存恢复和生命周期管理。
*
* @特性:
* - 支持六种标准消息框类型(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
{
Window& hWnd; //窗口引用
int textWidth = 0; //文本宽度
int textHeight = 0; //文本高度
int buttonNum = 0; // 按钮数量
StellarX::MessageBoxType type = StellarX::MessageBoxType::OK; //对话框类型
std::string titleText = "提示"; //标题文本
Label* title = nullptr; //标题标签(由 controls 持有)
std::string message; //提示信息
std::vector<std::string> lines; //消息内容按行分割
bool needsInitialization = true; //是否需要初始化
bool close = false; //是否关闭
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 shouldClose = false; //是否应该关闭
bool isCleaning = false; //是否正在清理
bool pendingCleanup = false; //延迟清理
public:
StellarX::ControlText textStyle; // 字体样式
// 清理方法声明
void performDelayedCleanup();
//工厂模式下调用非模态对话框时用来返回结果
std::function<void(StellarX::MessageBoxResult)> resultCallback;
//设置非模态获取结果的回调函数
void SetResultCallback(std::function<void(StellarX::MessageBoxResult)> cb);
//获取对话框消息,用以去重
std::string GetCaption() const;
//获取对话框消息,用以去重
std::string GetText() const;
public:
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();
//初始化
void setInitialization(bool init);
private:
// 初始化按钮
void initButtons();
// 初始化关闭按钮
void initCloseButton();
// 初始化标题
void initTitle();
// 按行分割消息内容
void splitMessageLines();
// 获取文本大小
void getTextSize();
//初始化对话框尺寸
void initDialogSize();
void addControl(std::unique_ptr<Control> control);
// 清除所有控件
void clearControls();
//创建对话框按钮
std::unique_ptr<Button> createDialogButton(int x, int y, const std::string& text);
void requestRepaint(Control* parent) override;
};
+79
View File
@@ -0,0 +1,79 @@
#include "Label.h"
Label::Label()
:Control(0, 0, 0, 0)
{
this->id = "Label";
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->text = text;
textStyle.color = textcolor;
textBkColor = bkColor; //默认白色背景
}
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); //设置字体样式
const int newWidth = textwidth(text.c_str());
const int newHeight = textheight(text.c_str());
if (newWidth != this->width || newHeight != this->height)
{
if (hasSnap)
discardBackground();
this->width = newWidth;
this->height = newHeight;
}
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()
{
restBackground(); // 还原屏幕像素
discardBackground(); // 作废快照,防止错贴旧图
dirty = false;
}
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;
this->dirty = true;
}
+46
View File
@@ -0,0 +1,46 @@
/*******************************************************************************
* @类: Label
* @摘要: 简单文本标签控件,用于显示静态文本
* @描述:
* 提供基本的文本显示功能,支持透明背景和自定义样式。
* 不支持用户交互,专注于文本呈现。
*
* @特性:
* - 支持背景透明/不透明模式
* - 完整的文本样式控制(字体、颜色、效果)
* - 自动适应文本内容
* - 轻量级无事件处理开销
*
* @使用场景: 显示说明文字、标题、状态信息等静态内容
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
#pragma once
#include "Control.h"
class Label : public Control
{
std::string text; //标签文本
COLORREF textBkColor; //标签背景颜色
bool textBkDisap = false; //标签背景是否透明
//标签事件处理(标签无事件)不实现具体代码
bool handleEvent(const ExMessage& msg) override { return false; }
//用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; };
public:
StellarX::ControlText textStyle; //标签文本样式
public:
Label();
Label(int x, int y, std::string text = "标签", COLORREF textcolor = BLACK, COLORREF bkColor = RGB(255, 255, 255));
void draw() override;
void hide();
//设置标签背景是否透明
void setTextdisap(bool key);
//设置标签背景颜色
void setTextBkColor(COLORREF color);
//设置标签文本
void setText(std::string text);
};
+39
View File
@@ -0,0 +1,39 @@
#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.setInitialization(true);
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();
dlgPtr->setInitialization(true);
// 设置回调
if (onResult)
dlgPtr->SetResultCallback(std::move(onResult));
// 交给 Window 管理生命周期
wnd.addDialog(std::move(dlg));
dlgPtr->Show();
}
}
+43
View File
@@ -0,0 +1,43 @@
/*******************************************************************************
* @类: 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
+47
View File
@@ -0,0 +1,47 @@
/*******************************************************************************
* @文件: StellarX.h
* @摘要: 星垣(StellarX) GUI框架 - 主包含头文件
* @版本: v3.0.1
* @描述:
* 一个为Windows平台打造的轻量级、模块化C++ GUI框架。
* 基于EasyX图形库,提供简洁易用的API和丰富的控件。
*
* 通过包含此单一头文件,即可使用框架的所有功能。
* 内部包含顺序经过精心设计,确保所有依赖关系正确解析。
*
* @作者: 我在人间做废物
* @邮箱: [3150131407@qq.com] | [ysm3150131407@gmail.com]
* @官网:https://stellarx-gui.top/
* @仓库: https://github.com/Ysm-04/StellarX
* @博客:https://blog.stellarx-gui.top/
*
* @许可证: MIT License
* @版权: Copyright (c) 2025 我在人间做废物
*
* @使用说明:
* 只需包含此文件即可使用框架所有功能。
* 示例: #include "StellarX.h"
* @包含顺序:
* 1. CoreTypes.h - 基础类型定义
* 2. Control.h - 控件基类
* 3. ...其他具体控件头文件
* 4. Dialog:继承自 CanvasDialog 为可包含子控件的对话框容器)
* 5. MessageBox:对话框工厂,提供便捷的模态/非模态调用方式
******************************************************************************/
#pragma once
#include "CoreTypes.h"
#include "SxLog.h"
#include "Control.h"
#include"Canvas.h"
#include"Window.h"
#include"Button.h"
#include"Label.h"
#include"TextBox.h"
#include"Table.h"
#include"Dialog.h"
#include"MessageBox.h"
#include"TabControl.h"
+443
View File
@@ -0,0 +1,443 @@
#include "SxLog.h"
#include <cstdlib>
#include <clocale>
/********************************************************************************
* @文件: SxLog.cpp
* @摘要: StellarX 日志系统实现(过滤/格式化/输出/文件滚动/RAII提交/作用域计时)
* @描述:
* 该实现文件主要包含 4 个关键点:
* 1) FileSink: 文件打开、写入、flush 与按阈值滚动
* 2) SxLogger: shouldLog 过滤、formatPrefix 前缀拼接、logLine 统一输出出口
* 3) SxLogLine: 析构提交(RAII)确保“一条语句输出一整行”
* 4) SxLogScope: 按需启用计时,析构输出耗时
*
* @实现难点提示:
* - shouldLog 必须“零副作用”,否则宏短路会带来不可预测行为
* - logLine 是统一出口,必须保证行级一致性,且避免在持锁状态下递归打日志
* - 文件滚动要处理文件名安全性与跨平台 rename 行为差异
* - 时间戳生成需要兼容 Windows 与 POSIXlocaltime_s/localtime_r
********************************************************************************/
namespace StellarX
{
// -------- FileSink --------
// 打开文件输出
// 难点:
// - 需要支持追加与清空两种模式
// - open 前先 close,避免重复打开导致句柄泄漏
bool FileSink::open(const std::string& path, bool append)
{
close();
filePath = path;
appendMode = append;
std::ios::openmode mode = std::ios::out;
mode |= (append ? std::ios::app : std::ios::trunc);
ofs.open(path.c_str(), mode);
return ofs.is_open();
}
// 关闭文件输出(可重复调用)
void FileSink::close()
{
if (ofs.is_open()) ofs.close();
}
// 查询是否已打开
bool FileSink::isOpen() const
{
return ofs.is_open();
}
// 写入一整行
// 难点:
// - 写入后若启用 rotateBytes,需要及时检测文件大小是否到阈值
void FileSink::writeLine(const std::string& line)
{
if (!ofs.is_open()) return;
ofs << line;
if (rotateBytes > 0) rotateIfNeeded();
}
// flush 文件缓冲
void FileSink::flush()
{
if (ofs.is_open()) ofs.flush();
}
// 滚动文件
// 难点:
// 1) tellp() 返回的是当前写指针位置,通常可近似视为文件大小
// 2) 时间戳用于文件名时需要做字符清洗,避免出现不友好字符
// 3) rename 行为与权限/占用有关,失败时需要保证不崩溃(此处选择“尽力而为”)
bool FileSink::rotateIfNeeded()
{
if (!ofs.is_open() || rotateBytes == 0) return false;
const std::streampos pos = ofs.tellp();
if (pos < 0) return false;
const std::size_t size = static_cast<std::size_t>(pos);
if (size < rotateBytes) return false;
ofs.flush();
ofs.close();
// xxx.log -> xxx.log.YYYYmmdd_HHMMSS
// 说明:
// - makeTimestampLocal 形如 "2026-01-09 12:34:56"
// - 文件名中把 '-' ' ' ':' 替换为 '_',只保留数字与 '_',降低环境差异
const std::string ts = SxLogger::makeTimestampLocal();
std::string safeTs;
safeTs.reserve(ts.size());
for (char ch : ts)
{
if (ch >= '0' && ch <= '9') safeTs.push_back(ch);
else if (ch == '-' || ch == ' ' || ch == ':') safeTs.push_back('_');
}
if (safeTs.empty()) safeTs = "rotated";
const std::string rotated = filePath + "." + safeTs;
std::rename(filePath.c_str(), rotated.c_str());
// 重新打开新文件
// 注意: 这里用 append=false,确保新文件从空开始
return open(filePath, false);
}
// -------- SxLogger --------
// 设置 Windows 控制台 codepage(只执行一次)
// 难点:
// - 只影响终端解释输出字节的方式,不影响源码文件编码
// - 使用 once_flag 避免重复 system 调用造成噪声与性能浪费
//
void SxLogger::setGBK()
{
#ifdef _WIN32
static std::once_flag once;
std::call_once(once, []() {
// 切到chcp 936GBK),避免中文日志在 CP936 控制台下乱码
// 说明:这不是 WinAPI;是执行系统命令
std::system("chcp 936 >nul");
// 补充说明:
// - chcp 936 实际是设置为 CP936GBK
// - 如果你的终端本身是 UTF-8 环境,调用它可能反而改变显示行为
// - 该函数建议只在“明确需要 GBK 控制台输出”的场景调用
// 尝试让 C/C++ 运行库按 UTF-8 工作(对部分流输出有帮助)
// std::setlocale(LC_ALL, ".UTF8");
});
#endif
}
// 获取单例
// 难点:
// - 作为全局入口,初始化必须线程安全
// - C++11 起函数内静态对象初始化由标准保证线程安全
SxLogger& SxLogger::Get()
{
static SxLogger inst;
return inst;
}
// 构造:设置默认语言
SxLogger::SxLogger()
: lang(SxLogLanguage::ZhCN)
{
}
// 设置最低输出级别
void SxLogger::setMinLevel(SxLogLevel level)
{
std::lock_guard<std::mutex> lock(mtx);
cfg.minLevel = level;
}
// 获取最低输出级别
SxLogLevel SxLogger::getMinLevel() const
{
std::lock_guard<std::mutex> lock(mtx);
return cfg.minLevel;
}
// 设置语言
// 难点:
// - 语言只影响 SX_T 的字符串选择
// - 这里用 atomic relaxed,避免频繁加锁
void SxLogger::setLanguage(SxLogLanguage l)
{
lang.store(l, std::memory_order_relaxed);
}
// 获取语言
SxLogLanguage SxLogger::getLanguage() const
{
return lang.load(std::memory_order_relaxed);
}
// 设置 Tag 过滤
// 难点:
// - 当前实现是 vector<string> 线性匹配,适合 tag 数量不大
// - 若未来 tag 很多,可考虑 unordered_set 优化(但会增加依赖与复杂度)
void SxLogger::setTagFilter(SxTagFilterMode mode, const std::vector<std::string>& tags)
{
std::lock_guard<std::mutex> lock(mtx);
cfg.tagFilterMode = mode;
cfg.tagList = tags;
}
// 清空 Tag 过滤
void SxLogger::clearTagFilter()
{
std::lock_guard<std::mutex> lock(mtx);
cfg.tagFilterMode = SxTagFilterMode::None;
cfg.tagList.clear();
}
// 开关控制台输出
// 难点:
// - ConsoleSink 持有 ostream 引用,不管理其生命周期
void SxLogger::enableConsole(bool enable)
{
std::lock_guard<std::mutex> lock(mtx);
if (enable)
{
if (!consoleSink) consoleSink.reset(new ConsoleSink(std::cout));
}
else
{
consoleSink.reset();
}
}
// 开启文件输出
// 难点:
// - enableFile 成功与否决定 cfg.fileEnabled
// - 需要把 rotateBytes 同步到 FileSink
bool SxLogger::enableFile(const std::string& path, bool append, std::size_t rotateBytes_)
{
std::lock_guard<std::mutex> lock(mtx);
if (!fileSink) fileSink.reset(new FileSink());
fileSink->setRotateBytes(rotateBytes_);
const bool ok = fileSink->open(path, append);
cfg.fileEnabled = ok;
cfg.filePath = path;
cfg.fileAppend = append;
cfg.rotateBytes = rotateBytes_;
return ok;
}
// 关闭文件输出
void SxLogger::disableFile()
{
std::lock_guard<std::mutex> lock(mtx);
if (fileSink) fileSink->close();
cfg.fileEnabled = false;
}
// 获取配置副本
// 难点:
// - 返回副本避免外部拿到内部引用后绕过锁修改
SxLogConfig SxLogger::getConfigCopy() const
{
std::lock_guard<std::mutex> lock(mtx);
return cfg;
}
// 设置配置(整体替换)
void SxLogger::setConfig(const SxLogConfig& c)
{
std::lock_guard<std::mutex> lock(mtx);
cfg = c;
}
// 级别转字符串
const char* SxLogger::levelToString(SxLogLevel level)
{
switch (level)
{
case SxLogLevel::Trace: return "TRACE";
case SxLogLevel::Debug: return "DEBUG";
case SxLogLevel::Info: return "INFO ";
case SxLogLevel::Warn: return "WARN ";
case SxLogLevel::Error: return "ERROR";
case SxLogLevel::Fatal: return "FATAL";
default: return "OFF ";
}
}
// 判断 tag 是否允许输出
// 难点:
// - 精确匹配 tag 字符串
// - tag==nullptr 时默认允许,避免“无 tag 日志被误杀”
bool SxLogger::tagAllowed(const SxLogConfig& c, const char* tag)
{
if (c.tagFilterMode == SxTagFilterMode::None) return true;
if (!tag) return true;
bool found = false;
for (const auto& t : c.tagList)
{
if (t == tag) { found = true; break; }
}
if (c.tagFilterMode == SxTagFilterMode::Whitelist) return found;
if (c.tagFilterMode == SxTagFilterMode::Blacklist) return !found;
return true;
}
// 快速判定是否需要输出(宏短路依赖)
// 难点:
// 1) 必须无副作用:返回 false 时调用端不会构造对象也不会拼接
// 2) 过滤维度要完整:级别、tag、sink 是否启用
// 3) 当前实现加锁保证 cfg 与 sink 状态一致;代价是高频路径会有锁开销
bool SxLogger::shouldLog(SxLogLevel level, const char* tag) const
{
std::lock_guard<std::mutex> lock(mtx);
if (cfg.minLevel == SxLogLevel::Off) return false;
if (level < cfg.minLevel) return false;
if (!tagAllowed(cfg, tag)) return false;
if (!consoleSink && !cfg.fileEnabled) return false;
return true;
}
// 生成本地时间戳字符串
// 难点:
// - Windows 与 POSIX 的线程安全 localtime API 不同
std::string SxLogger::makeTimestampLocal()
{
using namespace std::chrono;
const auto now = system_clock::now();
const std::time_t t = system_clock::to_time_t(now);
std::tm tmv{};
#if defined(_WIN32)
localtime_s(&tmv, &t);
#else
localtime_r(&t, &tmv);
#endif
std::ostringstream oss;
oss << std::put_time(&tmv, "%Y-%m-%d %H:%M:%S");
return oss.str();
}
// 拼接日志前缀(调用方已持锁)
// 难点:
// - 前缀拼接必须与配置项严格对应,且尽量避免多余开销
// - showSource 会输出 (file:line func),对定位时序问题很有价值
std::string SxLogger::formatPrefixUnlocked(
const SxLogConfig& c,
SxLogLevel level,
const char* tag,
const char* file,
int line,
const char* func) const
{
std::ostringstream oss;
if (c.showTimestamp) oss << "[" << makeTimestampLocal() << "] ";
if (c.showLevel) oss << "[" << levelToString(level) << "] ";
if (c.showTag && tag) oss << "[" << tag << "] ";
if (c.showThreadId)
{
oss << "[T:" << std::this_thread::get_id() << "] ";
}
if (c.showSource && file && func)
{
oss << "(" << file << ":" << line << " " << func << ") ";
}
return oss.str();
}
// 统一输出出口
// 难点:
// 1) 行级一致性:必须把 prefix + msg + "\n" 当作整体写入
// 2) 线程安全:持锁写入可避免不同线程日志互相穿插
// 3) 避免重入:在持锁期间不要再调用 SX_LOG...(会导致死锁)
void SxLogger::logLine(
SxLogLevel level,
const char* tag,
const char* file,
int line,
const char* func,
const std::string& msg)
{
std::lock_guard<std::mutex> lock(mtx);
if (cfg.minLevel == SxLogLevel::Off) return;
if (level < cfg.minLevel) return;
if (!tagAllowed(cfg, tag)) return;
const std::string prefix = formatPrefixUnlocked(cfg, level, tag, file, line, func);
const std::string lineText = prefix + msg + "\n";
if (consoleSink) consoleSink->writeLine(lineText);
if (cfg.fileEnabled && fileSink && fileSink->isOpen())
{
fileSink->writeLine(lineText);
}
if (cfg.autoFlush)
{
if (consoleSink) consoleSink->flush();
if (cfg.fileEnabled && fileSink) fileSink->flush();
}
}
// -------- SxLogLine --------
// 构造:只记录元信息
SxLogLine::SxLogLine(SxLogLevel level, const char* tag, const char* file, int line, const char* func)
: lvl(level), tg(tag), srcFile(file), srcLine(line), srcFunc(func)
{
}
// 析构:提交输出
// 难点:
// - 这是 RAII 设计的核心:保证语句结束时日志自动落地
// - 也要求调用端不要把临时对象跨语句保存(宏用法本身也不支持那样做)
SxLogLine::~SxLogLine()
{
SxLogger::Get().logLine(lvl, tg, srcFile, srcLine, srcFunc, ss.str());
}
// -------- SxLogScope --------
// 构造:按需启用计时
// 难点:
// - 只有 shouldLog 为 true 才记录起点,避免在未输出场景做无意义计时
SxLogScope::SxLogScope(SxLogLevel level, const char* tag, const char* file, int line, const char* func, const char* name)
: lvl(level), tg(tag), srcFile(file), srcLine(line), srcFunc(func), scopeName(name)
{
enabled = SxLogger::Get().shouldLog(lvl, tg);
if (enabled) t0 = std::chrono::steady_clock::now();
}
// 析构:输出耗时
// 难点:
// - steady_clock 用于衡量耗时,避免系统时间调整造成跳变
SxLogScope::~SxLogScope()
{
if (!enabled) return;
const auto t1 = std::chrono::steady_clock::now();
const auto us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
std::ostringstream oss;
oss << "SCOPE " << (scopeName ? scopeName : "") << " cost=" << us << "us";
SxLogger::Get().logLine(lvl, tg, srcFile, srcLine, srcFunc, oss.str());
}
} // namespace StellarX
+416
View File
@@ -0,0 +1,416 @@
#pragma once
/********************************************************************************
* @文件: SxLog.h
* @摘要: StellarX 日志系统对外接口定义(控制台/文件输出 + 级别过滤 + Tag过滤 + 中英文选择)
* @描述:
* 该日志系统采用“宏 + RAII(析构提交)”的方式实现:
* - 调用端通过 SX_LOGD/SX_LOGI... 写日志
* - 宏内部先 shouldLog 短路过滤,未命中时不构造对象、不拼接字符串
* - 命中时构造 SxLogLine,使用 operator<< 拼接内容
* - 语句结束时 SxLogLine 析构,统一提交到 SxLogger::logLine 输出
*
* 输出通道(Sink)目前提供:
* - ConsoleSink: 写入 std::cout(不走 WinAPI 调试输出通道)
* - FileSink: 写入文件,支持按字节阈值滚动
*
* @特性:
* - 日志级别:Trace/Debug/Info/Warn/Error/Fatal/Off
* - Tag 过滤:None/Whitelist/Blacklist
* - 可选前缀:时间戳/级别/Tag/线程ID/源码位置
* - 中英文选择:SX_T(zh, en) / setLanguage
* - 文件滚动:rotateBytes > 0 时按阈值滚动
*
* @使用场景:
* - 排查重绘链路、脏标记传播、Tab 切换、Table 数据刷新等时序问题
* - 输出可复现日志,配合回归验证
*
* @注意:
* - SX_T 仅做“字符串选择”,不做编码转换
* - 控制台显示是否乱码由“终端 codepage/字体/环境”决定
* - 该头文件只声明接口,实现位于 SxLog.cpp
*
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
********************************************************************************/
// SxLog.h - header-only interface (implementation in SxLog.cpp)
// Pure standard library: std::cout and optional file sink.
#include <atomic>
#include <chrono>
#include <cstdint>
#include <ctime>
#include <cstdio>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <memory>
#include <mutex>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#ifndef SX_LOG_ENABLE
#define SX_LOG_ENABLE 1
#endif
namespace StellarX
{
/* ========================= 日志级别 ========================= */
// 说明:
// - minLevel 表示最低输出级别,小于 minLevel 的日志会被 shouldLog 直接过滤
// - Off 表示全局关闭
enum class SxLogLevel : int
{
Trace = 0, // 最细粒度:高频路径追踪(谨慎开启)
Debug = 1, // 调试信息:状态变化/关键分支
Info = 2, // 业务信息:关键流程节点
Warn = 3, // 警告:非致命但异常的情况
Error = 4, // 错误:功能失败、需要关注
Fatal = 5, // 致命:通常意味着无法继续运行
Off = 6 // 关闭全部日志
};
/* ========================= 语言选择 ========================= */
// 说明:仅用于 SX_T 选择输出哪一段文本,不做编码转换
enum class SxLogLanguage : int
{
ZhCN = 0, // 中文
EnUS = 1 // 英文
};
/* ========================= Tag 过滤模式 ========================= */
// None : 不过滤,全部输出
// Whitelist : 只输出 tagList 中包含的 tag
// Blacklist : 输出除 tagList 以外的 tag
enum class SxTagFilterMode : int
{
None = 0,
Whitelist = 1,
Blacklist = 2
};
/* ========================= 日志配置 ========================= */
// 说明:SxLogger 内部持有该配置,shouldLog 与 logLine 都依赖它
struct SxLogConfig
{
SxLogLevel minLevel = SxLogLevel::Info; // 最低输出级别
bool showTimestamp = true; // 是否输出时间戳前缀
bool showLevel = true; // 是否输出级别前缀
bool showTag = true; // 是否输出 tag 前缀
bool showThreadId = false; // 是否输出线程ID(排查并发时开启)
bool showSource = false; // 是否输出源码位置(file:line func
bool autoFlush = true; // 每行写完是否 flush(排查问题更稳,性能略差)
SxTagFilterMode tagFilterMode = SxTagFilterMode::None; // Tag 过滤模式
std::vector<std::string> tagList; // Tag 列表(白名单/黑名单)
bool fileEnabled = false; // 文件输出是否启用(enableFile 成功才为 true
std::string filePath; // 文件路径
bool fileAppend = true; // 是否追加写入
std::size_t rotateBytes = 0; // 滚动阈值(0 表示不滚动)
};
/* ========================= Sink 接口 ========================= */
// 说明:
// - Sink 负责“把完整的一行日志写到某个地方”
// - SxLogger 负责过滤/格式化/分发
class ILogSink
{
public:
virtual ~ILogSink() = default;
// 返回 Sink 名称,用于调试识别(例如 "console"/"file"
virtual const char* name() const = 0;
// 写入一整行(调用方保证 line 已包含换行或按约定追加换行)
virtual void writeLine(const std::string& line) = 0;
// 刷新缓冲(可选实现)
virtual void flush() {}
};
/* ========================= 控制台输出 Sink ========================= */
// 作用:把日志写入指定输出流(默认用 std::cout)
class ConsoleSink : public ILogSink
{
public:
// 绑定一个输出流引用(常见用法:std::cout)
explicit ConsoleSink(std::ostream& os) : out(os) {}
const char* name() const override { return "console"; }
// 写入一行(不自动追加换行,换行由上层统一拼接)
void writeLine(const std::string& line) override { out << line; }
// 立即 flush(当 autoFlush=true 时由 SxLogger 调用)
void flush() override { out.flush(); }
private:
std::ostream& out; // 输出流引用(不负责生命周期)
};
/* ========================= 文件输出 Sink ========================= */
// 作用:把日志写入文件,支持按字节阈值滚动
class FileSink : public ILogSink
{
public:
FileSink() = default;
const char* name() const override { return "file"; }
// 打开文件
// path : 文件路径
// append : true 追加写;false 清空重写
bool open(const std::string& path, bool append);
// 关闭文件(安全可重复调用)
void close();
// 查询文件是否处于打开状态
bool isOpen() const;
// 设置滚动阈值(字节)
// bytes = 0 表示不滚动
void setRotateBytes(std::size_t bytes) { rotateBytes = bytes; }
// 写入一行,并在需要时触发滚动
void writeLine(const std::string& line) override;
// flush 文件缓冲
void flush() override;
private:
// 检查并执行滚动
// 返回值:是否发生滚动(或是否重新打开)
bool rotateIfNeeded();
std::ofstream ofs; // 文件输出流
std::string filePath; // 当前文件路径
bool appendMode = true; // 是否追加模式(用于 reopen)
std::size_t rotateBytes = 0; // 滚动阈值
};
/* ========================= 日志中心 SxLogger ========================= */
// 作用:
// - 保存配置(SxLogConfig
// - 过滤(level/tag/sink enabled
// - 格式化前缀(时间/级别/tag/线程/源码位置)
// - 分发到 console/file 等 sink
class SxLogger
{
public:
// 仅用于 Windows 控制台:把 codepage 切到 GBK,解决中文乱码。
// 不使用 WinAPI:内部通过 system("chcp 936") 实现
// 注意:这只影响终端解释输出字节的方式,不影响源码文件编码
static void setGBK();
// 获取全局单例
// 说明:函数内静态对象,C++11 起保证线程安全初始化
static SxLogger& Get();
// 设置最低输出级别
void setMinLevel(SxLogLevel level);
// 获取最低输出级别
SxLogLevel getMinLevel() const;
// 设置语言(用于 SX_T 选择)
void setLanguage(SxLogLanguage lang);
// 获取当前语言
SxLogLanguage getLanguage() const;
// 设置 Tag 过滤
// mode: None/Whitelist/Blacklist
// tags: 过滤列表(精确匹配)
void setTagFilter(SxTagFilterMode mode, const std::vector<std::string>& tags);
// 清空 Tag 过滤(恢复 None
void clearTagFilter();
// 开关控制台输出
void enableConsole(bool enable);
// 开启文件输出
// path : 文件路径
// append : 追加写/清空写
// rotateBytes: 滚动阈值(0 不滚动)
// 返回值:是否打开成功
bool enableFile(const std::string& path, bool append = true, std::size_t rotateBytes = 0);
// 关闭文件输出(不影响控制台输出)
void disableFile();
// 快速判定是否需要输出(宏层面的短路依赖它)
// 说明:
// - shouldLog 一定要“副作用为 0”
// - 若返回 false,调用端不会创建 SxLogLine,也不会拼接字符串
bool shouldLog(SxLogLevel level, const char* tag) const;
// 输出一条完整日志
// 说明:这是统一出口,SxLogLine 析构最终会走到这里
void logLine(
SxLogLevel level,
const char* tag,
const char* file,
int line,
const char* func,
const std::string& msg);
// 获取配置副本(避免外部直接改内部 cfg)
SxLogConfig getConfigCopy() const;
// 批量设置配置(整体替换)
void setConfig(const SxLogConfig& cfg);
// 工具:把级别转为字符串(用于前缀)
static const char* levelToString(SxLogLevel level);
// 工具:生成本地时间戳字符串(用于前缀与文件滚动名)
static std::string makeTimestampLocal();
private:
SxLogger();
// 判断 tag 是否允许输出(根据 Tag 过滤模式与 tagList
static bool tagAllowed(const SxLogConfig& cfg, const char* tag);
// 生成前缀(调用方需已持有锁)
std::string formatPrefixUnlocked(
const SxLogConfig& cfg,
SxLogLevel level,
const char* tag,
const char* file,
int line,
const char* func) const;
mutable std::mutex mtx; // 保护 cfg 与 sink 写入,确保多线程行级一致性
SxLogConfig cfg; // 当前配置
std::atomic<SxLogLanguage> lang; // 语言开关(仅影响 SX_T 选择)
std::unique_ptr<ConsoleSink> consoleSink; // 控制台 sinkenableConsole 控制)
std::unique_ptr<FileSink> fileSink; // 文件 sinkenableFile 控制)
};
/* ========================= 双语选择辅助 ========================= */
// 说明:
// - 只做“选择 zhCN 或 enUS”,不做编码转换
// - 输出显示是否正常由终端环境决定
inline const char* SxT(const char* zhCN, const char* enUS)
{
return (SxLogger::Get().getLanguage() == SxLogLanguage::ZhCN) ? zhCN : enUS;
}
#if defined(__cpp_char8_t) && (__cpp_char8_t >= 201811L)
// 说明:
// - C++20 的 u8"xxx" 是 char8_t*,为了兼容调用端,这里提供重载
// - reinterpret_cast 只是改指针类型,不做 UTF-8 -> GBK 转码
inline const char* SxT(const char8_t* zhCN, const char* enUS)
{
return (SxLogger::Get().getLanguage() == SxLogLanguage::ZhCN)
? reinterpret_cast<const char*>(zhCN)
: enUS;
}
#endif
/* ========================= RAII 日志行对象 ========================= */
// 作用:
// - 构造时记录 level/tag/源码位置
// - operator<< 拼接内容
// - 析构时统一提交给 SxLogger::logLine 输出
//
// 设计意义:
// - 避免调用端忘记写换行
// - 保证一行日志作为整体写出
class SxLogLine
{
public:
// 构造:记录元信息(不输出)
SxLogLine(SxLogLevel level, const char* tag, const char* file, int line, const char* func);
// 析构:提交输出(真正写出发生在这里)
~SxLogLine();
// 拼接内容(流式写法)
template<typename T>
SxLogLine& operator<<(const T& v)
{
ss << v;
return *this;
}
private:
SxLogLevel lvl; // 日志级别
const char* tg; // Tag(不拥有内存)
const char* srcFile; // 源文件名(来自 __FILE__
int srcLine; // 行号(来自 __LINE__
const char* srcFunc; // 函数名(来自 __func__
std::ostringstream ss; // 内容拼接缓冲
};
/* ========================= RAII 作用域计时对象 ========================= */
// 作用:
// - 仅在 shouldLog(Trace, tag) 为 true 时启用计时
// - 析构时输出耗时(微秒)
//
// 使用建议:
// - 只在需要定位性能瓶颈时开启 Trace
// - name 建议传入常量字符串,便于检索
class SxLogScope
{
public:
// 构造:根据 shouldLog 决定是否启用计时
SxLogScope(SxLogLevel level, const char* tag, const char* file, int line, const char* func, const char* name);
// 析构:若启用则输出耗时
~SxLogScope();
private:
bool enabled = false; // 是否启用(未启用则析构无输出)
SxLogLevel lvl = SxLogLevel::Trace; // 级别(通常用 Trace
const char* tg = nullptr; // Tag
const char* srcFile = nullptr; // 源文件
int srcLine = 0; // 行号
const char* srcFunc = nullptr; // 函数
const char* scopeName = nullptr; // 作用域名
std::chrono::steady_clock::time_point t0; // 起始时间点
};
} // namespace StellarX
#if SX_LOG_ENABLE
// SX_T:双语选择宏,调用 SxT 根据当前语言选择输出
#define SX_T(zh, en) ::StellarX::SxT(zh, en)
// 日志宏说明:
// 1) 先 shouldLog 短路过滤,未命中则不会构造 SxLogLine,也不会执行 else 分支的表达式
// 2) 命中则构造临时 SxLogLine,并允许继续使用 operator<< 拼接
// 3) 语句结束时临时对象析构,触发真正输出
#define SX_LOG_TRACE(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Trace, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Trace, tag, __FILE__, __LINE__, __func__)
#define SX_LOGD(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Debug, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Debug, tag, __FILE__, __LINE__, __func__)
#define SX_LOGI(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Info, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Info, tag, __FILE__, __LINE__, __func__)
#define SX_LOGW(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Warn, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Warn, tag, __FILE__, __LINE__, __func__)
#define SX_LOGE(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Error, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Error, tag, __FILE__, __LINE__, __func__)
#define SX_LOGF(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Fatal, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Fatal, tag, __FILE__, __LINE__, __func__)
// 作用域耗时统计宏:默认用 Trace 级别
#define SX_TRACE_SCOPE(tag, nameLiteral) ::StellarX::SxLogScope sx_scope_##__LINE__(::StellarX::SxLogLevel::Trace, tag, __FILE__, __LINE__, __func__, nameLiteral)
#else
// 关闭日志时的兼容宏:保证调用端代码不需要改动
#define SX_T(zh, en) (en)
#define SX_LOG_TRACE(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGD(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGI(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGW(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGE(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_LOGF(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
#define SX_TRACE_SCOPE(tag, nameLiteral) do {} while(0)
#endif
+431
View File
@@ -0,0 +1,431 @@
#include "TabControl.h"
#include "SxLog.h"
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;
//子控件坐标原点
int nX = 0;
int nY = 0;
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);
}
nX = this->x;
nY = this->y + tabBarHeight;
for (auto& c : controls)
{
for (auto& v : c.second->getControls())
{
v->setX(v->getLocalX() + nX);
v->setY(v->getLocalY() + nY);
}
}
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);
}
nX = this->x;
nY = this->y;
for (auto& c : controls)
{
for (auto& v : c.second->getControls())
{
v->setX(v->getLocalX() + nX);
v->setY(v->getLocalY() + nY);
}
}
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);
}
nX = this->x + tabBarHeight;
nY = this->y;
for (auto& c : controls)
{
for (auto& v : c.second->getControls())
{
v->setX(v->getLocalX() + nX);
v->setY(v->getLocalY() + nY);
}
}
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);
}
nX = this->x;
nY = this->y;
for (auto& c : controls)
{
for (auto& v : c.second->getControls())
{
v->setX(v->getLocalX() + nX);
v->setY(v->getLocalY() + nY);
}
}
break;
default:
break;
}
}
TabControl::TabControl() :Canvas()
{
this->id = "TabControl";
}
TabControl::TabControl(int x, int y, int width, int height)
: Canvas(x, y, width, height)
{
this->id = "TabControl";
}
TabControl::~TabControl()
{
}
void TabControl::setX(int x)
{
this->x = x;
initTabBar();
initTabPage();
dirty = true;
for (auto& c : controls)
{
c.first->onWindowResize();
c.second->onWindowResize();
}
}
void TabControl::setY(int y)
{
this->y = y;
initTabBar();
initTabPage();
dirty = true;
for (auto& c : controls)
{
c.first->onWindowResize();
c.second->onWindowResize();
}
}
void TabControl::draw()
{
if (!dirty || !show)return;
// 绘制画布背景和基本形状及其子画布控件
Canvas::draw();
for (auto& c : controls)
{
c.first->setDirty(true);
c.first->draw();
}
for (auto& c : controls)
{
c.second->setDirty(true);
c.second->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;
bool consume = false;
for (auto& c : controls)
if (c.first->handleEvent(msg))
{
consume = true;
break;
}
if (!consume)
{
for (auto& c : controls)
if (c.second->IsVisible())
if (c.second->handleEvent(msg))
{
consume = true;
break;
}
}
if (dirty)
requestRepaint(parent);
return consume;
}
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()
{
// 调用基类的窗口变化处理,丢弃快照并标记脏
Control::onWindowResize();
// 根据当前 TabControl 的新尺寸重新计算页签栏和页面区域
initTabBar();
initTabPage();
// 转发窗口尺寸变化给所有页签按钮和页面
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
{
if (idx >= 0 && idx < controls.size())
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 idx;
}
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 (this == parent)
{
for (auto& control : controls)
{
if (control.first->isDirty() && control.first->IsVisible())
control.first->draw();
if (control.second->isDirty() && control.second->IsVisible())
control.second->draw();
}
}
else
onRequestRepaintAsRoot();
}
+76
View File
@@ -0,0 +1,76 @@
/*******************************************************************************
* @类: TabControl
* @摘要: 选项卡容器控件,管理“页签按钮 + 对应页面(Canvas)”
* @描述:
* 提供页签栏布局(上/下/左/右)、选中切换、页内容区域定位;
* 与 Button 一起工作,支持窗口大小变化、可见性联动与脏区重绘。
*
* @特性:
* - 页签栏四向排列(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();
public:
TabControl();
TabControl(int x, int y, int width, int height);
~TabControl();
//重写位置设置以适应页签和页面布局
void setX(int x)override;
void setY(int y)override;
void draw() override;
bool handleEvent(const ExMessage& msg) override;
//添加页签+页
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;
};
+674
View File
@@ -0,0 +1,674 @@
#include "Table.h"
#include "SxLog.h"
// 绘制表格的当前页
// 使用双循环绘制行和列,考虑分页偏移
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);
// 记录原始宽高用于锚点布局的参考;此处仅在初始化单元尺寸时重置
this->localWidth = this->width;
this->localHeight = 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->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->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);
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::Table(int x, int y)
:Control(x, y, 0, 0)
{
this->id = "Table";
}
Table::~Table() = default;
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)
{
discardBackground();
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;
bool consume = false;
if (!this->isShowPageButton)
return consume;
else
{
if (prevButton)consume = prevButton->handleEvent(msg);
if (nextButton && !consume)
consume = nextButton->handleEvent(msg);
}
if (dirty)
requestRepaint(parent);
return consume;
}
void Table::setHeaders(std::initializer_list<std::string> headers)
{
this->headers.clear();
for (auto& lis : headers)
this->headers.push_back(lis);
SX_LOGI("Table") << SX_T("设置表头:id=","setHeaders: id=") << id << SX_T("总数="," count=") << (int)this->headers.size();
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
isNeedDrawHeaders = true; // 标记需要重新绘制表头
dirty = true;
}
void Table::setData(std::vector<std::string> data)
{
while (data.size() < headers.size())
data.push_back("");
this->data.push_back(data);
totalPages = ((int)this->data.size() + rowsPerPage - 1) / rowsPerPage;
if (totalPages < 1)
totalPages = 1;
isNeedCellSize = true;
dirty = true;
SX_LOGI("Table")
<< SX_T("新增Dataid=", "appendRow: id=") << id
<< SX_T(" 本行列数=", " cols=") << (int)data.size()
<< SX_T(" 数据总行数=", " totalRows=") << (int)this->data.size()
<< SX_T(" 总页数=", " totalPages=") << totalPages;
}
void Table::setData(std::initializer_list<std::vector<std::string>> data)
{
for (auto lis : data)
if (lis.size() < headers.size())
{
for (size_t i = lis.size(); i < headers.size(); i++)
lis.push_back("");
this->data.push_back(lis);
}
else
this->data.push_back(lis);
totalPages = ((int)this->data.size() + rowsPerPage - 1) / rowsPerPage;
if (totalPages < 1)
totalPages = 1;
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
dirty = true;
SX_LOGI("Table")
<< SX_T("新增Dataid=", "appendRow: id=") << id
<< SX_T(" 本行列数=", " cols=") << (int)data.size()
<< SX_T(" 数据总行数=", " totalRows=") << (int)this->data.size()
<< SX_T(" 总页数=", " totalPages=") << totalPages;
}
void Table::setRowsPerPage(int rows)
{
this->rowsPerPage = rows;
if (this->rowsPerPage < 1)
this->rowsPerPage = 1;
totalPages = ((int)data.size() + rowsPerPage - 1) / rowsPerPage;
if (totalPages < 1)
totalPages = 1;
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
dirty = true;
}
void Table::showPageButton(bool isShow)
{
this->isShowPageButton = isShow;
this->dirty = true;
}
void Table::setTableBorder(COLORREF color)
{
this->tableBorderClor = color;
this->dirty = true;
}
void Table::setTableBk(COLORREF color)
{
this->tableBkClor = color;
this->dirty = true;
}
void Table::setTableFillMode(StellarX::FillMode mode)
{
if (StellarX::FillMode::Solid == mode || StellarX::FillMode::Null == mode)
this->tableFillMode = mode;
else
this->tableFillMode = StellarX::FillMode::Solid;
if (this->prevButton && this->nextButton && this->pageNum)
{
this->prevButton->textStyle = this->textStyle;
this->nextButton->textStyle = this->textStyle;
this->prevButton->setFillMode(tableFillMode);
this->nextButton->setFillMode(tableFillMode);
if (StellarX::FillMode::Null == tableFillMode)
pageNum->setTextdisap(true);
this->prevButton->setDirty(true);
this->nextButton->setDirty(true);
}
this->dirty = true;
}
void Table::setTableLineStyle(StellarX::LineStyle style)
{
this->tableLineStyle = style;
this->dirty = true;
}
void Table::setTableBorderWidth(int width)
{
this->tableBorderWidth = width;
this->dirty = true;
}
void Table::clearHeaders()
{
this->headers.clear();
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
isNeedDrawHeaders = true; // 标记需要重新绘制表头
isNeedButtonAndPageNum = true;// 标记需要重新计算翻页按钮和页码信息
dirty = true;
SX_LOGI("Table") << SX_T("清除表头:id=","clearHeaders: id=" )<< id;
}
void Table::clearData()
{
this->data.clear();
this->currentPage = 1;
this->totalPages = 1;
isNeedCellSize = true; // 标记需要重新计算单元格尺寸
isNeedButtonAndPageNum = true;// 标记需要重新计算翻页按钮和页码信息
dirty = true;
SX_LOGI("Table") << SX_T("清除表格数据:id=","clearData: id=") << id;
}
void Table::resetTable()
{
clearHeaders();
clearData();
}
void Table::onWindowResize()
{
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;
}
+169
View File
@@ -0,0 +1,169 @@
/*******************************************************************************
* @类: 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 // 按钮垂直 paddinginitButton
#define TABLE_BTN_TEXT_PAD_V 8 // 计算页脚高度时的按钮文字垂直 paddinginitTextWaH
#define TABLE_FOOTER_PAD 16 // 页脚额外高度(底部留白)
#define TABLE_FOOTER_BLANK 8 // 页脚顶部留白
#define TABLE_PAGE_TEXT_OFFSET_X (-40) // 页码文本的临时水平修正
// === Table defaults (theme) ===
#define TABLE_DEFAULT_ROWS_PER_PAGE 5
#define TABLE_DEFAULT_BORDER_WIDTH 1
#define TABLE_DEFAULT_BORDER_COLOR RGB(0,0,0)
#define TABLE_DEFAULT_BG_COLOR RGB(255,255,255)
// === Strings (i18n ready) ===
#define TABLE_STR_PREV "上一页"
#define TABLE_STR_NEXT "下一页"
#define TABLE_STR_PAGE_PREFIX "第"
#define TABLE_STR_PAGE_MID "页/共"
#define TABLE_STR_PAGE_SUFFIX "页"
class Table :public Control
{
private:
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(); //绘制翻页按钮
private:
//用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; };
public:
StellarX::ControlText textStyle; // 文本样式
void setX(int x) override;
void setY(int y) override;
void setWidth(int width) override;
void setHeight(int height) override;
public:
Table(int x, int y);
~Table();
// 绘制表格
void draw() override;
//事件处理
bool handleEvent(const ExMessage& msg) override;
//设置表头
void setHeaders(std::initializer_list<std::string> headers);
//设置表格数据
void setData(std::vector<std::string> data);
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;
};
+247
View File
@@ -0,0 +1,247 @@
// TextBox.cpp
#include "TextBox.h"
#include "SxLog.h"
TextBox::TextBox(int x, int y, int width, int height, std::string text, StellarX::TextBoxmode mode, StellarX::ControlShape shape)
:Control(x, y, width, height), text(text), mode(mode), shape(shape)
{
this->id = "TextBox";
}
void TextBox::draw()
{
if (dirty && show)
{
saveStyle();
setfillcolor(textBoxBkClor);
setlinecolor(textBoxBorderClor);
if (textStyle.nHeight > height)
textStyle.nHeight = height;
if (textStyle.nWidth > width)
textStyle.nWidth = width;
settextstyle(textStyle.nHeight, textStyle.nWidth, textStyle.lpszFace,
textStyle.nEscapement, textStyle.nOrientation, textStyle.nWeight,
textStyle.bItalic, textStyle.bUnderline, textStyle.bStrikeOut);
settextcolor(textStyle.color);
setbkmode(TRANSPARENT);
int text_width = 0;
int text_height = 0;
std::string pwdText;
std::string displayText; // 用于显示的文本(可能被截断)
bool isTextTruncated = false; // 标记文本是否被截断
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)
{
// 需要截断文本,预留空间放置省略号
int ellipsisWidth = textwidth("...");
int truncatedWidth = availableWidth - ellipsisWidth;
std::string truncatedText = displayText;
while (truncatedText.size() > 0 && textwidth(LPCTSTR(truncatedText.c_str())) > truncatedWidth)
{
truncatedText.pop_back();
}
displayText = truncatedText + "...";
isTextTruncated = true;
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;
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);
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 = text.substr(0, 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;
}
+56
View File
@@ -0,0 +1,56 @@
/*******************************************************************************
* @类: TextBox
* @摘要: 文本框控件,支持输入和只读两种模式
* @描述:
* 提供文本输入和显示功能,集成EasyX的InputBox用于数据输入。
* 支持有限的形状样式和视觉定制。
*
* @特性:
* - 两种工作模式:输入模式和只读模式
* - 最大字符长度限制
* - 集成系统输入框简化文本输入
* - 支持四种矩形形状变体
*
* @使用场景: 数据输入、文本显示、表单字段等
* @所属框架: 星垣(StellarX) GUI框架
* @作者: 我在人间做废物
******************************************************************************/
#pragma once
#include "Control.h"
class TextBox : public Control
{
std::string text; //文本
StellarX::TextBoxmode mode; //模式
StellarX::ControlShape shape; //形状
bool click = false; //是否点击
size_t maxCharLen = 10;//最大字符长度
COLORREF textBoxBkClor = RGB(255, 255, 255); //背景颜色
COLORREF textBoxBorderClor = RGB(0, 0, 0); //边框颜色
public:
StellarX::ControlText textStyle; //文本样式
TextBox(int x, int y, int width, int height, std::string text = "", StellarX::TextBoxmode mode = StellarX::TextBoxmode::INPUT_MODE, StellarX::ControlShape shape = StellarX::ControlShape::RECTANGLE);
void draw() override;
bool handleEvent(const ExMessage& msg) override;
//设置模式
void setMode(StellarX::TextBoxmode mode);
//设置可输入最大字符长度
void setMaxCharLen(size_t len);
//设置形状
void setTextBoxshape(StellarX::ControlShape shape);
//设置边框颜色
void setTextBoxBorder(COLORREF color);
//设置背景颜色
void setTextBoxBk(COLORREF color);
//设置文本
void setText(std::string text);
//获取文本
std::string getText() const;
private:
//用来检查对话框是否模态,此控件不做实现
bool model() const override { return false; };
};
+888
View File
@@ -0,0 +1,888 @@
#include "Window.h"
#include "Dialog.h"
#include"SxLog.h"
#include <easyx.h>
#include <algorithm>
static bool SxIsNoisyMsg(UINT m)
{
return m == WM_MOUSEMOVE;
}
static const char* SxMsgName(UINT m)
{
switch (m)
{
case WM_MOUSEMOVE: return "WM_MOUSEMOVE";
case WM_LBUTTONDOWN: return "WM_LBUTTONDOWN";
case WM_LBUTTONUP: return "WM_LBUTTONUP";
case WM_RBUTTONDOWN: return "WM_RBUTTONDOWN";
case WM_RBUTTONUP: return "WM_RBUTTONUP";
case WM_KEYDOWN: return "WM_KEYDOWN";
case WM_KEYUP: return "WM_KEYUP";
case WM_CHAR: return "WM_CHAR";
case WM_SIZE: return "WM_SIZE";
default: return "WM_?";
}
}
/**
* ApplyResizableStyle
* 作用:统一设置可拉伸/裁剪样式,并按开关使用 WS_EX_COMPOSITED(合成双缓冲)。
* 关键点:
* - WS_THICKFRAME:允许从四边/四角拖动改变尺寸。
* - WS_CLIPCHILDREN / WS_CLIPSIBLINGS:避免子控件互相覆盖时闪烁。
* - WS_EX_COMPOSITED:在一些环境更平滑,但个别显卡/驱动可能带来一帧延迟感。
* - SWP_FRAMECHANGED:通知窗口样式已变更,强制系统重算非客户区(标题栏/边框)。
*/
static void ApplyResizableStyle(HWND h, bool useComposited)
{
LONG style = GetWindowLong(h, GWL_STYLE);
style |= WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_CLIPCHILDREN | WS_CLIPSIBLINGS;
SetWindowLong(h, GWL_STYLE, style);
LONG ex = GetWindowLong(h, GWL_EXSTYLE);
if (useComposited)
{
ex |= WS_EX_COMPOSITED;
}
else
{
ex &= ~WS_EX_COMPOSITED;
}
SetWindowLong(h, GWL_EXSTYLE, ex);
SetWindowPos(h, NULL, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
}
/**
* ApplyMinSizeOnSizing
* 作用:在 WM_SIZING 阶段执行“最小尺寸夹紧”。
* 规则:只回推“被拖动的那一侧”,另一侧当锚点(避免几何回弹/位置漂移)。
* 步骤:
* 1)将“最小客户区尺寸”通过 AdjustWindowRectEx 换算为“最小窗口矩形”(含非客户区)。
* 2)若当前矩形比最小还小,则根据 edge(哪条边/角在被拖)调整对应边,另一侧保持不动。
* 说明:仅保证不小于最小值;不做对齐/回滚等操作,把其余交给系统尺寸逻辑。
*/
static void ApplyMinSizeOnSizing(RECT* prc, WPARAM edge, HWND hWnd, int minClientW, int minClientH)
{
RECT rcFrame{ 0, 0, minClientW, minClientH };
DWORD style = GetWindowLong(hWnd, GWL_STYLE);
DWORD ex = GetWindowLong(hWnd, GWL_EXSTYLE);
AdjustWindowRectEx(&rcFrame, style, FALSE, ex);
const int minW = rcFrame.right - rcFrame.left;
const int minH = rcFrame.bottom - rcFrame.top;
const int curW = prc->right - prc->left;
const int curH = prc->bottom - prc->top;
if (curW < minW)
{
switch (edge)
{
case WMSZ_LEFT:
case WMSZ_TOPLEFT:
case WMSZ_BOTTOMLEFT:
prc->left = prc->right - minW; // 锚定右侧,回推左侧(左边被拖)
break;
default:
prc->right = prc->left + minW; // 锚定左侧,回推右侧(右边被拖)
break;
}
}
if (curH < minH)
{
switch (edge)
{
case WMSZ_TOP:
case WMSZ_TOPLEFT:
case WMSZ_TOPRIGHT:
prc->top = prc->bottom - minH; // 锚定下侧,回推上侧(上边被拖)
break;
default:
prc->bottom = prc->top + minH; // 锚定上侧,回推下侧(下边被拖)
break;
}
}
}
// ---------------- 构造 / 析构 ----------------
/**
* 构造:初始化当前尺寸、待应用尺寸、最小客户区尺寸与 EasyX 模式。
* 注意:样式设置与子类化放在 draw() 内第一次绘制时完成。
*/
Window::Window(int w, int h, int mode)
{
localwidth = minClientW = pendingW = width = w;
localheight = minClientH = pendingH = height = h;
windowMode = mode;
}
Window::Window(int w, int h, int mode, COLORREF bk)
{
localwidth = minClientW = pendingW = width = w;
localheight = minClientH = pendingH = height = h;
windowMode = mode;
wBkcolor = bk;
}
Window::Window(int w, int h, int mode, COLORREF bk, std::string title)
{
localwidth = minClientW = pendingW = width = w;
localheight = minClientH = pendingH = height = h;
windowMode = mode;
wBkcolor = bk;
headline = std::move(title);
}
Window::~Window()
{
// 先销毁控件树,再关闭图形环境,避免控件析构时访问已关闭的 EasyX 上下文。
dialogs.clear();
controls.clear();
background.reset();
if (hWnd && procHooked && oldWndProc)
{
SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldWndProc);
SetWindowLongPtr(hWnd, GWLP_USERDATA, 0);
}
if (hWnd)
closegraph();
}
// ---------------- 原生消息钩子----------------
/**
* WndProcThun
* 作用:替换 EasyX 的窗口过程,接管关键消息。
* 关键处理:
* - WM_ERASEBKGND:返回 1,交由自绘清屏,避免系统擦背景造成闪烁。
* - WM_ENTERSIZEMOVE:开始拉伸 → isSizing=true 且 WM_SETREDRAW(FALSE) 冻结重绘。
* - WM_SIZING:拉伸中 → 仅做“最小尺寸夹紧”(按被拖边回推),不回滚、不绘制。
* - WM_EXITSIZEMOVE:结束拉伸 → 读取最终客户区尺寸 → 标记 needResizeDirty,解冻并刷新。
* - WM_GETMINMAXINFO:提供系统最小轨迹限制(四边一致)。
*/
LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l)
{
auto* self = reinterpret_cast<Window*>(GetWindowLongPtr(h, GWLP_USERDATA));
if (!self)
{
return DefWindowProc(h, m, w, l);
}
// 关键点①:禁止系统擦背景,避免和我们自己的清屏/双缓冲打架造成闪烁
if (m == WM_ERASEBKGND)
{
return 1;
}
// 关键点②:拉伸开始 → 冻结重绘(系统调整窗口矩形时不触发即时重绘,防止抖)
if (m == WM_ENTERSIZEMOVE)
{
SX_LOGI("Resize") << SX_T("WM_ENTERSIZEMOVE: 开始测量尺寸","WM_ENTERSIZEMOVE: begin sizing");
self->isSizing = true;
SendMessage(h, WM_SETREDRAW, FALSE, 0);
return 0;
}
// 关键点③:拉伸中 → 仅执行“最小尺寸夹紧”,不做对齐/节流/回滚,保持系统自然流畅
if (m == WM_SIZING)
{
RECT* prc = reinterpret_cast<RECT*>(l);
// “尺寸异常值”快速过滤:仅保护极端值,不影响正常拖动
int currentWidth = prc->right - prc->left;
int currentHeight = prc->bottom - prc->top;
if (currentWidth < 0 || currentHeight < 0 || currentWidth > 10000 || currentHeight > 10000)
{
return TRUE;
}
RECT before = *prc;// 记录调整前矩形以便日志输出
ApplyMinSizeOnSizing(prc, w, h, self->minClientW, self->minClientH);
//if (before.left != prc->left || before.top != prc->top || before.right != prc->right || before.bottom != prc->bottom)
if (memcmp(&before, prc, sizeof(RECT)) != 0)
{
SX_LOGD("Resize")
<< SX_T("WM_SIZING 夹具:","WM_SIZING clamp: ")
<< SX_T("之前=(","before=(") << (before.right - before.left) << "x" << (before.bottom - before.top) << ") "
<< SX_T("之后=","after=(") << (prc->right - prc->left) << "x" << (prc->bottom - prc->top) << ")";
}
return TRUE;
}
// 关键点④:拉伸结束 → 解冻重绘 + 统一收口(记录最终尺寸 -> 标记 needResizeDirty
if (m == WM_EXITSIZEMOVE)
{
self->isSizing = false;
RECT rc; GetClientRect(h, &rc);
const int aw = rc.right - rc.left;
const int ah = rc.bottom - rc.top;
if (aw >= self->minClientW && ah >= self->minClientH && aw <= 10000 && ah <= 10000)
{
self->pendingW = aw;
self->pendingH = ah;
self->needResizeDirty = true;
SX_LOGI("Resize") << SX_T("WM_EXITSIZEMOVE: 最终尺寸,待重绘=(","WM_EXITSIZEMOVE: end sizing, pending=(" )<< self->pendingW << "x" << self->pendingH << "), needResizeDirty=1";
}
// 结束拉伸后不立即执行重绘,待事件循环统一收口。
// 立即解冻重绘标志,同时标记区域为有效,避免触发额外 WM_PAINT。
SendMessage(h, WM_SETREDRAW, TRUE, 0);
ValidateRect(h, nullptr);
return 0;
}
// 关键点⑤:系统级最小轨迹限制(与 WM_SIZING 的夹紧互相配合)
if (m == WM_GETMINMAXINFO)
{
auto* mmi = reinterpret_cast<MINMAXINFO*>(l);
RECT rc{ 0, 0, self->minClientW, self->minClientH };
DWORD style = GetWindowLong(h, GWL_STYLE);
DWORD ex = GetWindowLong(h, GWL_EXSTYLE);
// 若后续添加菜单,请把第三个参数改为 HasMenu(h)
AdjustWindowRectEx(&rc, style, FALSE, ex);
mmi->ptMinTrackSize.x = rc.right - rc.left;
mmi->ptMinTrackSize.y = rc.bottom - rc.top;
return 0;
}
// 其它消息:回落到旧过程
return self->oldWndProc ? CallWindowProc(self->oldWndProc, h, m, w, l)
: DefWindowProc(h, m, w, l);
}
// ---------------- 绘制 ----------------
/**
* draw()
* 作用:首次初始化 EasyX 窗口与子类化过程;应用可拉伸样式;清屏并批量绘制。
* 关键步骤:
* 1initgraph 拿到 hWnd
* 2SetWindowLongPtr 子类化到 WndProcThunk(只做一次);
* 3ApplyResizableStyle 设置 WS_THICKFRAME/裁剪/(可选)合成双缓冲;
* 4)去掉类样式 CS_HREDRAW/CS_VREDRAW,避免全窗无效化引发闪屏;
* 5)清屏 + Begin/EndBatchDraw 批量绘制控件&对话框。
*/
void Window::draw()
{
if (!hWnd)
{
hWnd = initgraph(width, height, windowMode);
}
// 子类化:让我们的 WndProcThunk 接管窗口消息(仅执行一次)
if (!procHooked)
{
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)this);
oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)&Window::WndProcThunk);
procHooked = (oldWndProc != nullptr);
}
if (!headline.empty())
{
SetWindowText(hWnd, headline.c_str());
}
ApplyResizableStyle(hWnd, useComposited);
// 关闭类样式的整窗重绘标志(减少尺寸变化时的整窗 redraw)
LONG_PTR cls = GetClassLongPtr(hWnd, GCL_STYLE);
cls &= ~(CS_HREDRAW | CS_VREDRAW);
SetClassLongPtr(hWnd, GCL_STYLE, cls);
setbkcolor(wBkcolor);
cleardevice();
BeginBatchDraw();
for (auto& c : controls)
{
c->draw();
}
for (auto& d : dialogs)
{
d->draw();
}
EndBatchDraw();
}
/**
* draw(imagePath)
* 作用:在 draw() 的基础上加载并绘制背景图;其它流程完全一致。
* 注意:这里按当前窗口客户区大小加载背景图(loadimage 的 w/h),保证铺满。
*/
void Window::draw(std::string imagePath)
{
if (!hWnd)
{
hWnd = initgraph(width, height, windowMode);
}
if (!procHooked)
{
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)this);
oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)&Window::WndProcThunk);
procHooked = (oldWndProc != nullptr);
}
bkImageFile = std::move(imagePath);
if (!headline.empty())
{
SetWindowText(hWnd, headline.c_str());
}
ApplyResizableStyle(hWnd, useComposited);
LONG_PTR cls = GetClassLongPtr(hWnd, GCL_STYLE);
cls &= ~(CS_HREDRAW | CS_VREDRAW);
SetClassLongPtr(hWnd, GCL_STYLE, cls);
if (background)
{
background.reset();
}
background = std::make_unique<IMAGE>();
loadimage(background.get(), bkImageFile.c_str(), width, height, true);
putimage(0, 0, background.get());
BeginBatchDraw();
for (auto& c : controls)
{
c->setDirty(true);
c->draw();
}
for (auto& d : dialogs)
{
d->draw();
}
EndBatchDraw();
}
// ---------------- 事件循环 ----------------
/**
* runEventLoop()
* 作用:驱动输入/窗口消息;集中处理“统一收口重绘”。
* 关键策略:
* - WM_SIZE:始终更新 pendingW/H(即使在拉伸中也只记录不立即绘制);
* - needResizeDirty:当尺寸确实变化时置位,随后在循环尾进行一次性重绘;
* - 非模态对话框优先消费事件(顶层从后往前);再交给普通控件。
*/
int Window::runEventLoop()
{
ExMessage msg;
bool running = true;
// 说明:统一使用 needResizeDirty 作为“收口重绘”的唯一标志位
// 不再引入额外 pendingResize 等状态,避免分叉导致状态不一致。
while (running)
{
bool consume = false; // 事件是否被消费的标志(用于输入事件分发)
bool redrawDialogs = false; // 是否需要重绘对话框(控件事件可能引起对话框状态变化)
if (peekmessage(&msg, EX_MOUSE | EX_KEY | EX_WINDOW, true))
{
if (msg.message == WM_CLOSE)
{
running = false;
return 0;
}
// 保险:如果 EX_WINDOW 转译了 GETMINMAXINFO,同样按最小客户区折算处理
if (msg.message == WM_GETMINMAXINFO)
{
auto* mmi = reinterpret_cast<MINMAXINFO*>(msg.lParam);
RECT rc{ 0, 0, minClientW, minClientH };
DWORD style = GetWindowLong(hWnd, GWL_STYLE);
DWORD ex = GetWindowLong(hWnd, GWL_EXSTYLE);
AdjustWindowRectEx(&rc, style, FALSE, ex);
mmi->ptMinTrackSize.x = rc.right - rc.left;
mmi->ptMinTrackSize.y = rc.bottom - rc.top;
continue;
}
// 关键点⑥:WM_SIZE 只记录新尺寸;若非拉伸阶段则立即置位 needResizeDirty
if (msg.message == WM_SIZE && msg.wParam != SIZE_MINIMIZED)
{
const int nw = LOWORD(msg.lParam);
const int nh = HIWORD(msg.lParam);
// 基本合法性校验(不小于最小值、不过大)
if (nw >= minClientW && nh >= minClientH && nw <= 10000 && nh <= 10000)
{
if (nw != width || nh != height)
{
pendingW = nw;
pendingH = nh;
// 在“非拉伸阶段”的 WM_SIZE(例如最大化/还原/程序化调整)直接触发收口
needResizeDirty = true;
SX_LOGD("Resize") <<SX_T("WM_SIZE:待处理=(", "WM_SIZE: pending=(") << pendingW << "x" << pendingH << "), isSizing=" << (isSizing ? 1 : 0);
}
}
continue;
}
// 输入优先:先给顶层“非模态对话框”,再传给普通控件
for (auto it = dialogs.rbegin(); it != dialogs.rend(); ++it)
{
auto& d = *it;
if (d->IsVisible() && !d->model())
consume = d->handleEvent(msg);
if (consume)
{
SX_LOGD("Event") << SX_T("事件被非模态对话框处理","Event consumed by non-modal dialog");
break;
}
}
if (!consume)
{
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
{
consume = (*it)->handleEvent(msg);
if (consume)
{
SX_LOGD("Event") << SX_T("事件被控件处理 id=", "Event consumed by control id=") << (*it)->getId();
redrawDialogs = true; // 控件事件可能引起对话框状态变化,标记需要重绘对话框
break;
}
}
}
}
// 关键点⑦:事件处理后,如果对话框状态可能变化(例如控件事件引起的控件重绘可能导致对话框被覆盖),优先重绘对话框以确保界面响应及时;后续再根据 needResizeDirty 进行一次性重绘。
if (redrawDialogs)
{
for (auto& d : dialogs)
{
if (!d->model() && d->IsVisible())
d->setDirty(true);
d->draw();
}
redrawDialogs = false; // 重置标志
}
//如果有对话框打开或者关闭强制重绘
bool needredraw = false;
if(dialogOpen)
{
for (auto& d : dialogs)
{
needredraw = d->IsVisible();
if (needredraw)break;
}
}
if (needredraw || dialogClose)
{
if (dialogClose)
{
// 对话框关闭后,需要手动合成一个鼠标移动消息并分发给所有普通控件,
// 以便它们能及时更新悬停状态(hover),否则悬停状态可能保持错误状态。
// 先把当前鼠标位置转换为客户区坐标,并合成一次 WM_MOUSEMOVE,先分发给控件更新 hover 状态
SX_LOGD("Event") << SX_T("对话框关闭,合成WM_MOUSEMOVE已下发", "Dialog closed; synthetic WM_MOUSEMOVE dispatched");
POINT pt;
if (GetCursorPos(&pt))
{
ScreenToClient(this->hWnd, &pt);
ExMessage mm;
mm.message = WM_MOUSEMOVE;
mm.x = (short)pt.x;
mm.y = (short)pt.y;
// 只分发给 window 层控件(因为 dialog 已经关闭或即将关闭)
for (auto it = controls.rbegin(); it != controls.rend(); ++it)
(*it)->handleEvent(mm);
}
dialogClose = false; // 重置标志
}
BeginBatchDraw();
SX_LOGD("Event") << SX_T("对话框打开/关闭,触发全量重绘", "The dialog box opens/closes, triggering a full redraw");
// 先绘制普通控件
for (auto& c : controls)
c->draw();
// 然后绘制对话框(确保对话框在最上层)
for (auto& d : dialogs)
{
if (!d->model() && d->IsVisible())
d->setDirty(true);
d->draw();
}
EndBatchDraw();
needredraw = false;
dialogOpen = false;
}
// —— 统一收口(needResizeDirty 为真时执行一次性重绘)——
if (needResizeDirty)
{
SX_LOGI("Resize") << SX_T("调整窗口尺寸开始:width=","Resize settle start: width=") << width << " height=" << height;
SX_TRACE_SCOPE(SX_T("调整尺寸","Resize"),SX_T("窗口:调整尺寸", "Window::resize_settle"));
// 以“实际客户区尺寸”为准,防止 pending 与真实尺寸出现偏差
RECT clientRect;
GetClientRect(hWnd, &clientRect);
int actualWidth = clientRect.right - clientRect.left;
int actualHeight = clientRect.bottom - clientRect.top;
const int finalW = (std::max)(minClientW, actualWidth);
const int finalH = (std::max)(minClientH, actualHeight);
// 变化过大/异常场景保护
if (finalW != width || finalH != height)
{
if (abs(finalW - width) > 1000 || abs(finalH - height) > 1000)
{
// 认为是异常帧,跳过本次(不改变任何状态)
needResizeDirty = false;
continue;
}
// 再次冻结窗口更新,保证批量绘制的原子性
SendMessage(hWnd, WM_SETREDRAW, FALSE, 0);
BeginBatchDraw();
// 调整底层画布尺寸
if (finalW != width || finalH != height)
{
// 批量通知控件“窗口尺寸变化”,并标记重绘
for (auto& c : controls)
adaptiveLayout(c, finalH, finalW);
for (auto& d : dialogs)
{
if (auto dd = dynamic_cast<Dialog*>(d.get()))
{
dd->setDirty(true);
dd->setInitialization(true);
}
}
//重绘窗口
Resize(nullptr, finalW, finalH);
// 重取一次实际客户区尺寸做确认
GetClientRect(hWnd, &clientRect);
int confirmedWidth = clientRect.right - clientRect.left;
int confirmedHeight = clientRect.bottom - clientRect.top;
int renderWidth = confirmedWidth;
int renderHeight = confirmedHeight;
// 背景:若设置了背景图则重载并铺满;否则清屏为纯色
if (background && !bkImageFile.empty())
{
background = std::make_unique<IMAGE>();
loadimage(background.get(), bkImageFile.c_str(), renderWidth, renderHeight, true);
putimage(0, 0, background.get());
}
else
{
setbkcolor(wBkcolor);
cleardevice();
}
// 最终提交“当前已应用尺寸”(用于外部查询/下次比较)
width = renderWidth;
height = renderHeight;
}
// 统一批量绘制
for (auto& c : controls) c->draw();
for (auto& d : dialogs) d->draw();
EndBatchDraw();
// 解冻后标记区域有效,避免系统再次触发 WM_PAINT 覆盖自绘内容。
SendMessage(hWnd, WM_SETREDRAW, TRUE, 0);
ValidateRect(hWnd, nullptr);
}
SX_LOGI("Resize") << SX_T("尺寸调整已完成:width=","Resize settle done: width=") << width << " height=" << height;
needResizeDirty = false; // 收口完成,清标志
}
// 轻微睡眠,削峰填谷(不阻塞拖拽体验)
Sleep(10);
}
return 1;
}
// ---------------- 其余接口 ----------------
void Window::setBkImage(std::string pImgFile)
{
// 更换背景图:立即加载并绘制一次;同时将所有控件标 dirty 并重绘
background = std::make_unique<IMAGE>();
bkImageFile = std::move(pImgFile);
loadimage(background.get(), bkImageFile.c_str(), width, height, true);
putimage(0, 0, background.get());
BeginBatchDraw();
for (auto& c : controls)
{
c->setDirty(true);
c->draw();
}
for (auto& d : dialogs)
{
d->setDirty(true);
d->draw();
}
EndBatchDraw();
}
void Window::setBkcolor(COLORREF c)
{
// 更换纯色背景:立即清屏并批量重绘控件/对话框
wBkcolor = c;
setbkcolor(wBkcolor);
cleardevice();
BeginBatchDraw();
for (auto& c : controls)
{
c->setDirty(true);
c->draw();
}
for (auto& d : dialogs)
{
d->setDirty(true);
d->draw();
}
EndBatchDraw();
}
void Window::setHeadline(std::string title)
{
// 设置窗口标题(仅改文本,不触发重绘)
headline = std::move(title);
if (hWnd)
SetWindowText(hWnd, headline.c_str());
}
void Window::addControl(std::unique_ptr<Control> control)
{
// 新增控件:仅加入管理容器,具体绘制在 draw()/收口时统一进行
controls.push_back(std::move(control));
}
void Window::addDialog(std::unique_ptr<Control> dlg)
{
// 新增非模态对话框:管理顺序决定事件优先级(顶层从后往前)
dialogs.push_back(std::move(dlg));
}
bool Window::hasNonModalDialogWithCaption(const std::string& caption, const std::string& message) const
{
// 查询是否存在“可见且非模态”的对话框(用于避免重复弹)
for (const auto& dptr : dialogs)
{
if (!dptr) continue;
if (auto* d = dynamic_cast<Dialog*>(dptr.get()))
if (d->IsVisible() && !d->model() && d->GetCaption() == caption && d->GetText() == message)
{
dialogOpen = true;
return true;
}
}
return false;
}
HWND Window::getHwnd() const
{
return hWnd;
}
int Window::getWidth() const
{
// 注意:这里返回 pendingW
// 表示“最近一次收到的尺寸”(可能尚未应用到画布,最终以收口时的 width 为准)
return pendingW;
}
int Window::getHeight() const
{
// 同上,返回 pendingH(与 getWidth 对应)
return pendingH;
}
std::string Window::getHeadline() const
{
return headline;
}
COLORREF Window::getBkcolor() const
{
return wBkcolor;
}
IMAGE* Window::getBkImage() const
{
return background.get();
}
std::string Window::getBkImageFile() const
{
return bkImageFile;
}
std::vector<std::unique_ptr<Control>>& Window::getControls()
{
return controls;
}
void Window::pumpResizeIfNeeded()
{
if (!needResizeDirty) return;
SX_LOGD("Resize")
<< SX_T("执行 pumpResizeIfNeededneedResizeDirty=",
"pumpResizeIfNeeded: needResizeDirty=")
<< (needResizeDirty ? 1 : 0)
<< SX_T("(需要进行一次缩放收口/重排重绘)", "");
RECT rc; GetClientRect(hWnd, &rc);
const int finalW = max(minClientW, rc.right - rc.left);
const int finalH = max(minClientH, rc.bottom - rc.top);
if (finalW == width && finalH == height) { needResizeDirty = false; return; }
SendMessage(hWnd, WM_SETREDRAW, FALSE, 0);
BeginBatchDraw();
// Resize + 背景
Resize(nullptr, finalW, finalH);
GetClientRect(hWnd, &rc);
if (background && !bkImageFile.empty())
{
background = std::make_unique<IMAGE>();
loadimage(background.get(), bkImageFile.c_str(), rc.right - rc.left, rc.bottom - rc.top, true);
putimage(0, 0, background.get());
}
else
{
setbkcolor(wBkcolor);
cleardevice();
}
width = rc.right - rc.left; height = rc.bottom - rc.top;
// 通知控件/对话框
for (auto& c : controls)
{
adaptiveLayout(c, finalH, finalW);
c->onWindowResize();
}
for (auto& d : dialogs)
if (auto* dd = dynamic_cast<Dialog*>(d.get()))
dd->setInitialization(true); // 强制对话框在新尺寸下重建布局/快照
// 重绘
for (auto& c : controls) c->draw();
for (auto& d : dialogs) d->draw();
EndBatchDraw();
SendMessage(hWnd, WM_SETREDRAW, TRUE, 0);
// 原实现在此调用 InvalidateRect 导致系统再次发送 WM_PAINT,从而重复绘制,
// 这里改为 ValidateRect:直接标记区域为有效,通知系统我们已完成绘制,不必再触发 WM_PAINT。
// 这样可以避免收口阶段的绘制与系统重绘叠加造成顺序错乱。
ValidateRect(hWnd, nullptr);
needResizeDirty = false;
}
void Window::scheduleResizeFromModal(int w, int h)
{
if (w < minClientW) w = minClientW;
if (h < minClientH) h = minClientH;
if (w > 10000) w = 10000;
if (h > 10000) h = 10000;
if (w != width || h != height)
{
pendingW = w;
pendingH = h;
needResizeDirty = true; // 交给 pumpResizeIfNeeded 做统一收口+重绘
SX_LOGD("Resize")
<< SX_T("模态对话框触发缩放调度:pending=(",
"scheduleResizeFromModal: pending=(")
<< pendingW << "x" << pendingH
<< SX_T(")needResizeDirty=1(标记需要缩放收口)",
"), needResizeDirty=1");
}
}
void Window::adaptiveLayout(std::unique_ptr<Control>& c, const int finalH, const int finalW)
{
int origParentW = this->localwidth;
int origParentH = this->localheight;
if (c->getLayoutMode() == StellarX::LayoutMode::AnchorToEdges)
{
if ((StellarX::Anchor::Left == c->getAnchor_1() && StellarX::Anchor::Right == c->getAnchor_2())
|| (StellarX::Anchor::Right == c->getAnchor_1() && StellarX::Anchor::Left == c->getAnchor_2()))
{
int origRightDist = origParentW - (c->getLocalX() + c->getLocalWidth());
int newWidth = finalW - c->getLocalX() - origRightDist;
c->setWidth(newWidth);
// 左侧距离固定,ctrl->x 保持为 localx 相对窗口左侧(父容器为窗口,偏移0)
c->setX(c->getLocalX());
}
else if ((StellarX::Anchor::Left == c->getAnchor_1() && StellarX::Anchor::NoAnchor == c->getAnchor_2())
|| (StellarX::Anchor::NoAnchor == c->getAnchor_1() && StellarX::Anchor::Left == c->getAnchor_2())
|| (StellarX::Anchor::Left == c->getAnchor_1() && StellarX::Anchor::Left == c->getAnchor_2()))
{
// 仅左锚定:宽度固定不变
c->setX(c->getLocalX());
c->setWidth(c->getLocalWidth());
}
else if ((StellarX::Anchor::Right == c->getAnchor_1() && StellarX::Anchor::NoAnchor == c->getAnchor_2())
|| (StellarX::Anchor::NoAnchor == c->getAnchor_1() && StellarX::Anchor::Right == c->getAnchor_2())
|| (StellarX::Anchor::Right == c->getAnchor_1() && StellarX::Anchor::Right == c->getAnchor_2()))
{
int origRightDist = origParentW - (c->getLocalX() + c->getLocalWidth());
c->setWidth(c->getLocalWidth()); // 宽度不变
c->setX(finalW - origRightDist - c->getWidth());
}
else if (StellarX::Anchor::NoAnchor == c->getAnchor_1() && StellarX::Anchor::NoAnchor == c->getAnchor_2())
{
c->setX(c->getLocalX());
c->setWidth(c->getLocalWidth());
}
if ((StellarX::Anchor::Top == c->getAnchor_1() && StellarX::Anchor::Bottom == c->getAnchor_2())
|| (StellarX::Anchor::Bottom == c->getAnchor_1() && StellarX::Anchor::Top == c->getAnchor_2()))
{
// 上下锚定:高度随窗口变化
int origBottomDist = origParentH - (c->getLocalY() + c->getLocalHeight());
int newHeight = finalH - c->getLocalY() - origBottomDist;
c->setHeight(newHeight);
c->setY(c->getLocalY());
}
else if ((StellarX::Anchor::Top == c->getAnchor_1() && StellarX::Anchor::NoAnchor == c->getAnchor_2())
|| (StellarX::Anchor::NoAnchor == c->getAnchor_1() && StellarX::Anchor::Top == c->getAnchor_2())
|| (StellarX::Anchor::Top == c->getAnchor_1() && StellarX::Anchor::Top == c->getAnchor_2()))
{
c->setY(c->getLocalY());
c->setHeight(c->getLocalHeight());
}
else if ((StellarX::Anchor::Bottom == c->getAnchor_1() && StellarX::Anchor::NoAnchor == c->getAnchor_2())
|| (StellarX::Anchor::NoAnchor == c->getAnchor_1() && StellarX::Anchor::Bottom == c->getAnchor_2())
|| (StellarX::Anchor::Bottom == c->getAnchor_1() && StellarX::Anchor::Bottom == c->getAnchor_2()))
{
int origBottomDist = origParentH - (c->getLocalY() + c->getLocalHeight());
c->setHeight(c->getLocalHeight());
c->setY(finalH - origBottomDist - c->getHeight());
}
else
{
// 垂直无锚点:默认为顶部定位,高度固定
c->setY(c->getLocalY());
c->setHeight(c->getLocalHeight());
}
}
c->onWindowResize();
}
+98
View File
@@ -0,0 +1,98 @@
/**
* Window(头文件)
*
* 设计目标:
* - 提供一个基于 Win32 + EasyX 的“可拉伸且稳定不抖”的窗口容器。
* - 通过消息过程子类化(WndProcThunk)接管关键消息(WM_SIZING/WM_SIZE/...)。
* - 将“几何变化记录(pendingW/H)”与“统一收口重绘(needResizeDirty)”解耦。
*
* 关键点(与 .cpp 中实现对应):
* - isSizing:处于交互拉伸阶段时,冻结重绘;松手后统一重绘,防止抖动。
* - WM_SIZING:只做“最小尺寸夹紧”,不回滚矩形、不做对齐;把其余交给系统。
* - WM_GETMINMAXINFO:按最小“客户区”换算到“窗口矩形”,提供系统层最小轨迹值。
* - runEventLoop:只记录 WM_SIZE 的新尺寸;真正绘制放在 needResizeDirty 时集中处理。
*/
//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;
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;
// —— 访问器 ——(只读接口,供外部查询当前窗口/标题/背景等)
HWND getHwnd() const;
int getWidth() const;
int getHeight() const;
std::string getHeadline() const;
COLORREF getBkcolor() const;
IMAGE* getBkImage() const;
std::string getBkImageFile() const;
std::vector<std::unique_ptr<Control>>& getControls();
// —— 尺寸调整 ——(供内部与外部调用的尺寸变化处理)
void pumpResizeIfNeeded(); // 执行一次统一收口重绘
void scheduleResizeFromModal(int w, int h);
private:
void adaptiveLayout(std::unique_ptr<Control>& c, const int finalH, const int finalW);
};
+31
View File
@@ -0,0 +1,31 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35828.75 d17.13
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "imGui-easyX", "imGui-easyX.vcxproj", "{FD33B55B-FD97-4087-8B95-6BF8285AAE73}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Debug|x64.ActiveCfg = Debug|x64
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Debug|x64.Build.0 = Debug|x64
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Debug|x86.ActiveCfg = Debug|Win32
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Debug|x86.Build.0 = Debug|Win32
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x64.ActiveCfg = Release|x64
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x64.Build.0 = Release|x64
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x86.ActiveCfg = Release|Win32
{FD33B55B-FD97-4087-8B95-6BF8285AAE73}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8E5EB082-F5AD-4E8A-8809-29A47F920B2B}
EndGlobalSection
EndGlobal
+174
View File
@@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Button.h" />
<ClInclude Include="Canvas.h" />
<ClInclude Include="Control.h" />
<ClInclude Include="CoreTypes.h" />
<ClInclude Include="Dialog.h" />
<ClInclude Include="label.h" />
<ClInclude Include="MessageBox.h" />
<ClInclude Include="StellarX.h" />
<ClInclude Include="SxLog.h" />
<ClInclude Include="TabControl.h" />
<ClInclude Include="table.h" />
<ClInclude Include="textBox.h" />
<ClInclude Include="window.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="Button.cpp" />
<ClCompile Include="Canvas.cpp" />
<ClCompile Include="Control.cpp" />
<ClCompile Include="Dialog.cpp" />
<ClCompile Include="label.cpp" />
<ClCompile Include="MessageBox.cpp" />
<ClCompile Include="SxLog.cpp" />
<ClCompile Include="TabControl.cpp" />
<ClCompile Include="table.cpp" />
<ClCompile Include="textBox.cpp" />
<ClCompile Include="window.cpp" />
<ClCompile Include="z-testDome.cpp" />
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{fd33b55b-fd97-4087-8b95-6bf8285aae73}</ProjectGuid>
<RootNamespace>imGuieasyX</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>
</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PrecompiledHeaderFile />
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>
</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PrecompiledHeaderFile />
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>
</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PrecompiledHeaderFile />
<LanguageStandard_C>stdc17</LanguageStandard_C>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>false</SDLCheck>
<PreprocessorDefinitions>
</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<PrecompiledHeaderFile />
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
+96
View File
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="源文件">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="头文件">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="资源文件">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Control.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="Button.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="window.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="label.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="textBox.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="Canvas.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="table.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="StellarX.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="CoreTypes.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="Dialog.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="MessageBox.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="TabControl.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="SxLog.h">
<Filter>头文件</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Control.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="Button.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="z-testDome.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="window.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="label.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="textBox.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="Canvas.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="table.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="Dialog.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="MessageBox.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="TabControl.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="SxLog.cpp">
<Filter>源文件</Filter>
</ClCompile>
</ItemGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ShowAllFiles>false</ShowAllFiles>
</PropertyGroup>
</Project>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+711
View File
@@ -0,0 +1,711 @@
// 本工具基于 StellarX 构建,轻量级的 Windows GUI 框架。
#include"StellarX.h"
#define KEY 1
#if 1 == KEY
int main()
{
//StellarX::SxLogger::setGBK();
//StellarX::SxLogger::Get().enableFile("stellarx.log", false, 1024);
StellarX::SxLogger::Get().enableConsole(true);
StellarX::SxLogger::Get().setMinLevel(StellarX::SxLogLevel::Debug); // Info/Debug/Trace 自己切
StellarX::SxLogger::Get().setLanguage(StellarX::SxLogLanguage::ZhCN); // ZhCN / EnUS
Window mainWindow(1200, 400, 1, RGB(255, 0, 0), "StellarX Hello");
auto table = std::make_unique<Table>(200, 50);
Table* table_ptr = table.get();
table->setHeaders({ "name","age","seorc","home" });
table->setData({
{"zhangsan","20","99.99","wadsacafadsawd"},
{"lisi","20","99.99","wadsacafadsawd"},
{"wangwu","20","99.99","wadsacafadsawd"},
{"zhaoliu","20","99.99","wadsacafadsawd"},
{"1","20","99.99","wadsacafadsawd" },
{"2","20","99.99","wadsacafadsawd" } ,
{"3","20","99.99","wadsacafadsawd" } ,
{"4","20","99.99","wadsacafadsawd" },
{"5","20","99.99","wadsacafadsawd" } ,
{"6","20","99.99","wadsacafadsawd" } ,
{"7","20","99.99","wadsacvvvvvvv我afads" } ,
{"8","2000000000000000000","99.999999999999","wadsacafa0000000d0000000sawd" } ,
{"9555555555","2000","99.9999999","wadsacafadsawd" } });
table->setRowsPerPage(4);
table->setTableBorderWidth(1);
table->textStyle.color = RGB(255, 0, 0);
table->setTableBorder(RGB(255, 0, 0));
table->setTableFillMode(StellarX::FillMode::Null);
auto but = std::make_unique<Button>(0, 0, 100, 15, "按钮1");
auto ta = std::make_unique<TabControl>(10, 10, 1100, 300);
ta->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
ta->setActiveIndex(2);
std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>> p;
std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>> p1;
std::pair<std::unique_ptr<Button>, std::unique_ptr<Canvas>> p2;
p2.first = std::make_unique<Button>(0, 0, 100, 15, "按钮3");
p2.second = std::make_unique<Canvas>(200, 200, 1100, 280);
p2.second->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
p1.first = std::make_unique<Button>(0, 0, 100, 15, "按钮2");
p1.second = std::make_unique<Canvas>(200, 200, 1100, 280);
p1.second->setCanvasBkColor(RGB(255, 200, 0));
p.first = std::move(but);
p.second = std::make_unique<Canvas>(100, 100, 1100, 280);
p.second->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
p.second->setCanvasBkColor(RGB(0, 255, 0));
p.second->setCanvasfillMode(StellarX::FillMode::Null);
auto table_get = table.get();
p.second->addControl(std::move(table));
ta->setTabPlacement(StellarX::TabPlacement::Bottom);
auto b = std::make_unique<Button>(0, 0, 100, 35, "按钮");
b->setOnClickListener([&]()
{
/* StellarX::MessageBox::showAsync(mainWindow, "韦世豪大傻逼", "笑话", StellarX::MessageBoxType::AbortRetryIgnore,
[](StellarX::MessageBoxResult result) { if (StellarX::MessageBoxResult::Retry == result)std::cout << "\a"; });
*/
table_ptr->clearHeaders();
table_ptr->clearData();
table_ptr->setHeaders({ "new_name","new_age","new_seorc","new""_home","new_test"});
table_ptr->setData({
{"new_zhangsan","30","88.88","new_wadsacafadsawd","123"},
{"new_lisi","30","88.88","new_wadsacafadsawd","456"},
{"new_wangwu","30","88.88","new_wadsacafadsawd","789"},
{"new_zhaoliu","30","88.88","new_wadsacafadsawd","101"} });
table_ptr->setData({"test_zhaoliu","test_30","88.88","test_wadsacafadsawd","test_101"});
});
p.second->addControl(std::move(b));
ta->add(std::move(p));
ta->add(std::move(p1));
ta->add(std::move(p2));
ta->setTabBarHeight(20);
ta->setCanvasfillMode(StellarX::FillMode::Null);
auto ta_ptr = ta.get();
auto test = std::make_unique<Button>(1100, 350, 40, 40, "test");
auto test_ptr = test.get();
test->setOnClickListener([&]()
{
ta_ptr->setActiveIndex(0);
});
//auto testTextBox = std::make_unique<TextBox>(100, 350, 80, 30);
auto testTextBox = std::make_unique<TextBox>(0, 0, 80, 30);
testTextBox->setText("hello");
auto _123 = std::make_unique<Canvas>(100, 340, 180, 130);
_123->addControl(std::move(testTextBox));
_123->setCanvasfillMode(StellarX::FillMode::Null);
mainWindow.addControl(std::move(ta));
mainWindow.addControl(std::move(test));
mainWindow.addControl(std::move(_123));
mainWindow.draw("D:\\编程相关\\素材\\星垣logo.png");
mainWindow.runEventLoop();
}
#endif
#if 2 == KEY
// 本工具基于 StellarX 构建,轻量级的 Windows GUI 框架。
#include"StellarX.h"
#include <sstream>
#include<iomanip>
#include<array>
auto blackColor = RGB(202, 255, 255);
char initData[33] = "00000000000000000000000000000000";//初始数据
bool gSigned = false; //是否为有符号数
int main()
{
StellarX::SxLogger::Get().enableConsole(true);
StellarX::SxLogger::Get().setMinLevel(StellarX::SxLogLevel::Debug); // Info/Debug/Trace 自己切
StellarX::SxLogger::Get().setLanguage(StellarX::SxLogLanguage::ZhCN); // ZhCN / EnUS
Window mainWindow(700, 510, 1, RGB(255, 255, 255), "寄存器查看工具 V1.0——我在人间做废物 (同类工具定制:3150131407(Q / V))");
//选择区控件
auto selectionAreaLabel = std::make_unique<Label>(18, 0, "32位选择区");
selectionAreaLabel->setTextdisap(true);
std::vector<std::unique_ptr<Label>>selectionAreaButtonLabel;
std::vector<std::unique_ptr<Button>>selectionAreaButton;
std::vector<Button*>selectionAreaButton_ptr;
auto selectionArea = std::make_unique <Canvas>(10, 10, 680, 150);
selectionArea->setCanvasBkColor(blackColor);
selectionArea->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
for (int y = 0; y < 2; y++)
{
std::ostringstream os;
for (int x = 0; x < 16; x++)
{
if (0 == y)
{
selectionAreaButtonLabel.push_back(std::make_unique<Label>(x * 35 + 25 + 28 * (x / 4), 26, "", RGB(208, 208, 208)));
os << std::setw(2) << std::setfill('0') << 31 - x;
selectionAreaButtonLabel.back()->setText(os.str());
selectionAreaButtonLabel.back()->setTextdisap(true);
selectionAreaButton.push_back(
std::make_unique<Button>(x * 35 + 27 + 28 * (x / 4), 58, 25, 30, "0",
blackColor, RGB(171, 196, 220), StellarX::ButtonMode::TOGGLE));
selectionAreaButton.back()->textStyle.color = RGB(226, 116, 152);
selectionAreaButton.back()->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
selectionAreaButton_ptr.push_back(selectionAreaButton.back().get());
int k = 32 - x - 1;
//选择区按钮被点击后在二进制0和1之间切换,并更新initData
selectionAreaButton_ptr.back()->setOnToggleOnListener([k, btn = selectionAreaButton_ptr.back()]()
{
btn->setButtonText("1");
initData[k] = '1';
});
selectionAreaButton_ptr.back()->setOnToggleOffListener([k, btn = selectionAreaButton_ptr.back()]()
{
btn->setButtonText("0");
initData[k] = '0';
});
}
else
{
selectionAreaButtonLabel.push_back(std::make_unique<Label>(x * 35 + 25 + 28 * (x / 4), 90, "", RGB(208, 208, 208)));
os << std::setw(2) << std::setfill('0') << 15 - x;
selectionAreaButtonLabel.back()->setText(os.str());
selectionAreaButtonLabel.back()->setTextdisap(true);
selectionAreaButton.push_back(
std::make_unique<Button>(x * 35 + 27 + 28 * (x / 4), 120, 25, 30, "0",
blackColor, RGB(171, 196, 220), StellarX::ButtonMode::TOGGLE));
selectionAreaButton.back()->textStyle.color = RGB(226, 116, 152);
selectionAreaButton.back()->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
selectionAreaButton_ptr.push_back(selectionAreaButton.back().get());
int k = 15 - x;
selectionAreaButton.back()->setOnToggleOnListener([k, btn = selectionAreaButton_ptr.back()]()
{
btn->setButtonText("1");
initData[k] = '1';
});
selectionAreaButton.back()->setOnToggleOffListener([k, btn = selectionAreaButton_ptr.back()]()
{
btn->setButtonText("0");
initData[k] = '0';
});
}
os.str("");
os.clear();
}
}
selectionArea->addControl(std::move(selectionAreaLabel));
for (auto& s : selectionAreaButton)
selectionArea->addControl(std::move(s));
for (auto& s : selectionAreaButtonLabel)
selectionArea->addControl(std::move(s));
//功能区控件
//功能区总容器
auto function = std::make_unique<Canvas>(10, 170, 680, 70);
function->setCanvasfillMode(StellarX::FillMode::Null);
function->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
function->setCanvasBkColor(blackColor);
auto bitInvert_que = std::make_unique<Canvas>(0, 0, 220, 70);
auto leftShift_que = std::make_unique<Canvas>(230, 0, 220, 70);
auto rightShift_que = std::make_unique<Canvas>(460, 0, 220, 70);
auto bitInvert = bitInvert_que.get();
auto leftShift = leftShift_que.get();
auto rightShift = rightShift_que.get();
bitInvert->setCanvasBkColor(blackColor);
bitInvert->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
leftShift->setCanvasBkColor(blackColor);
leftShift->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
rightShift->setCanvasBkColor(blackColor);
rightShift->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
function->addControl(std::move(bitInvert_que));
function->addControl(std::move(leftShift_que));
function->addControl(std::move(rightShift_que));
auto bitInvertLabel = std::make_unique<Label>(13, -10, "位取反");
bitInvertLabel->setTextdisap(true);
auto leftShiftLabel = std::make_unique<Label>(13, -10, "左移位");
leftShiftLabel->setTextdisap(true);
auto rightShiftLabel = std::make_unique<Label>(13, -10, "右移位");
rightShiftLabel->setTextdisap(true);
// ====== 公用小工具======
auto clamp = [](int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); };
auto toInt = [](const std::string& s, int def = 0) {
try { return std::stoi(s); }
catch (...) { return def; }
};
// bit号(31..0) -> selectionAreaButton下标(0..31)
auto vecIndexFromBit = [](int bit) { return 31 - bit; };
// 读取当前32位点击态
auto snapshotBits = [&]() {
std::array<bool, 32> a{};
for (int b = 0; b < 32; ++b)
a[b] = selectionAreaButton_ptr[vecIndexFromBit(b)]->isClicked();
return a;
};
// 应用目标态:仅当不同才 setButtonClick
auto applyBits = [&](const std::array<bool, 32>& a) {
for (int b = 0; b < 32; ++b) {
auto btn = selectionAreaButton_ptr[vecIndexFromBit(b)];
if (btn->isClicked() != a[b]) btn->setButtonClick(a[b]);
}
};
//取反区控件
std::array<std::unique_ptr<Label>, 4> bitInvertFunctionLabel;
bitInvertFunctionLabel[0] = std::make_unique<Label>(30, 10, "低位");
bitInvertFunctionLabel[1] = std::make_unique<Label>(90, 10, "高位");
bitInvertFunctionLabel[2] = std::make_unique<Label>(15, 38, "");
bitInvertFunctionLabel[3] = std::make_unique<Label>(75, 38, "");
std::array<std::unique_ptr<TextBox>, 2> bitInvertFunctionTextBox;
bitInvertFunctionTextBox[0] = std::make_unique<TextBox>(35, 35, 35, 30, "0");
bitInvertFunctionTextBox[1] = std::make_unique<TextBox>(95, 35, 35, 30, "0");
auto invL = bitInvertFunctionTextBox[0].get();
auto invH = bitInvertFunctionTextBox[1].get();
auto bitInvertFunctionButton = std::make_unique<Button>(135, 35, 80, 30, "位取反",
blackColor, RGB(171, 196, 220));
bitInvertFunctionButton->textStyle.color = RGB(226, 116, 152);
bitInvertFunctionButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
auto bitInvertFunctionButton_ptr = bitInvertFunctionButton.get();
bitInvert->addControl(std::move(bitInvertFunctionButton));
bitInvert->addControl(std::move(bitInvertLabel));
for (auto& b : bitInvertFunctionTextBox)
{
b->setMaxCharLen(3);
b->textStyle.color = RGB(226, 116, 152);
b->setTextBoxBk(RGB(244, 234, 142));
b->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
bitInvert->addControl(std::move(b));
}
for (auto& b : bitInvertFunctionLabel)
{
b->setTextdisap(true);
bitInvert->addControl(std::move(b));
}
//左移控件
auto leftShiftFunctionLabel = std::make_unique<Label>(198, 30, "");
leftShiftFunctionLabel->setTextdisap(true);
auto leftShiftFunctionTextBox = std::make_unique<TextBox>(90, 30, 100, 30, "0");
leftShiftFunctionTextBox->setMaxCharLen(3);
leftShiftFunctionTextBox->textStyle.color = RGB(226, 116, 152);
leftShiftFunctionTextBox->setTextBoxBk(RGB(244, 234, 142));
leftShiftFunctionTextBox->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
auto shlBox = leftShiftFunctionTextBox.get();
auto leftShiftFunctionButton = std::make_unique<Button>(15, 30, 60, 30, "左移",
blackColor, RGB(171, 196, 220));
leftShiftFunctionButton->textStyle.color = RGB(226, 116, 152);
leftShiftFunctionButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
auto leftShiftFunctionButton_ptr = leftShiftFunctionButton.get();
leftShift->addControl(std::move(leftShiftFunctionButton));
leftShift->addControl(std::move(leftShiftFunctionTextBox));
leftShift->addControl(std::move(leftShiftLabel));
leftShift->addControl(std::move(leftShiftFunctionLabel));
//右移控件
auto rightShiftFunctionLabel = std::make_unique<Label>(198, 30, "");
rightShiftFunctionLabel->setTextdisap(true);
auto rightShiftFunctionTextBox = std::make_unique<TextBox>(90, 30, 100, 30, "0");
rightShiftFunctionTextBox->setMaxCharLen(3);
rightShiftFunctionTextBox->textStyle.color = RGB(226, 116, 152);
rightShiftFunctionTextBox->setTextBoxBk(RGB(244, 234, 142));
rightShiftFunctionTextBox->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
auto shrBox = rightShiftFunctionTextBox.get();
auto rightShiftFunctionButton = std::make_unique<Button>(15, 30, 60, 30, "右移",
blackColor, RGB(171, 196, 220));
rightShiftFunctionButton->textStyle.color = RGB(226, 116, 152);
rightShiftFunctionButton->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
auto rightShiftFunctionButton_ptr = rightShiftFunctionButton.get();
rightShift->addControl(std::move(rightShiftFunctionButton));
rightShift->addControl(std::move(rightShiftFunctionTextBox));
rightShift->addControl(std::move(rightShiftLabel));
rightShift->addControl(std::move(rightShiftFunctionLabel));
//显示区控件
//数值显示
auto NumericalDisplayArea = std::make_unique<Canvas>(10, 255, 680, 70);
NumericalDisplayArea->setCanvasBkColor(blackColor);
NumericalDisplayArea->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
std::array<std::unique_ptr<Label>, 3> NumericalDisplayAreaLabel;
NumericalDisplayAreaLabel[0] = std::make_unique<Label>(18, -10, "数值显示区");
NumericalDisplayAreaLabel[1] = std::make_unique<Label>(20, 25, "十六进制");
NumericalDisplayAreaLabel[2] = std::make_unique<Label>(330, 25, "十进制");
std::array<std::unique_ptr<TextBox>, 2> NumericalDisplayAreaTextBox;
NumericalDisplayAreaTextBox[0] = std::make_unique<TextBox>(110, 25, 200, 30, "0");
NumericalDisplayAreaTextBox[1] = std::make_unique<TextBox>(400, 25, 200, 30, "0");
auto hex = NumericalDisplayAreaTextBox[0].get();
auto dec = NumericalDisplayAreaTextBox[1].get();
for (auto& b : NumericalDisplayAreaLabel)
{
b->setTextdisap(true);
NumericalDisplayArea->addControl(std::move(b));
}
for (auto& b : NumericalDisplayAreaTextBox)
{
b->setMaxCharLen(11);
b->textStyle.color = RGB(255, 69, 0);
b->setTextBoxBk(RGB(141, 141, 141));
b->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
b->setMode(StellarX::TextBoxmode::READONLY_MODE);
NumericalDisplayArea->addControl(std::move(b));
}
//二进制显示
auto BinaryDisplayArea = std::make_unique<Canvas>(10, 335, 680, 110);
BinaryDisplayArea->setCanvasBkColor(blackColor);
BinaryDisplayArea->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
std::array<std::unique_ptr<Label>, 3> BinaryDisplayAreaLabel;
BinaryDisplayAreaLabel[0] = std::make_unique<Label>(18, -10, "二进制显示区");
BinaryDisplayAreaLabel[1] = std::make_unique<Label>(35, 20, "上次值");
BinaryDisplayAreaLabel[2] = std::make_unique<Label>(35, 67, "本次值");
std::array<std::unique_ptr<TextBox>, 2> BinaryDisplayAreaTextBox;
BinaryDisplayAreaTextBox[0] = std::make_unique<TextBox>(110, 20, 520, 30, "0000_0000_0000_0000_0000_0000_0000_0000");
BinaryDisplayAreaTextBox[1] = std::make_unique<TextBox>(110, 67, 520, 30, "0000_0000_0000_0000_0000_0000_0000_0000");
auto Last = BinaryDisplayAreaTextBox[0].get();
auto This = BinaryDisplayAreaTextBox[1].get();
for (auto& b : BinaryDisplayAreaLabel)
{
b->setTextdisap(true);
BinaryDisplayArea->addControl(std::move(b));
}
for (auto& b : BinaryDisplayAreaTextBox)
{
b->setMaxCharLen(40);
b->textStyle.color = RGB(255, 69, 0);
b->setTextBoxBk(RGB(141, 141, 141));
b->setTextBoxshape(StellarX::ControlShape::B_RECTANGLE);
b->setMode(StellarX::TextBoxmode::READONLY_MODE);
BinaryDisplayArea->addControl(std::move(b));
}
// 由位图 bits 生成数值
auto valueFromBits = [](const std::array<bool, 32>& bits) -> uint32_t {
uint32_t v = 0;
for (int b = 0; b < 32; ++b) if (bits[b]) v |= (1u << b);
return v;
};
// 由位图 bits 生成 "0000_0000_..._0000"MSB→LSB31..0
auto binaryGroupedFromBits = [](const std::array<bool, 32>& bits) -> std::string {
std::string s; s.reserve(39);
for (int b = 31; b >= 0; --b) {
s.push_back(bits[b] ? '1' : '0');
if (b % 4 == 0 && b != 0) s.push_back('_');
}
return s;
};
// 用“目标位图 bits”刷新显示区
auto refreshDisplaysWithBits = [&](const std::string& prevThis,
const std::array<bool, 32>& bits,
TextBox* hex, TextBox* dec, TextBox* Last, TextBox* This)
{
const uint32_t val = valueFromBits(bits);
const int32_t s = static_cast<int32_t>(val);
char hexbuf[16];
std::snprintf(hexbuf, sizeof(hexbuf), "%08X", val);
hex->setText(hexbuf); // HEX(大写8位)
dec->setText(gSigned ? std::to_string(s) : std::to_string(val)); // DEC
Last->setText(prevThis); // 上次值 ← 刷新前的本次值
This->setText(binaryGroupedFromBits(bits)); // 本次值 ← 由“目标位图”生成
};
bitInvertFunctionButton_ptr->setOnClickListener([=, &snapshotBits, &applyBits, &refreshDisplaysWithBits]() {
const std::string prevThis = This->getText();
int L = clamp(toInt(invL->getText(), 0), 0, 31);
int H = clamp(toInt(invH->getText(), 0), 0, 31);
if (L > H) std::swap(L, H);
auto cur = snapshotBits();
for (int b = L; b <= H; ++b) cur[b] = !cur[b];
applyBits(cur); // 只改按钮点击态(触发位按钮自回调)
refreshDisplaysWithBits(prevThis, cur, hex, dec, Last, This);
});
leftShiftFunctionButton_ptr->setOnClickListener([=, &snapshotBits, &applyBits, &refreshDisplaysWithBits]() {
const std::string prevThis = This->getText();
int n = clamp(toInt(shlBox->getText(), 0), 0, 31);
auto cur = snapshotBits();
std::array<bool, 32> nxt{}; // 默认全 0
// 逻辑左移:高位丢弃、低位补 0
for (int b = 31; b >= 0; --b) nxt[b] = (b >= n) ? cur[b - n] : false;
applyBits(nxt);
refreshDisplaysWithBits(prevThis, nxt, hex, dec, Last, This);
});
rightShiftFunctionButton_ptr->setOnClickListener([=, &snapshotBits, &applyBits, &refreshDisplaysWithBits]() {
const std::string prevThis = This->getText();
int n = clamp(toInt(shrBox->getText(), 0), 0, 31);
auto cur = snapshotBits();
std::array<bool, 32> nxt{};
// 逻辑右移:低位丢弃、高位补 0
for (int b = 0; b < 32; ++b) nxt[b] = (b + n <= 31) ? cur[b + n] : false;
applyBits(nxt);
refreshDisplaysWithBits(prevThis, nxt, hex, dec, Last, This);
});
//配置区控件clearrectangle(10, 440, 690, 490);
auto configuration = std::make_unique<Canvas>(10, 455, 680, 40);
configuration->setCanvasBkColor(blackColor);
configuration->setShape(StellarX::ControlShape::B_ROUND_RECTANGLE);
auto configurationLabel = std::make_unique<Label>(20, -10, "配置区");
configurationLabel->setTextdisap(true);
std::array<std::unique_ptr<Button>, 2> configurationButton;
configurationButton[0] = std::make_unique<Button>(420, 10, 90, 20, "一键置0",
blackColor, RGB(171, 196, 220));
configurationButton[0]->textStyle.color = RGB(226, 116, 152);
configurationButton[0]->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
configurationButton[1] = std::make_unique<Button>(530, 10, 90, 20, "一键置1",
blackColor, RGB(171, 196, 220));
configurationButton[1]->textStyle.color = RGB(226, 116, 152);
configurationButton[1]->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
configurationButton[0]->setOnClickListener(
[&]() {
for (auto& s : selectionAreaButton_ptr)
if (s->isClicked()) s->setButtonClick(false);
// 刷新显示:prevThis 用当前 This 文本
const std::string prevThis = This->getText();
auto cur = snapshotBits();
{
char hexbuf[16];
uint32_t u = 0; for (int b = 0; b < 32; ++b) if (cur[b]) u |= (1u << b);
int32_t s = static_cast<int32_t>(u);
std::snprintf(hexbuf, sizeof(hexbuf), "%08X", u);
hex->setText(hexbuf);
dec->setText(gSigned ? std::to_string(s) : std::to_string(u));
Last->setText(prevThis);
This->setText(binaryGroupedFromBits(cur));
}
});
configurationButton[1]->setOnClickListener(
[&]() {
for (auto& s : selectionAreaButton_ptr)
if (!s->isClicked()) s->setButtonClick(true);
const std::string prevThis = This->getText();
auto cur = snapshotBits();
char hexbuf[16];
uint32_t u = 0; for (int b = 0; b < 32; ++b) if (cur[b]) u |= (1u << b);
int32_t s = static_cast<int32_t>(u);
std::snprintf(hexbuf, sizeof(hexbuf), "%08X", u);
hex->setText(hexbuf);
dec->setText(gSigned ? std::to_string(s) : std::to_string(u));
Last->setText(prevThis);
This->setText(binaryGroupedFromBits(cur));
});
auto signedToggle = std::make_unique<Button>(
330, 10, 80, 20, "无符号",
blackColor, RGB(171, 196, 220), StellarX::ButtonMode::TOGGLE);
signedToggle->textStyle.color = RGB(226, 116, 152);
signedToggle->setButtonShape(StellarX::ControlShape::B_RECTANGLE);
auto* signedTogglePtr = signedToggle.get();
signedTogglePtr->setOnToggleOnListener([&]() {
gSigned = true;
signedTogglePtr->setButtonText("有符号");
StellarX::MessageBox::showAsync(mainWindow, "有符号模式下,\n最高位为符号位,\n其余位为数值位。", "有符号模式");
// 立即刷新十进制显示:用当前位图算出新值,仅改 dec
auto cur = snapshotBits();
const uint32_t u = [&] { uint32_t v = 0; for (int b = 0; b < 32; ++b) if (cur[b]) v |= (1u << b); return v; }();
const int32_t s = static_cast<int32_t>(u);
dec->setText(std::to_string(s));
});
signedTogglePtr->setOnToggleOffListener([&]() {
gSigned = false;
signedTogglePtr->setButtonText("无符号");
StellarX::MessageBox::showAsync(mainWindow, "无符号模式下,\n所有位均为数值位。", "无符号模式");
auto cur = snapshotBits();
const uint32_t u = [&] { uint32_t v = 0; for (int b = 0; b < 32; ++b) if (cur[b]) v |= (1u << b); return v; }();
dec->setText(std::to_string(u));
});
signedTogglePtr->enableTooltip(true);
signedTogglePtr->setTooltipTextsForToggle("切换无符号模式", "切换有符号模式");
configuration->addControl(std::move(configurationButton[0]));
configuration->addControl(std::move(configurationButton[1]));
configuration->addControl(std::move(signedToggle));
configuration->addControl(std::move(configurationLabel));
mainWindow.addControl(std::move(selectionArea));
mainWindow.addControl(std::move(function));
mainWindow.addControl(std::move(NumericalDisplayArea));
mainWindow.addControl(std::move(BinaryDisplayArea));
mainWindow.addControl(std::move(configuration));
mainWindow.draw();
return mainWindow.runEventLoop();
}
#endif
#if 3 == KEY
// 本Demo基于 StellarX 构建,轻量级的 Windows GUI 框架。
#include"StellarX.h"
int main()
{
StellarX::SxLogger::setGBK();
StellarX::SxLogger::Get().enableConsole(true);
StellarX::SxLogger::Get().setMinLevel(StellarX::SxLogLevel::Trace); // Info/Debug/Trace 自己切
StellarX::SxLogger::Get().setLanguage(StellarX::SxLogLanguage::ZhCN); // ZhCN / EnUS
Window win(1300, 800, 1, RGB(255, 255, 0), "记账管理系统");
/*********登录界面***********/
//标签
std::unique_ptr<Label> logIn_label[3];
logIn_label[0] = std::make_unique<Label>(90, 150, "欢迎使用餐馆记账管理系统");
logIn_label[1] = std::make_unique<Label>(400, 300, "账号");
logIn_label[2] = std::make_unique<Label>(400, 400, "密码");
for (auto& log : logIn_label)
{
log->setTextdisap(true);
log->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
log->textStyle.lpszFace = "华文行楷";
}
logIn_label[0]->textStyle.nHeight = 100;
logIn_label[0]->setAnchor(StellarX::Anchor::Top, StellarX::Anchor::NoAnchor);
logIn_label[1]->textStyle.nHeight = 50;
logIn_label[1]->setAnchor(StellarX::Anchor::Bottom, StellarX::Anchor::NoAnchor);
logIn_label[2]->textStyle.nHeight = 50;
logIn_label[2]->setAnchor(StellarX::Anchor::Bottom, StellarX::Anchor::NoAnchor);
//输入框
std::unique_ptr<TextBox> logIn_textBox[2];
TextBox* logIn_textBox_ptr[2];
logIn_textBox[0] = std::make_unique<TextBox>(500, 295, 450, 50);
logIn_textBox[1] = std::make_unique<TextBox>(500, 395, 450, 50);
logIn_textBox[1]->setMode(StellarX::TextBoxmode::PASSWORD_MODE);
logIn_textBox_ptr[0] = logIn_textBox[0].get();
logIn_textBox_ptr[1] = logIn_textBox[1].get();
for (auto& tb : logIn_textBox)
{
tb->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
tb->setAnchor(StellarX::Anchor::Bottom, StellarX::Anchor::NoAnchor);
tb->setMaxCharLen(15);
}
//按钮
std::unique_ptr<Button> logIn_Button[2];
Button* logIn_Button_ptr[2];
logIn_Button[0] = std::make_unique<Button>(350, 500, 300, 50, "管理员登录");
logIn_Button[1] = std::make_unique<Button>(750, 500, 300, 50, "操作员登录");
logIn_Button_ptr[0] = logIn_Button[0].get();
logIn_Button_ptr[1] = logIn_Button[1].get();
for (auto& b : logIn_Button)
{
b->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
b->setAnchor(StellarX::Anchor::Bottom, StellarX::Anchor::NoAnchor);
}
//log画布
auto log_Canvas = std::make_unique<Canvas>(0, 0, 1300, 800);
Canvas* log_Canvas_ptr = log_Canvas.get();
log_Canvas_ptr->setCanvasfillMode(StellarX::FillMode::Null);
log_Canvas_ptr->setShape(StellarX::ControlShape::B_RECTANGLE);
log_Canvas_ptr->setLayoutMode(StellarX::LayoutMode::AnchorToEdges);
log_Canvas_ptr->setAnchor(StellarX::Anchor::Bottom, StellarX::Anchor::Top);
//将log界面控价加入logCanvas统一管理
for (auto& b : logIn_Button)
log_Canvas_ptr->addControl(std::move(b));
for (auto& tb : logIn_textBox)
log_Canvas_ptr->addControl(std::move(tb));
for (auto& la : logIn_label)
log_Canvas_ptr->addControl(std::move(la));
/**************业务UI****************/
auto tabControl = std::make_unique<TabControl>(10, 10, 1280, 780);
auto tabControl_ptr = tabControl.get();
tabControl_ptr->setIsVisible(false);
tabControl_ptr->setCanvasfillMode(StellarX::FillMode::Null);
tabControl_ptr->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
tabControl_ptr->setTabPlacement(StellarX::TabPlacement::Left);
tabControl_ptr->setTabBarHeight(60);
//添加页签及页
auto tabP = std::make_pair(std::make_unique<Button>(0, 0, 100, 100, "点餐"), std::make_unique<Canvas>(0, 0, 1290, 790));
tabP.second->setShape(StellarX::ControlShape::ROUND_RECTANGLE);
tabControl_ptr->add(std::move(tabP));
/*------------login事件-------------*/
//管理员登录按钮事件
logIn_Button_ptr[0]->setOnClickListener([&tabControl_ptr, &log_Canvas_ptr, &logIn_textBox_ptr, &win]()
{
if ("\0" == logIn_textBox_ptr[0]->getText() || "\0" == logIn_textBox_ptr[1]->getText())
{
if ("\0" == logIn_textBox_ptr[0]->getText())logIn_textBox_ptr[0]->setTextBoxBk(RGB(255, 0, 0));
if ("\0" == logIn_textBox_ptr[1]->getText())logIn_textBox_ptr[1]->setTextBoxBk(RGB(255, 0, 0));
std::cout << "\a";
StellarX::MessageBox::showAsync(win, "账号或密码不能为空!", "提示");
}
else if("admin" == logIn_textBox_ptr[0]->getText() && "123" == logIn_textBox_ptr[1]->getText())
{
log_Canvas_ptr->setIsVisible(false);
tabControl_ptr->setIsVisible(true);
win.draw("image\\bk1.jpg");
}
else
{
std::cout << "\a";
StellarX::MessageBox::showModal(win, "账号或密码错误!", "提示");
}
});
//操作员登录按钮事件
logIn_Button_ptr[1]->setOnClickListener([&tabControl_ptr, &log_Canvas_ptr, &logIn_textBox_ptr, &win]()
{
if ("\0" == logIn_textBox_ptr[0]->getText() || "\0" == logIn_textBox_ptr[1]->getText())
{
if ("\0" == logIn_textBox_ptr[0]->getText())logIn_textBox_ptr[0]->setTextBoxBk(RGB(255, 0, 0));
if ("\0" == logIn_textBox_ptr[1]->getText())logIn_textBox_ptr[1]->setTextBoxBk(RGB(255, 0, 0));
std::cout << "\a";
StellarX::MessageBox::showAsync(win, "账号或密码不能为空!", "提示");
}
else
{
log_Canvas_ptr->setIsVisible(false);
tabControl_ptr->setIsVisible(true);
win.draw("image\\bk1.jpg");
}
});
win.addControl(std::move(log_Canvas));
win.addControl(std::move(tabControl));
win.draw("image\\bk1.jpg");
return win.runEventLoop();
}
#endif