From e3a4c9be7d438943962d40822f986501236b21f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E6=98=A5=E9=98=B3?= <2715725160@qq.com> Date: Thu, 7 Aug 2025 14:05:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0HMI=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E8=83=BD=E5=A4=9F=E6=8B=96=E5=8A=A8=E6=8C=89=E9=92=AE=E5=92=8C?= =?UTF-8?q?=E6=8C=87=E7=A4=BA=E7=81=AF=EF=BC=8C=E5=B9=B6=E8=83=BD=E5=A4=9F?= =?UTF-8?q?=E5=A4=8D=E5=88=B6=E7=B2=98=E8=B4=B4=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- customgraphics.cpp | 117 ++++++++++++++++++++++++++++++ customgraphics.h | 72 +++++++++++++++++++ customgraphicsscene.cpp | 148 ++++++++++++++++++++++++++++++++++++++ customgraphicsscene.h | 46 ++++++++++++ hmimodule.cpp | 154 ++++++++++++++++++++++++++++++++++++++++ hmimodule.h | 49 +++++++++++++ mainwindow.cpp | 36 ++++++++++ mainwindow.h | 32 +++++++++ 8 files changed, 654 insertions(+) create mode 100644 customgraphics.cpp create mode 100644 customgraphics.h create mode 100644 customgraphicsscene.cpp create mode 100644 customgraphicsscene.h create mode 100644 hmimodule.cpp create mode 100644 hmimodule.h create mode 100644 mainwindow.cpp create mode 100644 mainwindow.h diff --git a/customgraphics.cpp b/customgraphics.cpp new file mode 100644 index 0000000..1d49125 --- /dev/null +++ b/customgraphics.cpp @@ -0,0 +1,117 @@ +#include "customgraphics.h" +#include +#include +#include +#include + +// HmiComponent 基类实现 + +HmiComponent::HmiComponent(QGraphicsItem *parent) : QGraphicsObject(parent) +{ + setFlag(QGraphicsItem::ItemIsMovable, true); + setFlag(QGraphicsItem::ItemIsSelectable, true); + setAcceptHoverEvents(true); +} + +void HmiComponent::setColor(const QColor& color) { + if (m_color != color) { + m_color = color; + update(); + } +} + +QColor HmiComponent::color() const { + return m_color; +} + +void HmiComponent::setComponentName(const QString& name) { + m_componentName = name; +} + +QString HmiComponent::componentName() const { + return m_componentName; +} + +void HmiComponent::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) +{ + Q_UNUSED(option); + Q_UNUSED(widget); + painter->setRenderHint(QPainter::Antialiasing); + + paintShape(painter); + + if (isSelected()) { + painter->setBrush(Qt::NoBrush); + painter->setPen(QPen(Qt::darkRed, 2, Qt::DashLine)); + painter->drawRect(boundingRect()); + } +} + +void HmiComponent::mousePressEvent(QGraphicsSceneMouseEvent *event) +{ + // 右键不触发选中信号,留给 contextMenuEvent 处理 + if (event->button() == Qt::LeftButton) { + emit selected(this); + } + QGraphicsObject::mousePressEvent(event); +} + +// 实现 contextMenuEvent +void HmiComponent::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) +{ + // 右键点击时,先清空场景中的其他选中项,然后选中当前项 + scene()->clearSelection(); + setSelected(true); + emit selected(this); // 同样可以发出选中信号,以便更新属性面板等 + + QMenu menu; + QAction *copyAction = menu.addAction("复制"); + QAction *deleteAction = menu.addAction("删除"); + + // 使用 connect 将菜单动作的触发连接到信号的发射,“复制”和“删除” + connect(copyAction, &QAction::triggered, this, [this]() { + emit copyRequested(this); + }); + connect(deleteAction, &QAction::triggered, this, [this]() { + emit deleteRequested(this); + }); + + // 在鼠标光标位置显示菜单 + menu.exec(event->screenPos()); +} + + +// HmiButton 类实现 + +HmiButton::HmiButton(QGraphicsItem *parent) : HmiComponent(parent) { + m_color = Qt::gray; + m_componentName = "Button"; +} + +QRectF HmiButton::boundingRect() const { + return QRectF(0, 0, 80, 30); +} + +void HmiButton::paintShape(QPainter *painter) { + painter->setPen(Qt::NoPen); + painter->setBrush(m_color); + painter->drawRect(boundingRect()); +} + + +// HmiIndicator 类实现 + +HmiIndicator::HmiIndicator(QGraphicsItem *parent) : HmiComponent(parent) { + m_color = Qt::green; + m_componentName = "Indicator"; +} + +QRectF HmiIndicator::boundingRect() const { + return QRectF(0, 0, 30, 30); +} + +void HmiIndicator::paintShape(QPainter *painter) { + painter->setPen(Qt::NoPen); + painter->setBrush(m_color); + painter->drawEllipse(boundingRect()); +} diff --git a/customgraphics.h b/customgraphics.h new file mode 100644 index 0000000..febc723 --- /dev/null +++ b/customgraphics.h @@ -0,0 +1,72 @@ +#ifndef CUSTOMGRAPHICS_H +#define CUSTOMGRAPHICS_H + +#include +// 只需要前向声明,因为头文件中只用到了指针 +class QPainter; +class QGraphicsSceneMouseEvent; +class QGraphicsSceneContextMenuEvent; + +enum class ComponentType { + Button, + Indicator +}; + +// HMI组件基类 +class HmiComponent : public QGraphicsObject +{ + Q_OBJECT + Q_PROPERTY(QColor color READ color WRITE setColor) + Q_PROPERTY(QString componentName READ componentName WRITE setComponentName) + +signals: + void selected(HmiComponent* item); + // 为复制和删除请求添加信号 + void copyRequested(HmiComponent* item); + void deleteRequested(HmiComponent* item); + +public: + explicit HmiComponent(QGraphicsItem *parent = nullptr); + + void setColor(const QColor& color); + QColor color() const; + void setComponentName(const QString& name); + QString componentName() const; + + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + +protected: + void mousePressEvent(QGraphicsSceneMouseEvent *event) override; + // 重写上下文菜单事件 + void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; + virtual void paintShape(QPainter* painter) = 0; + +protected: + QColor m_color; + QString m_componentName; +}; + + +// 按钮类 +class HmiButton : public HmiComponent +{ +public: + HmiButton(QGraphicsItem *parent = nullptr); + QRectF boundingRect() const override; + +protected: + void paintShape(QPainter *painter) override; +}; + +// 指示灯类 +class HmiIndicator : public HmiComponent +{ +public: + HmiIndicator(QGraphicsItem *parent = nullptr); + QRectF boundingRect() const override; + +protected: + void paintShape(QPainter *painter) override; +}; + +#endif // CUSTOMGRAPHICS_H diff --git a/customgraphicsscene.cpp b/customgraphicsscene.cpp new file mode 100644 index 0000000..2ee0101 --- /dev/null +++ b/customgraphicsscene.cpp @@ -0,0 +1,148 @@ +#include "customgraphicsscene.h" +#include +#include +#include +#include +#include + +// 实现新的构造函数 +CustomGraphicsScene::CustomGraphicsScene(QGraphicsView* view, QObject *parent) + : QGraphicsScene(parent), m_mode(Normal), m_view(view) +{ + // 断言确保传入的 view 是有效的 + Q_ASSERT(m_view); +} + +void CustomGraphicsScene::setMode(Mode mode) +{ + if (m_mode == mode) return; + + m_mode = mode; + + // 根据模式设置光标 + if (m_mode == CreateItem) { + m_view->setCursor(Qt::CrossCursor); + } else { + m_view->setCursor(Qt::ArrowCursor); // 恢复为标准箭头光标 + } +} + +// setComponentTypeToCreate 函数无需改动 +void CustomGraphicsScene::setComponentTypeToCreate(ComponentType type) +{ + m_componentTypeToCreate = type; +} + +// mousePressEvent 函数无需改动 +void CustomGraphicsScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) +{ + if (mouseEvent->button() != Qt::LeftButton) { + QGraphicsScene::mousePressEvent(mouseEvent); + return; + } + + if (m_mode == CreateItem) { + HmiComponent* item = nullptr; + + switch (m_componentTypeToCreate) { + case ComponentType::Button: + item = new HmiButton(); + break; + case ComponentType::Indicator: + item = new HmiIndicator(); + break; + } + + if (item) + { + item->setPos(mouseEvent->scenePos()); + if (item->scene() == nullptr) + { + addItem(item); + emit componentCreated(item); + } + else + { + // 如果 item 已经被添加到场景中,则删除它,避免内存泄漏 + delete item; + } + } + + setMode(Normal); + } else { + QGraphicsScene::mousePressEvent(mouseEvent); + } +} + +// mouseMoveEvent 函数无需改动 +void CustomGraphicsScene::mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) +{ + QGraphicsScene::mouseMoveEvent(mouseEvent); +} + + +// 修改或新增 keyPressEvent +void CustomGraphicsScene::keyPressEvent(QKeyEvent *keyEvent) +{ + // 退出创建模式 + if (keyEvent->key() == Qt::Key_Escape && m_mode == CreateItem) { + setMode(Normal); + keyEvent->accept(); + return; + } + + // 处理删除、复制、粘贴 + if (selectedItems().isEmpty()) { + QGraphicsScene::keyPressEvent(keyEvent); + return; + } + + switch (keyEvent->key()) { + case Qt::Key_Delete: + emit deleteRequestFromScene(); + keyEvent->accept(); + break; + case Qt::Key_C: + if (keyEvent->modifiers() & Qt::ControlModifier) { + emit copyRequestFromScene(); + keyEvent->accept(); + } + break; + case Qt::Key_V: + if (keyEvent->modifiers() & Qt::ControlModifier) { + // 获取最后一个鼠标事件的位置来确定粘贴位置 + emit pasteRequestFromScene(views().first()->mapToScene(views().first()->mapFromGlobal(QCursor::pos()))); + keyEvent->accept(); + } + break; + default: + break; + } + + QGraphicsScene::keyPressEvent(keyEvent); +} + +void CustomGraphicsScene::contextMenuEvent(QGraphicsSceneContextMenuEvent *contextMenuEvent) +{ + // 检查鼠标下方是否有图形项 + QGraphicsItem *item = itemAt(contextMenuEvent->scenePos(), views().first()->transform()); + + if (!item) { + // 如果点击在空白处,则显示我们的粘贴菜单 + contextMenuEvent->accept(); + + QMenu menu; + QAction *pasteAction = menu.addAction("粘贴"); + + // 将菜单动作连接到场景的粘贴请求信号 + connect(pasteAction, &QAction::triggered, this, [this, contextMenuEvent]() { + emit pasteRequestFromScene(contextMenuEvent->scenePos()); + }); + + menu.exec(contextMenuEvent->screenPos()); + } else { + // 如果点击在图形项上,则将事件传递给该图形项处理 + // (这会调用 HmiComponent::contextMenuEvent) + QGraphicsScene::contextMenuEvent(contextMenuEvent); + } +} diff --git a/customgraphicsscene.h b/customgraphicsscene.h new file mode 100644 index 0000000..b394f11 --- /dev/null +++ b/customgraphicsscene.h @@ -0,0 +1,46 @@ +#ifndef CUSTOMGRAPHICSSCENE_H +#define CUSTOMGRAPHICSSCENE_H + +#include +#include "customgraphics.h" + +class QGraphicsSceneMouseEvent; +class QGraphicsSceneContextMenuEvent; +class QGraphicsView; +class QKeyEvent; + +class CustomGraphicsScene : public QGraphicsScene +{ + Q_OBJECT + +public: + enum Mode { Normal, CreateItem }; + + explicit CustomGraphicsScene(QGraphicsView* view, QObject *parent = nullptr); + + void setMode(Mode mode); + void setComponentTypeToCreate(ComponentType type); + +signals: + void componentCreated(HmiComponent* item); + // 转发键盘操作请求 + void copyRequestFromScene(); + void pasteRequestFromScene(const QPointF& scenePos); // 传递粘贴位置 + void deleteRequestFromScene(); + +protected: + void mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) override; + + // 在这里添加下面这行代码 + void mouseMoveEvent(QGraphicsSceneMouseEvent *mouseEvent) override; + + void keyPressEvent(QKeyEvent *keyEvent) override; + void contextMenuEvent(QGraphicsSceneContextMenuEvent *contextMenuEvent) override; + +private: + Mode m_mode; + ComponentType m_componentTypeToCreate; + QGraphicsView* m_view; +}; + +#endif // CUSTOMGRAPHICSSCENE_H diff --git a/hmimodule.cpp b/hmimodule.cpp new file mode 100644 index 0000000..ed8b026 --- /dev/null +++ b/hmimodule.cpp @@ -0,0 +1,154 @@ +#include "hmimodule.h" +#include +#include // --- 确保包含 QGraphicsView 头文件 --- +#include + +// HMIModule 构造函数无需改动 +HMIModule::HMIModule(Ui::MainWindow* ui, QObject *parent) + : QObject{parent}, ui_(ui), m_scene(nullptr) +{} + +// 封装设置图标和大小的通用函数 +void HMIModule::setButtonIcon(QAbstractButton *button, const QString &iconPath) +{ + button->setIcon(QIcon(iconPath)); + button->setIconSize(QSize(30, 30)); +} + +void HMIModule::init() { + setButtonIcon(ui_->button, ":/resource/image/button.png"); + setButtonIcon(ui_->lamp, ":/resource/image/lamp.png"); + setButtonIcon(ui_->addPage, ":/resource/image/add.png"); + setButtonIcon(ui_->deletePage, ":/resource/image/delete.png"); + setButtonIcon(ui_->prePage, ":/resource/image/pre.png"); + setButtonIcon(ui_->nextPage, ":/resource/image/next.png"); + + if (!ui_->hmiGraphicsView->scene()) { + // 创建场景时,将 view 指针传入 + m_scene = new CustomGraphicsScene(ui_->hmiGraphicsView, this); + m_scene->setSceneRect(0, 0, 800, 600); + ui_->hmiGraphicsView->setScene(m_scene); + } else { + m_scene = qobject_cast(ui_->hmiGraphicsView->scene()); + Q_ASSERT(m_scene); + } + + connect(ui_->button, &QPushButton::clicked, this, &HMIModule::prepareToCreateButton); + connect(ui_->lamp, &QPushButton::clicked, this, &HMIModule::prepareToCreateIndicator); + connect(m_scene, &CustomGraphicsScene::componentCreated, this, &HMIModule::onComponentCreated); + // 连接来自场景的键盘事件信号 + connect(m_scene, &CustomGraphicsScene::copyRequestFromScene, this, &HMIModule::onCopyRequested); + connect(m_scene, &CustomGraphicsScene::pasteRequestFromScene, this, &HMIModule::onPasteRequested); + connect(m_scene, &CustomGraphicsScene::deleteRequestFromScene, this, &HMIModule::onDeleteRequested); +} +// 辅助函数,用于设置新创建的组件 +void HMIModule::setupNewComponent(HmiComponent* item) +{ + if (!item) return; + + // 连接所有需要的信号 + connect(item, &HmiComponent::selected, this, &HMIModule::onComponentSelected); + connect(item, &HmiComponent::copyRequested, this, &HMIModule::onCopyRequested); + connect(item, &HmiComponent::deleteRequested, this, &HMIModule::onDeleteRequested); + + // 生成日志 + QString currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); + QString log = QString("[%1] 创建 %2 组件") + .arg(currentTime) + .arg(item->componentName()); + emit logMessageGenerated(log); +} + +// 准备创建按钮的槽函数 +void HMIModule::prepareToCreateButton() +{ + m_scene->setComponentTypeToCreate(ComponentType::Button); + m_scene->setMode(CustomGraphicsScene::Mode::CreateItem); +} + +// 准备创建指示灯的槽函数 +void HMIModule::prepareToCreateIndicator() +{ + m_scene->setComponentTypeToCreate(ComponentType::Indicator); + m_scene->setMode(CustomGraphicsScene::Mode::CreateItem); +} + +// 处理新创建组件的槽函数 +void HMIModule::onComponentCreated(HmiComponent* item) +{ + setupNewComponent(item); +} + +void HMIModule::onCopyRequested() +{ + QList selected = m_scene->selectedItems(); + if (selected.isEmpty()) return; + + // 只复制第一个选中的项目 + HmiComponent* itemToCopy = qgraphicsitem_cast(selected.first()); + if (!itemToCopy) return; + + // 存储组件信息到 "剪贴板" + if (dynamic_cast(itemToCopy)) { + m_copiedType = ComponentType::Button; + } else if (dynamic_cast(itemToCopy)) { + m_copiedType = ComponentType::Indicator; + } + m_copiedColor = itemToCopy->color(); + m_hasCopiedItem = true; + + // 日志 + emit logMessageGenerated(QString("[%1] 复制组件: %2") + .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")) + .arg(itemToCopy->componentName())); +} + +void HMIModule::onPasteRequested(const QPointF& scenePos) +{ + if (!m_hasCopiedItem) return; + + HmiComponent* newItem = nullptr; + if (m_copiedType == ComponentType::Button) { + newItem = new HmiButton(); + } else { + newItem = new HmiIndicator(); + } + + if (newItem) { + newItem->setColor(m_copiedColor); + // 为了避免完全重叠,给一个小的偏移量 + newItem->setPos(scenePos + QPointF(10, 10)); + + // 使用辅助函数完成添加和信号连接 + // 注意:setupNewComponent内部会再次生成“创建”日志,这里是合理的 + setupNewComponent(newItem); + } +} + +void HMIModule::onDeleteRequested() +{ + QList selected = m_scene->selectedItems(); + if (selected.isEmpty()) return; + + // 日志 + emit logMessageGenerated(QString("[%1] 删除 %2 个组件") + .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")) + .arg(selected.count())); + + // 从后往前删,避免迭代器失效问题 + for (int i = selected.count() - 1; i >= 0; --i) { + QGraphicsItem* item = selected.at(i); + m_scene->removeItem(item); + delete item; // 释放内存 + } +} + +// 统一处理选中事件的槽函数,无需修改 +void HMIModule::onComponentSelected(HmiComponent* item) +{ + // if (item) { + // qDebug() << "Component selected:" << item->componentName() + // << "Color:" << item->color(); + // // 未来可以在这里将item对象传递给属性编辑器 + // } +} diff --git a/hmimodule.h b/hmimodule.h new file mode 100644 index 0000000..45596d5 --- /dev/null +++ b/hmimodule.h @@ -0,0 +1,49 @@ +#ifndef HMIMODULE_H +#define HMIMODULE_H + +#include +#include "ui_MainWindow.h" +// #include "customgraphics.h" // 已被 customgraphicsscene.h 包含 +#include "customgraphicsscene.h" // 引入新的场景头文件 + +class HMIModule : public QObject +{ + Q_OBJECT +public: + explicit HMIModule(Ui::MainWindow* ui, QObject *parent = nullptr); + void setButtonIcon(QAbstractButton* button, const QString& iconPath); + void init(); + +private slots: + // 准备创建组件,而不是直接创建 + void prepareToCreateButton(); + void prepareToCreateIndicator(); + + // 当场景创建了一个组件后,进行后续处理 + void onComponentCreated(HmiComponent* item); + + void onComponentSelected(HmiComponent* item); + + // 处理复制、粘贴、删除的槽函数 + void onCopyRequested(); + void onPasteRequested(const QPointF& scenePos); + void onDeleteRequested(); + + // 一个用于发送日志消息的信号 +signals: + void logMessageGenerated(const QString& message); + +private: + void setupNewComponent(HmiComponent* item); + +private: + Ui::MainWindow* ui_; + CustomGraphicsScene* m_scene; + + // 用于实现剪贴板功能的成员变量 + ComponentType m_copiedType; + QColor m_copiedColor; + bool m_hasCopiedItem = false; +}; + +#endif // HMIMODULE_H diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..5724f4b --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,36 @@ +#include "mainwindow.h" +#include +#include +#include +#include + + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui_(new Ui::MainWindow) +{ + ui_->setupUi(this); + hmi_ = new HMIModule(ui_, this); + hmi_->init(); + + initMainWindow(); + // 连接 HMIModule 的信号到 MainWindow 的槽 + connect(hmi_, &HMIModule::logMessageGenerated, this, &MainWindow::appendLog); +} + +MainWindow::~MainWindow() +{ + delete ui_; +} + +void MainWindow::initMainWindow() +{ + setWindowTitle("综合平台编辑器"); + setWindowIcon(QIcon(":/resource/image/editor.png")); +} + +// 实现槽函数 +void MainWindow::appendLog(const QString& message) +{ + ui_->textEdit->append(message); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..f53f325 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,32 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include "hmimodule.h" + +QT_BEGIN_NAMESPACE +namespace Ui { +class MainWindow; +} +QT_END_NAMESPACE + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private: + void initMainWindow(); + + // 一个用于接收日志并更新UI的槽函数 +private slots: + void appendLog(const QString& message); + +private: + Ui::MainWindow *ui_; + HMIModule* hmi_; +}; +#endif // MAINWINDOW_H