简介:本资源是一套基于Qt 5.13.1开发的流程图绘制软件完整源码工程,面向Qt中级开发者及图形界面编程学习者,解决流程图元素管理、交互操作与命令式架构设计等典型问题。项目采用QGraphicsView框架构建场景,融合命令模式实现撤销/重做,支持SVG图标渲染、文本编辑、磁吸连线、拖放布局及XML格式的流程图持久化存储,具备生产级功能雏形。压缩包共122个文件(121KB),含15个核心cpp实现类(如qgraphicsflowchartitem.cpp、qgraphicsconnectinglineitem.cpp)、14个头文件、28个SVG资源、55个PNG图标及pro/user/ui等工程配置文件,模块划分清晰,便于理解QGraphicsItem定制与场景事件分发机制。已有691人学习下载,提供可直接编译运行的Windows可执行程序与完整源码,涵盖从基础绘图到高级交互的全链路实现细节。
1. 项目缘起:为什么用QT和命令模式来造一个流程图工具?
几年前,我接手了一个需要频繁绘制和修改业务流程图的项目。市面上成熟的流程图工具很多,但要么是云端服务,数据安全有顾虑;要么是桌面软件,但二次开发接口封闭,无法与我们内部的业务系统深度集成。更头疼的是,业务逻辑经常变动,流程图需要反复修改,每次修改后,如何清晰地记录变更历史、如何方便地撤销和重做,成了团队协作的痛点。当时我就想,能不能自己动手,做一个既能满足定制化需求,又能完美支持“后悔药”功能的流程图工具?
这就是这个项目的起点。我选择了QT作为开发框架,这几乎是桌面端C++开发的不二之选,其强大的跨平台能力和成熟的GUI组件库,能让我把精力集中在业务逻辑而非底层渲染上。而要实现流畅的撤销重做,命令模式(Command Pattern)就成了架构设计的核心。这个模式将用户的操作(如添加节点、移动连线、修改属性)封装成一个个独立的“命令”对象。执行操作时,我们执行命令;撤销时,我们执行命令的逆操作。所有命令被按顺序压入栈中,这就天然地构成了一个完整的操作历史记录。
所以,这个项目不仅仅是一个“画图软件”,它更是一个展示如何将经典设计模式(命令模式)与强大的GUI框架(QT的Graphics View框架)相结合,来解决实际工程问题的典型案例。最终产出的,是一个功能完整、架构清晰、可直接编译运行的QT项目源代码。无论你是想学习QT的高级应用、深入理解命令模式,还是需要一个可扩展的流程图组件基础,这份代码都能提供一个扎实的起点。
2. 核心架构解析:Graphics View框架与命令模式的深度融合
要理解这个项目,必须吃透两大支柱:QT的Graphics View框架和命令模式。它们一个负责“看得见”的渲染与交互,一个负责“看不见”的命令管理与历史回溯。
2.1 QGraphicsView框架:构建可视化画布的基石
QT的Graphics View框架是一个用于管理和交互大量2D图形项(QGraphicsItem)的系统。它采用MVC(Model-View-Controller)的变体思想,非常适合用来构建流程图、绘图软件、数据可视化等应用。
- 场景(QGraphicsScene):这是所有图形项的容器,相当于一个虚拟的、无限大的画布。在我们的流程图软件中,场景管理着所有的流程节点(矩形、菱形等)和连接线。
- 视图(QGraphicsView):这是一个可视化组件,用于显示场景的内容。它提供了缩放、平移、滚动等视口变换功能。用户通过视图与场景中的图形项进行交互。
- 图形项(QGraphicsItem):这是所有可显示元素的基类。我们需要继承它来实现自定义的流程图节点(如
ProcessNodeItem、DecisionNodeItem)和连接线(ConnectionLineItem)。每个图形项可以处理自己的绘制、鼠标事件(点击、拖拽)和碰撞检测。
在这个项目中,我并没有使用现成的图形项,而是全部进行了自定义。例如,一个流程节点项,内部需要存储业务数据(如节点ID、名称、类型),对外需要提供连接点(锚点),以便连线能够正确地吸附。连接线项则需要实现动态路径计算(如折线),并在端点图形项移动时自动更新路径。
注意:直接使用QGraphicsRectItem等基础项虽然简单,但扩展性差。自定义项虽然前期工作量稍大,但能让你完全掌控项的行为和外观,是实现复杂交互(如连接点吸附、属性编辑)的前提。
2.2 命令模式:实现优雅的撤销/重做机制
命令模式的核心思想是“将请求封装为对象,从而允许用户使用不同的请求、队列或日志来参数化其他对象,并支持可撤销的操作”。听起来有点绕,结合我们的项目就很好理解。
假设用户执行了“添加一个矩形节点”的操作。在没有命令模式的情况下,我们可能在鼠标释放事件中直接调用scene->addItem(new NodeItem(...))。这样做,一旦需要撤销,我们很难知道该删除哪个项,或者这个项之前的状态是什么。
引入命令模式后,整个过程被重构:
- 定义命令接口(ICommand):通常包含
execute()(执行)、undo()(撤销)两个纯虚函数。 - 创建具体命令类:例如
AddNodeCommand。它的构造函数会接收必要的参数(如场景指针、节点类型、初始位置)。- 在
execute()方法中,它创建节点项,并将其添加到场景中。 - 在
undo()方法中,它将这个节点项从场景中移除并销毁。 - 关键点:命令对象自己持有对该节点项的引用(或唯一ID),这样在撤销时才能精准操作。
- 在
- 命令管理器(CommandManager):这是模式的大脑。它维护两个栈:
undoStack(撤销栈)和redoStack(重做栈)。- 当用户执行一个操作时,UI层不直接操作场景,而是创建一个对应的命令对象,调用其
execute(),然后将其压入undoStack,同时清空redoStack。 - 当用户点击“撤销”时,从
undoStack弹出栈顶命令,调用其undo(),然后将该命令压入redoStack。 - 当用户点击“重做”时,从
redoStack弹出栈顶命令,调用其execute(),然后将其压回undoStack。
- 当用户执行一个操作时,UI层不直接操作场景,而是创建一个对应的命令对象,调用其
这种设计的威力在于:
- 解耦:UI层(如鼠标事件处理)不再关心具体的图形操作,只负责创建和提交命令。
- 可扩展:新增一种操作(如“批量对齐节点”),只需新增一个命令类,无需修改现有命令管理逻辑。
- 支持宏命令:可以将多个简单命令组合成一个宏命令,实现“一步撤销一连串操作”。
在项目中,我实现了一个CommandManager单例类,方便在整个应用范围内访问。所有对流程图模型的修改,都必须通过命令来进行。
3. 关键实现细节:从图形项到命令流
理解了架构,我们深入到代码层面,看看几个最关键的实现细节。这些细节决定了软件的流畅度和健壮性。
3.1 自定义流程图图形项的设计与实现
图形项是用户直接操作的对象,其设计至关重要。
流程节点项(FlowNodeItem): 我设计了一个基类FlowNodeItem,它继承自QGraphicsRectItem(因为大部分节点是矩形或圆角矩形)。这个基类定义了所有节点的共性:
- 边界矩形与绘制:重写
paint()函数,根据节点类型(普通步骤、判断、开始/结束)绘制不同的外观(填充色、边框、图标)。 - 连接点(Anchor Points):节点通常有多个连接点(上、下、左、右)。我在项的内部维护了一个连接点列表(
QList<QPointF>),这些点的位置随节点大小和位置动态计算。paint()函数会将这些点绘制为小圆形。 - 数据模型:每个图形项持有一个指向底层数据模型(如
NodeData)的指针或ID。模型存储业务属性(名称、描述、责任人等),视图项负责显示。这种Model-Item的分离,便于未来将数据导出为JSON或XML。 - 事件处理:重写
mousePressEvent,mouseMoveEvent,mouseReleaseEvent来实现拖拽。重写itemChange()函数,监听ItemPositionChange事件,这样当节点被移动时,我们可以自动更新所有与之相连的连接线。
连接线项(ConnectionLineItem): 连接线继承自QGraphicsPathItem,因为它需要绘制复杂的路径(直线或折线)。
- 路径计算:这是难点。连线不能直接从源节点的中心画到目标节点的中心,那样会穿过节点内部。正确的方式是从源节点的一个连接点画到目标节点的一个连接点。我的实现是:
- 当用户开始拖拽连接时,确定源节点和起始连接点。
- 拖拽过程中,线的一端固定在起始连接点,另一端跟随鼠标。
- 当鼠标在目标节点的连接点上释放时,确定目标连接点。
- 使用
QPainterPath绘制一条折线。折线的控制点根据两个连接点的相对位置动态计算,以确保连线尽可能清晰、避免不必要的交叉。例如,如果是从上连接点连到下连接点,可能会画一条简单的竖直线;如果是从左连到右,可能会画一条水平线。
- 自动更新:在
ConnectionLineItem的构造函数中,它会监听源节点和目标节点的positionChanged信号(通过自定义信号或事件过滤)。一旦任一节点移动,连接线就自动调用一个updatePath()函数,重新计算并绘制路径。
3.2 命令类的具体封装与执行
让我们以最典型的AddNodeCommand为例,看看一个命令是如何被完整封装的。
// 命令接口 class ICommand { public: virtual ~ICommand() = default; virtual void execute() = 0; virtual void undo() = 0; QString description() const; // 用于在历史面板中显示 }; // 添加节点命令 class AddNodeCommand : public ICommand { public: AddNodeCommand(QGraphicsScene* scene, NodeType type, const QPointF& pos) : m_scene(scene), m_type(type), m_initPos(pos), m_nodeItem(nullptr) { m_nodeId = generateUniqueId(); // 生成唯一ID } void execute() override { if (!m_nodeItem) { // 首次执行:创建项和数据模型 NodeData* data = new NodeData(m_nodeId, m_type, "New Node"); m_nodeItem = new FlowNodeItem(data); m_nodeItem->setPos(m_initPos); } m_scene->addItem(m_nodeItem); m_scene->update(); // 触发视图刷新 emit nodeAdded(m_nodeId); // 通知其他模块(如属性面板) } void undo() override { m_scene->removeItem(m_nodeItem); m_scene->update(); emit nodeRemoved(m_nodeId); } QString description() const override { return QString("Add %1 Node").arg(nodeTypeToString(m_type)); } private: QGraphicsScene* m_scene; NodeType m_type; QPointF m_initPos; FlowNodeItem* m_nodeItem; // 命令持有项的指针 QString m_nodeId; };关键点分析:
- 命令持有资源:
AddNodeCommand持有了它创建的FlowNodeItem的指针。这是实现精确撤销的关键。在undo()时,它知道要移除哪个项。 - 执行与撤销的对称性:
execute()做addItem,undo()就做removeItem,逻辑完全对称。 - 幂等性考虑:
execute()函数中判断if (!m_nodeItem),确保命令被重复执行时(比如重做),不会重复创建图形项。这是健壮的命令实现需要注意的细节。 - 通知机制:命令执行后,通过信号(如
nodeAdded)通知UI的其他部分(如属性编辑器、树状列表)更新。这保持了模块间的松耦合。
类似的,MoveNodeCommand会在执行时记录节点的旧位置和新位置;DeleteNodeCommand则需要在执行时深度拷贝节点的状态,以便撤销时能完整恢复。
3.3 用户交互与命令的触发链路
整个软件的交互流程形成了一个清晰的链路:用户输入 -> 创建命令 -> 提交管理器 -> 更新视图。
以“拖拽移动一个节点”为例:
- 用户在节点上按下鼠标(
mousePressEvent)。 - 记录节点的初始位置和指针。
- 用户拖拽鼠标(
mouseMoveEvent),项的位置实时更新(这是为了提供流畅的视觉反馈)。 - 用户释放鼠标(
mouseReleaseEvent)。此时,才创建命令。- 计算节点的最终位置。
- 如果最终位置与初始位置不同,则创建一个
MoveNodeCommand对象,传入节点指针、旧位置和新位置。 - 调用
CommandManager::instance()->execute(command)。
- 命令管理器执行该命令(虽然移动已经在视觉上完成,但命令的
execute()方法可能只是更新一下内部状态或发送信号),并将其压入撤销栈。
实操心得:一定要区分“视觉反馈”和“命令提交”。在拖拽过程中实时更新项位置是必须的,但这只是临时状态。只有在交互动作确认完成(鼠标释放)时,才生成命令。这符合用户直觉,也避免了生成大量无意义的中间状态命令。
4. 项目实战:搭建、运行与功能扩展指南
拿到源代码后,你可能会想快速运行起来看看效果,并在此基础上进行修改。这一章就是你的实战手册。
4.1 环境配置与项目编译
这是一个标准的QT项目,使用CMake作为构建系统(这也是目前QT官方推荐的方式,比qmake更现代和强大)。
环境准备:
- 安装QT:建议安装QT 5.15 LTS或QT 6.2及以上版本。可以从QT官网下载在线安装器,在安装时勾选你常用的桌面组件(如MSVC 2019 64-bit或MinGW)和CMake。
- 安装CMake:确保CMake已安装并添加到系统PATH。
- 安装C++编译器:在Windows上,如果你选择了MSVC套件,则需要安装Visual Studio 2019/2022(只需安装C++桌面开发工作负载)。如果选择MinGW,则QT安装包通常会自带。
编译与运行:
- 使用CLion、VS Code with CMake Tools或Qt Creator打开项目根目录的
CMakeLists.txt文件。 - 配置CMake生成目标。通常需要指定
CMAKE_PREFIX_PATH变量为你的QT安装路径下的lib/cmake目录。例如:-DCMAKE_PREFIX_PATH=C:\Qt\6.5.0\msvc2019_64\lib\cmake。 - 生成项目并编译。编译成功后,你会在输出目录找到可执行文件。
- 首次运行,你将看到一个空白的画布。左侧是工具栏(选择、矩形节点、菱形节点、连线等),右侧是属性面板。
常见编译问题:
- 找不到QT模块:检查
CMakeLists.txt中的find_package(Qt6 COMPONENTS Core Gui Widgets)语句,确保你安装的QT版本和指定的组件是正确的。对于QT5,应使用find_package(Qt5 COMPONENTS Core Gui Widgets)。 - 链接错误:确保CMake的
target_link_libraries正确链接了所有用到的QT模块,如Qt6::Core Qt6::Gui Qt6::Widgets。 - 中文乱码:QT默认使用UTF-8。确保你的源代码文件是UTF-8编码(不带BOM)。在Windows上,如果遇到中文显示问题,可以在
main.cpp中早期加入QTextCodec::setCodecForLocale(QTextCodec::codecForName("UTF-8"));(QT5)或使用QString::fromUtf8。
4.2 核心功能使用与操作流程
软件的基本操作流程遵循“选择工具 -> 在画布上操作”的模式。
添加节点:
- 点击工具栏的“矩形节点”或“菱形节点”图标。
- 在画布空白处单击,一个对应类型的节点即被创建。
- 背后原理:工具模式切换后,画布的鼠标按下事件会创建对应的
AddNodeCommand并执行。
连接节点:
- 点击工具栏的“连接线”图标。
- 鼠标在一个节点的连接点(边缘的小圆圈)上按下并拖拽。
- 拖拽到另一个节点的连接点上释放。
- 背后原理:拖拽开始时会创建一个临时的连接线项跟随鼠标,释放时创建
AddConnectionCommand,命令对象会计算最优路径并创建最终的ConnectionLineItem。
移动与编辑:
- 切换回“选择工具”(箭头图标)。
- 点击节点或连线可以选中它们。选中后,可以拖拽移动。
- 在右侧属性面板,可以修改选中项的文本、颜色等属性。
- 背后原理:每次修改都会生成对应的命令(
MoveNodeCommand,ModifyPropertyCommand)。
撤销与重做:
- 使用工具栏的“撤销”/“重做”按钮,或快捷键
Ctrl+Z/Ctrl+Y。 - 历史操作列表会在一个停靠窗口中显示。
- 背后原理:点击按钮触发
CommandManager的undo()或redo()方法。
- 使用工具栏的“撤销”/“重做”按钮,或快捷键
保存与加载:
- 流程图可以保存为自定义的JSON格式文件。这个格式序列化了所有节点和连线的数据模型(位置、类型、属性、连接关系)。
- 背后原理:保存时,遍历场景中的所有项,将其底层数据模型转换为JSON对象。加载时,解析JSON文件,按顺序创建
AddNodeCommand和AddConnectionCommand来重建整个流程图。注意,加载过程本身也是一个可撤销的宏命令。
4.3 如何进行功能扩展与二次开发
这个项目的架构设计就是为了扩展而生的。以下是几个常见的扩展方向:
1. 添加新的节点类型: 假设你需要一个“数据库”节点,图标是个圆柱体。
- 步骤一:在
NodeType枚举中新增Database。 - 步骤二:创建一个新的图形项类
DatabaseNodeItem,继承自FlowNodeItem。主要重写paint()函数,在里面绘制你的自定义外观。 - 步骤三:在节点工厂(如果有的话)或工具类中,将
NodeType::Database映射到DatabaseNodeItem的创建逻辑。 - 步骤四:在工具栏添加对应的按钮。按钮点击后,将当前工具模式设置为“添加数据库节点”。
2. 修改连接线样式: 比如将折线改为贝塞尔曲线。
- 找到
ConnectionLineItem::updatePath()方法。 - 将其中计算折线路径的逻辑,替换为计算二次或三次贝塞尔曲线路径的逻辑。你需要确定合适的控制点。一个简单的策略是将控制点放在两个连接点连线的中垂线上。
- 在
paint()函数中,可以使用QPainterPath::cubicTo来绘制贝塞尔曲线。
3. 增加新的属性: 为节点增加一个“处理时限”属性。
- 步骤一:在
NodeData类中添加一个QTime或int类型的成员变量timeLimit,并提供getter/setter。 - 步骤二:修改节点属性面板的UI,增加一个用于编辑时限的控件(如QSpinBox)。
- 步骤三:建立属性控件与
NodeData模型的绑定。当控件值改变时,生成一个ModifyPropertyCommand命令。 - 步骤四:在
FlowNodeItem::paint()中,可以选择将这个时限信息也绘制在节点上。
4. 实现流程图导出为图片: QT提供了非常方便的导出功能。
void exportToImage(const QString& filePath, QGraphicsScene* scene) { QRectF sceneRect = scene->itemsBoundingRect(); // 获取所有项的边界矩形 QImage image(sceneRect.size().toSize(), QImage::Format_ARGB32); image.fill(Qt::transparent); // 透明背景 QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing); scene->render(&painter, QRectF(), sceneRect); // 将场景渲染到图像上 image.save(filePath); // 保存为PNG等格式 }你可以将这个功能添加到“文件”菜单中。
扩展性心得:在扩展时,时刻牢记“命令模式”和“数据-视图分离”。任何对流程图状态的修改,都应通过命令进行。任何新的图形表现,都应基于数据模型来驱动。这样能保证你的扩展不会破坏已有的撤销重做和历史记录功能。
5. 避坑指南:开发过程中遇到的典型问题与解决方案
在开发这个项目的过程中,我踩过不少坑。这里总结几个最具代表性的,希望能帮你节省时间。
5.1 图形项刷新与性能优化
问题描述:当画布上有数百个节点和连线时,进行平移、缩放操作会明显感到卡顿。
根因分析:
- 不必要的重绘:默认情况下,
QGraphicsView的视口更新策略可能导致整个视图频繁重绘。 - 复杂的图形项:自定义的
paint()函数如果包含复杂计算或多次绘制调用,会消耗大量CPU。 - 连接线更新风暴:移动一个节点,会触发所有与之相连的连接线重算路径并重绘。如果节点连接数很多,就是一次O(n)的操作。
解决方案:
设置视图更新标志:
m_graphicsView->setViewportUpdateMode(QGraphicsView::SmartViewportUpdate); // 或 FullViewportUpdate / MinimalViewportUpdate / NoViewportUpdate 根据场景选择 m_graphicsView->setOptimizationFlag(QGraphicsView::DontSavePainterState, true); m_graphicsView->setOptimizationFlag(QGraphicsView::DontAdjustForAntialiasing, true);SmartViewportUpdate是较好的平衡选择,它只重绘视图中发生变化的部分区域。优化paint函数:
- 避免在
paint()中进行任何非绘制的计算(如路径计算)。这些计算应该在数据改变时(如itemChange)预先算好,存储起来。 - 使用
QStyleOptionGraphicsItem来获取绘制状态,避免不必要的样式判断。 - 对于复杂的背景或静态部分,考虑使用缓存:
setCacheMode(QGraphicsItem::DeviceCoordinateCache)。这会将项渲染到像素图中,加速后续绘制,但会消耗更多内存。
- 避免在
连接线更新优化:
- 为连接线项实现一个“延迟更新”机制。当收到节点移动的信号时,不立即重算路径,而是设置一个脏标志(
m_pathDirty = true),并请求更新。 - 在
advance()阶段或下一个绘画事件中,批量处理所有标记为脏的连接线,一次性更新路径。这可以避免一帧内多次计算。 - 对于非常复杂的流程图,可以考虑使用空间索引(如
QGraphicsScene的itemIndexMethod设置为BspTreeIndex)来加速项的选择和碰撞检测,但对绘制性能提升有限。
- 为连接线项实现一个“延迟更新”机制。当收到节点移动的信号时,不立即重算路径,而是设置一个脏标志(
5.2 命令对象的生命周期与内存管理
问题描述:在频繁执行和撤销操作后,有时会出现内存泄漏或访问野指针的情况。
根因分析:
- 命令持有资源:命令对象(如
DeleteNodeCommand)为了能在撤销时恢复,必须保存被删除节点的完整状态(深拷贝)。如果管理不当,容易造成内存泄漏。 - 指针失效:命令被压入栈后可能长期存在。如果命令中持有图形项(
QGraphicsItem*)的裸指针,而该图形项被其他逻辑意外删除(非通过命令),就会导致悬空指针,在撤销时引发崩溃。
解决方案:
- 明确所有权:确立一个核心原则——场景(Scene)是图形项生命周期的唯一管理者。命令对象只能通过场景来添加或删除项,不能自行
new和delete(除非是命令自己创建且尚未交给场景的项)。 - 使用智能指针(推荐):在命令内部,对于需要持久化保存的数据(如节点状态快照),使用
std::unique_ptr或QScopedPointer来管理。确保在命令析构时,这些资源能被自动释放。class DeleteNodeCommand : public ICommand { // ... 其他成员 private: std::unique_ptr<NodeData> m_snapshot; // 使用智能指针管理快照 QPointer<FlowNodeItem> m_nodeItem; // 使用QPointer,它会在对象被删除后自动变为nullptr }; - 使用唯一标识符:另一种更安全但复杂的方法是,命令不保存图形项指针,而是保存图形项的唯一ID(如
QUuid)。在执行或撤销时,命令通过ID向一个中央管理器(或场景)请求获取对应的项指针。这完全解耦了命令和项的生命周期,但增加了查找开销。
5.3 复杂交互状态的管理
问题描述:在实现“连线”工具时,需要处理多个状态:等待点击起点、拖拽中、等待点击终点。状态管理混乱容易导致bug,比如鼠标释放事件处理不当。
根因分析:将不同交互模式(选择、添加节点、连线)的状态变量混杂在同一个主窗口或视图类中,使用大量的if-else进行判断,代码难以维护。
解决方案:引入状态模式(State Pattern)来管理画布的交互状态。
- 定义画布状态接口:
class CanvasState { public: virtual void onMousePress(...) = 0; virtual void onMouseMove(...) = 0; virtual void onMouseRelease(...) = 0; ... }。 - 实现具体状态:
SelectionState:处理框选、移动项。AddNodeState:处理点击画布添加节点。AddConnectionState:这是一个有内部子状态的状态机。它本身可以进一步分为IdleState(等待点击起点)、DraggingState(正在拖拽连线)等。或者,直接在AddConnectionState的方法内用枚举管理子状态。
- 在视图事件处理中委托给当前状态:
void GraphicsView::mousePressEvent(QMouseEvent* event) { if (m_currentState) { m_currentState->onMousePress(event, this->mapToScene(event->pos())); } QGraphicsView::mousePressEvent(event); // 仍传递给场景进行默认处理 } - 切换状态:当用户点击工具栏按钮时,将
m_currentState设置为对应的状态对象。
这样,每个交互模式的逻辑被封装在独立的类中,清晰且易于扩展。要新增一个“画笔”工具,只需新增一个FreeDrawState类即可。
5.4 自定义图形项的边界与碰撞检测
问题描述:自定义的圆角矩形节点,其可见的圆角边界和实际的矩形边界(boundingRect())不匹配,导致鼠标点击边缘区域(圆角外的直角部分)也能选中节点,体验不佳。
根因分析:QGraphicsItem的shape()函数默认返回一个基于boundingRect()的简单形状(通常是矩形)。碰撞检测(包括鼠标点击命中测试)和部分绘制优化(如裁剪)都依赖于shape()或boundingRect()。
解决方案:重写shape()和boundingRect()函数。
- 精确的
boundingRect():返回一个完全包含项所有像素的矩形,包括笔触的宽度。例如,如果你的圆角矩形宽高为w, h,笔触宽度为penWidth,那么boundingRect()应该返回QRectF(-penWidth/2, -penWidth/2, w+penWidth, h+penWidth)。 - 更精确的
shape():返回项的实际形状。对于圆角矩形,可以使用QPainterPath来创建一个圆角矩形路径并返回。QPainterPath FlowNodeItem::shape() const { QPainterPath path; path.addRoundedRect(boundingRect().adjusted(penWidth/2, penWidth/2, -penWidth/2, -penWidth/2), cornerRadius, cornerRadius); return path; } - 可选:重写
contains():如果shape()计算开销大,可以重写contains()函数来提供一个更快的点包含测试。但对于圆角矩形,直接使用shape().contains(point)通常即可。
经过这样优化后,鼠标只有在点击到圆角矩形实际的可见区域时,才会选中该项,交互体验更加精准。
这个项目从构思到实现,是一个不断权衡设计、性能和开发效率的过程。命令模式带来了清晰的历史管理,但也增加了每个操作的开销(需要创建命令对象)。Graphics View框架提供了强大的基础设施,但要做出流畅的体验,仍需在细节上精心打磨。希望这份详细的剖析和源代码,能为你自己的图形界面项目提供一个坚实的跳板。记住,好的架构不是限制,而是为了让后续的扩展和修改变得更简单。
本文还有配套的精品资源,点击获取