Files
Just-DataInspector/Components/Shared/Inc/PaintUtil.hpp
T
DeepSeek V4.1 Flash 1e50494c41 修复列表内联编辑(组名/信号名)看不到插入符与选区;把 UI 基础层迁入 Components/Shared
1) 行位图缓存键漏了插入符与选区:信号列表整行走 PaintRowCached,RowCacheKey
   在编辑态只混合“是否编辑 + 文本” ⇒ 左右方向键与鼠标框选虽然改变了 TextEdit
   状态(caret/anchor),键却不变 ⇒ 贴回旧位图。表现就是“无法部分选择、
   鼠标框选无反应、光标不动”,而绘图区的光标时间输入(无行缓存)一直正常。
   修法:GROUP_TITLE / SIGNAL_NAME 两个分支编辑态把 Caret / SelectionBegin /
   SelectionEnd 一并 mix 进键;新增 SignalPanel::EditFieldFont() 统一
   绘制(uiSmall / ui)与鼠标命中、插入符换算所用字体。

2) Theme / PaintUtil / TextEdit / PopupMenu / ColorPickerPopup 由
   Utilities/{Inc,Src} git mv 到 Components/Shared/{Inc,Src}:
   Utilities 只保留纯数据/逻辑模块(DataModel、DataTable、各 *Loader、MatH5、
   Utf8Util),Shared 成为自洽的 UI 基础层(Theme → PaintUtil → UiControls →
   PopupMenu / ColorPickerPopup)。依赖方向仍是 Components → Shared → Utilities;
   PaintUtil 一并迁移是因为它依赖 Theme(否则构成 Utilities → Shared 反向依赖)。
   Makefile 无需改动(include 平铺,两个目录都已在搜索路径),但需清理 Build/*
   里对应的旧 .o/.d。

验证(全部离屏,不占用鼠标/键盘/屏幕):
- 新增 .preflight/editcheck 20/20;A/B 修复前 6 项如实失败(键不变 + 屏幕不重绘);
- 既有 .preflight/check 137/137;
- make release / make debug 零警告。
2026-10-19 11:11:31 +08:00

264 lines
9.7 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 测量单行文本的像素宽度(无需 GDI+ 上下文,可在布局阶段调用)。
* @param [IN] font 字体句柄。
* @param [IN] text 文本内容。
* @return 文本像素宽度。
*/
float UiMeasureTextWidth(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);
} // namespace jdi
#endif // __PAINT_UTIL_HPP__