#include "PaintUtil.hpp" #include #include #include #include #include #include #include #include namespace jdi { namespace { /** * @brief 矩形面积超过该阈值(逻辑像素²)且为不透明色时,改用 GDI 填充。 * * GDI+ 的 FillRectangle 约 2.2ns/像素;面积过小时 GDI 的 GetHDC/ReleaseHDC * 固定开销反而不划算,因此在阈值以下仍走 GDI+。 */ const float kGdiFillAreaThreshold = 20000.0f; } // 圆角矩形路径是公开的几何构建器:`UiFillRoundRect` / `UiDrawRoundRect` / 需要把位图或渐变 // **裁剪进同一个圆角形状**的调用方共用它,保证"填充的形状"与"描边的形状"逐点一致。 void UiBuildRoundRectPath(gd::GraphicsPath& path, const gd::RectF& rect, float radius) { if (rect.Width <= 0.0f || rect.Height <= 0.0f) { return; } float diameter = radius * 2.0f; if (diameter > rect.Width) { diameter = rect.Width; } if (diameter > rect.Height) { diameter = rect.Height; } if (diameter < 1.0f) { path.AddRectangle(rect); return; } const float right = rect.X + rect.Width; const float bottom = rect.Y + rect.Height; path.AddArc(rect.X, rect.Y, diameter, diameter, 180.0f, 90.0f); path.AddArc(right - diameter, rect.Y, diameter, diameter, 270.0f, 90.0f); path.AddArc(right - diameter, bottom - diameter, diameter, diameter, 0.0f, 90.0f); path.AddArc(rect.X, bottom - diameter, diameter, diameter, 90.0f, 90.0f); path.CloseFigure(); } namespace { /** * @brief 创建指定字号的界面字体。 * @param [IN] face 字体名称。 * @param [IN] pixelHeight 字符高度(像素)。 * @param [IN] bold 是否加粗。 * @return 字体句柄,失败返回 nullptr。 */ HFONT CreateUiFont(const wchar_t* face, int pixelHeight, bool bold) { LOGFONTW logFont = {}; logFont.lfHeight = -pixelHeight; logFont.lfWeight = bold ? FW_SEMIBOLD : FW_NORMAL; logFont.lfCharSet = DEFAULT_CHARSET; logFont.lfOutPrecision = OUT_TT_PRECIS; logFont.lfClipPrecision = CLIP_DEFAULT_PRECIS; logFont.lfQuality = CLEARTYPE_QUALITY; logFont.lfPitchAndFamily = DEFAULT_PITCH | FF_DONTCARE; wcsncpy(logFont.lfFaceName, face, LF_FACESIZE - 1); logFont.lfFaceName[LF_FACESIZE - 1] = L'\0'; HFONT font = CreateFontIndirectW(&logFont); if (font == nullptr) { font = static_cast(GetStockObject(DEFAULT_GUI_FONT)); } return font; } } // namespace namespace { /** * @brief 文本宽度测量的缓存键(字体句柄 + 文本)。 */ struct MeasureKey { HFONT font = nullptr; std::wstring text; bool operator==(const MeasureKey& other) const { return font == other.font && text == other.text; } }; struct MeasureKeyHash { std::size_t operator()(const MeasureKey& key) const { std::size_t hash = std::hash()(key.font); for (const wchar_t character : key.text) { hash = hash * 131u + static_cast(character); } return hash; } }; /** 上限保护:超过时整体清空(缓存只是加速手段,清空后重新填充即可)。 */ constexpr std::size_t kMeasureCacheLimit = 4096; /** 文本的像素宽度与字符单元高度(后者与 `DT_VCENTER` 的居中口径一致)。 */ struct TextExtent { float width = 0.0f; /**< 像素宽度。 */ float height = 0.0f; /**< 字符单元高度。 */ }; std::unordered_map g_measureCache; std::mutex g_measureMutex; /** * @brief 取文本的宽 / 高(带缓存)。 * * ⚠️ 必须走 GDI(`GetDC` + `GetTextExtentPoint32W` + `ReleaseDC`),单次约 10µs; * 而布局与绘制每帧会把同一批字符串**重复测量成百上千次**,因此按 (字体, 文本) 缓存。 * 高度也一并缓存:`DrawUiText` 的快速路径要用它做垂直居中(见那里的说明)。 */ TextExtent MeasureText(HFONT font, const std::wstring& text) { if (text.empty() || font == nullptr) { return TextExtent(); } const MeasureKey key{font, text}; { std::lock_guard lock(g_measureMutex); const auto found = g_measureCache.find(key); if (found != g_measureCache.end()) { return found->second; } } HDC hdc = GetDC(nullptr); if (hdc == nullptr) { return TextExtent(); } HGDIOBJ oldFont = SelectObject(hdc, font); SIZE size = {}; GetTextExtentPoint32W(hdc, text.c_str(), static_cast(text.size()), &size); SelectObject(hdc, oldFont); ReleaseDC(nullptr, hdc); TextExtent extent; extent.width = static_cast(size.cx); extent.height = static_cast(size.cy); { std::lock_guard lock(g_measureMutex); if (g_measureCache.size() >= kMeasureCacheLimit) { g_measureCache.clear(); } g_measureCache.emplace(key, extent); } return extent; } } // namespace void UiClearMeasureCache() { std::lock_guard lock(g_measureMutex); g_measureCache.clear(); } bool UiFonts::Create(UINT targetDpi) { Destroy(); // 字体句柄会被重新创建,旧句柄可能被系统复用:必须作废按句柄缓存的文本宽度, // 否则复用同一句柄值的新字体会命中旧宽度(布局错位)。 UiClearMeasureCache(); dpi = (targetDpi == 0) ? 96u : targetDpi; const double scale = static_cast(dpi) / 96.0; const int uiHeight = static_cast(std::lround(14.0 * scale)); const int smallHeight = static_cast(std::lround(12.0 * scale)); const int tinyHeight = static_cast(std::lround(10.0 * scale)); const int titleHeight = static_cast(std::lround(15.0 * scale)); const int monoHeight = static_cast(std::lround(13.0 * scale)); ui = CreateUiFont(L"Segoe UI", uiHeight, false); uiBold = CreateUiFont(L"Segoe UI Semibold", uiHeight, true); uiSmall = CreateUiFont(L"Segoe UI", smallHeight, false); uiSmallBold = CreateUiFont(L"Segoe UI Semibold", smallHeight, true); uiTiny = CreateUiFont(L"Segoe UI", tinyHeight, false); title = CreateUiFont(L"Segoe UI Semibold", titleHeight, true); mono = CreateUiFont(L"Consolas", monoHeight, false); monoSmall = CreateUiFont(L"Consolas", smallHeight, false); return ui != nullptr && uiBold != nullptr && uiSmall != nullptr && uiTiny != nullptr && title != nullptr && mono != nullptr; } void UiFonts::Destroy() { const HFONT resources[] = {ui, uiBold, uiSmall, uiSmallBold, uiTiny, title, mono, monoSmall}; for (const HFONT font : resources) { if (font != nullptr) { DeleteObject(font); } } ui = nullptr; uiBold = nullptr; uiSmall = nullptr; uiSmallBold = nullptr; uiTiny = nullptr; title = nullptr; mono = nullptr; monoSmall = nullptr; } void UiFillRect(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb) { if (rect.Width <= 0.0f || rect.Height <= 0.0f) { 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(std::floor(rect.X)); rc.top = static_cast(std::floor(rect.Y)); rc.right = static_cast(std::ceil(rect.X + rect.Width)); rc.bottom = static_cast(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); } void UiFillRoundRect(gd::Graphics& g, const gd::RectF& rect, float radius, std::uint32_t argb) { if (rect.Width <= 0.0f || rect.Height <= 0.0f) { return; } gd::GraphicsPath path; UiBuildRoundRectPath(path, rect, radius); gd::SolidBrush brush(ToGdiColor(argb)); g.FillPath(&brush, &path); } void UiDrawRoundRect(gd::Graphics& g, const gd::RectF& rect, float radius, std::uint32_t argb, float width) { if (rect.Width <= 0.0f || rect.Height <= 0.0f) { return; } gd::GraphicsPath path; UiBuildRoundRectPath(path, rect, radius); gd::Pen pen(ToGdiColor(argb), width); g.DrawPath(&pen, &path); } void UiFillRoundRects(gd::Graphics& g, const std::vector& rects, float radius, std::uint32_t argb) { if (rects.empty()) { return; } gd::GraphicsPath path; for (const gd::RectF& rect : rects) { if (rect.Width <= 0.0f || rect.Height <= 0.0f) { continue; } UiBuildRoundRectPath(path, rect, radius); } if (path.GetPointCount() > 0) { gd::SolidBrush brush(ToGdiColor(argb)); g.FillPath(&brush, &path); } } std::uint32_t UiBlendOver(std::uint32_t background, std::uint32_t argb) { const unsigned int alpha = (argb >> 24) & 0xFFu; if (alpha >= 0xFFu) { return argb | 0xFF000000u; } if (alpha == 0u) { return background | 0xFF000000u; } const double ratio = static_cast(alpha) / 255.0; std::uint32_t result = 0xFF000000u; for (int shift = 0; shift <= 16; shift += 8) { const double top = static_cast((argb >> shift) & 0xFFu); const double bottom = static_cast((background >> shift) & 0xFFu); const double mixed = bottom + (top - bottom) * ratio; result |= (static_cast(mixed + 0.5) & 0xFFu) << shift; } return result; } void UiDrawSolidLines(gd::Graphics& g, const gd::RectF& clip, const float* segments, std::size_t segmentCount, std::uint32_t argb, int width) { if (segments == nullptr || segmentCount == 0 || (argb & 0xFF000000u) != 0xFF000000u) { return; } if (width < 1) { width = 1; } const int clipLeft = static_cast(std::floor(clip.X)); const int clipTop = static_cast(std::floor(clip.Y)); const int clipRight = static_cast(std::ceil(clip.X + clip.Width)); const int clipBottom = static_cast(std::ceil(clip.Y + clip.Height)); if (clipRight <= clipLeft || clipBottom <= clipTop) { return; } HDC hdc = g.GetHDC(); if (hdc == nullptr) { return; } // GDI 绘制不受 GDI+ 裁剪约束,这里显式设置并随后完整还原 DC 状态。 const int saved = SaveDC(hdc); if (saved != 0) { IntersectClipRect(hdc, clipLeft, clipTop, clipRight, clipBottom); } HPEN pen = CreatePen(PS_SOLID, width, ToColorRef(argb)); if (pen != nullptr) { HGDIOBJ oldPen = SelectObject(hdc, pen); for (std::size_t index = 0; index < segmentCount; ++index) { const float* segment = segments + index * 4; MoveToEx(hdc, static_cast(std::lround(segment[0])), static_cast(std::lround(segment[1])), nullptr); LineTo(hdc, static_cast(std::lround(segment[2])), static_cast(std::lround(segment[3]))); } SelectObject(hdc, oldPen); DeleteObject(pen); } if (saved != 0) { RestoreDC(hdc, saved); } g.ReleaseHDC(hdc); } void UiDrawLine(gd::Graphics& g, float x1, float y1, float x2, float y2, std::uint32_t argb, float width) { gd::Pen pen(ToGdiColor(argb), width); pen.SetStartCap(gd::LineCapFlat); pen.SetEndCap(gd::LineCapFlat); g.DrawLine(&pen, x1, y1, x2, y2); } namespace { /** @brief 当前安装的文本几何收集器(`nullptr` = 不收集,调试开关关闭)。 */ UiTextRectSink* g_textRectSink = nullptr; /** @brief 当前文本矩形的坐标偏移(离屏位图渲染时非零)。 */ float g_textRectOffsetX = 0.0f; float g_textRectOffsetY = 0.0f; /** * @brief 回报一段文本的墨迹矩形(仅在收集器已安装时执行)。 * * 入参 `rect` 是**布局矩形**(可能整行宽);这里按对齐方式与实测宽度/高度把它 * 收窄成文字真正占据的那一块 —— 否则每个文本 box 都会跟整行一样宽,边界失去意义。 * * @param [IN] text 文本内容。 * @param [IN] font 字体句柄。 * @param [IN] rect 布局矩形。 * @param [IN] align 水平对齐方式。 */ void ReportTextRect(const std::wstring& text, HFONT font, const gd::RectF& rect, TextAlign align) { if (g_textRectSink == nullptr || text.empty() || font == nullptr) { return; } const TextExtent extent = MeasureText(font, text); if (extent.width <= 0.0f) { return; } const float width = std::min(extent.width, rect.Width); const float height = std::min(std::max(extent.height, 1.0f), rect.Height); float x = rect.X; if (align == TextAlign::CENTER) { x = rect.X + (rect.Width - width) * 0.5f; } else if (align == TextAlign::RIGHT) { x = rect.X + rect.Width - width; } const float y = rect.Y + (rect.Height - height) * 0.5f; g_textRectSink->OnTextRect( gd::RectF(x + g_textRectOffsetX, y + g_textRectOffsetY, width, height)); } /** * @brief `UiText` 与 `UiTextClipped` 的公共实现。 * * ⚠️ `clip == nullptr` 时**不加任何 GDI 裁剪**(与历史行为完全一致); * 提供裁剪时在同一个 HDC 上 `SaveDC`/`IntersectClipRect` 并完整还原 —— * 这是唯一能让 `DrawTextW` 受限于指定范围的办法(GDI+ 的 `SetClip` 对 GDI 文本无效)。 * * @param [IN,OUT] g 绘图上下文。 * @param [IN] text 文本内容。 * @param [IN] font 字体句柄。 * @param [IN] rect 文本布局矩形。 * @param [IN] argb 文本颜色。 * @param [IN] align 水平对齐方式。 * @param [IN] clip 裁剪矩形(nullptr = 不裁剪)。 */ void DrawUiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb, TextAlign align, const gd::RectF* clip) { if (text.empty() || font == nullptr || rect.Width <= 0.0f || rect.Height <= 0.0f) { return; } if (clip != nullptr && (clip->Width <= 0.0f || clip->Height <= 0.0f)) { return; // 裁剪区为空:任何文字都落在界外 } HDC hdc = g.GetHDC(); if (hdc == nullptr) { return; } // GDI 文本不受 GDI+ 裁剪区约束:需要限制范围时显式裁剪,结束前完整还原 DC 状态。 int saved = 0; if (clip != nullptr) { saved = SaveDC(hdc); if (saved != 0) { IntersectClipRect(hdc, static_cast(std::floor(clip->X)), static_cast(std::floor(clip->Y)), static_cast(std::ceil(clip->X + clip->Width)), static_cast(std::ceil(clip->Y + clip->Height))); } } HGDIOBJ oldFont = SelectObject(hdc, font); const int oldMode = SetBkMode(hdc, TRANSPARENT); const COLORREF oldColor = SetTextColor(hdc, ToColorRef(argb)); RECT rc; rc.left = static_cast(std::floor(rect.X)); rc.top = static_cast(std::floor(rect.Y)); rc.right = static_cast(std::ceil(rect.X + rect.Width)); rc.bottom = static_cast(std::ceil(rect.Y + rect.Height)); // ⚠️ 这里必须继续用 `DrawTextW`:试过改用 `ExtTextOutW` 的"快速路径"(约快 2~4 倍), // 但同一段文字的**光栅化结果并不相同**(实测面板区域 10% 的像素有差异, // 亚像素边缘与基线差 1px)—— 全体界面文字观感都会变,因此放弃。 // 列表卡顿改由"行位图缓存 + 动画期间不预热网格图层"解决(见 `SignalPanel::PaintRowCached`)。 UINT flags = DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS; switch (align) { case TextAlign::CENTER: flags |= DT_CENTER; break; case TextAlign::RIGHT: flags |= DT_RIGHT; break; case TextAlign::LEFT: default: flags |= DT_LEFT; break; } DrawTextW(hdc, text.c_str(), static_cast(text.size()), &rc, flags); // 调试:回报文字真正占据的矩形(在恢复 DC 状态之前算,此时字体仍被选中)。 ReportTextRect(text, font, rect, align); SetTextColor(hdc, oldColor); SetBkMode(hdc, oldMode); SelectObject(hdc, oldFont); if (saved != 0) { RestoreDC(hdc, saved); } g.ReleaseHDC(hdc); } } // namespace void UiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb, TextAlign align) { DrawUiText(g, text, font, rect, argb, align, nullptr); } 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) { DrawUiText(g, text, font, rect, argb, align, &clipRect); } float UiMeasureTextWidth(HFONT font, const std::wstring& text) { // 记忆化(键 = 字体 + 文本,值 = 宽 / 高):布局代码每帧会把同一批字符串 // **重复测量成百上千次**,而 `GetTextExtentPoint32W` 单次约 10µs。 return MeasureText(font, text).width; } float UiMeasureTextHeight(HFONT font, const std::wstring& text) { return MeasureText(font, text).height; } void UiSetTextRectSink(UiTextRectSink* sink) { g_textRectSink = sink; } UiTextRectSink* UiGetTextRectSink() { return g_textRectSink; } void UiSetTextLayer(int layer) { if (g_textRectSink != nullptr) { g_textRectSink->OnTextLayer(layer); } } int UiGetTextLayer() { return (g_textRectSink != nullptr) ? g_textRectSink->TextLayer() : 0; } void UiSetTextRectOffset(float dx, float dy) { g_textRectOffsetX = dx; g_textRectOffsetY = dy; } std::wstring UiEllipsize(const std::wstring& text, HFONT font, float maxWidth) { if (text.empty() || font == nullptr || maxWidth <= 0.0f) { return std::wstring(); } if (UiMeasureTextWidth(font, text) <= maxWidth) { return text; } const std::wstring ellipsis = L"…"; const float ellipsisWidth = UiMeasureTextWidth(font, ellipsis); if (ellipsisWidth > maxWidth) { return std::wstring(); } std::wstring result = text; while (!result.empty()) { result.pop_back(); if (UiMeasureTextWidth(font, result) + ellipsisWidth <= maxWidth) { return result + ellipsis; } } return ellipsis; } 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) { if (text.empty() || font == nullptr || clipRect.Width <= 0.0f || clipRect.Height <= 0.0f) { return; } HDC hdc = g.GetHDC(); if (hdc == nullptr) { return; } HGDIOBJ oldFont = SelectObject(hdc, font); const int oldMode = SetBkMode(hdc, TRANSPARENT); const COLORREF oldColor = SetTextColor(hdc, ToColorRef(argb)); TEXTMETRICW metrics = {}; GetTextMetricsW(hdc, &metrics); const int baselineY = static_cast( std::floor(rect.Y + (rect.Height - static_cast(metrics.tmHeight)) * 0.5f)); RECT clip = {}; clip.left = static_cast(std::floor(clipRect.X)); clip.top = static_cast(std::floor(clipRect.Y)); clip.right = static_cast(std::ceil(clipRect.X + clipRect.Width)); clip.bottom = static_cast(std::ceil(clipRect.Y + clipRect.Height)); // ETO_CLIPPED 让 GDI 在指定矩形内裁剪,长文本不会再画到输入框之外。 ExtTextOutW(hdc, static_cast(std::floor(x)), baselineY, ETO_CLIPPED, &clip, text.c_str(), static_cast(text.size()), nullptr); // 调试:回报文字真正占据的矩形(起点由调用方给出,宽度取实测值)。 if (g_textRectSink != nullptr) { const float width = UiMeasureTextWidth(font, text); if (width > 0.0f) { const float height = std::max(1.0f, static_cast(metrics.tmHeight)); g_textRectSink->OnTextRect( gd::RectF(std::floor(x) + g_textRectOffsetX, rect.Y + (rect.Height - height) * 0.5f + g_textRectOffsetY, width, height)); } } SetTextColor(hdc, oldColor); SetBkMode(hdc, oldMode); SelectObject(hdc, oldFont); g.ReleaseHDC(hdc); } } // namespace jdi