2. 组态画布与图元基类设计

任务目标

在Qt工业组态中,不同设备图元(电机、阀门、传感器等)需要共享缩放、选中、拖拽、属性编辑等交互能力。本案例设计一个CustomGraphicsItem基类,继承自QGraphicsObject,封装通用交互逻辑,并实现一个派生图元验证基类可用性。

核心设计

  • 缩放锚点:以图元中心为基准进行缩放,保证视觉位置稳定
  • 选中态绘制:选中时绘制虚线边框,未选中时仅绘制图元本体
  • 属性接口:提供itemId与itemName两个属性,供外部组态配置使用
  • 信号通知:位置、缩放变化时发出geometryChanged信号,便于属性面板同步

关键完整代码

// customgraphicsitem.h
#pragma once
#include <QGraphicsObject>
#include <QPen>

class CustomGraphicsItem : public QGraphicsObject
{
    Q_OBJECT
    Q_PROPERTY(QString itemId READ itemId WRITE setItemId)
    Q_PROPERTY(QString itemName READ itemName WRITE setItemName)

public:
    explicit CustomGraphicsItem(QGraphicsItem *parent = nullptr);

    // 子类必须实现
    QRectF boundingRect() const override = 0;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
               QWidget *widget) override = 0;

    QString itemId() const { return m_id; }
    void setItemId(const QString &id) { m_id = id; }

    QString itemName() const { return m_name; }
    void setItemName(const QString &name) { m_name = name; }

    // 以中心为锚点缩放
    void scaleBy(qreal factor);

signals:
    void geometryChanged();

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
    void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override;
    void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override;
    QVariant itemChange(GraphicsItemChange change, const QVariant &value) override;

    void drawSelectionFrame(QPainter *painter);

private:
    QString m_id;
    QString m_name;
    bool m_dragging = false;
    bool m_hovered = false;
    QPointF m_dragOffset;
};

// customgraphicsitem.cpp
#include "customgraphicsitem.h"
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsSceneHoverEvent>
#include <QPainter>
#include <QStyleOptionGraphicsItem>

CustomGraphicsItem::CustomGraphicsItem(QGraphicsItem *parent)
    : QGraphicsObject(parent)
{
    setFlags(QGraphicsItem::ItemIsSelectable |
             QGraphicsItem::ItemIsMovable |
             QGraphicsItem::ItemSendsGeometryChanges);
    setAcceptHoverEvents(true);
}

void CustomGraphicsItem::scaleBy(qreal factor)
{
    prepareGeometryChange();
    QTransform t;
    t.translate(boundingRect().center().x(), boundingRect().center().y());
    t.scale(factor, factor);
    t.translate(-boundingRect().center().x(), -boundingRect().center().y());
    setTransform(t, true);
    emit geometryChanged();
}

void CustomGraphicsItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        m_dragging = true;
        m_dragOffset = event->scenePos() - pos();
    }
    QGraphicsObject::mousePressEvent(event);
}

void CustomGraphicsItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if (m_dragging) {
        setPos(event->scenePos() - m_dragOffset);
        emit geometryChanged();
    }
    QGraphicsObject::mouseMoveEvent(event);
}

void CustomGraphicsItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    m_dragging = false;
    QGraphicsObject::mouseReleaseEvent(event);
}

void CustomGraphicsItem::hoverEnterEvent(QGraphicsSceneHoverEvent *event)
{
    m_hovered = true;
    update();
    QGraphicsObject::hoverEnterEvent(event);
}

void CustomGraphicsItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *event)
{
    m_hovered = false;
    update();
    QGraphicsObject::hoverLeaveEvent(event);
}

QVariant CustomGraphicsItem::itemChange(GraphicsItemChange change, const QVariant &value)
{
    if (change == ItemPositionHasChanged || change == ItemTransformHasChanged)
        emit geometryChanged();
    return QGraphicsObject::itemChange(change, value);
}

void CustomGraphicsItem::drawSelectionFrame(QPainter *painter)
{
    if (!isSelected() && !m_hovered)
        return;
    QPen pen(isSelected() ? Qt::blue : Qt::gray);
    pen.setStyle(Qt::DashLine);
    pen.setWidth(1);
    painter->setPen(pen);
    painter->setBrush(Qt::NoBrush);
    painter->drawRect(boundingRect().adjusted(1, 1, -1, -1));
}

// 派生示例:矩形设备图元
// motoritem.h
#pragma once
#include "customgraphicsitem.h"

class MotorItem : public CustomGraphicsItem
{
public:
    MotorItem() { setItemName("Motor"); }
    QRectF boundingRect() const override { return QRectF(-30, -20, 60, 40); }
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
               QWidget *widget) override;
};

// motoritem.cpp
#include "motoritem.h"
#include <QPainter>

void MotorItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
                      QWidget *widget)
{
    Q_UNUSED(option); Q_UNUSED(widget);
    painter->setRenderHint(QPainter::Antialiasing);
    painter->setBrush(QColor(100, 180, 220));
    painter->setPen(QPen(Qt::black, 1.5));
    painter->drawRoundedRect(boundingRect(), 5, 5);
    painter->drawText(boundingRect(), Qt::AlignCenter, itemName());
    drawSelectionFrame(painter);
}

// main.cpp 最小验证入口
#include <QApplication>
#include <QGraphicsView>
#include <QGraphicsScene>
#include "motoritem.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QGraphicsScene scene;
    MotorItem *motor = new MotorItem();
    motor->setPos(50, 50);
    scene.addItem(motor);

    QGraphicsView view(&scene);
    view.setRenderHint(QPainter::Antialiasing);
    view.resize(400, 300);
    view.show();
    return app.exec();
}

工程配置

在.pro文件中添加:

QT += widgets
SOURCES += main.cpp motoritem.cpp customgraphicsitem.cpp
HEADERS += motoritem.h customgraphicsitem.h

运行与验证

  1. 使用Qt Creator打开项目,构建并运行
  2. 窗口中出现一个蓝色圆角矩形图元,中心文字显示"Motor"
  3. 拖拽验证:按住左键拖动图元,图元跟随移动,属性面板(若接入)可收到geometryChanged信号
  4. 选中验证:单击图元,出现蓝色虚线边框;点击空白处取消选中,虚线消失
  5. 缩放验证:在代码中调用motor->scaleBy(1.5)后重新运行,图元以自身中心放大1.5倍,且中心点位置不变
  6. 属性验证:在main()中添加motor->setItemId("MTR-001"),通过qDebug() << motor->itemId()输出确认属性读写正常

通过条件:上述交互行为全部符合预期,且派生类只需实现boundingRect()和paint()即可获得完整的选中、拖拽、缩放与信号通知能力。