利用QT 的 Graphics View 系统实现一个 简易的 Graph Editor

news2024/11/27 0:47:20

QT 中的 Graphics View 系统. 是一个相对成熟的渲染引擎的上层框架,通常也可以会叫做 Scene - View。

通常会有 QGraphicsView, QGraphicsScene, QGraphicsItem 这几个类构成。

view是视口(viewport);scene是一个场景,负责容纳各种item;而item就是可见的这些元件。

一般来说,绘图可以使用 QPainter直接在重绘事件中进行绘制,但是,当我们想要选择绘制的图形的时候,就犯难了。我们的painter是直接在屏幕上写写画画,没有人来管理,在当前的mouse事件中也不知道如何处理这些项。

这个时候,Graphics View 就解决了这个问题,通过scene来管理各种图元item项。item在scene上绘制,scene在view上显示。

本文,就是利用Graphics View 系统来实现了一个简单的 有向图/无向图 编辑器。

编辑的图输出效果如下:

image.png


绘制点和绘制线是一个图元,那么就是一个 QGraphicsItem,继承自 QGraphicsItem,然后去重写绘制方法

在绘制点和线的时候,需要重写QGraphicsItem的绘制函数,也就是 paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)

如何绘制点

graphNode类的设计:

class graphNode : public QObject, public QGraphicsItem
{
    Q_OBJECT
public:
    graphNode(QPointF point, int r = 10, QString str = "0");

    // QGraphicsItem interface
public:
    QRectF boundingRect() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;

    QPointF getPoint() const;
    int getR() const;
    void setR(int newR);
    const QString &getText() const;
    void setText(const QString &newText);
    const QColor &getFrontColor() const;
    void setFrontColor(const QColor &newFrontColor);
    const QColor &getBackColor() const;
    void setBackColor(const QColor &newBackColor);

    int getRoundWidth() const;
    void setRoundWidth(int newRoundWidth);

private:
    QPointF point;          // 绘制的初始点
    int r;                  // 半径
    QString text;           // 点的文字
    QColor frontColor;      // 前景色Ⅰ
    QColor backColor;       // 背景色Ⅰ
    int roundWidth;         // 圆的宽Ⅰ
};

在这个类中,我自定义了一些属性,方便配置点的颜色,大小等等。

核心还是在于paint函数,其余都是辅助功能

下面是paint函数的实现:

void graphNode::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    QPen pen;
    pen.setWidth(roundWidth);

    painter->setRenderHint(QPainter::HighQualityAntialiasing);
    if (option->state & QStyle::State_Selected)
        pen.setColor(QColor((frontColor.red() + 125) % 255,
                            (frontColor.green() ) % 255,
                            (frontColor.blue() + 125) % 255)); // 选中时颜色变化
    else pen.setColor(frontColor);

    painter->setPen(pen);
    painter->drawEllipse(QRectF(point.x() - r, point.y() - r, r * 2, r * 2));


    QPainterPath path;
    path.addEllipse(QRectF(point.x() - r, point.y() - r, r * 2, r * 2));
    painter->fillPath(path, QBrush(backColor));


    painter->drawText(boundingRect(),
                      Qt::AlignHCenter |
                      Qt::AlignVCenter, text);

}

paint一共做了两件事情,第一件事情绘制一个圆,第二件事情就是绘制一个标识文字。

image.png 其中的A就是标识文字

如何绘制线

graphLine类设计如下

class graphLine : public QObject, public QGraphicsLineItem
{
    Q_OBJECT

public:
    enum LineType {
        LeftToRight, // ==>
        RightToLeft, // <==
        TwoWayArrow, // <=>
        NoArrow,     // <=>
    };
    explicit graphLine(graphNode *begin,
                       graphNode *end,
                       LineType type = NoArrow,
                       QObject *parent = nullptr);
private:
    graphNode *begin;
    graphNode *end;
    int length;
    QColor color;
    LineType lineType;
private:
    void paintArrow(graphNode* begin, graphNode* end, QPainter* painter);

public:
    QPainterPath shape() const override;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override;
    const QColor &getColor() const;
    void setColor(const QColor &newColor);
    LineType getLineType() const;
    void setLineType(LineType newType);
    graphNode *getBegin() const;
    graphNode *getEnd() const;
};

其中paintArrow用来绘制箭头

绘制线的线有两种,一种是不带箭头的,一种是带方向箭头的。

不带箭头

不带箭头的比较好绘制,计算一下起点和终点的坐标,画一条线就是。

    auto r_begin = begin->getPoint() + begin->pos();
    auto r_end = end->getPoint() + end->pos();
    QLineF lines(r_begin, r_end);
    setLine(lines);

    QPen pen;
    pen.setWidth(2);

    painter->setPen(pen);
    painter->setRenderHint(QPainter::HighQualityAntialiasing);
    painter->drawLine(line());

带箭头

绘制箭头可能需要一些计算,不过由于我们这里这个图形选择的圆,其实还是比较容易计算的。

如果是多边形,要麻烦一点。

在这里,我们想要的效果是箭头始终紧贴着其指向的圆。

比如这种效果:

GIF 2023-1-13 16-54-33.gif

我们知道 起点 a 和 终点 b的坐标,知道圆的半径,其实就很容易的推导出 圆和这条直线的交点是多少了。

大概是这样:

点 a 坐标为 ( x 1 , y 1 ) , 点 b 坐标为 ( x 2 , y 2 ) 现在, a b 的距离 = ( x 2 − x 1 ) 2 + ( y 2 − y 1 ) 2 直线 a b 的斜率为 k = ( y 2 − y 1 ) / ( x 2 − x 1 ) 现在点 c ( x , y ) 在 a b 上,若与 a 的距离为 c 的话。则有: { ( x − x 1 ) 2 + ( y − y 1 ) 2 = c 2 ( y − y 1 ) / ( x − x 1 ) = k 点 a 已知,距离 c 已知,斜率 k 已知 联立方程可以解得: { x = ± c 1 + k 2 + x 1 y = ± c k 1 + k 2 + y 1 点 a 坐标为 (x_1, y_1), 点b坐标为(x_2, y_2) \\ 现在,a b的距离 = (x_2 - x_1)^2 +(y_2 - y_1)^2 \\ 直线 ab的斜率为 k = (y_2-y_1) / (x_2-x_1) \\ 现在点c(x, y)在ab上,若与a的距离为c的话。则有:\\ \begin{cases} (x - x_1)^2 +(y - y_1)^2 = c^2 \\ (y-y_1) / (x-x_1) = k \end{cases} 点a已知,距离c已知,斜率k已知 \\ 联立方程可以解得:\\ \begin{cases} x = \pm \frac{c}{\sqrt{1 + k^2}} +x_1\\ y = \pm \frac{c k}{\sqrt{1 + k^2}} +y_1 \end{cases} a坐标为(x1,y1),b坐标为(x2,y2)现在,ab的距离=(x2x1)2+(y2y1)2直线ab的斜率为k=(y2y1)/(x2x1)现在点c(x,y)ab上,若与a的距离为c的话。则有:{(xx1)2+(yy1)2=c2(yy1)/(xx1)=ka已知,距离c已知,斜率k已知联立方程可以解得:{x=±1+k2 c+x1y=±1+k2 ck+y1

直线和圆相交的点圆两个,只有一个是合法的,这里只需要判断一下即可

bool __graphLine__containsLine(QPointF begin, QPointF end, QPointF now) {
    QLineF a(begin, end);
    QLineF b(begin, now);
    QLineF c(now, end);
    if (fabs(a.length() - b.length() - c.length()) < 1e-6) return true;
    return false;
}

计算出圆与直线的交点之后,绘制两根直线,分别向上和向下偏移30°来充当箭头即可。

void graphLine::paintArrow(graphNode* begin, graphNode* end, QPainter* painter)
{
    auto r_begin = begin->getPoint() + begin->pos();
    auto r_end = end->getPoint() + end->pos();
    QLineF lines(r_begin, r_end);
    auto length = end->getR() + end->getRoundWidth() / 2;
    // 宽度是内圈外圈各渲染一部分
    qreal dx, dy;
    if (fabs(lines.dx()) < 1e-6) {
        dx = 0;
        dy = length;
    } else {
        auto k = lines.dy() / lines.dx();
        qreal base = sqrt(k * k + 1);
        dx = length / base;
        dy = length * k / base;
    }

    QPointF dis(dx, dy);
    QPointF now;
    if (__graphLine__containsLine(r_begin, r_end, QPointF(r_end + dis))) {
        now = QPointF(r_end + dis);
    } else {
        now = QPointF(r_end - dis);
    }

    QLineF arrowHead(now, r_begin);
    arrowHead.setLength(10 + end->getRoundWidth());

    arrowHead.setAngle(arrowHead.angle() - 30); // 上方
    painter->drawLine(arrowHead);

    arrowHead.setAngle(arrowHead.angle() + 60); // 下方
    painter->drawLine(arrowHead);
}

知道如何绘制箭头之后,和绘制直线组合起来,就可以了;
paint完整代码

void graphLine::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    auto r_begin = begin->getPoint() + begin->pos();
    auto r_end = end->getPoint() + end->pos();
    QLineF lines(r_begin, r_end);
    setLine(lines);

    QPen pen;
    pen.setWidth(2);

    if (isSelected())
    {
        pen.setColor(QColor((color.red() + 125) % 255,
                            (color.green() ) % 255,
                            (color.blue() + 125) % 255));
    }
    else
    {
        pen.setColor(color);
    }

    painter->setPen(pen);
    painter->setRenderHint(QPainter::HighQualityAntialiasing);
    painter->drawLine(line());
    switch (lineType) {
    case LeftToRight: paintArrow(begin, end, painter); break;
    case RightToLeft: paintArrow(end, begin, painter); break;
    case TwoWayArrow: paintArrow(begin, end, painter);
                      paintArrow(end, begin, painter); break;
    case NoArrow: ;
    default:;
    }
}

在这里,添加点我选择使用右键单击添加,连接点是选择两个点就自动添加一根线

这些处理将直接在 view类里面进行处理,因此,我自定义了一个graph类

class graph : public QGraphicsView
{
    Q_OBJECT
public:
    enum SelectItemMode {
        Line,
        Node,

        None = 10086,
    };
    explicit graph(QWidget *parent = nullptr);
    QList<graphLine*> Lines();
    QList<graphNode*> Nodes();
    void setMode(SelectItemMode);
private:
    SelectItemMode selectItemMode;
    QSet<graphLine*> graphLines;
    QSet<graphNode*> graphNodes;
    QHash<graphNode*, QSet<graphNode*>> graphMap;
private:
    void mouseLButtonClick(QMouseEvent *event);
    void mouseRButtonClick(QMouseEvent *event);
protected:
    void mousePressEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
signals:
    void mouseClickEvent(QPoint point);
    void mouseMoveEvent(QPoint point);
    void selectItem(QGraphicsItem *);
    // QWidget interface
protected:
    void resizeEvent(QResizeEvent *event) override;
    void paintEvent(QPaintEvent *event) override;

private slots:
    void on_scene_select_change();
    void on_selection_change(QGraphicsItem *, QGraphicsItem *, Qt::FocusReason);
};

添加点

graph类重写 mousePressEvent 方法。

void graph::mousePressEvent(QMouseEvent *event)
{
    switch (event->button()) {
    case Qt::MouseButton::RightButton: mouseRButtonClick(event); break;
    default:
        QGraphicsView::mousePressEvent(event);
    }
}

然后在mouseRButtonClick中处理右键事件

void graph::mouseRButtonClick(QMouseEvent *event)
{
    auto pointScene = mapToScene(event->pos());
    auto item = new graphNode(pointScene, 20, QString("A")));
    item->setFlag(QGraphicsItem::ItemIsMovable, true);
    if (selectItemMode == Node) {
        item->setFlags( item->flags() |
                        QGraphicsItem::ItemIsFocusable |
                        QGraphicsItem::ItemIsSelectable);
    }

    scene()->addItem(item);
    graphNodes.insert(item);
}

添加线

添加线需要通过处理 selectionChanged

connect(scene(), SIGNAL(selectionChanged()), this, SLOT(on_scene_select_change()));

当选择的item为2时,则连接一条直线

void graph::on_scene_select_change()
{   // mode select graphNode
    auto list = scene()->selectedItems();

    if (selectItemMode == Node)
    {
        static decltype(list) old_list;
        if (list.size() > 2) {
            scene()->clearSelection();
            return;
        }
        if (list.size() == 2) {
            auto a{dynamic_cast<graphNode*>(list[0])},
            b{dynamic_cast<graphNode*>(list[1])};
            if (old_list[0] != list[0]) std::swap(a, b);
            if (graphMap[a].contains(b)) return; // 两点之间有线不需要连接Ⅰ
            graphMap[a].insert(b);
            graphMap[b].insert(a);
            auto now = new graphLine(a, b);

            if (selectItemMode == Line) {
                now->setFlags( now->flags() |
                               QGraphicsItem::ItemIsFocusable |
                               QGraphicsItem::ItemIsSelectable);
            }
            scene()->addItem(now);
            graphLines.insert(now);
        }
        old_list = list;
    }
    else if (selectItemMode == Line) {
        if (list.size() > 1) {
            scene()->clearSelection();
            return;
        }
    }
    auto item = scene()->mouseGrabberItem();
    emit selectItem(item);
}

到这里,基本上,核心的东西就完成了,剩下的是ui界面了。

我的ui界面比较丑,大概长这样:

image.png

image.png

这就是一个最基本的 图 编辑器了

{来自 amjieker }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/167082.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JS语言基础

目录 语法 关键字与保留字 变量 var关键字 let声明 暂时性死区 全局变量 for循环中的let声明 条件声明 const声明 语法 1. 区分大小写 无论是变量、函数名还是操作符&#xff0c;都区分大小写。 2. 标识符 所谓标识符&#xff0c;就是变量、函数、属性或函数参数的名…

centos7配置(nvidia+cuda+cudnn+anaconda+tensorflow)gpu开发环境

一、安装准备 1、查看nvidia显卡&#xff0c;我的是T4显卡 lspci | grep -i nvidia2、查看linux系统版本 uname -m && cat /etc/redhat-release3、安装依赖 yum install gcc kernel-devel kernel-headers二、安装nvidia驱动 1、禁用nouveau lsmod | grep nouveau…

Powershell渗透框架

文章目录Powershell基础Powershell简介什么是 Windows PowerShell为什么使用 Windows PowerShell如何启动 Windows PowerShellPowerShell和Cmd命令提示符的区别PowerShellcmd管理员运行 PowerShellWindows PowerShell ISE创建并运行脚本文本编辑器创建脚本集成脚本环境创建脚本…

第二章.线性回归以及非线性回归—特征缩放,交叉验证法,过拟合

第二章.线性回归以及非线性回归 2.9 特征缩放 1.数据归一化 1).作用&#xff1a; 把数据的取值范围处理为0-1或者-1-1 2).数据范围处理为0-1之间的方法&#xff1a; newValue(oldValue-min)/(max-min) 例如&#xff1a;数组:&#xff08;1,3,5&#xff09;,value1:(1-1)/(5-1)0…

MyBatis-Plus分析打印SQL(开发环境)

项目创建POM依赖 <!-- https://mvnrepository.com/artifact/p6spy/p6spy --> <dependency><groupId>p6spy</groupId><artifactId>p6spy</artifactId><version>3.9.1</version> </dependency> YML配置 spring:datasource…

silicon labs Gateway HOST-NCP MQTT网关搭建

一、背景 目前正在开发一款中控网关,网关mcu跑Android系统,NCP采用EFR32MG21开发板,需要跑MQTT协议控制zigbee的网络。基于以上需求,下载了simplicity studio V5版本和最新的EmberZNet 7.2.0.0协议栈进行验证,发现新的GSDK已经不再支持MQTT功能,官方回答是EmberZNet 6.7…

论文解读 - 城市自动驾驶车辆运动规划与控制技术综述 (第2部分)

文章目录&#x1f697; II. Overview of the decision-making hierarchy used in driverless cars&#xff08;无人驾驶汽车的决策层综述&#xff09;&#x1f534; A. Route Planning&#xff08;路径规划&#xff09;&#x1f7e0; B. Behavioral Decision Making&#xff08…

论文工具大全+软件简介

文章目录**1.使用说明用哪个文库就打开&#xff0c;****2.在软件中复制粘贴网址点下载**3.点已下载文件右击鼠标另外保存**腾讯微云-https://share.weiyun.com/5U3fAjF**1.安装并上传论文点检测2.检测等待时间3.打开检测报告查看回复[文献]&#xff1a;参考文献自动生成器参考文…

贪心算法专题

1.Acwing 1055. 股票买卖 II 题目链接&#xff1a;1055. 股票买卖 II - AcWing题库 思路&#xff1a;逢涨就买 #include<iostream> using namespace std;int main() {int n;long long ans0;int a[100005];cin>>n;cin>>a[0];for(int i1;i<n;i){cin>&…

C语言—动态内存管理

专栏&#xff1a;C语言 个人主页&#xff1a;HaiFan. 专栏简介&#xff1a;本专栏主要更新一些C语言的基础知识&#xff0c;也会实现一些小游戏和通讯录&#xff0c;学时管理系统之类的&#xff0c;有兴趣的朋友可以关注一下。 动态内存管理前言一、为什么会存在动态内存分配二…

磨金石教育分享||CG特效技术主要应用在哪几个领域

前面我们介绍了很多关于CG特效的知识&#xff0c;我们知道CG特效发展的历史以及重大意义。那么我们今天再来详细讨论一下CG特效主要应用的几个领域。近几年文化艺术的发展伴随着互联网信息技术高速传播。文化艺术产业变得多元&#xff0c;动漫、3A大作游戏、商业大片、虚拟现实…

Java 元注解

​ 元注解是负责对其它注解进行说明的注解&#xff0c;自定义注解时可以使用元注解。Java 5 定义了 4 个注解&#xff0c;分别是 Documented、Target、Retention 和 Inherited。Java 8 又增加了 Repeatable 和 Native 两个注解。这些注解都可以在 java.lang.annotation 包中找到…

前端压缩图片为指定宽高

压缩图片原理 通过原生的input标签拿到要上传的图片文件将图片文件转化成img元素标签在canvas上压缩绘制该HTMLImageElement将图片转化为一个包含图片展示的data URI&#xff0c;即图片被转换成base64编码的字符串 实现 通过原生的input标签拿到上传的图片文件 css部分<…

Linux 通过监控监控系统内存并定时重启指定服务

一、使用Free命令监控系统内存 1.1 查看系统内存情况 free -m1.1.1 获取空闲物理内存 echo Mem-free: `free -m | grep Mem | awk {print $4}`M1.1.2 获取缓冲区内存

php 安装curl扩展支持sftp协议

原因&#xff1a;php默认安装的依赖的libcurl.so中不支持sftp 协议 解决方法&#xff1a;先安装libssh2&#xff0c;curl安装时编译参数带–with-libssh2&#xff0c;这样可以支持sftp 一.编译安装libssh2-1.8.0 1.下载源码 地址: https://github.com/libssh2/libssh2 2.编译…

requests页面常用操作(post、put、get、head、patch、delete方法)

1 requests主要方法2 请求返回信息3 应用3.1 登录页面3.2 退出登录3.3 修改参数3.4 上传文件Requests 是一个 Python 的 HTTP 客户端库。每次调用 requests 请求之后&#xff0c;会返回一个 response 对象&#xff0c;该对象包含了具体的响应信息。可以通过requests模拟提交请求…

蓝桥杯 stm32 实现 ADC 采集数据功能 CubeMX

文章内的代码使用 HAL 库。 ADC 即 模数转换器&#xff0c;是指 将 连续变化的模拟信号 转换成 离散的数字信号 的器件。 文章目录前言一、ADC 原理图二、CubeMX 配置三、ADC 代码讲解总结前言 一、ADC 原理图 从原理图我们可以看到 STM32G431 内部集成 两个 最高位 12 位 的 A…

通信电子、嵌入式类面试题刷题计划03

文章目录021——"Hello, world!"022——计算圆的面积023——打印10x10的星号024——字符串打印025——打印26个英文字母026——strlen函数的用法027——sizeof函数的使用028——if else、变量赋值语句029——if else语句030——if elseif else语句&#xff0c;判断是…

数字IC设计、验证、FPGA笔试必会 - Verilog经典习题 ( 七)求两个数的差值

数字IC设计、验证、FPGA笔试必会 - Verilog经典习题 &#xff08;七&#xff09;求两个数的差值 &#x1f508;声明&#xff1a; &#x1f603;博主主页&#xff1a;王_嘻嘻的CSDN博客 &#x1f9e8;未经作者允许&#xff0c;禁止转载 &#x1f511;系列专栏&#xff1a;牛客Ve…

Redis作为缓存应用场景分析

为什么使用缓存 Redis是一个内存型数据库&#xff0c;也就是说&#xff0c;所有的数据都会存在与内存中&#xff0c;基于Redis的高性能特性&#xff0c;我们将Redis用在缓存场景非常广泛。使用起来方便&#xff0c;响应也是远超关系型数据库。 应用场景 Redis的应用场景非常…