绘图交互完善 + 稠密区域拖拽性能修复(VERY_BIG_DATA 2ms/大格不再卡顿)
一、功能修正 - 修复「光标数据点在上下视界外不绘制」的问题,并让上下越界与左右越界一样用三角表示: 新增 `DataMarker` / `PaintDataMarker()`,统一处理「可视区内画圆点、越界则钉在网格边上 画三角指示方向」的四向情形(左/右/上/下)。光标取值与中心轴取值都改走这一套, 因此纵向被裁掉的数据点不再消失。 - 「边沿计数(中心轴)」的中心平均值曲线改为**跟随原信号的样式**: 与原始信号同色但更透明,线宽同样跟随(lineWidth + 0.6,下限 1.4), 原信号改颜色/线宽/透明度后曲线同步变化(曲线参与场景指纹,样式变化即失效重绘)。 光标显示中心值时的胶囊也改用**原始信号颜色**(一一对应),并**始终按线段模式**着色, 不再使用光标色。 - 「拆分当前网格」在可点击时追加尾缀「(N个信号)」(N = 该网格内信号数), 只有 1 条信号(不可点击)时不加尾缀。 二、新增:左侧信号筛选框 - 信号列表上方新增「筛选信号」输入框:空内容时显示灰色占位提示,单击进入编辑, 点击别处或回车结束编辑;列表内容随输入**实时**变化。 - 支持 `*` 通配符(`*` 等价于"任意字符",因此 `*` 单独出现等价于不过滤),忽略大小写。 - 筛选结果**仍以组为单位**,组默认展开,只显示匹配的信号;组内无匹配信号时整组不显示。 - 复用统一的 `TextEdit` 编辑模型,因此光标/选区/Ctrl+A/C/X/V 等行为与其它输入框一致。 三、性能:修复 VERY_BIG_DATA.CSV 在 2ms/大格密集区域拖拽卡顿 以「显示全部 5 条信号 + 时间轴缩放到 2ms 一大格」为场景逐段打点定位后修复: - **主因:稠密包络的锯齿描边光栅化**。抽稀时每列产生 min/max 两个顶点交替的锯齿, 2000+ 条近乎满幅的斜线段在 GDI+ 抗锯齿下要光栅化几十万像素(实测每网格 14ms, 5 网格约 70ms/帧)。 改为**包络带一次填充**:逐列记录上/下包络,收段时拼成「上包络正向 + 下包络反向」的 闭合多边形,用一条 `GraphicsPath` + `FillPath` 绘制(显式 `FillModeWinding`, 自交包络不会挖洞)。基准测试(1224 列):14.26ms → **1.69ms(约 8.4 倍)**, 且**保留抗锯齿**、静态帧与拖拽帧完全一致(不是"交互降级")。 逐像素对比确认观感等价(墨量覆盖率 90.8% vs 91.3%)。 包络带同时用于静态绘制与 MOVE_SIGNAL 的拖拽虚影。 - **大面积不透明填充改走 GDI**:`UiFillRect` 在面积 ≥ 20000px² 且颜色不透明时使用 `GetHDC + FillRect`(GDI+ FillRectangle 约 2.2ns/像素,整窗背景 1.7M 像素要 0.9ms、 面板背景 1.9ms)。半透明填充仍走 GDI+(GDI 不做 alpha 混色);小面积仍走 GDI+ (避免 GetHDC 固定开销)。顺序安全:GetHDC 会先刷入已排队的 GDI+ 内容。 - **`UiTextWidth` 不再触发 GDI+ 批次刷新**:改为复用 `UiMeasureTextWidth` (`GetDC(nullptr)`),`UiEllipsize` 逐字符回退时的十几次刷新被完全消除。 实测(125% 缩放、1725x996、5 信号、2ms 一大格): 绘图区绘制 47~87ms → **3.6~4.5ms**;整帧最大 196.8ms → **约 17~21ms(≈50~60fps)**, 与空闲帧基本持平,即"拖拽与不拖拽一样流畅"。 验证(VERY_BIG_DATA.CSV;面板行坐标因新增筛选框整体下移,复选框客户区 (61,250) 起、行距 33): - 筛选框:输入 `B` 后只剩「编码器B相」「电流钳采集(ABSx10)」,组仍展开;清空即恢复。 - 拆分:合并 3 条信号后右键显示「拆分当前网格(3个信号)」;单信号网格该项置灰无尾缀。 - 光标/中心轴:竖线光标取值与中心值胶囊均按四向越界正确显示三角/圆点。 - 稠密区域:5 信号全选 + 缩放到 2ms 一大格,左键拖拽信号、左键平移、中键平移均流畅; 渲染观感与修改前等价(叠图对比)。 - make clean && make all 无警告无错误。
This commit is contained in:
1 parent
82a29362eb
commit
fe72afc83c
6 files changed
+721
-126
No files matched your search
@@ -1442,10 +1442,14 @@ namespace jdi
|
||||
items.push_back(fitLaneTime);
|
||||
|
||||
// 「拆分当前网格」:把该网格内的信号按个拆成独立网格,插到其下方依次排列。
|
||||
// 可点击时在后方追加「(N个信号)」说明将拆出的网格数;只有 1 条信号时置灰且不加尾缀。
|
||||
MenuItem splitLane;
|
||||
splitLane.label = L"拆分当前网格";
|
||||
const std::size_t laneSignalCount = m_plot.LaneSignalCount(laneIndex);
|
||||
splitLane.label = (laneSignalCount > 1)
|
||||
? (L"拆分当前网格(" + std::to_wstring(laneSignalCount) + L"个信号)")
|
||||
: L"拆分当前网格";
|
||||
splitLane.id = PLOT_CMD_LANE_SPLIT;
|
||||
splitLane.enabled = m_plot.LaneSignalCount(laneIndex) > 1;
|
||||
splitLane.enabled = laneSignalCount > 1;
|
||||
items.push_back(splitLane);
|
||||
|
||||
// 实验性功能(二级菜单):横线上方,鼠标悬停即向右展开。
|
||||
|
||||
@@ -867,7 +867,9 @@ namespace jdi
|
||||
* @param [OUT] breaks 输出每段折线的起始下标(用于处理缺失值断线)。
|
||||
*/
|
||||
void BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks) const;
|
||||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks,
|
||||
std::vector<gd::PointF>& band, std::vector<std::size_t>& bandBreaks,
|
||||
bool& bandMode) const;
|
||||
|
||||
/**
|
||||
* @brief 绘制单个网格。
|
||||
@@ -982,6 +984,42 @@ namespace jdi
|
||||
void PaintCenterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
int smooth) const;
|
||||
|
||||
/**
|
||||
* @brief 数据点标记的位置与"越界方向"(用于决定画圆点还是贴边三角)。
|
||||
*/
|
||||
struct DataMarker
|
||||
{
|
||||
float x = 0.0f; /**< 标记锚点横坐标(已夹在可视区内)。 */
|
||||
float y = 0.0f; /**< 标记锚点纵坐标(已夹在可视区内)。 */
|
||||
bool offLeft = false; /**< 数据点位于左侧视界之外。 */
|
||||
bool offRight = false; /**< 数据点位于右侧视界之外。 */
|
||||
bool offUp = false; /**< 数据点位于上方视界之外。 */
|
||||
bool offDown = false; /**< 数据点位于下方视界之外。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 计算数据点标记:把越界的数据点"钉"在网格可见边缘,并记录越界方向。
|
||||
* @param [IN] rawX 数据点的原始横坐标。
|
||||
* @param [IN] rawY 数据点的原始纵坐标。
|
||||
* @param [IN] pinX 横向贴边位置(光标在视界外时的钉住横坐标)。
|
||||
* @param [IN] clipTop 网格可见区上边界。
|
||||
* @param [IN] clipBottom 网格可见区下边界。
|
||||
* @return 标记几何。
|
||||
*/
|
||||
DataMarker MakeDataMarker(float rawX, float rawY, float pinX, float clipTop,
|
||||
float clipBottom) const;
|
||||
|
||||
/**
|
||||
* @brief 绘制数据点标记:视界内为圆点,越界时在对应边缘绘制指示方向的三角。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
* @param [IN] marker 标记几何。
|
||||
* @param [IN] radius 圆点半径。
|
||||
* @param [IN] fillColor 前景色(圆点/三角内部)。
|
||||
* @param [IN] ringColor 描边色(圆环/三角外框)。
|
||||
*/
|
||||
void PaintDataMarker(gd::Graphics& g, const DataMarker& marker, float radius,
|
||||
std::uint32_t fillColor, std::uint32_t ringColor) const;
|
||||
|
||||
/**
|
||||
* @brief 放置一个光标取值胶囊:先夹到网格可见范围内,再向下(必要时向上)避让
|
||||
* 已占用的标注矩形,最后登记到 `m_cursorLabelRects`。
|
||||
@@ -1254,6 +1292,21 @@ namespace jdi
|
||||
double laneYMax = 0.0; /**< 网格幅值上界。 */
|
||||
std::vector<gd::PointF> points; /**< 折线顶点。 */
|
||||
std::vector<std::size_t> breaks; /**< 每段折线在 points 中的起始下标。 */
|
||||
|
||||
/**
|
||||
* @brief 是否处于「稠密包络」模式(采样点密度超过像素分辨率)。
|
||||
*
|
||||
* 此时 `points` 是每列 min/max 交替的锯齿,逐段描边的光栅化开销极大
|
||||
* (实测每网格约 14ms);改用下面的「包络带」一次填充后约 1.7ms,
|
||||
* 且保留抗锯齿,静态与拖拽帧的观感完全一致。
|
||||
*/
|
||||
bool bandMode = false;
|
||||
|
||||
/** 包络带顶点:每段为「上包络正向 + 下包络反向」,闭合后即为该段的填充多边形。 */
|
||||
std::vector<gd::PointF> band;
|
||||
|
||||
/** 每段包络带在 `band` 中的起始下标。 */
|
||||
std::vector<std::size_t> bandBreaks;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -295,7 +295,6 @@ namespace jdi
|
||||
return WithAlpha(style.color, static_cast<unsigned int>(opacity * 255.0 + 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
PlotView::PlotView() = default;
|
||||
|
||||
PlotView::~PlotView()
|
||||
@@ -984,8 +983,7 @@ namespace jdi
|
||||
}
|
||||
|
||||
std::size_t PlotView::LaneSignalCount(int laneIndex) const
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 ||
|
||||
{ if (m_tab == nullptr || laneIndex < 0 ||
|
||||
laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return 0;
|
||||
@@ -3596,16 +3594,22 @@ namespace jdi
|
||||
entry.laneYMin = lane.yMin;
|
||||
entry.laneYMax = lane.yMax;
|
||||
|
||||
BuildPolyline(resolved, lane, entry.points, entry.breaks);
|
||||
BuildPolyline(resolved, lane, entry.points, entry.breaks, entry.band, entry.bandBreaks,
|
||||
entry.bandMode);
|
||||
// BuildPolyline 通过引用填充,push_back 过程中不会再次进入本函数,引用安全。
|
||||
return &m_polylineCache.back();
|
||||
}
|
||||
|
||||
void PlotView::BuildPolyline(const ResolvedSignal& resolved, const LaneVisual& lane,
|
||||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks) const
|
||||
std::vector<gd::PointF>& points, std::vector<std::size_t>& breaks,
|
||||
std::vector<gd::PointF>& band, std::vector<std::size_t>& bandBreaks,
|
||||
bool& bandMode) const
|
||||
{
|
||||
points.clear();
|
||||
breaks.clear();
|
||||
band.clear();
|
||||
bandBreaks.clear();
|
||||
bandMode = false;
|
||||
|
||||
if (resolved.data == nullptr || resolved.file == nullptr)
|
||||
{
|
||||
@@ -3683,6 +3687,38 @@ namespace jdi
|
||||
(coarseStride > 0) && !coarseMin.empty() && (coarseMin.size() == coarseMax.size());
|
||||
|
||||
bool inSegment = false;
|
||||
|
||||
// 稠密包络带:逐列记录上/下包络,收段时拼成「上包络正向 + 下包络反向」的
|
||||
// 闭合多边形。绘制时一次 FillPath 即可,实测比逐段描边快约 8 倍
|
||||
// (1224 列:14.3ms → 1.7ms),且**保留抗锯齿**,
|
||||
// 因此静态帧与拖拽帧的观感完全一致(不是"交互降级")。
|
||||
bandMode = true;
|
||||
const float strokeHalf =
|
||||
std::max(1.0f, resolved.style->lineWidth) * m_scale * 0.5f;
|
||||
std::vector<gd::PointF> segmentTop;
|
||||
std::vector<gd::PointF> segmentBottom;
|
||||
const auto flushBandSegment = [&band, &bandBreaks, &segmentTop, &segmentBottom]()
|
||||
{
|
||||
if (segmentTop.size() < 2)
|
||||
{
|
||||
segmentTop.clear();
|
||||
segmentBottom.clear();
|
||||
return;
|
||||
}
|
||||
bandBreaks.push_back(band.size());
|
||||
for (const gd::PointF& point : segmentTop)
|
||||
{
|
||||
band.push_back(point);
|
||||
}
|
||||
// 下包络反向,形成闭合环。
|
||||
for (std::size_t index = segmentBottom.size(); index-- > 0;)
|
||||
{
|
||||
band.push_back(segmentBottom[index]);
|
||||
}
|
||||
segmentTop.clear();
|
||||
segmentBottom.clear();
|
||||
};
|
||||
|
||||
std::size_t searchFrom = begin;
|
||||
for (std::size_t column = 0; column < columns; ++column)
|
||||
{
|
||||
@@ -3706,6 +3742,7 @@ namespace jdi
|
||||
{
|
||||
// 该列没有采样点:说明数据在此处存在时间间隙,断开当前折线。
|
||||
inSegment = false;
|
||||
flushBandSegment();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3771,6 +3808,7 @@ namespace jdi
|
||||
{
|
||||
// 本列全是无效值。
|
||||
inSegment = false;
|
||||
flushBandSegment();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3780,6 +3818,10 @@ namespace jdi
|
||||
const float yBottom =
|
||||
static_cast<float>(ClampDouble(ValueToY(low, lane), limitTop, limitBottom));
|
||||
|
||||
// 包络带每列都记录(上下各外扩半个线宽,保证细波动也有可见粗细)。
|
||||
segmentTop.push_back(gd::PointF(x, yTop - strokeHalf));
|
||||
segmentBottom.push_back(gd::PointF(x, yBottom + strokeHalf));
|
||||
|
||||
if (yBottom - yTop < kEnvelopeMinPixelSpan)
|
||||
{
|
||||
// 波动不足半个像素:只留一个顶点,避免无意义的重复顶点。
|
||||
@@ -3813,9 +3855,11 @@ namespace jdi
|
||||
if (broken)
|
||||
{
|
||||
inSegment = false;
|
||||
flushBandSegment();
|
||||
}
|
||||
}
|
||||
|
||||
flushBandSegment();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4431,6 +4475,7 @@ namespace jdi
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(gd::RectF(m_area.X, clipTop, m_area.Width, clipBottom - clipTop));
|
||||
|
||||
|
||||
// 幅值网格线(先画线,标尺稍后作为覆盖层绘制,避免被波形遮挡)。
|
||||
// 刻度密度随网格高度自适应:所有网格高度相同 → 各网格的刻度数量一致,
|
||||
// 不会出现“有的网格只有 1 条标签、有的有 3 条”的不一致观感。
|
||||
@@ -4561,21 +4606,55 @@ namespace jdi
|
||||
|
||||
if (drawLine && !breaks.empty())
|
||||
{
|
||||
gd::Pen pen(ToGdiColor(StyleColor(*resolved.style)),
|
||||
resolved.style->lineWidth * m_scale);
|
||||
pen.SetLineJoin(gd::LineJoinRound);
|
||||
pen.SetStartCap(gd::LineCapRound);
|
||||
pen.SetEndCap(gd::LineCapRound);
|
||||
|
||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||
const std::uint32_t lineColor = StyleColor(*resolved.style);
|
||||
if (cached->bandMode && !cached->bandBreaks.empty())
|
||||
{
|
||||
const std::size_t start = breaks[k];
|
||||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||||
if (stop <= start + 1)
|
||||
// 稠密包络:一次 FillPath 填充包络带。
|
||||
// **必须显式设为缠绕填充规则**:包络带自交(相邻列上下包络交叉)时,
|
||||
// 交替(奇偶)规则会在交叠处挖出空洞。
|
||||
gd::GraphicsPath bandPath;
|
||||
bandPath.SetFillMode(gd::FillModeWinding);
|
||||
for (std::size_t k = 0; k < cached->bandBreaks.size(); ++k)
|
||||
{
|
||||
continue;
|
||||
const std::size_t start = cached->bandBreaks[k];
|
||||
const std::size_t stop = (k + 1 < cached->bandBreaks.size())
|
||||
? cached->bandBreaks[k + 1]
|
||||
: cached->band.size();
|
||||
const std::size_t total = stop - start;
|
||||
if (total < 4 || (total % 2) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::size_t half = total / 2;
|
||||
bandPath.StartFigure();
|
||||
bandPath.AddLines(cached->band.data() + start, static_cast<INT>(half));
|
||||
bandPath.AddLines(cached->band.data() + start + half,
|
||||
static_cast<INT>(half));
|
||||
bandPath.CloseFigure();
|
||||
}
|
||||
if (bandPath.GetPointCount() > 0)
|
||||
{
|
||||
gd::SolidBrush brush(ToGdiColor(lineColor));
|
||||
g.FillPath(&brush, &bandPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
gd::Pen pen(ToGdiColor(lineColor), resolved.style->lineWidth * m_scale);
|
||||
pen.SetLineJoin(gd::LineJoinRound);
|
||||
pen.SetStartCap(gd::LineCapRound);
|
||||
pen.SetEndCap(gd::LineCapRound);
|
||||
|
||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||
{
|
||||
const std::size_t start = breaks[k];
|
||||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||||
if (stop <= start + 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
g.DrawLines(&pen, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
g.DrawLines(&pen, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5106,9 +5185,9 @@ namespace jdi
|
||||
const bool showDelta = doubleMode && (which == rightCursor);
|
||||
|
||||
// 光标在可视区外时,把标注"钉"在对应的网格边上,保证取值仍然可见。
|
||||
// 具体是否越界(左右/上下)由 MakeDataMarker 逐点判定。
|
||||
const float pinX = CursorDisplayX(which);
|
||||
const float rawX = TimeToX(time);
|
||||
const bool pinned = (std::fabs(rawX - pinX) > 0.5f);
|
||||
|
||||
for (std::size_t laneIndex = 0; laneIndex < lanes.size(); ++laneIndex)
|
||||
{
|
||||
@@ -5137,40 +5216,15 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
|
||||
const float y = ValueToY(value, lane);
|
||||
if (y < clipTop - 2.0f || y > clipBottom + 2.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float rawY = ValueToY(value, lane);
|
||||
// 上下越界的数据点不再被丢弃:与左右越界一致,贴边绘制指示方向的三角。
|
||||
const DataMarker marker = MakeDataMarker(rawX, rawY, pinX, clipTop, clipBottom);
|
||||
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
|
||||
// 数据点:底色环 + 信号色实心点(贴边时改画成三角指示方向)。
|
||||
// 数据点:视界内为圆点,任一方向越界时改为贴边三角。
|
||||
const float radius = 3.4f * m_scale;
|
||||
gd::SolidBrush ring(ToGdiColor(m_theme->cardBg));
|
||||
gd::SolidBrush fill(ToGdiColor(signalColor));
|
||||
if (pinned)
|
||||
{
|
||||
const float arm = 5.0f * m_scale;
|
||||
const bool offLeft = (rawX < pinX);
|
||||
const float baseX = offLeft ? (pinX - arm) : (pinX + arm);
|
||||
gd::PointF tri[3];
|
||||
tri[0] = gd::PointF(baseX, y - arm);
|
||||
tri[1] = gd::PointF(baseX, y + arm);
|
||||
tri[2] = gd::PointF(pinX, y);
|
||||
g.FillPolygon(&ring, tri, 3);
|
||||
gd::PointF inner[3];
|
||||
inner[0] = gd::PointF(baseX + (offLeft ? 1.4f : -1.4f) * m_scale, y - arm * 0.72f);
|
||||
inner[1] = gd::PointF(baseX + (offLeft ? 1.4f : -1.4f) * m_scale, y + arm * 0.72f);
|
||||
inner[2] = gd::PointF(pinX - (offLeft ? -1.4f : 1.4f) * m_scale, y);
|
||||
g.FillPolygon(&fill, inner, 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.FillEllipse(&ring, pinX - radius - 1.2f * m_scale, y - radius - 1.2f * m_scale,
|
||||
(radius + 1.2f * m_scale) * 2.0f, (radius + 1.2f * m_scale) * 2.0f);
|
||||
g.FillEllipse(&fill, pinX - radius, y - radius, radius * 2.0f, radius * 2.0f);
|
||||
}
|
||||
PaintDataMarker(g, marker, radius, signalColor, m_theme->cardBg);
|
||||
|
||||
// 取值标注:底色用信号色、文字用反色,并加一层细描边提升对比度。
|
||||
std::wstring text = FormatValue(value);
|
||||
@@ -5198,7 +5252,7 @@ namespace jdi
|
||||
labelLeft = std::max(
|
||||
m_plotRect.X + 2.0f * m_scale,
|
||||
std::min(labelLeft, m_plotRect.X + m_plotRect.Width - textWidth - 2.0f * m_scale));
|
||||
float labelTop = y - textHeight * 0.5f;
|
||||
float labelTop = marker.y - textHeight * 0.5f;
|
||||
labelTop = std::max(clipTop + 1.0f * m_scale,
|
||||
std::min(labelTop, clipBottom - textHeight - 1.0f * m_scale));
|
||||
gd::RectF labelRect(labelLeft, labelTop, textWidth, textHeight);
|
||||
@@ -5221,40 +5275,14 @@ namespace jdi
|
||||
double centerValue = 0.0;
|
||||
if (CenterValueAt(*resolved.file, *resolved.data, smooth, time, centerValue))
|
||||
{
|
||||
const float centerY = ValueToY(centerValue, lane);
|
||||
if (centerY >= clipTop - 2.0f && centerY <= clipBottom + 2.0f)
|
||||
const float rawCenterY = ValueToY(centerValue, lane);
|
||||
const DataMarker centerMarker =
|
||||
MakeDataMarker(rawX, rawCenterY, pinX, clipTop, clipBottom);
|
||||
{
|
||||
// 中心轴数据点与常规数据点一样:在可视区内是圆点,
|
||||
// **光标在可视区之外(贴边显示)时同样改为三角**指示方向。
|
||||
const std::uint32_t centerColor = CursorColor(1);
|
||||
gd::SolidBrush centerFill(ToGdiColor(centerColor));
|
||||
if (pinned)
|
||||
{
|
||||
const float arm = 5.0f * m_scale;
|
||||
const bool offLeft = (rawX < pinX);
|
||||
const float baseX = offLeft ? (pinX - arm) : (pinX + arm);
|
||||
gd::PointF tri[3];
|
||||
tri[0] = gd::PointF(baseX, centerY - arm);
|
||||
tri[1] = gd::PointF(baseX, centerY + arm);
|
||||
tri[2] = gd::PointF(pinX, centerY);
|
||||
g.FillPolygon(&ring, tri, 3);
|
||||
gd::PointF inner[3];
|
||||
inner[0] = gd::PointF(baseX + (offLeft ? 1.4f : -1.4f) * m_scale,
|
||||
centerY - arm * 0.72f);
|
||||
inner[1] = gd::PointF(baseX + (offLeft ? 1.4f : -1.4f) * m_scale,
|
||||
centerY + arm * 0.72f);
|
||||
inner[2] = gd::PointF(pinX - (offLeft ? -1.4f : 1.4f) * m_scale, centerY);
|
||||
g.FillPolygon(¢erFill, inner, 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.FillEllipse(&ring, pinX - radius - 1.2f * m_scale,
|
||||
centerY - radius - 1.2f * m_scale,
|
||||
(radius + 1.2f * m_scale) * 2.0f,
|
||||
(radius + 1.2f * m_scale) * 2.0f);
|
||||
g.FillEllipse(¢erFill, pinX - radius, centerY - radius,
|
||||
radius * 2.0f, radius * 2.0f);
|
||||
}
|
||||
// 中心轴数据点的配色与原始信号一致(便于一一对应);
|
||||
// 但**始终按线段模式**绘制(不跟随原信号的散点模式)。
|
||||
const std::uint32_t centerColor = StyleColor(*resolved.style);
|
||||
PaintDataMarker(g, centerMarker, radius, centerColor, m_theme->cardBg);
|
||||
|
||||
// 取值文本:数值在前,尾缀「(中心轴)」;双光标的右侧光标还要
|
||||
// 附上增量,形如「(+0.0008 中心轴)」(参考常规信号的增量显示)。
|
||||
@@ -5286,13 +5314,13 @@ namespace jdi
|
||||
m_plotRect.X + 2.0f * m_scale,
|
||||
std::min(centerLeft, m_plotRect.X + m_plotRect.Width -
|
||||
centerTextWidth - 2.0f * m_scale));
|
||||
float centerTop = centerY - textHeight * 0.5f;
|
||||
float centerTop = centerMarker.y - textHeight * 0.5f;
|
||||
centerTop = std::max(
|
||||
clipTop + 1.0f * m_scale,
|
||||
std::min(centerTop, clipBottom - textHeight - 1.0f * m_scale));
|
||||
gd::RectF centerRect(centerLeft, centerTop, centerTextWidth, textHeight);
|
||||
PlaceCursorLabel(centerRect, centerTop, textHeight, clipTop, clipBottom);
|
||||
// 颜色样式与光标一致(同底色/同描边/同文字配色)。
|
||||
// 颜色样式与原始信号一致(同底色/同描边/同文字配色)。
|
||||
const bool centerDark = IsDarkColor(centerColor);
|
||||
UiFillRoundRect(g, centerRect, 3.0f * m_scale,
|
||||
WithAlpha(centerColor, 0xE0));
|
||||
@@ -5485,6 +5513,73 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
PlotView::DataMarker PlotView::MakeDataMarker(float rawX, float rawY, float pinX, float clipTop,
|
||||
float clipBottom) const
|
||||
{
|
||||
DataMarker marker;
|
||||
// 上下越界时同样"钉"在网格可见边缘(留出三角形的高度,保证整体可见)。
|
||||
const float margin = 7.0f * m_scale;
|
||||
marker.x = pinX;
|
||||
marker.y = std::max(clipTop + margin, 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);
|
||||
marker.offDown = (rawY > marker.y + 0.5f);
|
||||
return marker;
|
||||
}
|
||||
|
||||
void PlotView::PaintDataMarker(gd::Graphics& g, const DataMarker& marker, float radius,
|
||||
std::uint32_t fillColor, std::uint32_t ringColor) const
|
||||
{
|
||||
gd::SolidBrush ring(ToGdiColor(ringColor));
|
||||
gd::SolidBrush fill(ToGdiColor(fillColor));
|
||||
const float arm = 5.0f * m_scale;
|
||||
const float inset = 1.4f * m_scale;
|
||||
|
||||
// 左右越界:三角底边在外侧、尖端朝向网格内部。
|
||||
if (marker.offLeft || marker.offRight)
|
||||
{
|
||||
const bool offLeft = marker.offLeft;
|
||||
const float baseX = offLeft ? (marker.x - arm) : (marker.x + arm);
|
||||
const float inward = offLeft ? inset : -inset;
|
||||
gd::PointF outer[3];
|
||||
outer[0] = gd::PointF(baseX, marker.y - arm);
|
||||
outer[1] = gd::PointF(baseX, marker.y + arm);
|
||||
outer[2] = gd::PointF(marker.x, marker.y);
|
||||
g.FillPolygon(&ring, outer, 3);
|
||||
gd::PointF inner[3];
|
||||
inner[0] = gd::PointF(baseX + inward, marker.y - arm * 0.72f);
|
||||
inner[1] = gd::PointF(baseX + inward, marker.y + arm * 0.72f);
|
||||
inner[2] = gd::PointF(marker.x + inward, marker.y);
|
||||
g.FillPolygon(&fill, inner, 3);
|
||||
return;
|
||||
}
|
||||
|
||||
// 上下越界:三角底边在上/下方、尖端朝向网格内部(与左右越界同一套观感)。
|
||||
if (marker.offUp || marker.offDown)
|
||||
{
|
||||
const bool offUp = marker.offUp;
|
||||
const float baseY = offUp ? (marker.y - arm) : (marker.y + arm);
|
||||
const float inward = offUp ? inset : -inset;
|
||||
gd::PointF outer[3];
|
||||
outer[0] = gd::PointF(marker.x - arm, baseY);
|
||||
outer[1] = gd::PointF(marker.x + arm, baseY);
|
||||
outer[2] = gd::PointF(marker.x, marker.y);
|
||||
g.FillPolygon(&ring, outer, 3);
|
||||
gd::PointF inner[3];
|
||||
inner[0] = gd::PointF(marker.x - arm * 0.72f, baseY + inward);
|
||||
inner[1] = gd::PointF(marker.x + arm * 0.72f, baseY + inward);
|
||||
inner[2] = gd::PointF(marker.x, marker.y + inward);
|
||||
g.FillPolygon(&fill, inner, 3);
|
||||
return;
|
||||
}
|
||||
|
||||
// 视界内:常规圆点。
|
||||
g.FillEllipse(&ring, marker.x - radius - 1.2f * m_scale, marker.y - radius - 1.2f * m_scale,
|
||||
(radius + 1.2f * m_scale) * 2.0f, (radius + 1.2f * m_scale) * 2.0f);
|
||||
g.FillEllipse(&fill, marker.x - radius, marker.y - radius, radius * 2.0f, radius * 2.0f);
|
||||
}
|
||||
|
||||
void PlotView::PlaceCursorLabel(gd::RectF& rect, float preferredTop, float textHeight,
|
||||
float clipTop, float clipBottom)
|
||||
{
|
||||
@@ -5627,10 +5722,14 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 中心平均值曲线使用与光标(「边沿计数」横向光标、中心轴取值胶囊)一致的颜色样式:
|
||||
// 即光标的强调色。曲线与其取值标注、光标本身因此形成统一的视觉语言。
|
||||
const std::uint32_t derived = CursorColor(1);
|
||||
gd::Pen pen(ToGdiColor(derived), std::max(1.8f, resolved.style->lineWidth + 0.8f) * m_scale);
|
||||
// 中心平均值曲线:与原始信号**同色**,只是更透明一些;线宽等其它样式同样跟随原信号,
|
||||
// 因此原信号改颜色/线宽/透明度时曲线会自动同步。
|
||||
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));
|
||||
gd::Pen pen(ToGdiColor(derived), std::max(1.0f, resolved.style->lineWidth) * m_scale);
|
||||
pen.SetLineJoin(gd::LineJoinRound);
|
||||
pen.SetStartCap(gd::LineCapRound);
|
||||
pen.SetEndCap(gd::LineCapRound);
|
||||
@@ -5933,20 +6032,52 @@ namespace jdi
|
||||
|
||||
if (drawLine && !breaks.empty())
|
||||
{
|
||||
gd::Pen ghost(ToGdiColor(ghostColor),
|
||||
std::max(1.0f, resolved.style->lineWidth) * m_scale);
|
||||
ghost.SetLineJoin(gd::LineJoinRound);
|
||||
ghost.SetStartCap(gd::LineCapRound);
|
||||
ghost.SetEndCap(gd::LineCapRound);
|
||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||
if (ghostSource->bandMode && !ghostSource->bandBreaks.empty())
|
||||
{
|
||||
const std::size_t start = breaks[k];
|
||||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||||
if (stop <= start + 1)
|
||||
// 稠密包络虚影同样用一次 FillPath(见 PaintLane 的说明)。
|
||||
gd::GraphicsPath bandPath;
|
||||
bandPath.SetFillMode(gd::FillModeWinding);
|
||||
for (std::size_t k = 0; k < ghostSource->bandBreaks.size(); ++k)
|
||||
{
|
||||
continue;
|
||||
const std::size_t start = ghostSource->bandBreaks[k];
|
||||
const std::size_t stop = (k + 1 < ghostSource->bandBreaks.size())
|
||||
? ghostSource->bandBreaks[k + 1]
|
||||
: ghostSource->band.size();
|
||||
const std::size_t total = stop - start;
|
||||
if (total < 4 || (total % 2) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::size_t half = total / 2;
|
||||
bandPath.StartFigure();
|
||||
bandPath.AddLines(ghostSource->band.data() + start, static_cast<INT>(half));
|
||||
bandPath.AddLines(ghostSource->band.data() + start + half,
|
||||
static_cast<INT>(half));
|
||||
bandPath.CloseFigure();
|
||||
}
|
||||
if (bandPath.GetPointCount() > 0)
|
||||
{
|
||||
gd::SolidBrush brush(ToGdiColor(ghostColor));
|
||||
g.FillPath(&brush, &bandPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
gd::Pen ghost(ToGdiColor(ghostColor),
|
||||
std::max(1.0f, resolved.style->lineWidth) * m_scale);
|
||||
ghost.SetLineJoin(gd::LineJoinRound);
|
||||
ghost.SetStartCap(gd::LineCapRound);
|
||||
ghost.SetEndCap(gd::LineCapRound);
|
||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||
{
|
||||
const std::size_t start = breaks[k];
|
||||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||||
if (stop <= start + 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
g.DrawLines(&ghost, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
g.DrawLines(&ghost, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -166,7 +166,38 @@ namespace jdi
|
||||
* @brief 判断面板是否处于内联文本输入状态。
|
||||
* @return 正在输入返回 true。
|
||||
*/
|
||||
bool IsEditing() const { return m_editField != StyleEditField::NONE; }
|
||||
bool IsEditing() const { return m_editField != StyleEditField::NONE || m_filterEditing; }
|
||||
|
||||
/**
|
||||
* @brief 判断坐标是否落在列表上方的「筛选信号」输入框内。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 落在输入框内返回 true。
|
||||
*/
|
||||
bool IsOnFilterBox(int x, int y) const;
|
||||
|
||||
/**
|
||||
* @brief 进入筛选输入框的编辑状态(单击输入框时调用)。
|
||||
*/
|
||||
void BeginFilterEdit();
|
||||
|
||||
/**
|
||||
* @brief 结束筛选输入框的编辑状态(点击别处或回车时调用)。
|
||||
* @return 状态发生变化返回 true。
|
||||
*/
|
||||
bool EndFilterEdit();
|
||||
|
||||
/**
|
||||
* @brief 筛选框是否处于编辑状态。
|
||||
* @return 编辑中返回 true。
|
||||
*/
|
||||
bool IsFilterEditing() const { return m_filterEditing; }
|
||||
|
||||
/**
|
||||
* @brief 获取当前筛选文本。
|
||||
* @return 筛选文本(未输入时为空串)。
|
||||
*/
|
||||
const std::wstring& FilterText() const { return m_filterText; }
|
||||
|
||||
/**
|
||||
* @brief 向当前输入框送入一个字符。
|
||||
@@ -493,6 +524,12 @@ namespace jdi
|
||||
*/
|
||||
gd::RectF EditFieldRect() const;
|
||||
|
||||
/**
|
||||
* @brief 计算「筛选信号」输入框的内容矩形(编辑与显示共用,保证不跳动)。
|
||||
* @return 内容矩形。
|
||||
*/
|
||||
gd::RectF FilterFieldRect() const;
|
||||
|
||||
/**
|
||||
* @brief 计算指定行中名称区域的矩形(列表行内联编辑使用)。
|
||||
* @param [IN] row 行下标。
|
||||
@@ -534,6 +571,7 @@ namespace jdi
|
||||
|
||||
gd::RectF m_area; /**< 面板整体区域。 */
|
||||
gd::RectF m_headerRect; /**< 标题区域。 */
|
||||
gd::RectF m_filterRect; /**< 列表上方的「筛选信号」输入框区域。 */
|
||||
gd::RectF m_listRect; /**< 列表区域。 */
|
||||
gd::RectF m_styleRect; /**< 样式编辑区域。 */
|
||||
gd::RectF m_toggleAllRect; /**< 全部显示/隐藏按钮。 */
|
||||
@@ -573,6 +611,23 @@ namespace jdi
|
||||
int m_editRow = -1; /**< 行内输入所作用的列表行下标。 */
|
||||
TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */
|
||||
|
||||
bool m_filterEditing = false; /**< 「筛选信号」输入框是否处于编辑状态。 */
|
||||
std::wstring m_filterText; /**< 当前筛选文本(空串表示不过滤)。 */
|
||||
TextEdit m_filterEdit; /**< 筛选输入框的编辑模型。 */
|
||||
|
||||
/**
|
||||
* @brief 设置筛选文本并立即生效(重建行集合、回到顶部)。
|
||||
* @param [IN] text 新的筛选文本(空串表示不过滤)。
|
||||
*/
|
||||
void ApplyFilterText(const std::wstring& text);
|
||||
|
||||
/**
|
||||
* @brief 判断某信号的显示名是否匹配当前筛选表达式。
|
||||
* @param [IN] signal 信号数据。支持 `*` 通配符,忽略大小写。
|
||||
* @return 匹配(或未设置筛选)返回 true。
|
||||
*/
|
||||
bool FilterMatchesSignal(const SignalData& signal) const;
|
||||
|
||||
bool m_pendingLaneMenu = false; /**< 左键抬起时是否弹出网格归属菜单。 */
|
||||
SignalRef m_pendingLaneRef; /**< 待弹出菜单的目标信号。 */
|
||||
bool m_pendingColorPicker = false; /**< 左键抬起时是否弹出拾色器。 */
|
||||
|
||||
@@ -26,6 +26,65 @@ namespace jdi
|
||||
const float kCloseButtonSize = 18.0f; /**< 文件行关闭按钮尺寸。 */
|
||||
const float kGroupSeparatorGap = 7.0f; /**< 分组标签与分隔线/名称的间距。 */
|
||||
const float kGroupSeparatorWidth = 1.0f; /**< 分组标签右侧分隔线宽度。 */
|
||||
const float kFilterHeight = 26.0f; /**< 「筛选信号」输入框高度。 */
|
||||
const float kFilterPadV = 5.0f; /**< 筛选框上下留白。 */
|
||||
|
||||
/**
|
||||
* @brief 大小写无关的通配符匹配(`*` 匹配任意长度的任意字符序列)。
|
||||
* @param [IN] text 待匹配文本。
|
||||
* @param [IN] pattern 模式(可含多个 `*`)。
|
||||
* @return 匹配返回 true。
|
||||
*/
|
||||
bool WildcardMatchNoCase(const std::wstring& text, const std::wstring& pattern)
|
||||
{
|
||||
const auto fold = [](wchar_t character) -> wchar_t
|
||||
{
|
||||
// 仅对基本拉丁字母做大小写折叠(中文等其它字符原样比较)。
|
||||
if (character >= L'A' && character <= L'Z')
|
||||
{
|
||||
return static_cast<wchar_t>(character - L'A' + L'a');
|
||||
}
|
||||
return character;
|
||||
};
|
||||
|
||||
// 经典双指针回溯:`star` 记录最近一个 `*` 的位置,`resume` 记录当时尝试的文本位置。
|
||||
std::size_t t = 0;
|
||||
std::size_t p = 0;
|
||||
std::size_t star = std::wstring::npos;
|
||||
std::size_t resume = 0;
|
||||
|
||||
while (t < text.size())
|
||||
{
|
||||
if (p < pattern.size() && pattern[p] == L'*')
|
||||
{
|
||||
star = p;
|
||||
resume = t;
|
||||
++p;
|
||||
}
|
||||
else if (p < pattern.size() && fold(pattern[p]) == fold(text[t]))
|
||||
{
|
||||
++p;
|
||||
++t;
|
||||
}
|
||||
else if (star != std::wstring::npos)
|
||||
{
|
||||
p = star + 1;
|
||||
++resume;
|
||||
t = resume;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 文本已耗尽:模式剩余部分必须全是 `*`。
|
||||
while (p < pattern.size() && pattern[p] == L'*')
|
||||
{
|
||||
++p;
|
||||
}
|
||||
return p == pattern.size();
|
||||
}
|
||||
|
||||
// 样式编辑区:固定四行网格(颜色 / 样式 / 透明度 / 线宽),行高、间距与内容列宽度统一,
|
||||
// 保证面板宽度变化时四行控件始终对齐,且不会被无限制拉伸。
|
||||
@@ -154,9 +213,69 @@ namespace jdi
|
||||
return dirty;
|
||||
}
|
||||
|
||||
void SignalPanel::BuildRows()
|
||||
bool SignalPanel::IsOnFilterBox(int x, int y) const
|
||||
{
|
||||
m_rows.clear();
|
||||
return UiPointInRect(m_filterRect, static_cast<float>(x), static_cast<float>(y));
|
||||
}
|
||||
|
||||
void SignalPanel::BeginFilterEdit()
|
||||
{
|
||||
if (m_filterEditing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_filterEditing = true;
|
||||
// 单击进入编辑:光标落在末尾,不做全选(用户可以继续追加筛选词)。
|
||||
m_filterEdit.Begin(m_filterText, false, EditFilter::ANY, 0u);
|
||||
}
|
||||
|
||||
bool SignalPanel::EndFilterEdit()
|
||||
{
|
||||
if (!m_filterEditing)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// **必须**在 `End()` 之前取出文本:`TextEdit::End()` 会清空内部缓冲区。
|
||||
const std::wstring finalText = Trim(m_filterEdit.Text());
|
||||
m_filterEditing = false;
|
||||
m_filterEdit.End();
|
||||
// 文本在编辑过程中已实时生效,这里只需把最终值再同步一次并收起插入符。
|
||||
m_filterText = finalText;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SignalPanel::ApplyFilterText(const std::wstring& text)
|
||||
{
|
||||
if (m_filterText == text)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_filterText = text;
|
||||
// 筛选结果变化 → 重建行集合、回到列表顶部并重新计算滚动条。
|
||||
m_rowsDirty = true;
|
||||
m_scrollOffset = 0.0f;
|
||||
m_layoutDirty = true;
|
||||
}
|
||||
|
||||
bool SignalPanel::FilterMatchesSignal(const SignalData& signal) const
|
||||
{
|
||||
if (m_filterText.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 以 `*` 为通配符、忽略大小写,匹配"信号名中是否出现过符合该模式的片段"。
|
||||
// 两侧补 `*` 使模式等价于子串匹配,因此下面这些写法都能按直觉工作:
|
||||
// `编码器` → 名称包含"编码器"
|
||||
// `编码器*相` → 名称中出现以"编码器"开头、"相"结尾的片段
|
||||
// `*x10` / `ABS*` → 尾部/首部片段匹配(无需用户自己补另一侧的 `*`)
|
||||
std::wstring pattern = L"*";
|
||||
pattern += m_filterText;
|
||||
pattern += L'*';
|
||||
return WildcardMatchNoCase(signal.Title(), pattern);
|
||||
}
|
||||
|
||||
void SignalPanel::BuildRows()
|
||||
{ m_rows.clear();
|
||||
m_rowsDirty = false;
|
||||
|
||||
if (m_dataSet == nullptr || m_tab == nullptr)
|
||||
@@ -167,21 +286,55 @@ namespace jdi
|
||||
|
||||
const float fileHeight = kFileRowHeight * m_scale;
|
||||
const float signalHeight = kSignalRowHeight * m_scale;
|
||||
const bool filtering = !m_filterText.empty();
|
||||
|
||||
const std::vector<DataFile>& files = m_dataSet->Files();
|
||||
for (std::size_t fileIndex = 0; fileIndex < files.size(); ++fileIndex)
|
||||
{
|
||||
// 筛选时先挑出本组内匹配的信号;整组都没有匹配则**不显示该组**。
|
||||
std::vector<int> matched;
|
||||
if (filtering)
|
||||
{
|
||||
const std::size_t signalCount = files[fileIndex].signals.size();
|
||||
for (std::size_t signalIndex = 0; signalIndex < signalCount; ++signalIndex)
|
||||
{
|
||||
if (FilterMatchesSignal(files[fileIndex].signals[signalIndex]))
|
||||
{
|
||||
matched.push_back(static_cast<int>(signalIndex));
|
||||
}
|
||||
}
|
||||
if (matched.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
ListRow fileRow;
|
||||
fileRow.kind = RowKind::FILE;
|
||||
fileRow.fileIndex = static_cast<int>(fileIndex);
|
||||
fileRow.height = fileHeight;
|
||||
m_rows.push_back(fileRow);
|
||||
|
||||
if (m_tab->IsCollapsed(static_cast<int>(fileIndex)))
|
||||
// 筛选结果**默认展开**(忽略折叠状态),便于直接看到匹配项。
|
||||
if (!filtering && m_tab->IsCollapsed(static_cast<int>(fileIndex)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filtering)
|
||||
{
|
||||
for (const int signalIndex : matched)
|
||||
{
|
||||
ListRow signalRow;
|
||||
signalRow.kind = RowKind::SIGNAL;
|
||||
signalRow.fileIndex = static_cast<int>(fileIndex);
|
||||
signalRow.signalIndex = signalIndex;
|
||||
signalRow.height = signalHeight;
|
||||
m_rows.push_back(signalRow);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t signalCount = files[fileIndex].signals.size();
|
||||
for (std::size_t signalIndex = 0; signalIndex < signalCount; ++signalIndex)
|
||||
{
|
||||
@@ -498,10 +651,17 @@ namespace jdi
|
||||
|
||||
m_headerRect = gd::RectF(area.X, area.Y, area.Width, kHeaderHeight * m_scale);
|
||||
|
||||
// 标题下方的「筛选信号」输入框(列表内容过滤)。
|
||||
const float filterHeight = kFilterHeight * m_scale;
|
||||
m_filterRect = gd::RectF(area.X + kPadding * m_scale,
|
||||
m_headerRect.Y + m_headerRect.Height + kFilterPadV * m_scale,
|
||||
std::max(20.0f, area.Width - kPadding * 2.0f * m_scale), filterHeight);
|
||||
|
||||
const float styleHeight = DesiredStyleHeight() * m_scale;
|
||||
m_styleRect = gd::RectF(area.X, area.Y + area.Height - styleHeight, area.Width, styleHeight);
|
||||
m_listRect = gd::RectF(area.X, m_headerRect.Y + m_headerRect.Height, area.Width,
|
||||
std::max(0.0f, m_styleRect.Y - m_headerRect.Y - m_headerRect.Height));
|
||||
const float listTop = m_filterRect.Y + m_filterRect.Height + kFilterPadV * m_scale;
|
||||
m_listRect = gd::RectF(area.X, listTop, area.Width,
|
||||
std::max(0.0f, m_styleRect.Y - listTop));
|
||||
|
||||
// 标题区右侧的“全部显示/隐藏”按钮。
|
||||
const float toggleWidth = 68.0f * m_scale;
|
||||
@@ -557,6 +717,34 @@ namespace jdi
|
||||
enabled ? m_theme->text : m_theme->textMuted, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
// 「筛选信号」输入框:未输入内容时显示占位提示,单击进入编辑,输入实时过滤下方列表。
|
||||
{
|
||||
UiFillRoundRect(g, m_filterRect, kCorner * m_scale, m_theme->cardBg);
|
||||
UiDrawRoundRect(g, m_filterRect, kCorner * m_scale,
|
||||
m_filterEditing ? m_theme->accent : m_theme->border,
|
||||
m_filterEditing ? 1.6f : 1.0f);
|
||||
if (m_filterEditing)
|
||||
{
|
||||
UiDrawEditField(g, FilterFieldRect(), 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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 列表区。
|
||||
{
|
||||
const int saveState = g.Save();
|
||||
@@ -1203,6 +1391,15 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
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,
|
||||
std::max(8.0f, m_filterRect.Width - inset * 2.0f),
|
||||
std::max(8.0f, m_filterRect.Height - 4.0f * m_scale));
|
||||
}
|
||||
|
||||
gd::RectF SignalPanel::EditFieldRect() const
|
||||
{
|
||||
switch (m_editField)
|
||||
@@ -1222,6 +1419,45 @@ namespace jdi
|
||||
|
||||
bool SignalPanel::OnEditChar(wchar_t character)
|
||||
{
|
||||
// 「筛选信号」输入框:字符实时进入筛选(无需回车即可看到结果变化)。
|
||||
if (m_filterEditing)
|
||||
{
|
||||
switch (character)
|
||||
{
|
||||
case 0x03: // Ctrl+C
|
||||
m_filterEdit.CopyToClipboard(m_host);
|
||||
return false;
|
||||
case 0x18: // Ctrl+X
|
||||
m_filterEdit.CutToClipboard(m_host);
|
||||
ApplyFilterText(m_filterEdit.Text());
|
||||
return true;
|
||||
case 0x16: // Ctrl+V
|
||||
m_filterEdit.PasteFromClipboard(m_host);
|
||||
ApplyFilterText(m_filterEdit.Text());
|
||||
return true;
|
||||
case 0x01: // Ctrl+A
|
||||
m_filterEdit.SelectAll();
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (character == L'\r' || character == L'\n')
|
||||
{
|
||||
return EndFilterEdit();
|
||||
}
|
||||
// 退格/删除已由 WM_KEYDOWN 的 OnEditKey 处理,运里忽略控制字符避免重复删除。
|
||||
if (character < 0x20 || character == 0x7F)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!m_filterEdit.Insert(character))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ApplyFilterText(m_filterEdit.Text());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_editField == StyleEditField::NONE)
|
||||
{
|
||||
return false;
|
||||
@@ -1264,6 +1500,60 @@ namespace jdi
|
||||
|
||||
bool SignalPanel::OnEditKey(WPARAM key)
|
||||
{
|
||||
// 「筛选信号」输入框的键盘处理(回车结束编辑,Esc 清空并结束)。
|
||||
if (m_filterEditing)
|
||||
{
|
||||
const bool shift = (GetKeyState(VK_SHIFT) & 0x8000) != 0;
|
||||
const bool control = (GetKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||
switch (key)
|
||||
{
|
||||
case VK_RETURN:
|
||||
return EndFilterEdit();
|
||||
case VK_ESCAPE:
|
||||
{
|
||||
const bool changed = m_filterEdit.Text() != m_filterText;
|
||||
EndFilterEdit();
|
||||
return changed;
|
||||
}
|
||||
case VK_BACK:
|
||||
{
|
||||
if (!m_filterEdit.Backspace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ApplyFilterText(m_filterEdit.Text());
|
||||
return true;
|
||||
}
|
||||
case VK_DELETE:
|
||||
{
|
||||
if (!m_filterEdit.DeleteForward())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ApplyFilterText(m_filterEdit.Text());
|
||||
return true;
|
||||
}
|
||||
case VK_LEFT:
|
||||
return m_filterEdit.MoveCaret(-1, shift);
|
||||
case VK_RIGHT:
|
||||
return m_filterEdit.MoveCaret(1, shift);
|
||||
case VK_HOME:
|
||||
return m_filterEdit.MoveHome(shift);
|
||||
case VK_END:
|
||||
return m_filterEdit.MoveEnd(shift);
|
||||
case 'A':
|
||||
if (control)
|
||||
{
|
||||
m_filterEdit.SelectAll();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_editField == StyleEditField::NONE)
|
||||
{
|
||||
return false;
|
||||
@@ -1305,6 +1595,10 @@ namespace jdi
|
||||
|
||||
bool SignalPanel::CollapseEditSelectionToEnd()
|
||||
{
|
||||
if (m_filterEditing)
|
||||
{
|
||||
return m_filterEdit.DeselectToEnd();
|
||||
}
|
||||
if (m_editField == StyleEditField::NONE)
|
||||
{
|
||||
return false;
|
||||
@@ -1314,6 +1608,10 @@ namespace jdi
|
||||
|
||||
bool SignalPanel::OnEditMouseDown(int x, int y)
|
||||
{
|
||||
if (m_filterEditing)
|
||||
{
|
||||
return UiEditFieldMouseDown(m_fonts->uiSmall, FilterFieldRect(), m_filterEdit, m_scale, x, y);
|
||||
}
|
||||
if (m_editField == StyleEditField::NONE)
|
||||
{
|
||||
return false;
|
||||
@@ -1323,6 +1621,10 @@ namespace jdi
|
||||
|
||||
bool SignalPanel::OnEditMouseMove(int x)
|
||||
{
|
||||
if (m_filterEditing)
|
||||
{
|
||||
return UiEditFieldMouseMove(m_fonts->uiSmall, FilterFieldRect(), m_filterEdit, m_scale, x);
|
||||
}
|
||||
if (m_editField == StyleEditField::NONE)
|
||||
{
|
||||
return false;
|
||||
@@ -1332,6 +1634,15 @@ namespace jdi
|
||||
|
||||
bool SignalPanel::OnEditMouseUp()
|
||||
{
|
||||
if (m_filterEditing)
|
||||
{
|
||||
if (!m_filterEdit.Dragging())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_filterEdit.EndDrag();
|
||||
return true;
|
||||
}
|
||||
if (!m_edit.Dragging())
|
||||
{
|
||||
return false;
|
||||
@@ -1424,6 +1735,11 @@ namespace jdi
|
||||
{
|
||||
changed = EndEdit(true) || changed;
|
||||
}
|
||||
// 点击面板之外也要结束「筛选信号」的编辑状态。
|
||||
if (m_filterEditing)
|
||||
{
|
||||
changed = EndFilterEdit() || changed;
|
||||
}
|
||||
if (m_colorPicker != nullptr && m_colorPicker->IsOpen())
|
||||
{
|
||||
m_colorPicker->Close();
|
||||
@@ -1540,6 +1856,18 @@ namespace jdi
|
||||
const float fx = static_cast<float>(x);
|
||||
const float fy = static_cast<float>(y);
|
||||
|
||||
// 「筛选信号」输入框:单击进入编辑;点击其它位置则先结束筛选编辑(文本已实时生效)。
|
||||
if (IsOnFilterBox(x, y))
|
||||
{
|
||||
BeginFilterEdit();
|
||||
return true;
|
||||
}
|
||||
if (m_filterEditing)
|
||||
{
|
||||
// 文本已实时生效,这里只需收起编辑态,随后继续处理本次点击。
|
||||
EndFilterEdit();
|
||||
}
|
||||
|
||||
// 滚动条拖拽优先于其它命中测试。
|
||||
if (m_scrollBarVisible && UiPointInRect(m_scrollBarRect, fx, fy))
|
||||
{
|
||||
|
||||
+42
-18
@@ -16,6 +16,14 @@ namespace jdi
|
||||
*/
|
||||
const float kSwatchOutlineOpacity = 0.45f;
|
||||
|
||||
/**
|
||||
* @brief 矩形面积超过该阈值(逻辑像素²)且为不透明色时,改用 GDI 填充。
|
||||
*
|
||||
* GDI+ 的 FillRectangle 约 2.2ns/像素;面积过小时 GDI 的 GetHDC/ReleaseHDC
|
||||
* 固定开销反而不划算,因此在阈值以下仍走 GDI+。
|
||||
*/
|
||||
const float kGdiFillAreaThreshold = 20000.0f;
|
||||
|
||||
/**
|
||||
* @brief 构造圆角矩形路径。
|
||||
* @param [IN,OUT] path 目标路径对象。
|
||||
@@ -130,6 +138,34 @@ namespace jdi
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 大面积且**不透明**的填充走 GDI 路线:GDI+ 的 FillRectangle 实测约 2.2ns/像素
|
||||
// (整个窗口背景 1.7M 像素约 0.9ms,信号面板背景约 1.9ms),
|
||||
// 而 GDI 的 FillRect 快一个数量级。半透明填充必须留在 GDI+(GDI 不做 alpha 混色)。
|
||||
// 顺序安全:GetHDC() 会先把已排队的 GDI+ 内容刷入 DC,本次填充之后再发生的 GDI+ 绘制
|
||||
// 会在 ReleaseHDC() 之后刷出、正确地覆盖在上面。
|
||||
const float area = rect.Width * rect.Height;
|
||||
if ((argb & 0xFF000000u) == 0xFF000000u && area >= kGdiFillAreaThreshold)
|
||||
{
|
||||
HDC hdc = g.GetHDC();
|
||||
if (hdc != nullptr)
|
||||
{
|
||||
RECT rc;
|
||||
rc.left = static_cast<LONG>(std::floor(rect.X));
|
||||
rc.top = static_cast<LONG>(std::floor(rect.Y));
|
||||
rc.right = static_cast<LONG>(std::ceil(rect.X + rect.Width));
|
||||
rc.bottom = static_cast<LONG>(std::ceil(rect.Y + rect.Height));
|
||||
HBRUSH brush = CreateSolidBrush(ToColorRef(argb));
|
||||
if (brush != nullptr)
|
||||
{
|
||||
FillRect(hdc, &rc, brush);
|
||||
DeleteObject(brush);
|
||||
}
|
||||
g.ReleaseHDC(hdc);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
gd::SolidBrush brush(ToGdiColor(argb));
|
||||
g.FillRectangle(&brush, rect);
|
||||
}
|
||||
@@ -215,24 +251,12 @@ namespace jdi
|
||||
|
||||
float UiTextWidth(gd::Graphics& g, const std::wstring& text, HFONT font)
|
||||
{
|
||||
if (text.empty() || font == nullptr)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
HDC hdc = g.GetHDC();
|
||||
if (hdc == nullptr)
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
HGDIOBJ oldFont = SelectObject(hdc, font);
|
||||
SIZE size = {};
|
||||
GetTextExtentPoint32W(hdc, text.c_str(), static_cast<int>(text.size()), &size);
|
||||
SelectObject(hdc, oldFont);
|
||||
g.ReleaseHDC(hdc);
|
||||
|
||||
return static_cast<float>(size.cx);
|
||||
// 直接复用 UiMeasureTextWidth:后者用 GetDC(nullptr) 取度量 HDC,
|
||||
// 不会触发 **Graphics::GetHDC() 的 GDI+ 批次刷新**(实测每次约 5µs)。
|
||||
// UiEllipsize 会反复调用本函数(逐字符回退时每步一次),
|
||||
// 若走 GetHDC 路线,一个长名称就要刷新十几次,是拖拽掉帧的隐性来源之一。
|
||||
(void)g;
|
||||
return UiMeasureTextWidth(font, text);
|
||||
}
|
||||
|
||||
float UiMeasureTextWidth(HFONT font, const std::wstring& text)
|
||||
|
||||
Reference in new issue
Block a user