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,160 @@
|
||||
#ifndef __TOOLBAR_HPP__
|
||||
#define __TOOLBAR_HPP__
|
||||
|
||||
#include <windows.h>
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "PaintUtil.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief 工具栏按钮触发的动作。
|
||||
*/
|
||||
enum class ToolbarAction : int
|
||||
{
|
||||
NONE = 0, /**< 未触发任何动作。 */
|
||||
OPEN_FILE, /**< 打开 CSV 文件。 */
|
||||
FIT_ALL, /**< 时间与幅值同时自适应。 */
|
||||
FIT_TIME, /**< 仅时间轴自适应。 */
|
||||
ZOOM_IN, /**< 放大时间轴。 */
|
||||
ZOOM_OUT, /**< 缩小时间轴。 */
|
||||
MERGE_LANES, /**< 合并为单一网格。 */
|
||||
SPLIT_LANES, /**< 按信号拆分为多网格。 */
|
||||
RESET_VIEW /**< 恢复默认视图范围。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 顶部工具栏:文件操作与视图控制的入口。
|
||||
*/
|
||||
class Toolbar
|
||||
{
|
||||
public:
|
||||
Toolbar();
|
||||
|
||||
/**
|
||||
* @brief 绑定字体与主题资源。
|
||||
* @param [IN] fonts 字体集合,需在工具栏生命周期内保持有效。
|
||||
* @param [IN] theme 主题配色,需在工具栏生命周期内保持有效。
|
||||
*/
|
||||
void Setup(const UiFonts* fonts, const Theme* theme);
|
||||
|
||||
/**
|
||||
* @brief 设置 DPI 缩放比例。
|
||||
* @param [IN] scale 缩放比例(1.0 对应 96 DPI)。
|
||||
*/
|
||||
void SetScale(float scale);
|
||||
|
||||
/**
|
||||
* @brief 设置是否已加载数据(未加载时大部分按钮被禁用)。
|
||||
* @param [IN] hasDocument 是否已加载数据。
|
||||
*/
|
||||
void SetDocumentLoaded(bool hasDocument);
|
||||
|
||||
/**
|
||||
* @brief 设置工具栏右侧的状态文本。
|
||||
* @param [IN] text 状态文本。
|
||||
*/
|
||||
void SetStatusText(const std::wstring& text);
|
||||
|
||||
/**
|
||||
* @brief 计算工具栏在指定缩放下的期望高度。
|
||||
* @return 期望高度(像素)。
|
||||
*/
|
||||
float DesiredHeight() const;
|
||||
|
||||
/**
|
||||
* @brief 按给定区域重新排布按钮。
|
||||
* @param [IN] area 工具栏可用区域。
|
||||
*/
|
||||
void Layout(const gd::RectF& area);
|
||||
|
||||
/**
|
||||
* @brief 绘制工具栏。
|
||||
* @param [IN,OUT] g 绘图上下文。
|
||||
*/
|
||||
void Paint(gd::Graphics& g);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标移动,更新悬停状态。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 悬停状态发生变化返回 true。
|
||||
*/
|
||||
bool OnMouseMove(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标左键按下。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @param [OUT] action 命中的按钮动作。
|
||||
* @return 命中按钮返回 true。
|
||||
*/
|
||||
bool OnLButtonDown(int x, int y, ToolbarAction& action);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标左键抬起。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @param [OUT] action 抬起时命中的按钮动作(仅在按钮上抬起时有效)。
|
||||
* @return 视觉状态发生变化返回 true。
|
||||
*/
|
||||
bool OnLButtonUp(int x, int y, ToolbarAction& action);
|
||||
|
||||
/**
|
||||
* @brief 处理鼠标离开工具栏。
|
||||
* @return 视觉状态发生变化返回 true。
|
||||
*/
|
||||
bool OnMouseLeave();
|
||||
|
||||
/**
|
||||
* @brief 判断坐标是否落在工具栏区域内。
|
||||
* @param [IN] x 客户区横坐标。
|
||||
* @param [IN] y 客户区纵坐标。
|
||||
* @return 落在区域内返回 true。
|
||||
*/
|
||||
bool HitTest(int x, int y) const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 单个工具栏按钮的运行时状态。
|
||||
*/
|
||||
struct Button
|
||||
{
|
||||
ToolbarAction action = ToolbarAction::NONE; /**< 按钮动作。 */
|
||||
std::wstring label; /**< 按钮文本。 */
|
||||
gd::RectF rect; /**< 按钮矩形。 */
|
||||
float width = 0.0f; /**< 按钮宽度。 */
|
||||
bool primary = false; /**< 是否为主按钮(强调色填充)。 */
|
||||
bool needsDocument = true; /**< 是否依赖已加载数据。 */
|
||||
bool hover = false; /**< 悬停状态。 */
|
||||
bool pressed = false; /**< 按下状态。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 判断按钮当前是否可用。
|
||||
* @param [IN] button 目标按钮。
|
||||
* @return 可用返回 true。
|
||||
*/
|
||||
bool IsEnabled(const Button& button) const;
|
||||
|
||||
const UiFonts* m_fonts = nullptr; /**< 字体资源。 */
|
||||
const Theme* m_theme = nullptr; /**< 主题资源。 */
|
||||
float m_scale = 1.0f; /**< DPI 缩放比例。 */
|
||||
bool m_documentLoaded = false; /**< 是否已加载数据。 */
|
||||
std::wstring m_statusText; /**< 右侧状态文本。 */
|
||||
std::vector<Button> m_buttons; /**< 按钮集合。 */
|
||||
gd::RectF m_area; /**< 工具栏整体区域。 */
|
||||
int m_hoverIndex = -1; /**< 当前悬停按钮下标。 */
|
||||
int m_pressedIndex = -1; /**< 当前按下按钮下标。 */
|
||||
};
|
||||
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __TOOLBAR_HPP__
|
||||
@@ -0,0 +1,275 @@
|
||||
#include "Toolbar.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "Utf8Util.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
const float kButtonHeight = 30.0f; /**< 按钮高度(逻辑像素)。 */
|
||||
const float kButtonGap = 6.0f; /**< 按钮间距。 */
|
||||
const float kButtonPadding = 14.0f; /**< 按钮左右内边距。 */
|
||||
const float kGroupGap = 18.0f; /**< 分组间距。 */
|
||||
const float kButtonRadius = 5.0f; /**< 按钮圆角半径。 */
|
||||
const float kSidePadding = 12.0f; /**< 工具栏左右留白。 */
|
||||
const float kTopPadding = 8.0f; /**< 工具栏上下留白。 */
|
||||
}
|
||||
|
||||
Toolbar::Toolbar() = default;
|
||||
|
||||
void Toolbar::Setup(const UiFonts* fonts, const Theme* theme)
|
||||
{
|
||||
m_fonts = fonts;
|
||||
m_theme = theme;
|
||||
}
|
||||
|
||||
void Toolbar::SetScale(float scale)
|
||||
{
|
||||
m_scale = (scale > 0.1f) ? scale : 1.0f;
|
||||
}
|
||||
|
||||
void Toolbar::SetDocumentLoaded(bool hasDocument)
|
||||
{
|
||||
m_documentLoaded = hasDocument;
|
||||
}
|
||||
|
||||
void Toolbar::SetStatusText(const std::wstring& text)
|
||||
{
|
||||
m_statusText = text;
|
||||
}
|
||||
|
||||
float Toolbar::DesiredHeight() const
|
||||
{
|
||||
return (kButtonHeight + kTopPadding * 2.0f) * m_scale;
|
||||
}
|
||||
|
||||
bool Toolbar::IsEnabled(const Button& button) const
|
||||
{
|
||||
return button.needsDocument ? m_documentLoaded : true;
|
||||
}
|
||||
|
||||
void Toolbar::Layout(const gd::RectF& area)
|
||||
{
|
||||
m_area = area;
|
||||
m_buttons.clear();
|
||||
|
||||
if (m_fonts == nullptr || m_fonts->ui == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
struct Spec
|
||||
{
|
||||
ToolbarAction action;
|
||||
const wchar_t* label;
|
||||
bool primary;
|
||||
bool needsDocument;
|
||||
};
|
||||
|
||||
const Spec specs[] = {
|
||||
{ToolbarAction::OPEN_FILE, L"打开 CSV", true, false},
|
||||
{ToolbarAction::FIT_ALL, L"适应窗口", false, true},
|
||||
{ToolbarAction::FIT_TIME, L"适应时间", false, true},
|
||||
{ToolbarAction::ZOOM_IN, L"放大", false, true},
|
||||
{ToolbarAction::ZOOM_OUT, L"缩小", false, true},
|
||||
{ToolbarAction::MERGE_LANES, L"合并网格", false, true},
|
||||
{ToolbarAction::SPLIT_LANES, L"每信号一格", false, true},
|
||||
};
|
||||
|
||||
// 按钮宽度先按固定预算计算,避免依赖绘图上下文。
|
||||
float cursorX = area.X + kSidePadding * m_scale;
|
||||
const float top = area.Y + kTopPadding * m_scale;
|
||||
const float height = kButtonHeight * m_scale;
|
||||
|
||||
for (const Spec& spec : specs)
|
||||
{
|
||||
Button button;
|
||||
button.action = spec.action;
|
||||
button.label = spec.label;
|
||||
button.primary = spec.primary;
|
||||
button.needsDocument = spec.needsDocument;
|
||||
button.width = (kButtonPadding * 2.0f +
|
||||
static_cast<float>(button.label.size()) * 13.0f) * m_scale;
|
||||
button.rect = gd::RectF(cursorX, top, button.width, height);
|
||||
cursorX += button.width + kButtonGap * m_scale;
|
||||
|
||||
if (spec.action == ToolbarAction::OPEN_FILE || spec.action == ToolbarAction::ZOOM_OUT ||
|
||||
spec.action == ToolbarAction::SPLIT_LANES)
|
||||
{
|
||||
cursorX += kGroupGap * m_scale;
|
||||
}
|
||||
|
||||
m_buttons.push_back(std::move(button));
|
||||
}
|
||||
}
|
||||
|
||||
void Toolbar::Paint(gd::Graphics& g)
|
||||
{
|
||||
if (m_theme == nullptr || m_fonts == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UiFillRect(g, m_area, m_theme->toolbarBg);
|
||||
|
||||
for (const Button& button : m_buttons)
|
||||
{
|
||||
const bool enabled = IsEnabled(button);
|
||||
std::uint32_t background = 0;
|
||||
std::uint32_t border = 0;
|
||||
std::uint32_t textColor = 0;
|
||||
|
||||
if (button.primary)
|
||||
{
|
||||
background = enabled ? m_theme->accent : MixColor(m_theme->accent, m_theme->toolbarBg, 0.6);
|
||||
if (button.pressed)
|
||||
{
|
||||
background = MixColor(m_theme->accent, 0xFF000000u, 0.12);
|
||||
}
|
||||
else if (enabled && button.hover)
|
||||
{
|
||||
background = MixColor(m_theme->accent, 0xFFFFFFFFu, 0.12);
|
||||
}
|
||||
border = background;
|
||||
textColor = m_theme->textOnAccent;
|
||||
}
|
||||
else
|
||||
{
|
||||
background = m_theme->toolbarBg;
|
||||
if (button.pressed)
|
||||
{
|
||||
background = m_theme->pressed;
|
||||
}
|
||||
else if (enabled && button.hover)
|
||||
{
|
||||
background = m_theme->hover;
|
||||
}
|
||||
border = MixColor(m_theme->border, m_theme->toolbarBg, enabled ? 0.0 : 0.5);
|
||||
textColor = enabled ? m_theme->text : m_theme->textMuted;
|
||||
}
|
||||
|
||||
if (background != m_theme->toolbarBg)
|
||||
{
|
||||
UiFillRoundRect(g, button.rect, kButtonRadius * m_scale, background);
|
||||
}
|
||||
if (!button.primary)
|
||||
{
|
||||
UiDrawRoundRect(g, button.rect, kButtonRadius * m_scale, border, 1.0f);
|
||||
}
|
||||
|
||||
UiText(g, button.label, m_fonts->ui, button.rect, textColor, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
// 右侧状态文本。
|
||||
if (!m_statusText.empty())
|
||||
{
|
||||
const float rightPadding = kSidePadding * m_scale;
|
||||
gd::RectF textRect(m_area.X, m_area.Y, m_area.Width - rightPadding, m_area.Height);
|
||||
UiText(g, m_statusText, m_fonts->uiSmall, textRect, m_theme->textMuted, TextAlign::RIGHT);
|
||||
}
|
||||
|
||||
// 底部分隔线。
|
||||
UiDrawLine(g, m_area.X, m_area.Y + m_area.Height - 1.0f, m_area.X + m_area.Width,
|
||||
m_area.Y + m_area.Height - 1.0f, m_theme->border, 1.0f);
|
||||
}
|
||||
|
||||
bool Toolbar::HitTest(int x, int y) const
|
||||
{
|
||||
return UiPointInRect(m_area, static_cast<float>(x), static_cast<float>(y));
|
||||
}
|
||||
|
||||
bool Toolbar::OnMouseMove(int x, int y)
|
||||
{
|
||||
int hoverIndex = -1;
|
||||
for (std::size_t index = 0; index < m_buttons.size(); ++index)
|
||||
{
|
||||
if (UiPointInRect(m_buttons[index].rect, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
hoverIndex = static_cast<int>(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hoverIndex == m_hoverIndex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_hoverIndex = hoverIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Toolbar::OnLButtonDown(int x, int y, ToolbarAction& action)
|
||||
{
|
||||
action = ToolbarAction::NONE;
|
||||
|
||||
for (std::size_t index = 0; index < m_buttons.size(); ++index)
|
||||
{
|
||||
Button& button = m_buttons[index];
|
||||
if (!UiPointInRect(button.rect, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!IsEnabled(button))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
m_pressedIndex = static_cast<int>(index);
|
||||
button.pressed = true;
|
||||
action = button.action;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Toolbar::OnLButtonUp(int x, int y, ToolbarAction& action)
|
||||
{
|
||||
action = ToolbarAction::NONE;
|
||||
if (m_pressedIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int index = m_pressedIndex;
|
||||
m_pressedIndex = -1;
|
||||
if (index >= static_cast<int>(m_buttons.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Button& button = m_buttons[static_cast<std::size_t>(index)];
|
||||
button.pressed = false;
|
||||
|
||||
const bool inside = UiPointInRect(button.rect, static_cast<float>(x), static_cast<float>(y));
|
||||
m_hoverIndex = inside ? index : -1;
|
||||
if (inside && IsEnabled(button))
|
||||
{
|
||||
action = button.action;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Toolbar::OnMouseLeave()
|
||||
{
|
||||
bool changed = false;
|
||||
if (m_hoverIndex >= 0)
|
||||
{
|
||||
m_hoverIndex = -1;
|
||||
changed = true;
|
||||
}
|
||||
if (m_pressedIndex >= 0)
|
||||
{
|
||||
if (m_pressedIndex < static_cast<int>(m_buttons.size()))
|
||||
{
|
||||
m_buttons[static_cast<std::size_t>(m_pressedIndex)].pressed = false;
|
||||
}
|
||||
m_pressedIndex = -1;
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
} // namespace jdi
|
||||
Reference in new issue
Block a user