// PLCDocument.cpp 的完整修改版 #include "plcdocument.h" #include "plcitems.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // 固定表格尺寸的常量 const int DEFAULT_ROWS = 20; const int DEFAULT_COLUMNS = 25; const int DEFAULT_CELL_SIZE = 50; PLCDocument::PLCDocument(QWidget *parent) : BaseDocument(PLC, parent) , m_rows(DEFAULT_ROWS) , m_columns(DEFAULT_COLUMNS) ,m_cellSize(DEFAULT_CELL_SIZE) { // 创建绘图场景 m_scene = new QGraphicsScene(this); m_scene->setSceneRect(0, 0, m_columns * m_cellSize, m_rows * m_cellSize); // 创建视图 m_view = new QGraphicsView(m_scene, this); m_view->setRenderHint(QPainter::Antialiasing); m_view->setDragMode(QGraphicsView::RubberBandDrag); m_view->setMouseTracking(true); m_view->viewport()->installEventFilter(this); m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_view->setAlignment(Qt::AlignLeft | Qt::AlignTop); m_view->setFixedSize(m_columns * m_cellSize, m_rows * m_cellSize); setAcceptDrops(true); m_view->setAcceptDrops(true); m_view->viewport()->setAcceptDrops(true); // 布局(让视图占满文档区域) auto layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(m_view); setLayout(layout); // 创建固定大小的表格 createRealTable(); // 连接场景信号 connect(m_scene, &QGraphicsScene::selectionChanged, this, [this]() { auto items = m_scene->selectedItems(); if (items.size() == 1 && dynamic_cast(items.first())) { PLCItem *item = static_cast(items.first()); if (m_connectionStart) { endConnection(item); } } }); // 连接场景变化信号(用于更新连接线位置) connect(m_scene, &QGraphicsScene::changed, this, &PLCDocument::handleSceneChanged); } PLCDocument::~PLCDocument() { qDeleteAll(m_connections); clearTable(); } QString PLCDocument::title() const { return "PLC文档"; } void PLCDocument::createRealTable() { clearTable(); // 表格整体区域(覆盖整个场景) QRectF tableRect(0, 0, m_columns * m_cellSize, m_rows * m_cellSize); m_tableFrame = new QGraphicsRectItem(tableRect); m_tableFrame->setPen(QPen(Qt::black, 2)); m_tableFrame->setBrush(QBrush(QColor(245, 245, 245))); // 浅灰背景区分表格区域 m_tableFrame->setZValue(-10); m_scene->addItem(m_tableFrame); // 添加垂直线(单元格分隔线) for (int col = 0; col <= m_columns; col++) { int x = col * m_cellSize; QGraphicsLineItem* line = new QGraphicsLineItem(x, 0, x, m_rows * m_cellSize); line->setPen(QPen(Qt::darkGray, 1)); line->setZValue(-9); m_scene->addItem(line); m_verticalLines.append(line); } // 添加水平线(单元格分隔线) for (int row = 0; row <= m_rows; row++) { int y = row * m_cellSize; QGraphicsLineItem* line = new QGraphicsLineItem(0, y, m_columns * m_cellSize, y); line->setPen(QPen(Qt::darkGray, 1)); line->setZValue(-9); m_scene->addItem(line); m_horizontalLines.append(line); } } void PLCDocument::clearTable() { if (m_tableFrame) { m_scene->removeItem(m_tableFrame); delete m_tableFrame; m_tableFrame = nullptr; } for (auto line : m_horizontalLines) { m_scene->removeItem(line); delete line; } m_horizontalLines.clear(); for (auto line : m_verticalLines) { m_scene->removeItem(line); delete line; } m_verticalLines.clear(); } QPointF PLCDocument::snapToCellCenter(const QPointF& pos) const { // 计算所在单元格列号(0~m_columns-1) int col = static_cast(std::floor(pos.x() / m_cellSize)); // 计算所在单元格行号(0~m_rows-1) int row = static_cast(std::floor(pos.y() / m_cellSize)); // 限制在表格范围内 col = qBound(0, col, m_columns - 1); row = qBound(0, row, m_rows - 1); return QPointF(col * m_cellSize + m_cellSize / 2.0, row * m_cellSize + m_cellSize / 2.0); } QPointF PLCDocument::constrainToTable(const QPointF &pos) const { int col = static_cast(std::floor(pos.x() / m_cellSize)); int row = static_cast(std::floor(pos.y() / m_cellSize)); col = qBound(0, col, m_columns - 1); row = qBound(0, row, m_rows - 1); return QPointF(col * m_cellSize + m_cellSize / 2.0, row * m_cellSize + m_cellSize / 2.0); } void PLCDocument::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); // 不再调整表格大小,只更新视图尺寸 m_view->setFixedSize(m_columns * m_cellSize, m_rows * m_cellSize); } bool PLCDocument::eventFilter(QObject *obj, QEvent *event) { // 仅处理viewport的事件 if (obj == m_view->viewport()) { // 处理拖拽进入事件 if (event->type() == QEvent::DragEnter) { QDragEnterEvent *dragEvent = static_cast(event); if (dragEvent->mimeData()->hasText()) { dragEvent->acceptProposedAction(); return true; } } // 处理拖拽移动事件 else if (event->type() == QEvent::DragMove) { QDragMoveEvent *dragEvent = static_cast(event); if(dragEvent->mimeData()->hasText()) { dragEvent->acceptProposedAction(); return true; } } // 处理拖拽释放事件(使用单元格中心对齐) else if (event->type() == QEvent::Drop) { QDropEvent *dropEvent = static_cast(event); const QMimeData *mimeData = dropEvent->mimeData(); if (mimeData->hasText()) { QPoint viewportPos = m_view->viewport()->mapFromParent(dropEvent->pos()); QPointF scenePos = m_view->mapToScene(viewportPos); // 对齐到单元格中心 QPointF cellCenter = snapToCellCenter(scenePos); // 检查单元格是否已被占用 QSet occupiedCells; for (QGraphicsItem* item : m_scene->items()) { if (PLCItem* plcItem = dynamic_cast(item)) { int col = static_cast(plcItem->x() / m_cellSize); int row = static_cast(plcItem->y() / m_cellSize); occupiedCells.insert(col * 1000 + row); // 简单的行列唯一键 } } int col = static_cast(cellCenter.x() / m_cellSize); int row = static_cast(cellCenter.y() / m_cellSize); int cellKey = col * 1000 + row; // 如果单元格被占用,尝试移动到最近的空单元格 if (occupiedCells.contains(cellKey)) { bool found = false; // 尝试向右移动 for (int offset = 1; offset < m_columns; offset++) { int newCol = col + offset; if (newCol < m_columns) { int newKey = newCol * 1000 + row; if (!occupiedCells.contains(newKey)) { cellCenter = QPointF(newCol * m_cellSize + m_cellSize / 2.0, row * m_cellSize + m_cellSize / 2.0); found = true; break; } } } if (!found) { // 尝试向下移动 for (int offset = 1; offset < m_rows; offset++) { int newRow = row + offset; if (newRow < m_rows) { int newKey = col * 1000 + newRow; if (!occupiedCells.contains(newKey)) { cellCenter = QPointF(col * m_cellSize + m_cellSize / 2.0, newRow * m_cellSize + m_cellSize / 2.0); found = true; break; } } } } } // 创建PLC控件 QString toolType = mimeData->text(); createPLCItem(toolType, cellCenter); m_currentTool.clear(); dropEvent->acceptProposedAction(); return true; } } } if (obj != m_view->viewport()) { return QWidget::eventFilter(obj, event); } // 鼠标移动事件(更新临时连线)- 基于单元格中心 if (event->type() == QEvent::MouseMove && m_tempLine) { QMouseEvent *mouseEvent = static_cast(event); QPointF scenePos = m_view->mapToScene(mouseEvent->pos()); QPointF cellCenter = snapToCellCenter(scenePos); // 使用单元格中心 QLineF line(m_connectionStart->rightTerminal(), cellCenter); m_tempLine->setLine(line); } // 鼠标按下事件 if (event->type() == QEvent::MouseButtonPress) { QMouseEvent *mouseEvent = static_cast(event); // 右键点击 - 显示上下文菜单 if (mouseEvent->button() == Qt::RightButton) { createContextMenu(mouseEvent->globalPos()); return true; } // 左键点击 if (mouseEvent->button() == Qt::LeftButton) { QPointF scenePos = m_view->mapToScene(mouseEvent->pos()); QPointF cellCenter = snapToCellCenter(scenePos); // 单元格中心 // 使用当前工具创建控件 if (!m_currentTool.isEmpty()) { // 检查单元格是否已被占用 QSet occupiedCells; for (QGraphicsItem* item : m_scene->items()) { if (PLCItem* plcItem = dynamic_cast(item)) { int col = static_cast(plcItem->x() / m_cellSize); int row = static_cast(plcItem->y() / m_cellSize); occupiedCells.insert(col * 1000 + row); // 简单的行列唯一键 } } int col = static_cast(cellCenter.x() / m_cellSize); int row = static_cast(cellCenter.y() / m_cellSize); int cellKey = col * 1000 + row; // 如果单元格被占用,显示警告 if (occupiedCells.contains(cellKey)) { QMessageBox::warning(this, "位置被占用", "此单元格已被占用,请选择其他位置。"); return true; } createPLCItem(m_currentTool, cellCenter); m_currentTool.clear(); return true; } // 连线模式处理 if (m_connectionStart) { QGraphicsItem *item = m_scene->itemAt(cellCenter, m_view->transform()); if (PLCItem *plcItem = dynamic_cast(item)) { endConnection(plcItem); return true; } } // 开始连线 QGraphicsItem *item = m_scene->itemAt(cellCenter, m_view->transform()); if (PLCItem *plcItem = dynamic_cast(item)) { startConnection(plcItem); return true; } } } return QWidget::eventFilter(obj, event); } void PLCDocument::createPLCItem(const QString &type, const QPointF &pos) { PLCItem *item = nullptr; if (type == "常开") { item = new NormallyOpenItem; } else if (type == "常闭") { item = new NormallyClosedItem; } else if (type == "大于") { item = new GreaterThanItem; } else if (type == "大于等于") { item = new GreaterEqualItem; } else if (type == "小于") { item = new LessThanItem; } else if (type == "小于等于") { item = new LessEqualItem; } else if (type == "等于") { item = new EqualItem; } else if (type == "线圈") { item = new CoilItem; } if (item) { // 确保位置在表格内并居中 QPointF constrainedPos = constrainToTable(pos); item->setPos(constrainedPos); m_scene->addItem(item); setModified(true); // 连接状态变化信号 connect(item, &PLCItem::stateChanged, this, [this, item](bool active) { if (m_simulationRunning) { if (active) { m_activeItems.insert(item); } else { m_activeItems.remove(item); } updateConnections(); } }); } } void PLCDocument::handleSceneChanged() { // 遍历所有项目,处理位置变化 for (QGraphicsItem *item : m_scene->items()) { if (PLCItem *plcItem = dynamic_cast(item)) { handleItemPositionChange(plcItem); } } } void PLCDocument::handleItemPositionChange(PLCItem *item) { // 限制项目位置在表格内 QPointF newPos = constrainToTable(item->pos()); if (newPos != item->pos()) { item->setPos(newPos); } // 更新所有与该项目相关的连接 for (ConnectionLine *line : m_connections) { if (line->startItem() == item || line->endItem() == item) { line->updatePosition(); } } } void PLCDocument::startConnection(PLCItem *startItem) { if (m_connectionStart) { clearCurrentConnection(); } m_connectionStart = startItem; m_tempLine = new QGraphicsLineItem; m_tempLine->setPen(QPen(Qt::blue, 2, Qt::DashLine)); m_tempLine->setLine(QLineF(startItem->rightTerminal(), startItem->rightTerminal())); m_scene->addItem(m_tempLine); } void PLCDocument::endConnection(PLCItem *endItem) { if (!m_connectionStart || m_connectionStart == endItem) { clearCurrentConnection(); return; } // 创建永久连接线 ConnectionLine *connection = new ConnectionLine(m_connectionStart, endItem); m_scene->addItem(connection); m_connections.append(connection); clearCurrentConnection(); setModified(true); } void PLCDocument::clearCurrentConnection() { if (m_tempLine) { m_scene->removeItem(m_tempLine); delete m_tempLine; m_tempLine = nullptr; } m_connectionStart = nullptr; } void PLCDocument::updateConnections() { for (ConnectionLine *line : m_connections) { PLCItem *start = line->startItem(); PLCItem *end = line->endItem(); if (m_activeItems.contains(start) && m_activeItems.contains(end)) { line->setPen(QPen(Qt::green, 2)); } else { line->setPen(QPen(Qt::black, 1)); } } } void PLCDocument::setSimulationRunning(bool running) { m_simulationRunning = running; if (!running) { resetSimulation(); } } void PLCDocument::resetSimulation() { m_activeItems.clear(); for (QGraphicsItem *item : m_scene->items()) { if (PLCItem *plcItem = dynamic_cast(item)) { plcItem->setState(false); } } updateConnections(); } void PLCDocument::createContextMenu(QPoint globalPos) { QMenu menu(this); QAction *runAction = menu.addAction("运行模拟"); QAction *stopAction = menu.addAction("停止模拟"); QAction *resetAction = menu.addAction("重置模拟"); menu.addSeparator(); QAction *nameAction = menu.addAction("重命名"); menu.addSeparator(); QAction *tableSizeAction = menu.addAction("调整表格尺寸"); QAction *tableGridAction = menu.addAction("隐藏/显示网格"); connect(runAction, &QAction::triggered, this, [this]() { setSimulationRunning(true); }); connect(stopAction, &QAction::triggered, this, [this]() { setSimulationRunning(false); }); connect(resetAction, &QAction::triggered, this, &PLCDocument::resetSimulation); connect(nameAction, &QAction::triggered, this, [this]() { QList selected = m_scene->selectedItems(); if (selected.size() == 1) { if (PLCItem *item = dynamic_cast(selected.first())) { QString name = QInputDialog::getText(this, "重命名", "输入名称:", QLineEdit::Normal, item->name()); if (!name.isEmpty()) { item->setName(name); setModified(true); } } } }); connect(tableSizeAction, &QAction::triggered, this, [this]() { bool ok; int cellSize = QInputDialog::getInt(this, "单元格大小", "输入单元格大小(像素):", m_cellSize, 10, 100, 10, &ok); if (ok && cellSize != m_cellSize) { m_cellSize = cellSize; m_view->setFixedSize(m_columns * m_cellSize, m_rows * m_cellSize); createRealTable(); setModified(true); } }); connect(tableGridAction, &QAction::triggered, this, [this]() { for (auto line : m_horizontalLines) { line->setVisible(!line->isVisible()); } for (auto line : m_verticalLines) { line->setVisible(!line->isVisible()); } }); menu.exec(globalPos); } bool PLCDocument::saveToFile(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::WriteOnly)) { QMessageBox::warning(this, "保存失败", "无法打开文件进行写入:" + filePath); return false; } // 创建JSON文档结构 QJsonObject docObject; docObject["type"] = "PLCDocument"; // 保存表格参数 docObject["table_rows"] = m_rows; docObject["table_columns"] = m_columns; docObject["table_cell_size"] = m_cellSize; // 序列化所有PLC元件 - 改为保存单元格坐标 QJsonArray itemsArray; for (QGraphicsItem *item : m_scene->items()) { if (PLCItem *plcItem = dynamic_cast(item)) { QJsonObject itemObj; itemObj["type"] = plcItem->itemType(); int col = static_cast(plcItem->x() / m_cellSize); int row = static_cast(plcItem->y() / m_cellSize); itemObj["col"] = col; // 保存列索引 itemObj["row"] = row; // 保存行索引 itemObj["name"] = plcItem->name(); itemObj["active"] = plcItem->isActive(); itemsArray.append(itemObj); } } docObject["items"] = itemsArray; // 序列化所有连接 - 改为保存起始和终止元件的坐标 QJsonArray connectionsArray; for (ConnectionLine *line : m_connections) { QJsonObject connObj; // 通过坐标查找起始和终止元件 int startCol = static_cast(line->startItem()->x() / m_cellSize); int startRow = static_cast(line->startItem()->y() / m_cellSize); int endCol = static_cast(line->endItem()->x() / m_cellSize); int endRow = static_cast(line->endItem()->y() / m_cellSize); connObj["startCol"] = startCol; connObj["startRow"] = startRow; connObj["endCol"] = endCol; connObj["endRow"] = endRow; connectionsArray.append(connObj); } docObject["connections"] = connectionsArray; // 写入文件 QJsonDocument doc(docObject); file.write(doc.toJson()); file.close(); // 更新文档状态 setFilePath(filePath); setModified(false); return true; } bool PLCDocument::loadFromFile(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { QMessageBox::warning(this, "打开失败", "无法打开文件:" + filePath); return false; } // 读取并解析JSON QByteArray data = file.readAll(); file.close(); QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); if (parseError.error != QJsonParseError::NoError) { QMessageBox::warning(this, "文件格式错误", "JSON解析错误: " + parseError.errorString()); return false; } if (doc.isNull() || !doc.isObject()) { QMessageBox::warning(this, "文件格式错误", "不是有效的PLC文档"); return false; } // 解析文档 QJsonObject docObject = doc.object(); if (docObject["type"].toString() != "PLCDocument") { QMessageBox::warning(this, "文件类型错误", "不是有效的PLC文档"); return false; } // 获取表格参数 m_rows = docObject["table_rows"].toInt(DEFAULT_ROWS); m_columns = docObject["table_columns"].toInt(DEFAULT_COLUMNS); m_cellSize = docObject["table_cell_size"].toInt(DEFAULT_CELL_SIZE); // 停止任何模拟 setSimulationRunning(false); // 安全地重置场景 m_scene->blockSignals(true); // 防止在加载过程中发送变化信号 // 清理现有内容 - 更安全的方式 for (auto conn : m_connections) { m_scene->removeItem(conn); delete conn; } m_connections.clear(); QList allItems = m_scene->items(); for (QGraphicsItem* item : allItems) { // 保留表格项,只删除PLC元件 if (dynamic_cast(item) || dynamic_cast(item)) { m_scene->removeItem(item); delete item; } } m_activeItems.clear(); m_connectionStart = nullptr; delete m_tempLine; m_tempLine = nullptr; // 创建表格 createRealTable(); // 重新创建表格以确保正确 // 加载PLC元件 QMap, PLCItem*> itemMap; // (列, 行) -> PLCItem QJsonArray itemsArray = docObject["items"].toArray(); for (const QJsonValue &itemValue : itemsArray) { QJsonObject itemObj = itemValue.toObject(); PLCItem::ItemType type = static_cast(itemObj["type"].toInt()); int col = itemObj["col"].toInt(); int row = itemObj["row"].toInt(); QString name = itemObj["name"].toString(); bool active = itemObj["active"].toBool(); // 确保位置在表格范围内 col = qBound(0, col, m_columns - 1); row = qBound(0, row, m_rows - 1); PLCItem *item = createItemByType(type); if (item) { QPointF pos(col * m_cellSize + m_cellSize / 2.0, row * m_cellSize + m_cellSize / 2.0); item->setPos(pos); item->setName(name); item->setState(active); m_scene->addItem(item); // 添加到映射表 itemMap.insert(qMakePair(col, row), item); if (active) { m_activeItems.insert(item); } connect(item, &PLCItem::stateChanged, this, [this, item](bool active) { if (m_simulationRunning) { if (active) m_activeItems.insert(item); else m_activeItems.remove(item); updateConnections(); } }); } } // 加载连接 QJsonArray connectionsArray = docObject["connections"].toArray(); for (const QJsonValue &connValue : connectionsArray) { QJsonObject connObj = connValue.toObject(); int startCol = connObj["startCol"].toInt(); int startRow = connObj["startRow"].toInt(); int endCol = connObj["endCol"].toInt(); int endRow = connObj["endRow"].toInt(); PLCItem *startItem = itemMap.value(qMakePair(startCol, startRow)); PLCItem *endItem = itemMap.value(qMakePair(endCol, endRow)); if (startItem && endItem && startItem != endItem) { ConnectionLine *line = new ConnectionLine(startItem, endItem); m_scene->addItem(line); m_connections.append(line); } } // 完成加载 m_scene->blockSignals(false); handleSceneChanged(); // 手动触发位置更新 // 更新UI状态 setFilePath(filePath); setModified(false); qDebug() << "成功加载文件:" << filePath << "项目数:" << itemMap.size() << "连接数:" << m_connections.size(); return true; } PLCItem* PLCDocument::createItemByType(PLCItem::ItemType type) { switch (type) { case PLCItem::NormallyOpen: return new NormallyOpenItem; case PLCItem::NormallyClosed: return new NormallyClosedItem; case PLCItem::GreaterThan: return new GreaterThanItem; case PLCItem::GreaterEqual: return new GreaterEqualItem; case PLCItem::LessThan: return new LessThanItem; case PLCItem::LessEqual: return new LessEqualItem; case PLCItem::Equal: return new EqualItem; case PLCItem::Coil: return new CoilItem; default: qWarning() << "未知的PLC元件类型:" << type; return nullptr; } } // ConnectionLine 实现 ConnectionLine::ConnectionLine(PLCItem *start, PLCItem *end, QGraphicsItem *parent) : QGraphicsLineItem(parent), m_startItem(start), m_endItem(end) { setPen(QPen(Qt::black, 1)); setZValue(-1); updatePosition(); } void ConnectionLine::updatePosition() { if (m_startItem && m_endItem) { // 基于控件端子位置更新连线 QLineF line(m_startItem->rightTerminal(), m_endItem->leftTerminal()); setLine(line); } }