diff --git a/customgraphics.cpp b/customgraphics.cpp index c85af8e..9a05ad1 100644 --- a/customgraphics.cpp +++ b/customgraphics.cpp @@ -4,6 +4,14 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include // HmiComponent 基类实现 HmiComponent::HmiComponent(QGraphicsItem *parent) : QGraphicsObject(parent) @@ -47,6 +55,19 @@ void HmiComponent::paint(QPainter *painter, const QStyleOptionGraphicsItem *opti } } +void HmiComponent::setAddress(int address) +{ + if (m_address != address) { + m_address = address; + update(); + } +} + +int HmiComponent::address() const +{ + return m_address; +} + void HmiComponent::mousePressEvent(QGraphicsSceneMouseEvent *event) { // 右键不触发选中信号,留给 contextMenuEvent 处理 @@ -58,38 +79,212 @@ void HmiComponent::mousePressEvent(QGraphicsSceneMouseEvent *event) void HmiComponent::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { - // 右键点击时,先清空场景中的其他选中项,然后选中当前项 scene()->clearSelection(); setSelected(true); - emit selected(this); // 同样可以发出选中信号,以便更新属性面板等 + emit selected(this); QMenu menu; + + QAction *propertyAction = menu.addAction("属性"); QAction *copyAction = menu.addAction("复制"); QAction *deleteAction = menu.addAction("删除"); - QAction *appearanceAction = menu.addAction("改变外观"); - // 使用 connect 将菜单动作的触发连接到信号的发射,复制和删除 + connect(propertyAction, &QAction::triggered, this, [this]() { + // 弹出属性对话框 + QDialog dialog; + dialog.setWindowTitle("属性"); + + QLabel* addressLabel = new QLabel("地址: "); + QSpinBox* addressSpin = new QSpinBox(); + addressSpin->setRange(0, 4000); + addressSpin->setValue(m_address); // 使用当前组件地址初始化 + + // ON状态颜色设置 + QLabel* onColorLabel = new QLabel("ON状态外观: "); + // 颜色显示,用QLabel设置背景色简单示范 + QLabel* onColorDisplay = new QLabel; + onColorDisplay->setFixedSize(40, 20); + onColorDisplay->setAutoFillBackground(true); + QPalette onPal = onColorDisplay->palette(); + onPal.setColor(QPalette::Window, m_onColor); + onColorDisplay->setPalette(onPal); + onColorDisplay->setFrameShape(QFrame::Box); + + QPushButton* onColorBtn = new QPushButton("选择颜色"); + + // OFF状态颜色设置 + QLabel* offColorLabel = new QLabel("OFF状态外观: "); + QLabel* offColorDisplay = new QLabel; + offColorDisplay->setFixedSize(40, 20); + offColorDisplay->setAutoFillBackground(true); + QPalette offPal = offColorDisplay->palette(); + offPal.setColor(QPalette::Window, m_offColor); + offColorDisplay->setPalette(offPal); + offColorDisplay->setFrameShape(QFrame::Box); + + QPushButton* offColorBtn = new QPushButton("选择颜色"); + + QObject::connect(offColorBtn, &QPushButton::clicked, [&]() { + QColor color = QColorDialog::getColor(m_offColor, nullptr, "选择 OFF 状态颜色"); + if (color.isValid()) { + m_offColor = color; + QPalette pal = offColorDisplay->palette(); + pal.setColor(QPalette::Window, color); + offColorDisplay->setPalette(pal); + offColorDisplay->update(); + setColor(color); // 默认显示OFF状态颜色 + } + }); + + // 地址布局 + QHBoxLayout* addressLayout = new QHBoxLayout(); + addressLayout->addWidget(addressLabel); + addressLayout->addWidget(addressSpin); + + // ON颜色布局 + QHBoxLayout* onColorLayout = new QHBoxLayout(); + onColorLayout->addWidget(onColorLabel); + onColorLayout->addWidget(onColorDisplay); + onColorLayout->addWidget(onColorBtn); + + // OFF颜色布局 + QHBoxLayout* offColorLayout = new QHBoxLayout(); + offColorLayout->addWidget(offColorLabel); + offColorLayout->addWidget(offColorDisplay); + offColorLayout->addWidget(offColorBtn); + + // 确定/取消按钮 + QPushButton* okBtn = new QPushButton("确定"); + QPushButton* cancelBtn = new QPushButton("取消"); + QHBoxLayout* btnLayout = new QHBoxLayout(); + btnLayout->addStretch(); + btnLayout->addWidget(okBtn); + btnLayout->addWidget(cancelBtn); + + // 颜色选择按钮点击槽 + QObject::connect(onColorBtn, &QPushButton::clicked, [&]() { + QColor color = QColorDialog::getColor(m_onColor, nullptr, "选择 ON 状态颜色"); + if (color.isValid()) { + m_onColor = color; + QPalette pal = onColorDisplay->palette(); + pal.setColor(QPalette::Window, color); + onColorDisplay->setPalette(pal); + onColorDisplay->update(); + } + }); + + QVBoxLayout* mainLayout = new QVBoxLayout(); + mainLayout->addLayout(addressLayout); + mainLayout->addLayout(onColorLayout); + mainLayout->addLayout(offColorLayout); + mainLayout->addLayout(btnLayout); + + dialog.setLayout(mainLayout); + + // 关闭逻辑 + QObject::connect(okBtn, &QPushButton::clicked, [&]() { + // 保存地址 + int newAddress = addressSpin->value(); + setAddress(newAddress); + + // 已经在onColorBtn / offColorBtn中修改了m_onColor / m_offColor,需要触发界面更新 + update(); + dialog.accept(); + }); + QObject::connect(cancelBtn, &QPushButton::clicked, &dialog, &QDialog::reject); + + dialog.exec(); + }); + connect(copyAction, &QAction::triggered, this, [this]() { emit copyRequested(this); }); + connect(deleteAction, &QAction::triggered, this, [this]() { emit deleteRequested(this); }); - connect(appearanceAction, &QAction::triggered, this, &HmiComponent::changeAppearance); - // 在鼠标光标位置显示菜单 menu.exec(event->screenPos()); } -void HmiComponent::changeAppearance() +void HmiComponent::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { - emit appearanceChangedRequested(this); + if (!(flags() & QGraphicsItem::ItemIsMovable)) { + // 如果项不可移动,直接调用基类处理 + QGraphicsObject::mouseMoveEvent(event); + return; + } + + // 计算拖动后的位置 + QPointF delta = event->scenePos() - event->lastScenePos(); + QPointF newPos = pos() + delta; + + // 获取视图(假设只有1个视图) + QList views = scene()->views(); + if (views.isEmpty()) { + // 没有视图则默认处理 + QGraphicsObject::mouseMoveEvent(event); + return; + } + QGraphicsView* view = views.first(); + + // 获取视口矩形(像素坐标) + QRect viewportRect = view->viewport()->rect(); + + // 将视口矩形映射到场景坐标 + QPointF topLeftScene = view->mapToScene(viewportRect.topLeft()); + QPointF bottomRightScene = view->mapToScene(viewportRect.bottomRight()); + QRectF visibleSceneRect(topLeftScene, bottomRightScene); + + // 组件的边界矩形(局部坐标) + QRectF bounds = boundingRect(); + + // 计算移动后组件边界矩形(在场景坐标) + QRectF newBounds = bounds.translated(newPos); + + // 限制组件边界必须完全在视图可见区域内 + qreal limitedX = newPos.x(); + qreal limitedY = newPos.y(); + + if (newBounds.left() < visibleSceneRect.left()) + limitedX += visibleSceneRect.left() - newBounds.left(); + if (newBounds.top() < visibleSceneRect.top()) + limitedY += visibleSceneRect.top() - newBounds.top(); + if (newBounds.right() > visibleSceneRect.right()) + limitedX -= newBounds.right() - visibleSceneRect.right(); + if (newBounds.bottom() > visibleSceneRect.bottom()) + limitedY -= newBounds.bottom() - visibleSceneRect.bottom(); + + setPos(QPointF(limitedX, limitedY)); + event->accept(); +} + + +void HmiComponent::setOnColor(const QColor& color) { + m_onColor = color; +} + +void HmiComponent::setOffColor(const QColor& color) { + m_offColor = color; + setColor(color); // 默认展示 OFF 状态颜色 +} + +QColor HmiComponent::onColor() const { + return m_onColor; } +QColor HmiComponent::offColor() const { + return m_offColor; +} + + HmiButton::HmiButton(QGraphicsItem *parent) : HmiComponent(parent) { m_color = Qt::gray; + m_offColor = m_color; // OFF状态颜色设为默认颜色 + m_onColor = Qt::green; // ON状态颜色为绿色 m_componentName = "Button"; + setColor(m_offColor); // 当前显示OFF颜色 } QRectF HmiButton::boundingRect() const @@ -106,8 +301,11 @@ void HmiButton::paintShape(QPainter *painter) HmiIndicator::HmiIndicator(QGraphicsItem *parent) : HmiComponent(parent) { - m_color = Qt::green; + m_color = Qt::red; + m_offColor = m_color; // OFF状态颜色设为默认颜色 + m_onColor = Qt::green; // ON状态颜色为绿色 m_componentName = "Indicator"; + setColor(m_offColor); // 当前显示OFF颜色 } QRectF HmiIndicator::boundingRect() const diff --git a/customgraphics.h b/customgraphics.h index 422afb7..9d7c434 100644 --- a/customgraphics.h +++ b/customgraphics.h @@ -2,6 +2,7 @@ #define CUSTOMGRAPHICS_H #include +#include // 只需要前向声明,因为头文件中只用到了指针 class QPainter; class QGraphicsSceneMouseEvent; @@ -16,41 +17,56 @@ enum class ComponentType { class HmiComponent : public QGraphicsObject { Q_OBJECT - Q_PROPERTY(QColor color READ color WRITE setColor) + Q_PROPERTY(QColor color READ color WRITE setColor) // 当前颜色(默认使用 OFF 状态) Q_PROPERTY(QString componentName READ componentName WRITE setComponentName) + // 新增address属性 + Q_PROPERTY(int address READ address WRITE setAddress) signals: void selected(HmiComponent* item); - // 为复制和删除请求添加信号 void copyRequested(HmiComponent* item); void deleteRequested(HmiComponent* item); - void appearanceChangedRequested(HmiComponent* item); + + // 添加信号用于改变状态颜色 + void changeOnColorRequested(HmiComponent* item); + void changeOffColorRequested(HmiComponent* item); public: explicit HmiComponent(QGraphicsItem *parent = nullptr); - void setColor(const QColor& color); + 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; + // 设置 ON / OFF 状态颜色 + void setOnColor(const QColor& color); + void setOffColor(const QColor& color); + QColor onColor() const; + QColor offColor() const; -public slots: - void changeAppearance(); + void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; + // 地址相关 + void setAddress(int address); + int address() const; protected: void mousePressEvent(QGraphicsSceneMouseEvent *event) override; - // 重写上下文菜单事件 void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override; virtual void paintShape(QPainter* painter) = 0; + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; protected: - QColor m_color; + QColor m_color; // 当前显示颜色(编辑器中为 OFF) + QColor m_onColor = Qt::green; + QColor m_offColor; QString m_componentName; + int m_address = 0; }; + // 按钮类 class HmiButton : public HmiComponent { diff --git a/hmimodule.cpp b/hmimodule.cpp index f90c1a0..f5903d0 100644 --- a/hmimodule.cpp +++ b/hmimodule.cpp @@ -7,6 +7,11 @@ #include #include #include +#include +#include +#include +#include +#include HMIModule::HMIModule(Ui::MainWindow* ui, QObject *parent) : QObject{parent}, ui_(ui), m_scene(nullptr) @@ -56,6 +61,236 @@ void HMIModule::init() { connect(this, &HMIModule::pageRemoved, this, &HMIModule::onPageRemoved); } +bool HMIModule::saveToFile(const QString& filePath) +{ + QTabWidget* tabWidget = ui_->tabWidget_2; + + QJsonArray pagesArray; + + for (int i = 0; i < tabWidget->count(); ++i) { + QWidget* page = tabWidget->widget(i); + QString pageName = tabWidget->tabText(i); + + QGraphicsView* graphicsView = page->findChild(); + if (!graphicsView) continue; + + QGraphicsScene* scene = graphicsView->scene(); + if (!scene) continue; + + QJsonObject pageObject; + pageObject["pageName"] = pageName; + + QJsonArray componentsArray; + + for (QGraphicsItem* item : scene->items()) { + HmiComponent* component = dynamic_cast(item); + if (!component) + continue; + + QJsonObject compObj; + // 组件类型 + if (dynamic_cast(component)) { + compObj["type"] = "Button"; + } else if (dynamic_cast(component)) { + compObj["type"] = "Indicator"; + } else { + continue; // 忽略未知类型 + } + + // 位置 + QPointF pos = component->pos(); + compObj["x"] = pos.x(); + compObj["y"] = pos.y(); + + // 颜色 (保存在HEX) + compObj["color"] = component->color().name(); + // On / Off颜色 + compObj["onColor"] = component->onColor().name(); + compObj["offColor"] = component->offColor().name(); + // 组件名称 + compObj["componentName"] = component->componentName(); + // 新增地址属性 + compObj["address"] = component->address(); + + componentsArray.append(compObj); + } + + pageObject["components"] = componentsArray; + pagesArray.append(pageObject); + } + + QJsonObject rootObject; + rootObject["pages"] = pagesArray; + + QJsonDocument doc(rootObject); + + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly)) { + emit logMessageGenerated(QString("无法打开文件进行保存: %1").arg(filePath)); + return false; + } + + file.write(doc.toJson(QJsonDocument::Indented)); + file.close(); + + emit logMessageGenerated(QString("[%1] 保存成功: %2") + .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")) + .arg(filePath)); + return true; +} + +// 清空所有页面,重建页面并加载组件 +bool HMIModule::openFromFile(const QString& filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + emit logMessageGenerated(QString("无法打开文件进行加载: %1").arg(filePath)); + return false; + } + + QByteArray data = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); + if (parseError.error != QJsonParseError::NoError) { + emit logMessageGenerated(QString("JSON解析错误: %1").arg(parseError.errorString())); + return false; + } + + if (!doc.isObject()) { + emit logMessageGenerated("JSON格式错误,不是对象"); + return false; + } + + QJsonObject rootObj = doc.object(); + if (!rootObj.contains("pages") || !rootObj["pages"].isArray()) { + emit logMessageGenerated("JSON缺少pages数组"); + return false; + } + + QJsonArray pagesArray = rootObj["pages"].toArray(); + + QTabWidget* tabWidget = ui_->tabWidget_2; + + // 清空所有现有页面 + while (tabWidget->count() > 0) { + QWidget* page = tabWidget->widget(0); + tabWidget->removeTab(0); + delete page; + } + + m_availablePageNumbers.clear(); + m_pageOrder.clear(); + + for (const QJsonValue& pageVal : pagesArray) { + if (!pageVal.isObject()) + continue; + + QJsonObject pageObj = pageVal.toObject(); + + QString pageName = pageObj["pageName"].toString("Unnamed"); + int pageNumber = 0; + if (pageName.startsWith("Page ")) { + pageNumber = pageName.mid(5).toInt(); + } + + // 新建页面 + QWidget* newPage = new QWidget(tabWidget); + newPage->setObjectName(pageName); + + QGraphicsView* newGraphicsView = new QGraphicsView(newPage); + CustomGraphicsScene* newScene = new CustomGraphicsScene(newGraphicsView, this); + + // 设置大小,与默认页面相同 + QSize defaultSize(691, 381); + newGraphicsView->resize(defaultSize); + newScene->setSceneRect(0, 0, 800, 600); + + newGraphicsView->setScene(newScene); + + QVBoxLayout* layout = new QVBoxLayout(newPage); + layout->addWidget(newGraphicsView); + newPage->setLayout(layout); + + int tabIndex = tabWidget->addTab(newPage, pageName); + + // 连接信号 + connect(newScene, &CustomGraphicsScene::componentCreated, this, &HMIModule::onComponentCreated); + connect(newScene, &CustomGraphicsScene::copyRequestFromScene, this, &HMIModule::onCopyRequested); + connect(newScene, &CustomGraphicsScene::pasteRequestFromScene, this, &HMIModule::onPasteRequested); + connect(newScene, &CustomGraphicsScene::deleteRequestFromScene, this, &HMIModule::onDeleteRequested); + + m_pageOrder.append(pageNumber); + + QJsonArray compArray = pageObj["components"].toArray(); + for (const QJsonValue& compVal : compArray) { + if (!compVal.isObject()) + continue; + + QJsonObject compObj = compVal.toObject(); + + QString typeStr = compObj["type"].toString(); + double x = compObj["x"].toDouble(); + double y = compObj["y"].toDouble(); + QString colorName = compObj["color"].toString(); + QString onColorName = compObj["onColor"].toString(); + QString offColorName = compObj["offColor"].toString(); + QString componentName = compObj["componentName"].toString(); + int address = compObj["address"].toInt(0); + + HmiComponent* newItem = nullptr; + if (typeStr == "Button") { + newItem = new HmiButton(); + } else if (typeStr == "Indicator") { + newItem = new HmiIndicator(); + } else { + continue; + } + + if (newItem) { + newItem->setPos(QPointF(x, y)); + QColor color(colorName); + if (color.isValid()) { + newItem->setColor(color); + } + QColor onColor(onColorName); + if (onColor.isValid()) + newItem->setOnColor(onColor); + QColor offColor(offColorName); + if (offColor.isValid()) + newItem->setOffColor(offColor); + + QColor currentColor(colorName); + if (currentColor.isValid()) { + // 可以根据实际业务逻辑判断,是否恢复为onColor或offColor + // 这里直接恢复当前颜色 + newItem->setColor(currentColor); + } else { + // 如果当前颜色无效,则使用 offColor 作为默认颜色 + newItem->setColor(newItem->offColor()); + } + + newItem->setComponentName(componentName); + // 设置地址 + newItem->setAddress(address); + newScene->addItem(newItem); + setupNewComponent(newItem); + } + } + } + + // 自动切换到第一页 + if (tabWidget->count() > 0) { + tabWidget->setCurrentIndex(0); + } + + emit logMessageGenerated(QString("[%1] 打开文件成功: %2") + .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")) + .arg(filePath)); + return true; +} + void HMIModule::setupNewComponent(HmiComponent* item) { if (!item) return; @@ -63,7 +298,9 @@ void HMIModule::setupNewComponent(HmiComponent* item) connect(item, &HmiComponent::selected, this, &HMIModule::onComponentSelected); connect(item, &HmiComponent::copyRequested, this, &HMIModule::onCopyRequested); connect(item, &HmiComponent::deleteRequested, this, &HMIModule::onDeleteRequested); - connect(item, &HmiComponent::appearanceChangedRequested, this, &HMIModule::onChangeAppearanceRequested); // 连接信号 + connect(item, &HmiComponent::changeOnColorRequested, this, &HMIModule::onChangeOnColorRequested); + connect(item, &HmiComponent::changeOffColorRequested, this, &HMIModule::onChangeOffColorRequested); + QString currentTime = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); QString log = QString("[%1] 创建 %2 组件").arg(currentTime).arg(item->componentName()); @@ -177,13 +414,6 @@ void HMIModule::onDeleteRequested() } } -void HMIModule::onChangeAppearanceRequested(HmiComponent* item) -{ - QColor color = QColorDialog::getColor(item->color(), nullptr, "选择颜色"); - if (color.isValid()) { - item->setColor(color); - } -} // 添加页面后的槽函数 void HMIModule::onPageAdded(int pageNumber, int index) @@ -203,6 +433,30 @@ void HMIModule::onPageRemoved(int pageNumber, int index) } } +void HMIModule::onChangeOnColorRequested(HmiComponent* item) +{ + QColor color = QColorDialog::getColor(item->onColor(), nullptr, "选择 ON 状态颜色"); + if (color.isValid()) { + item->setOnColor(color); + emit logMessageGenerated(QString("[%1] 设置 %2 的 ON 状态颜色为 %3") + .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")) + .arg(item->componentName()) + .arg(color.name())); + } +} + +void HMIModule::onChangeOffColorRequested(HmiComponent* item) +{ + QColor color = QColorDialog::getColor(item->offColor(), nullptr, "选择 OFF 状态颜色"); + if (color.isValid()) { + item->setOffColor(color); // 同时更新当前显示颜色 + emit logMessageGenerated(QString("[%1] 设置 %2 的 OFF 状态颜色为 %3") + .arg(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")) + .arg(item->componentName()) + .arg(color.name())); + } +} + void HMIModule::onComponentSelected(HmiComponent* item) { // 不需要任何操作,可以留空 @@ -266,7 +520,7 @@ void HMIModule::addPage() tabIndex = tabWidget->addTab(newPage, pageName); } - // 连接信号和槽 ******************************************************************* + // 连接信号和槽 connect(newScene, &CustomGraphicsScene::componentCreated, this, &HMIModule::onComponentCreated); connect(newScene, &CustomGraphicsScene::copyRequestFromScene, this, &HMIModule::onCopyRequested); connect(newScene, &CustomGraphicsScene::pasteRequestFromScene, this, &HMIModule::onPasteRequested); diff --git a/hmimodule.h b/hmimodule.h index 598d72f..ae28dc2 100644 --- a/hmimodule.h +++ b/hmimodule.h @@ -15,6 +15,8 @@ public: explicit HMIModule(Ui::MainWindow* ui, QObject *parent = nullptr); void setButtonIcon(QAbstractButton* button, const QString& iconPath); void init(); + bool saveToFile(const QString& filePath); + bool openFromFile(const QString& filePath); signals: void pageAdded(int pageNumber, int index); @@ -29,13 +31,14 @@ private slots: void onCopyRequested(); void onPasteRequested(const QPointF& scenePos); void onDeleteRequested(); - void onChangeAppearanceRequested(HmiComponent* item); void addPage(); void deletePage(); void prePage(); void nextPage(); void onPageAdded(int pageNumber, int index); void onPageRemoved(int pageNumber, int index); + void onChangeOnColorRequested(HmiComponent* item); + void onChangeOffColorRequested(HmiComponent* item); private: void setupNewComponent(HmiComponent* item); diff --git a/mainwindow.cpp b/mainwindow.cpp index 5724f4b..000da13 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -3,7 +3,8 @@ #include #include #include - +#include +#include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) @@ -16,6 +17,24 @@ MainWindow::MainWindow(QWidget *parent) initMainWindow(); // 连接 HMIModule 的信号到 MainWindow 的槽 connect(hmi_, &HMIModule::logMessageGenerated, this, &MainWindow::appendLog); + + connect(ui_->action_3, &QAction::triggered, this, [this]() { + QString fileName = QFileDialog::getSaveFileName(this, "保存HMI设计", "", "JSON文件 (*.json)"); + if (!fileName.isEmpty()) { + if (!hmi_->saveToFile(fileName)) { + QMessageBox::warning(this, "保存失败", "文件保存失败!"); + } + } + }); + + connect(ui_->action_2, &QAction::triggered, this, [this]() { + QString fileName = QFileDialog::getOpenFileName(this, "打开HMI设计", "", "JSON文件 (*.json)"); + if (!fileName.isEmpty()) { + if (!hmi_->openFromFile(fileName)) { + QMessageBox::warning(this, "打开失败", "文件打开失败或格式不正确!"); + } + } + }); } MainWindow::~MainWindow()