qt5-入门-2D绘图-Graphics View 架构

发布于:2024-05-04 ⋅ 阅读:(33) ⋅ 点赞:(0)

参考:
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_NAMESPACE

class Node : public QGraphicsItem
{
    // 允许用tr(),不用再写QObject::tr()或者QCoreApplication::translate()
    Q_DECLARE_TR_FUNCTIONS(Node)

public:
    Node();
    ~Node();
    // 一些setter, getter
    void 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:
    // 双击以快速修改text
    void 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时候要删掉所有关联的link
    foreach (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);
}



网站公告

今日签到

点亮在社区的每一天
去签到