Initial baseline
This commit is contained in:
+16
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
@@ -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(¤tFont); // 获取当前字体样式
|
||||||
|
currentColor = gettextcolor(); // 获取当前字体颜色
|
||||||
|
currentBorderColor = getlinecolor(); //保存当前边框颜色
|
||||||
|
getlinestyle(¤tLineStyle); //保存当前线型
|
||||||
|
currentBkColor = getfillcolor(); //保存当前填充色
|
||||||
|
}
|
||||||
|
// 恢复之前保存的绘图状态
|
||||||
|
// 在控件绘制完成后调用,恢复全局绘图状态
|
||||||
|
void Control::restoreStyle()
|
||||||
|
{
|
||||||
|
settextstyle(¤tFont); // 恢复默认字体样式
|
||||||
|
settextcolor(currentColor); // 恢复默认字体颜色
|
||||||
|
setfillcolor(currentBkColor);
|
||||||
|
setlinestyle(¤tLineStyle);
|
||||||
|
setlinecolor(currentBorderColor);
|
||||||
|
setfillstyle(BS_SOLID);//恢复填充
|
||||||
|
}
|
||||||
|
|
||||||
|
void Control::requestRepaint(Control* parent)
|
||||||
|
{
|
||||||
|
// 说明:
|
||||||
|
// - 常规路径:子控件调用 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();
|
||||||
|
}
|
||||||
@@ -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
@@ -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: 次轴=Y;VBox: 次轴=X;Grid: 单元内
|
||||||
|
int alignY = Start; // Grid :控制单元内垂直(HBox / VBox通常只用 alignX)
|
||||||
|
// Grid 专用(可先不做)
|
||||||
|
int gridRow = 0, gridCol = 0, rowSpan = 1, colSpan = 1;
|
||||||
|
// Flow 专用(可先不做)
|
||||||
|
int flowBreak = 0; // 1=强制换行
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @枚举名称: TabPlacement
|
||||||
|
* @功能描述: 定义了选项卡页签的不同位置
|
||||||
|
*
|
||||||
|
* @详细说明:
|
||||||
|
* 根据选项卡页签的位置,选项卡页签可以有不同的布局方式。
|
||||||
|
*
|
||||||
|
* @成员说明:
|
||||||
|
* Top, - 选项卡页签位于顶部
|
||||||
|
* Bottom, - 选项卡页签位于底部
|
||||||
|
* Left, - 选项卡页签位于左侧
|
||||||
|
* Right - 选项卡页签位于右侧
|
||||||
|
*
|
||||||
|
* @使用示例:
|
||||||
|
* TabPlacement placement = TabPlacement::Top;
|
||||||
|
*/
|
||||||
|
enum class TabPlacement
|
||||||
|
{
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
Left,
|
||||||
|
Right
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* @枚举名称: LayoutMode
|
||||||
|
* @功能描述: 定义了两种布局模式
|
||||||
|
*
|
||||||
|
* @详细说明:
|
||||||
|
* 根据不同模式,在窗口拉伸时采用不同的布局策略
|
||||||
|
*
|
||||||
|
* @成员说明:
|
||||||
|
* Fixed, - 固定布局
|
||||||
|
* AnchorToEdges - 锚定布局
|
||||||
|
*
|
||||||
|
* @使用示例:
|
||||||
|
* LayoutMode mode = LayoutMode::Fixed;
|
||||||
|
*/
|
||||||
|
enum class LayoutMode
|
||||||
|
{
|
||||||
|
Fixed,
|
||||||
|
AnchorToEdges
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* @枚举名称: Anchor
|
||||||
|
* @功能描述: 定义了控件相对于窗口锚定的位置
|
||||||
|
*
|
||||||
|
* @详细说明:
|
||||||
|
* 根据不同的锚定位置,有不同的拉伸策略
|
||||||
|
*
|
||||||
|
* @成员说明:
|
||||||
|
* Top, - 锚定上边,控件上边与窗口上侧距离保持不变
|
||||||
|
* Bottom, - 锚定底边,控件底边与窗口底边距离保持不变
|
||||||
|
* Left, - 锚定左边,控件左边与窗口左侧距离保持不变
|
||||||
|
* Right - 锚定右边,控件上边与窗口右侧距离保持不变
|
||||||
|
*
|
||||||
|
* @使用示例:
|
||||||
|
* Anchor a = Anchor::Top;
|
||||||
|
*/
|
||||||
|
enum class Anchor
|
||||||
|
{
|
||||||
|
NoAnchor = 0,
|
||||||
|
Left = 1,
|
||||||
|
Right,
|
||||||
|
Top,
|
||||||
|
Bottom
|
||||||
|
};
|
||||||
|
}
|
||||||
+722
@@ -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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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:继承自 Canvas(Dialog 为可包含子控件的对话框容器)
|
||||||
|
* 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"
|
||||||
|
|
||||||
|
|
||||||
@@ -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 与 POSIX(localtime_s/localtime_r)
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
namespace StellarX
|
||||||
|
{
|
||||||
|
// -------- FileSink --------
|
||||||
|
|
||||||
|
// 打开文件输出
|
||||||
|
// 难点:
|
||||||
|
// - 需要支持追加与清空两种模式
|
||||||
|
// - open 前先 close,避免重复打开导致句柄泄漏
|
||||||
|
bool FileSink::open(const std::string& path, bool append)
|
||||||
|
{
|
||||||
|
close();
|
||||||
|
filePath = path;
|
||||||
|
appendMode = append;
|
||||||
|
|
||||||
|
std::ios::openmode mode = std::ios::out;
|
||||||
|
mode |= (append ? std::ios::app : std::ios::trunc);
|
||||||
|
|
||||||
|
ofs.open(path.c_str(), mode);
|
||||||
|
return ofs.is_open();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭文件输出(可重复调用)
|
||||||
|
void FileSink::close()
|
||||||
|
{
|
||||||
|
if (ofs.is_open()) ofs.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询是否已打开
|
||||||
|
bool FileSink::isOpen() const
|
||||||
|
{
|
||||||
|
return ofs.is_open();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入一整行
|
||||||
|
// 难点:
|
||||||
|
// - 写入后若启用 rotateBytes,需要及时检测文件大小是否到阈值
|
||||||
|
void FileSink::writeLine(const std::string& line)
|
||||||
|
{
|
||||||
|
if (!ofs.is_open()) return;
|
||||||
|
ofs << line;
|
||||||
|
if (rotateBytes > 0) rotateIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush 文件缓冲
|
||||||
|
void FileSink::flush()
|
||||||
|
{
|
||||||
|
if (ofs.is_open()) ofs.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动文件
|
||||||
|
// 难点:
|
||||||
|
// 1) tellp() 返回的是当前写指针位置,通常可近似视为文件大小
|
||||||
|
// 2) 时间戳用于文件名时需要做字符清洗,避免出现不友好字符
|
||||||
|
// 3) rename 行为与权限/占用有关,失败时需要保证不崩溃(此处选择“尽力而为”)
|
||||||
|
bool FileSink::rotateIfNeeded()
|
||||||
|
{
|
||||||
|
if (!ofs.is_open() || rotateBytes == 0) return false;
|
||||||
|
|
||||||
|
const std::streampos pos = ofs.tellp();
|
||||||
|
if (pos < 0) return false;
|
||||||
|
|
||||||
|
const std::size_t size = static_cast<std::size_t>(pos);
|
||||||
|
if (size < rotateBytes) return false;
|
||||||
|
|
||||||
|
ofs.flush();
|
||||||
|
ofs.close();
|
||||||
|
|
||||||
|
// xxx.log -> xxx.log.YYYYmmdd_HHMMSS
|
||||||
|
// 说明:
|
||||||
|
// - makeTimestampLocal 形如 "2026-01-09 12:34:56"
|
||||||
|
// - 文件名中把 '-' ' ' ':' 替换为 '_',只保留数字与 '_',降低环境差异
|
||||||
|
const std::string ts = SxLogger::makeTimestampLocal();
|
||||||
|
std::string safeTs;
|
||||||
|
safeTs.reserve(ts.size());
|
||||||
|
for (char ch : ts)
|
||||||
|
{
|
||||||
|
if (ch >= '0' && ch <= '9') safeTs.push_back(ch);
|
||||||
|
else if (ch == '-' || ch == ' ' || ch == ':') safeTs.push_back('_');
|
||||||
|
}
|
||||||
|
if (safeTs.empty()) safeTs = "rotated";
|
||||||
|
|
||||||
|
const std::string rotated = filePath + "." + safeTs;
|
||||||
|
std::rename(filePath.c_str(), rotated.c_str());
|
||||||
|
|
||||||
|
// 重新打开新文件
|
||||||
|
// 注意: 这里用 append=false,确保新文件从空开始
|
||||||
|
return open(filePath, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- SxLogger --------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 设置 Windows 控制台 codepage(只执行一次)
|
||||||
|
// 难点:
|
||||||
|
// - 只影响终端解释输出字节的方式,不影响源码文件编码
|
||||||
|
// - 使用 once_flag 避免重复 system 调用造成噪声与性能浪费
|
||||||
|
//
|
||||||
|
void SxLogger::setGBK()
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
static std::once_flag once;
|
||||||
|
std::call_once(once, []() {
|
||||||
|
// 切到chcp 936(GBK),避免中文日志在 CP936 控制台下乱码
|
||||||
|
// 说明:这不是 WinAPI;是执行系统命令
|
||||||
|
std::system("chcp 936 >nul");
|
||||||
|
|
||||||
|
// 补充说明:
|
||||||
|
// - chcp 936 实际是设置为 CP936(GBK)
|
||||||
|
// - 如果你的终端本身是 UTF-8 环境,调用它可能反而改变显示行为
|
||||||
|
// - 该函数建议只在“明确需要 GBK 控制台输出”的场景调用
|
||||||
|
|
||||||
|
// 尝试让 C/C++ 运行库按 UTF-8 工作(对部分流输出有帮助)
|
||||||
|
// std::setlocale(LC_ALL, ".UTF8");
|
||||||
|
});
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取单例
|
||||||
|
// 难点:
|
||||||
|
// - 作为全局入口,初始化必须线程安全
|
||||||
|
// - C++11 起函数内静态对象初始化由标准保证线程安全
|
||||||
|
SxLogger& SxLogger::Get()
|
||||||
|
{
|
||||||
|
static SxLogger inst;
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造:设置默认语言
|
||||||
|
SxLogger::SxLogger()
|
||||||
|
: lang(SxLogLanguage::ZhCN)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置最低输出级别
|
||||||
|
void SxLogger::setMinLevel(SxLogLevel level)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
cfg.minLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最低输出级别
|
||||||
|
SxLogLevel SxLogger::getMinLevel() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
return cfg.minLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置语言
|
||||||
|
// 难点:
|
||||||
|
// - 语言只影响 SX_T 的字符串选择
|
||||||
|
// - 这里用 atomic relaxed,避免频繁加锁
|
||||||
|
void SxLogger::setLanguage(SxLogLanguage l)
|
||||||
|
{
|
||||||
|
lang.store(l, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取语言
|
||||||
|
SxLogLanguage SxLogger::getLanguage() const
|
||||||
|
{
|
||||||
|
return lang.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 Tag 过滤
|
||||||
|
// 难点:
|
||||||
|
// - 当前实现是 vector<string> 线性匹配,适合 tag 数量不大
|
||||||
|
// - 若未来 tag 很多,可考虑 unordered_set 优化(但会增加依赖与复杂度)
|
||||||
|
void SxLogger::setTagFilter(SxTagFilterMode mode, const std::vector<std::string>& tags)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
cfg.tagFilterMode = mode;
|
||||||
|
cfg.tagList = tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空 Tag 过滤
|
||||||
|
void SxLogger::clearTagFilter()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
cfg.tagFilterMode = SxTagFilterMode::None;
|
||||||
|
cfg.tagList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开关控制台输出
|
||||||
|
// 难点:
|
||||||
|
// - ConsoleSink 持有 ostream 引用,不管理其生命周期
|
||||||
|
void SxLogger::enableConsole(bool enable)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
if (enable)
|
||||||
|
{
|
||||||
|
if (!consoleSink) consoleSink.reset(new ConsoleSink(std::cout));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
consoleSink.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开启文件输出
|
||||||
|
// 难点:
|
||||||
|
// - enableFile 成功与否决定 cfg.fileEnabled
|
||||||
|
// - 需要把 rotateBytes 同步到 FileSink
|
||||||
|
bool SxLogger::enableFile(const std::string& path, bool append, std::size_t rotateBytes_)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
|
||||||
|
if (!fileSink) fileSink.reset(new FileSink());
|
||||||
|
fileSink->setRotateBytes(rotateBytes_);
|
||||||
|
|
||||||
|
const bool ok = fileSink->open(path, append);
|
||||||
|
cfg.fileEnabled = ok;
|
||||||
|
cfg.filePath = path;
|
||||||
|
cfg.fileAppend = append;
|
||||||
|
cfg.rotateBytes = rotateBytes_;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭文件输出
|
||||||
|
void SxLogger::disableFile()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
if (fileSink) fileSink->close();
|
||||||
|
cfg.fileEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取配置副本
|
||||||
|
// 难点:
|
||||||
|
// - 返回副本避免外部拿到内部引用后绕过锁修改
|
||||||
|
SxLogConfig SxLogger::getConfigCopy() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置配置(整体替换)
|
||||||
|
void SxLogger::setConfig(const SxLogConfig& c)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
cfg = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 级别转字符串
|
||||||
|
const char* SxLogger::levelToString(SxLogLevel level)
|
||||||
|
{
|
||||||
|
switch (level)
|
||||||
|
{
|
||||||
|
case SxLogLevel::Trace: return "TRACE";
|
||||||
|
case SxLogLevel::Debug: return "DEBUG";
|
||||||
|
case SxLogLevel::Info: return "INFO ";
|
||||||
|
case SxLogLevel::Warn: return "WARN ";
|
||||||
|
case SxLogLevel::Error: return "ERROR";
|
||||||
|
case SxLogLevel::Fatal: return "FATAL";
|
||||||
|
default: return "OFF ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断 tag 是否允许输出
|
||||||
|
// 难点:
|
||||||
|
// - 精确匹配 tag 字符串
|
||||||
|
// - tag==nullptr 时默认允许,避免“无 tag 日志被误杀”
|
||||||
|
bool SxLogger::tagAllowed(const SxLogConfig& c, const char* tag)
|
||||||
|
{
|
||||||
|
if (c.tagFilterMode == SxTagFilterMode::None) return true;
|
||||||
|
if (!tag) return true;
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
for (const auto& t : c.tagList)
|
||||||
|
{
|
||||||
|
if (t == tag) { found = true; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.tagFilterMode == SxTagFilterMode::Whitelist) return found;
|
||||||
|
if (c.tagFilterMode == SxTagFilterMode::Blacklist) return !found;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快速判定是否需要输出(宏短路依赖)
|
||||||
|
// 难点:
|
||||||
|
// 1) 必须无副作用:返回 false 时调用端不会构造对象也不会拼接
|
||||||
|
// 2) 过滤维度要完整:级别、tag、sink 是否启用
|
||||||
|
// 3) 当前实现加锁保证 cfg 与 sink 状态一致;代价是高频路径会有锁开销
|
||||||
|
bool SxLogger::shouldLog(SxLogLevel level, const char* tag) const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
if (cfg.minLevel == SxLogLevel::Off) return false;
|
||||||
|
if (level < cfg.minLevel) return false;
|
||||||
|
if (!tagAllowed(cfg, tag)) return false;
|
||||||
|
if (!consoleSink && !cfg.fileEnabled) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成本地时间戳字符串
|
||||||
|
// 难点:
|
||||||
|
// - Windows 与 POSIX 的线程安全 localtime API 不同
|
||||||
|
std::string SxLogger::makeTimestampLocal()
|
||||||
|
{
|
||||||
|
using namespace std::chrono;
|
||||||
|
const auto now = system_clock::now();
|
||||||
|
const std::time_t t = system_clock::to_time_t(now);
|
||||||
|
|
||||||
|
std::tm tmv{};
|
||||||
|
#if defined(_WIN32)
|
||||||
|
localtime_s(&tmv, &t);
|
||||||
|
#else
|
||||||
|
localtime_r(&t, &tmv);
|
||||||
|
#endif
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << std::put_time(&tmv, "%Y-%m-%d %H:%M:%S");
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拼接日志前缀(调用方已持锁)
|
||||||
|
// 难点:
|
||||||
|
// - 前缀拼接必须与配置项严格对应,且尽量避免多余开销
|
||||||
|
// - showSource 会输出 (file:line func),对定位时序问题很有价值
|
||||||
|
std::string SxLogger::formatPrefixUnlocked(
|
||||||
|
const SxLogConfig& c,
|
||||||
|
SxLogLevel level,
|
||||||
|
const char* tag,
|
||||||
|
const char* file,
|
||||||
|
int line,
|
||||||
|
const char* func) const
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
|
||||||
|
if (c.showTimestamp) oss << "[" << makeTimestampLocal() << "] ";
|
||||||
|
if (c.showLevel) oss << "[" << levelToString(level) << "] ";
|
||||||
|
if (c.showTag && tag) oss << "[" << tag << "] ";
|
||||||
|
|
||||||
|
if (c.showThreadId)
|
||||||
|
{
|
||||||
|
oss << "[T:" << std::this_thread::get_id() << "] ";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.showSource && file && func)
|
||||||
|
{
|
||||||
|
oss << "(" << file << ":" << line << " " << func << ") ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一输出出口
|
||||||
|
// 难点:
|
||||||
|
// 1) 行级一致性:必须把 prefix + msg + "\n" 当作整体写入
|
||||||
|
// 2) 线程安全:持锁写入可避免不同线程日志互相穿插
|
||||||
|
// 3) 避免重入:在持锁期间不要再调用 SX_LOG...(会导致死锁)
|
||||||
|
void SxLogger::logLine(
|
||||||
|
SxLogLevel level,
|
||||||
|
const char* tag,
|
||||||
|
const char* file,
|
||||||
|
int line,
|
||||||
|
const char* func,
|
||||||
|
const std::string& msg)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
|
||||||
|
if (cfg.minLevel == SxLogLevel::Off) return;
|
||||||
|
if (level < cfg.minLevel) return;
|
||||||
|
if (!tagAllowed(cfg, tag)) return;
|
||||||
|
|
||||||
|
const std::string prefix = formatPrefixUnlocked(cfg, level, tag, file, line, func);
|
||||||
|
const std::string lineText = prefix + msg + "\n";
|
||||||
|
|
||||||
|
if (consoleSink) consoleSink->writeLine(lineText);
|
||||||
|
|
||||||
|
if (cfg.fileEnabled && fileSink && fileSink->isOpen())
|
||||||
|
{
|
||||||
|
fileSink->writeLine(lineText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.autoFlush)
|
||||||
|
{
|
||||||
|
if (consoleSink) consoleSink->flush();
|
||||||
|
if (cfg.fileEnabled && fileSink) fileSink->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- SxLogLine --------
|
||||||
|
|
||||||
|
// 构造:只记录元信息
|
||||||
|
SxLogLine::SxLogLine(SxLogLevel level, const char* tag, const char* file, int line, const char* func)
|
||||||
|
: lvl(level), tg(tag), srcFile(file), srcLine(line), srcFunc(func)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// 析构:提交输出
|
||||||
|
// 难点:
|
||||||
|
// - 这是 RAII 设计的核心:保证语句结束时日志自动落地
|
||||||
|
// - 也要求调用端不要把临时对象跨语句保存(宏用法本身也不支持那样做)
|
||||||
|
SxLogLine::~SxLogLine()
|
||||||
|
{
|
||||||
|
SxLogger::Get().logLine(lvl, tg, srcFile, srcLine, srcFunc, ss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- SxLogScope --------
|
||||||
|
|
||||||
|
// 构造:按需启用计时
|
||||||
|
// 难点:
|
||||||
|
// - 只有 shouldLog 为 true 才记录起点,避免在未输出场景做无意义计时
|
||||||
|
SxLogScope::SxLogScope(SxLogLevel level, const char* tag, const char* file, int line, const char* func, const char* name)
|
||||||
|
: lvl(level), tg(tag), srcFile(file), srcLine(line), srcFunc(func), scopeName(name)
|
||||||
|
{
|
||||||
|
enabled = SxLogger::Get().shouldLog(lvl, tg);
|
||||||
|
if (enabled) t0 = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 析构:输出耗时
|
||||||
|
// 难点:
|
||||||
|
// - steady_clock 用于衡量耗时,避免系统时间调整造成跳变
|
||||||
|
SxLogScope::~SxLogScope()
|
||||||
|
{
|
||||||
|
if (!enabled) return;
|
||||||
|
const auto t1 = std::chrono::steady_clock::now();
|
||||||
|
const auto us = std::chrono::duration_cast<std::chrono::microseconds>(t1 - t0).count();
|
||||||
|
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "SCOPE " << (scopeName ? scopeName : "") << " cost=" << us << "us";
|
||||||
|
SxLogger::Get().logLine(lvl, tg, srcFile, srcLine, srcFunc, oss.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace StellarX
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
/********************************************************************************
|
||||||
|
* @文件: SxLog.h
|
||||||
|
* @摘要: StellarX 日志系统对外接口定义(控制台/文件输出 + 级别过滤 + Tag过滤 + 中英文选择)
|
||||||
|
* @描述:
|
||||||
|
* 该日志系统采用“宏 + RAII(析构提交)”的方式实现:
|
||||||
|
* - 调用端通过 SX_LOGD/SX_LOGI... 写日志
|
||||||
|
* - 宏内部先 shouldLog 短路过滤,未命中时不构造对象、不拼接字符串
|
||||||
|
* - 命中时构造 SxLogLine,使用 operator<< 拼接内容
|
||||||
|
* - 语句结束时 SxLogLine 析构,统一提交到 SxLogger::logLine 输出
|
||||||
|
*
|
||||||
|
* 输出通道(Sink)目前提供:
|
||||||
|
* - ConsoleSink: 写入 std::cout(不走 WinAPI 调试输出通道)
|
||||||
|
* - FileSink: 写入文件,支持按字节阈值滚动
|
||||||
|
*
|
||||||
|
* @特性:
|
||||||
|
* - 日志级别:Trace/Debug/Info/Warn/Error/Fatal/Off
|
||||||
|
* - Tag 过滤:None/Whitelist/Blacklist
|
||||||
|
* - 可选前缀:时间戳/级别/Tag/线程ID/源码位置
|
||||||
|
* - 中英文选择:SX_T(zh, en) / setLanguage
|
||||||
|
* - 文件滚动:rotateBytes > 0 时按阈值滚动
|
||||||
|
*
|
||||||
|
* @使用场景:
|
||||||
|
* - 排查重绘链路、脏标记传播、Tab 切换、Table 数据刷新等时序问题
|
||||||
|
* - 输出可复现日志,配合回归验证
|
||||||
|
*
|
||||||
|
* @注意:
|
||||||
|
* - SX_T 仅做“字符串选择”,不做编码转换
|
||||||
|
* - 控制台显示是否乱码由“终端 codepage/字体/环境”决定
|
||||||
|
* - 该头文件只声明接口,实现位于 SxLog.cpp
|
||||||
|
*
|
||||||
|
* @所属框架: 星垣(StellarX) GUI框架
|
||||||
|
* @作者: 我在人间做废物
|
||||||
|
********************************************************************************/
|
||||||
|
|
||||||
|
// SxLog.h - header-only interface (implementation in SxLog.cpp)
|
||||||
|
// Pure standard library: std::cout and optional file sink.
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <ctime>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef SX_LOG_ENABLE
|
||||||
|
#define SX_LOG_ENABLE 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace StellarX
|
||||||
|
{
|
||||||
|
/* ========================= 日志级别 ========================= */
|
||||||
|
// 说明:
|
||||||
|
// - minLevel 表示最低输出级别,小于 minLevel 的日志会被 shouldLog 直接过滤
|
||||||
|
// - Off 表示全局关闭
|
||||||
|
enum class SxLogLevel : int
|
||||||
|
{
|
||||||
|
Trace = 0, // 最细粒度:高频路径追踪(谨慎开启)
|
||||||
|
Debug = 1, // 调试信息:状态变化/关键分支
|
||||||
|
Info = 2, // 业务信息:关键流程节点
|
||||||
|
Warn = 3, // 警告:非致命但异常的情况
|
||||||
|
Error = 4, // 错误:功能失败、需要关注
|
||||||
|
Fatal = 5, // 致命:通常意味着无法继续运行
|
||||||
|
Off = 6 // 关闭全部日志
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ========================= 语言选择 ========================= */
|
||||||
|
// 说明:仅用于 SX_T 选择输出哪一段文本,不做编码转换
|
||||||
|
enum class SxLogLanguage : int
|
||||||
|
{
|
||||||
|
ZhCN = 0, // 中文
|
||||||
|
EnUS = 1 // 英文
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ========================= Tag 过滤模式 ========================= */
|
||||||
|
// None : 不过滤,全部输出
|
||||||
|
// Whitelist : 只输出 tagList 中包含的 tag
|
||||||
|
// Blacklist : 输出除 tagList 以外的 tag
|
||||||
|
enum class SxTagFilterMode : int
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Whitelist = 1,
|
||||||
|
Blacklist = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ========================= 日志配置 ========================= */
|
||||||
|
// 说明:SxLogger 内部持有该配置,shouldLog 与 logLine 都依赖它
|
||||||
|
struct SxLogConfig
|
||||||
|
{
|
||||||
|
SxLogLevel minLevel = SxLogLevel::Info; // 最低输出级别
|
||||||
|
|
||||||
|
bool showTimestamp = true; // 是否输出时间戳前缀
|
||||||
|
bool showLevel = true; // 是否输出级别前缀
|
||||||
|
bool showTag = true; // 是否输出 tag 前缀
|
||||||
|
bool showThreadId = false; // 是否输出线程ID(排查并发时开启)
|
||||||
|
bool showSource = false; // 是否输出源码位置(file:line func)
|
||||||
|
bool autoFlush = true; // 每行写完是否 flush(排查问题更稳,性能略差)
|
||||||
|
|
||||||
|
SxTagFilterMode tagFilterMode = SxTagFilterMode::None; // Tag 过滤模式
|
||||||
|
std::vector<std::string> tagList; // Tag 列表(白名单/黑名单)
|
||||||
|
|
||||||
|
bool fileEnabled = false; // 文件输出是否启用(enableFile 成功才为 true)
|
||||||
|
std::string filePath; // 文件路径
|
||||||
|
bool fileAppend = true; // 是否追加写入
|
||||||
|
std::size_t rotateBytes = 0; // 滚动阈值(0 表示不滚动)
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ========================= Sink 接口 ========================= */
|
||||||
|
// 说明:
|
||||||
|
// - Sink 负责“把完整的一行日志写到某个地方”
|
||||||
|
// - SxLogger 负责过滤/格式化/分发
|
||||||
|
class ILogSink
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~ILogSink() = default;
|
||||||
|
|
||||||
|
// 返回 Sink 名称,用于调试识别(例如 "console"/"file")
|
||||||
|
virtual const char* name() const = 0;
|
||||||
|
|
||||||
|
// 写入一整行(调用方保证 line 已包含换行或按约定追加换行)
|
||||||
|
virtual void writeLine(const std::string& line) = 0;
|
||||||
|
|
||||||
|
// 刷新缓冲(可选实现)
|
||||||
|
virtual void flush() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ========================= 控制台输出 Sink ========================= */
|
||||||
|
// 作用:把日志写入指定输出流(默认用 std::cout)
|
||||||
|
class ConsoleSink : public ILogSink
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// 绑定一个输出流引用(常见用法:std::cout)
|
||||||
|
explicit ConsoleSink(std::ostream& os) : out(os) {}
|
||||||
|
|
||||||
|
const char* name() const override { return "console"; }
|
||||||
|
|
||||||
|
// 写入一行(不自动追加换行,换行由上层统一拼接)
|
||||||
|
void writeLine(const std::string& line) override { out << line; }
|
||||||
|
|
||||||
|
// 立即 flush(当 autoFlush=true 时由 SxLogger 调用)
|
||||||
|
void flush() override { out.flush(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::ostream& out; // 输出流引用(不负责生命周期)
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ========================= 文件输出 Sink ========================= */
|
||||||
|
// 作用:把日志写入文件,支持按字节阈值滚动
|
||||||
|
class FileSink : public ILogSink
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FileSink() = default;
|
||||||
|
|
||||||
|
const char* name() const override { return "file"; }
|
||||||
|
|
||||||
|
// 打开文件
|
||||||
|
// path : 文件路径
|
||||||
|
// append : true 追加写;false 清空重写
|
||||||
|
bool open(const std::string& path, bool append);
|
||||||
|
|
||||||
|
// 关闭文件(安全可重复调用)
|
||||||
|
void close();
|
||||||
|
|
||||||
|
// 查询文件是否处于打开状态
|
||||||
|
bool isOpen() const;
|
||||||
|
|
||||||
|
// 设置滚动阈值(字节)
|
||||||
|
// bytes = 0 表示不滚动
|
||||||
|
void setRotateBytes(std::size_t bytes) { rotateBytes = bytes; }
|
||||||
|
|
||||||
|
// 写入一行,并在需要时触发滚动
|
||||||
|
void writeLine(const std::string& line) override;
|
||||||
|
|
||||||
|
// flush 文件缓冲
|
||||||
|
void flush() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// 检查并执行滚动
|
||||||
|
// 返回值:是否发生滚动(或是否重新打开)
|
||||||
|
bool rotateIfNeeded();
|
||||||
|
|
||||||
|
std::ofstream ofs; // 文件输出流
|
||||||
|
std::string filePath; // 当前文件路径
|
||||||
|
bool appendMode = true; // 是否追加模式(用于 reopen)
|
||||||
|
std::size_t rotateBytes = 0; // 滚动阈值
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ========================= 日志中心 SxLogger ========================= */
|
||||||
|
// 作用:
|
||||||
|
// - 保存配置(SxLogConfig)
|
||||||
|
// - 过滤(level/tag/sink enabled)
|
||||||
|
// - 格式化前缀(时间/级别/tag/线程/源码位置)
|
||||||
|
// - 分发到 console/file 等 sink
|
||||||
|
class SxLogger
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// 仅用于 Windows 控制台:把 codepage 切到 GBK,解决中文乱码。
|
||||||
|
// 不使用 WinAPI:内部通过 system("chcp 936") 实现
|
||||||
|
// 注意:这只影响终端解释输出字节的方式,不影响源码文件编码
|
||||||
|
static void setGBK();
|
||||||
|
|
||||||
|
// 获取全局单例
|
||||||
|
// 说明:函数内静态对象,C++11 起保证线程安全初始化
|
||||||
|
static SxLogger& Get();
|
||||||
|
|
||||||
|
// 设置最低输出级别
|
||||||
|
void setMinLevel(SxLogLevel level);
|
||||||
|
|
||||||
|
// 获取最低输出级别
|
||||||
|
SxLogLevel getMinLevel() const;
|
||||||
|
|
||||||
|
// 设置语言(用于 SX_T 选择)
|
||||||
|
void setLanguage(SxLogLanguage lang);
|
||||||
|
|
||||||
|
// 获取当前语言
|
||||||
|
SxLogLanguage getLanguage() const;
|
||||||
|
|
||||||
|
// 设置 Tag 过滤
|
||||||
|
// mode: None/Whitelist/Blacklist
|
||||||
|
// tags: 过滤列表(精确匹配)
|
||||||
|
void setTagFilter(SxTagFilterMode mode, const std::vector<std::string>& tags);
|
||||||
|
|
||||||
|
// 清空 Tag 过滤(恢复 None)
|
||||||
|
void clearTagFilter();
|
||||||
|
|
||||||
|
// 开关控制台输出
|
||||||
|
void enableConsole(bool enable);
|
||||||
|
|
||||||
|
// 开启文件输出
|
||||||
|
// path : 文件路径
|
||||||
|
// append : 追加写/清空写
|
||||||
|
// rotateBytes: 滚动阈值(0 不滚动)
|
||||||
|
// 返回值:是否打开成功
|
||||||
|
bool enableFile(const std::string& path, bool append = true, std::size_t rotateBytes = 0);
|
||||||
|
|
||||||
|
// 关闭文件输出(不影响控制台输出)
|
||||||
|
void disableFile();
|
||||||
|
|
||||||
|
// 快速判定是否需要输出(宏层面的短路依赖它)
|
||||||
|
// 说明:
|
||||||
|
// - shouldLog 一定要“副作用为 0”
|
||||||
|
// - 若返回 false,调用端不会创建 SxLogLine,也不会拼接字符串
|
||||||
|
bool shouldLog(SxLogLevel level, const char* tag) const;
|
||||||
|
|
||||||
|
// 输出一条完整日志
|
||||||
|
// 说明:这是统一出口,SxLogLine 析构最终会走到这里
|
||||||
|
void logLine(
|
||||||
|
SxLogLevel level,
|
||||||
|
const char* tag,
|
||||||
|
const char* file,
|
||||||
|
int line,
|
||||||
|
const char* func,
|
||||||
|
const std::string& msg);
|
||||||
|
|
||||||
|
// 获取配置副本(避免外部直接改内部 cfg)
|
||||||
|
SxLogConfig getConfigCopy() const;
|
||||||
|
|
||||||
|
// 批量设置配置(整体替换)
|
||||||
|
void setConfig(const SxLogConfig& cfg);
|
||||||
|
|
||||||
|
// 工具:把级别转为字符串(用于前缀)
|
||||||
|
static const char* levelToString(SxLogLevel level);
|
||||||
|
|
||||||
|
// 工具:生成本地时间戳字符串(用于前缀与文件滚动名)
|
||||||
|
static std::string makeTimestampLocal();
|
||||||
|
|
||||||
|
private:
|
||||||
|
SxLogger();
|
||||||
|
|
||||||
|
// 判断 tag 是否允许输出(根据 Tag 过滤模式与 tagList)
|
||||||
|
static bool tagAllowed(const SxLogConfig& cfg, const char* tag);
|
||||||
|
|
||||||
|
// 生成前缀(调用方需已持有锁)
|
||||||
|
std::string formatPrefixUnlocked(
|
||||||
|
const SxLogConfig& cfg,
|
||||||
|
SxLogLevel level,
|
||||||
|
const char* tag,
|
||||||
|
const char* file,
|
||||||
|
int line,
|
||||||
|
const char* func) const;
|
||||||
|
|
||||||
|
mutable std::mutex mtx; // 保护 cfg 与 sink 写入,确保多线程行级一致性
|
||||||
|
SxLogConfig cfg; // 当前配置
|
||||||
|
std::atomic<SxLogLanguage> lang; // 语言开关(仅影响 SX_T 选择)
|
||||||
|
|
||||||
|
std::unique_ptr<ConsoleSink> consoleSink; // 控制台 sink(enableConsole 控制)
|
||||||
|
std::unique_ptr<FileSink> fileSink; // 文件 sink(enableFile 控制)
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ========================= 双语选择辅助 ========================= */
|
||||||
|
// 说明:
|
||||||
|
// - 只做“选择 zhCN 或 enUS”,不做编码转换
|
||||||
|
// - 输出显示是否正常由终端环境决定
|
||||||
|
inline const char* SxT(const char* zhCN, const char* enUS)
|
||||||
|
{
|
||||||
|
return (SxLogger::Get().getLanguage() == SxLogLanguage::ZhCN) ? zhCN : enUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__cpp_char8_t) && (__cpp_char8_t >= 201811L)
|
||||||
|
// 说明:
|
||||||
|
// - C++20 的 u8"xxx" 是 char8_t*,为了兼容调用端,这里提供重载
|
||||||
|
// - reinterpret_cast 只是改指针类型,不做 UTF-8 -> GBK 转码
|
||||||
|
inline const char* SxT(const char8_t* zhCN, const char* enUS)
|
||||||
|
{
|
||||||
|
return (SxLogger::Get().getLanguage() == SxLogLanguage::ZhCN)
|
||||||
|
? reinterpret_cast<const char*>(zhCN)
|
||||||
|
: enUS;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ========================= RAII 日志行对象 ========================= */
|
||||||
|
// 作用:
|
||||||
|
// - 构造时记录 level/tag/源码位置
|
||||||
|
// - operator<< 拼接内容
|
||||||
|
// - 析构时统一提交给 SxLogger::logLine 输出
|
||||||
|
//
|
||||||
|
// 设计意义:
|
||||||
|
// - 避免调用端忘记写换行
|
||||||
|
// - 保证一行日志作为整体写出
|
||||||
|
class SxLogLine
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// 构造:记录元信息(不输出)
|
||||||
|
SxLogLine(SxLogLevel level, const char* tag, const char* file, int line, const char* func);
|
||||||
|
|
||||||
|
// 析构:提交输出(真正写出发生在这里)
|
||||||
|
~SxLogLine();
|
||||||
|
|
||||||
|
// 拼接内容(流式写法)
|
||||||
|
template<typename T>
|
||||||
|
SxLogLine& operator<<(const T& v)
|
||||||
|
{
|
||||||
|
ss << v;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
SxLogLevel lvl; // 日志级别
|
||||||
|
const char* tg; // Tag(不拥有内存)
|
||||||
|
const char* srcFile; // 源文件名(来自 __FILE__)
|
||||||
|
int srcLine; // 行号(来自 __LINE__)
|
||||||
|
const char* srcFunc; // 函数名(来自 __func__)
|
||||||
|
std::ostringstream ss; // 内容拼接缓冲
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ========================= RAII 作用域计时对象 ========================= */
|
||||||
|
// 作用:
|
||||||
|
// - 仅在 shouldLog(Trace, tag) 为 true 时启用计时
|
||||||
|
// - 析构时输出耗时(微秒)
|
||||||
|
//
|
||||||
|
// 使用建议:
|
||||||
|
// - 只在需要定位性能瓶颈时开启 Trace
|
||||||
|
// - name 建议传入常量字符串,便于检索
|
||||||
|
class SxLogScope
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// 构造:根据 shouldLog 决定是否启用计时
|
||||||
|
SxLogScope(SxLogLevel level, const char* tag, const char* file, int line, const char* func, const char* name);
|
||||||
|
|
||||||
|
// 析构:若启用则输出耗时
|
||||||
|
~SxLogScope();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool enabled = false; // 是否启用(未启用则析构无输出)
|
||||||
|
SxLogLevel lvl = SxLogLevel::Trace; // 级别(通常用 Trace)
|
||||||
|
const char* tg = nullptr; // Tag
|
||||||
|
const char* srcFile = nullptr; // 源文件
|
||||||
|
int srcLine = 0; // 行号
|
||||||
|
const char* srcFunc = nullptr; // 函数
|
||||||
|
const char* scopeName = nullptr; // 作用域名
|
||||||
|
std::chrono::steady_clock::time_point t0; // 起始时间点
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace StellarX
|
||||||
|
|
||||||
|
#if SX_LOG_ENABLE
|
||||||
|
|
||||||
|
// SX_T:双语选择宏,调用 SxT 根据当前语言选择输出
|
||||||
|
#define SX_T(zh, en) ::StellarX::SxT(zh, en)
|
||||||
|
|
||||||
|
// 日志宏说明:
|
||||||
|
// 1) 先 shouldLog 短路过滤,未命中则不会构造 SxLogLine,也不会执行 else 分支的表达式
|
||||||
|
// 2) 命中则构造临时 SxLogLine,并允许继续使用 operator<< 拼接
|
||||||
|
// 3) 语句结束时临时对象析构,触发真正输出
|
||||||
|
#define SX_LOG_TRACE(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Trace, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Trace, tag, __FILE__, __LINE__, __func__)
|
||||||
|
#define SX_LOGD(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Debug, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Debug, tag, __FILE__, __LINE__, __func__)
|
||||||
|
#define SX_LOGI(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Info, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Info, tag, __FILE__, __LINE__, __func__)
|
||||||
|
#define SX_LOGW(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Warn, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Warn, tag, __FILE__, __LINE__, __func__)
|
||||||
|
#define SX_LOGE(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Error, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Error, tag, __FILE__, __LINE__, __func__)
|
||||||
|
#define SX_LOGF(tag) if(!::StellarX::SxLogger::Get().shouldLog(::StellarX::SxLogLevel::Fatal, tag)) ; else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Fatal, tag, __FILE__, __LINE__, __func__)
|
||||||
|
|
||||||
|
// 作用域耗时统计宏:默认用 Trace 级别
|
||||||
|
#define SX_TRACE_SCOPE(tag, nameLiteral) ::StellarX::SxLogScope sx_scope_##__LINE__(::StellarX::SxLogLevel::Trace, tag, __FILE__, __LINE__, __func__, nameLiteral)
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
// 关闭日志时的兼容宏:保证调用端代码不需要改动
|
||||||
|
#define SX_T(zh, en) (en)
|
||||||
|
#define SX_LOG_TRACE(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
|
||||||
|
#define SX_LOGD(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
|
||||||
|
#define SX_LOGI(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
|
||||||
|
#define SX_LOGW(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
|
||||||
|
#define SX_LOGE(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
|
||||||
|
#define SX_LOGF(tag) if(true) {} else ::StellarX::SxLogLine(::StellarX::SxLogLevel::Off, tag, "", 0, "")
|
||||||
|
#define SX_TRACE_SCOPE(tag, nameLiteral) do {} while(0)
|
||||||
|
|
||||||
|
#endif
|
||||||
+431
@@ -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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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("新增Data:id=", "appendRow: id=") << id
|
||||||
|
<< SX_T(" 本行列数=", " cols=") << (int)data.size()
|
||||||
|
<< SX_T(" 数据总行数=", " totalRows=") << (int)this->data.size()
|
||||||
|
<< SX_T(" 总页数=", " totalPages=") << totalPages;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Table::setData(std::initializer_list<std::vector<std::string>> data)
|
||||||
|
{
|
||||||
|
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("新增Data:id=", "appendRow: id=") << id
|
||||||
|
<< SX_T(" 本行列数=", " cols=") << (int)data.size()
|
||||||
|
<< SX_T(" 数据总行数=", " totalRows=") << (int)this->data.size()
|
||||||
|
<< SX_T(" 总页数=", " totalPages=") << totalPages;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void Table::setRowsPerPage(int rows)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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 // 按钮垂直 padding(initButton)
|
||||||
|
#define TABLE_BTN_TEXT_PAD_V 8 // 计算页脚高度时的按钮文字垂直 padding(initTextWaH)
|
||||||
|
#define TABLE_FOOTER_PAD 16 // 页脚额外高度(底部留白)
|
||||||
|
#define TABLE_FOOTER_BLANK 8 // 页脚顶部留白
|
||||||
|
#define TABLE_PAGE_TEXT_OFFSET_X (-40) // 页码文本的临时水平修正
|
||||||
|
|
||||||
|
// === Table defaults (theme) ===
|
||||||
|
#define TABLE_DEFAULT_ROWS_PER_PAGE 5
|
||||||
|
#define TABLE_DEFAULT_BORDER_WIDTH 1
|
||||||
|
#define TABLE_DEFAULT_BORDER_COLOR RGB(0,0,0)
|
||||||
|
#define TABLE_DEFAULT_BG_COLOR RGB(255,255,255)
|
||||||
|
|
||||||
|
// === Strings (i18n ready) ===
|
||||||
|
#define TABLE_STR_PREV "上一页"
|
||||||
|
#define TABLE_STR_NEXT "下一页"
|
||||||
|
#define TABLE_STR_PAGE_PREFIX "第"
|
||||||
|
#define TABLE_STR_PAGE_MID "页/共"
|
||||||
|
#define TABLE_STR_PAGE_SUFFIX "页"
|
||||||
|
|
||||||
|
class Table :public Control
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::vector<std::vector<std::string>> data; // 表格数据
|
||||||
|
std::vector<std::string> headers; // 表格表头
|
||||||
|
std::string pageNumtext = "页码标签"; // 页码标签文本
|
||||||
|
|
||||||
|
int tableBorderWidth = TABLE_DEFAULT_BORDER_WIDTH; // 边框宽度
|
||||||
|
|
||||||
|
std::vector<int> colWidths; // 每列的宽度
|
||||||
|
std::vector<int> lineHeights; // 每行的高度
|
||||||
|
|
||||||
|
int rowsPerPage = TABLE_DEFAULT_ROWS_PER_PAGE; // 每页显示的行数
|
||||||
|
int currentPage = 1; // 当前页码
|
||||||
|
int totalPages = 1; // 总页数
|
||||||
|
|
||||||
|
bool isShowPageButton = true; // 是否显示翻页按钮
|
||||||
|
bool isNeedDrawHeaders = true; // 是否需要绘制表头(暂时废弃,单做保留,后期优化可能用到)
|
||||||
|
bool isNeedCellSize = true; // 是否需要计算单元格尺寸
|
||||||
|
bool isNeedButtonAndPageNum = true; // 是否需要计算翻页按钮和页码信息
|
||||||
|
|
||||||
|
std::unique_ptr<Button> prevButton; // 上一页按钮
|
||||||
|
std::unique_ptr<Button> nextButton; // 下一页按钮
|
||||||
|
std::unique_ptr<Label> pageNum; //页码文本
|
||||||
|
|
||||||
|
int dX = x, dY = y; // 单元格的开始坐标
|
||||||
|
int uX = x, uY = y; // 单元格的结束坐标
|
||||||
|
|
||||||
|
int pX = 0; //标签左上角坐标
|
||||||
|
int pY = 0; //标签左上角坐标
|
||||||
|
|
||||||
|
StellarX::FillMode tableFillMode = StellarX::FillMode::Solid; //填充模式
|
||||||
|
StellarX::LineStyle tableLineStyle = StellarX::LineStyle::Solid; // 线型
|
||||||
|
COLORREF tableBorderClor = TABLE_DEFAULT_BORDER_COLOR; // 表格边框颜色
|
||||||
|
COLORREF tableBkClor = TABLE_DEFAULT_BG_COLOR; // 表格背景颜色
|
||||||
|
|
||||||
|
void initTextWaH(); //初始化文本像素宽度和高度
|
||||||
|
void initButton(); //初始化翻页按钮
|
||||||
|
void initPageNum(); //初始化页码标签
|
||||||
|
|
||||||
|
void drawTable(); //绘制当前页
|
||||||
|
void drawHeader(); //绘制表头
|
||||||
|
void drawPageNum(); //绘制页码信息
|
||||||
|
void drawButton(); //绘制翻页按钮
|
||||||
|
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
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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 窗口与子类化过程;应用可拉伸样式;清屏并批量绘制。
|
||||||
|
* 关键步骤:
|
||||||
|
* 1)initgraph 拿到 hWnd;
|
||||||
|
* 2)SetWindowLongPtr 子类化到 WndProcThunk(只做一次);
|
||||||
|
* 3)ApplyResizableStyle 设置 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("执行 pumpResizeIfNeeded:needResizeDirty=",
|
||||||
|
"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();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
+711
@@ -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→LSB:31..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
|
||||||
Reference in New Issue
Block a user