保姆级教程:用Qt GraphicsView从零撸一个可拖拽、能折叠的思维导图(附完整源码)
从零构建Qt GraphicsView思维导图轻量化实现与深度定制指南在当今信息爆炸的时代思维导图已成为知识工作者不可或缺的工具。市面上成熟的思维导图软件功能丰富但体积庞大而许多开发者希望在自己的应用中集成轻量级的思维导图功能。本文将带你使用Qt的GraphicsView框架从零开始构建一个完全可定制、支持拖拽和折叠功能的思维导图组件无需依赖任何第三方库。1. 项目架构设计与核心组件1.1 GraphicsView框架的三层结构Qt的GraphicsView框架由三个核心类组成QGraphicsView、QGraphicsScene和QGraphicsItem。理解这三者的关系是构建思维导图的基础QGraphicsView可视化窗口负责显示场景内容和处理用户交互QGraphicsScene逻辑容器管理所有图形项及其空间关系QGraphicsItem基础图形元素我们将继承它创建自定义节点// 基础场景初始化代码 QGraphicsScene *scene new QGraphicsScene(this); QGraphicsView *view new QGraphicsView(scene, this); view-setRenderHint(QPainter::Antialiasing); // 抗锯齿渲染1.2 思维导图节点的数据结构设计一个典型的思维导图节点需要包含以下属性属性类型说明textQString节点显示的文本内容isExpandedbool是否展开子节点parentItemMindMapNode*父节点指针childItemsQListMindMapNode*子节点列表depthint节点在树中的深度class MindMapNode : public QGraphicsItem { public: explicit MindMapNode(const QString text, MindMapNode *parent nullptr); // 必须实现的纯虚函数 QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; // 自定义方法 void addChild(MindMapNode *child); void collapse(); void expand(); private: QString m_text; bool m_isExpanded; // 其他成员变量... };2. 核心功能实现细节2.1 节点绘制与样式定制节点的视觉呈现是用户体验的关键。我们可以通过重写paint()方法实现高度自定义的绘制逻辑void MindMapNode::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *) { // 设置不同深度节点的颜色 QColor fillColor QColor::fromHsv((depth() * 30) % 360, 150, 240); // 绘制圆角矩形背景 painter-setBrush(fillColor); painter-setPen(Qt::NoPen); painter-drawRoundedRect(m_boundingRect, 5, 5); // 绘制文本 painter-setPen(Qt::black); QTextOption textOption(Qt::AlignCenter); painter-drawText(m_boundingRect, m_text, textOption); // 绘制折叠/展开指示器 if (!childItems().isEmpty()) { QRectF indicatorRect m_boundingRect.adjusted(5, 5, -5, -5); painter-drawEllipse(indicatorRect.topRight(), 3, 3); } }提示使用QStyleOptionGraphicsItem可以获取系统主题颜色和状态信息使你的自定义项与系统UI风格保持一致。2.2 拖拽操作的实现实现节点的拖拽功能需要处理以下事件鼠标按下事件记录拖拽起点鼠标移动事件计算偏移量并移动项鼠标释放事件处理可能的父子关系变更void MindMapNode::mousePressEvent(QGraphicsSceneMouseEvent *event) { if (event-button() Qt::LeftButton) { m_dragStartPos event-pos(); setCursor(Qt::ClosedHandCursor); } QGraphicsItem::mousePressEvent(event); } void MindMapNode::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (event-buttons() Qt::LeftButton) { QPointF delta event-pos() - m_dragStartPos; moveBy(delta.x(), delta.y()); } } void MindMapNode::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) { setCursor(Qt::OpenHandCursor); // 检查是否应该建立新的父子关系 checkParentRelationship(); QGraphicsItem::mouseReleaseEvent(event); }2.3 折叠/展开逻辑的实现折叠功能需要隐藏/显示所有子节点调整连接线的可见性重新布局同级节点void MindMapNode::collapse() { if (!m_isExpanded || childItems().isEmpty()) return; m_isExpanded false; foreach (MindMapNode *child, childItems()) { child-hide(); child-collapse(); // 递归折叠所有子节点 } updateConnectors(); emit layoutChanged(); } void MindMapNode::expand() { if (m_isExpanded || childItems().isEmpty()) return; m_isExpanded true; foreach (MindMapNode *child, childItems()) { child-show(); } updateConnectors(); emit layoutChanged(); }3. 高级功能与性能优化3.1 连接线的智能绘制思维导图中的连接线需要考虑父子节点的相对位置关系折叠状态下的显示逻辑美观的曲线过渡void MindMapNode::updateConnectors() { if (!parentItem()) return; QPainterPath path; QPointF start mapToScene(boundingRect().center()); QPointF end parentItem()-mapToScene(parentItem()-boundingRect().center()); // 贝塞尔曲线计算 qreal ctrlOffset qAbs(start.y() - end.y()) / 2; if (start.y() end.y()) { // 子节点在下方 QPointF ctrl1(start.x(), start.y() ctrlOffset); QPointF ctrl2(end.x(), end.y() - ctrlOffset); path.moveTo(start); path.cubicTo(ctrl1, ctrl2, end); } else { // 子节点在上方 // 类似逻辑处理上方连接 } m_connector-setPath(path); m_connector-setVisible(m_isExpanded); }3.2 布局算法优化自动布局是思维导图的核心挑战之一。我们可以采用改进的Walker算法后序遍历计算每个子树的大小前序遍历确定每个节点的位置考虑折叠状态下的空间压缩void MindMapNode::calculateLayout() { if (childItems().isEmpty()) return; // 计算子树的宽度和高度 qreal totalWidth 0; qreal maxHeight 0; foreach (MindMapNode *child, childItems()) { if (!child-isExpanded()) continue; child-calculateLayout(); totalWidth child-subtreeWidth(); maxHeight qMax(maxHeight, child-subtreeHeight()); } // 定位子节点 qreal xOffset -totalWidth / 2; foreach (MindMapNode *child, childItems()) { if (!child-isExpanded()) continue; qreal childWidth child-subtreeWidth(); child-setPos(xOffset childWidth / 2, m_boundingRect.height() 20); xOffset childWidth 30; // 节点间距 } }3.3 性能优化技巧处理大量节点时这些优化策略很有效延迟渲染对不可见区域暂停复杂绘制细节层次(LOD)根据缩放级别调整渲染细节项缓存对静态内容使用setCacheMode(QGraphicsItem::DeviceCoordinateCache)// 在构造函数中启用项缓存 MindMapNode::MindMapNode() { setCacheMode(QGraphicsItem::DeviceCoordinateCache); setFlag(QGraphicsItem::ItemIsMovable); setFlag(QGraphicsItem::ItemSendsGeometryChanges); }4. 集成与扩展应用4.1 数据持久化方案实现XML格式的导入导出QString MindMapNode::toXml() const { QString xml; QXmlStreamWriter writer(xml); writer.writeStartElement(node); writer.writeAttribute(text, m_text); writer.writeAttribute(expanded, m_isExpanded ? true : false); foreach (MindMapNode *child, childItems()) { writer.writeTextElement(child, child-toXml()); } writer.writeEndElement(); return xml; } MindMapNode* MindMapNode::fromXml(const QString xml) { // 解析XML并重建节点树 }4.2 与现代C特性的结合利用C17特性改进代码std::optional处理可能缺失的节点属性std::variant支持多种节点类型结构化绑定简化数据访问std::optionalMindMapNode::NodeStyle MindMapNode::parseStyle(const QVariantMap styleData) { try { return NodeStyle{ .fillColor styleData[fillColor].valueQColor(), .textColor styleData[textColor].valueQColor(), .borderWidth styleData[borderWidth].toDouble() }; } catch (...) { return std::nullopt; } }4.3 与Qt Quick的混合编程通过QQuickPaintedItem将GraphicsView组件嵌入QML界面class MindMapQuickItem : public QQuickPaintedItem { Q_OBJECT public: void paint(QPainter *painter) override { m_scene.render(painter, contentsBoundingRect()); } private: QGraphicsScene m_scene; MindMapNode *m_rootNode; };在QML中使用MindMapQuickItem { width: parent.width height: parent.height onNodeClicked: console.log(Node clicked:, nodeText) }

相关新闻

最新新闻

日新闻

周新闻

月新闻