您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

1317 行
43 KiB

  1. #include "plcdocument.h"
  2. #include "plcitems.h"
  3. #include <QPainter>
  4. #include <QPen>
  5. #include <QBrush>
  6. #include <QMenu>
  7. #include <QAction>
  8. #include <QGraphicsSceneMouseEvent>
  9. #include <QJsonDocument>
  10. #include <QJsonObject>
  11. #include <QJsonArray>
  12. #include <QFile>
  13. #include <QFileInfo>
  14. #include <QVBoxLayout>
  15. #include <QMessageBox>
  16. #include <QInputDialog>
  17. #include <QDragEnterEvent>
  18. #include <QDragMoveEvent>
  19. #include <QCursor>
  20. #include <QDropEvent>
  21. #include <QMimeData>
  22. #include <cmath>
  23. #include <QResizeEvent>
  24. #include <QDebug>
  25. #include <QGraphicsItem>
  26. #include <QSet>
  27. #include <QMap>
  28. #include <QPointer>
  29. #include <QScrollArea>
  30. PLCDocument::PLCDocument(QWidget *parent)
  31. : BaseDocument(PLC, parent)
  32. , m_rows(DEFAULT_ROWS)
  33. , m_columns(DEFAULT_COLUMNS)
  34. , m_cellSize(DEFAULT_CELL_SIZE)
  35. {
  36. // 创建绘图场景
  37. m_scene = new QGraphicsScene(this);
  38. m_scene->setSceneRect(0, 0, m_columns * m_cellSize, m_rows * m_cellSize);
  39. // 创建视图
  40. m_view = new QGraphicsView(m_scene, this);
  41. m_view->setRenderHint(QPainter::Antialiasing);
  42. m_view->setDragMode(QGraphicsView::RubberBandDrag);
  43. m_view->setMouseTracking(true);
  44. m_view->viewport()->installEventFilter(this);
  45. m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  46. m_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  47. m_view->setAlignment(Qt::AlignLeft | Qt::AlignTop);
  48. // 创建滚动区域
  49. QScrollArea *scrollArea = new QScrollArea(this);
  50. scrollArea->setWidget(m_view);
  51. scrollArea->setWidgetResizable(true);
  52. // 设置布局
  53. auto layout = new QVBoxLayout(this);
  54. layout->setContentsMargins(0, 0, 0, 0);
  55. layout->addWidget(scrollArea);
  56. setLayout(layout);
  57. m_view->setAcceptDrops(true);
  58. // 创建固定大小的表格
  59. createRealTable();
  60. // 连接场景信号
  61. connect(m_scene, &QGraphicsScene::selectionChanged, this, [this]()
  62. {
  63. auto items = m_scene->selectedItems();
  64. if (items.size() == 1 && dynamic_cast<PLCItem*>(items.first()))
  65. {
  66. clearCurrentConnection();
  67. }
  68. });
  69. // 连接场景变化信号
  70. connect(m_scene, &QGraphicsScene::changed, this, &PLCDocument::handleSceneChanged);
  71. }
  72. PLCDocument::~PLCDocument()
  73. {
  74. qDeleteAll(m_connections);
  75. clearTable();
  76. }
  77. QString PLCDocument::title() const {
  78. return "PLC文档";
  79. }
  80. void PLCDocument::createRealTable()
  81. {
  82. //clearTable();
  83. // 表格整体区域
  84. QRectF tableRect(0, 0, m_columns * m_cellSize, m_rows * m_cellSize);
  85. m_tableFrame = new QGraphicsRectItem(tableRect);
  86. m_tableFrame->setPen(QPen(Qt::black, 2));
  87. m_tableFrame->setBrush(QBrush(QColor(245, 245, 245)));
  88. m_tableFrame->setZValue(-10);
  89. m_scene->addItem(m_tableFrame);
  90. // 添加垂直线
  91. for (int col = 0; col <= m_columns; col++) {
  92. int x = col * m_cellSize;
  93. QGraphicsLineItem* line = new QGraphicsLineItem(x, 0, x, m_rows * m_cellSize);
  94. line->setPen(QPen(Qt::darkGray, 1));
  95. line->setZValue(-9);
  96. m_scene->addItem(line);
  97. m_verticalLines.append(line);
  98. }
  99. // 添加水平线
  100. for (int row = 0; row <= m_rows; row++) {
  101. int y = row * m_cellSize;
  102. QGraphicsLineItem* line = new QGraphicsLineItem(0, y, m_columns * m_cellSize, y);
  103. line->setPen(QPen(Qt::darkGray, 1));
  104. line->setZValue(-9);
  105. m_scene->addItem(line);
  106. m_horizontalLines.append(line);
  107. }
  108. // 添加行触点
  109. for (int row = 0; row < m_rows; row++)
  110. {
  111. const int terminalSize = 15;
  112. QPointF terminalPos(0, row * m_cellSize + m_cellSize / 2.0 - terminalSize / 2);
  113. QGraphicsEllipseItem* terminal = new QGraphicsEllipseItem(
  114. terminalPos.x(), terminalPos.y(), terminalSize, terminalSize);
  115. terminal->setFlag(QGraphicsItem::ItemIsSelectable, true);
  116. terminal->setFlag(QGraphicsItem::ItemIsFocusable, true);
  117. terminal->setAcceptHoverEvents(true);
  118. terminal->setVisible(false);
  119. m_scene->addItem(terminal);
  120. m_rowTerminals.append(terminal);
  121. m_rowTerminalMap[terminal] = row;
  122. }
  123. }
  124. //void PLCDocument::clearTable()
  125. //{
  126. // if (m_tableFrame) {
  127. // m_scene->removeItem(m_tableFrame);
  128. // delete m_tableFrame;
  129. // m_tableFrame = nullptr;
  130. // }
  131. // for (auto line : m_horizontalLines) {
  132. // m_scene->removeItem(line);
  133. // delete line;
  134. // }
  135. // m_horizontalLines.clear();
  136. // for (auto line : m_verticalLines) {
  137. // m_scene->removeItem(line);
  138. // delete line;
  139. // }
  140. // m_verticalLines.clear();
  141. // // 清理行端子
  142. // for (auto terminal : m_rowTerminals) {
  143. // m_scene->removeItem(terminal);
  144. // delete terminal;
  145. // }
  146. // m_rowTerminals.clear();
  147. // m_rowTerminalMap.clear();
  148. //}
  149. QPointF PLCDocument::snapToCellCenter(const QPointF& pos) const
  150. {
  151. int col = static_cast<int>(std::floor(pos.x() / m_cellSize));
  152. int row = static_cast<int>(std::floor(pos.y() / m_cellSize));
  153. col = qBound(0, col, m_columns - 1);
  154. row = qBound(0, row, m_rows - 1);
  155. return QPointF(col * m_cellSize + m_cellSize / 2.0,
  156. row * m_cellSize + m_cellSize / 2.0);
  157. }
  158. QPointF PLCDocument::constrainToTable(const QPointF &pos) const
  159. {
  160. int col = static_cast<int>(std::floor(pos.x() / m_cellSize));
  161. int row = static_cast<int>(std::floor(pos.y() / m_cellSize));
  162. col = qBound(0, col, m_columns - 1);
  163. row = qBound(0, row, m_rows - 1);
  164. return QPointF(col * m_cellSize + m_cellSize / 2.0,
  165. row * m_cellSize + m_cellSize / 2.0);
  166. }
  167. void PLCDocument::resizeEvent(QResizeEvent *event)
  168. {
  169. QWidget::resizeEvent(event);
  170. m_view->setFixedSize(m_columns * m_cellSize, m_rows * m_cellSize);
  171. }
  172. bool PLCDocument::eventFilter(QObject *obj, QEvent *event)
  173. {
  174. if (obj == m_view->viewport())
  175. {
  176. if (event->type() == QEvent::DragEnter) {
  177. QDragEnterEvent *dragEvent = static_cast<QDragEnterEvent*>(event);
  178. if (dragEvent->mimeData()->hasText()) {
  179. dragEvent->acceptProposedAction();
  180. return true;
  181. }
  182. }
  183. else if (event->type() == QEvent::DragMove) {
  184. QDragMoveEvent *dragEvent = static_cast<QDragMoveEvent*>(event);
  185. if(dragEvent->mimeData()->hasText()) {
  186. dragEvent->acceptProposedAction();
  187. return true;
  188. }
  189. }
  190. else if (event->type() == QEvent::Drop) {
  191. QDropEvent *dropEvent = static_cast<QDropEvent*>(event);
  192. const QMimeData *mimeData = dropEvent->mimeData();
  193. if (mimeData->hasText()) {
  194. QPoint viewportPos = m_view->viewport()->mapFromParent(dropEvent->pos());
  195. QPointF scenePos = m_view->mapToScene(viewportPos);
  196. QPointF cellCenter = snapToCellCenter(scenePos);
  197. QSet<int> occupiedCells;
  198. for (QGraphicsItem* item : m_scene->items()) {
  199. if (PLCItem* plcItem = dynamic_cast<PLCItem*>(item)) {
  200. int col = static_cast<int>(plcItem->x() / m_cellSize);
  201. int row = static_cast<int>(plcItem->y() / m_cellSize);
  202. occupiedCells.insert(col * 1000 + row);
  203. }
  204. }
  205. int col = static_cast<int>(cellCenter.x() / m_cellSize);
  206. int row = static_cast<int>(cellCenter.y() / m_cellSize);
  207. int cellKey = col * 1000 + row;
  208. if (occupiedCells.contains(cellKey)) {
  209. bool found = false;
  210. for (int offset = 1; offset < m_columns; offset++) {
  211. int newCol = col + offset;
  212. if (newCol < m_columns) {
  213. int newKey = newCol * 1000 + row;
  214. if (!occupiedCells.contains(newKey)) {
  215. cellCenter = QPointF(newCol * m_cellSize + m_cellSize / 2.0,
  216. row * m_cellSize + m_cellSize / 2.0);
  217. found = true;
  218. break;
  219. }
  220. }
  221. }
  222. if (!found) {
  223. for (int offset = 1; offset < m_rows; offset++) {
  224. int newRow = row + offset;
  225. if (newRow < m_rows) {
  226. int newKey = col * 1000 + newRow;
  227. if (!occupiedCells.contains(newKey)) {
  228. cellCenter = QPointF(col * m_cellSize + m_cellSize / 2.0,
  229. newRow * m_cellSize + m_cellSize / 2.0);
  230. found = true;
  231. break;
  232. }
  233. }
  234. }
  235. }
  236. }
  237. QString toolType = mimeData->text();
  238. createPLCItem(toolType, cellCenter);
  239. m_currentTool.clear();
  240. dropEvent->acceptProposedAction();
  241. return true;
  242. }
  243. }
  244. }
  245. if (obj != m_view->viewport()) {
  246. return QWidget::eventFilter(obj, event);
  247. }
  248. if (event->type() == QEvent::MouseMove && m_tempLine) {
  249. QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
  250. QPointF scenePos = m_view->mapToScene(mouseEvent->pos());
  251. QPointF cellCenter = snapToCellCenter(scenePos);
  252. if (m_connectionSourceType != ConnectionNone) {
  253. if (m_connectionSourceType == ConnectionFromItem && m_connectionStartItem) {
  254. if (m_startTerminal == LeftTerminal) {
  255. QPointF startPos = m_connectionStartItem->leftTerminal();
  256. QLineF line(startPos, cellCenter);
  257. m_tempLine->setLine(line);
  258. } else if (m_startTerminal == RightTerminal) {
  259. QPointF startPos = m_connectionStartItem->rightTerminal();
  260. QLineF line(startPos, cellCenter);
  261. m_tempLine->setLine(line);
  262. }
  263. } else if (m_connectionSourceType == ConnectionFromRowTerminal) {
  264. QPointF startPos(0, (m_connectionStartRow + 0.5) * m_cellSize);
  265. QLineF line(startPos, cellCenter);
  266. m_tempLine->setLine(line);
  267. }
  268. }
  269. }
  270. if (event->type() == QEvent::MouseButtonPress) {
  271. QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
  272. if (mouseEvent->button() == Qt::RightButton) {
  273. createContextMenu(mouseEvent->globalPos());
  274. return true;
  275. }
  276. if (mouseEvent->button() == Qt::LeftButton) {
  277. QPointF scenePos = m_view->mapToScene(mouseEvent->pos());
  278. // 检查是否点击到行触点
  279. QGraphicsEllipseItem* clickedRowTerminal = nullptr;
  280. int rowTerminalIndex = -1;
  281. for (auto terminal : m_rowTerminals) {
  282. QPointF localPos = terminal->mapFromScene(scenePos);
  283. if (terminal->contains(localPos)) {
  284. clickedRowTerminal = terminal;
  285. rowTerminalIndex = m_rowTerminalMap[terminal];
  286. break;
  287. }
  288. }
  289. if (clickedRowTerminal) {
  290. // 行触点被点击
  291. if (m_connectionSourceType != ConnectionNone) {
  292. // 已有连接起点,尝试作为终点
  293. if (tryEndConnection(nullptr, RowTerminal)) {
  294. return true;
  295. }
  296. } else {
  297. // 没有连接起点,开始新连接(行触点作为起点)
  298. startConnectionFromRowTerminal(rowTerminalIndex);
  299. return true;
  300. }
  301. }
  302. QPointF cellCenter = snapToCellCenter(scenePos);
  303. QGraphicsItem *clickedItem = m_scene->itemAt(cellCenter, m_view->transform());
  304. PLCItem *plcItem = dynamic_cast<PLCItem*>(clickedItem);
  305. if (!m_currentTool.isEmpty()) {
  306. QSet<int> occupiedCells;
  307. for (QGraphicsItem* item : m_scene->items()) {
  308. if (PLCItem* existingItem = dynamic_cast<PLCItem*>(item)) {
  309. int col = static_cast<int>(existingItem->x() / m_cellSize);
  310. int row = static_cast<int>(existingItem->y() / m_cellSize);
  311. occupiedCells.insert(col * 1000 + row);
  312. }
  313. }
  314. int col = static_cast<int>(cellCenter.x() / m_cellSize);
  315. int row = static_cast<int>(cellCenter.y() / m_cellSize);
  316. int cellKey = col * 1000 + row;
  317. if (!occupiedCells.contains(cellKey)) {
  318. createPLCItem(m_currentTool, cellCenter);
  319. m_currentTool.clear();
  320. return true;
  321. } else {
  322. QMessageBox::warning(this, "位置被占用", "此单元格已被占用,请选择其他位置。");
  323. return true;
  324. }
  325. }
  326. if (plcItem) {
  327. TerminalType clickedTerminal = whichTerminal(scenePos, plcItem);
  328. if (m_connectionSourceType != ConnectionNone) {
  329. if (clickedTerminal != NoTerminal) {
  330. if (tryEndConnection(plcItem, clickedTerminal)) {
  331. return true;
  332. }
  333. }
  334. } else {
  335. if (clickedTerminal != NoTerminal && !isTerminalUsed(plcItem, clickedTerminal))
  336. {
  337. startConnection(plcItem, clickedTerminal);
  338. return true;
  339. }
  340. }
  341. }
  342. }
  343. }
  344. return QWidget::eventFilter(obj, event);
  345. }
  346. void PLCDocument::startConnectionFromRowTerminal(int row)
  347. {
  348. clearCurrentConnection();
  349. m_connectionSourceType = ConnectionFromRowTerminal;
  350. m_connectionStartRow = row;
  351. m_tempLine = new QGraphicsLineItem;
  352. m_tempLine->setPen(QPen(Qt::blue, 2, Qt::DashLine));
  353. QPointF startPos(0, (row + 0.5) * m_cellSize);
  354. m_tempLine->setLine(QLineF(startPos, startPos));
  355. m_scene->addItem(m_tempLine);
  356. }
  357. PLCDocument::TerminalType PLCDocument::whichTerminal(const QPointF& scenePos, PLCItem* item) const
  358. {
  359. const qreal terminalRadius = 20.0;
  360. QPointF leftTerminal = item->leftTerminal();
  361. qreal dxLeft = scenePos.x() - leftTerminal.x();
  362. qreal dyLeft = scenePos.y() - leftTerminal.y();
  363. qreal distLeft = std::sqrt(dxLeft*dxLeft + dyLeft*dyLeft);
  364. if (distLeft <= terminalRadius) {
  365. return LeftTerminal;
  366. }
  367. QPointF rightTerminal = item->rightTerminal();
  368. qreal dxRight = scenePos.x() - rightTerminal.x();
  369. qreal dyRight = scenePos.y() - rightTerminal.y();
  370. qreal distRight = std::sqrt(dxRight*dxRight + dyRight*dyRight);
  371. if (distRight <= terminalRadius) {
  372. return RightTerminal;
  373. }
  374. return NoTerminal;
  375. }
  376. bool PLCDocument::isTerminalUsed(PLCItem* item, TerminalType terminal) const
  377. {
  378. if (item->itemType() == PLCItem::Coil && terminal == RightTerminal)
  379. {
  380. return true; // 这一行是关键修改
  381. }
  382. for (ConnectionLine* line : m_connections) {
  383. if (line->startItem() == item && line->startTerminal() == terminal) {
  384. return true;
  385. }
  386. if (line->endItem() == item && line->endTerminal() == terminal) {
  387. return true;
  388. }
  389. }
  390. return false;
  391. }
  392. void PLCDocument::startConnection(PLCItem *startItem, TerminalType startTerminal)
  393. {
  394. clearCurrentConnection();
  395. m_connectionSourceType = ConnectionFromItem;
  396. m_connectionStartItem = startItem;
  397. m_startTerminal = startTerminal;
  398. m_tempLine = new QGraphicsLineItem;
  399. m_tempLine->setPen(QPen(Qt::blue, 2, Qt::DashLine));
  400. QPointF startPos;
  401. if (startTerminal == LeftTerminal) {
  402. startPos = startItem->leftTerminal();
  403. } else if (startTerminal == RightTerminal) {
  404. startPos = startItem->rightTerminal();
  405. }
  406. m_tempLine->setLine(QLineF(startPos, startPos));
  407. m_scene->addItem(m_tempLine);
  408. }
  409. bool PLCDocument::tryEndConnection(PLCItem *endItem, TerminalType endTerminal)
  410. {
  411. // 情况1:行触点作为起点,连接到元件左端子
  412. if (m_connectionSourceType == ConnectionFromRowTerminal) {
  413. // 行触点只能连接元件的左端子
  414. if (endTerminal != LeftTerminal) {
  415. QMessageBox::warning(this, "连线错误", "行触点只能连接到元件的左端子");
  416. clearCurrentConnection();
  417. return false;
  418. }
  419. // 检查是否在同一行
  420. int endRow = static_cast<int>(endItem->y() / m_cellSize);
  421. if (endRow != m_connectionStartRow) {
  422. QMessageBox::warning(this, "连线错误", "行触点只能连接到同一行元件的左端子");
  423. clearCurrentConnection();
  424. return false;
  425. }
  426. // 检查端子是否已被使用
  427. if (isTerminalUsed(endItem, LeftTerminal)) {
  428. QMessageBox::warning(this, "连线错误", "该元件的左端子已被使用");
  429. clearCurrentConnection();
  430. return false;
  431. }
  432. ConnectionLine *connection = new ConnectionLine(
  433. nullptr, RowTerminal,
  434. endItem, LeftTerminal
  435. );
  436. connection->setStartTerminalPoint(QPointF(0, (m_connectionStartRow + 0.5) * m_cellSize));
  437. connection->setRowTerminalSourceRow(m_connectionStartRow);
  438. m_scene->addItem(connection);
  439. m_connections.append(connection);
  440. terminalConnections[endItem][LeftTerminal] = true;
  441. clearCurrentConnection();
  442. setModified(true);
  443. return true;
  444. }
  445. // 情况2:元件作为起点,连接到行触点(终点)
  446. if (endTerminal == RowTerminal) {
  447. // 行触点只能被元件的左端子连接
  448. if (m_startTerminal != LeftTerminal) {
  449. QMessageBox::warning(this, "连线错误", "行触点只能连接到元件的左端子");
  450. clearCurrentConnection();
  451. return false;
  452. }
  453. int startRow = static_cast<int>(m_connectionStartItem->y() / m_cellSize);
  454. ConnectionLine *connection = new ConnectionLine(
  455. m_connectionStartItem, m_startTerminal,
  456. nullptr, RowTerminal
  457. );
  458. connection->setEndTerminalPoint(QPointF(0, (startRow + 0.5) * m_cellSize));
  459. connection->setRowTerminalTargetRow(startRow);
  460. m_scene->addItem(connection);
  461. m_connections.append(connection);
  462. terminalConnections[m_connectionStartItem][m_startTerminal] = true;
  463. clearCurrentConnection();
  464. setModified(true);
  465. return true;
  466. }
  467. // 情况3:元件连接到元件(标准连接)
  468. if (m_connectionSourceType == ConnectionFromItem && m_connectionStartItem) {
  469. if (endItem == m_connectionStartItem && endTerminal == m_startTerminal) {
  470. clearCurrentConnection();
  471. return false;
  472. }
  473. int startCol = static_cast<int>(m_connectionStartItem->pos().x() / m_cellSize);
  474. int endCol = static_cast<int>(endItem->pos().x() / m_cellSize);
  475. if (m_startTerminal == LeftTerminal) {
  476. if (endTerminal != RightTerminal) {
  477. QMessageBox::warning(this, "连线错误", "左端子只能连接到其他元件的右端子");
  478. clearCurrentConnection();
  479. return false;
  480. }
  481. if (startCol <= endCol) {
  482. QMessageBox::warning(this, "连线错误", "左端子只能连接到左边元件的右端子");
  483. clearCurrentConnection();
  484. return false;
  485. }
  486. } else if (m_startTerminal == RightTerminal) {
  487. if (endTerminal != LeftTerminal) {
  488. QMessageBox::warning(this, "连线错误", "右端子只能连接到其他元件的左端子");
  489. clearCurrentConnection();
  490. return false;
  491. }
  492. if (startCol >= endCol) {
  493. QMessageBox::warning(this, "连线错误", "右端子只能连接到右边元件的左端子");
  494. clearCurrentConnection();
  495. return false;
  496. }
  497. }
  498. if (isTerminalUsed(endItem, endTerminal)) {
  499. QMessageBox::warning(this, "连线错误", "该端子已被使用");
  500. clearCurrentConnection();
  501. return false;
  502. }
  503. ConnectionLine *connection = new ConnectionLine(
  504. m_connectionStartItem, m_startTerminal,
  505. endItem, endTerminal
  506. );
  507. m_scene->addItem(connection);
  508. m_connections.append(connection);
  509. terminalConnections[m_connectionStartItem][m_startTerminal] = true;
  510. terminalConnections[endItem][endTerminal] = true;
  511. clearCurrentConnection();
  512. setModified(true);
  513. return true;
  514. }
  515. return false;
  516. }
  517. void PLCDocument::clearCurrentConnection()
  518. {
  519. if (m_tempLine) {
  520. m_scene->removeItem(m_tempLine);
  521. delete m_tempLine;
  522. m_tempLine = nullptr;
  523. }
  524. m_connectionSourceType = ConnectionNone;
  525. m_connectionStartItem = nullptr;
  526. m_startTerminal = NoTerminal;
  527. m_connectionStartRow = -1;
  528. }
  529. void PLCDocument::createPLCItem(const QString &type, const QPointF &pos)
  530. {
  531. PLCItem *item = nullptr;
  532. if (type == "常开") {
  533. item = new NormallyOpenItem;
  534. } else if (type == "常闭") {
  535. item = new NormallyClosedItem;
  536. } else if (type == "大于") {
  537. item = new GreaterThanItem;
  538. } else if (type == "大于等于") {
  539. item = new GreaterEqualItem;
  540. } else if (type == "小于") {
  541. item = new LessThanItem;
  542. } else if (type == "小于等于") {
  543. item = new LessEqualItem;
  544. } else if (type == "等于") {
  545. item = new EqualItem;
  546. } else if (type == "线圈") {
  547. item = new CoilItem;
  548. }
  549. if (item) {
  550. QPointF constrainedPos = constrainToTable(pos);
  551. item->setPos(constrainedPos);
  552. m_scene->addItem(item);
  553. setModified(true);
  554. // 初始化端子状态
  555. terminalConnections[item][LeftTerminal] = false;
  556. terminalConnections[item][RightTerminal] = false;
  557. // 保存有效位置
  558. m_lastValidPositions[item] = constrainedPos;
  559. connect(item, &PLCItem::stateChanged, this, [this, item](bool active) {
  560. if (m_simulationRunning) {
  561. if (active) {
  562. m_activeItems.insert(item);
  563. } else {
  564. m_activeItems.remove(item);
  565. }
  566. updateConnections();
  567. }
  568. });
  569. }
  570. }
  571. void PLCDocument::handleSceneChanged()
  572. {
  573. for (auto* connection : qAsConst(m_connections))
  574. {
  575. connection->updatePosition();
  576. }
  577. if (m_tempLine && m_connectionSourceType != ConnectionNone)
  578. {
  579. QPointF cursorPos = m_view->mapFromGlobal(QCursor::pos());
  580. QPointF scenePos = m_view->mapToScene(cursorPos.toPoint());
  581. QLineF line = m_tempLine->line();
  582. line.setP2(scenePos);
  583. m_tempLine->setLine(line);
  584. }
  585. for (QGraphicsItem *item : m_scene->items())
  586. {
  587. if (PLCItem *plcItem = dynamic_cast<PLCItem*>(item))
  588. {
  589. handleItemPositionChange(plcItem);
  590. }
  591. }
  592. }
  593. QList<PLCItem*> PLCDocument::allPLCItems() const
  594. {
  595. QList<PLCItem*> items;
  596. for (QGraphicsItem *item : m_scene->items()) {
  597. if (PLCItem *plcItem = dynamic_cast<PLCItem*>(item)) {
  598. items.append(plcItem);
  599. }
  600. }
  601. return items;
  602. }
  603. bool PLCDocument::isCellOccupied(int col, int row, PLCItem* excludeItem) const
  604. {
  605. for (PLCItem* item : allPLCItems()) {
  606. if (item == excludeItem) continue;
  607. int itemCol = static_cast<int>(item->x() / m_cellSize);
  608. int itemRow = static_cast<int>(item->y() / m_cellSize);
  609. if (itemCol == col && itemRow == row) {
  610. return true;
  611. }
  612. }
  613. return false;
  614. }
  615. void PLCDocument::handleItemPositionChange(PLCItem *item)
  616. {
  617. if (m_loadingFile) {
  618. QPointF newPos = constrainToTable(item->pos());
  619. item->setPos(newPos);
  620. m_lastValidPositions[item] = newPos;
  621. return;
  622. }
  623. if (m_processingItems.contains(item)) {
  624. return;
  625. }
  626. m_processingItems.insert(item);
  627. QPointF newPos = constrainToTable(item->pos());
  628. int newCol = static_cast<int>(newPos.x() / m_cellSize);
  629. int newRow = static_cast<int>(newPos.y() / m_cellSize);
  630. if (isCellOccupied(newCol, newRow, item))
  631. {
  632. if (m_lastValidPositions.contains(item)) {
  633. item->setPos(m_lastValidPositions[item]);
  634. QMessageBox::warning(this, "移动失败", "无法移动,该单元格已被占用");
  635. }
  636. }
  637. else
  638. {
  639. // 获取移动前的列位置
  640. int oldCol = static_cast<int>(m_lastValidPositions[item].x() / m_cellSize);
  641. item->setPos(newPos);
  642. m_lastValidPositions[item] = newPos;
  643. // 检查连接是否因移动而无效
  644. if (newCol != oldCol) {
  645. removeInvalidConnectionsForItem(item);
  646. }
  647. for (ConnectionLine *line : m_connections)
  648. {
  649. if (line->startItem() == item || line->endItem() == item) {
  650. line->updatePosition();
  651. }
  652. }
  653. }
  654. m_processingItems.remove(item);
  655. }
  656. void PLCDocument::removeInvalidConnectionsForItem(PLCItem* movedItem)
  657. {
  658. // 1. 基础防护
  659. if (!movedItem || m_connections.isEmpty()) return;
  660. if (m_cellSize <= 0) {
  661. qWarning() << "Invalid cell size!";
  662. return;
  663. }
  664. int movedCol = static_cast<int>(movedItem->x() / m_cellSize);
  665. QSet<ConnectionLine*> connectionsToRemove;
  666. bool shouldRemoveAll = false;
  667. // 2. 第一遍遍历:检查位置条件
  668. for (ConnectionLine* line : m_connections) {
  669. if (!line) continue;
  670. PLCItem* otherItem = nullptr;
  671. TerminalType movedTerminal = NoTerminal; // 使用已定义的枚举值
  672. TerminalType otherTerminal = NoTerminal;
  673. // 确定连接的另一个元件
  674. if (line->startItem() == movedItem) {
  675. otherItem = line->endItem();
  676. movedTerminal = line->startTerminal();
  677. otherTerminal = line->endTerminal();
  678. } else if (line->endItem() == movedItem) {
  679. otherItem = line->startItem();
  680. movedTerminal = line->endTerminal();
  681. otherTerminal = line->startTerminal();
  682. } else {
  683. continue; // 不是与移动元件相关的连接
  684. }
  685. if (!otherItem) continue;
  686. int otherCol = static_cast<int>(otherItem->x() / m_cellSize);
  687. // 核心规则检查 - 使用连接线的端子类型,而不是元件的类型
  688. if (movedTerminal == LeftTerminal && otherTerminal == RightTerminal) {
  689. // 左端子移动到右端子元件左侧或同列
  690. if (movedCol <= otherCol) {
  691. shouldRemoveAll = true;
  692. break;
  693. }
  694. } else if (movedTerminal == RightTerminal && otherTerminal == LeftTerminal) {
  695. // 右端子移动到左端子元件右侧或同列
  696. if (movedCol >= otherCol) {
  697. shouldRemoveAll = true;
  698. break;
  699. }
  700. }
  701. }
  702. // 3. 如果需要删除,收集所有相关连接
  703. if (shouldRemoveAll) {
  704. for (ConnectionLine* line : m_connections) {
  705. if (!line) continue;
  706. if (line->startItem() == movedItem || line->endItem() == movedItem) {
  707. connectionsToRemove.insert(line);
  708. }
  709. }
  710. }
  711. // 4. 执行删除
  712. for (ConnectionLine* line : connectionsToRemove) {
  713. if (!line) continue;
  714. // 从终端连接表移除
  715. if (line->startItem()) {
  716. terminalConnections[line->startItem()].remove(line->startTerminal());
  717. }
  718. if (line->endItem()) {
  719. terminalConnections[line->endItem()].remove(line->endTerminal());
  720. }
  721. // 从场景中移除
  722. if (m_scene && m_scene->items().contains(line)) {
  723. m_scene->removeItem(line);
  724. }
  725. // 从连接列表中移除并删除对象
  726. m_connections.removeAll(line);
  727. delete line;
  728. }
  729. // 5. 标记文档修改
  730. if (!connectionsToRemove.isEmpty()) {
  731. setModified(true);
  732. }
  733. }
  734. void PLCDocument::setSimulationRunning(bool running)
  735. {
  736. m_simulationRunning = running;
  737. if (!running) {
  738. resetSimulation();
  739. }
  740. }
  741. void PLCDocument::resetSimulation()
  742. {
  743. m_activeItems.clear();
  744. for (QGraphicsItem *item : m_scene->items()) {
  745. if (PLCItem *plcItem = dynamic_cast<PLCItem*>(item)) {
  746. plcItem->setState(false);
  747. }
  748. }
  749. terminalConnections.clear();
  750. updateConnections();
  751. }
  752. void PLCDocument::updateConnections()
  753. {
  754. for (ConnectionLine *line : m_connections) {
  755. PLCItem *start = line->startItem();
  756. PLCItem *end = line->endItem();
  757. // 行触点连接的线路特殊处理(总是激活)
  758. if (line->endTerminal() == RowTerminal || line->startTerminal() == RowTerminal) {
  759. line->setPen(QPen(Qt::green, 2));
  760. continue;
  761. }
  762. if (m_activeItems.contains(start) && m_activeItems.contains(end)) {
  763. line->setPen(QPen(Qt::green, 2));
  764. } else {
  765. line->setPen(QPen(Qt::black, 1));
  766. }
  767. }
  768. }
  769. void PLCDocument::createContextMenu(QPoint globalPos)
  770. {
  771. QMenu menu(this);
  772. QAction *runAction = menu.addAction("运行模拟");
  773. QAction *stopAction = menu.addAction("停止模拟");
  774. QAction *resetAction = menu.addAction("重置模拟");
  775. menu.addSeparator();
  776. QAction *nameAction = menu.addAction("重命名");
  777. menu.addSeparator();
  778. QAction *tableSizeAction = menu.addAction("调整表格尺寸");
  779. QAction *tableGridAction = menu.addAction("隐藏/显示网格");
  780. QList<QGraphicsItem*> selected = m_scene->selectedItems();
  781. QAction *rowTerminalAction = nullptr;
  782. if (selected.size() == 1) {
  783. if (PLCItem *item = dynamic_cast<PLCItem*>(selected.first())) {
  784. int col = static_cast<int>(item->x() / m_cellSize);
  785. if (col == 0) {
  786. rowTerminalAction = menu.addAction("连接到行触点");
  787. }
  788. }
  789. }
  790. connect(runAction, &QAction::triggered, this, [this]() {
  791. setSimulationRunning(true);
  792. });
  793. connect(stopAction, &QAction::triggered, this, [this]() {
  794. setSimulationRunning(false);
  795. });
  796. connect(resetAction, &QAction::triggered, this, &PLCDocument::resetSimulation);
  797. connect(nameAction, &QAction::triggered, this, [this, selected]() {
  798. if (selected.size() == 1) {
  799. if (PLCItem *item = dynamic_cast<PLCItem*>(selected.first())) {
  800. QString name = QInputDialog::getText(this, "重命名", "输入名称:",
  801. QLineEdit::Normal, item->name());
  802. if (!name.isEmpty()) {
  803. item->setName(name);
  804. setModified(true);
  805. }
  806. }
  807. }
  808. });
  809. connect(tableSizeAction, &QAction::triggered, this, [this]() {
  810. bool ok;
  811. int cellSize = QInputDialog::getInt(this, "单元格大小", "输入单元格大小(像素):",
  812. m_cellSize, 10, 100, 10, &ok);
  813. if (ok && cellSize != m_cellSize) {
  814. m_cellSize = cellSize;
  815. m_view->setFixedSize(m_columns * m_cellSize, m_rows * m_cellSize);
  816. createRealTable();
  817. setModified(true);
  818. }
  819. });
  820. connect(tableGridAction, &QAction::triggered, this, [this]() {
  821. for (auto line : m_horizontalLines)
  822. {
  823. line->setVisible(!line->isVisible());
  824. }
  825. for (auto line : m_verticalLines)
  826. {
  827. line->setVisible(!line->isVisible());
  828. }
  829. });
  830. if (rowTerminalAction) {
  831. connect(rowTerminalAction, &QAction::triggered, this, &PLCDocument::onRowTerminalConnection);
  832. }
  833. menu.exec(globalPos+QPoint(10, 10));
  834. }
  835. void PLCDocument::onRowTerminalConnection()
  836. {
  837. QList<QGraphicsItem*> selected = m_scene->selectedItems();
  838. if (selected.size() != 1) return;
  839. PLCItem* item = dynamic_cast<PLCItem*>(selected.first());
  840. if (!item) return;
  841. int itemRow = static_cast<int>(item->y() / m_cellSize);
  842. int itemCol = static_cast<int>(item->x() / m_cellSize);
  843. if (itemCol != 0) {
  844. QMessageBox::warning(this, "连接错误", "只有最左侧列的元件可以连接到行触点");
  845. return;
  846. }
  847. if (isTerminalUsed(item, LeftTerminal)) {
  848. QMessageBox::warning(this, "连接失败", "该元件的左端子已被使用");
  849. return;
  850. }
  851. ConnectionLine* connection = new ConnectionLine(
  852. item, LeftTerminal,
  853. nullptr, RowTerminal
  854. );
  855. connection->setRowTerminalTargetRow(itemRow);
  856. connection->setEndTerminalPoint(QPointF(0, (itemRow + 0.5) * m_cellSize));
  857. connection->updatePosition();
  858. m_scene->addItem(connection);
  859. m_connections.append(connection);
  860. terminalConnections[item][LeftTerminal] = true;
  861. setModified(true);
  862. }
  863. PLCItem* PLCDocument::createItemByType(PLCItem::ItemType type)
  864. {
  865. switch (type) {
  866. case PLCItem::NormallyOpen: return new NormallyOpenItem;
  867. case PLCItem::NormallyClosed: return new NormallyClosedItem;
  868. case PLCItem::GreaterThan: return new GreaterThanItem;
  869. case PLCItem::GreaterEqual: return new GreaterEqualItem;
  870. case PLCItem::LessThan: return new LessThanItem;
  871. case PLCItem::LessEqual: return new LessEqualItem;
  872. case PLCItem::Equal: return new EqualItem;
  873. case PLCItem::Coil: return new CoilItem;
  874. default:
  875. qWarning() << "未知的PLC元件类型:" << type;
  876. return nullptr;
  877. }
  878. }
  879. bool PLCDocument::saveToFile(const QString &filePath)
  880. {
  881. QFile file(filePath);
  882. if (!file.open(QIODevice::WriteOnly)) {
  883. QMessageBox::warning(this, "保存失败", "无法打开文件进行写入:" + filePath);
  884. return false;
  885. }
  886. QJsonObject docObject;
  887. docObject["type"] = "PLCDocument";
  888. docObject["table_rows"] = m_rows;
  889. docObject["table_columns"] = m_columns;
  890. docObject["table_cell_size"] = m_cellSize;
  891. QJsonArray itemsArray;
  892. for (QGraphicsItem *item : m_scene->items()) {
  893. if (PLCItem *plcItem = dynamic_cast<PLCItem*>(item)) {
  894. QJsonObject itemObj;
  895. itemObj["type"] = static_cast<int>(plcItem->itemType());
  896. int col = static_cast<int>(plcItem->x() / m_cellSize);
  897. int row = static_cast<int>(plcItem->y() / m_cellSize);
  898. itemObj["col"] = col;
  899. itemObj["row"] = row;
  900. itemObj["name"] = plcItem->name();
  901. itemObj["active"] = plcItem->isActive();
  902. itemsArray.append(itemObj);
  903. }
  904. }
  905. docObject["items"] = itemsArray;
  906. QJsonArray connectionsArray;
  907. for (ConnectionLine *line : m_connections) {
  908. QJsonObject connObj;
  909. // 起点信息
  910. if (line->startItem()) {
  911. int startCol = static_cast<int>(line->startItem()->pos().x() / m_cellSize);
  912. int startRow = static_cast<int>(line->startItem()->pos().y() / m_cellSize);
  913. connObj["startCol"] = startCol;
  914. connObj["startRow"] = startRow;
  915. } else if (line->startTerminal() == RowTerminal) {
  916. connObj["startRowTerminal"] = line->rowTerminalSourceRow();
  917. }
  918. connObj["startTerminal"] = static_cast<int>(line->startTerminal());
  919. // 终点信息
  920. connObj["endTerminal"] = static_cast<int>(line->endTerminal());
  921. if (line->endTerminal() == RowTerminal) {
  922. connObj["endRowTerminal"] = line->rowTerminalTargetRow();
  923. } else if (line->endItem()) {
  924. int endCol = static_cast<int>(line->endItem()->pos().x() / m_cellSize);
  925. int endRow = static_cast<int>(line->endItem()->pos().y() / m_cellSize);
  926. connObj["endCol"] = endCol;
  927. connObj["endRow"] = endRow;
  928. }
  929. connectionsArray.append(connObj);
  930. }
  931. docObject["connections"] = connectionsArray;
  932. QJsonDocument doc(docObject);
  933. file.write(doc.toJson());
  934. file.close();
  935. setFilePath(filePath);
  936. setModified(false);
  937. return true;
  938. }
  939. bool PLCDocument::loadFromFile(const QString &filePath)
  940. {
  941. QFile file(filePath);
  942. if (!file.open(QIODevice::ReadOnly)) {
  943. QMessageBox::warning(this, "打开失败", "无法打开文件:" + filePath);
  944. return false;
  945. }
  946. QByteArray data = file.readAll();
  947. file.close();
  948. QJsonParseError parseError;
  949. QJsonDocument doc = QJsonDocument::fromJson(data, &parseError);
  950. if (parseError.error != QJsonParseError::NoError) {
  951. QMessageBox::warning(this, "文件格式错误", "JSON解析错误: " + parseError.errorString());
  952. return false;
  953. }
  954. if (doc.isNull() || !doc.isObject()) {
  955. QMessageBox::warning(this, "文件格式错误", "不是有效的PLC文档");
  956. return false;
  957. }
  958. QJsonObject docObject = doc.object();
  959. if (docObject["type"].toString() != "PLCDocument") {
  960. QMessageBox::warning(this, "文件类型错误", "不是有效的PLC文档");
  961. return false;
  962. }
  963. m_rows = docObject["table_rows"].toInt(DEFAULT_ROWS);
  964. m_columns = docObject["table_columns"].toInt(DEFAULT_COLUMNS);
  965. m_cellSize = docObject["table_cell_size"].toInt(DEFAULT_CELL_SIZE);
  966. setSimulationRunning(false);
  967. m_scene->blockSignals(true);
  968. for (auto conn : m_connections) {
  969. m_scene->removeItem(conn);
  970. delete conn;
  971. }
  972. m_connections.clear();
  973. QList<QGraphicsItem*> allItems = m_scene->items();
  974. for (QGraphicsItem* item : allItems) {
  975. if (dynamic_cast<PLCItem*>(item) || dynamic_cast<ConnectionLine*>(item)) {
  976. m_scene->removeItem(item);
  977. delete item;
  978. }
  979. }
  980. m_activeItems.clear();
  981. terminalConnections.clear();
  982. clearCurrentConnection();
  983. createRealTable();
  984. QMap<QPair<int, int>, PLCItem*> itemMap;
  985. QJsonArray itemsArray = docObject["items"].toArray();
  986. for (const QJsonValue &itemValue : itemsArray) {
  987. QJsonObject itemObj = itemValue.toObject();
  988. PLCItem::ItemType type = static_cast<PLCItem::ItemType>(itemObj["type"].toInt());
  989. int col = itemObj["col"].toInt();
  990. int row = itemObj["row"].toInt();
  991. QString name = itemObj["name"].toString();
  992. bool active = itemObj["active"].toBool();
  993. col = qBound(0, col, m_columns - 1);
  994. row = qBound(0, row, m_rows - 1);
  995. PLCItem *item = createItemByType(type);
  996. if (item) {
  997. QPointF pos(col * m_cellSize + m_cellSize / 2.0,
  998. row * m_cellSize + m_cellSize / 2.0);
  999. item->setPos(pos);
  1000. item->setName(name);
  1001. item->setState(active);
  1002. m_scene->addItem(item);
  1003. itemMap.insert(qMakePair(col, row), item);
  1004. if (active) {
  1005. m_activeItems.insert(item);
  1006. }
  1007. // 初始化端子状态
  1008. terminalConnections[item][LeftTerminal] = false;
  1009. terminalConnections[item][RightTerminal] = false;
  1010. connect(item, &PLCItem::stateChanged, this, [this, item](bool active) {
  1011. if (m_simulationRunning) {
  1012. if (active) m_activeItems.insert(item);
  1013. else m_activeItems.remove(item);
  1014. updateConnections();
  1015. }
  1016. });
  1017. }
  1018. }
  1019. QJsonArray connectionsArray = docObject["connections"].toArray();
  1020. for (const QJsonValue &connValue : connectionsArray) {
  1021. QJsonObject connObj = connValue.toObject();
  1022. TerminalType startTerminal = static_cast<TerminalType>(connObj["startTerminal"].toInt());
  1023. TerminalType endTerminal = static_cast<TerminalType>(connObj["endTerminal"].toInt());
  1024. PLCItem *startItem = nullptr;
  1025. PLCItem *endItem = nullptr;
  1026. // 处理起点
  1027. if (connObj.contains("startRowTerminal")) {
  1028. // 起点是行触点
  1029. int startRow = connObj["startRowTerminal"].toInt();
  1030. ConnectionLine *line = new ConnectionLine(
  1031. nullptr, RowTerminal,
  1032. endItem, endTerminal
  1033. );
  1034. line->setStartTerminalPoint(QPointF(0, (startRow + 0.5) * m_cellSize));
  1035. line->setRowTerminalSourceRow(startRow);
  1036. m_scene->addItem(line);
  1037. m_connections.append(line);
  1038. }
  1039. else if (connObj.contains("startCol") && connObj.contains("startRow")) {
  1040. int startCol = connObj["startCol"].toInt();
  1041. int startRow = connObj["startRow"].toInt();
  1042. startItem = itemMap.value(qMakePair(startCol, startRow));
  1043. // 处理终点
  1044. if (endTerminal == RowTerminal && connObj.contains("endRowTerminal")) {
  1045. // 终点是行触点
  1046. int endRow = connObj["endRowTerminal"].toInt();
  1047. ConnectionLine *line = new ConnectionLine(
  1048. startItem, startTerminal,
  1049. nullptr, RowTerminal
  1050. );
  1051. line->setEndTerminalPoint(QPointF(0, (endRow + 0.5) * m_cellSize));
  1052. line->setRowTerminalTargetRow(endRow);
  1053. m_scene->addItem(line);
  1054. m_connections.append(line);
  1055. }
  1056. else if (connObj.contains("endCol") && connObj.contains("endRow")) {
  1057. int endCol = connObj["endCol"].toInt();
  1058. int endRow = connObj["endRow"].toInt();
  1059. endItem = itemMap.value(qMakePair(endCol, endRow));
  1060. if (startItem && endItem) {
  1061. ConnectionLine *line = new ConnectionLine(
  1062. startItem, startTerminal,
  1063. endItem, endTerminal
  1064. );
  1065. m_scene->addItem(line);
  1066. m_connections.append(line);
  1067. }
  1068. }
  1069. }
  1070. }
  1071. // 更新连接状态
  1072. for (ConnectionLine* line : m_connections) {
  1073. if (line->startItem()) {
  1074. terminalConnections[line->startItem()][line->startTerminal()] = true;
  1075. }
  1076. if (line->endItem()) {
  1077. terminalConnections[line->endItem()][line->endTerminal()] = true;
  1078. }
  1079. }
  1080. m_scene->blockSignals(false);
  1081. handleSceneChanged();
  1082. setFilePath(filePath);
  1083. setModified(false);
  1084. return true;
  1085. }
  1086. ConnectionLine::ConnectionLine(
  1087. PLCItem *startItem, PLCDocument::TerminalType startTerminal,
  1088. PLCItem *endItem, PLCDocument::TerminalType endTerminal,
  1089. QGraphicsItem *parent
  1090. ) : QGraphicsLineItem(parent)
  1091. , m_startItem(startItem)
  1092. , m_startTerminal(startTerminal)
  1093. , m_endItem(endItem)
  1094. , m_endTerminal(endTerminal)
  1095. {
  1096. setPen(QPen(Qt::black, 1));
  1097. setZValue(-1);
  1098. updatePosition();
  1099. }
  1100. void ConnectionLine::updatePosition()
  1101. {
  1102. QPointF startPoint;
  1103. if (m_startItem) {
  1104. if (m_startTerminal == PLCDocument::LeftTerminal) {
  1105. startPoint = m_startItem->leftTerminal();
  1106. } else if (m_startTerminal == PLCDocument::RightTerminal) {
  1107. startPoint = m_startItem->rightTerminal();
  1108. }
  1109. } else if (!m_startTerminalPoint.isNull()) {
  1110. startPoint = m_startTerminalPoint;
  1111. }
  1112. QPointF endPoint;
  1113. if (m_endItem) {
  1114. if (m_endTerminal == PLCDocument::LeftTerminal) {
  1115. endPoint = m_endItem->leftTerminal();
  1116. } else if (m_endTerminal == PLCDocument::RightTerminal) {
  1117. endPoint = m_endItem->rightTerminal();
  1118. }
  1119. } else if (!m_endTerminalPoint.isNull()) {
  1120. endPoint = m_endTerminalPoint;
  1121. }
  1122. if (!startPoint.isNull() && !endPoint.isNull()) {
  1123. setLine(QLineF(startPoint, endPoint));
  1124. }
  1125. }