fix: 修正样式区对齐与线宽输入交互,分组标题改为“组X|文件名”并支持同文件重复加载

样式编辑区:
- 标签列与内容列改为靠左对齐(原为整体居中),拉宽左侧面板时不再向中间偏移。
- 颜色行不再居中,与样式、透明度、线宽三行共用同一左边界与同一宽度上限
  (260 逻辑像素),实测四行均为 89-413px,右侧留 93px 空白。

线宽输入:
- 双击数值框后数值整体居中并高亮(外框与数值呈选中态),首次键入即整体覆盖原值,
  之后显示插入符;回车确认、Esc 取消、点击其它区域确认,交互与常见登录输入框一致。

散点绘制:
- 散点半径改为随线宽线性增长(0.5-10px 映射到 1.6-12px,系数 1.5),
  修复此前半径被上限 4px 过早截断、线宽较大时数据点不再放大的问题。

透明度描边:
- 新增统一色块绘制入口 UiDrawSwatch:按不透明度填充,且仅在不透明度低于 45% 时
  补一圈描边;可见时不绘制描边。
- 预设色块、自定义色块、信号行色块与波形图例色块统一走该入口,
  修复透明度过低时完全不可见的问题。

文件分组:
- 分组标题改为“组X|<文件名>”,序号使用强调色;序号按当前位置连续编号,
  关闭某个分组后其余分组自动重新编号。
- 移除按路径查重的限制,同一文件可多次加载并分别归入不同分组。
- 关闭分组改为“移至末位后弹出 + 收缩容量”,使该文件的时间轴与全部信号缓冲区
  在本次调用内立即析构归还系统。
- 确认加载后不再依赖原始文件:读取阶段即关闭句柄并整份载入内存,
  已实测删除源文件后仍可正常勾选与绘制波形。

验证:make clean && make all(Release+Debug)零错误零警告;
逐项以真实/合成鼠标与像素扫描核对以上行为。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-09-29 18:40:17 +08:00
1 parent dbe9b904ab
commit cba62c8218
7 files changed
+134 -82

No files matched your search

+4 -11
View File
@@ -734,15 +734,8 @@ namespace jdi
return false;
}
const int existing = m_dataSet.IndexOfFile(path);
if (existing >= 0)
{
const DataFile* file = m_dataSet.FileAt(existing);
SetStatusMessage(L"该文件已在工作区中:" + (file != nullptr ? file->fileName : path), true);
InvalidateRect(m_hwnd, nullptr, FALSE);
return false;
}
// 同一文件允许多次加载:每次都会新建一个独立分组,各自持有独立的采样数据副本,
// 因此后续对原始文件的修改或删除都不会影响已加载内容。
CsvTable table;
std::wstring error;
if (!LoadCsvTable(path, table, error))
@@ -767,8 +760,8 @@ namespace jdi
const DataFile* file = m_dataSet.FileAt(fileIndex);
if (file != nullptr)
{
std::wstring message = L"已加载 " + file->fileName + L"(" +
FormatCount(static_cast<unsigned long long>(file->time.size())) +
std::wstring message = L"已加载 组" + std::to_wstring(fileIndex + 1) + L"|" + file->fileName +
L"(" + FormatCount(static_cast<unsigned long long>(file->time.size())) +
L" 行采样," + std::to_wstring(file->signals.size()) +
L" 条信号,时间列:" + file->timeColumnName + L")";
if (!file->timeMonotonic)
+6 -5
View File
@@ -19,7 +19,8 @@ namespace jdi
const size_t kMaxPolylinePoints = 20000; /**< 单条折线最大顶点数。 */
const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */
const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */
const float kMaxScatterRadius = 4.0f; /**< 散点最大半径(逻辑像素)。 */
const float kMaxScatterRadius = 12.0f; /**< 散点最大半径(逻辑像素)。 */
const float kScatterRadiusScale = 1.5f; /**< 散点半径相对线宽的增长系数。 */
const double kMinTimeSpan = 1.0e-12; /**< 允许的最小时间跨度。 */
/**
@@ -1125,8 +1126,9 @@ namespace jdi
if (drawScatter && !points.empty())
{
// 散点半径随线宽线性增长(达到上限前不过早触顶),使“线宽”直观控制数据点大小。
const float radius = static_cast<float>(
ClampDouble(std::max(1.0f, resolved.style->lineWidth) * (kMinScatterRadius + 0.6f),
ClampDouble(static_cast<double>(resolved.style->lineWidth) * kScatterRadiusScale,
kMinScatterRadius, kMaxScatterRadius) *
m_scale);
const std::size_t scatterStride =
@@ -1236,9 +1238,8 @@ namespace jdi
drawnWidth += itemWidth;
const float swatchSize = 8.0f * m_scale;
UiFillRoundRect(g,
gd::RectF(cursorX, top + (height - swatchSize) * 0.5f, swatchSize, swatchSize),
1.5f * m_scale, StyleColor(*resolved.style));
UiDrawSwatch(g, gd::RectF(cursorX, top + (height - swatchSize) * 0.5f, swatchSize, swatchSize),
resolved.style->color, resolved.style->opacity, 1.5f * m_scale, m_scale);
cursorX += swatchSize + 5.0f * m_scale;
UiText(g, label, m_fonts->uiSmall, gd::RectF(cursorX, top, textWidth + 2.0f, height),
+51 -45
View File
@@ -92,19 +92,6 @@ namespace jdi
knobRadius * 2.0f);
}
/**
* @brief 将颜色按不透明度叠加为实际显示颜色。
* @param [IN] color 0xAARRGGBB 颜色值。
* @param [IN] opacity 不透明度(0-1)。
* @return 叠加后的颜色值。
*/
std::uint32_t ApplyOpacity(std::uint32_t color, float opacity)
{
double alpha = static_cast<double>(opacity);
alpha = std::max(0.0, std::min(1.0, alpha));
return WithAlpha(color, static_cast<unsigned int>(alpha * 255.0 + 0.5));
}
/**
* @brief 判断颜色是否偏暗(用于在色块之上选择可读的图标颜色)。
* @param [IN] color 0xAARRGGBB 颜色值。
@@ -447,9 +434,9 @@ namespace jdi
const float available = std::max(40.0f, m_styleRect.Width - padH * 2.0f - labelWidth - labelGap);
const float contentWidth = std::min(available, kStyleContentMax * m_scale);
// 标签列 + 内容列作为整体在面板内居中,避免拉宽后控件偏向一侧。
// 标签列 + 内容列整体靠左对齐(始终从左侧内边距开始),不随面板变宽而居中偏移。
const float groupWidth = labelWidth + labelGap + contentWidth;
const float groupLeft = m_styleRect.X + std::max(padH, (m_styleRect.Width - groupWidth) * 0.5f);
const float groupLeft = m_styleRect.X + padH;
const float contentLeft = groupLeft + labelWidth + labelGap;
m_styleContentRect = gd::RectF(contentLeft, firstTop, contentWidth,
@@ -492,10 +479,8 @@ namespace jdi
}
m_colorCount = std::max(1, count);
// 若受色块尺寸上限约束仍有剩余空间,则整行在内容列内居中。
const float colorRowWidth = swatchSize * static_cast<float>(m_colorCount + 1) +
gap * static_cast<float>(m_colorCount);
const float colorRowLeft = contentLeft + std::max(0.0f, (contentWidth - colorRowWidth) * 0.5f);
// 颜色行与其余三行共用同一左边界(不再居中),保证四行纵向对齐。
const float colorRowLeft = contentLeft;
const float swatchTop = colorRow.Y + (rowHeight - swatchSize) * 0.5f;
for (int index = 0; index < m_colorCount; ++index)
@@ -697,15 +682,22 @@ namespace jdi
const gd::RectF closeRect = FileCloseRect(row);
// 文件名称与统计。
const float nameLeft = checkRect.X + checkRect.Width + 8.0f * m_scale;
// 分组标题:以“组X | <文件名>”呈现,X 为该组在当前列表中的序号。
const std::wstring groupLabel = L"组" + std::to_wstring(item.fileIndex + 1) + L" | ";
const std::wstring stats = std::to_wstring(file->signals.size()) + L" 项";
const float groupWidth = UiMeasureTextWidth(m_fonts->uiBold, groupLabel);
const float statsWidth = UiMeasureTextWidth(m_fonts->uiSmall, stats);
const float statsRight = closeRect.X - 6.0f * m_scale;
const float nameWidth = std::max(20.0f, statsRight - statsWidth - 6.0f * m_scale - nameLeft);
const float nameLeft = checkRect.X + checkRect.Width + 8.0f * m_scale;
const float nameWidth =
std::max(20.0f, statsRight - statsWidth - 6.0f * m_scale - nameLeft - groupWidth);
// 序号以强调色突出,便于在多组之间快速定位。
UiText(g, groupLabel, m_fonts->uiBold,
gd::RectF(nameLeft, rect.Y, groupWidth, rect.Height), m_theme->accent, TextAlign::LEFT);
UiText(g, UiEllipsize(g, file->fileName, m_fonts->uiBold, nameWidth), m_fonts->uiBold,
gd::RectF(nameLeft, rect.Y, nameWidth, rect.Height), m_theme->textStrong, TextAlign::LEFT);
gd::RectF(nameLeft + groupWidth, rect.Y, nameWidth, rect.Height), m_theme->textStrong,
TextAlign::LEFT);
UiText(g, stats, m_fonts->uiSmall, gd::RectF(statsRight - statsWidth, rect.Y, statsWidth, rect.Height),
m_theme->textMuted, TextAlign::RIGHT);
@@ -756,13 +748,12 @@ namespace jdi
kCheckSize * m_scale, kCheckSize * m_scale);
UiDrawCheckBox(g, checkRect, style->visible ? 1 : 0, 3.0f * m_scale);
// 颜色块(叠加不透明度,直观反映显示效果)。
const std::uint32_t swatchColor = ApplyOpacity(style->color, style->opacity);
// 颜色块:叠加不透明度,透明度过低时自动补描边(与样式区色块规则一致)。
const gd::RectF swatchRect(checkRect.X + checkRect.Width + 6.0f * m_scale,
centerY - kSwatchSize * m_scale * 0.5f, kSwatchSize * m_scale,
kSwatchSize * m_scale);
UiFillRoundRect(g, swatchRect, 2.5f * m_scale,
style->visible ? swatchColor : WithAlpha(style->color, 0x50));
UiDrawSwatch(g, swatchRect, style->color, style->visible ? style->opacity : 0.3f, 2.5f * m_scale,
m_scale);
// 网格徽标(可点击,用于调整所属网格)。
const int laneIndex = m_tab->LaneOfSignal(ref);
@@ -865,8 +856,8 @@ namespace jdi
const bool hovered = (m_hoverStyle == StyleHit::COLOR && m_hoverStyleIndex == index);
const bool active = !customActive && (style->color == color);
// 预设色块同样叠加不透明度,直观反映实际显示效果。
UiFillRoundRect(g, rect, kCorner * m_scale, ApplyOpacity(color, style->opacity));
// 预设色块同样叠加不透明度;透明度过低时自动补描边以保证可辨识(与自定义色块一致)。
UiDrawSwatch(g, rect, color, style->opacity, kCorner * m_scale, m_scale);
if (active || hovered)
{
const gd::RectF ring(rect.X - 2.0f * m_scale, rect.Y - 2.0f * m_scale,
@@ -877,16 +868,12 @@ namespace jdi
}
}
// 自定义颜色入口:与预设色块完全同尺寸、同外框样式的小方格,仅多一个加号图标。
// 自定义颜色入口:与预设色块完全同尺寸、同外框规则的小方格,仅多一个加号图标。
// 方格始终显示记忆的自定义颜色,因此从任意预设色再次点击它都能立即回到上次的自定义色。
const bool chipHot = (m_hoverStyle == StyleHit::CUSTOM_CHIP);
const std::uint32_t chipColor = ApplyOpacity(style->customColor, style->opacity);
const std::uint32_t chipColor =
UiDrawSwatch(g, m_customChipRect, style->customColor, style->opacity, kCorner * m_scale, m_scale);
const std::uint32_t iconColor = IsDarkColor(chipColor) ? 0xFFFDF6E3u : 0xFF073642u;
UiFillRoundRect(g, m_customChipRect, kCorner * m_scale, chipColor);
UiDrawRoundRect(g, m_customChipRect, kCorner * m_scale,
customActive ? m_theme->textStrong : (chipHot ? m_theme->accent : m_theme->borderStrong),
customActive ? 1.6f : 1.0f);
UiDrawPlusIcon(g, m_customChipRect, iconColor, 1.6f * m_scale);
// 当前正在使用自定义颜色、或鼠标悬停时,显示与预设色块一致的外框。
@@ -943,21 +930,40 @@ namespace jdi
if (m_widthEditing)
{
// 输入状态:右对齐并显示插入符,便于用户看到键入内容。
const std::wstring editText = m_widthEditText.empty() ? std::wstring(L" ") : m_widthEditText;
// 输入状态:数值整体居中;处于“全部选中”状态时以高亮背景表示内容将被覆盖,
// 交互观感与常见登录界面的账号/密码输入框一致。
UiFillRoundRect(g, m_widthValueRect, kCorner * m_scale, m_theme->cardBg);
UiDrawRoundRect(g, m_widthValueRect, kCorner * m_scale, m_theme->accent, 1.6f);
const std::wstring editText = m_widthEditText.empty() ? std::wstring(L" ") : m_widthEditText;
const float textWidth = UiTextWidth(g, editText, m_fonts->monoSmall);
UiText(g, editText, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong,
TextAlign::RIGHT);
const float textX =
m_widthValueRect.X + std::max(2.0f * m_scale, (m_widthValueRect.Width - textWidth) * 0.5f);
const float caretX = m_widthValueRect.X + m_widthValueRect.Width - 6.0f * m_scale - textWidth;
if (caretX > m_widthValueRect.X)
if (m_widthEditFresh)
{
UiDrawLine(g, caretX, m_widthValueRect.Y + 5.0f * m_scale, caretX,
m_widthValueRect.Y + rowHeight - 5.0f * m_scale, m_theme->accent,
1.4f * m_scale);
// 高亮选区:覆盖整段数值,提示首次键入即整体替换。
const float padX = 3.0f * m_scale;
UiFillRoundRect(g, gd::RectF(textX - padX, m_widthValueRect.Y + 4.0f * m_scale,
textWidth + padX * 2.0f,
m_widthValueRect.Height - 8.0f * m_scale),
2.0f * m_scale, m_theme->accent);
UiText(g, editText, m_fonts->monoSmall, m_widthValueRect, m_theme->textOnAccent,
TextAlign::CENTER);
}
else
{
UiText(g, editText, m_fonts->monoSmall, m_widthValueRect, m_theme->textStrong,
TextAlign::CENTER);
// 已开始键入:在文本末尾显示插入符。
const float caretX = textX + textWidth + 1.0f * m_scale;
if (caretX < m_widthValueRect.X + m_widthValueRect.Width - 2.0f * m_scale)
{
UiDrawLine(g, caretX, m_widthValueRect.Y + 5.0f * m_scale, caretX,
m_widthValueRect.Y + rowHeight - 5.0f * m_scale, m_theme->accent,
1.4f * m_scale);
}
}
}
else
+1 -8
View File
@@ -121,7 +121,7 @@ namespace jdi
bool AddFromTable(const CsvTable& table, const std::wstring& filePath, std::wstring& error, int& fileIndex);
/**
* @brief 移除指定下标的文件。
* @brief 移除指定下标的文件,并立即释放其占用的全部采样内存。
* @param [IN] fileIndex 文件下标。
* @return 移除成功返回 true。
*/
@@ -165,13 +165,6 @@ namespace jdi
*/
const SignalData* Resolve(const SignalRef& ref) const;
/**
* @brief 按路径查找已加载文件下标。
* @param [IN] filePath 文件完整路径。
* @return 文件下标,未找到返回 -1。
*/
int IndexOfFile(const std::wstring& filePath) const;
/**
* @brief 计算全部文件时间范围的并集下限。
* @return 时间下限。
+20
View File
@@ -232,6 +232,26 @@ namespace jdi
*/
void UiDrawCheckBox(gd::Graphics& g, const gd::RectF& rect, int state, float radius);
/**
* @brief 绘制颜色样块:叠加不透明度填充,透明度过低时补充描边以保证可辨识。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] rect 样块矩形。
* @param [IN] color 基础颜色(0xAARRGGBB,其 Alpha 通道会被不透明度覆盖)。
* @param [IN] opacity 不透明度(0-1)。
* @param [IN] radius 圆角半径。
* @param [IN] scale DPI 缩放比例,用于推导描边粗细。
* @return 实际填充颜色的 0xAARRGGBB 值(供需要同步其他元素的调用方使用)。
*/
std::uint32_t UiDrawSwatch(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color, float opacity,
float radius, float scale);
/**
* @brief 判断给定不透明度是否低到需要为样块补充描边。
* @param [IN] opacity 不透明度(0-1)。
* @return 需要描边返回 true。
*/
bool UiSwatchNeedsOutline(float opacity);
} // namespace jdi
#endif // __PAINT_UTIL_HPP__
+10 -13
View File
@@ -414,7 +414,16 @@ namespace jdi
{
return false;
}
m_files.erase(m_files.begin() + fileIndex);
// 用移动到末位后弹出的方式移除,使被移除文件的时间轴与全部信号缓冲区在本调用内
// 立即析构归还操作系统,不再等待后续插入覆盖;最后收缩容量,避免残留空闲块。
const std::size_t position = static_cast<std::size_t>(fileIndex);
for (std::size_t index = position; index + 1 < m_files.size(); ++index)
{
m_files[index] = std::move(m_files[index + 1]);
}
m_files.pop_back();
m_files.shrink_to_fit();
return true;
}
@@ -451,18 +460,6 @@ namespace jdi
return &file->signals[static_cast<std::size_t>(ref.signalIndex)];
}
int DataSet::IndexOfFile(const std::wstring& filePath) const
{
for (std::size_t index = 0; index < m_files.size(); ++index)
{
if (m_files[index].filePath == filePath)
{
return static_cast<int>(index);
}
}
return -1;
}
double DataSet::MinTime() const
{
double value = std::numeric_limits<double>::infinity();
+42
View File
@@ -1,5 +1,6 @@
#include "PaintUtil.hpp"
#include <algorithm>
#include <cmath>
namespace jdi
@@ -7,6 +8,14 @@ namespace jdi
namespace
{
/**
* @brief 颜色样块补充描边的不透明度阈值。
*
* 不透明度低于该值时,色块自身与浅色背景的对比度已不足,需要额外描边才能辨识;
* 高于该值时按需求不绘制描边,保持与预设色块一致的纯净外观。
*/
const float kSwatchOutlineOpacity = 0.45f;
/**
* @brief 构造圆角矩形路径。
* @param [IN,OUT] path 目标路径对象。
@@ -420,4 +429,37 @@ namespace jdi
}
}
bool UiSwatchNeedsOutline(float opacity)
{
return !(opacity >= kSwatchOutlineOpacity);
}
std::uint32_t UiDrawSwatch(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color, float opacity,
float radius, float scale)
{
double alpha = static_cast<double>(opacity);
if (!(alpha > 0.0))
{
alpha = 0.0;
}
if (alpha > 1.0)
{
alpha = 1.0;
}
const std::uint32_t filled = WithAlpha(color, static_cast<unsigned int>(alpha * 255.0 + 0.5));
if (rect.Width > 0.0f && rect.Height > 0.0f)
{
UiFillRoundRect(g, rect, radius, filled);
// 透明度过低时色块本身难以辨识,补充一圈描边以保持可读性;可见时不绘制描边。
if (UiSwatchNeedsOutline(opacity))
{
UiDrawRoundRect(g, rect, radius, SolarizedLight().borderStrong,
std::max(1.0f, scale));
}
}
return filled;
}
} // namespace jdi