diff --git a/include/StellarX/Button.h b/include/StellarX/Button.h index 906748f..3774257 100644 --- a/include/StellarX/Button.h +++ b/include/StellarX/Button.h @@ -22,9 +22,9 @@ #include"label.h" #define DISABLEDCOLOUR RGB(96, 96, 96) //禁用状态颜色 -#define TEXTMARGINS_X 6 +#define TEXTMARGINS_X 6 #define TEXTMARGINS_Y 4 -constexpr int bordWith = 1; //边框宽度,用于快照恢复时的偏移计算 +constexpr int bordWith = 1; //边框宽度,用于快照恢复时的偏移计算 constexpr int bordHeight = 1; //边框高度,用于快照恢复时的偏移计算 class Button : public Control @@ -39,16 +39,16 @@ class Button : public Control int padX = TEXTMARGINS_X; // 文本最小左右内边距 int padY = TEXTMARGINS_Y; // 文本最小上下内边距 - COLORREF buttonTrueColor; // 按钮被点击后的颜色 - COLORREF buttonFalseColor; // 按钮未被点击的颜色 - COLORREF buttonHoverColor; // 按钮被鼠标悬停的颜色 + COLORREF buttonTrueColor; // 按钮被点击后的颜色 + COLORREF buttonFalseColor; // 按钮未被点击的颜色 + COLORREF buttonHoverColor; // 按钮被鼠标悬停的颜色 COLORREF buttonBorderColor = RGB(0, 0, 0);// 按钮边框颜色 - StellarX::ButtonMode mode; // 按钮模式 + StellarX::ButtonMode mode; // 按钮模式 StellarX::ControlShape shape; // 按钮形状 - StellarX::FillMode buttonFillMode = StellarX::FillMode::Solid; //按钮填充模式 - StellarX::FillStyle buttonFillIma = StellarX::FillStyle::BDiagonal; //按钮填充图案 + StellarX::FillMode buttonFillMode = StellarX::FillMode::Solid; //按钮填充模式 + StellarX::FillStyle buttonFillIma = StellarX::FillStyle::BDiagonal; //按钮填充图案 IMAGE* buttonFileIMAGE = nullptr; //按钮填充图像 std::function onClickCallback; //回调函数 @@ -63,20 +63,20 @@ class Button : public Control // === Tooltip === bool tipEnabled = false; // 是否启用 - bool tipVisible = false; // 当前是否显示 + bool tipVisible = false; // 当前是否显示 bool tipFollowCursor = false; // 是否跟随鼠标 - bool tipUserOverride = false; // 是否用户自定义了tip文本 + bool tipUserOverride = false; // 是否用户自定义了tip文本 int tipDelayMs = 1000; // 延时(毫秒) - int tipOffsetX = 12; // 相对鼠标偏移 + int tipOffsetX = 12; // 相对鼠标偏移 int tipOffsetY = 18; - ULONGLONG tipHoverTick = 0; // 开始悬停的时间戳 - int lastMouseX = 0; // 最新鼠标位置(用于定位) + ULONGLONG tipHoverTick = 0; // 开始悬停的时间戳 + int lastMouseX = 0; // 最新鼠标位置(用于定位) int lastMouseY = 0; - std::string tipTextClick; //NORMAL 模式下用 + std::string tipTextClick; // NORMAL 模式下用 std::string tipTextOn; // click==true 时用 std::string tipTextOff; // click==false 时用 - Label tipLabel; // 直接复用Label作为提示 + Label tipLabel; // 直接复用Label作为提示 public: StellarX::ControlText textStyle; // 按钮文字样式 @@ -178,6 +178,7 @@ private: bool isMouseInEllipse(int mouseX, int mouseY, int x, int y, int width, int height); //获取对话框类型 bool model() const override { return false; } + //文本截断 void cutButtonText(); // 统一隐藏&恢复背景 void hideTooltip(); diff --git a/include/StellarX/CoreTypes.h b/include/StellarX/CoreTypes.h index dd2f1ac..3225ecb 100644 --- a/include/StellarX/CoreTypes.h +++ b/include/StellarX/CoreTypes.h @@ -195,7 +195,7 @@ namespace StellarX /** * @枚举类名称: TextBoxmode - * @功能描述: 定义了文本框的两种模式 + * @功能描述: 定义了文本框的三种模式 * * @详细说明: * 需要限制文本框是否接受用户输入时使用 @@ -203,12 +203,13 @@ namespace StellarX * @取值说明: * INPUT_MODE, // 用户可输入模式 * READONLY_MODE // 只读模式 + * PASSWORD_MODE // 密码模式 */ enum class TextBoxmode { - INPUT_MODE, // 用户可输入模式 + INPUT_MODE, // 用户可输入模式 READONLY_MODE, // 只读模式 - PASSWORD_MODE// 密码模式 + PASSWORD_MODE // 密码模式 }; /** diff --git a/include/StellarX/StellarX.h b/include/StellarX/StellarX.h index bb98192..d0d9b60 100644 --- a/include/StellarX/StellarX.h +++ b/include/StellarX/StellarX.h @@ -1,7 +1,7 @@ /******************************************************************************* * @文件: StellarX.h * @摘要: 星垣(StellarX) GUI框架 - 主包含头文件 - * @版本: v3.0.0 + * @版本: v3.0.1 * @描述: * 一个为Windows平台打造的轻量级、模块化C++ GUI框架。 * 基于EasyX图形库,提供简洁易用的API和丰富的控件。 @@ -12,7 +12,8 @@ * @作者: 我在人间做废物 * @邮箱: [3150131407@qq.com] | [ysm3150131407@gmail.com] * @官网:https://stellarx-gui.top/ - * @仓库: [https://github.com/Ysm-04/StellarX] + * @仓库: https://github.com/Ysm-04/StellarX + * @博客:https://blog.stellarx-gui.top/ * * @许可证: MIT License * @版权: Copyright (c) 2025 我在人间做废物 diff --git a/include/StellarX/TabControl.h b/include/StellarX/TabControl.h index 65bd055..2413f42 100644 --- a/include/StellarX/TabControl.h +++ b/include/StellarX/TabControl.h @@ -21,13 +21,13 @@ #include "CoreTypes.h" #include "Button.h" #include "Canvas.h" -#define BUTMINHEIGHT 15 -#define BUTMINWIDTH 30 +#define BUTMINHEIGHT 15 //页签按钮最小尺寸,过小会导致显示问题 +#define BUTMINWIDTH 30 //页签按钮最小尺寸,过小会导致显示问题 class TabControl :public Canvas { - int tabBarHeight = BUTMINWIDTH; //页签栏高度 + int tabBarHeight = BUTMINWIDTH; //页签栏高度 bool IsFirstDraw = true; //首次绘制标记 - int defaultActivation = -1; //默认激活页签索引 + int defaultActivation = -1; //默认激活页签索引 StellarX::TabPlacement tabPlacement = StellarX::TabPlacement::Top; //页签排列方式 std::vector, std::unique_ptr>> controls; //页签/页列表 @@ -35,6 +35,7 @@ private: using Canvas::addControl; // 禁止外部误用 void addControl(std::unique_ptr) = delete; // 精准禁用该重载 private: + // 初始化页签按钮位置和尺寸 inline void initTabBar(); inline void initTabPage(); public: @@ -42,6 +43,7 @@ public: TabControl(int x, int y, int width, int height); ~TabControl(); + //重写位置设置以适应页签和页面布局 void setX(int x)override; void setY(int y)override; @@ -67,6 +69,8 @@ public: int count() const; //通过页签文本返回索引 int indexOf(const std::string& tabText) const; + //设置脏区并请求重绘 void setDirty(bool dirty) override; + //请求父控件重绘 void requestRepaint(Control* parent)override; }; diff --git a/include/StellarX/window.h b/include/StellarX/window.h index 83993b9..522b87c 100644 --- a/include/StellarX/window.h +++ b/include/StellarX/window.h @@ -12,7 +12,9 @@ * - WM_GETMINMAXINFO:按最小“客户区”换算到“窗口矩形”,提供系统层最小轨迹值。 * - runEventLoop:只记录 WM_SIZE 的新尺寸;真正绘制放在 needResizeDirty 时集中处理。 */ - //fuck windows fuck win32 + //fuck windows + //fuck win32 + //fuck xiaomi #pragma once #include "Control.h" @@ -55,6 +57,7 @@ class Window public: bool dialogClose = false; // 项目内使用的状态位,对话框关闭标志 + mutable bool dialogOpen = false; // 项目内使用的状态位,对话框打开标志 // —— 构造/析构 ——(仅初始化成员;实际样式与子类化在 draw() 中完成) Window(int width, int height, int mode); diff --git a/src/Button.cpp b/src/Button.cpp index c45fe14..b0c1d15 100644 --- a/src/Button.cpp +++ b/src/Button.cpp @@ -313,6 +313,7 @@ bool Button::handleEvent(const ExMessage& msg) if (hover != oldHover) { SX_LOGD("Button") << SX_T("悬停变化: ","hover change: ") << "id=" << id + << " text= " << text << " " << (oldHover ? 1 : 0) << "->" << (hover ? 1 : 0); } // 处理鼠标点击事件 @@ -321,7 +322,7 @@ bool Button::handleEvent(const ExMessage& msg) if (mode == StellarX::ButtonMode::NORMAL) { click = true; - SX_LOGD("Button") << SX_T("被点击: ","lbtn - down:")<< "id = " << id << " mode = " << (int)mode; + SX_LOGD("Button") << SX_T("被点击: ","lbtn - down:")<< "id = " << id <<" text = "<setOnClickListener([this]() { this->SetResult(StellarX::MessageBoxResult::OK); + this->hWnd.dialogClose = true; this->Close(); }); okbutton->textStyle = this->textStyle; @@ -540,6 +543,8 @@ void Dialog::getTextSize() 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())); @@ -549,6 +554,7 @@ void Dialog::getTextSize() if (this->textWidth < w) this->textWidth = w; } + restoreStyle(); } // 计算逻辑:对话框宽度取【文本区域最大宽度】和【按钮区域总宽度】中的较大值。 @@ -651,7 +657,6 @@ void Dialog::performDelayedCleanup() // 所有普通控件 for (auto& c : hWnd.getControls()) c->draw(); // 其他对话框(this 已经 show=false,会早退不绘) - // 注意:此处若有容器管理,需要按你的现状遍历 dialogs 再 draw EndBatchDraw(); FlushBatchDraw(); } diff --git a/src/SxLog.cpp b/src/SxLog.cpp index 7475751..cd27b3e 100644 --- a/src/SxLog.cpp +++ b/src/SxLog.cpp @@ -117,15 +117,12 @@ namespace StellarX // - 只影响终端解释输出字节的方式,不影响源码文件编码 // - 使用 once_flag 避免重复 system 调用造成噪声与性能浪费 // - // 注意: - // - 下面原注释写“切到 UTF-8”,但实际命令是 chcp 936(GBK) - // - 为避免改动你原注释,这里补充说明事实,保持行为不变 void SxLogger::setGBK() { #ifdef _WIN32 static std::once_flag once; std::call_once(once, []() { - // 切到 UTF-8,避免中文日志在 CP936 控制台下乱码 + // 切到chcp 936(GBK),避免中文日志在 CP936 控制台下乱码 // 说明:这不是 WinAPI;是执行系统命令 std::system("chcp 936 >nul"); diff --git a/src/TabControl.cpp b/src/TabControl.cpp index ef605c7..144b142 100644 --- a/src/TabControl.cpp +++ b/src/TabControl.cpp @@ -368,7 +368,7 @@ int TabControl::getActiveIndex() const if (c.first->isClicked()) return idx; } - return idx; + return -1; } void TabControl::setActiveIndex(int idx) diff --git a/src/textBox.cpp b/src/textBox.cpp index c182bb2..0f5de89 100644 --- a/src/textBox.cpp +++ b/src/textBox.cpp @@ -29,19 +29,44 @@ void TextBox::draw() 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 += '*'; - text_width = textwidth(LPCTSTR(pwdText.c_str())); - text_height = textheight(LPCTSTR(pwdText.c_str())); + displayText = pwdText; } else { - text_width = textwidth(LPCTSTR(text.c_str())); - text_height = textheight(LPCTSTR(text.c_str())); + 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); // 恢复背景(清除旧内容) @@ -51,23 +76,19 @@ void TextBox::draw() { case StellarX::ControlShape::RECTANGLE: fillrectangle(x, y, x + width, y + height);//有边框填充矩形 - StellarX::TextBoxmode::PASSWORD_MODE == mode ? outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(pwdText.c_str())) - : outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str())); + 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);//无边框填充矩形 - StellarX::TextBoxmode::PASSWORD_MODE == mode ? outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(pwdText.c_str())) - : outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str())); + 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);//有边框填充圆角矩形 - StellarX::TextBoxmode::PASSWORD_MODE == mode ? outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(pwdText.c_str())) - :outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str())); + 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);//无边框填充圆角矩形 - StellarX::TextBoxmode::PASSWORD_MODE == mode ? outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(pwdText.c_str())) - :outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(text.c_str())); + outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str())); break; } restoreStyle(); @@ -197,10 +218,24 @@ void TextBox::setTextBoxBk(COLORREF color) 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; + this->dirty = true; // 标记需要重绘,不论是否窗口图形上下文是否已初始化,等第一次绘制时由窗口真正调用 draw() 来重绘显示文本 + + //有父控件时请求父控件重绘,无父控件时直接重绘,确保文本更新后界面正确刷新显示 + if (nullptr != parent) + { + //通过hasSnap是否持有有效快照,判断控件是否已经绘制过,避免在控件未绘制前/窗口图形上下文未初始化调用draw()导致的错误 + if (hasSnap) + requestRepaint(parent); + } + else + if (hasSnap) + draw(); + } std::string TextBox::getText() const diff --git a/src/window.cpp b/src/window.cpp index 9fc1313..6e371f9 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -193,9 +193,10 @@ LRESULT CALLBACK Window::WndProcThunk(HWND h, UINT m, WPARAM w, LPARAM l) return TRUE; } - ApplyMinSizeOnSizing(prc, w, h, self->minClientW, self->minClientH); RECT before = *prc;// 记录调整前矩形以便日志输出 - if (before.left != prc->left || before.top != prc->top || before.right != prc->right || before.bottom != prc->bottom) + 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: ") @@ -372,7 +373,8 @@ int Window::runEventLoop() while (running) { - bool consume = false; + bool consume = false; // 事件是否被消费的标志(用于输入事件分发) + bool redrawDialogs = false; // 是否需要重绘对话框(控件事件可能引起对话框状态变化) if (peekmessage(&msg, EX_MOUSE | EX_KEY | EX_WINDOW, true)) { @@ -422,9 +424,7 @@ int Window::runEventLoop() { 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"); @@ -433,44 +433,66 @@ int Window::runEventLoop() } if (!consume) { - for (auto& c : controls) + for (auto it = controls.rbegin(); it != controls.rend(); ++it) { - consume = c->handleEvent(msg); + consume = (*it)->handleEvent(msg); if (consume) { - SX_LOGD("Event") << SX_T("事件被控件处理 id=","Event consumed by control id=") << c->getId(); + 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; - for (auto& d : dialogs) + if(dialogOpen) { - needredraw = d->IsVisible(); - if (needredraw)break; + for (auto& d : dialogs) + { + needredraw = d->IsVisible(); + if (needredraw)break; + } } if (needredraw || dialogClose) { - // 对话框关闭后,需要手动合成一个鼠标移动消息并分发给所有普通控件, - // 以便它们能及时更新悬停状态(hover),否则悬停状态可能保持错误状态。 - // 先把当前鼠标位置转换为客户区坐标,并合成一次 WM_MOUSEMOVE,先分发给控件更新 hover 状态 - POINT pt; - if (GetCursorPos(&pt)) + if (dialogClose) { - ScreenToClient(this->hWnd, &pt); - ExMessage mm; - mm.message = WM_MOUSEMOVE; - mm.x = (short)pt.x; - mm.y = (short)pt.y; - // 只分发给 window 层控件(因为 dialog 已经关闭或即将关闭) - for (auto& c : controls) - c->handleEvent(mm); + // 对话框关闭后,需要手动合成一个鼠标移动消息并分发给所有普通控件, + // 以便它们能及时更新悬停状态(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(); @@ -483,6 +505,8 @@ int Window::runEventLoop() } EndBatchDraw(); needredraw = false; + dialogOpen = false; + } // —— 统一收口(needResizeDirty 为真时执行一次性重绘)—— if (needResizeDirty) @@ -655,7 +679,10 @@ bool Window::hasNonModalDialogWithCaption(const std::string& caption, const std: if (!dptr) continue; if (auto* d = dynamic_cast(dptr.get())) if (d->IsVisible() && !d->model() && d->GetCaption() == caption && d->GetText() == message) + { + dialogOpen = true; return true; + } } return false; } @@ -853,4 +880,5 @@ void Window::adaptiveLayout(std::unique_ptr& c, const int finalH, const } } c->onWindowResize(); -} \ No newline at end of file +} +