commit e3a4c9be7d438943962d40822f986501236b21f9 Author: 付春阳 <2715725160@qq.com> Date: Thu Aug 7 14:05:18 2025 +0800 实现HMI页面能够拖动按钮和指示灯,并能够复制粘贴删除 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