① 主题自适应(`Theme.cpp`):新增 `SrgbLuminance` / `ContrastRatio` / `EnsureContrast` /
`MixToLuminance` / `ThemeLaneBackground`;四套主题的结构色(gridWeak 1.30 / gridStrong 1.65 /
axisLine 2.10 / borderStrong 1.70)按底色自动校正,Solarised Light 逐位不变;网格叠底改用
`ThemeLaneBackground()`(4 处),深色主题下网格层与绘图区底色可辨。
② 派生预览曲线(`HighContrastDerived`)重写为**连续**方案:方向沿信号所在侧远离底色,
目标亮度 = max(与底色对比度 3.2, 信号亮度 ± 0.18),再按亮度平滑混色;调用点传入真实网格底色
(旧实现默认写死 Solarised Light 底色 ⇒ 深色主题永远选到更暗的那一侧、几乎看不见)。
③ 信号样式区「线宽 / 透明度」两行右侧新增**复位按钮**(`UiPaintResetIcon` 成为唯一实现,
菜单/弱峰面板/样式区共用同一套笔画);默认线宽统一到 `DefaultSignalLineWidth(finiteCount)`
(单点信号取 10.0)。按钮右边界与内容列对齐,内容列过窄时整列不显示。
④ 「均值滤波」面板重做:信号色叠底标题栏 + 复选框 + 展开三角 + 「均值点数 / 迭代次数」两行滑块
(点击数值可改滑块上限、右侧复位)+ 内容区滚动;新增 `MeanFilterItem::{maxPoints,maxIterations,
collapsed}` 与 `Lane::meanFilterScroll`,缓存键同步补全。
⑤ 浮动面板命中像素缓存时只贴**面板圆角形状之内**的像素(`CreateRoundRectRgn` 裁剪)并按本帧重画投影
⇒ 中键移视界后面板周围不再出现一圈旧波形。
⑥ 显示布局边界:每个浮动面板**独占一个绘制层**(按 `m_panelOrder` 递增),绘制侧
`UiSetTextLayer` 在同一循环内同步推进 ⇒ 下层面板的边框/文字不再画到上层面板上。
⑦ 自由标记:采样候选距离改为**二维欧氏距离**(单点信号不再在整幅宽度内吸住光标),
纵向光标画在鼠标位置、圆点落在真实采样点(新增 `FreeMarkerProbe::pointX`);
`MinorCellSparse` 只对胜出信号算一次;折线缓存容量改为 `max(64, 信号数×2)` 且查找改为
单键哈希 ⇒ MF4 的 285 信号合并网格中 `ProbeFreeMarker` 平均 0.11ms。
验证:`.preflight` 探针重建(`panelcheck` 71/71、`markercheck` 13/13、`ringcheck` 6/6,合计 90 条,
LTO 目标文件上复跑同样全绿);`make release` / `make debug` 零警告;产物 815,104 B。
文档:`agent_mem.md` 第十六轮(含补丁脚本与编码/行尾踩坑)。
375 lines
14 KiB
C++
375 lines
14 KiB
C++
#ifndef __PAINT_UTIL_HPP__
|
||
#define __PAINT_UTIL_HPP__
|
||
|
||
#include <windows.h>
|
||
#include <objidl.h>
|
||
#include <gdiplus.h>
|
||
|
||
#include <cstddef>
|
||
#include <cstdint>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include "TextEdit.hpp"
|
||
#include "Theme.hpp"
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
/**
|
||
* @brief 文本水平对齐方式(垂直方向始终居中)。
|
||
*/
|
||
enum class TextAlign : int
|
||
{
|
||
LEFT = 0, /**< 左对齐。 */
|
||
CENTER = 1, /**< 水平居中。 */
|
||
RIGHT = 2 /**< 右对齐。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 图标朝向。
|
||
*/
|
||
enum class IconDirection : int
|
||
{
|
||
LEFT = 0, /**< 指向左侧。 */
|
||
RIGHT = 1, /**< 指向右侧。 */
|
||
UP = 2, /**< 指向上方。 */
|
||
DOWN = 3 /**< 指向下方。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 按 DPI 创建并缓存整套界面字体(GDI 字体句柄)。
|
||
*/
|
||
struct UiFonts
|
||
{
|
||
HFONT ui = nullptr; /**< 常规界面字体。 */
|
||
HFONT uiBold = nullptr; /**< 加粗界面字体。 */
|
||
HFONT uiSmall = nullptr; /**< 小号界面字体。 */
|
||
HFONT uiSmallBold = nullptr;/**< 小号加粗界面字体。 */
|
||
HFONT uiTiny = nullptr; /**< 微型界面字体(图例文字:必须能放进 GX 徽标同款的托底)。 */
|
||
HFONT title = nullptr; /**< 标题字体。 */
|
||
HFONT mono = nullptr; /**< 等宽数值字体。 */
|
||
HFONT monoSmall = nullptr; /**< 小号等宽字体。 */
|
||
UINT dpi = 96; /**< 创建时所使用的 DPI。 */
|
||
|
||
/**
|
||
* @brief 依据 DPI 创建全部字体。
|
||
* @param [IN] dpi 目标 DPI(96 对应 100% 缩放)。
|
||
* @return 创建成功返回 true。
|
||
*/
|
||
bool Create(UINT dpi);
|
||
|
||
/**
|
||
* @brief 释放全部字体句柄。
|
||
*/
|
||
void Destroy();
|
||
|
||
~UiFonts() { Destroy(); }
|
||
};
|
||
|
||
/**
|
||
* @brief 将 0xAARRGGBB 颜色值转换为 GDI COLORREF。
|
||
* @param [IN] argb 颜色值。
|
||
* @return GDI 颜色值。
|
||
*/
|
||
inline COLORREF ToColorRef(std::uint32_t argb)
|
||
{
|
||
return RGB((argb >> 16) & 0xFFu, (argb >> 8) & 0xFFu, argb & 0xFFu);
|
||
}
|
||
|
||
/**
|
||
* @brief 判断点是否落在矩形内部。
|
||
* @param [IN] rect 目标矩形。
|
||
* @param [IN] x 横坐标。
|
||
* @param [IN] y 纵坐标。
|
||
* @return 落在内部返回 true。
|
||
*/
|
||
inline bool UiPointInRect(const gd::RectF& rect, float x, float y)
|
||
{
|
||
return x >= rect.X && x <= (rect.X + rect.Width) && y >= rect.Y && y <= (rect.Y + rect.Height);
|
||
}
|
||
|
||
/**
|
||
* @brief 填充矩形区域。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] rect 目标矩形。
|
||
* @param [IN] argb 填充颜色。
|
||
*/
|
||
void UiFillRect(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb);
|
||
|
||
/**
|
||
* @brief 填充圆角矩形区域。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] rect 目标矩形。
|
||
* @param [IN] radius 圆角半径。
|
||
* @param [IN] argb 填充颜色。
|
||
*/
|
||
void UiFillRoundRect(gd::Graphics& g, const gd::RectF& rect, float radius, std::uint32_t argb);
|
||
|
||
/**
|
||
* @brief 描边圆角矩形。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] rect 目标矩形。
|
||
* @param [IN] radius 圆角半径。
|
||
* @param [IN] argb 线条颜色。
|
||
* @param [IN] width 线条宽度。
|
||
*/
|
||
void UiDrawRoundRect(gd::Graphics& g, const gd::RectF& rect, float radius, std::uint32_t argb, float width);
|
||
|
||
/**
|
||
* @brief 构造圆角矩形路径(与 `UiFillRoundRect` / `UiDrawRoundRect` 使用完全相同的几何)。
|
||
*
|
||
* 需要把一张位图 / 渐变**裁剪进圆角形状**时使用:
|
||
* `g.SetClip(&path)` 之后再填充,就不会在圆角之外漏出直角矩形的角。
|
||
*
|
||
* @param [IN,OUT] path 目标路径对象(不会被清空,直接追加)。
|
||
* @param [IN] rect 目标矩形。
|
||
* @param [IN] radius 圆角半径。
|
||
*/
|
||
void UiBuildRoundRectPath(gd::GraphicsPath& path, const gd::RectF& rect, float radius);
|
||
|
||
/**
|
||
* @brief 绘制一条直线。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] x1 起点横坐标。
|
||
* @param [IN] y1 起点纵坐标。
|
||
* @param [IN] x2 终点横坐标。
|
||
* @param [IN] y2 终点纵坐标。
|
||
* @param [IN] argb 线条颜色。
|
||
* @param [IN] width 线条宽度。
|
||
*/
|
||
void UiDrawLine(gd::Graphics& g, float x1, float y1, float x2, float y2, std::uint32_t argb, float width);
|
||
|
||
/**
|
||
* @brief 在指定矩形内绘制单行文本(垂直居中,超长自动省略)。
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] text 文本内容。
|
||
* @param [IN] font 字体句柄。
|
||
* @param [IN] rect 文本布局矩形。
|
||
* @param [IN] argb 文本颜色。
|
||
* @param [IN] align 水平对齐方式。
|
||
*/
|
||
void UiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb,
|
||
TextAlign align = TextAlign::LEFT);
|
||
|
||
/**
|
||
* @brief 在**显式裁剪矩形**内绘制单行文本(除裁剪范围外与 `UiText` 完全一致)。
|
||
*
|
||
* `UiText` 走 `DrawTextW`,而 **GDI 文本不遵循 GDI+ 的 `SetClip`**:滚动类列表在滚出
|
||
* 视口的行上仍会把文字画到相邻区域(例如把组名/信号名画到上方筛选框上)。
|
||
* 本函数在同一个 HDC 上做 `SaveDC` + `IntersectClipRect` 并在结束后完整还原,
|
||
* 因此对齐、垂直居中与省略号规则都与 `UiText` 一模一样,只是多了 GDI 级裁剪。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] text 文本内容。
|
||
* @param [IN] font 字体句柄。
|
||
* @param [IN] rect 文本布局矩形。
|
||
* @param [IN] argb 文本颜色。
|
||
* @param [IN] align 水平对齐方式。
|
||
* @param [IN] clipRect 裁剪矩形(超出部分不绘制)。
|
||
*/
|
||
void UiTextClipped(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect,
|
||
std::uint32_t argb, TextAlign align, const gd::RectF& clipRect);
|
||
|
||
/**
|
||
* @brief 文本几何收集器(调试用「显示布局边界」)。
|
||
*
|
||
* 文本是本项目里**唯一**没有几何身份的绘制元素:`UiText` / `UiTextClipped` /
|
||
* `UiTextAtClipped` 每次都在调用点现算一个矩形、画完即弃,全仓有 60+ 处这样的调用点。
|
||
* 与其在 60 处逐个补登记(必漏),不如在**绘制唯一出口**里回报一次。
|
||
*
|
||
* ⚠️ 只在调试开关打开时才被安装(见 `UiSetTextRectSink`),平时为空指针、
|
||
* 一次判空即返回,对正常绘制零影响。
|
||
*/
|
||
class UiTextRectSink
|
||
{
|
||
public:
|
||
virtual ~UiTextRectSink() = default;
|
||
|
||
/**
|
||
* @brief 回报一段文本**实际占用**的矩形(已按对齐方式与实测宽度收窄)。
|
||
* @param [IN] rect 文本墨迹矩形(客户区坐标)。
|
||
*/
|
||
virtual void OnTextRect(const gd::RectF& rect) = 0;
|
||
|
||
/**
|
||
* @brief 设置**后续文本**所属的绘制层(越大越靠上)。
|
||
*
|
||
* 为什么需要它:文本是在**绘制过程中**收集的,而各层的先后顺序只在绘制时才真实存在。
|
||
* 若所有文本都固定记到同一层,就会出现“浮层上的文字框被压在浮层下面”
|
||
*(表现为:光标取值胶囊的文字框穿透显示在浮动面板上)。
|
||
* 因此宿主在绘制每个组件前把层号告诉收集器,绘制浮层时组件再往上提一层。
|
||
*
|
||
* 默认空实现:只关心“文字在哪”而不关心层序的收集器(如行位图缓存)可忽略它。
|
||
*
|
||
* @param [IN] layer 层号(越大越靠上)。
|
||
*/
|
||
virtual void OnTextLayer(int layer) { (void)layer; }
|
||
|
||
/**
|
||
* @brief 当前文本层号(用于“在当前层之上再画一层”的场景)。
|
||
* @return 当前层号(默认 0)。
|
||
*/
|
||
virtual int TextLayer() const { return 0; }
|
||
};
|
||
|
||
/**
|
||
* @brief 安装 / 卸载文本几何收集器(nullptr = 卸载)。
|
||
*
|
||
* 单线程 UI 绘制 ⇒ 用文件级静态指针即可,不需要加锁。
|
||
*
|
||
* @param [IN] sink 收集器(允许为空)。
|
||
*/
|
||
void UiSetTextRectSink(UiTextRectSink* sink);
|
||
|
||
/**
|
||
* @brief 取当前安装的文本几何收集器(`nullptr` = 未安装)。
|
||
*
|
||
* 供"先画进离屏位图再贴回"的组件使用:它需要暂存自己的文本框,
|
||
* 并在贴图时按原点偏移回放给外层收集器。
|
||
*
|
||
* @return 当前收集器(可能为空)。
|
||
*/
|
||
UiTextRectSink* UiGetTextRectSink();
|
||
|
||
/**
|
||
* @brief 设置**当前绘制层**(未安装收集器时什么都不做)。
|
||
*
|
||
* 宿主在绘制每个组件前调用一次,把“这一层是第几层”告知收集器;
|
||
* 绘制不透明浮层的组件再把层号 +1 后画内容。
|
||
*
|
||
* @param [IN] layer 层号(越大越靠上)。
|
||
*/
|
||
void UiSetTextLayer(int layer);
|
||
|
||
/**
|
||
* @brief 取当前绘制层号(未安装收集器时为 0)。
|
||
* @return 层号。
|
||
*/
|
||
int UiGetTextLayer();
|
||
|
||
/**
|
||
* @brief 设置文本矩形的坐标偏移(默认 `(0, 0)`)。
|
||
*
|
||
* 有些组件把内容先画进**离屏位图**再贴回(`SignalPanel` 的行位图缓存),
|
||
* 此时回报的矩形处在位图坐标系 —— 传一位图原点偏移即可换算回屏幕坐标,
|
||
* 调试层才能把框画在对的位置。
|
||
*
|
||
* @param [IN] dx 横坐标偏移。
|
||
* @param [IN] dy 纵坐标偏移。
|
||
*/
|
||
void UiSetTextRectOffset(float dx, float dy);
|
||
|
||
/**
|
||
* @brief 测量单行文本的像素宽度(无需 GDI+ 上下文,可在布局阶段调用)。
|
||
* @param [IN] font 字体句柄。
|
||
* @param [IN] text 文本内容。
|
||
* @return 文本像素宽度。
|
||
*/
|
||
float UiMeasureTextWidth(HFONT font, const std::wstring& text);
|
||
|
||
/**
|
||
* @brief 测量单行文本的字符单元高度(垂直居中口径与 `UiText` 一致)。
|
||
* @param [IN] font 字体句柄。
|
||
* @param [IN] text 文本内容。
|
||
* @return 字符单元高度。
|
||
*/
|
||
float UiMeasureTextHeight(HFONT font, const std::wstring& text);
|
||
|
||
/**
|
||
* @brief 在指定裁剪矩形内以显式起点绘制一段文本(使用 GDI ExtTextOut 裁剪)。
|
||
*
|
||
* `UiText` 走 `DrawTextW`,**不遵循 GDI+ 裁剪区**;输入框在长内容、水平滚动的情况下
|
||
* 会把字符画到框体之外。本函数用 `ETO_CLIPPING` 让 GDI 自己裁剪,彻底避免越界绘制。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] text 文本内容。
|
||
* @param [IN] font 字体句柄。
|
||
* @param [IN] x 文本起始横坐标。
|
||
* @param [IN] rect 垂直居中参考矩形。
|
||
* @param [IN] argb 文本颜色。
|
||
* @param [IN] clipRect 裁剪矩形(超出部分不绘制)。
|
||
*/
|
||
void UiTextAtClipped(gd::Graphics& g, const std::wstring& text, HFONT font, float x, const gd::RectF& rect,
|
||
std::uint32_t argb, const gd::RectF& clipRect);
|
||
|
||
/**
|
||
* @brief 在超出可用宽度时对文本做尾部省略截断(不绘制任何内容)。
|
||
* @param [IN] text 原始文本。
|
||
* @param [IN] font 字体句柄。
|
||
* @param [IN] maxWidth 允许的最大像素宽度。
|
||
* @return 截断后的文本。
|
||
*/
|
||
std::wstring UiEllipsize(const std::wstring& text, HFONT font, float maxWidth);
|
||
|
||
/**
|
||
* @brief 用 GDI 一次性绘制若干条**不透明 1px 实线**(每条线段相互独立)。
|
||
*
|
||
* 性能关键:GDI+ 的 `DrawLine` 实测每条约 0.1–0.19 ms(带抗锯齿的长线更慢),
|
||
* 而一张网格通常要画几十条网格线 —— 逐条调用会让单帧多花十几毫秒。
|
||
* 这里改用 GDI(一次取 HDC + 一支画笔 + 多个 LineTo),快一两个数量级,
|
||
* 而且像素级网格线本来就不需要抗锯齿,观感反而更清晰。
|
||
*
|
||
* ⚠️ GDI 绘制**不受 GDI+ 裁剪区约束**,因此必须显式传入裁剪矩形
|
||
* (内部用 `SaveDC` + `IntersectClipRect` 保证不越界)。
|
||
* ⚠️ 只能用于**不透明**颜色(GDI 不做 alpha 混合);半透明色需调用方先与底色混合。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] clip 裁剪矩形(客户区坐标)。
|
||
* @param [IN] segments 线段数组,每 4 个 float 为一条(x1,y1,x2,y2)。
|
||
* @param [IN] segmentCount 线段数量。
|
||
* @param [IN] argb 颜色(Alpha 必须为 0xFF,否则不做任何绘制)。
|
||
* @param [IN] width 线宽(像素,>= 1;仅精确支持 1,更宽时按整数宽画)。
|
||
*/
|
||
void UiDrawSolidLines(gd::Graphics& g, const gd::RectF& clip, const float* segments,
|
||
std::size_t segmentCount, std::uint32_t argb, int width = 1);
|
||
|
||
/**
|
||
* @brief 把半透明颜色与已知不透明底色混合,得到可直接用于 GDI 的不透明颜色。
|
||
* @param [IN] background 不透明底色(0xFFRRGGBB)。
|
||
* @param [IN] argb 待叠加颜色(含 alpha)。
|
||
* @return 混合后的不透明颜色。
|
||
*/
|
||
std::uint32_t UiBlendOver(std::uint32_t background, std::uint32_t argb);
|
||
|
||
/**
|
||
* @brief 作废「文本宽度测量」的记忆化缓存。
|
||
*
|
||
* `UiMeasureTextWidth` 内部按 (字体句柄, 文本) 缓存测量结果以摊薄 GDI 调用开销;
|
||
* 字体句柄被重建(DPI 变化等)时必须调用本函数,否则系统复用的同一句柄值会命中
|
||
* 旧宽度,造成布局错位。
|
||
*/
|
||
void UiClearMeasureCache();
|
||
|
||
/**
|
||
* @brief 一次性填充多个圆角矩形(同色同圆角)。
|
||
*
|
||
* 每帧要画几十个刻度数值底板时,逐个调用 `UiFillRoundRect` 会带来可观的 GDI+ 调用开销;
|
||
* 合并成一条 GraphicsPath 后只 FillPath 一次(实测约快 3 倍)。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] rects 矩形集合。
|
||
* @param [IN] radius 圆角半径。
|
||
* @param [IN] argb 填充色。
|
||
*/
|
||
void UiFillRoundRects(gd::Graphics& g, const std::vector<gd::RectF>& rects, float radius,
|
||
std::uint32_t argb);
|
||
|
||
/**
|
||
* @brief 绘制「还原 / 重置」图标(逆时针回环箭头)。
|
||
*
|
||
* **唯一实现**:弹出菜单标题栏的还原图标(`PopupMenu`)、弱峰调整面板每行滑块的复位控件、
|
||
* 信号样式区「线宽 / 透明度」两行右侧的复位按钮全部调用它,因此同一语义在任何位置
|
||
* 都是同一套笔画,任意 DPI 下都清晰(不依赖字体或图标资源)。
|
||
*
|
||
* @param [IN,OUT] g 绘图上下文。
|
||
* @param [IN] rect 图标外接矩形(正方形;边长为 0 时不做任何绘制)。
|
||
* @param [IN] argb 笔画颜色(0xAARRGGBB)。
|
||
* @param [IN] scale DPI 缩放比例。
|
||
*/
|
||
void UiPaintResetIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float scale);
|
||
|
||
} // namespace jdi
|
||
|
||
#endif // __PAINT_UTIL_HPP__
|