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