From f988e94e75d56d9b1440135279d6b7c04af94e3b Mon Sep 17 00:00:00 2001 From: email <15737449156@163.com> Date: Tue, 12 Aug 2025 16:19:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=8D=E5=88=B6=E7=B2=98=E8=B4=B4=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E6=8F=92=E5=85=A5=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- untitled/hmidocument.cpp | 2 +- untitled/mainwindow.cpp | 13 +- untitled/plcdocument.cpp | 475 ++++++++++++++++++++++++++------------- untitled/plcdocument.h | 29 ++- 4 files changed, 361 insertions(+), 158 deletions(-) diff --git a/untitled/hmidocument.cpp b/untitled/hmidocument.cpp index e02cba4..e91e52c 100644 --- a/untitled/hmidocument.cpp +++ b/untitled/hmidocument.cpp @@ -37,7 +37,7 @@ HMIDocument::HMIDocument(QWidget *parent) // 创建视图 - 关键修改:使用滚动区域包装 m_scrollArea = new QScrollArea(this); m_scrollArea->setWidgetResizable(false); // 禁止内容自动调整大小 - m_scrollArea->setFrameShape(QFrame::NoFrame); // 无边框 + //m_scrollArea->setFrameShape(QFrame::NoFrame); // 无边框 // 创建真正的视图部件 m_view = new QGraphicsView(m_scene, this); diff --git a/untitled/mainwindow.cpp b/untitled/mainwindow.cpp index 513f5c8..87274b3 100644 --- a/untitled/mainwindow.cpp +++ b/untitled/mainwindow.cpp @@ -146,25 +146,32 @@ void MainWindow::createMenus() m_saveAsAction->setFont(itemFont); connect(m_saveAsAction, &QAction::triggered, this, &MainWindow::onSaveAs); fileMenu->addAction(m_saveAsAction); - // 操作菜单 - 添加复制、粘贴、删除功能 QAction *copyAction = new QAction("复制(&C)", this); copyAction->setShortcut(QKeySequence::Copy); copyAction->setStatusTip("复制选中的项目"); copyAction->setFont(itemFont); connect(copyAction, &QAction::triggered, this, [this]() { - if (auto hmiDoc = dynamic_cast(m_tabWidget->currentWidget())) { + if (auto hmiDoc = dynamic_cast(m_tabWidget->currentWidget())) + { hmiDoc->copySelectedItems(); } + else if (auto plcDoc = dynamic_cast(m_tabWidget->currentWidget())) + { + plcDoc->copySelectedItem();//添加 PLC 文档支持 + } }); QAction *pasteAction = new QAction("粘贴(&V)", this); pasteAction->setShortcut(QKeySequence::Paste); pasteAction->setStatusTip("粘贴复制的项目"); + pasteAction->setFont(itemFont); connect(pasteAction, &QAction::triggered, this, [this]() { if (auto hmiDoc = dynamic_cast(m_tabWidget->currentWidget())) { hmiDoc->pasteItems(); + } else if (auto plcDoc = dynamic_cast(m_tabWidget->currentWidget())) { + plcDoc->pasteItem(); // 添加 PLC 文档支持 } }); @@ -175,6 +182,8 @@ void MainWindow::createMenus() connect(deleteAction, &QAction::triggered, this, [this]() { if (auto hmiDoc = dynamic_cast(m_tabWidget->currentWidget())) { hmiDoc->deleteSelectedItems(); + } else if (auto plcDoc = dynamic_cast(m_tabWidget->currentWidget())) { + plcDoc->deleteSelectedItems(); // 添加 PLC 文档支持 } }); diff --git a/untitled/plcdocument.cpp b/untitled/plcdocument.cpp index e879d9e..27fb5f8 100644 --- a/untitled/plcdocument.cpp +++ b/untitled/plcdocument.cpp @@ -27,6 +27,7 @@ #include #include #include +#include PLCDocument::PLCDocument(QWidget *parent) : BaseDocument(PLC, parent) @@ -34,36 +35,28 @@ PLCDocument::PLCDocument(QWidget *parent) , 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); - // 创建滚动区域 QScrollArea *scrollArea = new QScrollArea(this); scrollArea->setWidget(m_view); scrollArea->setWidgetResizable(true); - // 设置布局 auto layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(scrollArea); setLayout(layout); m_view->setAcceptDrops(true); - // 创建固定大小的表格 createRealTable(); - // 连接场景信号 connect(m_scene, &QGraphicsScene::selectionChanged, this, [this]() { auto items = m_scene->selectedItems(); @@ -73,14 +66,13 @@ PLCDocument::PLCDocument(QWidget *parent) } }); - // 连接场景变化信号 connect(m_scene, &QGraphicsScene::changed, this, &PLCDocument::handleSceneChanged); } PLCDocument::~PLCDocument() { qDeleteAll(m_connections); - clearTable(); + m_copiedItem = nullptr; } QString PLCDocument::title() const { @@ -89,9 +81,6 @@ QString PLCDocument::title() const { 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)); @@ -99,7 +88,6 @@ void PLCDocument::createRealTable() 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); @@ -109,7 +97,6 @@ void PLCDocument::createRealTable() 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); @@ -119,7 +106,6 @@ void PLCDocument::createRealTable() m_horizontalLines.append(line); } - // 添加行触点 for (int row = 0; row < m_rows; row++) { const int terminalSize = 15; @@ -131,7 +117,7 @@ void PLCDocument::createRealTable() terminal->setFlag(QGraphicsItem::ItemIsSelectable, true); terminal->setFlag(QGraphicsItem::ItemIsFocusable, true); terminal->setAcceptHoverEvents(true); - terminal->setVisible(false); + terminal->setVisible(true); m_scene->addItem(terminal); m_rowTerminals.append(terminal); @@ -139,35 +125,6 @@ void PLCDocument::createRealTable() } } -//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(); - -// // 清理行端子 -// for (auto terminal : m_rowTerminals) { -// m_scene->removeItem(terminal); -// delete terminal; -// } -// m_rowTerminals.clear(); -// m_rowTerminalMap.clear(); -//} - QPointF PLCDocument::snapToCellCenter(const QPointF& pos) const { int col = static_cast(std::floor(pos.x() / m_cellSize)); @@ -192,12 +149,6 @@ QPointF PLCDocument::constrainToTable(const QPointF &pos) const 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) { if (obj == m_view->viewport()) @@ -314,7 +265,6 @@ bool PLCDocument::eventFilter(QObject *obj, QEvent *event) if (mouseEvent->button() == Qt::LeftButton) { QPointF scenePos = m_view->mapToScene(mouseEvent->pos()); - // 检查是否点击到行触点 QGraphicsEllipseItem* clickedRowTerminal = nullptr; int rowTerminalIndex = -1; for (auto terminal : m_rowTerminals) { @@ -327,14 +277,11 @@ bool PLCDocument::eventFilter(QObject *obj, QEvent *event) } if (clickedRowTerminal) { - // 行触点被点击 if (m_connectionSourceType != ConnectionNone) { - // 已有连接起点,尝试作为终点 if (tryEndConnection(nullptr, RowTerminal)) { return true; } } else { - // 没有连接起点,开始新连接(行触点作为起点) startConnectionFromRowTerminal(rowTerminalIndex); return true; } @@ -435,7 +382,7 @@ bool PLCDocument::isTerminalUsed(PLCItem* item, TerminalType terminal) const { if (item->itemType() == PLCItem::Coil && terminal == RightTerminal) { - return true; // 这一行是关键修改 + return true; } for (ConnectionLine* line : m_connections) { if (line->startItem() == item && line->startTerminal() == terminal) { @@ -473,16 +420,13 @@ void PLCDocument::startConnection(PLCItem *startItem, TerminalType startTerminal bool PLCDocument::tryEndConnection(PLCItem *endItem, TerminalType endTerminal) { - // 情况1:行触点作为起点,连接到元件左端子 if (m_connectionSourceType == ConnectionFromRowTerminal) { - // 行触点只能连接元件的左端子 if (endTerminal != LeftTerminal) { QMessageBox::warning(this, "连线错误", "行触点只能连接到元件的左端子"); clearCurrentConnection(); return false; } - // 检查是否在同一行 int endRow = static_cast(endItem->y() / m_cellSize); if (endRow != m_connectionStartRow) { QMessageBox::warning(this, "连线错误", "行触点只能连接到同一行元件的左端子"); @@ -490,7 +434,6 @@ bool PLCDocument::tryEndConnection(PLCItem *endItem, TerminalType endTerminal) return false; } - // 检查端子是否已被使用 if (isTerminalUsed(endItem, LeftTerminal)) { QMessageBox::warning(this, "连线错误", "该元件的左端子已被使用"); clearCurrentConnection(); @@ -514,9 +457,7 @@ bool PLCDocument::tryEndConnection(PLCItem *endItem, TerminalType endTerminal) return true; } - // 情况2:元件作为起点,连接到行触点(终点) if (endTerminal == RowTerminal) { - // 行触点只能被元件的左端子连接 if (m_startTerminal != LeftTerminal) { QMessageBox::warning(this, "连线错误", "行触点只能连接到元件的左端子"); clearCurrentConnection(); @@ -541,7 +482,6 @@ bool PLCDocument::tryEndConnection(PLCItem *endItem, TerminalType endTerminal) return true; } - // 情况3:元件连接到元件(标准连接) if (m_connectionSourceType == ConnectionFromItem && m_connectionStartItem) { if (endItem == m_connectionStartItem && endTerminal == m_startTerminal) { clearCurrentConnection(); @@ -641,11 +581,9 @@ void PLCDocument::createPLCItem(const QString &type, const QPointF &pos) m_scene->addItem(item); setModified(true); - // 初始化端子状态 terminalConnections[item][LeftTerminal] = false; terminalConnections[item][RightTerminal] = false; - // 保存有效位置 m_lastValidPositions[item] = constrainedPos; connect(item, &PLCItem::stateChanged, this, [this, item](bool active) { @@ -712,8 +650,31 @@ bool PLCDocument::isCellOccupied(int col, int row, PLCItem* excludeItem) const return false; } +// 计算元件在连线上的位置比例(0-1) +qreal PLCDocument::getPositionRatioOnLine(PLCItem* item, ConnectionLine* line) const +{ + if (!item || !line) return -1; + + QLineF lineSeg = line->line(); + QPointF itemPos = item->pos(); + + // 计算投影比例 + qreal dx = lineSeg.dx(); + qreal dy = lineSeg.dy(); + qreal lengthSquared = dx*dx + dy*dy; + + if (lengthSquared == 0) return 0; + + qreal t = ((itemPos.x() - lineSeg.p1().x()) * dx + + (itemPos.y() - lineSeg.p1().y()) * dy) / lengthSquared; + + return qBound(0.0, t, 1.0); +} + void PLCDocument::handleItemPositionChange(PLCItem *item) { + if (!item) return; + if (m_loadingFile) { QPointF newPos = constrainToTable(item->pos()); item->setPos(newPos); @@ -732,27 +693,41 @@ void PLCDocument::handleItemPositionChange(PLCItem *item) if (isCellOccupied(newCol, newRow, item)) { - if (m_lastValidPositions.contains(item)) { + if (m_lastValidPositions.contains(item)) + { item->setPos(m_lastValidPositions[item]); QMessageBox::warning(this, "移动失败", "无法移动,该单元格已被占用"); } } else { - // 获取移动前的列位置 int oldCol = static_cast(m_lastValidPositions[item].x() / m_cellSize); - item->setPos(newPos); m_lastValidPositions[item] = newPos; - // 检查连接是否因移动而无效 if (newCol != oldCol) { removeInvalidConnectionsForItem(item); } - for (ConnectionLine *line : m_connections) - { - if (line->startItem() == item || line->endItem() == item) { + // 获取要拆分的连线并排序 + QList connectionsToSplit = findConnectionsUnderItem(item); + + // 按元件在连线上的位置排序,确保从左到右处理 + std::sort(connectionsToSplit.begin(), connectionsToSplit.end(), + [this, item](ConnectionLine* a, ConnectionLine* b) { + return getPositionRatioOnLine(item, a) < getPositionRatioOnLine(item, b); + }); + + // 处理每条连线 + for (ConnectionLine* line : connectionsToSplit) { + if (m_connections.contains(line) && !line->isProcessing()) { + splitConnection(line, item); + } + } + + // 更新所有连接位置 + for (ConnectionLine *line : m_connections) { + if (line) { line->updatePosition(); } } @@ -761,9 +736,160 @@ void PLCDocument::handleItemPositionChange(PLCItem *item) m_processingItems.remove(item); } +//QList PLCDocument::findConnectionsUnderItem(PLCItem* item) const +//{ +// QList result; +// if (!item) return result; + +// QRectF itemRect = item->boundingRect(); +// itemRect.adjust(-2, -2, 2, 2); +// QPolygonF itemPolygon = QPolygonF(itemRect).translated(item->pos()); + +// foreach (ConnectionLine* line, m_connections) { +// if (!line || line->isProcessing()) continue; + +// QLineF lineGeometry = line->line(); +// QRectF lineRect(lineGeometry.p1(), lineGeometry.p2()); +// lineRect.adjust(-10, -10, 10, 10); + +// if (itemPolygon.intersects(lineRect)) { +// result.append(line); +// } +// } +// return result; +//} + +// 在PLCDocument类的splitConnection函数中,修改逻辑以支持行触点连线的插入 +bool PLCDocument::splitConnection(ConnectionLine* originalLine, PLCItem* insertItem) +{ + if (!originalLine || !insertItem || originalLine->isProcessing()) { + return false; + } + + originalLine->setProcessing(true); + + PLCItem* originalStart = originalLine->startItem(); + TerminalType originalStartTerminal = originalLine->startTerminal(); + PLCItem* originalEnd = originalLine->endItem(); + TerminalType originalEndTerminal = originalLine->endTerminal(); + + // 保存原始行触点信息 + int sourceRow = originalLine->rowTerminalSourceRow(); + int targetRow = originalLine->rowTerminalTargetRow(); + + PLCItem* logicalInput = originalStart; + TerminalType inputTerminal = originalStartTerminal; + PLCItem* logicalOutput = originalEnd; + TerminalType outputTerminal = originalEndTerminal; + + // 标准化方向时同步交换行触点信息 + if ((originalStartTerminal == LeftTerminal && originalEndTerminal == RightTerminal) || + (originalStartTerminal == LeftTerminal && originalEndTerminal == RowTerminal)) + { + std::swap(logicalInput, logicalOutput); + std::swap(inputTerminal, outputTerminal); + std::swap(sourceRow, targetRow); // 同步交换行号 + } + + // 位置有效性校验 + int inputCol = (inputTerminal == RowTerminal) ? 0 : + static_cast(logicalInput->x() / m_cellSize); + + int outputCol = (outputTerminal == RowTerminal) ? 0 : + static_cast(logicalOutput->x() / m_cellSize); + + int insertCol = static_cast(insertItem->x() / m_cellSize); + + // 统一位置验证逻辑 + bool isBetween = (insertCol > qMin(inputCol, outputCol)) && + (insertCol < qMax(inputCol, outputCol)); + + if (!isBetween) { + originalLine->setProcessing(false); + return false; + } + + // 检查端子可用性 + if (isTerminalUsed(insertItem, LeftTerminal) || + isTerminalUsed(insertItem, RightTerminal)) + { + originalLine->setProcessing(false); + return false; + } + + // 创建新连线 + ConnectionLine* line1 = new ConnectionLine( + logicalInput, inputTerminal, + insertItem, LeftTerminal + ); + + // 保留行触点源信息 + if (inputTerminal == RowTerminal) { + line1->setStartTerminalPoint(QPointF(0, (sourceRow + 0.5) * m_cellSize)); + line1->setRowTerminalSourceRow(sourceRow); + } + + ConnectionLine* line2 = new ConnectionLine( + insertItem, RightTerminal, + logicalOutput, outputTerminal + ); + + // 保留行触点目标信息 + if (outputTerminal == RowTerminal) { + line2->setEndTerminalPoint(QPointF(0, (targetRow + 0.5) * m_cellSize)); + line2->setRowTerminalTargetRow(targetRow); + } + + // 添加新连线 + m_scene->addItem(line1); + m_scene->addItem(line2); + m_connections.append(line1); + m_connections.append(line2); + + // 更新端子状态 + terminalConnections[insertItem][LeftTerminal] = true; + terminalConnections[insertItem][RightTerminal] = true; + + // 移除原连线 + m_connections.removeAll(originalLine); + m_scene->removeItem(originalLine); + delete originalLine; + + setModified(true); + originalLine->setProcessing(false); + return true; +} + + +// 补充:确保行触点连线能被检测为"需要拆分的连线"(扩展findConnectionsUnderItem) +QList PLCDocument::findConnectionsUnderItem(PLCItem* item) const +{ + QList result; + if (!item) return result; + + // 元件的边界框(扩大检测范围,确保行触点连线也能被检测) + QRectF itemRect = item->boundingRect(); + itemRect.adjust(-5, -5, 5, 5); // 扩大边界框,提高检测灵敏度 + QPolygonF itemPolygon = QPolygonF(itemRect).translated(item->pos()); + + foreach (ConnectionLine* line, m_connections) { + if (!line || line->isProcessing()) continue; + + // 行触点连线的特殊处理:起点是(0, row),终点是元件左端子 + QLineF lineGeometry = line->line(); + QRectF lineRect = QRectF(lineGeometry.p1(), lineGeometry.p2()).normalized(); + lineRect.adjust(-10, -10, 10, 10); // 扩大连线的检测范围 + + // 检查元件是否与连线相交(包括行触点连线) + if (itemPolygon.intersects(lineRect)) { + result.append(line); + } + } + return result; +} + void PLCDocument::removeInvalidConnectionsForItem(PLCItem* movedItem) { - // 1. 基础防护 if (!movedItem || m_connections.isEmpty()) return; if (m_cellSize <= 0) { qWarning() << "Invalid cell size!"; @@ -774,15 +900,13 @@ void PLCDocument::removeInvalidConnectionsForItem(PLCItem* movedItem) QSet connectionsToRemove; bool shouldRemoveAll = false; - // 2. 第一遍遍历:检查位置条件 for (ConnectionLine* line : m_connections) { if (!line) continue; PLCItem* otherItem = nullptr; - TerminalType movedTerminal = NoTerminal; // 使用已定义的枚举值 + TerminalType movedTerminal = NoTerminal; TerminalType otherTerminal = NoTerminal; - // 确定连接的另一个元件 if (line->startItem() == movedItem) { otherItem = line->endItem(); movedTerminal = line->startTerminal(); @@ -792,22 +916,19 @@ void PLCDocument::removeInvalidConnectionsForItem(PLCItem* movedItem) movedTerminal = line->endTerminal(); otherTerminal = line->startTerminal(); } else { - continue; // 不是与移动元件相关的连接 + continue; } if (!otherItem) continue; int otherCol = static_cast(otherItem->x() / m_cellSize); - // 核心规则检查 - 使用连接线的端子类型,而不是元件的类型 if (movedTerminal == LeftTerminal && otherTerminal == RightTerminal) { - // 左端子移动到右端子元件左侧或同列 if (movedCol <= otherCol) { shouldRemoveAll = true; break; } } else if (movedTerminal == RightTerminal && otherTerminal == LeftTerminal) { - // 右端子移动到左端子元件右侧或同列 if (movedCol >= otherCol) { shouldRemoveAll = true; break; @@ -815,7 +936,6 @@ void PLCDocument::removeInvalidConnectionsForItem(PLCItem* movedItem) } } - // 3. 如果需要删除,收集所有相关连接 if (shouldRemoveAll) { for (ConnectionLine* line : m_connections) { if (!line) continue; @@ -825,11 +945,9 @@ void PLCDocument::removeInvalidConnectionsForItem(PLCItem* movedItem) } } - // 4. 执行删除 for (ConnectionLine* line : connectionsToRemove) { if (!line) continue; - // 从终端连接表移除 if (line->startItem()) { terminalConnections[line->startItem()].remove(line->startTerminal()); } @@ -837,17 +955,16 @@ void PLCDocument::removeInvalidConnectionsForItem(PLCItem* movedItem) terminalConnections[line->endItem()].remove(line->endTerminal()); } - // 从场景中移除 - if (m_scene && m_scene->items().contains(line)) { + if (m_scene && line->scene() == m_scene) { m_scene->removeItem(line); } - // 从连接列表中移除并删除对象 - m_connections.removeAll(line); + if (m_connections.contains(line)) { + m_connections.removeAll(line); + } delete line; } - // 5. 标记文档修改 if (!connectionsToRemove.isEmpty()) { setModified(true); } @@ -879,7 +996,6 @@ void PLCDocument::updateConnections() PLCItem *start = line->startItem(); PLCItem *end = line->endItem(); - // 行触点连接的线路特殊处理(总是激活) if (line->endTerminal() == RowTerminal || line->startTerminal() == RowTerminal) { line->setPen(QPen(Qt::green, 2)); continue; @@ -893,66 +1009,132 @@ void PLCDocument::updateConnections() } } -void PLCDocument::createContextMenu(QPoint globalPos) +void PLCDocument::deleteSelectedItems() { - 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("隐藏/显示网格"); - QList selected = m_scene->selectedItems(); + if (selected.isEmpty()) return; - QAction *rowTerminalAction = nullptr; - if (selected.size() == 1) { - if (PLCItem *item = dynamic_cast(selected.first())) { - int col = static_cast(item->x() / m_cellSize); - if (col == 0) { - rowTerminalAction = menu.addAction("连接到行触点"); - } + QList itemsToDelete; + QList linesToDelete; + QSet connectionsToDelete; + + for (QGraphicsItem* item : selected) { + if (PLCItem* plcItem = dynamic_cast(item)) { + itemsToDelete.append(plcItem); + } else if (ConnectionLine* line = dynamic_cast(item)) { + linesToDelete.append(line); } } - connect(runAction, &QAction::triggered, this, [this]() { - setSimulationRunning(true); - }); + for (ConnectionLine* line : m_connections) { + if (line->startItem() && itemsToDelete.contains(line->startItem())) { + connectionsToDelete.insert(line); + } + if (line->endItem() && itemsToDelete.contains(line->endItem())) { + connectionsToDelete.insert(line); + } + } - connect(stopAction, &QAction::triggered, this, [this]() { - setSimulationRunning(false); - }); + for (ConnectionLine* line : linesToDelete) { + connectionsToDelete.insert(line); + } - connect(resetAction, &QAction::triggered, this, &PLCDocument::resetSimulation); + for (ConnectionLine* line : connectionsToDelete) { + if (!line) continue; - connect(nameAction, &QAction::triggered, this, [this, selected]() { - 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); - } - } + if (line->startItem()) { + terminalConnections[line->startItem()].remove(line->startTerminal()); + } + if (line->endItem()) { + terminalConnections[line->endItem()].remove(line->endTerminal()); } - }); - 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); + if (m_scene && m_scene->items().contains(line)) { + m_scene->removeItem(line); } - }); + m_connections.removeAll(line); + delete line; + } + + for (PLCItem* item : itemsToDelete) { + terminalConnections.remove(item); + m_lastValidPositions.remove(item); + m_scene->removeItem(item); + delete item; + } + + setModified(true); +} + +void PLCDocument::copySelectedItem() +{ + QList selected = m_scene->selectedItems(); + if (selected.size() != 1) return; + + if (PLCItem* item = dynamic_cast(selected.first())) { + m_copiedItem = item; + } +} + +void PLCDocument::pasteItem() +{ + if (!m_copiedItem) return; + QPointF pastePos = snapToCellCenter(m_view->mapToScene(m_view->viewport()->mapFromGlobal(QCursor::pos()))); + pastePos.setX(pastePos.x() - 70); + pastePos.setY(pastePos.y() - 70); + + int col = static_cast(pastePos.x() / m_cellSize); + int row = static_cast(pastePos.y() / m_cellSize); + if (isCellOccupied(col, row, nullptr)) + { + QMessageBox::warning(this, "粘贴失败", "目标单元格已被占用"); + return; + } + + PLCItem* newItem = createItemByType(m_copiedItem->itemType()); + if (!newItem) return; + newItem->setPos(pastePos); + newItem->setName(m_copiedItem->name() + "_副本"); + m_scene->addItem(newItem); + + terminalConnections[newItem][LeftTerminal] = false; + terminalConnections[newItem][RightTerminal] = false; + m_lastValidPositions[newItem] = pastePos; + setModified(true); +} + +void PLCDocument::keyPressEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_Delete) + { + deleteSelectedItems(); + event->accept(); + return; + } + BaseDocument::keyPressEvent(event); +} + +void PLCDocument::createContextMenu(QPoint globalPos) +{ + QMenu menu(this); + + QAction* deleteAction = menu.addAction("删除"); + QAction* copyAction = menu.addAction("复制"); + QAction* pasteAction = menu.addAction("粘贴"); + + QList selected = m_scene->selectedItems(); + deleteAction->setEnabled(!selected.isEmpty()); + copyAction->setEnabled(selected.size() == 1&&dynamic_cast(selected.first())); + pasteAction->setEnabled(m_copiedItem != nullptr); + + connect(deleteAction, &QAction::triggered, this, &PLCDocument::deleteSelectedItems); + connect(copyAction, &QAction::triggered, this, &PLCDocument::copySelectedItem); + connect(pasteAction, &QAction::triggered, this, &PLCDocument::pasteItem); + + menu.addSeparator(); + + QAction *tableGridAction = menu.addAction("隐藏/显示网格"); connect(tableGridAction, &QAction::triggered, this, [this]() { for (auto line : m_horizontalLines) { @@ -964,11 +1146,7 @@ void PLCDocument::createContextMenu(QPoint globalPos) } }); - if (rowTerminalAction) { - connect(rowTerminalAction, &QAction::triggered, this, &PLCDocument::onRowTerminalConnection); - } - - menu.exec(globalPos+QPoint(10, 10)); + menu.exec(globalPos + QPoint(10, 10)); } void PLCDocument::onRowTerminalConnection() @@ -1060,7 +1238,6 @@ bool PLCDocument::saveToFile(const QString &filePath) for (ConnectionLine *line : m_connections) { QJsonObject connObj; - // 起点信息 if (line->startItem()) { int startCol = static_cast(line->startItem()->pos().x() / m_cellSize); int startRow = static_cast(line->startItem()->pos().y() / m_cellSize); @@ -1071,7 +1248,6 @@ bool PLCDocument::saveToFile(const QString &filePath) } connObj["startTerminal"] = static_cast(line->startTerminal()); - // 终点信息 connObj["endTerminal"] = static_cast(line->endTerminal()); if (line->endTerminal() == RowTerminal) { connObj["endRowTerminal"] = line->rowTerminalTargetRow(); @@ -1180,7 +1356,6 @@ bool PLCDocument::loadFromFile(const QString &filePath) m_activeItems.insert(item); } - // 初始化端子状态 terminalConnections[item][LeftTerminal] = false; terminalConnections[item][RightTerminal] = false; @@ -1204,9 +1379,7 @@ bool PLCDocument::loadFromFile(const QString &filePath) PLCItem *startItem = nullptr; PLCItem *endItem = nullptr; - // 处理起点 if (connObj.contains("startRowTerminal")) { - // 起点是行触点 int startRow = connObj["startRowTerminal"].toInt(); ConnectionLine *line = new ConnectionLine( nullptr, RowTerminal, @@ -1222,9 +1395,7 @@ bool PLCDocument::loadFromFile(const QString &filePath) int startRow = connObj["startRow"].toInt(); startItem = itemMap.value(qMakePair(startCol, startRow)); - // 处理终点 if (endTerminal == RowTerminal && connObj.contains("endRowTerminal")) { - // 终点是行触点 int endRow = connObj["endRowTerminal"].toInt(); ConnectionLine *line = new ConnectionLine( startItem, startTerminal, @@ -1246,13 +1417,13 @@ bool PLCDocument::loadFromFile(const QString &filePath) endItem, endTerminal ); m_scene->addItem(line); + m_connections.append(line); } } } } - // 更新连接状态 for (ConnectionLine* line : m_connections) { if (line->startItem()) { terminalConnections[line->startItem()][line->startTerminal()] = true; @@ -1280,7 +1451,11 @@ ConnectionLine::ConnectionLine( , m_startTerminal(startTerminal) , m_endItem(endItem) , m_endTerminal(endTerminal) + , m_rowTerminalTargetRow(-1) + , m_rowTerminalSourceRow(-1) + , m_processing(false) { + setFlag(QGraphicsItem::ItemIsSelectable, true); setPen(QPen(Qt::black, 1)); setZValue(-1); updatePosition(); diff --git a/untitled/plcdocument.h b/untitled/plcdocument.h index def7d95..bd4549b 100644 --- a/untitled/plcdocument.h +++ b/untitled/plcdocument.h @@ -18,7 +18,7 @@ #include const int DEFAULT_ROWS = 11; -const int DEFAULT_COLUMNS = 15; +const int DEFAULT_COLUMNS = 24; const int DEFAULT_CELL_SIZE = 70; class PLCItem; @@ -28,14 +28,16 @@ class PLCDocument : public BaseDocument { Q_OBJECT public: - enum TerminalType { + enum TerminalType + { NoTerminal = 0, LeftTerminal, RightTerminal, RowTerminal }; - enum ConnectionSourceType { + enum ConnectionSourceType + { ConnectionNone, // 无连接 ConnectionFromItem, // 起点是元件的端子 ConnectionFromRowTerminal // 起点是行触点 @@ -69,17 +71,24 @@ public: void setTitle(const QString &title) { m_title = title; } + void deleteSelectedItems(); + void copySelectedItem(); + void pasteItem(); + protected: bool eventFilter(QObject *obj, QEvent *event) override; - void resizeEvent(QResizeEvent *event) override; + void keyPressEvent(QKeyEvent* event) override; private slots: void handleSceneChanged(); void onRowTerminalConnection(); private: + QList findConnectionsUnderItem(PLCItem* item) const; + bool lineIntersectsRect(const QLineF& line, const QRectF& rect) const; + bool splitConnection(ConnectionLine* originalLine, PLCItem* insertItem); + bool isPointOnLineSegment(const QPointF& point, const QLineF& line) const; void createRealTable(); - //void clearTable(); TerminalType whichTerminal(const QPointF& scenePos, PLCItem* item) const; bool isTerminalUsed(PLCItem* item, TerminalType terminal) const; void handleItemPositionChange(PLCItem *item); @@ -90,6 +99,9 @@ private: void removeInvalidConnectionsForItem(PLCItem* movedItem); QList allPLCItems() const; + // 新增:获取元件在连线上的位置比例(0-1) + qreal getPositionRatioOnLine(PLCItem* item, ConnectionLine* line) const; + QGraphicsScene *m_scene; QGraphicsView *m_view; QString m_currentTool; @@ -114,6 +126,8 @@ private: QMap m_lastValidPositions; QSet m_processingItems; bool m_loadingFile = false; + + PLCItem* m_copiedItem = nullptr; }; class ConnectionLine : public QGraphicsLineItem @@ -138,6 +152,10 @@ public: void setStartTerminalPoint(const QPointF& point) { m_startTerminalPoint = point; } void setEndTerminalPoint(const QPointF& point) { m_endTerminalPoint = point; } + // 新增:设置这条连线是否正在被处理 + void setProcessing(bool processing) { m_processing = processing; } + bool isProcessing() const { return m_processing; } + private: PLCItem *m_startItem; PLCDocument::TerminalType m_startTerminal; @@ -147,6 +165,7 @@ private: QPointF m_endTerminalPoint; int m_rowTerminalTargetRow = -1; int m_rowTerminalSourceRow = -1; + bool m_processing = false; // 用于防止递归处理 }; #endif // PLCDOCUMENT_H