输入框鼠标编辑、图例省略、信号跨视图共享选择与网格菜单布局修复

- 输入框:支持鼠标左键点击定位插入符、拖拽框选区域,并且不再限制字符长度
- 输入框:长文本使用 ETO_CLIPPED 裁剪绘制,避免字符溢出到编辑区域之外
- 图例:信号名称超过 200px 时省略号截断,不再挤压标尺区域
- 选中状态从 TabConfig 迁移到 DataSet,切换视图后信号选中状态保持一致
- 网格右键菜单:Y 轴最大值/最小值直接显示当前值,点击后原位置弹出内联编辑框
- 标尺数值改为绘制在图例下层,不再因避让图例而隐藏
- 移除已无用的 LegendBandRect,新增共享的编辑框布局与命中检测工具
- .gitignore 忽略 TestData 测试数据目录
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-09-30 17:10:23 +08:00
1 parent e3744f3eff
commit 12b3531485
19 files changed
+790 -186

No files matched your search

+146 -39
View File
@@ -428,6 +428,101 @@ namespace jdi
knobRadius * 2.0f);
}
float UiEditTextLeft(const gd::RectF& rect, const TextEdit& edit, HFONT font, float scale,
float& innerWidth)
{
innerWidth = std::max(4.0f, rect.Width - 12.0f * scale);
const float textLeft = rect.X + 6.0f * scale;
const std::wstring& text = edit.Text();
if (text.empty())
{
return textLeft;
}
const float textWidth = UiMeasureTextWidth(font, text);
if (textWidth <= innerWidth)
{
return textLeft;
}
// 保证插入符可见:插入符超出右边界时向左滚动,并夹到总可滚动范围内。
const float caretX = UiMeasureTextWidth(font, text.substr(0, edit.Caret()));
float scroll = 0.0f;
if (caretX > innerWidth)
{
scroll = caretX - innerWidth;
}
const float maximum = textWidth - innerWidth;
scroll = std::max(0.0f, std::min(scroll, maximum));
return textLeft - scroll;
}
std::size_t UiEditOffsetAt(HFONT font, const std::wstring& text, float localX)
{
if (text.empty() || font == nullptr)
{
return 0;
}
if (localX <= 0.0f)
{
return 0;
}
// 逐字符累加宽度,找到距离最近的字符边界(文本通常很短,线性扫描足够)。
float previous = 0.0f;
for (std::size_t index = 0; index < text.size(); ++index)
{
const float current = UiMeasureTextWidth(font, text.substr(0, index + 1));
if (localX < current)
{
// 落在本字符的左半 → 取左边界,右半 → 取右边界。
return (localX - previous <= current - localX) ? index : (index + 1);
}
previous = current;
}
return text.size();
}
void UiTextAtClipped(gd::Graphics& g, const std::wstring& text, HFONT font, float x, const gd::RectF& rect,
std::uint32_t argb, const gd::RectF& clipRect)
{
if (text.empty() || font == nullptr || clipRect.Width <= 0.0f || clipRect.Height <= 0.0f)
{
return;
}
HDC hdc = g.GetHDC();
if (hdc == nullptr)
{
return;
}
HGDIOBJ oldFont = SelectObject(hdc, font);
const int oldMode = SetBkMode(hdc, TRANSPARENT);
const COLORREF oldColor = SetTextColor(hdc, ToColorRef(argb));
TEXTMETRICW metrics = {};
GetTextMetricsW(hdc, &metrics);
const int baselineY = static_cast<int>(
std::floor(rect.Y + (rect.Height - static_cast<float>(metrics.tmHeight)) * 0.5f));
RECT clip = {};
clip.left = static_cast<LONG>(std::floor(clipRect.X));
clip.top = static_cast<LONG>(std::floor(clipRect.Y));
clip.right = static_cast<LONG>(std::ceil(clipRect.X + clipRect.Width));
clip.bottom = static_cast<LONG>(std::ceil(clipRect.Y + clipRect.Height));
// ETO_CLIPPED 让 GDI 在指定矩形内裁剪,长文本不会再画到输入框之外。
ExtTextOutW(hdc, static_cast<int>(std::floor(x)), baselineY, ETO_CLIPPED, &clip, text.c_str(),
static_cast<UINT>(text.size()), nullptr);
SetTextColor(hdc, oldColor);
SetBkMode(hdc, oldMode);
SelectObject(hdc, oldFont);
g.ReleaseHDC(hdc);
}
void UiDrawEditField(gd::Graphics& g, const gd::RectF& rect, const TextEdit& edit, HFONT font, float scale)
{
if (rect.Width <= 0.0f || rect.Height <= 0.0f || font == nullptr)
@@ -442,70 +537,52 @@ namespace jdi
UiDrawRoundRect(g, rect, corner, theme.accent, 1.6f);
const std::wstring& text = edit.Text();
const float innerLeft = rect.X + 6.0f * scale;
float innerWidth = 0.0f;
const float textLeft = UiEditTextLeft(rect, edit, font, scale, innerWidth);
const gd::RectF clipRect(innerLeft, rect.Y + 1.0f * scale, innerWidth, rect.Height - 2.0f * scale);
if (text.empty())
{
// 空内容仍绘制插入符,保持焦点可见。
const float caretX = rect.X + 6.0f * scale;
UiDrawLine(g, caretX, rect.Y + 4.0f * scale, caretX, rect.Y + rect.Height - 4.0f * scale,
UiDrawLine(g, textLeft, rect.Y + 4.0f * scale, textLeft, rect.Y + rect.Height - 4.0f * scale,
theme.accent, 1.4f * scale);
return;
}
const float innerLeft = rect.X + 6.0f * scale;
const float innerWidth = std::max(4.0f, rect.Width - 12.0f * scale);
// 光标与选区端点对应的像素位置(以文本左端为原点)。
const float caretX = UiTextWidth(g, text.substr(0, edit.Caret()), font);
const float caretX = UiMeasureTextWidth(font, text.substr(0, edit.Caret()));
const float selBeginX =
edit.HasSelection() ? UiTextWidth(g, text.substr(0, edit.SelectionBegin()), font) : 0.0f;
edit.HasSelection() ? UiMeasureTextWidth(font, text.substr(0, edit.SelectionBegin())) : 0.0f;
const float selEndX =
edit.HasSelection() ? UiTextWidth(g, text.substr(0, edit.SelectionEnd()), font) : 0.0f;
const float textWidth = UiTextWidth(g, text, font);
edit.HasSelection() ? UiMeasureTextWidth(font, text.substr(0, edit.SelectionEnd())) : 0.0f;
// 内容超出可视宽度时横向滚动,保证光标始终可见(向常规输入框靠齐)。
float scroll = 0.0f;
if (textWidth > innerWidth)
{
if (caretX > innerWidth)
{
scroll = caretX - innerWidth;
}
const float maximum = textWidth - innerWidth;
scroll = std::max(0.0f, std::min(scroll, maximum));
}
const bool fullSelection =
edit.HasSelection() && edit.SelectionBegin() == 0 && edit.SelectionEnd() == text.size();
const gd::RectF clipRect(innerLeft, rect.Y + 1.0f * scale, innerWidth, rect.Height - 2.0f * scale);
const int saveState = g.Save();
g.SetClip(clipRect, gd::CombineModeIntersect);
const float textLeft = innerLeft - scroll;
// 选区高亮绘制在文本之下,整段选中时呈现“已全选”观感。
const bool fullSelection = edit.HasSelection() && edit.SelectionBegin() == 0 &&
edit.SelectionEnd() == text.size();
// 选区高亮绘制在文本之下,整段选中时呈现“已全选”观感;
// 叠加上 GDI+ 裁剪,高亮同样不会越过输入框边界。
if (edit.HasSelection() && selEndX > selBeginX)
{
const gd::RectF highlight(textLeft + selBeginX, rect.Y + 3.0f * scale,
selEndX - selBeginX, rect.Height - 6.0f * scale);
const gd::RectF highlight(textLeft + selBeginX, rect.Y + 3.0f * scale, selEndX - selBeginX,
rect.Height - 6.0f * scale);
UiFillRect(g, highlight, theme.accent);
}
UiText(g, text, font, gd::RectF(textLeft, rect.Y, std::max(textWidth, innerWidth), rect.Height),
theme.textStrong, TextAlign::LEFT);
// 长文本用 ETO_CLIPPING 交给 GDI 裁剪,避免字符画到输入框之外。
UiTextAtClipped(g, text, font, textLeft, rect,
fullSelection ? theme.textOnAccent : theme.textStrong, clipRect);
if (edit.HasSelection() && !fullSelection && selEndX > selBeginX)
if (!fullSelection && edit.HasSelection() && selEndX > selBeginX)
{
// 局部选区:在反白底衬上重绘被选中的片段,其余文字保持常规颜色。
const std::wstring selected =
text.substr(edit.SelectionBegin(), edit.SelectionEnd() - edit.SelectionBegin());
UiText(g, selected, font,
gd::RectF(textLeft + selBeginX, rect.Y, selEndX - selBeginX, rect.Height),
theme.textOnAccent, TextAlign::LEFT);
}
else if (fullSelection)
{
UiText(g, text, font, gd::RectF(textLeft, rect.Y, std::max(textWidth, innerWidth), rect.Height),
theme.textOnAccent, TextAlign::LEFT);
UiTextAtClipped(g, selected, font, textLeft + selBeginX, rect, theme.textOnAccent, clipRect);
}
// 插入符。
@@ -516,6 +593,36 @@ namespace jdi
g.Restore(saveState);
}
bool UiEditFieldMouseDown(HFONT font, const gd::RectF& rect, TextEdit& edit, float scale, int x, int y)
{
if (!edit.Active() || font == nullptr || rect.Width <= 0.0f)
{
return false;
}
if (!UiPointInRect(rect, static_cast<float>(x), static_cast<float>(y)))
{
return false;
}
// 用与绘制完全相同的布局函数换算文本起点,保证点哪儿光标就落在哪儿。
float innerWidth = 0.0f;
const float textLeft = UiEditTextLeft(rect, edit, font, scale, innerWidth);
edit.BeginDrag(UiEditOffsetAt(font, edit.Text(), static_cast<float>(x) - textLeft));
return true;
}
bool UiEditFieldMouseMove(HFONT font, const gd::RectF& rect, TextEdit& edit, float scale, int x)
{
if (!edit.Active() || !edit.Dragging() || font == nullptr || rect.Width <= 0.0f)
{
return false;
}
float innerWidth = 0.0f;
const float textLeft = UiEditTextLeft(rect, edit, font, scale, innerWidth);
return edit.UpdateDrag(UiEditOffsetAt(font, edit.Text(), static_cast<float>(x) - textLeft));
}
bool UiSwatchNeedsOutline(float opacity)
{
return !(opacity >= kSwatchOutlineOpacity);