中心轴曲线配色 + 筛选框交互完善 + 双光标拖拽性能修复
- 中心轴边沿计数的平均值曲线改为「由信号色派生的高对比度颜色」:保持色相、 大幅拉开明度(与原始波形高对比),透明度与原信号一致;原信号改色时曲线 颜色随之变化,而光标显示该曲线数值时仍沿用原始信号的配色样式。 - 信号筛选框:内容区左右内缩改为与上下一致(不再「左右空隙明显更大」); 有输入时右侧显示清空 ✕(悬停高亮 + 手型光标,点击清空且保持编辑态); 筛选无结果时在列表区提示「无匹配的筛选信号」。 - 光标数值超出上下视界时,三角箭头标识下移 kCursorHandleReserve, 不再遮挡光标自身的三角把手(含双光标模式)。 - 性能:新增「装饰区域脏标记 + 绘图区局部回送」快路径——绘图区内的悬停/拖拽 期间跳过标签栏/工具栏/面板/分隔条的重绘,且只 BitBlt 绘图区矩形; WM_PAINT 依据更新区域是否落在绘图区内决定快慢路径,窗口缩放/遮挡露出/ 菜单与拾色器弹出等场景仍整帧重绘。 以 VERY_BIG_DATA.CSV 全信号双光标拖拽实测:单帧耗时由约 28ms 降至约 12ms。 同时修复拖拽期间残留的信号悬停提示气泡。
This commit is contained in:
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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1004,10 +1004,12 @@ namespace jdi
|
||||
* @param [IN] pinX 横向贴边位置(光标在视界外时的钉住横坐标)。
|
||||
* @param [IN] clipTop 网格可见区上边界。
|
||||
* @param [IN] clipBottom 网格可见区下边界。
|
||||
* @param [IN] topReserve 上边界额外需要预留的高度(例如时间光标的顶部三角把手,
|
||||
* 避免数据点三角压住光标把手)。默认 0。
|
||||
* @return 标记几何。
|
||||
*/
|
||||
DataMarker MakeDataMarker(float rawX, float rawY, float pinX, float clipTop,
|
||||
float clipBottom) const;
|
||||
float clipBottom, float topReserve = 0.0f) const;
|
||||
|
||||
/**
|
||||
* @brief 绘制数据点标记:视界内为圆点,越界时在对应边缘绘制指示方向的三角。
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace jdi
|
||||
const size_t kPolylinePointsPerPixel = 2; /**< 每个像素列允许的折线顶点数(上/下包络两顶点)。 */
|
||||
const size_t kMinPolylinePoints = 512; /**< 折线最少顶点数(窗口很窄时仍保留细节)。 */
|
||||
const float kEnvelopeMinPixelSpan = 0.5f; /**< 包络纵向跨度小于该像素值时合并为单个顶点。 */
|
||||
const float kCursorHandleReserve = 9.0f; /**< 光标顶部三角把手占用的高度(数据点贴边时需避让)。 */
|
||||
const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */
|
||||
const size_t kPolylineCacheLimit = 64; /**< 折线顶点缓存条目上限。 */
|
||||
const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */
|
||||
@@ -294,6 +295,42 @@ namespace jdi
|
||||
}
|
||||
return WithAlpha(style.color, static_cast<unsigned int>(opacity * 255.0 + 0.5));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 由信号色派生一个「高对比度」颜色:保持色相、大幅拉开明度。
|
||||
*
|
||||
* 用途:「边沿计数(中心轴)」的平均值曲线叠加在原始波形之上,
|
||||
* 若直接用信号色会被波形淹没。这里把信号色向黑或白方向大幅偏移,
|
||||
* 并**同时考虑与原始波形色以及与绘图区底色的距离**,取对比更强的一侧,
|
||||
* 保证曲线既能与波形区分、又能压在底图上看得清。
|
||||
*
|
||||
* 结果是从信号色派生的,因此原信号改颜色时曲线颜色会自动跟着变。
|
||||
*
|
||||
* @param [IN] color 原始信号色(0xAARRGGBB)。
|
||||
* @param [IN] background 绘图区底色,用于评估可见度。
|
||||
* @return 派生出的高对比度颜色(alpha 与输入一致,由调用方再覆盖)。
|
||||
*/
|
||||
std::uint32_t HighContrastDerived(std::uint32_t color, std::uint32_t background = 0xFFFDF6E3u)
|
||||
{
|
||||
const auto squaredDistance = [](std::uint32_t a, std::uint32_t b)
|
||||
{
|
||||
const double dr =
|
||||
static_cast<double>((a >> 16) & 0xFFu) - static_cast<double>((b >> 16) & 0xFFu);
|
||||
const double dg =
|
||||
static_cast<double>((a >> 8) & 0xFFu) - static_cast<double>((b >> 8) & 0xFFu);
|
||||
const double db = static_cast<double>(a & 0xFFu) - static_cast<double>(b & 0xFFu);
|
||||
return dr * dr + dg * dg + db * db;
|
||||
};
|
||||
|
||||
const std::uint32_t darker = MixColor(color, 0xFF000000u, 0.72);
|
||||
const std::uint32_t lighter = MixColor(color, 0xFFFFFFFFu, 0.72);
|
||||
// 评分:与原始波形色的区分度为主(权重 1.0),与底色的可见度为辅(权重 0.6)。
|
||||
const double scoreDarker =
|
||||
squaredDistance(darker, color) + 0.6 * squaredDistance(darker, background);
|
||||
const double scoreLighter =
|
||||
squaredDistance(lighter, color) + 0.6 * squaredDistance(lighter, background);
|
||||
return (scoreDarker >= scoreLighter) ? darker : lighter;
|
||||
}
|
||||
}
|
||||
PlotView::PlotView() = default;
|
||||
|
||||
@@ -5218,7 +5255,9 @@ namespace jdi
|
||||
|
||||
const float rawY = ValueToY(value, lane);
|
||||
// 上下越界的数据点不再被丢弃:与左右越界一致,贴边绘制指示方向的三角。
|
||||
const DataMarker marker = MakeDataMarker(rawX, rawY, pinX, clipTop, clipBottom);
|
||||
// 上边界额外预留光标顶部把手的高度,避免数据点三角压住光标把手。
|
||||
const DataMarker marker =
|
||||
MakeDataMarker(rawX, rawY, pinX, clipTop, clipBottom, kCursorHandleReserve * m_scale);
|
||||
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
|
||||
@@ -5276,8 +5315,8 @@ namespace jdi
|
||||
if (CenterValueAt(*resolved.file, *resolved.data, smooth, time, centerValue))
|
||||
{
|
||||
const float rawCenterY = ValueToY(centerValue, lane);
|
||||
const DataMarker centerMarker =
|
||||
MakeDataMarker(rawX, rawCenterY, pinX, clipTop, clipBottom);
|
||||
const DataMarker centerMarker = MakeDataMarker(
|
||||
rawX, rawCenterY, pinX, clipTop, clipBottom, kCursorHandleReserve * m_scale);
|
||||
{
|
||||
// 中心轴数据点的配色与原始信号一致(便于一一对应);
|
||||
// 但**始终按线段模式**绘制(不跟随原信号的散点模式)。
|
||||
@@ -5514,13 +5553,14 @@ namespace jdi
|
||||
}
|
||||
|
||||
PlotView::DataMarker PlotView::MakeDataMarker(float rawX, float rawY, float pinX, float clipTop,
|
||||
float clipBottom) const
|
||||
float clipBottom, float topReserve) const
|
||||
{
|
||||
DataMarker marker;
|
||||
// 上下越界时同样"钉"在网格可见边缘(留出三角形的高度,保证整体可见)。
|
||||
// 上下越界时"钉"在网格可见边缘(留出三角形的高度,保证整体可见);
|
||||
// 另外预留 topReserve(时间光标的顶部三角把手),避免数据点三角压住把手。
|
||||
const float margin = 7.0f * m_scale;
|
||||
marker.x = pinX;
|
||||
marker.y = std::max(clipTop + margin, std::min(rawY, clipBottom - margin));
|
||||
marker.y = std::max(clipTop + margin + topReserve, std::min(rawY, clipBottom - margin));
|
||||
marker.offLeft = (rawX < marker.x - 0.5f);
|
||||
marker.offRight = (rawX > marker.x + 0.5f);
|
||||
marker.offUp = (rawY < marker.y - 0.5f);
|
||||
@@ -5722,13 +5762,13 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 中心平均值曲线:与原始信号**同色**,只是更透明一些;线宽等其它样式同样跟随原信号,
|
||||
// 因此原信号改颜色/线宽/透明度时曲线会自动同步。
|
||||
// 中心平均值曲线:与原信号**同色相**但大幅拉开明度,使其与原始波形形成高对比
|
||||
// (原信号改颜色时曲线随之改变,因为它是从信号色派生出来的);
|
||||
// 透明度与原信号保持一致。
|
||||
const unsigned int baseAlpha = static_cast<unsigned int>(
|
||||
std::max(0.0, std::min(1.0, static_cast<double>(resolved.style->opacity))) * 255.0 + 0.5);
|
||||
const std::uint32_t derived =
|
||||
WithAlpha(StyleColor(*resolved.style),
|
||||
static_cast<unsigned int>(static_cast<double>(baseAlpha) * 0.55 + 0.5));
|
||||
const std::uint32_t derived = WithAlpha(HighContrastDerived(StyleColor(*resolved.style)),
|
||||
baseAlpha);
|
||||
gd::Pen pen(ToGdiColor(derived), std::max(1.0f, resolved.style->lineWidth) * m_scale);
|
||||
pen.SetLineJoin(gd::LineJoinRound);
|
||||
pen.SetStartCap(gd::LineCapRound);
|
||||
@@ -6137,6 +6177,13 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 其它拖拽(光标/平移/中键/横向光标/面板)期间不显示信号悬停提示:
|
||||
// 这些操作不会更新 m_hoverSignal,按下前的值会残留,表现为提示气泡"挂在原地"。
|
||||
if (m_dragMode != PlotDragMode::NONE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 悬停提示:光标贴近曲线时给出可拖拽说明(与时间光标的提示方式一致)。
|
||||
if (!m_hoverSignal.IsValid())
|
||||
{
|
||||
|
||||
@@ -293,6 +293,14 @@ namespace jdi
|
||||
*/
|
||||
bool HitTest(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief 查询该位置应使用的鼠标光标(当前仅「筛选信号」清空按钮 ✕ 使用手型)。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 需要特殊光标时返回光标句柄,否则返回 nullptr。
|
||||
*/
|
||||
HCURSOR QueryCursor(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief 将列表滚动到指定信号所在位置。
|
||||
* @param [IN] ref 目标信号引用。
|
||||
@@ -347,7 +355,8 @@ namespace jdi
|
||||
OPACITY_VALUE, /**< 不透明度数值框(可点击输入)。 */
|
||||
WIDTH_SLIDER, /**< 线宽滑块。 */
|
||||
WIDTH_VALUE, /**< 线宽数值框(可点击输入)。 */
|
||||
TOGGLE_ALL /**< 全部显示/隐藏按钮。 */
|
||||
TOGGLE_ALL, /**< 全部显示/隐藏按钮。 */
|
||||
FILTER_CLEAR /**< 「筛选信号」输入框右侧的清空 ✕。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -530,6 +539,17 @@ namespace jdi
|
||||
*/
|
||||
gd::RectF FilterFieldRect() const;
|
||||
|
||||
/**
|
||||
* @brief 计算「筛选信号」输入框右侧清空按钮 ✕ 的矩形(无输入时为空矩形)。
|
||||
* @return 清空按钮矩形。
|
||||
*/
|
||||
gd::RectF FilterClearRect() const;
|
||||
|
||||
/**
|
||||
* @brief 清空筛选内容(点击 ✕ 时调用),并刷新列表。
|
||||
*/
|
||||
void ClearFilter();
|
||||
|
||||
/**
|
||||
* @brief 计算指定行中名称区域的矩形(列表行内联编辑使用)。
|
||||
* @param [IN] row 行下标。
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace jdi
|
||||
const float kGroupSeparatorWidth = 1.0f; /**< 分组标签右侧分隔线宽度。 */
|
||||
const float kFilterHeight = 26.0f; /**< 「筛选信号」输入框高度。 */
|
||||
const float kFilterPadV = 5.0f; /**< 筛选框上下留白。 */
|
||||
const float kFilterTextPad = 4.0f; /**< 筛选框内文字/输入框相对外框的内缩(四边一致)。 */
|
||||
|
||||
/**
|
||||
* @brief 大小写无关的通配符匹配(`*` 匹配任意长度的任意字符序列)。
|
||||
@@ -218,6 +219,36 @@ namespace jdi
|
||||
return UiPointInRect(m_filterRect, static_cast<float>(x), static_cast<float>(y));
|
||||
}
|
||||
|
||||
void SignalPanel::ClearFilter()
|
||||
{
|
||||
if (m_filterText.empty() && !m_filterEditing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_filterText.clear();
|
||||
if (m_filterEditing)
|
||||
{
|
||||
// 编辑态下一并重置输入模型,避免残留旧文本(Begin 会整段选中/清空)。
|
||||
m_filterEdit.Begin(std::wstring(), false, EditFilter::ANY, 0u);
|
||||
}
|
||||
// 清空后回到完整列表:重建行集合、回到顶部。
|
||||
m_rowsDirty = true;
|
||||
m_scrollOffset = 0.0f;
|
||||
m_layoutDirty = true;
|
||||
}
|
||||
|
||||
HCURSOR SignalPanel::QueryCursor(int x, int y) const
|
||||
{
|
||||
// 清空按钮 ✕ 是独立可点击控件,用手型光标给出除高亮之外的额外交互提示。
|
||||
const gd::RectF clearRect = FilterClearRect();
|
||||
if (clearRect.Width > 0.0f &&
|
||||
UiPointInRect(clearRect, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void SignalPanel::BeginFilterEdit()
|
||||
{
|
||||
if (m_filterEditing)
|
||||
@@ -723,25 +754,47 @@ namespace jdi
|
||||
UiDrawRoundRect(g, m_filterRect, kCorner * m_scale,
|
||||
m_filterEditing ? m_theme->accent : m_theme->border,
|
||||
m_filterEditing ? 1.6f : 1.0f);
|
||||
|
||||
const float pad = kFilterTextPad * m_scale;
|
||||
const gd::RectF clearRect = FilterClearRect();
|
||||
// 有输入内容时在右侧留出 ✕ 的位置,文本不会压到按钮上。
|
||||
const float textRight = (clearRect.Width > 0.0f) ? clearRect.X : (m_filterRect.X + m_filterRect.Width);
|
||||
const float textWidth = std::max(8.0f, textRight - pad - (m_filterRect.X + pad));
|
||||
const gd::RectF textRect(m_filterRect.X + pad, m_filterRect.Y, textWidth,
|
||||
m_filterRect.Height);
|
||||
|
||||
if (m_filterEditing)
|
||||
{
|
||||
UiDrawEditField(g, FilterFieldRect(), m_filterEdit, m_fonts->uiSmall, m_scale);
|
||||
gd::RectF fieldRect = FilterFieldRect();
|
||||
fieldRect.Width = std::max(8.0f, textWidth + pad);
|
||||
UiDrawEditField(g, fieldRect, m_filterEdit, m_fonts->uiSmall, m_scale);
|
||||
}
|
||||
else if (m_filterText.empty())
|
||||
{
|
||||
UiText(g, L"筛选信号", m_fonts->uiSmall,
|
||||
gd::RectF(m_filterRect.X + 8.0f * m_scale, m_filterRect.Y,
|
||||
m_filterRect.Width - 16.0f * m_scale, m_filterRect.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
UiText(g, L"筛选信号", m_fonts->uiSmall, textRect, m_theme->textMuted, TextAlign::LEFT);
|
||||
}
|
||||
else
|
||||
{
|
||||
UiText(g, UiEllipsize(g, m_filterText, m_fonts->uiSmall,
|
||||
m_filterRect.Width - 16.0f * m_scale),
|
||||
m_fonts->uiSmall,
|
||||
gd::RectF(m_filterRect.X + 8.0f * m_scale, m_filterRect.Y,
|
||||
m_filterRect.Width - 16.0f * m_scale, m_filterRect.Height),
|
||||
m_theme->text, TextAlign::LEFT);
|
||||
UiText(g, UiEllipsize(g, m_filterText, m_fonts->uiSmall, textWidth), m_fonts->uiSmall,
|
||||
textRect, m_theme->text, TextAlign::LEFT);
|
||||
}
|
||||
|
||||
// 清空按钮 ✕:仅在筛选框有内容时出现,悬停高亮(鼠标光标保持箭头)。
|
||||
if (clearRect.Width > 0.0f)
|
||||
{
|
||||
const bool clearHot = (m_hoverStyle == StyleHit::FILTER_CLEAR);
|
||||
if (clearHot)
|
||||
{
|
||||
UiFillRoundRect(g, clearRect, 3.0f * m_scale, WithAlpha(m_theme->danger, 0x33));
|
||||
}
|
||||
const std::uint32_t crossColor = clearHot ? m_theme->danger : m_theme->textMuted;
|
||||
const float crossPad = clearRect.Width * 0.28f;
|
||||
UiDrawLine(g, clearRect.X + crossPad, clearRect.Y + crossPad,
|
||||
clearRect.X + clearRect.Width - crossPad,
|
||||
clearRect.Y + clearRect.Height - crossPad, crossColor, 1.5f * m_scale);
|
||||
UiDrawLine(g, clearRect.X + clearRect.Width - crossPad, clearRect.Y + crossPad,
|
||||
clearRect.X + crossPad, clearRect.Y + clearRect.Height - crossPad, crossColor,
|
||||
1.5f * m_scale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -755,6 +808,12 @@ namespace jdi
|
||||
UiText(g, L"尚未加载数据,请打开 CSV 文件", m_fonts->uiSmall, m_listRect, m_theme->textMuted,
|
||||
TextAlign::CENTER);
|
||||
}
|
||||
else if (m_rows.empty() && !m_filterText.empty())
|
||||
{
|
||||
// 筛选结果为空:明确提示"无匹配",而不是让人误以为数据丢失。
|
||||
UiText(g, L"无匹配的筛选信号", m_fonts->uiSmall, m_listRect, m_theme->textMuted,
|
||||
TextAlign::CENTER);
|
||||
}
|
||||
else
|
||||
{
|
||||
const int rowCount = static_cast<int>(m_rows.size());
|
||||
@@ -1393,11 +1452,23 @@ namespace jdi
|
||||
|
||||
gd::RectF SignalPanel::FilterFieldRect() const
|
||||
{
|
||||
// 输入框内容区:与占位提示/显示文本使用同一内缩,保证编辑时不跳动。
|
||||
const float inset = 7.0f * m_scale;
|
||||
return gd::RectF(m_filterRect.X + inset, m_filterRect.Y + 2.0f * m_scale,
|
||||
// 输入框内容区:左右与上下使用**同一内缩**,避免编辑态出现"左右空隙明显更大"的观感;
|
||||
// 占位提示与显示文本也用同一内缩,因此进入/退出编辑时文字不会跳动。
|
||||
const float inset = kFilterTextPad * m_scale;
|
||||
return gd::RectF(m_filterRect.X + inset, m_filterRect.Y + inset,
|
||||
std::max(8.0f, m_filterRect.Width - inset * 2.0f),
|
||||
std::max(8.0f, m_filterRect.Height - 4.0f * m_scale));
|
||||
std::max(8.0f, m_filterRect.Height - inset * 2.0f));
|
||||
}
|
||||
|
||||
gd::RectF SignalPanel::FilterClearRect() const
|
||||
{
|
||||
if (m_filterText.empty())
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
const float size = std::max(12.0f, m_filterRect.Height - kFilterTextPad * 2.0f * m_scale);
|
||||
const float right = m_filterRect.X + m_filterRect.Width - kFilterTextPad * m_scale;
|
||||
return gd::RectF(right - size, m_filterRect.Y + (m_filterRect.Height - size) * 0.5f, size, size);
|
||||
}
|
||||
|
||||
gd::RectF SignalPanel::EditFieldRect() const
|
||||
@@ -1610,6 +1681,15 @@ namespace jdi
|
||||
{
|
||||
if (m_filterEditing)
|
||||
{
|
||||
// 编辑态下 ✕ 位于输入框内容区之内,必须优先于插入符定位处理,
|
||||
// 否则点击会被当成"把光标移到此处"而清空不了内容。
|
||||
const gd::RectF clearRect = FilterClearRect();
|
||||
if (clearRect.Width > 0.0f &&
|
||||
UiPointInRect(clearRect, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
ClearFilter();
|
||||
return true;
|
||||
}
|
||||
return UiEditFieldMouseDown(m_fonts->uiSmall, FilterFieldRect(), m_filterEdit, m_scale, x, y);
|
||||
}
|
||||
if (m_editField == StyleEditField::NONE)
|
||||
@@ -1786,6 +1866,11 @@ namespace jdi
|
||||
{
|
||||
hoverStyle = StyleHit::TOGGLE_ALL;
|
||||
}
|
||||
else if (UiPointInRect(FilterClearRect(), fx, fy))
|
||||
{
|
||||
// 筛选框右侧的清空 ✕(仅在筛选框有内容时存在)。
|
||||
hoverStyle = StyleHit::FILTER_CLEAR;
|
||||
}
|
||||
else if (m_tab != nullptr && StyleOf(SelectedRef()) != nullptr)
|
||||
{
|
||||
for (int index = 0; index < m_colorCount; ++index)
|
||||
@@ -1859,6 +1944,12 @@ namespace jdi
|
||||
// 「筛选信号」输入框:单击进入编辑;点击其它位置则先结束筛选编辑(文本已实时生效)。
|
||||
if (IsOnFilterBox(x, y))
|
||||
{
|
||||
// 右侧 ✕:清空筛选内容(不退出编辑态,便于继续输入)。
|
||||
if (UiPointInRect(FilterClearRect(), fx, fy))
|
||||
{
|
||||
ClearFilter();
|
||||
return true;
|
||||
}
|
||||
BeginFilterEdit();
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user