【QT进阶】第十四章 自定义QGraphicsItem的实现设备节点

时间:2024-10-30 08:31:02

❤️作者主页:凉开水白菜
❤️作者简介:共同学习,互相监督,热于分享,多加讨论,一起进步!
❤️专栏目录:【零基础学QT】文章导航篇
❤️专栏资料:/s/192A28BTIYFHmixRcQwmaHw 提取码:qtqt
❤️点赞 ???? 收藏 ⭐再看,养成习惯

订阅的粉丝可通过PC端文末加我微信,可对文章的内容进行一对一答疑!


文章目录

  • 一、简介
  • 二、view层
  • 三、自定义QGraphicsItem
    • 3.1 继承QGraphicsItem类
    • 3.2 设置尺寸
    • 3.3 绘制图形
    • 3.4 设置鼠标状态
    • 3.5 数据更新和获取
  • 四、节点管理与工程管理
    • 4.1 添加设备
    • 4.2 遍历设备
    • 4.3 删除设备
    • 4.4 更新设备状态
    • 4.5 保存工程和加载工程
  • 五、最后


一、简介

在第六章讲解了GraphicsView的使用,在该章节中主要是使用现有的画框写字,在实际使用中不会是简单的绘画一个框或则绘画一个文字,本章节引入一个自定义QGraphicsItem的用法实现设备节点,在这个章节会使用到QPainter和GraphicsView的内容建议配合使用。

二、view层

这一章节的实现就直接沿用第六章的内容来实现

#include ""

DeviceView::DeviceView(QWidget *parent)
{

}

void DeviceView::mouseMoveEvent(QMouseEvent *event)
{
    //鼠标移动事件
    QPoint point=event->pos();          //QGraphicsView的坐标
    emit mouseMovePoint(point);         //释放信号
    QGraphicsView::mouseMoveEvent(event);
}

void DeviceView::mousePressEvent(QMouseEvent *event)
{
    //鼠标左键按下事件
    if (event->button()==Qt::LeftButton)
    {
        QPoint point=event->pos();      //QGraphicsView的坐标
        emit mouseClicked(point);       //释放信号
    }
    QGraphicsView::mousePressEvent(event);
}

void DeviceView::mouseDoubleClickEvent(QMouseEvent *event)
{
    //鼠标双击事件
    if (event->button()==Qt::LeftButton)
    {
        QPoint point=event->pos();      //QGraphicsView的坐标
        emit mouseDoubleClick(point);   //释放信号
    }
    QGraphicsView::mouseDoubleClickEvent(event);
}

void DeviceView::keyPressEvent(QKeyEvent *event)
{
    //按键事件
    emit keyPress(event);
    QGraphicsView::keyPressEvent(event);
}

void DeviceView::wheelEvent(QWheelEvent *event)
{
    if(event->delta() > 0){
            emit wheel("add");// 发送向上滚动信号
    }else{
            emit wheel("sub");// 发送向下滚动信号
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

#ifndef DEVICEVIEW_H
#define DEVICEVIEW_H

#include <QObject>
#include <QGraphicsView>
#include <QMouseEvent>
#include <QPoint>

class DeviceView : public QGraphicsView
{
    Q_OBJECT
public:
    explicit DeviceView(QWidget *parent = nullptr);

protected:
    void mouseMoveEvent(QMouseEvent *event);
    void mousePressEvent(QMouseEvent *event);
    void mouseDoubleClickEvent(QMouseEvent *event);
    void keyPressEvent(QKeyEvent *event);
    void wheelEvent(QWheelEvent *event);

signals:
    void mouseMovePoint(QPoint point); //鼠标移动
    void mouseClicked(QPoint point); //鼠标单击
    void mouseDoubleClick(QPoint point); //双击事件
    void keyPress(QKeyEvent *event); //按键事件
    void wheel(QString dat); // 鼠标滚轮事件

public slots:
};


#endif // DEVICEVIEW_H

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

在本文中不会实现的非常完整,所以很多信号我们这里是用不上的,有学习心态的同学可以根据预留的这些信号对这个自定义设备进行完善。

三、自定义QGraphicsItem

要继承这个类只需要重写boundingRect和paint两个函数就可以实现,boundingRect负责设置图形项的边界矩形,确保图形项可以被绘制在合适的区域,paint也就是我们的绘制函数,例如数据更新和样式显示都在这里面绘制,本章是我们要实现的一个效果;
在这里插入图片描述

3.1 继承QGraphicsItem类

class DeviceNode :  public QGraphicsItem
{
public:
    explicit DeviceNode(QGraphicsItem *parent = nullptr);
    virtual ~DeviceNode(){}
protected:
    QRectF boundingRect() const override;
    void paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget * widget) override;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

3.2 设置尺寸

既然要画一个图形那我们首先就需要尺寸,所以我们还需要重写一个setrect的函数,来保存图形尺寸

QRectF DeviceNode::rect() const
{
    return m_rect;
}

void DeviceNode::setRect(const QRectF &rect)
{
    if (m_rect == rect)
            return;

    prepareGeometryChange(); //正如 setRect(),无论以任何方式更改 item 的几何形状,必须首先调用prepareGeometryChange(),以保证 QGraphicsScene 中的索引是最新的。
    m_rect = rect;

    update(); //调用paintEvent重绘
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

有了尺寸后我们就可以实现限制尺寸边界

QRectF DeviceNode::boundingRect() const
{
    // 自定义图元边界,计算图元轮廓的垂直边界最小矩形
    // 设置图形项的边界矩形,确保图形项可以被绘制在合适的区域
    qreal adjust = 0.5; // 返回上下左右+0.5个像素
    return QRectF((m_rect.left()) - adjust, (m_rect.top()) - adjust,
                  m_rect.width() + adjust, m_rect.height() + adjust);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

3.3 绘制图形

既然我们有了尺寸那我们只需要根据坐标绘制我们的图形了,这里首先首先绘制圆角矩形,分为两种情况选中设备和未选中状态,这个可以根据QStyleOptionGraphicsItem *option来进行判断;

// 绘制矩形
    if (option->state & QStyle::State_Selected) {
        // 绘制选中样式
        painter->setBrush(QColor(80, 80, 80));
        painter->setPen(QPen(QColor(150, 100, 100)));
    }else
    {
        // 绘制未选中样式
        painter->setBrush(QColor(60, 60, 60));
        painter->setPen(QPen(QColor(255, 255, 255)));
    }
    painter->drawRoundRect(m_rect, m_rect.height() / 10, m_rect.width() / 10); // 圆角参数根据长宽计算
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

然后我们需要根据显示要求我们需要绘制我们的文字

painter->drawText(QPointF(20, m_rect.height() / 3), "设备号: " + QString::number(this->m_id));
painter->drawText(QPointF(20, m_rect.height() / 3 * 2), "温度: " + QString::number(this->m_temp) + "℃");
painter->drawText(QPointF(20, m_rect.height()), "湿度: " + QString::number(this->m_humi) + "%");
  • 1
  • 2
  • 3

完整代码如下

void DeviceNode::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
    Q_UNUSED(widget);
    QPen pen;
    QFont font("Arial", 28); // 设置字体为 Arial,大小为 20
    painter->setRenderHint(QPainter::Antialiasing, true);

    // 绘制矩形
    if (option->state & QStyle::State_Selected) {
        // 绘制选中样式
        painter->setBrush(QColor(80, 80, 80));
        painter->setPen(QPen(QColor(150, 100, 100)));
    }else
    {
        // 绘制未选中样式
        painter->setBrush(QColor(60, 60, 60));
        painter->setPen(QPen(QColor(255, 255, 255)));
    }
    painter->drawRoundRect(m_rect, m_rect.height() / 10, m_rect.width() / 10); // 圆角参数根据长宽计算

    // 绘制文字
    font.setPointSize(15);
    painter->setFont(font);
    painter->drawText(QPointF(20, m_rect.height() / 3), "设备号: " + QString::number(this->m_id));
    painter->drawText(QPointF(20, m_rect.height() / 3 * 2), "温度: " + QString::number(this->m_temp) + "℃");
    painter->drawText(QPointF(20, m_rect.height()), "湿度: " + QString::number(this->m_humi) + "%");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

3.4 设置鼠标状态

这里继承三个事件分别是鼠标按下事件将鼠标设置手掌形态,拖动是将鼠标设置为抓握手掌,松开鼠标后鼠标设置为原来样式,到这里样式方面基本就完成了,但我们还需要实现一个数据接口,供外部更新我们的数据;

void DeviceNode::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    setFocus();
    this->setCursor(Qt::OpenHandCursor);
    QGraphicsItem::mousePressEvent(event);
}

void DeviceNode::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    this->setCursor(Qt::ClosedHandCursor);
    QGraphicsItem::mouseMoveEvent(event);
}

void DeviceNode::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    this->setCursor(Qt::CrossCursor);
    QGraphicsItem::mouseReleaseEvent(event);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

3.5 数据更新和获取

在前面的绘制事件部分我们就可以看到使用了几个变量来绘制我们的数据,所以我们这里的设置数据和获取数据直接对这个变量进行赋值和返回即可实现我们的画面更新,这里需要注意的一个点,在我们界面更新的时候需要先调用prepareGeometryChange让画面做好准备再调用updata;

void DeviceNode::setData(int key, const QVariant &value)
{
    if((key < deviceBegin)&&(key > deviceEnd))
    {
        // 没有这个key直接退出
        return;
    }
    switch(key)
    {
    case deviceId:
        this->m_id = value.toUInt();
        break;
    case deviceTemp:
        this->m_temp = value.toDouble();
        break;
    case deviceHumi:
        this->m_humi = value.toUInt();
        break;
    default:break;
    }


    prepareGeometryChange();
    update();
}

QVariant DeviceNode::data(int key) const
{
    if((key < deviceBegin)&&(key > deviceEnd))
    {
        // 没有这个key直接退出
        return QVariant(0);
    }

    switch(key)
    {
    case deviceId:
        return this->m_id;
        break;
    case deviceTemp:
        return this->m_temp;
        break;
    case deviceHumi:
        return this->m_humi;
        break;
    default:break;
    }
    return QVariant(0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

到这里我们的自定义设备就简单完成了,其实这个画面还是比较丑的,但配合painter一章节我们可以通过自己的想法去画自己项目上需要的图像都可以实现了;

四、节点管理与工程管理

4.1 添加设备

添加新的设备比较简单和之前新建一个QT自带的item类型是相同的

// 添加新的设备
bool DeviceManage::addDevice(uint id)
{
    DeviceNode   *tmp;
    tmp = getDeviceFromID(id);
    if(tmp != NULL)
    {
        QMessageBox::warning(this, tr("Error"), "已存在设备");
        return false; // 已有该id的设备
    }
    DeviceNode   *item = new DeviceNode(id, QRectF(10,10,200,100));
    item->setFlags(QGraphicsItem::ItemIsMovable
                   | QGraphicsItem::ItemIsSelectable
                   | QGraphicsItem::ItemIsFocusable);
    item->setPos(-50+(qrand() % 100),-50+(qrand() % 100));

    scene->addItem(item);
    scene->clearSelection();

    return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

4.2 遍历设备

这里通过sence的items直接一个个遍历去对比id就可以查询全部的设备了

// 根据id查找设备item
DeviceNode *DeviceManage::getDeviceFromID(uint id)
{
    DeviceNode *tmp;
    for (int i = 0;i < scene->items().count(); i++)
    {
        tmp = static_cast<DeviceNode*>(scene->items().at(i));
        if(tmp->data(deviceId).toUInt() == (uint)id)
        {
            return tmp;
        }
    }
    return NULL;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

4.3 删除设备

删除设备直接使用scene的移除方法就可以实现,再加上前面的根据id获取到这个item进行删除

// 删除设备
bool DeviceManage::deleteDevice(uint id)
{
    DeviceNode   *tmp;
    tmp = getDeviceFromID(id);
    if(tmp == NULL)
    {
        QMessageBox::warning(this, tr("Error"), "设备不存在");
        return false; // 已有该id的设备
    }
    scene->removeItem(tmp);
    return true;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

4.4 更新设备状态

前面我们继承了setData方法,我们这里根据查询ID直接调用这个方法就可以实现item的界面数据更新

// 更新item数据显示
void DeviceManage::updata(deviceData_t data)
{
    DeviceNode *node = getDeviceFromID(data.id);
    if(node != NULL)
    {
        node->setData(deviceHumi, data.humi);
        node->setData(deviceTemp, data.temp);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

4.5 保存工程和加载工程

这里就不完整实现工程参数的保存就做临时性演示效果,如果数据量比较大可以使用xml,数据量小的话采用ini

void DeviceManage::saveDeviceList(QString path)
{
    DeviceNode *tmp;
    deviceMap map;

    device_map.clear();
    for (int i = 0;i < scene->items().count(); i++)
    {
        tmp = static_cast<DeviceNode*>(scene->items().at(i));
        // 保存一个设备只需要id号和设备放置的位置即可
        map.id =tmp->data(deviceId).toUInt();
        map.pos = tmp->pos();
        device_map.append(map);
    }
}

void DeviceManage::loadDeviceList(QString path)
{
    scene->clear(); // 删除所有的item

    foreach (deviceMap map, device_map) {
        DeviceNode   *item = new DeviceNode(map.id, QRectF(10,10,200,100));
        item->setFlags(QGraphicsItem::ItemIsMovable
                       | QGraphicsItem::ItemIsSelectable
                       | QGraphicsItem::ItemIsFocusable);
        item->setPos(map.pos);

        scene->addItem(item);
        scene->clearSelection();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

widget上使用

    QGridLayout *layout = new QGridLayout();
    layout->setMargin(0);
    device_manage = new DeviceManage();
    layout->addWidget(device_manage);
    ui->widget->setLayout(layout);

    device_manage->addDevice(1);
    device_manage->addDevice(2);
    device_manage->addDevice(3);

    device_manage->deleteDevice(2);

    QTimer *timer = new QTimer;
    connect(timer, &QTimer::timeout, this, [=](){
        deviceData_t dat;
        dat.humi = qrand() % 100;
        dat.temp = qrand() % 24 + 10 + ((qrand() % 10)/);
        dat.id = 1;
        device_manage->updata(dat);

        dat.humi = qrand() % 100;
        dat.temp = qrand() % 24 + 10 + ((qrand() % 10)/);
        dat.id = 2;
        device_manage->updata(dat);

        dat.humi = qrand() % 100;
        dat.temp = qrand() % 24 + 10 + ((qrand() % 10)/);
        dat.id = 3;
        device_manage->updata(dat);
    });
    timer->start(1000);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

最终效果如下
在这里插入图片描述

五、最后

完整的代码工程我都放在百度云盘的软件里面,如果需要可以自行下载;

❤️专栏资料:/s/192A28BTIYFHmixRcQwmaHw 提取码:qtqt


我是凉开水白菜,我们下文见~