# quickdrawpanel **Repository Path**: QingChenMaoYu/quickdrawpanel ## Basic Information - **Project Name**: quickdrawpanel - **Description**: 画板 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-08 - **Last Updated**: 2026-06-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # QuickDrawPanel 基于 Qt6 的跨平台绘图面板库,提供可嵌入 QML / Qt Widgets 的高性能 2D 绘图能力。支持线段、矩形、椭圆、圆角矩形、三角形、菱形、自由路径、文本、箭头(两种样式)、星星、月亮、对勾等十余种图形绘制,具备选择、拖拽缩放、编辑、JSON 序列化及网络同步等高级功能。 --- ## 目录 - [功能特性](#功能特性) - [设计架构](#设计架构) - [整体结构](#整体结构) - [核心层(DrawPanel)](#核心层drawpanel) - [QML 封装层(DrawPanelQuick)](#qml-封装层drawpanelquick) - [设计思路](#设计思路) - [依赖与环境](#依赖与环境) - [快速开始](#快速开始) - [使用说明](#使用说明) - [C++ 直接使用(Qt Widgets)](#c-直接使用qt-widgets) - [QML 中使用](#qml-中使用) - [安装与作为外部依赖使用](#安装与作为外部依赖使用) - [构建选项](#构建选项) - [安装](#安装) - [find_package 集成](#find_package-集成) - [安装产物目录](#安装产物目录) - [扩展指南:添加自定义图形](#扩展指南添加自定义图形) - [目录结构](#目录结构) - [许可证](#许可证) --- ## 功能特性 | 特性 | 说明 | |------|------| | **多图形支持** | 线段、矩形、椭圆、圆角矩形、三角形、菱形、自由路径、文本、箭头(两种样式)、星星、月亮、对勾 | | **交互模式** | 创建模式、选择模式(框选/多选/拖拽缩放)、只读模式,每种模式由独立处理器类实现 | | **图形操作** | 绘制、选中、框选、移动、拖拽缩放、删除 | | **文本编辑** | 支持键盘输入、退格删除、输入法事件 | | **JSON 序列化** | 所有图形支持 `toJson` / `fromJson`,便于持久化和网络传输 | | **网络同步** | 内置 `DrawPanelNetwork` 扩展,支持图形数据的远程同步(需自行实现网络传输层) | | **QML 集成** | 以 `QQuickPaintedItem` 方式嵌入 QML,暴露属性绑定和信号通知 | | **自动注册** | `DrawPanelAutoRegister` 在面板构造时自动完成所有内置图形的工厂注册,上层无需手动维护映射表 | | **多播通知** | `IDrawPanelNotifier` 接口支持多个通知器同时监听面板事件,便于工具栏、状态栏、网络层同时接入 | --- ## 设计架构 ### 整体结构 ``` ┌─────────────────────────────────────────────────────────────┐ │ 应用层 (Application) │ │ ┌─────────────────┐ ┌─────────────────────────────────────┐│ │ │ Main.qml │ │ main.cpp (Widgets) ││ │ │ DrawPanelControl│ │ QApplication + DrawPanelWidget ││ │ └────────┬────────┘ └─────────────────────────────────────┘│ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐│ │ │ 前端封装层 (DrawPanelQuick / Widget) ││ │ │ DrawPanelQuick : public QQuickPaintedItem ││ │ │ DrawPanelWidget: public QWidget ││ │ │ • 实现 IDrawPanelNotifier,接收核心层事件 ││ │ │ • 暴露 QML 属性 / QWidget 接口 ││ │ │ • 转发鼠标 / 键盘 / 输入法事件到核心层 ││ │ └────────────────────────┬────────────────────────────────┘│ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐│ │ │ 核心绘图层 (DrawPanel) ││ │ │ DrawPanel : public QObject ││ │ │ • 形状列表管理 (std::vector) ││ │ │ • 自描述工厂注册表 (QMap)││ │ │ • 事件分发 → 模式处理器委托 ││ │ │ • IDrawPanelNotifier 多播通知接口 ││ │ └────────────────────────┬────────────────────────────────┘│ │ │ │ │ ┌───────────────┴───────────────┐ │ │ ▼ ▼ │ │ ┌──────────────────┐ ┌──────────────────────────┐ │ │ │ I_DrawShape │ │ Drawable │ │ │ │ 图形基类接口 │ │ CRTP Mixin 注册接口 │ │ │ │ • paint / rect │ │ • 不继承 I_DrawShape │ │ │ │ • move / dragZoom│ │ • 不干扰继承链 │ │ │ │ • buildMove │ │ • 显式 registerType() │ │ │ │ • toJson/fromJson│ └──────────────────────────┘ │ │ └──────────────────┘ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐│ │ │ 模式处理器 (Mode Handlers) ││ │ │ ReadOnlyMode / SelectMode / BuildMode ││ │ │ • I_DrawPanelMode 接口 ││ │ │ • 友元访问 DrawPanel 私有状态 ││ │ └─────────────────────────────────────────────────────────┘│ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐│ │ │ DrawPanelAutoRegister(集中自动注册) ││ │ │ • 在 DrawPanel 构造函数中通过 static 局部变量触发 ││ │ │ • 统一调用所有 Drawable::registerType() ││ │ └─────────────────────────────────────────────────────────┘│ └─────────────────────────────────────────────────────────────┘ ``` ### 核心层(DrawPanel) 核心层是纯 C++ 实现,不依赖 QML,可被任何 Qt Widgets / QML / 控制台程序复用。 #### 1. 图形接口 `I_DrawShape` 所有可绘制对象的抽象基类,定义了图形的完整生命周期方法: | 方法 | 职责 | |------|------| | `paint(QPainter*)` | 绘制图形本体 | | `rect() -> QRectF` | 返回图形的包围矩形,用于命中检测 | | `move(dx, dy)` | 平移图形 | | `dragZoom(type, wx, wy)` | 按拖拽方向进行单边缩放 | | `buildMove(pressPos, pos)` | **创建模式下**,根据鼠标拖拽更新图形几何 | | `toJson(QJsonObject*)` / `fromJson(...)` | JSON 序列化与反序列化 | | `C_Scale` / `C_R_Scale` | 坐标缩放工具(分子/分母比例) | | `toSubClass()` | 安全的向下转型模板方法 | **指针语义**: - `I_DrawShape::Ptr` = `I_DrawShape*`(观察指针,不拥有所有权) - `I_DrawShape::OwnerPtr` = `std::unique_ptr`(所有指针,RAII 自动释放) - `DrawPanel::shapes` 使用 `std::vector` 管理生命周期,外部回调仍使用 `Ptr` 观察 每个具体图形(`DrawRectItem`、`DrawLineItem`、`DrawEllipseItem`、`DrawStarItem`…)通过公开数据成员(`x`、`y`、`pen`、`brush`…)直接暴露状态,上层可通过 `IDrawPanelNotifier` 回调监听图形变化。 #### 2. 图形自描述注册机制 **零宏、零工厂类**。每个图形类通过多重继承 `Drawable` Mixin 提供注册能力: ```cpp class DrawRectItem : public I_DrawShape, public Drawable { public: static Descriptor descriptor(); // 描述"我是什么类型、怎么创建" static Ptr create(const QPointF& pos); // 工厂方法 void buildMove(...) override; // 拖拽时更新几何 // ... }; ``` - `Descriptor` 包含 `type`(类型枚举)、`create`(工厂函数)和可选的 `init`(类型特定初始化回调) - `Drawable` 是 **CRTP Mixin**,不继承 `I_DrawShape`,因此不会干扰现有的继承链(如 `DrawArrowItem → DrawLineItem`) - 所有图形的注册由 **`DrawPanelAutoRegister`** 在 `DrawPanel` 构造函数中**集中完成**,避免依赖 C++17 `inline static` 自动初始化(在 MSVC `/OPT:REF` 下可能失效) - `DrawPanel` 构造函数遍历注册表即可收集所有可用图形 #### 3. 控制器 `DrawPanel` `DrawPanel` 是整个核心层的**状态机 + 事件总线**,主要职责: - **模式切换**:`readOnly`(只读)、`selectMode`(选择)、`dragMode`(拖拽缩放),事件处理委托给对应的 `I_DrawPanelMode` 处理器 - **形状生命周期管理**:创建(`buildShape` 返回 `OwnerPtr`)、追加(`appendShape` 转移所有权)、删除(`removeShape` / `removeLastShape` 自动析构)、清空(`clearShapes`) - **事件处理**:鼠标移动/按下/释放、悬浮移动、键盘按键、输入法事件;核心业务逻辑委托给模式处理器 - **选中与框选**:拖拽生成选中矩形,框选多个图形,单选图形;状态封装在 `SelectionState` 结构体中 - **编辑模式**:针对文本图形,启用定时器闪烁光标,接收键盘和输入法输入;状态封装在 `EditState` 结构体中 - **多播通知**:通过 `IDrawPanelNotifier` 接口 + `addNotifier()` / `removeNotifier()` 暴露 `onBuildShape`、`onUpdate`、`onSetCursorShape`、`onEditStart` 等事件,与 QML / Widgets 层零耦合集成 #### 4. 模式处理器(Mode Handlers) `DrawPanel` 中的鼠标事件处理逻辑已抽离为三个独立的模式处理器: | 模式类 | 职责 | |--------|------| | `ReadOnlyMode` | 仅设置 `ArrowCursor`,拒绝所有交互操作 | | `SelectMode` | 处理图形选中、框选、多选拖拽移动、拖拽缩放光标与几何变换 | | `BuildMode` | 处理新图形的创建和拖拽构建过程,文本图形创建后自动进入编辑模式 | 处理器通过 **友元机制** 访问 `DrawPanel` 的私有状态(如 `_selectState.currentShape`、`_selectState.selectRect`、`_currentBuildShape`),既保持了 `DrawPanel` 对外的封装边界,又将各模式的复杂逻辑隔离到独立的类中。 ```cpp class DrawPanel : public QObject { friend class ReadOnlyMode; friend class SelectMode; friend class BuildMode; // ... I_DrawPanelMode *currentMode() noexcept { if (readOnly) return &_readOnlyMode; if (selectMode) return &_selectMode; return &_buildMode; } }; ``` 事件入口函数(如 `mousePressEvent`)保留在 `DrawPanel` 中,负责通用状态更新(`_currentLeftDown`、`_currentPressPos`)和编辑模式的退出逻辑,随后将事件委托给 `currentMode()` 返回的处理器。 #### 5. 网络同步扩展 `DrawPanelNetwork` 继承自 `DrawPanel`,同时实现 `IDrawPanelNotifier` 接口: ```cpp class DrawPanelNetwork : public DrawPanel, public IDrawPanelNotifier { // ... void onBuildShape(I_DrawShape::Ptr shape) override { // 序列化并通过网络发送 handleDrawPanelBuildShape(shape); } }; ``` 通过 `initNetwork()` / `initNotify()` 将本地创建图形的事件自动序列化为 JSON 并通过网络层发送。接收端解析 JSON 后调用 `buildShape` 重建图形,实现多端实时同步。 ### QML 封装层(DrawPanelQuick) `DrawPanelQuick` 继承 `QQuickPaintedItem`,实现 `IDrawPanelNotifier` 接口,将 `DrawPanel` 渲染到 QML 场景中。 **暴露的 QML 属性**: | 属性 | 类型 | 说明 | |------|------|------| | `readOnly` | `bool` | 只读模式,禁用所有交互 | | `selectMode` | `bool` | 选择模式,启用图形选中与框选 | | `dragMode` | `bool` | 拖拽缩放模式,选中图形后可通过边框拖拽调整大小 | | `buildDrawType` | `DrawType::Type` | 当前创建图形的类型 | **信号**: - `readOnlyChanged()` / `selectModeChanged()` / `dragModeChanged()` / `buildDrawTypeChanged()` **QML 工具栏**:`DrawPanelControl.qml` 提供了一个示例工具栏,可切换模式并选择要创建的图形类型。 ### Widgets 封装层(DrawPanelWidget) `DrawPanelWidget` 继承 `QWidget`,实现 `IDrawPanelNotifier` 接口,将 `DrawPanel` 渲染到 Qt Widgets 场景中。使用方式与 QML 层类似,适合传统的桌面应用程序。 --- ## 设计思路 ### 1. 图形自描述 = 开闭原则 + 消灭工厂类 传统设计将**图形数据模型**与**图形创建逻辑**拆分为两个类(`DrawRectItem` + `DrawRectComponent`)。本项目将创建逻辑直接内聚到图形类自身: ```cpp class DrawRectItem : public I_DrawShape, public Drawable { static Descriptor descriptor(); // "我是谁、怎么创建" static Ptr create(const QPointF& pos); // 创建实例 void buildMove(...) override; // 拖拽时更新几何 }; ``` 新增一种图形时,**只需一个类**:继承 `I_DrawShape` 和 `Drawable`,实现绘制与几何变换、实现 `create` 和 `buildMove`。`DrawPanel` 控制器完全不需要修改,符合**开闭原则**。 ### 2. 集中自动注册,避免链接器优化问题 `Drawable` 仅提供显式注册接口 `registerType()`,不依赖 `inline static` 自动初始化: ```cpp template class Drawable { public: static void registerType() { auto ® = I_DrawShape::registry(); auto desc = T::descriptor(); reg[desc.type] = desc; } }; ``` 所有内置图形的注册统一收敛到 **`DrawPanelAutoRegister`**: ```cpp DrawPanelAutoRegister::DrawPanelAutoRegister() { Drawable::registerType(); Drawable::registerType(); Drawable::registerType(); // ... 其他图形 } ``` 在 `DrawPanel` 构造函数中通过 `static DrawPanelAutoRegister _autoRegister;` 触发,确保: - **只执行一次** - **一定在面板使用前完成** - **不受 MSVC `/OPT:REF` 链接优化影响** 上层(`app/widget/main.cpp`、`DrawPanelQuick.cpp`)**无需手动注册任何图形**。 ### 3. IDrawPanelNotifier 多播接口,实现核心层与 UI 层解耦 `DrawPanel` 作为纯 C++ 核心层,不依赖 `QObject` 的信号槽(虽然继承 `QObject` 是为了内存管理和 `QTimer`),而是使用 `IDrawPanelNotifier` 纯虚接口 + `addNotifier()` / `removeNotifier()` 实现多播通知: ```cpp class IDrawPanelNotifier { public: virtual void onUpdate() {} virtual void onSetCursorShape(Qt::CursorShape) {} virtual void onBuildShape(I_DrawShape::Ptr) {} virtual void onEditStart() {} // ... }; class DrawPanel : public QObject { QList notifiers; void addNotifier(IDrawPanelNotifier *n); void removeNotifier(IDrawPanelNotifier *n); template void notifyAll(Func func, Args &&...args) const { for (auto *n : notifiers) (n->*func)(std::forward(args)...); } }; ``` 这样做的好处: - **核心层可独立测试**:无需启动 QML 引擎即可单元测试; - **多前端适配**:除了 QML,可轻松适配 Qt Widgets、WebAssembly、甚至非 Qt 框架; - **多播支持**:工具栏、状态栏、网络层可以同时监听同一事件,无需链式回调; - **类型安全**:模板 `notifyAll(&IDrawPanelNotifier::onXxx, ...)` 编译期检查接口一致性。 ### 4. 模式抽离:降低核心类的复杂度 `DrawPanel` 原先的事件处理函数(如 `mousePressEvent`)包含 `if (readOnly) ... else if (selectMode) ... else ...` 的长分支,逻辑混杂。通过引入 `I_DrawPanelMode` 接口和三个具体实现类,将模式相关的业务逻辑完全抽离: - `DrawPanel` 负责:**状态机管理**、**通用输入状态**、**编辑模式**、**事件入口分发** - `SelectMode` 负责:**单选/框选/多选拖拽/拖拽缩放** 的全部逻辑 - `BuildMode` 负责:**图形创建** 和 **buildMove** 更新 - `ReadOnlyMode` 负责:**拒绝交互** + **默认光标** 处理器通过友元访问 `DrawPanel` 私有状态,避免了为所有内部状态编写 getter/setter 的繁琐,同时保持了类的封装边界。 ### 5. 公开数据成员 + 回调,替代传统 Getter/Setter 图形状态(`x`、`y`、`pen`、`brush`、`selectFlag`…)直接以公开数据成员暴露,上层通过回调监听变化。相比传统封装: - **减少样板代码**:不需要为每个属性写 `Q_PROPERTY` + getter + setter; - **序列化友好**:JSON 读写可直接访问字段,无需通过接口方法; - **性能更优**:避免虚函数调用和信号发射开销。 对于必须暴露给 QML 的属性(如 `DrawPanelQuick::readOnly`),则在 QML 层单独封装 `Q_PROPERTY`。 ### 6. `OwnerPtr` + `std::vector`:消除内存泄漏风险 `shapes` 容器从 `QList` 升级为 `std::vector`,带来两个好处: - **自动释放**:`shapes.pop_back()` / `shapes.erase()` / `shapes.clear()` 时由 `unique_ptr` 自动 `delete`,杜绝手动释放遗漏 - **所有权语义清晰**:`buildShape()` 返回 `OwnerPtr`,调用方通过 `std::move` 显式转移所有权到 `shapes`;外部回调(如 `onBuildShape`)仍使用 `Ptr`(观察指针),不参与所有权管理 ### 7. `Descriptor::init` 解耦类型特化 `buildShape()` 原先对 `DrawTextType` 有硬编码的 `if` 分支: ```cpp if (currentBuildType == DrawType::DrawTextType) { auto text = shape->toSubClass(); text->font.setPixelSize(defaultFontSize); } ``` 这违反了开闭原则。现在 `Descriptor` 增加可选的 `init` 回调,由具体图形类自行注册类型特定的初始化逻辑: ```cpp // DrawTextItem::descriptor() return { DrawType::DrawTextType, [](const QPointF& pos) -> I_DrawShape::OwnerPtr { return DrawTextItem::create(pos); }, [](I_DrawShape::Ptr shape, DrawPanel *panel) { auto text = shape->toSubClass(); text->font.setPixelSize(panel->defaultFontSize); } }; ``` 新增图形时若不需要额外初始化,直接省略 `init` 即可;`buildShape()` 内部完全无感知。 ### 8. 分层架构:核心层 → 封装层 → 应用层 | 层级 | 依赖关系 | 可替换性 | |------|----------|----------| | 核心层 `DrawPanel` | 仅依赖 `QtCore` + `QtGui` | 可被 Widgets / 控制台 / 服务端复用 | | 封装层 `DrawPanelQuick` / `DrawPanelWidget` | 依赖 `QtQuick` / `QtWidgets` + 核心层 | 两种前端可互相替换 | | 应用层 `Main.qml` / `main.cpp` | 依赖封装层 | 用户自定义 | --- ## 依赖与环境 - **Qt**:6.5 或更高版本(依赖 `QtCore`、`QtGui`、`QtQuick`、`QtWidgets`) - **CMake**:3.16 或更高版本 - **C++ 标准**:C++17(使用 `std::any`、`std::function`、`inline static` 等特性) - **编译器**:MSVC 2019+ / GCC 9+ / Clang 10+ --- ## 快速开始 ### 1. 克隆与构建 ```bash git clone cd QuickDrawPanel mkdir build && cd build cmake .. -DCMAKE_PREFIX_PATH=<你的Qt安装路径>/6.5.3/msvc2019_64 cmake --build . --config Release ``` ### 2. 运行示例 构建完成后,直接运行生成的可执行文件: ```bash # Windows .\Release\appQuickDrawPanel.exe .\Release\appQuickDrawPanelWidget.exe # Linux / macOS ./appQuickDrawPanel ``` 示例界面包含: - 顶部的 `DrawPanelControl` 工具栏(切换模式、选择图形类型) - 下方的 `DrawPanelQuick` / `DrawPanelWidget` 绘图区域 --- ## 使用说明 ### C++ 直接使用(Qt Widgets) 如果你不需要 QML,可直接在 Qt Widgets 或控制台程序中使用核心层: ```cpp #include "DrawPanel.h" #include "DrawType.h" #include "IDrawPanelNotifier.h" // 1. 创建控制器 DrawPanel *panel = new DrawPanel(this); // 2. 实现通知器接口 struct MyNotifier : public IDrawPanelNotifier { QWidget *widget; MyNotifier(QWidget *w) : widget(w) {} void onUpdate() override { widget->update(); } void onSetCursorShape(Qt::CursorShape shape) override { widget->setCursor(shape); } }; auto *notifier = new MyNotifier(this); panel->addNotifier(notifier); // 3. 配置模式 panel->readOnly = false; panel->selectMode = true; panel->dragMode = true; panel->currentBuildType = DrawType::DrawRectType; // 4. 在 paintEvent 中绘制 void MyWidget::paintEvent(QPaintEvent *) { QPainter painter(this); panel->update(&painter); } // 5. 转发事件 void MyWidget::mousePressEvent(QMouseEvent *e) { panel->mousePressEvent(e); } void MyWidget::mouseMoveEvent(QMouseEvent *e) { panel->mouseMoveEvent(e); } void MyWidget::mouseReleaseEvent(QMouseEvent *e){ panel->mouseReleaseEvent(e); } ``` > **注意**:`DrawPanel` 构造函数内部会自动通过 `DrawPanelAutoRegister` 完成所有内置图形的注册,**无需在 main() 中手动调用 `Drawable::registerType()`**。 ### QML 中使用 ```qml import QtQuick import QtQuick.Layouts import DrawPanelQuick 1.0 as Jie Window { width: 1280 height: 720 visible: true title: "My Drawing App" ColumnLayout { anchors.fill: parent // 工具栏 Jie.DrawPanelControl { drawPanel: drawPanel Layout.preferredHeight: 60 Layout.fillWidth: true } // 绘图区域 Jie.DrawPanelQuick { id: drawPanel Layout.fillHeight: true Layout.fillWidth: true readOnly: false selectMode: true dragMode: true } } } ``` **属性绑定示例**: ```qml // 通过属性控制面板状态 Button { text: "Read Only" onClicked: drawPanel.readOnly = !drawPanel.readOnly } // 监听模式变化 Connections { target: drawPanel function onSelectModeChanged() { console.log("Select mode:", drawPanel.selectMode) } } ``` --- ## 安装与作为外部依赖使用 仓库提供了完整的 `install` / `find_package()` 支持,第三方项目可以直接以 CMake 包的形式集成 QuickDrawPanel,而无需把源码拷贝到自己的工程里。 > 想了解安装规则**内部是怎么写的、为什么这么写**, 见 [`docs/PACKAGING.md`](docs/PACKAGING.md)。本节面向使用方,讲怎么装、怎么 `find_package`。 ### 构建选项 顶层 `CMakeLists.txt` 暴露了两个可控开关: | 选项 | 默认值 | 说明 | |------|--------|------| | `QUICKDRAWPANEL_BUILD_EXAMPLES` | `ON` | 是否构建 `app/qml` 与 `app/widget` 示例程序。作为外部依赖使用时建议关掉。 | | `QUICKDRAWPANEL_INSTALL` | `ON` | 是否生成 `install` / `EXPORT` / `Config.cmake` 规则。仅做开发时也可保留默认值。 | ### 安装 ```bash # 1. 配置(推荐显式指定安装目录) cmake -S . -B build ^ -DCMAKE_PREFIX_PATH=/6.5.3/msvc2019_64 ^ -DCMAKE_INSTALL_PREFIX= ^ -DQUICKDRAWPANEL_BUILD_EXAMPLES=OFF # 2. 编译 + 安装 cmake --build build --config Release cmake --install build --config Release ``` 安装完成后会在 `` 下生成: - 头文件:`include/QuickDrawPanel/*.h`(包含核心层与两个封装层的全部公共头) - 主库:`bin/DrawPanelQuick.dll` + `lib/DrawPanelQuick.lib`(导入库;Linux 下对应 `lib/libDrawPanelQuick.so`) - QWidget 封装:`lib/DrawPanelWidget.lib`(静态库;Linux 下 `lib/libDrawPanelWidget.a`) - QML 模块:`lib/qml/DrawPanelQuick/`(含 `qmldir`、`*.qmltypes`、`*.qml`、以及插件 DLL `DrawPanelQuickplugin.dll`,QML 引擎运行时按这个目录加载模块) - CMake 包:`lib/cmake/QuickDrawPanel/{QuickDrawPanelConfig.cmake, QuickDrawPanelConfigVersion.cmake, QuickDrawPanelTargets.cmake}` ### find_package 集成 外部项目可这样使用: ```cmake cmake_minimum_required(VERSION 3.16) project(MyApp LANGUAGES CXX) # 让 CMake 找到 QuickDrawPanel 的安装位置(也可以通过 -DCMAKE_PREFIX_PATH 传入) list(APPEND CMAKE_PREFIX_PATH "") # 整体加载: DrawPanelQuick + DrawPanelWidget 都会被引入 find_package(QuickDrawPanel 1.0 REQUIRED) # 或者只引入需要的组件 # find_package(QuickDrawPanel 1.0 REQUIRED COMPONENTS DrawPanelQuick) # find_package(QuickDrawPanel 1.0 REQUIRED COMPONENTS DrawPanelWidget) find_package(Qt6 6.5 REQUIRED COMPONENTS Quick Widgets) qt_add_executable(MyApp main.cpp) # 注意命名空间前缀 QuickDrawPanel:: target_link_libraries(MyApp PRIVATE QuickDrawPanel::DrawPanelQuick # 需要 QML 时 QuickDrawPanel::DrawPanelWidget # 需要 QWidget 时 ) ``` - 包提供两个组件:`DrawPanelQuick` 和 `DrawPanelWidget`,未显式指定 `COMPONENTS` 时两者都会被加载。 - Qt 依赖会按组件自动解析:用到 `DrawPanelQuick` 时会自动 `find_dependency(Qt6 ... Quick)`,用到 `DrawPanelWidget` 时会自动 `find_dependency(Qt6 ... Widgets)`。 - 头文件统一暴露在 `include/QuickDrawPanel/`,源码里直接 `#include "DrawPanel.h"`、`#include "DrawPanelWidget.h"` 即可。 **QML 项目的运行时提示**:`DrawPanelQuick` 是动态库(SHARED),运行时需要两点配合。`QuickDrawPanelConfig.cmake` 暴露了一个变量 `QuickDrawPanel_QML_IMPORT_PATH`(值为 `/lib/qml`),下面三种场景全部用它即可,不必再手填路径。 1. **部署 DLL**:把主库与 QML 插件 DLL 拷到使用方可执行文件能找到的地方,常用 `windeployqt`: ```cmake add_custom_command(TARGET MyApp POST_BUILD COMMAND Qt6::windeployqt --qmldir "${QuickDrawPanel_QML_IMPORT_PATH}" "$" ) ``` 2. **告诉 QML 引擎去哪儿找模块**。把变量值注入到使用方源码,运行期调 `addImportPath`: ```cmake target_compile_definitions(MyApp PRIVATE QUICKDRAWPANEL_QML_IMPORT_PATH="${QuickDrawPanel_QML_IMPORT_PATH}" ) ``` ```cpp // main.cpp engine.addImportPath(QUICKDRAWPANEL_QML_IMPORT_PATH); ``` 或者在启动脚本里设环境变量, 不改源码: ```bash set QML_IMPORT_PATH=/lib/qml # Windows export QML_IMPORT_PATH=/lib/qml # Linux / macOS ``` 3. **让 Qt Creator / qmllint 在 IDE 中识别本模块**,在使用方工程的 `CMakeLists.txt` 里追加: ```cmake set(QML_IMPORT_PATH "${QuickDrawPanel_QML_IMPORT_PATH}" CACHE STRING "" FORCE) ``` ### 安装产物目录 以默认前缀为例(`` = `CMAKE_INSTALL_PREFIX`): ``` / ├── bin/ │ └── DrawPanelQuick.dll # 主库 DLL (Windows; Linux 对应 lib/libDrawPanelQuick.so) ├── include/ │ └── QuickDrawPanel/ │ ├── DrawPanel.h │ ├── DrawPanelQuick.h │ ├── DrawPanelWidget.h │ ├── IDrawPanelNotifier.h │ ├── I_DrawShape.h │ ├── DrawType.h │ ├── QuickDrawPanel_Export.h # __declspec 切换宏, 所有公开头文件都会包含它 │ └── ...(全部核心层头文件) └── lib/ ├── DrawPanelQuick.lib # Windows 导入库 (Linux 上无此项) ├── DrawPanelWidget.lib # QWidget 封装层 (STATIC) ├── qml/ │ └── DrawPanelQuick/ # QML 引擎运行时按此目录加载模块 │ ├── qmldir │ ├── DrawPanelQuick.qmltypes │ ├── DrawPanelControl.qml │ └── DrawPanelQuickplugin.dll # qt_add_qml_module 自动生成的 QML 插件 └── cmake/ └── QuickDrawPanel/ ├── QuickDrawPanelConfig.cmake ├── QuickDrawPanelConfigVersion.cmake └── QuickDrawPanelTargets.cmake ``` --- ## 扩展指南:添加自定义图形 以添加 **圆形(DrawCircleItem)** 为例,演示完整的扩展流程。 ### 第一步:添加类型枚举 在 `DrawPanel/DrawType.h` 的 `Type` 枚举中新增: ```cpp enum Type { DrawNoneType = 0x00, DrawLineType, DrawRectType, DrawTextType, DrawPathType, DrawArrowType, DrawArrow_V2Type, DrawEllipseType, DrawRoundedRectType, DrawTriangleType, DrawDiamondType, DrawStarType, DrawMoonType, DrawCheckType, DrawCircleType, // <-- 新增 }; ``` ### 第二步:实现图形类(仅此一个类) 创建 `DrawPanel/DrawCircleItem.h`: ```cpp #pragma once #include "I_DrawShape.h" /** * @brief 圆形图形类 * @details 继承 I_DrawShape 和 Drawable,通过 DrawPanelAutoRegister 注册。 * 不需要额外的 Component 工厂类。 */ class DrawCircleItem : public I_DrawShape, public Drawable { public: double cx = 0; ///< 圆心 X double cy = 0; ///< 圆心 Y double radius = 0; ///< 半径 static Descriptor descriptor(); static OwnerPtr create(const QPointF& pos); DrawCircleItem() noexcept; void paint(QPainter *painter) override; QRectF rect() const noexcept override; void move(int diffX, int diffY) noexcept override; void dragZoom(DragType type, double widthZoom, double heightZoom) noexcept override; void buildMove(const QPointF& pressPos, const QPointF& pos) override; void toJson(QJsonObject *object) const override; void fromJson(const QJsonObject &object) override; }; ``` 创建 `DrawPanel/DrawCircleItem.cpp`: ```cpp #include "DrawCircleItem.h" #include "DrawType.h" #include I_DrawShape::Descriptor DrawCircleItem::descriptor() { return { DrawType::DrawCircleType, [](const QPointF& pos) -> I_DrawShape::OwnerPtr { return DrawCircleItem::create(pos); } }; } I_DrawShape::OwnerPtr DrawCircleItem::create(const QPointF& pos) { auto shape = std::make_unique(); shape->type = DrawType::DrawCircleType; shape->pen.setColor(Qt::black); shape->pen.setWidth(1); shape->brush = Qt::NoBrush; shape->cx = pos.x(); shape->cy = pos.y(); shape->radius = 0; return shape; } void DrawCircleItem::buildMove(const QPointF& pressPos, const QPointF& pos) { double dx = pos.x() - pressPos.x(); double dy = pos.y() - pressPos.y(); radius = std::sqrt(dx * dx + dy * dy); } // ... 实现 paint、rect、move、dragZoom、toJson、fromJson ... ``` **关键点**: - 继承 `I_DrawShape` **和** `Drawable` - 实现 `static Descriptor descriptor()` 和 `static OwnerPtr create(const QPointF&)` - 实现 `void buildMove(...)` 覆盖,定义拖拽创建时的几何更新逻辑 ### 第三步:注册到构建系统 在 `DrawPanel/DrawPanel.cmake` 中添加源文件: ```cmake target_sources(${PROJECT_NAME} PRIVATE # ... 原有文件 ... ${CMAKE_CURRENT_LIST_DIR}/DrawCircleItem.cpp ${CMAKE_CURRENT_LIST_DIR}/DrawCircleItem.h ) ``` ### 第四步:注册到 DrawPanelAutoRegister 在 `DrawPanel/DrawPanelAutoRegister.cpp` 中加入: ```cpp #include "DrawCircleItem.h" DrawPanelAutoRegister::DrawPanelAutoRegister() { // ... 原有注册 ... Drawable::registerType(); } ``` ### 第五步:(可选)在 QML 工具栏添加按钮 在 `DrawPanelQuick/DrawPanelControl.qml` 中新增按钮: ```qml Button { text: '圆形' flat: drawPanel.buildDrawType !== Jie.drawType.DrawCircleType onClicked: drawPanel.buildDrawType = Jie.drawType.DrawCircleType } ``` ### 完成 重新构建项目,圆形图形即可正常使用。 **整个过程中只需要新增一个类 `DrawCircleItem`,不需要写 `DrawCircleComponent`,不需要任何宏,不需要修改 `DrawPanel` 控制器**。`DrawPanelAutoRegister` 会在面板构造时自动将新类型注册到全局注册表。 --- ## 目录结构 ``` QuickDrawPanel/ ├── CMakeLists.txt # 顶层 CMake ├── README.md # 本文件 ├── DrawPanel/ # 核心绘图库(纯 C++,不依赖 QML) │ ├── DrawPanel.cmake # 核心层源文件列表 │ ├── DrawType.h # 图形类型枚举(同时注册为 QML 枚举) │ ├── I_DrawShape.h / .cpp # 图形基类接口、默认实现及自描述注册机制 │ ├── IDrawPanelNotifier.h # 多播通知接口 │ ├── DrawPanel.h / .cpp # 核心控制器(状态机 + 事件总线 + 模式分发) │ ├── I_DrawPanelMode.h # 模式处理器接口 │ ├── ReadOnlyMode.h / .cpp # 只读模式处理器 │ ├── SelectMode.h / .cpp # 选择模式处理器(框选/多选/拖拽缩放) │ ├── BuildMode.h / .cpp # 创建模式处理器 │ ├── DrawPanelAutoRegister.h/.cpp # 集中自动注册所有内置图形 │ ├── DrawPanelNetwork.h/.cpp # 网络同步扩展(继承 DrawPanel) │ ├── DrawLineItem.h / .cpp # 线段图形 │ ├── DrawRectItem.h / .cpp # 矩形图形 │ ├── DrawEllipseItem.h / .cpp # 椭圆图形 │ ├── DrawRoundedRectItem.h/.cpp # 圆角矩形图形 │ ├── DrawTriangleItem.h / .cpp # 三角形图形 │ ├── DrawDiamondItem.h / .cpp # 菱形图形 │ ├── DrawPathItem.h / .cpp # 自由路径图形 │ ├── DrawTextItem.h / .cpp # 文本图形(支持编辑) │ ├── DrawArrowItem.h / .cpp # 箭头图形 V1(继承 DrawLineItem) │ ├── DrawArrow_V2Item.h / .cpp # 箭头图形 V2(继承 DrawArrowItem) │ ├── DrawStarItem.h / .cpp # 五角星图形 │ ├── DrawMoonItem.h / .cpp # 月亮/月牙图形 │ └── DrawCheckItem.h / .cpp # 对勾图形 ├── DrawPanelQuick/ # QML 封装层 │ ├── CMakeLists.txt # QML 模块构建脚本 │ ├── DrawPanelQuick.h / .cpp # QQuickPaintedItem 封装类 │ └── DrawPanelControl.qml # QML 工具栏示例控件 ├── DrawPanelWidget/ # Qt Widgets 封装层 │ └── DrawPanelWidget.h / .cpp # QWidget 封装类 ├── app/ # 示例程序 │ ├── qml/ # QML 示例 │ │ ├── CMakeLists.txt │ │ ├── main.cpp │ │ └── Main.qml │ └── widget/ # Widget 示例 │ ├── CMakeLists.txt │ └── main.cpp └── build/ # 构建输出目录 ``` --- ## 常见问题 ### 为什么 `DrawPanelQuick` 是动态库(SHARED)而 `DrawPanelWidget` 是静态库(STATIC)? 这是由 **QML 模块的加载机制** 决定的: | 封装层 | 库类型 | 原因 | |--------|--------|------| | `DrawPanelQuick` | **SHARED** (DLL) | `qt_add_qml_module()` 生成的 QML 插件在 SHARED 背景库下是真正的运行时插件,QML 引擎按 `lib/qml/DrawPanelQuick/qmldir` 通过 `QPluginLoader` 加载;export 端只需要标准的 `install(TARGETS ... LIBRARY/ARCHIVE/RUNTIME)`,不必处理 `_qmlcache` / `_resources` / `plugin_init` 这些静态 QML 模块带来的 OBJECT 库传播。 | | `DrawPanelWidget` | **STATIC** | Widgets 前端没有 QML 加载需求,直接静态链接进可执行文件即可。静态链接无需部署 DLL,也没有 ABI 边界,部署最简单。 | **核心层 `DrawPanel` 本身既不是独立动态库也不是独立静态库**,而是通过 `target_sources()` 把源码直接注入两个封装层。每个封装层各自携带一份完整的核心层代码,互不依赖,可独立部署。 如果希望核心层作为**独立静态库**被两者共享,可以进一步将 `DrawPanel` 抽取为 `add_library(DrawPanelCore STATIC ...)`,但这会增加 CMake 的复杂度。当前设计保持了简单性。 ### Windows 上的导出/导入符号是怎么处理的? 走的是常规的 `__declspec(dllexport/dllimport)` + 编译期宏切换,**不**依赖 `WINDOWS_EXPORT_ALL_SYMBOLS`。具体由 `DrawPanel/QuickDrawPanel_Export.h` 中的 `QUICKDRAWPANEL_EXPORT` 宏统一控制: ```cpp #if defined(_WIN32) || defined(__CYGWIN__) # if defined(QUICKDRAWPANEL_STATIC_DEFINE) # define QUICKDRAWPANEL_EXPORT # elif defined(QUICKDRAWPANEL_BUILDING_LIBRARY) # define QUICKDRAWPANEL_EXPORT __declspec(dllexport) # else # define QUICKDRAWPANEL_EXPORT __declspec(dllimport) # endif #else # define QUICKDRAWPANEL_EXPORT __attribute__((visibility("default"))) #endif ``` 三种使用场景下宏的展开: | 场景 | 上下文宏 | `QUICKDRAWPANEL_EXPORT` 展开为 | 来源 | |------|----------|--------------------------------|------| | 编译 `DrawPanelQuick.dll` (SHARED) | `QUICKDRAWPANEL_BUILDING_LIBRARY` 已定义 | `__declspec(dllexport)` | `DrawPanelQuick/CMakeLists.txt` 里 `target_compile_definitions(... PRIVATE QUICKDRAWPANEL_BUILDING_LIBRARY)` | | 编译 `DrawPanelWidget.lib` (STATIC) 或它的下游 | `QUICKDRAWPANEL_STATIC_DEFINE` 已定义 | 空 | `DrawPanelWidget/CMakeLists.txt` 里 `target_compile_definitions(... PUBLIC QUICKDRAWPANEL_STATIC_DEFINE)`, 自动随 INTERFACE 传给下游 | | 编译使用 `DrawPanelQuick.dll` 的应用 | 两个宏都未定义 | `__declspec(dllimport)` | `find_package` 不向下游传任何 DLL 相关宏, 下游处于默认状态 | 源码中只有真正公开给使用方的类才打 `QUICKDRAWPANEL_EXPORT` 注解 (`DrawPanel`、`I_DrawShape`、所有 `Draw*Item`、`DrawPanelQuick`、`DrawPanelWidget`、`DrawPanelNetwork` 等共 23 处); 纯虚接口 (`IDrawPanelNotifier`、`I_DrawPanelMode`) 和模板 (`Drawable`) 不需要注解, 因为它们只产生 inline / template 符号, 在使用方编译时各自实例化。 这种做法带来的好处: - 编译出来的 DLL 只导出真正公开的符号, 内部函数随意改不破坏 ABI; - Linux/macOS 上同一套宏天然适配 GCC/Clang 的 `visibility("default")`, 给目标加 `-fvisibility=hidden` 就能做符号隐藏, 不需要换写法; - 改头文件的时候编辑器/IDE 能直观看出哪些是公开 API。 --- ## 许可证 本项目采用 MIT 许可证(或根据实际仓库许可证修改)。