fix: 统一输入框编辑行为、外观跨视图同步并优化标尺与网格可读性

输入框(TextEdit,全组件共用):
- 修复退格一次删除两个字符:WM_CHAR 与 WM_KEYDOWN 重复处理了退格/删除,
  现统一由 OnEditKey 处理,WM_CHAR 忽略全部控制字符。
- 进入编辑(整段选中)后按右键:取消全选并把插入符移到文本最右侧。
- 存在选区时松开 Shift 再按左右方向键,插入符收拢到选区的左/右端。
- 修复整段选中后按 Home/End 不收拢选区的问题。

标签页:
- 按加粗字体测量宽度,避免激活页长名称被提前省略(预览过短)。
- 重命名过程中按正在输入的文本测量并加宽上限(320px),标签随输入增长,
  解决长内容绘制错误;输入溢出时整体等比收缩,保证「+」始终可见。

信号显示外观跨标签页同步:
- 外观(颜色/自定义色记忆/透明度/线宽/样式)从 TabConfig 上移到 DataSet
  的 SignalData,成为信号的全局属性;可见性仍按标签页独立保存。
- 因此任一视图中修改某信号的颜色/线宽/透明度/样式,其它视图自动一致。

工具栏:
- 移除「全选信号」按钮;「清空勾选」改为「清空网格」,
  「拆分信号网格」改为「拆分网格」。

自适应:
- 无可见信号(未勾选/全部取消/清空网格)时复位 ViewInitialized,
  下次勾选第一个信号会重新自适应,而不是停留在旧视图范围。

绘图区标尺与网格:
- 幅值刻度密度按网格高度自适应(3-8 档),各网格刻度数量一致;
  网格较矮时同步收紧标签行距,提高可读密度。
- 网格线与五等分估读线加深,与网格边界(蓝灰)保持明显区分。
- 时间轴列名补上单位,显示为 time (s)。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-09-30 15:16:53 +08:00
1 parent 53768b8b25
commit e3744f3eff
16 files changed
+462 -340

No files matched your search

+9 -2
View File
@@ -318,6 +318,12 @@ namespace jdi
*/
bool OnEditKey(WPARAM key);
/**
* @brief 编辑态下按下鼠标右键:取消选区并把插入符移到文本末尾。
* @return 发生改变返回 true;不在编辑态返回 false。
*/
bool CollapseEditSelectionToEnd();
/**
* @brief 获取当前视图左边界时间。
* @return 视图左边界时间。
@@ -351,9 +357,10 @@ namespace jdi
*/
struct ResolvedSignal
{
const SignalData* data = nullptr; /**< 信号数据。 */
const SignalStyle* style = nullptr; /**< 显示配置。 */
const SignalData* data = nullptr; /**< 信号数据(含全局共享的显示外观)。 */
const SignalStyle* style = nullptr; /**< 显示外观。 */
const DataFile* file = nullptr; /**< 所属文件。 */
bool visible = false; /**< 在当前标签页中是否可见。 */
};
/**
+47 -27
View File
@@ -12,8 +12,13 @@ namespace jdi
namespace
{
const int kTimeTickTarget = 10; /**< 时间轴期望刻度数。 */
const int kYTickTarget = 6; /**< 幅值轴期望刻度数。 */
const int kYTickTarget = 6; /**< 幅值轴期望刻度数(网格较高时)。 */
const int kYTickTargetMin = 3; /**< 幅值轴最少刻度数(网格很矮时也要保证可读数)。 */
const int kYTickTargetMax = 8; /**< 幅值轴最多刻度数。 */
const float kYTickSpacing = 28.0f; /**< 幅值刻度之间的理想间距(逻辑像素)。 */
const float kYLabelMinGap = 15.0f; /**< 幅值标签的最小行距(逻辑像素)。 */
const int kMinorDivisions = 5; /**< 相邻主刻度之间的等分数(用于估读)。 */
const unsigned int kMinorLineAlpha = 0x9Au; /**< 辅助等分线的透明度(越高越清晰)。 */
const float kScrollBarThickness = 9.0f; /**< 滚动条宽度(逻辑像素)。 */
const float kLanePadding = 8.0f; /**< 网格内边距。 */
const size_t kMaxPolylinePoints = 20000; /**< 单条折线最大顶点数。 */
@@ -162,10 +167,8 @@ namespace jdi
}
result.data = m_dataSet->Resolve(ref);
result.file = m_dataSet->FileAt(ref.fileIndex);
if (m_tab != nullptr)
{
result.style = m_tab->Style(ref);
}
result.style = m_dataSet->Style(ref);
result.visible = (m_tab != nullptr) && m_tab->IsVisible(ref);
return result;
}
@@ -229,8 +232,8 @@ namespace jdi
int firstFile = -1;
for (const SignalRef& ref : m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals)
{
const SignalStyle* style = m_tab->Style(ref);
if (style == nullptr || !style->visible)
const SignalStyle* style = m_dataSet->Style(ref);
if (style == nullptr || !m_tab->IsVisible(ref))
{
continue;
}
@@ -888,7 +891,7 @@ namespace jdi
for (const SignalRef& ref : m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible)
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
{
continue;
}
@@ -1157,21 +1160,23 @@ namespace jdi
{
return EndEdit(true);
}
if (character == L'\b')
{
return m_edit.Backspace();
}
if (character == 0x7F)
{
return m_edit.DeleteForward();
}
if (character < 0x20)
// 退格/删除已由 WM_KEYDOWN 的 OnEditKey 处理,此处忽略控制字符避免重复删除。
if (character < 0x20 || character == 0x7F)
{
return false;
}
return m_edit.Insert(character);
}
bool PlotView::CollapseEditSelectionToEnd()
{
if (m_editTarget == PlotEditTarget::NONE)
{
return false;
}
return m_edit.DeselectToEnd();
}
bool PlotView::OnEditKey(WPARAM key)
{
if (m_editTarget == PlotEditTarget::NONE)
@@ -1893,7 +1898,11 @@ namespace jdi
}
// 幅值网格线(先画线,标尺稍后作为覆盖层绘制,避免被波形遮挡)。
const double yStep = NiceStep(lane.yMax - lane.yMin, kYTickTarget);
// 刻度密度随网格高度自适应:所有网格高度相同 → 各网格的刻度数量一致,
// 不会出现“有的网格只有 1 条标签、有的有 3 条”的不一致观感。
int yTickTarget = static_cast<int>(std::lround(lane.rect.Height / (kYTickSpacing * m_scale)));
yTickTarget = std::max(kYTickTargetMin, std::min(yTickTarget, kYTickTargetMax));
const double yStep = NiceStep(lane.yMax - lane.yMin, yTickTarget);
std::vector<double> yTicks;
BuildTicks(lane.yMin, lane.yMax, yStep, yTicks);
@@ -1916,7 +1925,7 @@ namespace jdi
continue;
}
UiDrawLine(g, lane.rect.X, y, lane.rect.X + lane.rect.Width, y,
WithAlpha(m_theme->gridWeak, 0x70), 1.0f);
WithAlpha(m_theme->gridWeak, kMinorLineAlpha), 1.0f);
}
for (const double tick : yTicks)
@@ -1952,7 +1961,7 @@ namespace jdi
continue;
}
UiDrawLine(g, x, lane.rect.Y, x, lane.rect.Y + lane.rect.Height,
WithAlpha(m_theme->gridWeak, 0x70), 1.0f);
WithAlpha(m_theme->gridWeak, kMinorLineAlpha), 1.0f);
}
for (const double tick : timeTicks)
@@ -1983,7 +1992,7 @@ namespace jdi
for (const SignalRef& ref : laneSignals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible)
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
{
continue;
}
@@ -2074,6 +2083,11 @@ namespace jdi
const bool showLabels = (lane.rect.Height >= 34.0f * m_scale);
float lastLabelY = -1.0e9f;
// 网格较矮时同步收紧标签行距,让密度随可用高度提升而不是整片被过滤掉。
const float labelMinGap =
std::min(kYLabelMinGap * m_scale,
lane.rect.Height / static_cast<float>(yTickTarget + 1));
for (const double tick : yTicks)
{
if (!showLabels)
@@ -2086,7 +2100,7 @@ namespace jdi
{
continue;
}
if (std::fabs(y - lastLabelY) < 15.0f * m_scale)
if (std::fabs(y - lastLabelY) < labelMinGap)
{
continue;
}
@@ -2150,7 +2164,7 @@ namespace jdi
for (const SignalRef& ref : m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible)
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
{
continue;
}
@@ -2223,7 +2237,7 @@ namespace jdi
for (const SignalRef& ref : laneSignals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible)
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
{
continue;
}
@@ -2266,7 +2280,7 @@ namespace jdi
for (const SignalRef& ref : laneSignals)
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible)
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.visible)
{
continue;
}
@@ -2370,7 +2384,7 @@ namespace jdi
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || resolved.file == nullptr ||
!resolved.style->visible)
!resolved.visible)
{
continue;
}
@@ -2455,6 +2469,12 @@ namespace jdi
if (file != nullptr && !file->timeColumnName.empty())
{
axisName = file->timeColumnName;
// CSV 中该列通常直接叫 "time",这里补上时间单位秒便于阅读;
// 若列名自带单位描述(以右括号结尾)则不重复追加。
if (axisName.back() != L')')
{
axisName += L" (s)";
}
}
break;
}
@@ -2532,7 +2552,7 @@ namespace jdi
{
const ResolvedSignal resolved = ResolveSignal(ref);
if (resolved.data == nullptr || resolved.style == nullptr || resolved.file == nullptr ||
!resolved.style->visible)
!resolved.visible)
{
continue;
}