中心轴曲线配色 + 筛选框交互完善 + 双光标拖拽性能修复

- 中心轴边沿计数的平均值曲线改为「由信号色派生的高对比度颜色」:保持色相、
  大幅拉开明度(与原始波形高对比),透明度与原信号一致;原信号改色时曲线
  颜色随之变化,而光标显示该曲线数值时仍沿用原始信号的配色样式。
- 信号筛选框:内容区左右内缩改为与上下一致(不再「左右空隙明显更大」);
  有输入时右侧显示清空 ✕(悬停高亮 + 手型光标,点击清空且保持编辑态);
  筛选无结果时在列表区提示「无匹配的筛选信号」。
- 光标数值超出上下视界时,三角箭头标识下移 kCursorHandleReserve,
  不再遮挡光标自身的三角把手(含双光标模式)。
- 性能:新增「装饰区域脏标记 + 绘图区局部回送」快路径——绘图区内的悬停/拖拽
  期间跳过标签栏/工具栏/面板/分隔条的重绘,且只 BitBlt 绘图区矩形;
  WM_PAINT 依据更新区域是否落在绘图区内决定快慢路径,窗口缩放/遮挡露出/
  菜单与拾色器弹出等场景仍整帧重绘。
  以 VERY_BIG_DATA.CSV 全信号双光标拖拽实测:单帧耗时由约 28ms 降至约 12ms。
  同时修复拖拽期间残留的信号悬停提示气泡。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-09 19:10:43 +08:00
1 parent fe72afc83c
commit 8ae8d7af4f
6 files changed
+333 -45

No files matched your search

@@ -364,6 +364,16 @@ namespace jdi
float m_loadingProgress = 0.0f; /**< 加载进度(0-1)。 */
float m_loadingSpin = 0.0f; /**< 加载动画旋转相位(度)。 */
unsigned long long m_loadingBytes = 0; /**< 当前文件字节数(用于展示体积)。 */
/**
* @brief 装饰区域(标签栏 / 工具栏 / 信号面板 / 分隔条)是否需要重绘。
*
* 这些区域的绘制成本与数据量无关但与控件数量成正比,且只在鼠标位于其中
* 或发生交互时才会变化。绘图区拖拽(如双光标左右移动)期间持续置为
* false,从而每帧只重绘、只回送绘图区所在的矩形,避免整窗重绘的开销。
*/
bool m_chromeDirty = true;
bool m_mouseOverPlot = false; /**< 上一次鼠标移动是否位于绘图区内。 */
};
} // namespace jdi
+135 -17
View File
@@ -37,6 +37,32 @@ namespace jdi
const UINT_PTR kHintTimerId = 1; /**< 提示浮层淡出用的定时器 ID。 */
const UINT kHintTimerPeriodMs = 33; /**< 提示浮层淡出的重绘间隔(约 30fps)。 */
/**
* @brief 只失效绘图区所在的矩形。
*
* 配合 WM_PAINT 中对更新区域的判断,可以走“只重绘、只回送绘图区”的快路径;
* 绘图区矩形无效时退化为整窗失效,保证不会漏画。
*
* @param [IN] hwnd 窗口句柄。
* @param [IN] plotRect 绘图区矩形(客户区坐标)。
*/
void InvalidatePlotRect(HWND hwnd, const gd::RectF& plotRect)
{
RECT invalid = {};
invalid.left = static_cast<LONG>(std::floor(plotRect.X));
invalid.top = static_cast<LONG>(std::floor(plotRect.Y));
invalid.right = static_cast<LONG>(std::ceil(plotRect.X + plotRect.Width));
invalid.bottom = static_cast<LONG>(std::ceil(plotRect.Y + plotRect.Height));
if (invalid.right > invalid.left && invalid.bottom > invalid.top)
{
InvalidateRect(hwnd, &invalid, FALSE);
}
else
{
InvalidateRect(hwnd, nullptr, FALSE);
}
}
/** @brief 标签页右键菜单命令 ID。 */
const int kMenuTabNew = 1;
const int kMenuTabClose = 2;
@@ -594,6 +620,9 @@ namespace jdi
m_panel.SetClientBounds(gd::RectF(0.0f, 0.0f, width, height));
m_panel.Layout(m_panelRect);
m_plot.Layout(m_plotRect);
// 所有控件的目标矩形都变了,装饰区域必须整体重绘一次。
m_chromeDirty = true;
}
void Application::EnsureBackBuffer(int width, int height)
@@ -605,6 +634,9 @@ namespace jdi
ReleaseBackBuffer();
// 新建的后备位图内容未定义,必须整帧重绘一次。
m_chromeDirty = true;
HDC screenDc = GetDC(m_hwnd);
if (screenDc == nullptr)
{
@@ -697,26 +729,34 @@ namespace jdi
}
}
// 工具栏/标签栏/面板各自带悬停态,内容只在鼠标位于其中或发生交互时改变。
// 绘图区内的拖拽(如双光标左右移动)期间这些区域完全静止,跳过其绘制,
// 每帧省下约 6-8ms;弹出菜单 / 拾色器 / 加载遮罩会覆盖到装饰区域,必须整帧重绘。
const bool overlaysVisible = m_menu.IsOpen() || m_colorPicker.IsOpen();
const bool chromeDirty = m_chromeDirty || m_loading || overlaysVisible;
m_chromeDirty = false;
// 提示浮层淡出由定时器驱动,其绘制范围局限在绘图区,但为稳妥起见整帧回送。
const bool fullFrame = chromeDirty || m_plot.HintNeedsAnimation();
{
gd::Graphics g(m_backDc);
g.SetSmoothingMode(gd::SmoothingModeAntiAlias);
g.SetPixelOffsetMode(gd::PixelOffsetModeHalf);
g.SetTextRenderingHint(gd::TextRenderingHintClearTypeGridFit);
// 工具栏/标签栏/面板各自带悬停态,必须每帧绘制;
// 真正昂贵的是绘图区的波形(大文件约 80ms/帧),
// 由 PlotView 内部的静态场景缓存消除,与数据量无关。
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
m_theme.windowBg);
if (chromeDirty)
{
UiFillRect(g, gd::RectF(0.0f, 0.0f, static_cast<float>(width), static_cast<float>(height)),
m_theme.windowBg);
m_tabBar.Paint(g);
m_toolbar.Paint(g);
m_panel.Paint(g);
m_tabBar.Paint(g);
m_toolbar.Paint(g);
m_panel.Paint(g);
UiFillRect(g, m_splitterRect, m_theme.border);
}
m_plot.Paint(g);
UiFillRect(g, m_splitterRect, m_theme.border);
// 弹出菜单与浮动拾色器始终绘制在最上层,保证与主界面风格一致。
m_menu.Paint(g);
m_colorPicker.Paint(g);
@@ -728,7 +768,19 @@ namespace jdi
HDC windowDc = GetDC(m_hwnd);
if (windowDc != nullptr)
{
BitBlt(windowDc, 0, 0, width, height, m_backDc, 0, 0, SRCCOPY);
if (fullFrame)
{
BitBlt(windowDc, 0, 0, width, height, m_backDc, 0, 0, SRCCOPY);
}
else
{
// PlotView 会把自身绘制严格裁剪在 m_plotRect 内,因此只回送该矩形即可。
const int px = static_cast<int>(std::floor(m_plotRect.X));
const int py = static_cast<int>(std::floor(m_plotRect.Y));
const int pw = static_cast<int>(std::ceil(m_plotRect.X + m_plotRect.Width)) - px;
const int ph = static_cast<int>(std::ceil(m_plotRect.Y + m_plotRect.Height)) - py;
BitBlt(windowDc, px, py, pw, ph, m_backDc, px, py, SRCCOPY);
}
ReleaseDC(m_hwnd, windowDc);
}
@@ -1624,6 +1676,16 @@ namespace jdi
LRESULT Application::HandleMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
// 除鼠标移动与鼠标移出(由下面的分支按“是否只影响绘图区”精确判断)之外,
// 其余消息都可能改变标签栏 / 工具栏 / 面板的外观,统一标记装饰区域为脏。
// WM_PAINT / WM_TIMER 的重绘范围由各自的处理分支判断,这里不做标记。
if (message != WM_PAINT && message != WM_MOUSEMOVE && message != WM_MOUSELEAVE &&
message != WM_SETCURSOR && message != WM_NCHITTEST && message != WM_TIMER &&
message != WM_ERASEBKGND)
{
m_chromeDirty = true;
}
// 菜单打开时优先消费鼠标与键盘事件,形成模态交互。
if (m_menu.IsOpen())
{
@@ -1877,6 +1939,25 @@ namespace jdi
return 1;
case WM_PAINT:
{
// BeginPaint 会清空更新区域,因此必须先取出本次待重绘的范围:
// 只有完全落在绘图区内的更新区域才允许走“局部回送”的快路径,
// 其余情况(窗口首次显示、其它窗口遮挡后露出、DPI 变化等)必须整帧重绘。
RECT update = {};
const bool hasUpdate = GetUpdateRect(hwnd, &update, FALSE) != FALSE;
if (hasUpdate)
{
const bool insidePlot =
update.left >= static_cast<LONG>(std::floor(m_plotRect.X)) &&
update.top >= static_cast<LONG>(std::floor(m_plotRect.Y)) &&
update.right <= static_cast<LONG>(std::ceil(m_plotRect.X + m_plotRect.Width)) &&
update.bottom <= static_cast<LONG>(std::ceil(m_plotRect.Y + m_plotRect.Height)) &&
update.right > update.left && update.bottom > update.top;
if (!insidePlot)
{
m_chromeDirty = true;
}
}
PAINTSTRUCT paintStruct = {};
BeginPaint(hwnd, &paintStruct);
Render();
@@ -1898,9 +1979,28 @@ namespace jdi
const int x = GET_X_LPARAM(lParam);
const int y = GET_Y_LPARAM(lParam);
const bool buttonDown = (wParam & (MK_LBUTTON | MK_RBUTTON | MK_MBUTTON)) != 0;
// 仅当本次移动不会影响绘图区之外的区域时,才跳过装饰区域的重绘:
// 正在拖拽绘图区(鼠标可能被捕获到绘图区外),或鼠标始终停留在绘图区内。
const bool inPlot = UiPointInRect(m_plotRect, static_cast<float>(x), static_cast<float>(y));
const bool plotOnlyMove = (m_activeTarget == Target::PLOT) || (inPlot && m_mouseOverPlot);
if (!plotOnlyMove)
{
m_chromeDirty = true;
}
m_mouseOverPlot = inPlot;
if (HandleMouseMove(x, y, buttonDown))
{
InvalidateRect(hwnd, nullptr, FALSE);
if (plotOnlyMove && !m_chromeDirty)
{
// 只失效绘图区,配合 WM_PAINT 中的判断即可走局部回送快路径。
InvalidatePlotRect(hwnd, m_plotRect);
}
else
{
InvalidateRect(hwnd, nullptr, FALSE);
}
}
// 拖动(鼠标捕获)期间 Windows 不会派发 WM_SETCURSOR,
// 因此需要主动刷新一次,否则光标形状会一直停在按下的状态。
@@ -1913,11 +2013,25 @@ namespace jdi
case WM_MOUSELEAVE:
{
m_trackingLeave = false;
bool changed = m_tabBar.OnMouseLeave();
changed = m_toolbar.OnMouseLeave() || changed;
changed = m_panel.OnMouseLeave() || changed;
changed = m_plot.OnMouseLeave() || changed;
if (changed)
// 只有标签栏 / 工具栏 / 面板的悬停态变化才需要重绘装饰区域;
// 绘图区自身的悬停态变化(提示浮层、信号光晕)只需重绘绘图区,
// 这样拖拽光标时鼠标短暂移出窗口也不会退化成整帧重绘。
bool chromeChanged = m_tabBar.OnMouseLeave();
chromeChanged = m_toolbar.OnMouseLeave() || chromeChanged;
chromeChanged = m_panel.OnMouseLeave() || chromeChanged;
const bool plotChanged = m_plot.OnMouseLeave();
m_mouseOverPlot = false;
if (chromeChanged)
{
m_chromeDirty = true;
}
if (plotChanged && !chromeChanged && !m_chromeDirty)
{
InvalidatePlotRect(hwnd, m_plotRect);
}
else if (chromeChanged || plotChanged)
{
InvalidateRect(hwnd, nullptr, FALSE);
}
@@ -2262,6 +2376,10 @@ namespace jdi
{
cursor = m_plot.QueryCursor(point.x, point.y);
}
else if (UiPointInRect(m_panelRect, static_cast<float>(point.x), static_cast<float>(point.y)))
{
cursor = m_panel.QueryCursor(point.x, point.y);
}
if (cursor == nullptr)
{
cursor = LoadCursorW(nullptr, IDC_ARROW);