9155a86a8a
收口 Dialog/overlay 后鼠标状态同步、Tooltip 临时 coverage 与持久 coverage 拆分、跨 root 脏区补提交、TextBox/Button 绘制副作用修复,并补充 KEY6 回归用例和 BUG/Fix/Feature 开发记录。
345 lines
9.5 KiB
C++
345 lines
9.5 KiB
C++
// TextBox.cpp
|
|
#include "TextBox.h"
|
|
#include "SxLog.h"
|
|
|
|
namespace
|
|
{
|
|
int SxTextBoxMbcCharLen(const std::string& s, size_t i)
|
|
{
|
|
unsigned char b = static_cast<unsigned char>(s[i]);
|
|
if (b <= 0x7F)
|
|
return 1;
|
|
|
|
if (b >= 0x81 && b <= 0xFE && i + 1 < s.size())
|
|
{
|
|
unsigned char b2 = static_cast<unsigned char>(s[i + 1]);
|
|
if (b2 >= 0x40 && b2 <= 0xFE && b2 != 0x7F)
|
|
return 2;
|
|
}
|
|
|
|
// 非法或不完整字节按单字节容错,避免死循环。
|
|
return 1;
|
|
}
|
|
|
|
void SxTextBoxTrimTrailingSpaces(std::string& s)
|
|
{
|
|
while (!s.empty() && s.back() == ' ')
|
|
s.pop_back();
|
|
|
|
while (s.size() >= 2)
|
|
{
|
|
unsigned char a = static_cast<unsigned char>(s[s.size() - 2]);
|
|
unsigned char b = static_cast<unsigned char>(s[s.size() - 1]);
|
|
if (a == 0xA1 && b == 0xA1)
|
|
s.resize(s.size() - 2);
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
|
|
std::string SxTextBoxTruncateByMbcBoundary(const std::string& text, size_t maxBytes)
|
|
{
|
|
size_t i = 0;
|
|
size_t lastSafe = 0;
|
|
while (i < text.size())
|
|
{
|
|
const int charLen = SxTextBoxMbcCharLen(text, i);
|
|
const size_t next = i + static_cast<size_t>(charLen);
|
|
if (next > maxBytes || next > text.size())
|
|
break;
|
|
lastSafe = next;
|
|
i = next;
|
|
}
|
|
return text.substr(0, lastSafe);
|
|
}
|
|
|
|
std::string SxTextBoxEllipsizeMbc(const std::string& text, int maxWidth)
|
|
{
|
|
if (maxWidth <= 0)
|
|
return "";
|
|
if (textwidth(LPCTSTR(text.c_str())) <= maxWidth)
|
|
return text;
|
|
|
|
const std::string ellipsis = "...";
|
|
const int ellipsisWidth = textwidth(LPCTSTR(ellipsis.c_str()));
|
|
if (ellipsisWidth > maxWidth)
|
|
{
|
|
std::string clippedEllipsis = ellipsis;
|
|
while (!clippedEllipsis.empty() && textwidth(LPCTSTR(clippedEllipsis.c_str())) > maxWidth)
|
|
clippedEllipsis.pop_back();
|
|
return clippedEllipsis;
|
|
}
|
|
|
|
const int contentLimit = maxWidth - ellipsisWidth;
|
|
size_t i = 0;
|
|
size_t lastFit = 0;
|
|
while (i < text.size())
|
|
{
|
|
const int charLen = SxTextBoxMbcCharLen(text, i);
|
|
const size_t next = i + static_cast<size_t>(charLen);
|
|
if (next > text.size())
|
|
break;
|
|
|
|
const std::string candidate = text.substr(0, next);
|
|
if (textwidth(LPCTSTR(candidate.c_str())) <= contentLimit)
|
|
{
|
|
lastFit = next;
|
|
i = next;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (lastFit == 0)
|
|
return ellipsis;
|
|
|
|
std::string head = text.substr(0, lastFit);
|
|
SxTextBoxTrimTrailingSpaces(head);
|
|
return head + ellipsis;
|
|
}
|
|
}
|
|
|
|
TextBox::TextBox(int x, int y, int width, int height, std::string text, StellarX::TextBoxmode mode, StellarX::ControlShape shape)
|
|
:Control(x, y, width, height), text(text), mode(mode), shape(shape)
|
|
{
|
|
this->id = "TextBox";
|
|
// TextBox 当前阶段的默认布局策略:
|
|
// - 水平方向允许通过锚点语义拉伸
|
|
// - 垂直方向当前实现保持固定尺寸,不在本轮引入随高度变化的视觉自适应
|
|
this->layoutCapability.allowStretchX = true;
|
|
this->layoutCapability.allowStretchY = false;
|
|
}
|
|
|
|
void TextBox::draw()
|
|
{
|
|
if (dirty && show)
|
|
{
|
|
saveStyle();
|
|
setfillcolor(textBoxBkClor);
|
|
setlinecolor(textBoxBorderClor);
|
|
StellarX::ControlText drawStyle = textStyle;
|
|
if (drawStyle.nHeight > height)
|
|
drawStyle.nHeight = height;
|
|
if (drawStyle.nWidth > width)
|
|
drawStyle.nWidth = width;
|
|
settextstyle(drawStyle.nHeight, drawStyle.nWidth, drawStyle.lpszFace,
|
|
drawStyle.nEscapement, drawStyle.nOrientation, drawStyle.nWeight,
|
|
drawStyle.bItalic, drawStyle.bUnderline, drawStyle.bStrikeOut);
|
|
|
|
settextcolor(drawStyle.color);
|
|
setbkmode(TRANSPARENT);
|
|
|
|
int text_width = 0;
|
|
int text_height = 0;
|
|
std::string pwdText;
|
|
std::string displayText; // 用于显示的文本(可能被截断)
|
|
|
|
if (StellarX::TextBoxmode::PASSWORD_MODE == mode)
|
|
{
|
|
for (size_t i = 0; i < text.size(); ++i)
|
|
pwdText += '*';
|
|
displayText = pwdText;
|
|
}
|
|
else
|
|
{
|
|
displayText = text;
|
|
}
|
|
|
|
// 计算可用宽度(留出左右边距)
|
|
int availableWidth = width - 20; // 左右各10像素边距
|
|
|
|
// 截断文本以适应可用宽度
|
|
int currentWidth = textwidth(LPCTSTR(displayText.c_str()));
|
|
if (currentWidth > availableWidth && availableWidth > 0)
|
|
{
|
|
// 按 GBK/MBCS 字符边界裁切,避免中文文本被截断到半个字节。
|
|
displayText = SxTextBoxEllipsizeMbc(displayText, availableWidth);
|
|
currentWidth = textwidth(LPCTSTR(displayText.c_str()));
|
|
}
|
|
|
|
text_width = currentWidth;
|
|
text_height = textheight(LPCTSTR(displayText.c_str()));
|
|
|
|
if ((saveBkX != this->x) || (saveBkY != this->y) || (!hasSnap) || (saveWidth != this->width) || (saveHeight != this->height) || !saveBkImage)
|
|
saveBackground(this->x, this->y, this->width, this->height);
|
|
// 恢复背景(清除旧内容)
|
|
restBackground();
|
|
//根据形状绘制
|
|
switch (shape)
|
|
{
|
|
case StellarX::ControlShape::RECTANGLE:
|
|
fillrectangle(x, y, x + width, y + height);//有边框填充矩形
|
|
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str()));
|
|
break;
|
|
case StellarX::ControlShape::B_RECTANGLE:
|
|
solidrectangle(x, y, x + width, y + height);//无边框填充矩形
|
|
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str()));
|
|
break;
|
|
case StellarX::ControlShape::ROUND_RECTANGLE:
|
|
fillroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//有边框填充圆角矩形
|
|
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str()));
|
|
break;
|
|
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
|
solidroundrect(x, y, x + width, y + height, rouRectangleSize.ROUND_RECTANGLEwidth, rouRectangleSize.ROUND_RECTANGLEheight);//无边框填充圆角矩形
|
|
outtextxy(x + 10, (y + (height - text_height) / 2), LPCTSTR(displayText.c_str()));
|
|
break;
|
|
}
|
|
restoreStyle();
|
|
dirty = false; //标记不需要重绘
|
|
}
|
|
}
|
|
|
|
bool TextBox::handleEvent(const ExMessage& msg)
|
|
{
|
|
if (!show) return false;
|
|
resetEventVisualChanged();
|
|
|
|
bool hover = false;
|
|
bool oldClick = click;
|
|
bool consume = false;
|
|
const bool isMouseMessage =
|
|
msg.message == WM_MOUSEMOVE ||
|
|
msg.message == WM_LBUTTONDOWN ||
|
|
msg.message == WM_LBUTTONUP;
|
|
|
|
if (isMouseMessage)
|
|
{
|
|
switch (shape)
|
|
{
|
|
case StellarX::ControlShape::RECTANGLE:
|
|
case StellarX::ControlShape::B_RECTANGLE:
|
|
case StellarX::ControlShape::ROUND_RECTANGLE:
|
|
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
|
hover = (msg.x > x && msg.x < (x + width) && msg.y > y && msg.y < (y + height));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hover && msg.message == WM_LBUTTONUP)
|
|
{
|
|
click = true;
|
|
|
|
const size_t oldLen = text.size();
|
|
SX_LOGI("TextBox") << SX_T("激活:id=","activate: id=") << id << " mode=" << (int)mode << " oldLen=" << oldLen;
|
|
|
|
if (StellarX::TextBoxmode::INPUT_MODE == mode)
|
|
{
|
|
std::vector<char> temp(maxCharLen + 1, '\0');
|
|
dirty = InputBox(temp.data(), (int)maxCharLen + 1, "输入框", NULL, text.c_str(), NULL, NULL, false);
|
|
if (dirty) text = temp.data();
|
|
consume = true;
|
|
}
|
|
else if (StellarX::TextBoxmode::READONLY_MODE == mode)
|
|
{
|
|
dirty = false;
|
|
InputBox(NULL, (int)maxCharLen, "输出框(输入无效!)", NULL, text.c_str(), NULL, NULL, false);
|
|
consume = true;
|
|
}
|
|
else if (StellarX::TextBoxmode::PASSWORD_MODE == mode)
|
|
{
|
|
std::vector<char> temp(maxCharLen + 1, '\0');
|
|
// 不记录明文,只记录长度变化
|
|
dirty = InputBox(temp.data(), (int)maxCharLen + 1, "输入框\n不可见输入,覆盖即可", NULL, NULL, NULL, NULL, false);
|
|
if (dirty) text = temp.data();
|
|
consume = true;
|
|
}
|
|
|
|
if (dirty)
|
|
{
|
|
SX_LOGI("TextBox") << SX_T("文本已更改: id=","text changed: id=") << id
|
|
<< " oldLen=" << oldLen << " newLen=" << text.size();
|
|
}
|
|
else
|
|
{
|
|
SX_LOGD("TextBox") << SX_T("文本无变化:id=","no change: id=") << id;
|
|
}
|
|
}
|
|
|
|
if (dirty)
|
|
requestRepaint(parent);
|
|
markEventVisualChanged(dirty);
|
|
|
|
if (click)
|
|
click = false;
|
|
|
|
return consume;
|
|
}
|
|
|
|
|
|
void TextBox::setMode(StellarX::TextBoxmode mode)
|
|
{
|
|
this->mode = mode;
|
|
this->dirty = true;
|
|
}
|
|
|
|
void TextBox::setMaxCharLen(size_t len)
|
|
{
|
|
if (len > 0)
|
|
maxCharLen = len;
|
|
this->dirty = true;
|
|
}
|
|
|
|
void TextBox::setTextBoxshape(StellarX::ControlShape shape)
|
|
{
|
|
switch (shape)
|
|
{
|
|
case StellarX::ControlShape::RECTANGLE:
|
|
case StellarX::ControlShape::B_RECTANGLE:
|
|
case StellarX::ControlShape::ROUND_RECTANGLE:
|
|
case StellarX::ControlShape::B_ROUND_RECTANGLE:
|
|
this->shape = shape;
|
|
this->dirty = true;
|
|
break;
|
|
case StellarX::ControlShape::CIRCLE:
|
|
case StellarX::ControlShape::B_CIRCLE:
|
|
case StellarX::ControlShape::ELLIPSE:
|
|
case StellarX::ControlShape::B_ELLIPSE:
|
|
this->shape = StellarX::ControlShape::RECTANGLE;
|
|
this->dirty = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void TextBox::setTextBoxBorder(COLORREF color)
|
|
{
|
|
textBoxBorderClor = color;
|
|
this->dirty = true;
|
|
}
|
|
|
|
void TextBox::setTextBoxBk(COLORREF color)
|
|
{
|
|
textBoxBkClor = color;
|
|
this->dirty = true;
|
|
}
|
|
|
|
void TextBox::setText(std::string text)
|
|
{
|
|
if(text == this->text)
|
|
return; // 文本未改变,无需更新和重绘
|
|
if (text.size() > maxCharLen)
|
|
text = SxTextBoxTruncateByMbcBoundary(text, maxCharLen);
|
|
this->text = text;
|
|
this->dirty = true; // 标记需要重绘,不论是否窗口图形上下文是否已初始化,等第一次绘制时由窗口真正调用 draw() 来重绘显示文本
|
|
|
|
//有父控件时请求父控件重绘,无父控件时直接重绘,确保文本更新后界面正确刷新显示
|
|
if (nullptr != parent)
|
|
{
|
|
//通过hasSnap是否持有有效快照,判断控件是否已经绘制过,避免在控件未绘制前/窗口图形上下文未初始化调用draw()导致的错误
|
|
if (hasSnap)
|
|
requestRepaint(parent);
|
|
}
|
|
else
|
|
if (hasSnap)
|
|
draw();
|
|
|
|
}
|
|
|
|
std::string TextBox::getText() const
|
|
{
|
|
return this->text;
|
|
}
|