参考:
Qt Graphics View Framework_w3cschool
https://www.w3cschool.cn/learnroadqt/4mvj1j53.html
C++ GUI Programming with Qt 4, Second Edition
本地环境:
win10专业版,64位,Qt 5.12
基础知识
QPainter
比较适合少量绘图的情况,如果一个图里包含成千上百个单元,同时要让用户可以点击、拖动、选择,那么应该使用grahics view。
这个架构包含:一个scene(对应QGraphicsScene
类),里面包含很多item(对应QGraphicsItem
类)。用户要看到这个scene,需要借助view(对应QGraphicsView
类)。一个scene可以对应多个view,比如不同的view可以看到scene的不同部分,或者不同的view可以看到经过不同transformation后的scene。如下图所示:
一个scene包含三层:foreground layer, background layer和item layer。前两个可以用QBrush
修改,也可以通过重写drawBackground()
和drawForeground()
实现完全的控制。举个例子,如果我们想用一个pixmap作为背景,那可以用QBrush
制作一个基于这个pixmap的纹理。
scene可以告诉我们哪些item冲突了,哪些被选择了,或者哪些在哪里这样的信息。item也可以嵌套,此时如果对这个item做transformation,那么它包含的item也会自动被transform。
graphics view架构提供了两种item分组方式:1是前面提到的嵌套,就是让一个item是另一个item的孩子;2是使用QGraphicsItemGroup
,加入group的item不会像嵌套一样传递transformation,它们每一个都像独立的个体(类似集合?)
item有预定义的(见下图),也可以重写。
QGraphicsView
实际是一个组件(widget),提供滚动条(如果需要),并且通过transformation改变scene的呈现效果。它默认使用Qt内置的2D绘制引擎,但是也可以用OpenGL组件(构造后调用setViewport()
,用到再写)。
graphics view架构使用三种坐标系:(同时提供了三种坐标系互转的函数)
- viewport坐标系:
QGraphicsView
的viewport - scene坐标系:是逻辑坐标系,用来放置scene顶层的item
- item坐标系:只与item相关,用来放置子item和绘制item
在实际操作中,我们通常只关心后两个坐标系。
简单例子
如下图所示,在窗口中绘制一条线,无论怎么放缩窗口,这条线始终居中。如果使用QPainter
来画,保持居中就需要一些计算,但是用graphics view架构,代码非常短,也没有计算。
#include <QGraphicsScene>
#include <QGraphicsView>QGraphicsScene *scene = new QGraphicsScene;
scene->addLine(10, 10, 150, 300);
QGraphicsView *view = new QGraphicsView(scene);
view->resize(500, 500);
view->setWindowTitle("Graphics View");
view->show();
进阶例子:自定义item
效果
例子来自ch8-Item-Based Rendering with Graphics View-diagram(实在找不到书和代码的可以私信我,虽然我觉得挺好找的)。大概是构造一个软件,用户可以放置一些Node,并且彼此相连,同时可以拖动上面的元素,并且设置一些属性。
知识储备
QGraphicsLineItem
不是QObject
的子类,如果要添加信号需要另外继承QObject
(多继承)- graphics-view架构使用bounding rectangle技术。通过这个,可以判断某个点是否在item只中,也可以判断两个item是否碰撞,或者某个item是否需要被绘制(如果scene很大,当只需要绘制一部分时,如果某个item不在可视范围,那通过这个矩形是可以快速判断的,从而提升效率)
- 因此如果要完全自定义item,需要重新实现
boundingRect()
和paint()
(也许还有shape()
) 。其中shape是比boundingrect更精确的形状,可以应对细粒度碰撞检测(fine-grained collision detection)的情况,特别是当外形是圆角矩形时。比如如果重写了shape()
,那么当点击夹在shape和bounding rectangle之间的区域时,是不会选中item的。也可以不重写它。 - 根据前面的知识可知,如果图形要重绘需要调用
update()
,那么如果bounding rectangle可能会改变,就要调用prepareGeometryChange()
设计思路
此处不展开完整的实现,只说明文档结构。
link是两个node之间的连线,也是最简单的item,因为只需要考虑起点、终点和线条颜色(线宽默认),所以使用QGraphicsLineItem
的一些函数就可以写好。Node是包含文字的圆角矩形框,需要完整地绘制,包括bounding rectangle,也包括一些互动(双击快速修改文字,修改属性)。propertiesdialog是属性设置窗口,写的是窗口上那些按钮的响应。最后diagramwindow就是主窗口,写的是主窗口上那些工具的布局、QAction和元素的初始化等。
实现
这里只展示 Node 和 Link。
Link
#ifndef LINK_H
#define LINK_H#include <QGraphicsLineItem>QT_BEGIN_NAMESPACE
class Node;
QT_END_NAMESPACE// QGraphicsLineItem不是QObject的子类,如果要添加信号需要另外继承QObject
// 不用额外的QColor变量记录颜色是因为颜色设置很简单,只是用一下QPen
class Link : public QGraphicsLineItem
{
public:Link(Node *fromNode, Node *toNode);~Link();// 获取两个端点Node *fromNode() const;Node *toNode() const;// 线的颜色void setColor(const QColor &color);QColor color() const;// 连线操作void trackNodes();private:Node *myFromNode;Node *myToNode;
};#endif// -------------------------------------------------------#include <QtWidgets>#include "link.h"
#include "node.h"Link::Link(Node *fromNode, Node *toNode)
{myFromNode = fromNode;myToNode = toNode;myFromNode->addLink(this);myToNode->addLink(this);// 选中是为了可以删除setFlags(QGraphicsItem::ItemIsSelectable);// z表示离窗口的远近,-1就是放在两个node下面,这样虽然是// 连接两个Node中心,但是看起来好像连接的是两个最近的边缘一样setZValue(-1);// 设置线的颜色setColor(Qt::darkRed);// 连线trackNodes();
}Link::~Link()
{myFromNode->removeLink(this);myToNode->removeLink(this);
}Node *Link::fromNode() const
{return myFromNode;
}Node *Link::toNode() const
{return myToNode;
}void Link::setColor(const QColor &color)
{// 1.0是线宽setPen(QPen(color, 1.0));
}QColor Link::color() const
{return pen().color();
}void Link::trackNodes()
{/* QGraphicsLineItem::pos()返回的是相对于父的坐标,这个父可以是* scene(如果这个item是top-level的),也可以是item(嵌套的情况)* QLineF是创建一条2D线段* QGraphicsLineItem::setLine*/setLine(QLineF(myFromNode->pos(), myToNode->pos()));
}
Node
#ifndef NODE_H
#define NODE_H#include <QApplication>
#include <QColor>
#include <QGraphicsItem>
#include <QSet>QT_BEGIN_NAMESPACE
class Link;
QT_END_NAMESPACEclass Node : public QGraphicsItem
{// 允许用tr(),不用再写QObject::tr()或者QCoreApplication::translate()Q_DECLARE_TR_FUNCTIONS(Node)public:Node();~Node();// 一些setter, gettervoid setText(const QString &text);QString text() const;void setTextColor(const QColor &color);QColor textColor() const;void setOutlineColor(const QColor &color);QColor outlineColor() const;void setBackgroundColor(const QColor &color);QColor backgroundColor() const;void addLink(Link *link);void removeLink(Link *link);QRectF boundingRect() const;QPainterPath shape() const;void paint(QPainter *painter,const QStyleOptionGraphicsItem *option, QWidget *widget);protected:// 双击以快速修改textvoid mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event);// 处理node被移动的情况,此时需要更新相关的Link// itemChange是在图形项的任何属性变化时都会被调用,而 mouseMoveEvent 只在鼠标移动时才会触发QVariant itemChange(GraphicsItemChange change,const QVariant &value);private:QRectF outlineRect() const;int roundness(double size) const;QSet<Link *> myLinks;QString myText;QColor myTextColor;QColor myBackgroundColor;QColor myOutlineColor;
};#endif// -------------------------------------------------------#include <QtWidgets>#include "link.h"
#include "node.h"Node::Node()
{myTextColor = Qt::darkGreen;myOutlineColor = Qt::darkBlue;myBackgroundColor = Qt::white;setFlags(ItemIsMovable | ItemIsSelectable);
}Node::~Node()
{// 删除node时候要删掉所有关联的linkforeach (Link *link, myLinks)delete link;
}void Node::setText(const QString &text)
{// 表示bounding rectangle可能会改变prepareGeometryChange();myText = text;// 重绘update();
}QString Node::text() const
{return myText;
}// color类的setter getter就不展示了,注意setter需要调用update()重绘void Node::addLink(Link *link)
{myLinks.insert(link);
}void Node::removeLink(Link *link)
{myLinks.remove(link);
}QRectF Node::boundingRect() const
{const int Margin = 1;// 左上右下// 表示对outlineRect()返回的矩形进行调整,向左、上、右、下各移动Margin像素// margin的取值应该至少是pen宽度的一半return outlineRect().adjusted(-Margin, -Margin, +Margin, +Margin);
}QPainterPath Node::shape() const
{QRectF rect = outlineRect();QPainterPath path;path.addRoundRect(rect, roundness(rect.width()),roundness(rect.height()));return path;
}void Node::paint(QPainter *painter,const QStyleOptionGraphicsItem *option,QWidget * /* widget */)
{QPen pen(myOutlineColor);if (option->state & QStyle::State_Selected) {pen.setStyle(Qt::DotLine);pen.setWidth(2);}painter->setPen(pen);painter->setBrush(myBackgroundColor);QRectF rect = outlineRect();painter->drawRoundRect(rect, roundness(rect.width()),roundness(rect.height()));painter->setPen(myTextColor);painter->drawText(rect, Qt::AlignCenter, myText);
}void Node::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{QString text = QInputDialog::getText(event->widget(),tr("Edit Text"), tr("Enter new text:"),QLineEdit::Normal, myText);if (!text.isEmpty())setText(text);
}QVariant Node::itemChange(GraphicsItemChange change,const QVariant &value)
{// 每次拖动Node,位置就发生变化了,此时需要重新画线if (change == ItemPositionHasChanged) {foreach (Link *link, myLinks)link->trackNodes();}return QGraphicsItem::itemChange(change, value);
}QRectF Node::outlineRect() const
{const int Padding = 8;QFontMetricsF metrics = static_cast<QFontMetricsF>(qApp->font());QRectF rect = metrics.boundingRect(myText);rect.adjust(-Padding, -Padding, +Padding, +Padding);// 转换坐标是为了让text放在中心,本来默认的坐标是左上角(0,0)rect.translate(-rect.center());return rect;
}int Node::roundness(double size) const
{const int Diameter = 12;return 100 * Diameter / int(size);
}