Files
Just-DataInspector/Utilities/Src/PaintUtil.cpp
T
DeepSeek V4.1 Flash cba62c8218 fix: 修正样式区对齐与线宽输入交互,分组标题改为“组X|文件名”并支持同文件重复加载
样式编辑区:
- 标签列与内容列改为靠左对齐(原为整体居中),拉宽左侧面板时不再向中间偏移。
- 颜色行不再居中,与样式、透明度、线宽三行共用同一左边界与同一宽度上限
  (260 逻辑像素),实测四行均为 89-413px,右侧留 93px 空白。

线宽输入:
- 双击数值框后数值整体居中并高亮(外框与数值呈选中态),首次键入即整体覆盖原值,
  之后显示插入符;回车确认、Esc 取消、点击其它区域确认,交互与常见登录输入框一致。

散点绘制:
- 散点半径改为随线宽线性增长(0.5-10px 映射到 1.6-12px,系数 1.5),
  修复此前半径被上限 4px 过早截断、线宽较大时数据点不再放大的问题。

透明度描边:
- 新增统一色块绘制入口 UiDrawSwatch:按不透明度填充,且仅在不透明度低于 45% 时
  补一圈描边;可见时不绘制描边。
- 预设色块、自定义色块、信号行色块与波形图例色块统一走该入口,
  修复透明度过低时完全不可见的问题。

文件分组:
- 分组标题改为“组X|<文件名>”,序号使用强调色;序号按当前位置连续编号,
  关闭某个分组后其余分组自动重新编号。
- 移除按路径查重的限制,同一文件可多次加载并分别归入不同分组。
- 关闭分组改为“移至末位后弹出 + 收缩容量”,使该文件的时间轴与全部信号缓冲区
  在本次调用内立即析构归还系统。
- 确认加载后不再依赖原始文件:读取阶段即关闭句柄并整份载入内存,
  已实测删除源文件后仍可正常勾选与绘制波形。

验证:make clean && make all(Release+Debug)零错误零警告;
逐项以真实/合成鼠标与像素扫描核对以上行为。
2026-09-29 18:40:17 +08:00

466 lines
13 KiB
C++

#include "PaintUtil.hpp"
#include <algorithm>
#include <cmath>
namespace jdi
{
namespace
{
/**
* @brief 颜色样块补充描边的不透明度阈值。
*
* 不透明度低于该值时,色块自身与浅色背景的对比度已不足,需要额外描边才能辨识;
* 高于该值时按需求不绘制描边,保持与预设色块一致的纯净外观。
*/
const float kSwatchOutlineOpacity = 0.45f;
/**
* @brief 构造圆角矩形路径。
* @param [IN,OUT] path 目标路径对象。
* @param [IN] rect 目标矩形。
* @param [IN] radius 圆角半径。
*/
void BuildRoundRectPath(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();
}
/**
* @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<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
}
return font;
}
}
bool UiFonts::Create(UINT targetDpi)
{
Destroy();
dpi = (targetDpi == 0) ? 96u : targetDpi;
const double scale = static_cast<double>(dpi) / 96.0;
const int uiHeight = static_cast<int>(std::lround(14.0 * scale));
const int smallHeight = static_cast<int>(std::lround(12.0 * scale));
const int titleHeight = static_cast<int>(std::lround(15.0 * scale));
const int monoHeight = static_cast<int>(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);
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 && title != nullptr && mono != nullptr;
}
void UiFonts::Destroy()
{
const HFONT resources[] = {ui, uiBold, uiSmall, uiSmallBold, title, mono, monoSmall};
for (const HFONT font : resources)
{
if (font != nullptr)
{
DeleteObject(font);
}
}
ui = nullptr;
uiBold = nullptr;
uiSmall = nullptr;
uiSmallBold = 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;
}
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;
BuildRoundRectPath(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;
BuildRoundRectPath(path, rect, radius);
gd::Pen pen(ToGdiColor(argb), width);
g.DrawPath(&pen, &path);
}
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);
}
void UiDrawDashedLine(gd::Graphics& g, float x1, float y1, float x2, float y2, std::uint32_t argb,
float width, gd::DashStyle dashPattern)
{
gd::Pen pen(ToGdiColor(argb), width);
pen.SetDashStyle(dashPattern);
pen.SetStartCap(gd::LineCapFlat);
pen.SetEndCap(gd::LineCapFlat);
g.DrawLine(&pen, x1, y1, x2, y2);
}
void UiText(gd::Graphics& g, const std::wstring& text, HFONT font, const gd::RectF& rect, std::uint32_t argb,
TextAlign align)
{
if (text.empty() || font == nullptr || rect.Width <= 0.0f || rect.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));
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));
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<int>(text.size()), &rc, flags);
SetTextColor(hdc, oldColor);
SetBkMode(hdc, oldMode);
SelectObject(hdc, oldFont);
g.ReleaseHDC(hdc);
}
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);
}
float UiMeasureTextWidth(HFONT font, const std::wstring& text)
{
if (text.empty() || font == nullptr)
{
return 0.0f;
}
HDC hdc = GetDC(nullptr);
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);
ReleaseDC(nullptr, hdc);
return static_cast<float>(size.cx);
}
std::wstring UiEllipsize(gd::Graphics& g, const std::wstring& text, HFONT font, float maxWidth)
{
if (text.empty() || font == nullptr || maxWidth <= 0.0f)
{
return std::wstring();
}
if (UiTextWidth(g, text, font) <= maxWidth)
{
return text;
}
const std::wstring ellipsis = L"…";
const float ellipsisWidth = UiTextWidth(g, ellipsis, font);
if (ellipsisWidth > maxWidth)
{
return std::wstring();
}
std::wstring result = text;
while (!result.empty())
{
result.pop_back();
if (UiTextWidth(g, result, font) + ellipsisWidth <= maxWidth)
{
return result + ellipsis;
}
}
return ellipsis;
}
void UiDrawPlusIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float strokeWidth)
{
const float cx = rect.X + rect.Width * 0.5f;
const float cy = rect.Y + rect.Height * 0.5f;
const float arm = std::min(rect.Width, rect.Height) * 0.3f;
UiDrawLine(g, cx - arm, cy, cx + arm, cy, argb, strokeWidth);
UiDrawLine(g, cx, cy - arm, cx, cy + arm, argb, strokeWidth);
}
void UiDrawCrossIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float strokeWidth)
{
const float cx = rect.X + rect.Width * 0.5f;
const float cy = rect.Y + rect.Height * 0.5f;
const float arm = std::min(rect.Width, rect.Height) * 0.3f;
UiDrawLine(g, cx - arm, cy - arm, cx + arm, cy + arm, argb, strokeWidth);
UiDrawLine(g, cx + arm, cy - arm, cx - arm, cy + arm, argb, strokeWidth);
}
void UiDrawArrowIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, float strokeWidth,
IconDirection direction)
{
const float cx = rect.X + rect.Width * 0.5f;
const float cy = rect.Y + rect.Height * 0.5f;
const float arm = std::min(rect.Width, rect.Height) * 0.26f;
switch (direction)
{
case IconDirection::LEFT:
UiDrawLine(g, cx + arm * 0.6f, cy - arm, cx - arm * 0.6f, cy, argb, strokeWidth);
UiDrawLine(g, cx - arm * 0.6f, cy, cx + arm * 0.6f, cy + arm, argb, strokeWidth);
break;
case IconDirection::RIGHT:
UiDrawLine(g, cx - arm * 0.6f, cy - arm, cx + arm * 0.6f, cy, argb, strokeWidth);
UiDrawLine(g, cx + arm * 0.6f, cy, cx - arm * 0.6f, cy + arm, argb, strokeWidth);
break;
case IconDirection::UP:
UiDrawLine(g, cx - arm, cy + arm * 0.6f, cx, cy - arm * 0.6f, argb, strokeWidth);
UiDrawLine(g, cx, cy - arm * 0.6f, cx + arm, cy + arm * 0.6f, argb, strokeWidth);
break;
case IconDirection::DOWN:
default:
UiDrawLine(g, cx - arm, cy - arm * 0.6f, cx, cy + arm * 0.6f, argb, strokeWidth);
UiDrawLine(g, cx, cy + arm * 0.6f, cx + arm, cy - arm * 0.6f, argb, strokeWidth);
break;
}
}
void UiDrawTriangleIcon(gd::Graphics& g, const gd::RectF& rect, std::uint32_t argb, IconDirection direction)
{
const float cx = rect.X + rect.Width * 0.5f;
const float cy = rect.Y + rect.Height * 0.5f;
const float arm = std::min(rect.Width, rect.Height) * 0.28f;
gd::PointF points[3];
switch (direction)
{
case IconDirection::LEFT:
points[0] = gd::PointF(cx + arm * 0.7f, cy - arm);
points[1] = gd::PointF(cx - arm * 0.7f, cy);
points[2] = gd::PointF(cx + arm * 0.7f, cy + arm);
break;
case IconDirection::RIGHT:
points[0] = gd::PointF(cx - arm * 0.7f, cy - arm);
points[1] = gd::PointF(cx + arm * 0.7f, cy);
points[2] = gd::PointF(cx - arm * 0.7f, cy + arm);
break;
case IconDirection::UP:
points[0] = gd::PointF(cx - arm, cy + arm * 0.7f);
points[1] = gd::PointF(cx, cy - arm * 0.7f);
points[2] = gd::PointF(cx + arm, cy + arm * 0.7f);
break;
case IconDirection::DOWN:
default:
points[0] = gd::PointF(cx - arm, cy - arm * 0.7f);
points[1] = gd::PointF(cx, cy + arm * 0.7f);
points[2] = gd::PointF(cx + arm, cy - arm * 0.7f);
break;
}
gd::SolidBrush brush(ToGdiColor(argb));
g.FillPolygon(&brush, points, 3);
}
double UiSliderRatioFromX(const gd::RectF& trackRect, float x)
{
if (trackRect.Width <= 0.0f)
{
return 0.0;
}
const double ratio = static_cast<double>(x - trackRect.X) / static_cast<double>(trackRect.Width);
if (ratio < 0.0)
{
return 0.0;
}
if (ratio > 1.0)
{
return 1.0;
}
return ratio;
}
void UiDrawCheckBox(gd::Graphics& g, const gd::RectF& rect, int state, float radius)
{
const Theme& theme = SolarizedLight();
const float centerY = rect.Y + rect.Height * 0.5f;
if (state == 1)
{
UiFillRoundRect(g, rect, radius, theme.accent);
gd::Pen pen(ToGdiColor(theme.textOnAccent), std::max(1.4f, rect.Height * 0.11f));
pen.SetStartCap(gd::LineCapRound);
pen.SetEndCap(gd::LineCapRound);
g.DrawLine(&pen, rect.X + rect.Width * 0.22f, centerY, rect.X + rect.Width * 0.42f,
rect.Y + rect.Height * 0.72f);
g.DrawLine(&pen, rect.X + rect.Width * 0.42f, rect.Y + rect.Height * 0.72f,
rect.X + rect.Width * 0.78f, rect.Y + rect.Height * 0.28f);
}
else if (state == 2)
{
UiFillRoundRect(g, rect, radius, theme.accentSoft);
UiDrawRoundRect(g, rect, radius, theme.accent, 1.0f);
UiFillRect(g, gd::RectF(rect.X + rect.Width * 0.22f, centerY - rect.Height * 0.08f,
rect.Width * 0.56f, rect.Height * 0.16f),
theme.accent);
}
else
{
UiFillRoundRect(g, rect, radius, theme.cardBg);
UiDrawRoundRect(g, rect, radius, theme.borderStrong, 1.0f);
}
}
bool UiSwatchNeedsOutline(float opacity)
{
return !(opacity >= kSwatchOutlineOpacity);
}
std::uint32_t UiDrawSwatch(gd::Graphics& g, const gd::RectF& rect, std::uint32_t color, float opacity,
float radius, float scale)
{
double alpha = static_cast<double>(opacity);
if (!(alpha > 0.0))
{
alpha = 0.0;
}
if (alpha > 1.0)
{
alpha = 1.0;
}
const std::uint32_t filled = WithAlpha(color, static_cast<unsigned int>(alpha * 255.0 + 0.5));
if (rect.Width > 0.0f && rect.Height > 0.0f)
{
UiFillRoundRect(g, rect, radius, filled);
// 透明度过低时色块本身难以辨识,补充一圈描边以保持可读性;可见时不绘制描边。
if (UiSwatchNeedsOutline(opacity))
{
UiDrawRoundRect(g, rect, radius, SolarizedLight().borderStrong,
std::max(1.0f, scale));
}
}
return filled;
}
} // namespace jdi