feat: 搭建 Just DataInspector 波形数据查看器基础功能
基于 Win32 API + GDI+ 从零实现仿真数据检查器,用于替代 Simulink.sdi.view 查看带时间轴的 CSV 数据。 - 新增 Utilities 层:UTF-8/宽字符与数值格式化工具、CSV 解析器(自动识别分隔符与引号)、 信号数据集模型(时间轴识别、信号名解析、取值采样)、Solarised Light 主题色板与绘图基础工具 - 新增 Toolbar 组件:打开 CSV、适应窗口、适应时间、放大、缩小、合并网格、每信号一格 - 新增 SignalPanel 组件:信号列表(可见性开关、颜色标识、所属网格标记)、样式编辑区 (颜色、线宽、线段/散点/点线/阶梯显示方式、所属网格),支持右键上下文菜单与滚轮滚动 - 新增 PlotView 组件:多网格波形绘制、自适应时间轴与幅值轴、网格线、信号图例、 时间光标与实时数值读数、纵向滚动条 - 新增 Application 组件:DPI 感知(Per-Monitor V2)、双缓冲绘制、区域布局与分隔条拖拽、 鼠标与键盘消息分发、文件拖放、状态栏提示 - 交互能力:滚轮缩放、Shift+滚轮平移、Ctrl+滚轮缩放幅值、拖拽平移、幅值轴纵向缩放、 右键框选缩放、单击固定/取消时间光标、双击自适应、方向键平移与滚动 - 补齐 Makefile 的源文件与头文件索引,并新增 gdiplus、comdlg32 链接库
This commit is contained in:
commit
132b305544
22 files changed
+7146
No files matched your search
@@ -0,0 +1,269 @@
|
||||
#include "PaintUtil.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
Reference in new issue
Block a user