diff --git a/untitled/basedocument.h b/untitled/basedocument.h index d7f9e78..be3f4b5 100644 --- a/untitled/basedocument.h +++ b/untitled/basedocument.h @@ -23,10 +23,28 @@ public: virtual bool saveToFile(const QString &filePath) = 0; virtual bool loadFromFile(const QString &filePath) = 0; + + // 设置变换矩阵 + virtual void setTransform(const QTransform& transform) { + m_transform = transform; + update(); + } + + // 获取文档内容尺寸(子类需要重写) + virtual QSize getContentSize() const { return QSize(800, 600); } // 默认尺寸 + + // 重写paintEvent应用变换 +// virtual void paintEvent(QPaintEvent *event) override { +// QPainter painter(this); +// painter.setTransform(m_transform); +// // 子类应调用父类的paintEvent? 或者自行绘制 +// } + protected: DocumentType m_type; QString m_filePath; bool m_modified = false; + QTransform m_transform; }; #endif // BASEDOCUMENT_H diff --git a/untitled/hmidocument.cpp b/untitled/hmidocument.cpp index 09d4138..e02cba4 100644 --- a/untitled/hmidocument.cpp +++ b/untitled/hmidocument.cpp @@ -29,22 +29,38 @@ HMIDocument::HMIDocument(QWidget *parent) m_canDrawRectangle(false), m_lastPastePos(0, 0) { - // 创建绘图场景 + // 创建绘图场景 - 固定大小 m_scene = new QGraphicsScene(this); - m_scene->setSceneRect(0, 0, 800, 600); + m_scene->setSceneRect(0, 0, 1000, 600); // 固定画布尺寸 m_scene->setBackgroundBrush(QBrush(Qt::lightGray)); - // 创建视图 + // 创建视图 - 关键修改:使用滚动区域包装 + m_scrollArea = new QScrollArea(this); + m_scrollArea->setWidgetResizable(false); // 禁止内容自动调整大小 + m_scrollArea->setFrameShape(QFrame::NoFrame); // 无边框 + + // 创建真正的视图部件 m_view = new QGraphicsView(m_scene, this); m_view->setRenderHint(QPainter::Antialiasing); m_view->setDragMode(QGraphicsView::RubberBandDrag); m_view->setAcceptDrops(true); + m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // 隐藏自身滚动条 + m_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + // 关键:视图固定大小与场景匹配 + m_view->setMinimumSize(1000, 600); + m_view->setMaximumSize(1000, 600); + m_view->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_view->viewport()->installEventFilter(this); - // 布局(让视图占满文档区域) + // 添加视图到滚动区域 + m_scrollArea->setWidget(m_view); + m_scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + // 设置主布局 auto layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); - layout->addWidget(m_view); + layout->addWidget(m_scrollArea); setLayout(layout); } diff --git a/untitled/hmidocument.h b/untitled/hmidocument.h index edb698b..10f9ea8 100644 --- a/untitled/hmidocument.h +++ b/untitled/hmidocument.h @@ -40,14 +40,12 @@ public: bool loadFromFile(const QString &filePath) override; void markModified(); - signals: void titleChanged(const QString &title); protected: bool eventFilter(QObject *obj, QEvent *event) override; - -private: +private: // 辅助方法 void createShape(const QString& type, const QPointF &pos); void resetDrawFlags(); @@ -63,6 +61,7 @@ private: QString m_title; QGraphicsScene *m_scene; QGraphicsView *m_view; + QScrollArea *m_scrollArea; bool m_canDrawEllipse; bool m_canDrawRectangle; diff --git a/untitled/mainwindow.cpp b/untitled/mainwindow.cpp index bdaaaf5..513f5c8 100644 --- a/untitled/mainwindow.cpp +++ b/untitled/mainwindow.cpp @@ -17,7 +17,7 @@ #include #include #include - +#include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { @@ -32,11 +32,64 @@ MainWindow::MainWindow(QWidget *parent) connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &MainWindow::onCloseTab); createMenus();// 创建菜单和工具栏 createToolbars(); + createLogPanel(); // 创建日志面板 } MainWindow::~MainWindow() { } +void MainWindow::createLogPanel() +{ + // 创建可停靠窗口 + m_logDock = new QDockWidget("消息输出", this); + m_logDock->setAllowedAreas(Qt::BottomDockWidgetArea); + m_logDock->setFeatures(QDockWidget::DockWidgetClosable | + QDockWidget::DockWidgetMovable | + QDockWidget::DockWidgetFloatable); // 添加垂直标题栏 + + // 设置高度范围而非固定高度 + m_logDock->setMinimumHeight(150); + m_logDock->setMaximumHeight(400); + + // 创建主内容部件 + QWidget* logWidget = new QWidget; + QVBoxLayout* logLayout = new QVBoxLayout; + logLayout->setContentsMargins(2, 2, 2, 2); + + // 创建文本编辑框用于显示日志 + m_logEdit = new QTextEdit; + m_logEdit->setReadOnly(true); + m_logEdit->setFont(QFont("Consolas", 10)); + m_logEdit->setStyleSheet("background-color: #f8f8f8; border: 1px solid #d9d9d9;"); + m_logEdit->setMinimumHeight(100); + + // 创建控制按钮 + QHBoxLayout* btnLayout = new QHBoxLayout; + m_clearLogBtn = new QPushButton("清空日志"); + + btnLayout->addWidget(m_clearLogBtn); + btnLayout->addStretch(); + + // 组合布局 + logLayout->addLayout(btnLayout); + logLayout->addWidget(m_logEdit); + logWidget->setLayout(logLayout); + + m_logDock->setWidget(logWidget); + addDockWidget(Qt::BottomDockWidgetArea, m_logDock); + + // 初始显示状态 + m_logDock->show(); + + // 连接信号 + connect(m_clearLogBtn, &QPushButton::clicked, this, &MainWindow::onClearLogButtonClicked); +} +// 槽函数实现 +void MainWindow::onClearLogButtonClicked() +{ + m_logEdit->clear(); + m_logEdit->append("日志已清空"); +} // 创建菜单栏 void MainWindow::createMenus() diff --git a/untitled/mainwindow.h b/untitled/mainwindow.h index e690372..88d5959 100644 --- a/untitled/mainwindow.h +++ b/untitled/mainwindow.h @@ -7,7 +7,7 @@ #include #include #include "basedocument.h" - +#include QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE @@ -31,9 +31,14 @@ private slots: void onSaveAs(); // 另存为文档 void onOpen(); // 打开文档 void onCloseTab(int index); // 关闭标签页 + void onClearLogButtonClicked(); private: void createMenus(); // 创建菜单栏 + QDockWidget* m_logDock; + QTextEdit* m_logEdit; + QPushButton* m_clearLogBtn; + void createLogPanel(); void createToolbars(); // 创建工具栏(左侧) void updateToolBar(BaseDocument *doc); // 根据文档类型更新工具栏 void saveDocument(BaseDocument *doc); // 保存文档 diff --git a/untitled/plcdocument.cpp b/untitled/plcdocument.cpp index 31a64f7..e879d9e 100644 --- a/untitled/plcdocument.cpp +++ b/untitled/plcdocument.cpp @@ -1,4 +1,3 @@ -// PLCDocument.cpp 的完整修改版 #include "plcdocument.h" #include "plcitems.h" #include @@ -24,19 +23,16 @@ #include #include #include -#include #include - -// 固定表格尺寸的常量 -const int DEFAULT_ROWS = 20; -const int DEFAULT_COLUMNS = 25; -const int DEFAULT_CELL_SIZE = 50; +#include +#include +#include PLCDocument::PLCDocument(QWidget *parent) : BaseDocument(PLC, parent) , m_rows(DEFAULT_ROWS) , m_columns(DEFAULT_COLUMNS) - ,m_cellSize(DEFAULT_CELL_SIZE) + , m_cellSize(DEFAULT_CELL_SIZE) { // 创建绘图场景 m_scene = new QGraphicsScene(this); @@ -51,33 +47,33 @@ PLCDocument::PLCDocument(QWidget *parent) 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); + // 创建滚动区域 + 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(m_view); + layout->addWidget(scrollArea); setLayout(layout); + m_view->setAcceptDrops(true); // 创建固定大小的表格 createRealTable(); // 连接场景信号 - connect(m_scene, &QGraphicsScene::selectionChanged, this, [this]() { + 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); - } + if (items.size() == 1 && dynamic_cast(items.first())) + { + clearCurrentConnection(); } }); - // 连接场景变化信号(用于更新连接线位置) + // 连接场景变化信号 connect(m_scene, &QGraphicsScene::changed, this, &PLCDocument::handleSceneChanged); } @@ -93,17 +89,17 @@ QString PLCDocument::title() const { void PLCDocument::createRealTable() { - clearTable(); + //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->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); @@ -113,7 +109,7 @@ 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); @@ -122,37 +118,61 @@ void PLCDocument::createRealTable() 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 (int row = 0; row < m_rows; row++) + { + const int terminalSize = 15; + QPointF terminalPos(0, row * m_cellSize + m_cellSize / 2.0 - terminalSize / 2); - for (auto line : m_horizontalLines) { - m_scene->removeItem(line); - delete line; - } - m_horizontalLines.clear(); + QGraphicsEllipseItem* terminal = new QGraphicsEllipseItem( + terminalPos.x(), terminalPos.y(), terminalSize, terminalSize); - for (auto line : m_verticalLines) { - m_scene->removeItem(line); - delete line; + terminal->setFlag(QGraphicsItem::ItemIsSelectable, true); + terminal->setFlag(QGraphicsItem::ItemIsFocusable, true); + terminal->setAcceptHoverEvents(true); + terminal->setVisible(false); + + m_scene->addItem(terminal); + m_rowTerminals.append(terminal); + m_rowTerminalMap[terminal] = row; } - m_verticalLines.clear(); } +//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 { - // 计算所在单元格列号(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); @@ -175,16 +195,13 @@ QPointF PLCDocument::constrainToTable(const QPointF &pos) const 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()) { @@ -192,7 +209,6 @@ bool PLCDocument::eventFilter(QObject *obj, QEvent *event) return true; } } - // 处理拖拽移动事件 else if (event->type() == QEvent::DragMove) { QDragMoveEvent *dragEvent = static_cast(event); if(dragEvent->mimeData()->hasText()) { @@ -200,22 +216,20 @@ bool PLCDocument::eventFilter(QObject *obj, QEvent *event) 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); // 简单的行列唯一键 + occupiedCells.insert(col * 1000 + row); } } @@ -223,10 +237,8 @@ bool PLCDocument::eventFilter(QObject *obj, QEvent *event) 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) { @@ -240,7 +252,6 @@ bool PLCDocument::eventFilter(QObject *obj, QEvent *event) } } if (!found) { - // 尝试向下移动 for (int offset = 1; offset < m_rows; offset++) { int newRow = row + offset; if (newRow < m_rows) { @@ -256,7 +267,6 @@ bool PLCDocument::eventFilter(QObject *obj, QEvent *event) } } - // 创建PLC控件 QString toolType = mimeData->text(); createPLCItem(toolType, cellCenter); m_currentTool.clear(); @@ -264,46 +274,83 @@ bool PLCDocument::eventFilter(QObject *obj, QEvent *event) 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); + QPointF cellCenter = snapToCellCenter(scenePos); + + if (m_connectionSourceType != ConnectionNone) { + if (m_connectionSourceType == ConnectionFromItem && m_connectionStartItem) { + if (m_startTerminal == LeftTerminal) { + QPointF startPos = m_connectionStartItem->leftTerminal(); + QLineF line(startPos, cellCenter); + m_tempLine->setLine(line); + } else if (m_startTerminal == RightTerminal) { + QPointF startPos = m_connectionStartItem->rightTerminal(); + QLineF line(startPos, cellCenter); + m_tempLine->setLine(line); + } + } else if (m_connectionSourceType == ConnectionFromRowTerminal) { + QPointF startPos(0, (m_connectionStartRow + 0.5) * m_cellSize); + QLineF line(startPos, 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); // 单元格中心 - // 使用当前工具创建控件 + // 检查是否点击到行触点 + QGraphicsEllipseItem* clickedRowTerminal = nullptr; + int rowTerminalIndex = -1; + for (auto terminal : m_rowTerminals) { + QPointF localPos = terminal->mapFromScene(scenePos); + if (terminal->contains(localPos)) { + clickedRowTerminal = terminal; + rowTerminalIndex = m_rowTerminalMap[terminal]; + break; + } + } + + if (clickedRowTerminal) { + // 行触点被点击 + if (m_connectionSourceType != ConnectionNone) { + // 已有连接起点,尝试作为终点 + if (tryEndConnection(nullptr, RowTerminal)) { + return true; + } + } else { + // 没有连接起点,开始新连接(行触点作为起点) + startConnectionFromRowTerminal(rowTerminalIndex); + return true; + } + } + + QPointF cellCenter = snapToCellCenter(scenePos); + QGraphicsItem *clickedItem = m_scene->itemAt(cellCenter, m_view->transform()); + PLCItem *plcItem = dynamic_cast(clickedItem); + 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); // 简单的行列唯一键 + if (PLCItem* existingItem = dynamic_cast(item)) { + int col = static_cast(existingItem->x() / m_cellSize); + int row = static_cast(existingItem->y() / m_cellSize); + occupiedCells.insert(col * 1000 + row); } } @@ -311,36 +358,259 @@ bool PLCDocument::eventFilter(QObject *obj, QEvent *event) int row = static_cast(cellCenter.y() / m_cellSize); int cellKey = col * 1000 + row; - // 如果单元格被占用,显示警告 - if (occupiedCells.contains(cellKey)) { + if (!occupiedCells.contains(cellKey)) { + createPLCItem(m_currentTool, cellCenter); + m_currentTool.clear(); + return true; + } else { 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; + if (plcItem) { + TerminalType clickedTerminal = whichTerminal(scenePos, plcItem); + + if (m_connectionSourceType != ConnectionNone) { + if (clickedTerminal != NoTerminal) { + if (tryEndConnection(plcItem, clickedTerminal)) { + return true; + } + } + } else { + if (clickedTerminal != NoTerminal && !isTerminalUsed(plcItem, clickedTerminal)) + { + startConnection(plcItem, clickedTerminal); + 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::startConnectionFromRowTerminal(int row) +{ + clearCurrentConnection(); + + m_connectionSourceType = ConnectionFromRowTerminal; + m_connectionStartRow = row; + + m_tempLine = new QGraphicsLineItem; + m_tempLine->setPen(QPen(Qt::blue, 2, Qt::DashLine)); + + QPointF startPos(0, (row + 0.5) * m_cellSize); + m_tempLine->setLine(QLineF(startPos, startPos)); + m_scene->addItem(m_tempLine); +} + +PLCDocument::TerminalType PLCDocument::whichTerminal(const QPointF& scenePos, PLCItem* item) const +{ + const qreal terminalRadius = 20.0; + + QPointF leftTerminal = item->leftTerminal(); + qreal dxLeft = scenePos.x() - leftTerminal.x(); + qreal dyLeft = scenePos.y() - leftTerminal.y(); + qreal distLeft = std::sqrt(dxLeft*dxLeft + dyLeft*dyLeft); + + if (distLeft <= terminalRadius) { + return LeftTerminal; + } + + QPointF rightTerminal = item->rightTerminal(); + qreal dxRight = scenePos.x() - rightTerminal.x(); + qreal dyRight = scenePos.y() - rightTerminal.y(); + qreal distRight = std::sqrt(dxRight*dxRight + dyRight*dyRight); + + if (distRight <= terminalRadius) { + return RightTerminal; + } + + return NoTerminal; +} + +bool PLCDocument::isTerminalUsed(PLCItem* item, TerminalType terminal) const +{ + if (item->itemType() == PLCItem::Coil && terminal == RightTerminal) + { + return true; // 这一行是关键修改 + } + for (ConnectionLine* line : m_connections) { + if (line->startItem() == item && line->startTerminal() == terminal) { + return true; + } + if (line->endItem() == item && line->endTerminal() == terminal) { + return true; + } + } + + return false; +} + +void PLCDocument::startConnection(PLCItem *startItem, TerminalType startTerminal) +{ + clearCurrentConnection(); + + m_connectionSourceType = ConnectionFromItem; + m_connectionStartItem = startItem; + m_startTerminal = startTerminal; + + m_tempLine = new QGraphicsLineItem; + m_tempLine->setPen(QPen(Qt::blue, 2, Qt::DashLine)); + + QPointF startPos; + if (startTerminal == LeftTerminal) { + startPos = startItem->leftTerminal(); + } else if (startTerminal == RightTerminal) { + startPos = startItem->rightTerminal(); + } + + m_tempLine->setLine(QLineF(startPos, startPos)); + m_scene->addItem(m_tempLine); +} + +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, "连线错误", "行触点只能连接到同一行元件的左端子"); + clearCurrentConnection(); + return false; + } + + // 检查端子是否已被使用 + if (isTerminalUsed(endItem, LeftTerminal)) { + QMessageBox::warning(this, "连线错误", "该元件的左端子已被使用"); + clearCurrentConnection(); + return false; + } + + ConnectionLine *connection = new ConnectionLine( + nullptr, RowTerminal, + endItem, LeftTerminal + ); + connection->setStartTerminalPoint(QPointF(0, (m_connectionStartRow + 0.5) * m_cellSize)); + connection->setRowTerminalSourceRow(m_connectionStartRow); + + m_scene->addItem(connection); + m_connections.append(connection); + + terminalConnections[endItem][LeftTerminal] = true; + + clearCurrentConnection(); + setModified(true); + return true; + } + + // 情况2:元件作为起点,连接到行触点(终点) + if (endTerminal == RowTerminal) { + // 行触点只能被元件的左端子连接 + if (m_startTerminal != LeftTerminal) { + QMessageBox::warning(this, "连线错误", "行触点只能连接到元件的左端子"); + clearCurrentConnection(); + return false; + } + + int startRow = static_cast(m_connectionStartItem->y() / m_cellSize); + ConnectionLine *connection = new ConnectionLine( + m_connectionStartItem, m_startTerminal, + nullptr, RowTerminal + ); + connection->setEndTerminalPoint(QPointF(0, (startRow + 0.5) * m_cellSize)); + connection->setRowTerminalTargetRow(startRow); + + m_scene->addItem(connection); + m_connections.append(connection); + + terminalConnections[m_connectionStartItem][m_startTerminal] = true; + + clearCurrentConnection(); + setModified(true); + return true; + } + + // 情况3:元件连接到元件(标准连接) + if (m_connectionSourceType == ConnectionFromItem && m_connectionStartItem) { + if (endItem == m_connectionStartItem && endTerminal == m_startTerminal) { + clearCurrentConnection(); + return false; + } + + int startCol = static_cast(m_connectionStartItem->pos().x() / m_cellSize); + int endCol = static_cast(endItem->pos().x() / m_cellSize); + + if (m_startTerminal == LeftTerminal) { + if (endTerminal != RightTerminal) { + QMessageBox::warning(this, "连线错误", "左端子只能连接到其他元件的右端子"); + clearCurrentConnection(); + return false; + } + if (startCol <= endCol) { + QMessageBox::warning(this, "连线错误", "左端子只能连接到左边元件的右端子"); + clearCurrentConnection(); + return false; + } + } else if (m_startTerminal == RightTerminal) { + if (endTerminal != LeftTerminal) { + QMessageBox::warning(this, "连线错误", "右端子只能连接到其他元件的左端子"); + clearCurrentConnection(); + return false; + } + if (startCol >= endCol) { + QMessageBox::warning(this, "连线错误", "右端子只能连接到右边元件的左端子"); + clearCurrentConnection(); + return false; } } + + if (isTerminalUsed(endItem, endTerminal)) { + QMessageBox::warning(this, "连线错误", "该端子已被使用"); + clearCurrentConnection(); + return false; + } + + ConnectionLine *connection = new ConnectionLine( + m_connectionStartItem, m_startTerminal, + endItem, endTerminal + ); + + m_scene->addItem(connection); + m_connections.append(connection); + + terminalConnections[m_connectionStartItem][m_startTerminal] = true; + terminalConnections[endItem][endTerminal] = true; + + clearCurrentConnection(); + setModified(true); + return true; } - return QWidget::eventFilter(obj, event); + return false; +} + +void PLCDocument::clearCurrentConnection() +{ + if (m_tempLine) { + m_scene->removeItem(m_tempLine); + delete m_tempLine; + m_tempLine = nullptr; + } + m_connectionSourceType = ConnectionNone; + m_connectionStartItem = nullptr; + m_startTerminal = NoTerminal; + m_connectionStartRow = -1; } void PLCDocument::createPLCItem(const QString &type, const QPointF &pos) @@ -366,13 +636,18 @@ void PLCDocument::createPLCItem(const QString &type, const QPointF &pos) } if (item) { - // 确保位置在表格内并居中 QPointF constrainedPos = constrainToTable(pos); item->setPos(constrainedPos); 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) { if (m_simulationRunning) { if (active) { @@ -388,80 +663,193 @@ void PLCDocument::createPLCItem(const QString &type, const QPointF &pos) void PLCDocument::handleSceneChanged() { - // 遍历所有项目,处理位置变化 + for (auto* connection : qAsConst(m_connections)) + { + connection->updatePosition(); + } + + if (m_tempLine && m_connectionSourceType != ConnectionNone) + { + QPointF cursorPos = m_view->mapFromGlobal(QCursor::pos()); + QPointF scenePos = m_view->mapToScene(cursorPos.toPoint()); + QLineF line = m_tempLine->line(); + line.setP2(scenePos); + m_tempLine->setLine(line); + } + + for (QGraphicsItem *item : m_scene->items()) + { + if (PLCItem *plcItem = dynamic_cast(item)) + { + handleItemPositionChange(plcItem); + } + } +} + +QList PLCDocument::allPLCItems() const +{ + QList items; for (QGraphicsItem *item : m_scene->items()) { if (PLCItem *plcItem = dynamic_cast(item)) { - handleItemPositionChange(plcItem); + items.append(plcItem); } } + return items; +} + +bool PLCDocument::isCellOccupied(int col, int row, PLCItem* excludeItem) const +{ + for (PLCItem* item : allPLCItems()) { + if (item == excludeItem) continue; + + int itemCol = static_cast(item->x() / m_cellSize); + int itemRow = static_cast(item->y() / m_cellSize); + + if (itemCol == col && itemRow == row) { + return true; + } + } + return false; } void PLCDocument::handleItemPositionChange(PLCItem *item) { - // 限制项目位置在表格内 - QPointF newPos = constrainToTable(item->pos()); - if (newPos != item->pos()) { + if (m_loadingFile) { + QPointF newPos = constrainToTable(item->pos()); item->setPos(newPos); + m_lastValidPositions[item] = newPos; + return; } - // 更新所有与该项目相关的连接 - for (ConnectionLine *line : m_connections) { - if (line->startItem() == item || line->endItem() == item) { - line->updatePosition(); + if (m_processingItems.contains(item)) { + return; + } + m_processingItems.insert(item); + + QPointF newPos = constrainToTable(item->pos()); + int newCol = static_cast(newPos.x() / m_cellSize); + int newRow = static_cast(newPos.y() / m_cellSize); + + if (isCellOccupied(newCol, newRow, 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); -void PLCDocument::startConnection(PLCItem *startItem) -{ - if (m_connectionStart) { - clearCurrentConnection(); + item->setPos(newPos); + m_lastValidPositions[item] = newPos; + + // 检查连接是否因移动而无效 + if (newCol != oldCol) { + removeInvalidConnectionsForItem(item); + } + + for (ConnectionLine *line : m_connections) + { + if (line->startItem() == item || line->endItem() == item) { + line->updatePosition(); + } + } } - 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); + m_processingItems.remove(item); } -void PLCDocument::endConnection(PLCItem *endItem) +void PLCDocument::removeInvalidConnectionsForItem(PLCItem* movedItem) { - if (!m_connectionStart || m_connectionStart == endItem) { - clearCurrentConnection(); + // 1. 基础防护 + if (!movedItem || m_connections.isEmpty()) return; + if (m_cellSize <= 0) { + qWarning() << "Invalid cell size!"; return; } - // 创建永久连接线 - ConnectionLine *connection = new ConnectionLine(m_connectionStart, endItem); - m_scene->addItem(connection); - m_connections.append(connection); + int movedCol = static_cast(movedItem->x() / m_cellSize); + QSet connectionsToRemove; + bool shouldRemoveAll = false; + + // 2. 第一遍遍历:检查位置条件 + for (ConnectionLine* line : m_connections) { + if (!line) continue; + + PLCItem* otherItem = nullptr; + TerminalType movedTerminal = NoTerminal; // 使用已定义的枚举值 + TerminalType otherTerminal = NoTerminal; + + // 确定连接的另一个元件 + if (line->startItem() == movedItem) { + otherItem = line->endItem(); + movedTerminal = line->startTerminal(); + otherTerminal = line->endTerminal(); + } else if (line->endItem() == movedItem) { + otherItem = line->startItem(); + movedTerminal = line->endTerminal(); + otherTerminal = line->startTerminal(); + } else { + continue; // 不是与移动元件相关的连接 + } - clearCurrentConnection(); - setModified(true); -} + if (!otherItem) continue; -void PLCDocument::clearCurrentConnection() -{ - if (m_tempLine) { - m_scene->removeItem(m_tempLine); - delete m_tempLine; - m_tempLine = nullptr; + 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; + } + } } - m_connectionStart = nullptr; -} -void PLCDocument::updateConnections() -{ - for (ConnectionLine *line : m_connections) { - PLCItem *start = line->startItem(); - PLCItem *end = line->endItem(); + // 3. 如果需要删除,收集所有相关连接 + if (shouldRemoveAll) { + for (ConnectionLine* line : m_connections) { + if (!line) continue; + if (line->startItem() == movedItem || line->endItem() == movedItem) { + connectionsToRemove.insert(line); + } + } + } - if (m_activeItems.contains(start) && m_activeItems.contains(end)) { - line->setPen(QPen(Qt::green, 2)); - } else { - line->setPen(QPen(Qt::black, 1)); + // 4. 执行删除 + for (ConnectionLine* line : connectionsToRemove) { + if (!line) continue; + + // 从终端连接表移除 + if (line->startItem()) { + terminalConnections[line->startItem()].remove(line->startTerminal()); + } + if (line->endItem()) { + terminalConnections[line->endItem()].remove(line->endTerminal()); + } + + // 从场景中移除 + if (m_scene && m_scene->items().contains(line)) { + m_scene->removeItem(line); } + + // 从连接列表中移除并删除对象 + m_connections.removeAll(line); + delete line; + } + + // 5. 标记文档修改 + if (!connectionsToRemove.isEmpty()) { + setModified(true); } } @@ -481,9 +869,30 @@ void PLCDocument::resetSimulation() plcItem->setState(false); } } + terminalConnections.clear(); updateConnections(); } +void PLCDocument::updateConnections() +{ + for (ConnectionLine *line : m_connections) { + PLCItem *start = line->startItem(); + PLCItem *end = line->endItem(); + + // 行触点连接的线路特殊处理(总是激活) + if (line->endTerminal() == RowTerminal || line->startTerminal() == RowTerminal) { + line->setPen(QPen(Qt::green, 2)); + continue; + } + + if (m_activeItems.contains(start) && m_activeItems.contains(end)) { + line->setPen(QPen(Qt::green, 2)); + } else { + line->setPen(QPen(Qt::black, 1)); + } + } +} + void PLCDocument::createContextMenu(QPoint globalPos) { QMenu menu(this); @@ -497,6 +906,18 @@ void PLCDocument::createContextMenu(QPoint globalPos) QAction *tableSizeAction = menu.addAction("调整表格尺寸"); QAction *tableGridAction = menu.addAction("隐藏/显示网格"); + QList selected = m_scene->selectedItems(); + + 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("连接到行触点"); + } + } + } + connect(runAction, &QAction::triggered, this, [this]() { setSimulationRunning(true); }); @@ -507,8 +928,7 @@ void PLCDocument::createContextMenu(QPoint globalPos) connect(resetAction, &QAction::triggered, this, &PLCDocument::resetSimulation); - connect(nameAction, &QAction::triggered, this, [this]() { - QList selected = m_scene->selectedItems(); + connect(nameAction, &QAction::triggered, this, [this, selected]() { if (selected.size() == 1) { if (PLCItem *item = dynamic_cast(selected.first())) { QString name = QInputDialog::getText(this, "重命名", "输入名称:", @@ -534,15 +954,75 @@ void PLCDocument::createContextMenu(QPoint globalPos) }); connect(tableGridAction, &QAction::triggered, this, [this]() { - for (auto line : m_horizontalLines) { + for (auto line : m_horizontalLines) + { line->setVisible(!line->isVisible()); } - for (auto line : m_verticalLines) { + for (auto line : m_verticalLines) + { line->setVisible(!line->isVisible()); } }); - menu.exec(globalPos); + if (rowTerminalAction) { + connect(rowTerminalAction, &QAction::triggered, this, &PLCDocument::onRowTerminalConnection); + } + + menu.exec(globalPos+QPoint(10, 10)); +} + +void PLCDocument::onRowTerminalConnection() +{ + QList selected = m_scene->selectedItems(); + if (selected.size() != 1) return; + + PLCItem* item = dynamic_cast(selected.first()); + if (!item) return; + + int itemRow = static_cast(item->y() / m_cellSize); + int itemCol = static_cast(item->x() / m_cellSize); + if (itemCol != 0) { + QMessageBox::warning(this, "连接错误", "只有最左侧列的元件可以连接到行触点"); + return; + } + + if (isTerminalUsed(item, LeftTerminal)) { + QMessageBox::warning(this, "连接失败", "该元件的左端子已被使用"); + return; + } + + ConnectionLine* connection = new ConnectionLine( + item, LeftTerminal, + nullptr, RowTerminal + ); + + connection->setRowTerminalTargetRow(itemRow); + connection->setEndTerminalPoint(QPointF(0, (itemRow + 0.5) * m_cellSize)); + connection->updatePosition(); + + m_scene->addItem(connection); + m_connections.append(connection); + + terminalConnections[item][LeftTerminal] = true; + + setModified(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; + } } bool PLCDocument::saveToFile(const QString &filePath) @@ -553,55 +1033,63 @@ bool PLCDocument::saveToFile(const QString &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); - } + QJsonArray itemsArray; + for (QGraphicsItem *item : m_scene->items()) { + if (PLCItem *plcItem = dynamic_cast(item)) { + QJsonObject itemObj; + itemObj["type"] = static_cast(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); + } + docObject["items"] = itemsArray; + + QJsonArray connectionsArray; + 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); connObj["startCol"] = startCol; connObj["startRow"] = startRow; + } else if (line->startTerminal() == RowTerminal) { + connObj["startRowTerminal"] = line->rowTerminalSourceRow(); + } + connObj["startTerminal"] = static_cast(line->startTerminal()); + + // 终点信息 + connObj["endTerminal"] = static_cast(line->endTerminal()); + if (line->endTerminal() == RowTerminal) { + connObj["endRowTerminal"] = line->rowTerminalTargetRow(); + } else if (line->endItem()) { + int endCol = static_cast(line->endItem()->pos().x() / m_cellSize); + int endRow = static_cast(line->endItem()->pos().y() / m_cellSize); connObj["endCol"] = endCol; connObj["endRow"] = endRow; - connectionsArray.append(connObj); } - docObject["connections"] = connectionsArray; - // 写入文件 + connectionsArray.append(connObj); + } + docObject["connections"] = connectionsArray; + QJsonDocument doc(docObject); file.write(doc.toJson()); file.close(); - // 更新文档状态 setFilePath(filePath); setModified(false); return true; @@ -615,7 +1103,6 @@ bool PLCDocument::loadFromFile(const QString &filePath) return false; } - // 读取并解析JSON QByteArray data = file.readAll(); file.close(); @@ -631,25 +1118,20 @@ bool PLCDocument::loadFromFile(const QString &filePath) 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); // 防止在加载过程中发送变化信号 + m_scene->blockSignals(true); - // 清理现有内容 - 更安全的方式 for (auto conn : m_connections) { m_scene->removeItem(conn); delete conn; @@ -658,7 +1140,6 @@ bool PLCDocument::loadFromFile(const QString &filePath) QList allItems = m_scene->items(); for (QGraphicsItem* item : allItems) { - // 保留表格项,只删除PLC元件 if (dynamic_cast(item) || dynamic_cast(item)) { m_scene->removeItem(item); delete item; @@ -666,15 +1147,12 @@ bool PLCDocument::loadFromFile(const QString &filePath) } m_activeItems.clear(); - m_connectionStart = nullptr; - delete m_tempLine; - m_tempLine = nullptr; + terminalConnections.clear(); + clearCurrentConnection(); - // 创建表格 - createRealTable(); // 重新创建表格以确保正确 + createRealTable(); - // 加载PLC元件 - QMap, PLCItem*> itemMap; // (列, 行) -> PLCItem + QMap, PLCItem*> itemMap; QJsonArray itemsArray = docObject["items"].toArray(); for (const QJsonValue &itemValue : itemsArray) { QJsonObject itemObj = itemValue.toObject(); @@ -684,7 +1162,6 @@ bool PLCDocument::loadFromFile(const QString &filePath) QString name = itemObj["name"].toString(); bool active = itemObj["active"].toBool(); - // 确保位置在表格范围内 col = qBound(0, col, m_columns - 1); row = qBound(0, row, m_rows - 1); @@ -697,13 +1174,16 @@ bool PLCDocument::loadFromFile(const QString &filePath) item->setState(active); m_scene->addItem(item); - // 添加到映射表 itemMap.insert(qMakePair(col, row), item); if (active) { m_activeItems.insert(item); } + // 初始化端子状态 + terminalConnections[item][LeftTerminal] = false; + terminalConnections[item][RightTerminal] = false; + connect(item, &PLCItem::stateChanged, this, [this, item](bool active) { if (m_simulationRunning) { if (active) m_activeItems.insert(item); @@ -714,57 +1194,92 @@ bool PLCDocument::loadFromFile(const QString &filePath) } } - // 加载连接 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); + TerminalType startTerminal = static_cast(connObj["startTerminal"].toInt()); + TerminalType endTerminal = static_cast(connObj["endTerminal"].toInt()); + + PLCItem *startItem = nullptr; + PLCItem *endItem = nullptr; + + // 处理起点 + if (connObj.contains("startRowTerminal")) { + // 起点是行触点 + int startRow = connObj["startRowTerminal"].toInt(); + ConnectionLine *line = new ConnectionLine( + nullptr, RowTerminal, + endItem, endTerminal + ); + line->setStartTerminalPoint(QPointF(0, (startRow + 0.5) * m_cellSize)); + line->setRowTerminalSourceRow(startRow); m_scene->addItem(line); m_connections.append(line); } + else if (connObj.contains("startCol") && connObj.contains("startRow")) { + int startCol = connObj["startCol"].toInt(); + 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, + nullptr, RowTerminal + ); + line->setEndTerminalPoint(QPointF(0, (endRow + 0.5) * m_cellSize)); + line->setRowTerminalTargetRow(endRow); + m_scene->addItem(line); + m_connections.append(line); + } + else if (connObj.contains("endCol") && connObj.contains("endRow")) { + int endCol = connObj["endCol"].toInt(); + int endRow = connObj["endRow"].toInt(); + endItem = itemMap.value(qMakePair(endCol, endRow)); + + if (startItem && endItem) { + ConnectionLine *line = new ConnectionLine( + startItem, startTerminal, + endItem, endTerminal + ); + m_scene->addItem(line); + m_connections.append(line); + } + } + } + } + + // 更新连接状态 + for (ConnectionLine* line : m_connections) { + if (line->startItem()) { + terminalConnections[line->startItem()][line->startTerminal()] = true; + } + if (line->endItem()) { + terminalConnections[line->endItem()][line->endTerminal()] = true; + } } - // 完成加载 m_scene->blockSignals(false); - handleSceneChanged(); // 手动触发位置更新 + 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) + +ConnectionLine::ConnectionLine( + PLCItem *startItem, PLCDocument::TerminalType startTerminal, + PLCItem *endItem, PLCDocument::TerminalType endTerminal, + QGraphicsItem *parent +) : QGraphicsLineItem(parent) + , m_startItem(startItem) + , m_startTerminal(startTerminal) + , m_endItem(endItem) + , m_endTerminal(endTerminal) { setPen(QPen(Qt::black, 1)); setZValue(-1); @@ -773,9 +1288,29 @@ ConnectionLine::ConnectionLine(PLCItem *start, PLCItem *end, QGraphicsItem *pare void ConnectionLine::updatePosition() { - if (m_startItem && m_endItem) { - // 基于控件端子位置更新连线 - QLineF line(m_startItem->rightTerminal(), m_endItem->leftTerminal()); - setLine(line); + QPointF startPoint; + if (m_startItem) { + if (m_startTerminal == PLCDocument::LeftTerminal) { + startPoint = m_startItem->leftTerminal(); + } else if (m_startTerminal == PLCDocument::RightTerminal) { + startPoint = m_startItem->rightTerminal(); + } + } else if (!m_startTerminalPoint.isNull()) { + startPoint = m_startTerminalPoint; + } + + QPointF endPoint; + if (m_endItem) { + if (m_endTerminal == PLCDocument::LeftTerminal) { + endPoint = m_endItem->leftTerminal(); + } else if (m_endTerminal == PLCDocument::RightTerminal) { + endPoint = m_endItem->rightTerminal(); + } + } else if (!m_endTerminalPoint.isNull()) { + endPoint = m_endTerminalPoint; + } + + if (!startPoint.isNull() && !endPoint.isNull()) { + setLine(QLineF(startPoint, endPoint)); } } diff --git a/untitled/plcdocument.h b/untitled/plcdocument.h index 70c7980..def7d95 100644 --- a/untitled/plcdocument.h +++ b/untitled/plcdocument.h @@ -1,13 +1,13 @@ -// plcdocument.h #ifndef PLCDOCUMENT_H #define PLCDOCUMENT_H #include "basedocument.h" -#include "plcitems.h" // 确保包含PLCItem的定义 +#include "plcitems.h" #include #include #include #include +#include #include #include #include @@ -17,6 +17,10 @@ #include #include +const int DEFAULT_ROWS = 11; +const int DEFAULT_COLUMNS = 15; +const int DEFAULT_CELL_SIZE = 70; + class PLCItem; class ConnectionLine; @@ -24,6 +28,19 @@ class PLCDocument : public BaseDocument { Q_OBJECT public: + enum TerminalType { + NoTerminal = 0, + LeftTerminal, + RightTerminal, + RowTerminal + }; + + enum ConnectionSourceType { + ConnectionNone, // 无连接 + ConnectionFromItem, // 起点是元件的端子 + ConnectionFromRowTerminal // 起点是行触点 + }; + explicit PLCDocument(QWidget *parent = nullptr); ~PLCDocument() override; @@ -34,28 +51,20 @@ public: bool saveToFile(const QString &filePath) override; bool loadFromFile(const QString &filePath) override; - // 创建PLC元件 void createPLCItem(const QString &type, const QPointF &pos); - - // 连接管理 - void startConnection(PLCItem *startItem); - void endConnection(PLCItem *endItem); + void startConnection(PLCItem *startItem, TerminalType startTerminal); + void startConnectionFromRowTerminal(int row); void clearCurrentConnection(); - - // 运行状态控制 + bool tryEndConnection(PLCItem *endItem, TerminalType endTerminal); void setSimulationRunning(bool running); void resetSimulation(); void setCurrentTool(const QString &tool) { m_currentTool = tool; } - // 获取表格参数 int rowCount() const { return m_rows; } int columnCount() const { return m_columns; } int cellSize() const { return m_cellSize; } - // 计算单元格中心位置 QPointF snapToCellCenter(const QPointF &pos) const; - - // 检查并修正位置,确保在表格内 QPointF constrainToTable(const QPointF &pos) const; void setTitle(const QString &title) { m_title = title; } @@ -65,63 +74,79 @@ protected: void resizeEvent(QResizeEvent *event) override; private slots: - // 处理场景变化(用于更新连接线) void handleSceneChanged(); + void onRowTerminalConnection(); private: - // 表格管理 void createRealTable(); - void clearTable(); - void adjustTableToFitView(); // 调整表格以适应视图大小 - - // 项目位置改变处理 + //void clearTable(); + TerminalType whichTerminal(const QPointF& scenePos, PLCItem* item) const; + bool isTerminalUsed(PLCItem* item, TerminalType terminal) const; void handleItemPositionChange(PLCItem *item); - - // 仿真与连接管理 + bool isCellOccupied(int col, int row, PLCItem* excludeItem) const; void updateConnections(); void createContextMenu(QPoint globalPos); - - // 根据类型创建PLC项 - 新增的辅助函数 PLCItem* createItemByType(PLCItem::ItemType type); + void removeInvalidConnectionsForItem(PLCItem* movedItem); + QList allPLCItems() const; QGraphicsScene *m_scene; QGraphicsView *m_view; - QString m_currentTool; // 当前选择的工具 - - // 连接管理 - PLCItem *m_connectionStart = nullptr; + QString m_currentTool; + ConnectionSourceType m_connectionSourceType = ConnectionNone; + PLCItem *m_connectionStartItem = nullptr; + TerminalType m_startTerminal = NoTerminal; + int m_connectionStartRow = -1; QGraphicsLineItem *m_tempLine = nullptr; QList m_connections; - - // 运行状态 + QMap> terminalConnections; bool m_simulationRunning = false; QSet m_activeItems; - - // 表格参数 - 使用常量定义默认值 - int m_rows = 15; // 表格行数 - int m_columns = 20; // 表格列数 - int m_cellSize = 50; // 单元格大小(像素) - - // 表格图形项 + int m_rows = DEFAULT_ROWS; + int m_columns = DEFAULT_COLUMNS; + int m_cellSize = DEFAULT_CELL_SIZE; QGraphicsRectItem* m_tableFrame = nullptr; QVector m_horizontalLines; QVector m_verticalLines; - QString m_title; // 添加标题成员变量 + QString m_title; + QVector m_rowTerminals; + QMap m_rowTerminalMap; + QMap m_lastValidPositions; + QSet m_processingItems; + bool m_loadingFile = false; }; -// 连接线类 class ConnectionLine : public QGraphicsLineItem { public: - ConnectionLine(PLCItem *start, PLCItem *end, QGraphicsItem *parent = nullptr); + ConnectionLine( + PLCItem *startItem, PLCDocument::TerminalType startTerminal, + PLCItem *endItem, PLCDocument::TerminalType endTerminal, + QGraphicsItem *parent = nullptr + ); void updatePosition(); + PLCItem* startItem() const { return m_startItem; } PLCItem* endItem() const { return m_endItem; } + PLCDocument::TerminalType startTerminal() const { return m_startTerminal; } + PLCDocument::TerminalType endTerminal() const { return m_endTerminal; } + void setRowTerminalTargetRow(int row) { m_rowTerminalTargetRow = row; } + int rowTerminalTargetRow() const { return m_rowTerminalTargetRow; } + void setRowTerminalSourceRow(int row) { m_rowTerminalSourceRow = row; } + int rowTerminalSourceRow() const { return m_rowTerminalSourceRow; } + void setStartTerminalPoint(const QPointF& point) { m_startTerminalPoint = point; } + void setEndTerminalPoint(const QPointF& point) { m_endTerminalPoint = point; } private: PLCItem *m_startItem; + PLCDocument::TerminalType m_startTerminal; PLCItem *m_endItem; + PLCDocument::TerminalType m_endTerminal; + QPointF m_startTerminalPoint; + QPointF m_endTerminalPoint; + int m_rowTerminalTargetRow = -1; + int m_rowTerminalSourceRow = -1; }; #endif // PLCDOCUMENT_H diff --git a/untitled/plcitems.cpp b/untitled/plcitems.cpp index 197757a..52c10c5 100644 --- a/untitled/plcitems.cpp +++ b/untitled/plcitems.cpp @@ -11,7 +11,7 @@ PLCItem::PLCItem(ItemType type, QGraphicsItem *parent) QRectF PLCItem::boundingRect() const { - return m_boundingRect; // 返回我们设置的边界矩形 + return m_boundingRect; //返回我们设置的边界矩形 } void PLCItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) @@ -104,7 +104,7 @@ void PLCItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, Q painter->drawLine(QPointF(-25, 0), QPointF(-15, 0)); // 左侧横线 painter->drawArc(QRectF(-15, -15, 15, 30), 90 * 16, 180 * 16);// 绘制左括号 "(" painter->drawArc(QRectF(0, -15, 15, 30), 270 * 16, 180 * 16);// 绘制右括号 ")" - painter->drawLine(QPointF(15, 0), QPointF(25, 0));// 右侧横线 + //painter->drawLine(QPointF(15, 0), QPointF(25, 0));// 右侧横线 painter->setPen(QPen(Qt::black, 4));// 调整线条宽度使括号更清晰 break; } diff --git a/untitled/untitled.pro.user b/untitled/untitled.pro.user index ee40880..ab91761 100644 --- a/untitled/untitled.pro.user +++ b/untitled/untitled.pro.user @@ -1,6 +1,6 @@ - + EnvironmentId