网格菜单重构、大文件异步加载与内存/解析优化、修复应用图标
网格右键菜单: - 菜单顺序改为: 网格 N / Y 轴最大值 / Y 轴最小值 / 选区 Y 轴自适应 / --分隔线-- / Y 轴自适应 / 时间轴自适应 / 全部自适应 (恢复自适应移到分隔线以下、时间轴自适应上方) - Y 轴最大值/最小值改为菜单内联输入框:点击就地编辑、回车提交,菜单不关闭, 处理方式与拾色器的 RGB 内联编辑一致;提交后按真实夹取结果回写菜单显示值, 并同步刷新自适应项的文案/对勾/可用状态 - 新增"选区 Y 轴自适应":只按当前显示时间段内的数据做幅值自适应 (时间单调时二分定位区间,非单调时全扫并按区间过滤) 大文件加载: - 加载改为工作线程执行(解析 + 按列重组),主线程抽送消息并播放过渡动画 (旋转弧线 + 进度条 + 文件名 + 百分比 + 按 Esc 取消),界面不再整块卡死 - 加载完成后主线程只做一次 O(1) 的移动入队,不会产生可见卡顿 - 新增 DataModel::BuildDataFile / DataSet::AddPreparedFile:不依赖 DataSet, 因此可以安全地在线程上运行 内存与解析效率: - CsvTable 由"行优先二维数组"改为"列优先":百万行文件原先会产生上百万次独立堆分配 (每行还有 vector 控制块与容量对齐开销),现在每列一整块连续内存 - 解析改为流式分块读取(4MB),不再把整个文件读入内存,也没有体积上限 (原先超过 512MB 直接拒绝);实测 51MB 文件峰值内存约 126MB(数据本身 113MB) - BuildDataFile 直接把整列移动进 SignalData(零拷贝),不再逐元素复制 - TryParseDouble 增加十进制快速路径:ucrt strtod 实测约 335ns/次,手写路径约 13ns (快 25 倍);非十进制写法(十六进制浮点、inf/nan、带尾部杂字符)原样交回 strtod, 输入接受语义完全不变(实测与 strtod 的差异不超过 1 ULP) → 640MB 测试文件加载时间由约 40s 降到 2s 以内 交互流畅性: - 拖拽(平移/框选/拖动信号)期间关闭波形抗锯齿并压缩顶点预算(2 像素一列包络), 松开后立即恢复全质量绘制;实测 BIG_DATA 10 信号全显示的帧耗时由 105~168ms 降到 75~90ms(其中折线绘制 55ms→23ms) - 折线包络抽稀改为"列边界单调推进"的二分,避免每列在全区间二分造成缓存缺失 - 未对波形构建引入多线程:构建已由几何键缓存消除,且拖拽降级后仅约 1~2ms/帧, 并行化收益不足以抵消线程与同步的复杂度(加载路径的多线程收益明确,故只在该处引入) 图标: - 窗口类与 WM_SETICON 原先使用系统默认图标 IDI_APPLICATION,导致标题栏、 任务栏与 Alt+Tab 缩略图都显示成通用应用图标;改为按 SM_CXICON/SM_CXSMICON 从应用的 IDI_APP_ICON 资源加载(多尺寸 ICO 会挑选最接近的一帧) 其他: - PopupMenu 新增 EDIT 项类型(菜单内联输入框)与 UpdateItemValue/UpdateItemLabel
This commit is contained in:
1 parent
d6a81839bb
commit
45b44eae57
11 files changed
+1637
-222
No files matched your search
@@ -9,6 +9,7 @@
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
const int kTimeTickTarget = 10; /**< 时间轴期望刻度数。 */
|
||||
@@ -831,6 +832,109 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::AutoRangeLaneToView(int laneIndex)
|
||||
{
|
||||
if (m_tab == nullptr || m_dataSet == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::vector<Lane>& lanes = m_tab->Lanes();
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(lanes.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const double viewMin = Tab().ViewMin();
|
||||
const double viewMax = Tab().ViewMax();
|
||||
if (!(viewMax > viewMin))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 只统计时间轴当前可见区间内的采样点,因此放大局部波形后可以快速把该段铺满网格。
|
||||
double low = std::numeric_limits<double>::infinity();
|
||||
double high = -std::numeric_limits<double>::infinity();
|
||||
|
||||
for (const SignalRef& ref : lanes[static_cast<std::size_t>(laneIndex)].signals)
|
||||
{
|
||||
if (!Tab().IsVisible(ref))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const SignalData* signal = m_dataSet->Resolve(ref);
|
||||
const DataFile* file = m_dataSet->FileAt(ref.fileIndex);
|
||||
if (signal == nullptr || file == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t count = std::min(file->time.size(), signal->values.size());
|
||||
std::size_t begin = 0;
|
||||
std::size_t end = count;
|
||||
if (file->timeMonotonic)
|
||||
{
|
||||
// 时间单调时直接二分定位区间,百万级采样也只需扫过可见段。
|
||||
begin = static_cast<std::size_t>(
|
||||
std::lower_bound(file->time.begin(), file->time.end(), viewMin) - file->time.begin());
|
||||
if (begin > 0)
|
||||
{
|
||||
--begin;
|
||||
}
|
||||
end = static_cast<std::size_t>(
|
||||
std::lower_bound(file->time.begin(), file->time.end(), viewMax) - file->time.begin());
|
||||
if (end < count)
|
||||
{
|
||||
++end;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t index = begin; index < end; ++index)
|
||||
{
|
||||
if (!file->timeMonotonic)
|
||||
{
|
||||
const double t = file->time[index];
|
||||
if (!std::isfinite(t) || t < viewMin || t > viewMax)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const double value = signal->values[index];
|
||||
if (std::isfinite(value))
|
||||
{
|
||||
low = std::min(low, value);
|
||||
high = std::max(high, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!std::isfinite(low) || !std::isfinite(high))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 与自动自适应保持一致的上下留白,避免波形顶到网格边界。
|
||||
if (high > low)
|
||||
{
|
||||
const double pad = (high - low) * 0.05;
|
||||
low -= pad;
|
||||
high += pad;
|
||||
}
|
||||
else
|
||||
{
|
||||
const double pad = std::max(1.0e-9, std::fabs(low) * 0.05);
|
||||
low -= pad;
|
||||
high += pad;
|
||||
}
|
||||
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
lane.autoRange = false;
|
||||
lane.yMin = low;
|
||||
lane.yMax = high;
|
||||
BuildLaneVisuals();
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::ScaleLaneRange(int laneIndex, double factor, double anchorValue)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||||
@@ -904,6 +1008,68 @@ namespace jdi
|
||||
FormatValue(current), anchorRect);
|
||||
}
|
||||
|
||||
bool PlotView::ApplyLaneValueText(int laneIndex, bool editMax, const std::wstring& text)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (m_lanes.empty())
|
||||
{
|
||||
BuildLaneVisuals();
|
||||
}
|
||||
if (laneIndex >= static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring trimmed = Trim(text);
|
||||
if (trimmed.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
wchar_t* stop = nullptr;
|
||||
const double parsed = std::wcstod(trimmed.c_str(), &stop);
|
||||
if (stop == trimmed.c_str() || !std::isfinite(parsed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
const double span = visual.yMax - visual.yMin;
|
||||
const double minimumGap = (span > 0.0) ? span * 0.01 : std::max(1.0e-9, std::fabs(parsed) * 0.01);
|
||||
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
// 手工输入即视为取消自动自适应,与拖拽框选缩放的语义一致。
|
||||
// 必须先把当前(自适应计算出的)显示范围固化到 lane 里,再只改动被编辑的一端,
|
||||
// 否则另一端会回退到 Lane 的初始占位值,导致量程被莫名其妙地放大。
|
||||
if (lane.autoRange)
|
||||
{
|
||||
lane.autoRange = false;
|
||||
lane.yMin = visual.yMin;
|
||||
lane.yMax = visual.yMax;
|
||||
}
|
||||
if (editMax)
|
||||
{
|
||||
lane.yMax = parsed;
|
||||
if (!(lane.yMax > lane.yMin))
|
||||
{
|
||||
lane.yMin = lane.yMax - minimumGap;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
lane.yMin = parsed;
|
||||
if (!(lane.yMax > lane.yMin))
|
||||
{
|
||||
lane.yMax = lane.yMin + minimumGap;
|
||||
}
|
||||
}
|
||||
|
||||
BuildLaneVisuals();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::LaneRange(int laneIndex, double& yMin, double& yMax) const
|
||||
{
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()))
|
||||
@@ -961,7 +1127,7 @@ namespace jdi
|
||||
|
||||
// 复用绘制阶段的折线顶点:悬停检测通常与上一帧几何完全相同,
|
||||
// 不再为“鼠标移动但命中结果未变”重复扫描全量采样点。
|
||||
const PolylineCacheEntry* cached = AcquirePolyline(ref, resolved, lane);
|
||||
const PolylineCacheEntry* cached = AcquirePolyline(ref, resolved, lane, false);
|
||||
if (cached == nullptr)
|
||||
{
|
||||
continue;
|
||||
@@ -1831,7 +1997,7 @@ namespace jdi
|
||||
|
||||
const PlotView::PolylineCacheEntry* PlotView::AcquirePolyline(const SignalRef& ref,
|
||||
const ResolvedSignal& resolved,
|
||||
const LaneVisual& lane) const
|
||||
const LaneVisual& lane, bool coarse) const
|
||||
{
|
||||
if (resolved.data == nullptr || resolved.file == nullptr)
|
||||
{
|
||||
@@ -1851,7 +2017,7 @@ namespace jdi
|
||||
entry.viewMin == viewMin && entry.viewMax == viewMax && entry.plotX == m_plotRect.X &&
|
||||
entry.plotWidth == m_plotRect.Width && entry.laneY == lane.rect.Y &&
|
||||
entry.laneHeight == lane.rect.Height && entry.laneYMin == lane.yMin &&
|
||||
entry.laneYMax == lane.yMax)
|
||||
entry.laneYMax == lane.yMax && entry.coarse == coarse)
|
||||
{
|
||||
return &entry;
|
||||
}
|
||||
@@ -1876,13 +2042,14 @@ namespace jdi
|
||||
entry.laneHeight = lane.rect.Height;
|
||||
entry.laneYMin = lane.yMin;
|
||||
entry.laneYMax = lane.yMax;
|
||||
entry.coarse = coarse;
|
||||
|
||||
BuildPolyline(resolved, lane, entry.points, entry.breaks);
|
||||
BuildPolyline(resolved, lane, coarse, entry.points, entry.breaks);
|
||||
// BuildPolyline 通过引用填充,push_back 过程中不会再次进入本函数,引用安全。
|
||||
return &m_polylineCache.back();
|
||||
}
|
||||
|
||||
void PlotView::BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
void PlotView::BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane, bool coarse,
|
||||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks) const
|
||||
{
|
||||
points.clear();
|
||||
@@ -1936,7 +2103,9 @@ namespace jdi
|
||||
// 顶点预算由绘图区宽度决定(而不是固定值):折线顶点数直接决定
|
||||
// GDI+ 抗锯齿折线的绘制耗时,百万级采样文件若按固定 20000 顶点绘制,
|
||||
// 会把大量顶点堆在同一个像素列上(画出来完全重合),纯粹浪费 CPU。
|
||||
std::size_t budget = static_cast<std::size_t>(std::max(1.0f, m_plotRect.Width)) * kPolylinePointsPerPixel;
|
||||
// 拖拽期间进一步降半:顶点数就是绘制耗时,交互时用“2 像素一列”的包络即可。
|
||||
const std::size_t pointsPerPixel = coarse ? 1 : kPolylinePointsPerPixel;
|
||||
std::size_t budget = static_cast<std::size_t>(std::max(1.0f, m_plotRect.Width)) * pointsPerPixel;
|
||||
budget = std::max(budget, kMinPolylinePoints);
|
||||
budget = std::min(budget, kMaxPolylinePoints);
|
||||
|
||||
@@ -1951,22 +2120,33 @@ namespace jdi
|
||||
const std::size_t columns = std::max<std::size_t>(1, budget / 2);
|
||||
const float columnWidth = plotWidth / static_cast<float>(columns);
|
||||
|
||||
const std::vector<double>::const_iterator rangeBegin = time.begin() + static_cast<std::ptrdiff_t>(begin);
|
||||
const std::vector<double>::const_iterator rangeEnd = time.begin() + static_cast<std::ptrdiff_t>(end);
|
||||
|
||||
// 交互降级:拖拽期间限定每列扫描的采样点数。
|
||||
// 全量扫描是 O(可见采样数),千万级采样 × 多条信号会让拖拽掉到 10FPS 以下;
|
||||
// 每列取 32 个点已经足够看出包络形状,松手后会重新做一次全质量绘制。
|
||||
const std::size_t coarsePointsPerColumn = 32;
|
||||
|
||||
bool inSegment = false;
|
||||
std::size_t searchFrom = begin;
|
||||
for (std::size_t column = 0; column < columns; ++column)
|
||||
{
|
||||
const double t0 = viewMin + viewSpan * (static_cast<double>(column) / static_cast<double>(columns));
|
||||
const double t1 =
|
||||
viewMin + viewSpan * (static_cast<double>(column + 1) / static_cast<double>(columns));
|
||||
|
||||
// 列边界随 column 单调递增,从上一列的位置继续二分,
|
||||
// 把每次搜索的范围压到剩余区间,避免 2400 次全区间二分带来的缓存缺失。
|
||||
const std::size_t i0 = static_cast<std::size_t>(
|
||||
std::lower_bound(rangeBegin, rangeEnd, t0) - time.begin());
|
||||
std::lower_bound(time.begin() + static_cast<std::ptrdiff_t>(searchFrom), rangeEnd, t0) -
|
||||
time.begin());
|
||||
searchFrom = i0;
|
||||
const std::size_t i1 = (column + 1 == columns)
|
||||
? end
|
||||
: static_cast<std::size_t>(
|
||||
std::lower_bound(rangeBegin, rangeEnd, t1) - time.begin());
|
||||
std::lower_bound(time.begin() + static_cast<std::ptrdiff_t>(i0),
|
||||
rangeEnd, t1) -
|
||||
time.begin());
|
||||
if (i1 <= i0)
|
||||
{
|
||||
// 该列没有采样点:说明数据在此处存在时间间隙,断开当前折线。
|
||||
@@ -1974,18 +2154,31 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
|
||||
std::size_t scanStep = 1;
|
||||
if (coarse)
|
||||
{
|
||||
const std::size_t columnPoints = i1 - i0;
|
||||
if (columnPoints > coarsePointsPerColumn)
|
||||
{
|
||||
scanStep = columnPoints / coarsePointsPerColumn;
|
||||
}
|
||||
}
|
||||
|
||||
double low = std::numeric_limits<double>::infinity();
|
||||
double high = -std::numeric_limits<double>::infinity();
|
||||
std::size_t lowIndex = 0;
|
||||
std::size_t highIndex = 0;
|
||||
bool broken = false;
|
||||
for (std::size_t index = i0; index < i1; ++index)
|
||||
for (std::size_t index = i0; index < i1; index += scanStep)
|
||||
{
|
||||
const double value = values[index];
|
||||
if (!std::isfinite(value))
|
||||
{
|
||||
// 该列含无效值:包络绘制到本列结束,后续另起一段。
|
||||
broken = true;
|
||||
if (!coarse)
|
||||
{
|
||||
broken = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (value < low)
|
||||
@@ -2312,6 +2505,15 @@ namespace jdi
|
||||
const int waveSave = g.Save();
|
||||
g.SetClip(lane.rect, gd::CombineModeIntersect);
|
||||
|
||||
// 交互(拖拽平移/框选/拖动信号)期间关闭抗锯齿:波形折线是整帧中最贵的绘制调用,
|
||||
// 关闭抗锯齿可显著降低单帧耗时,使拖拽跟手;松开后立即恢复抗锯齿全质量重绘。
|
||||
const bool interactive = (m_dragMode != PlotDragMode::NONE);
|
||||
const gd::SmoothingMode previousSmoothing = g.GetSmoothingMode();
|
||||
if (interactive)
|
||||
{
|
||||
g.SetSmoothingMode(gd::SmoothingModeNone);
|
||||
}
|
||||
|
||||
for (const SignalRef& ref : laneSignals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
@@ -2322,7 +2524,9 @@ namespace jdi
|
||||
|
||||
const DrawMode mode = resolved.style->mode;
|
||||
// 顶点由“几何键”缓存提供:同一视图下重复绘制不再重算全量采样。
|
||||
const PolylineCacheEntry* cached = AcquirePolyline(ref, resolved, lane);
|
||||
// 拖拽期间用降级顶点(扫描点数受限),松手后自然回到全质量版本。
|
||||
const bool coarse = interactive;
|
||||
const PolylineCacheEntry* cached = AcquirePolyline(ref, resolved, lane, coarse);
|
||||
if (cached == nullptr)
|
||||
{
|
||||
continue;
|
||||
@@ -2392,6 +2596,12 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 交互期间临时关闭的抗锯齿在此恢复(g.Restore 也会还原状态,这里显式恢复以免依赖栈深)。
|
||||
if (interactive)
|
||||
{
|
||||
g.SetSmoothingMode(previousSmoothing);
|
||||
}
|
||||
|
||||
g.Restore(waveSave);
|
||||
}
|
||||
|
||||
@@ -2937,7 +3147,7 @@ namespace jdi
|
||||
|
||||
std::vector<gd::PointF> points;
|
||||
std::vector<std::size_t> breaks;
|
||||
BuildPolyline(resolved, sourceVisual, points, breaks);
|
||||
BuildPolyline(resolved, sourceVisual, false, points, breaks);
|
||||
|
||||
for (gd::PointF& point : points)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user