diff --git a/UI/Main/Main.py b/UI/Main/Main.py
index 86d497e..dbfed97 100644
--- a/UI/Main/Main.py
+++ b/UI/Main/Main.py
@@ -207,7 +207,7 @@ class MainWindow(QMainWindow):
def reFreshTrendWidget(self):
if Globals.getValue('currentPro') == -1:
return
- self.trendWidget.getMems()
+ # self.trendWidget.getMems()
self.exButtonClicked(2)
def initProcedureDB(self):
diff --git a/UI/TrendManage/ActualTrendWidget.py b/UI/TrendManage/ActualTrendWidget.py
index 8d16d56..a1b02b3 100644
--- a/UI/TrendManage/ActualTrendWidget.py
+++ b/UI/TrendManage/ActualTrendWidget.py
@@ -9,88 +9,77 @@ from matplotlib.figure import Figure
from protocol.Celery.MBTCPMaster import app as MBTCPMApp
from celery.result import AsyncResult
-from utils.Queue import Queue
from model.ProjectModel.VarManage import ModbusVarManage
+from utils import Globals
class ActualTrend(QtWidgets.QMainWindow):
def __init__(self, parent=None, varName = None):
super(ActualTrend, self).__init__(parent)
self.varName = varName
+ self.protocolManage = Globals.getValue('protocolManage')
_main = QtWidgets.QWidget()
self.setCentralWidget(_main)
layout = QtWidgets.QVBoxLayout(_main)
-
dynamic_canvas = FigureCanvas(Figure(figsize=(5, 3)))
layout.addWidget(dynamic_canvas)
self.addToolBar(QtCore.Qt.BottomToolBarArea,
NavigationToolbar(dynamic_canvas, self))
- self.x = [] #建立空的x轴数组和y轴数组
+ self.x = [] # 建立空的x轴数组和y轴数组
self.y = []
-
- self.xQueue = Queue(length = 20)
- self.yQueue = Queue(length = 20)
- # self.n = 0
+ self.maxPoints = 200
self._dynamic_ax = dynamic_canvas.figure.subplots()
self._timer = dynamic_canvas.new_timer(
- 1000, [(self._update_canvas, (), {})])
+ 500, [(self._update_canvas, (), {})])
self._timer.start()
-
-
def _update_canvas(self):
- # self.n += 1
+ # 防止canvas已被销毁时报错
+ if not hasattr(self, '_dynamic_ax') or not hasattr(self._dynamic_ax, 'figure') or not hasattr(self._dynamic_ax.figure, 'canvas'):
+ return
varName = self.varName
- varMM = ModbusVarManage.getByName(varName)
-
-
- self._dynamic_ax.clear()
+ if not varName or not self.protocolManage:
+ return
+ # 获取实时值
+ yValue = self.protocolManage.readVariableValue(varName)
acttime = time.strftime('%H:%M:%S', time.localtime(time.time()))
- # print(acttime)
- if varName != '':
- if MBTCPMApp.backend.get('ModBus') is not None:
- uid = MBTCPMApp.backend.get('ModBus').decode('utf-8')
- else:
- return
- # print(uid)
- res = AsyncResult(uid) # 参数为task id
- if res.result:
- # print(res.result, res.date_done)
- try:
- yValue = (res.result[varName])
- self.yQueue.enqueue(yValue)
- self.xQueue.enqueue(acttime)
- except Exception as e:
- print(e)
-
- if not varMM[-2] and not varMM[-1]:
- minY = min(self.yQueue.list) - 5
- maxY = max(self.yQueue.list) + 5
- else:
- minY = float(varMM[-2]) - float(5)
- maxY = float(varMM[-1]) + float(5)
-
- name_list = self.xQueue.list
- value_list = np.array(self.yQueue.list)
- pos_list = np.arange(len(name_list))
-
+ if yValue is not None:
+ self.x.append(acttime)
+ self.y.append(yValue)
+ # 只保留最新maxPoints个点
+ if len(self.x) > self.maxPoints:
+ self.x = self.x[-self.maxPoints:]
+ self.y = self.y[-self.maxPoints:]
+ self._dynamic_ax.clear()
+ pos_list = np.arange(len(self.x))
self._dynamic_ax.set_xticks(pos_list)
- self._dynamic_ax.set_xticklabels(name_list)
-
- self._dynamic_ax.plot(pos_list, value_list, 'bo-', linewidth=2)
- # self._dynamic_ax.set_xlim(0,10)
- self._dynamic_ax.set_ylim(minY, maxY)
- # print(id(self._dynamic_ax))
+ # 限制最多显示16个主刻度,自动旋转,字体更大更明显
+ import matplotlib.ticker as ticker
+ max_xticks = 12
+ if len(self.x) > max_xticks:
+ step = max(1, len(self.x) // max_xticks)
+ display_labels = [label if i % step == 0 else '' for i, label in enumerate(self.x)]
+ else:
+ display_labels = self.x
+ self._dynamic_ax.set_xticklabels(display_labels, rotation=30, fontsize=13, fontweight='bold')
+ self._dynamic_ax.plot(pos_list, self.y, 'bo-', linewidth=2)
+ if self.y:
+ minY = min(self.y) - 5
+ maxY = max(self.y) + 5
+ self._dynamic_ax.set_ylim(minY, maxY)
+ from matplotlib.ticker import MaxNLocator
+ self._dynamic_ax.yaxis.set_major_locator(MaxNLocator(nbins=12, prune=None))
self._dynamic_ax.figure.canvas.draw()
def closeEvent(self, event):
- # 判断是否点击了窗口的关闭按钮
- if event.type() == event.Close:
+ # 停止定时器,防止回调访问已销毁的canvas
+ if hasattr(self, '_timer'):
self._timer.stop()
- self.hide()
+ super().closeEvent(event)
+ self.hide()
def showEvent(self, event):
self._timer.start()
diff --git a/UI/TrendManage/TrendWidget.py b/UI/TrendManage/TrendWidget.py
index c4e1502..97070d4 100644
--- a/UI/TrendManage/TrendWidget.py
+++ b/UI/TrendManage/TrendWidget.py
@@ -1,161 +1,701 @@
from PyQt5 import QtCore, QtGui, QtWidgets
-from PyQt5.QtCore import QSize, Qt
-from PyQt5.Qt import *
+from PyQt5.QtCore import QSize, Qt, QTimer, QEvent
from PyQt5.QtGui import QPixmap, QIcon
-from PyQt5.QtWidgets import QHBoxLayout, QAbstractItemView, QTableView, QVBoxLayout, QSplitter, \
- QApplication, QGroupBox, QLabel, QGridLayout, QLineEdit, QComboBox, QTextEdit, QCheckBox,QVBoxLayout,QListView, QMainWindow
-from PyQt5.QtWidgets import QListWidget, QStackedWidget, QFrame
-from PyQt5.QtWidgets import QListWidgetItem, QSizePolicy
-from PyQt5.QtWidgets import QWidget, QSpacerItem, QHeaderView
-
-from PyQt5.QtWebEngineWidgets import QWebEngineView
-from PyQt5.QtWebEngineWidgets import QWebEnginePage
-
-from utils.DBModels.ProtocolModel import InfluxMem
+from PyQt5.QtWidgets import (QApplication, QGridLayout, QListWidget, QListWidgetItem,
+ QToolBar, QAction, QLabel, QWidget, QVBoxLayout, QHBoxLayout,
+ QSplitter, QGroupBox, QLineEdit, QComboBox, QTextEdit,
+ QCheckBox, QFrame, QSpacerItem, QSizePolicy, QHeaderView, QToolButton)
+from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from utils import Globals
-from model.HistoryDBModel.HistoryDBManage import HistoryDBManage
-
-import datetime
-
-import pyecharts.options as opts
-from pyecharts.charts import Line
-
+import matplotlib.pyplot as plt
+import matplotlib.dates as mdates
+from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.backends.backend_qt import NavigationToolbar2QT as NavigationToolbar
+from matplotlib.figure import Figure
import numpy as np
-from Static import static
-
-class HistoryTrend(object):
- def __init__(self, view):
- self.line = Line(init_opts= opts.InitOpts(width = str(view.size().width() - 20) + 'px', height = str(view.size().height() - 30) + 'px'))
- self.setOpts()
-
- def addXAxis(self, xAxis):
- self.line.add_xaxis(xaxis_data=xAxis)
-
- def addYAxis(self, lineName, yAxis):
- self.line.add_yaxis(
- series_name=lineName,
- y_axis=yAxis,
- markpoint_opts=opts.MarkPointOpts(
- data=[
- opts.MarkPointItem(type_="max", name="最大值"),
- opts.MarkPointItem(type_="min", name="最小值"),
- ]
- ),
- markline_opts=opts.MarkLineOpts(
- data=[opts.MarkLineItem(type_="average", name="平均值")]
- ),
- )
-
- def setOpts(self):
- self.line.set_global_opts(
- title_opts=opts.TitleOpts(title="趋势图"),
- tooltip_opts=opts.TooltipOpts(trigger="axis"),
- toolbox_opts=opts.ToolboxOpts(is_show=True),
- xaxis_opts=opts.AxisOpts(type_="category", boundary_gap=False),
- datazoom_opts=[
- opts.DataZoomOpts(xaxis_index=0, range_start=0, range_end=100),
- ],
- )
-
- @property
- def html(self):
- return self.line.render_embed()
-
+import datetime
+import sys
+import pandas as pd
+import bisect
+from matplotlib.widgets import RectangleSelector
+import matplotlib.patches as patches
+from matplotlib.widgets import SpanSelector
+from matplotlib.gridspec import GridSpec
+
+# 配置中文字体支持
+plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans']
+plt.rcParams['axes.unicode_minus'] = False
class TrendWidgets(QWidget):
- def __init__(self, parent=None):
- super(TrendWidgets, self).__init__(parent)
- # self.setAttribute(Qt.WA_StyledBackground, True) # 移除无效属性
- self.gridLayout = QtWidgets.QGridLayout(self)
- self.gridLayout.setObjectName("gridLayout")
-
+ def __init__(self):
+ super().__init__()
+ self.setWindowTitle("历史趋势浏览器")
+ self.setMinimumSize(120, 120)
+ # 设置窗口标志,禁用窗口拖拽
+ self.setWindowFlags(QtCore.Qt.WindowType.Window)
+ # 初始化数据
+ self.historyDB = None
+ self.currentVarName = None
+ self.xData = []
+ self.yData = []
+ # 多变量支持
+ self.multiVarData = {} # 存储多个变量的数据 {varName: {'x': [], 'y': []}}
+ self.selectedVars = [] # 当前选中的变量列表
+ self.varColors = {} # 存储变量颜色 {varName: color}
+ # 初始化历史缩放记录
+ self.history = []
+ self.historyIndex = 0
+ # 主布局
+ mainLayout = QHBoxLayout(self)
+ mainLayout.setSpacing(10)
+ mainLayout.setContentsMargins(10, 10, 10, 10)
+ # 左侧变量列表区域
+ self.createVariableListPanel()
+ mainLayout.addWidget(self.variableListGroup, 1) # 1份宽度
+ # 右侧趋势图区域
+ self.trendViewerGroup = QWidget()
+ self.createTrendViewerPanel()
+ mainLayout.addWidget(self.trendViewerGroup, 6) # 6份宽度
+ # 连接信号
+ self.connectSignals()
+ Globals.setValue('HistoryWidget', self)
+
+ def createVariableListPanel(self):
+ # 创建变量列表面板
+ self.variableListGroup = QGroupBox("变量列表")
+ layout = QVBoxLayout(self.variableListGroup)
+ # 搜索框单独一行
+ searchInputLayout = QHBoxLayout()
+ self.searchInput = QLineEdit()
+ self.searchInput.setPlaceholderText("搜索变量...")
+ self.searchInput.textChanged.connect(self.filterVarList)
+ self.searchInput.setMinimumWidth(220)
+ searchInputLayout.addWidget(self.searchInput)
+ layout.addLayout(searchInputLayout)
# 变量列表
self.varListWidget = QListWidget()
- self.varListWidget.setObjectName("varListWidget")
self.varListWidget.itemDoubleClicked.connect(self.showVarTrend)
- self.gridLayout.addWidget(self.varListWidget, 0, 0, 15, 4)
-
- # 趋势图WebView
- self.trendWebView = QWebEngineView()
- self.trendWebView.setObjectName("trendWebView")
- self.gridLayout.addWidget(self.trendWebView, 0, 4, 15, 22)
-
- self.gridLayout.setSpacing(10)
- self.gridLayout.setContentsMargins(20, 20, 20, 20)
- Globals.setValue('HistoryWidget', self)
-
+ self.varListWidget.itemSelectionChanged.connect(self.onVarSelected)
+ layout.addWidget(self.varListWidget)
+ # 时间选择+便捷查询区域
+ searchLayout = QHBoxLayout()
+ from PyQt5.QtCore import QDateTime
+ self.startTimeEdit = QtWidgets.QDateTimeEdit()
+ self.startTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
+ self.startTimeEdit.setCalendarPopup(True)
+ self.endTimeEdit = QtWidgets.QDateTimeEdit()
+ self.endTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss")
+ self.endTimeEdit.setCalendarPopup(True)
+ now = QDateTime.currentDateTime()
+ self.startTimeEdit.setDateTime(now.addDays(-1))
+ self.endTimeEdit.setDateTime(now)
+ searchLayout.addWidget(self.startTimeEdit)
+ searchLayout.addWidget(self.endTimeEdit)
+ self.quickRangeCombo = QtWidgets.QComboBox()
+ self.quickRangeCombo.addItems(["最近一天", "最近6小时", "最近1小时", "最近30分钟", "自定义"])
+ self.quickRangeCombo.currentIndexChanged.connect(self.onQuickRangeChanged)
+ searchLayout.addWidget(self.quickRangeCombo)
+ self.queryBtn = QToolButton()
+ self.queryBtn.setText("查询")
+ self.queryBtn.setToolTip("按时间范围查询变量数据")
+ self.queryBtn.clicked.connect(self.onTimeRangeQuery)
+ searchLayout.addWidget(self.queryBtn)
+ layout.addLayout(searchLayout)
+ # 按钮区域
+ buttonLayout = QHBoxLayout()
+ # 刷新按钮
+ refreshBtn = QToolButton()
+ refreshBtn.setText("刷新列表")
+ refreshBtn.clicked.connect(self.refreshVarList)
+ buttonLayout.addWidget(refreshBtn)
+ # 添加到趋势图按钮
+ addToTrendBtn = QToolButton()
+ addToTrendBtn.setText("添加到趋势图")
+ addToTrendBtn.setToolTip("将选中的变量添加到趋势图")
+ addToTrendBtn.clicked.connect(self.addSelectedVarsToTrend)
+ buttonLayout.addWidget(addToTrendBtn)
+ # 清除所有变量按钮
+ clearAllBtn = QToolButton()
+ clearAllBtn.setText("清除所有")
+ clearAllBtn.setToolTip("清除趋势图中的所有变量")
+ clearAllBtn.clicked.connect(self.clearAllVars)
+ buttonLayout.addWidget(clearAllBtn)
+ layout.addLayout(buttonLayout)
+ self.variableListGroup.setLayout(layout)
+ # 记录当前时间范围
+ self._query_time_range = (self.startTimeEdit.dateTime().toPyDateTime(), self.endTimeEdit.dateTime().toPyDateTime())
+
+ def createTrendViewerPanel(self):
+ # 创建趋势图查看器面板
+ layout = QVBoxLayout(self.trendViewerGroup)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+ # 创建包含图表的组框
+ chartGroup = QGroupBox()
+ chartLayout = QVBoxLayout(chartGroup)
+ chartLayout.setContentsMargins(5, 5, 5, 5)
+ # 创建matplotlib图表,设置主图和缩略图比例为6:1
+ self.figure = Figure(figsize=(12, 8), dpi=100)
+ gs = GridSpec(7, 1, figure=self.figure)
+ self.ax_main = self.figure.add_subplot(gs[:6, 0]) # 主图,占6份
+ self.ax_overview = self.figure.add_subplot(gs[6, 0]) # 缩略图,占1份
+ self.canvas = FigureCanvas(self.figure)
+ chartLayout.addWidget(self.canvas, 20)
+ # 底部信息栏(含状态显示)
+ infoLayout = QHBoxLayout()
+ infoLayout.setContentsMargins(5, 5, 5, 5)
+ self.varNameLabel = QLabel("当前变量: 无")
+ self.dataCountLabel = QLabel("数据点: 0")
+ self.timeRangeLabel = QLabel("时间范围: 无数据")
+ self.statusLabel = QLabel("就绪")
+ self.statusLabel.setFont(QtGui.QFont("Arial", 9))
+ self.statusLabel.setStyleSheet("color: #555; padding: 2px;")
+ infoLayout.addWidget(self.varNameLabel)
+ infoLayout.addStretch()
+ infoLayout.addWidget(self.dataCountLabel)
+ infoLayout.addStretch()
+ infoLayout.addWidget(self.timeRangeLabel)
+ infoLayout.addStretch()
+ infoLayout.addWidget(self.statusLabel)
+ chartLayout.addLayout(infoLayout, 1)
+ layout.addWidget(chartGroup)
+ # 悬浮信息气泡label
+ self.infoBubble = QLabel(self.trendViewerGroup)
+ self.infoBubble.setStyleSheet("background:rgba(255,255,220,0.95); border:1px solid #aaa; border-radius:4px; padding:4px; color:#222;")
+ self.infoBubble.setFont(QtGui.QFont("Consolas", 10))
+ self.infoBubble.setVisible(False)
+ # 连接鼠标事件,支持拖拽平移
+ self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
+ self.canvas.mpl_connect('axes_leave_event', self.on_mouse_leave)
+ self.canvas.mpl_connect('scroll_event', self.on_mouse_wheel)
+ self.canvas.mpl_connect('button_press_event', self.on_mouse_press)
+ self.canvas.mpl_connect('button_release_event', self.on_mouse_release)
+ # 拖拽状态
+ self._is_panning = False
+ self._pan_start_x = None
+ self._pan_start_xlim = None
+ # 初始化选择器
+ self.selector = None
+ self.is_selecting = False
+ self.zoom_start = None
+ # 添加SpanSelector用于区间选择
+ self.span = SpanSelector(
+ self.ax_overview, self.on_select, 'horizontal',
+ useblit=True, interactive=True, props=dict(alpha=0.3, facecolor='orange')
+ )
+ # 绑定双击事件恢复全局视图
+ self.canvas.mpl_connect('button_press_event', self.on_overview_double_click)
+ self._selected_range = None # 记录选中区间
+ # 设置下方缩略图x轴显示更多时间内容
+ import matplotlib.dates as mdates
+ from matplotlib.ticker import MaxNLocator
+ self.ax_overview.xaxis.set_major_locator(mdates.AutoDateLocator())
+ self.ax_overview.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
+
+ def connectSignals(self):
+ # 连接所有信号
+ self.ax_overview.callbacks.connect('xlim_changed', self.on_overview_xlim_changed)
def exchangeProject(self):
+ # 项目切换时调用
self.historyDB = Globals.getValue('historyDBManage')
self.refreshVarList()
-
- def deleteMem(self):
- pass # 历史mem相关功能已废弃
-
- def itemCheckstate(self):
- pass # 历史mem相关功能已废弃
-
- @QtCore.pyqtSlot(str)
- def on_lineEdit_textChanged(self, text):
- pass # 历史mem相关功能已废弃
-
- def listContext(self, position):
- pass # 历史mem相关功能已废弃
-
- def delVarRecard(self):
- pass # 历史mem相关功能已废弃
-
-
- def getMems(self):
- pass # 历史mem相关功能已废弃
-
- def refreshList(self, index):
- pass # 历史mem相关功能已废弃
-
- def createHtml(self, varNames):
- pass # 历史mem相关功能已废弃
-
+
def refreshVarList(self):
- # 显示进度条
+ # 刷新变量列表
self.varListWidget.clear()
- varNames = []
- try:
- sql = f'SELECT DISTINCT("varName") FROM "{self.historyDB.table}"'
- df = self.historyDB.client.query(sql, mode="pandas")
- import pandas as pd
- if isinstance(df, pd.DataFrame) and 'varName' in df.columns:
- varNames = df['varName'].tolist()
- except Exception as e:
- print(f"获取变量名失败: {e}")
- varNames = []
+ self.statusLabel.setText("正在加载变量列表...")
+ if not self.historyDB or not hasattr(self.historyDB, 'getAllVarNames'):
+ self.statusLabel.setText("未连接历史数据库,无法获取变量列表")
+ return
+ varNames = self.historyDB.getAllVarNames()
+ # print(varNames)
+ # 对变量名称进行排序
+ varNames.sort(key=lambda x: x.lower())
+ # 添加排序后的变量到列表
for name in varNames:
- item = QListWidgetItem(str(name))
+ item = QListWidgetItem(name)
self.varListWidget.addItem(item)
-
-
- def showVarTrend(self, item):
- varName = item.text()
- # 查询该变量历史数据
- data, timeList = self.historyDB.queryVarHistory(varName)
- if not data or not timeList:
- self.trendWebView.setHtml('
无历史数据
')
+ self.statusLabel.setText(f"已加载 {len(varNames)} 个变量(已排序)")
+
+ def sortVarList(self):
+ # 手动排序变量列表
+ try:
+ varNames = []
+ for i in range(self.varListWidget.count()):
+ item = self.varListWidget.item(i)
+ if item and not item.isHidden():
+ varNames.append(item.text())
+ varNames.sort(key=lambda x: x.lower())
+ self.varListWidget.clear()
+ for name in varNames:
+ item = QListWidgetItem(name)
+ self.varListWidget.addItem(item)
+ self.statusLabel.setText(f"已排序 {len(varNames)} 个变量")
+ except Exception as e:
+ self.statusLabel.setText(f"排序失败: {str(e)}")
+
+ def addSelectedVarsToTrend(self):
+ # 将选中的变量添加到趋势图
+ selectedItems = self.varListWidget.selectedItems()
+ if not selectedItems:
+ self.statusLabel.setText("请先选择要添加的变量")
return
- # 构建趋势图
- line = Line(init_opts=opts.InitOpts(width='900px', height='500px'))
- line.add_xaxis(timeList)
- line.add_yaxis(varName, data)
- line.set_global_opts(
- title_opts=opts.TitleOpts(title=f"{varName} 历史趋势"),
- tooltip_opts=opts.TooltipOpts(trigger="axis"),
- xaxis_opts=opts.AxisOpts(type_="category", boundary_gap=False),
- datazoom_opts=[opts.DataZoomOpts(xaxis_index=0, range_start=0, range_end=100)],
+
+ addedCount = 0
+ for item in selectedItems:
+ varName = item.text()
+ if varName not in self.selectedVars:
+ self.selectedVars.append(varName)
+ self.loadVarData(varName)
+ addedCount += 1
+ if addedCount > 0:
+ self.updateMultiVarChart()
+ self.statusLabel.setText(f"已添加 {addedCount} 个变量到趋势图")
+ else:
+ self.statusLabel.setText("选中的变量已在趋势图中")
+
+ def onTimeRangeQuery(self):
+ # 查询按钮点击,记录时间范围
+ self._query_time_range = (
+ self.startTimeEdit.dateTime().toPyDateTime(),
+ self.endTimeEdit.dateTime().toPyDateTime()
)
- html = line.render_embed()
- page = QWebEnginePage(self.trendWebView)
- page.setHtml('\n\n' + html)
- self.trendWebView.setPage(page)
- self.trendWebView.reload()
-
-
-
-
+ import datetime
+ # 重新加载所有已选变量的数据
+ for varName in self.selectedVars:
+ start_time, end_time = self._query_time_range
+ if isinstance(start_time, datetime.datetime):
+ start_time = start_time.isoformat()
+ if isinstance(end_time, datetime.datetime):
+ end_time = end_time.isoformat()
+ if self.historyDB and hasattr(self.historyDB, 'queryVarHistory'):
+ data, timeList = self.historyDB.queryVarHistory(varName, start_time, end_time)
+ xData = []
+ for t in timeList:
+ dt = None
+ if isinstance(t, datetime.datetime):
+ dt = t
+ elif hasattr(t, 'to_pydatetime'):
+ dt = t.to_pydatetime()
+ elif isinstance(t, str):
+ try:
+ dt = datetime.datetime.fromisoformat(t)
+ except:
+ continue
+ if dt:
+ xData.append(dt)
+ self.multiVarData[varName] = {'x': xData, 'y': data}
+ self.updateMultiVarChart()
+ self.statusLabel.setText(f"已设置时间范围: {self._query_time_range[0].strftime('%Y-%m-%d %H:%M:%S')} ~ {self._query_time_range[1].strftime('%Y-%m-%d %H:%M:%S')}")
+
+ def loadVarData(self, varName):
+ # 加载单个变量的数据,按选定时间范围
+ try:
+ self.statusLabel.setText(f"加载 {varName} 数据...")
+ import datetime
+ if hasattr(self, '_query_time_range') and self._query_time_range:
+ start_time, end_time = self._query_time_range
+ else:
+ start_time = None
+ end_time = None
+ # 从数据库获取数据
+ if self.historyDB and hasattr(self.historyDB, 'queryVarHistory'):
+ print(start_time, end_time)
+ data, timeList = self.historyDB.queryVarHistory(varName, start_time, end_time)
+ # 直接使用数据库返回的时间,不做任何时区转换
+ xData = []
+ for t in timeList:
+ if isinstance(t, datetime.datetime):
+ xData.append(t)
+ elif isinstance(t, pd.Timestamp):
+ xData.append(t.to_pydatetime())
+ elif isinstance(t, str):
+ try:
+ # 直接按本地时间字符串解析
+ xData.append(datetime.datetime.fromisoformat(t))
+ except:
+ xData.append(datetime.datetime.now())
+ else:
+ xData.append(datetime.datetime.now())
+ # 存储数据
+ self.multiVarData[varName] = {'x': xData, 'y': data}
+ # 分配颜色
+ if varName not in self.varColors:
+ colors = ['blue', 'red', 'green', 'cyan', 'magenta', 'yellow', 'black']
+ colorIndex = len(self.varColors) % len(colors)
+ self.varColors[varName] = colors[colorIndex]
+ except Exception as e:
+ self.statusLabel.setText(f"加载 {varName} 数据失败: {str(e)}")
+
+ def updateMultiVarChart(self):
+ import matplotlib.dates as mdates
+ # 更新多变量图表
+ self.ax_main.clear()
+ self.ax_overview.clear()
+ # 设置网格
+ self.ax_main.grid(True, alpha=0.3)
+ self.ax_overview.grid(True, alpha=0.3)
+ # 复用数据处理逻辑,提升性能
+ for varName in self.selectedVars:
+ if varName in self.multiVarData:
+ data = self.multiVarData[varName]
+ if data['x'] and data['y']:
+ # 处理断线数据(复用逻辑)
+ x_processed, y_processed = self._process_break_line_data(data['x'], data['y'])
+
+ # 缩略图:显示全量数据
+ self.ax_overview.plot(x_processed, y_processed, color=self.varColors[varName], linewidth=1, alpha=0.7)
+
+ # 主图:根据选中区间显示数据
+ if self._selected_range is not None:
+ xlim = self._selected_range
+ # 筛选区间数据
+ x_num = [mdates.date2num(xx) for xx in x_processed if xx is not None]
+ x_filtered, y_filtered = [], []
+ for xx, yy in zip(x_processed, y_processed):
+ if xx is not None:
+ xn = mdates.date2num(xx)
+ if xlim[0] <= xn <= xlim[1]:
+ x_filtered.append(xx)
+ y_filtered.append(yy)
+ x_main, y_main = x_filtered, y_filtered
+ else:
+ # 没有选择范围时,主图显示全部数据
+ x_main, y_main = x_processed, y_processed
+
+ self.ax_main.plot(x_main, y_main, color=self.varColors[varName], linewidth=2, marker='o', markersize=4, label=varName)
+ # 设置时间格式
+ self.ax_main.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
+ # 增加主图和缩略图的坐标轴刻度数量,并显示完整日期时间
+ from matplotlib.ticker import MaxNLocator
+ self.ax_main.xaxis.set_major_locator(mdates.AutoDateLocator())
+ self.ax_main.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d %H:%M:%S'))
+ self.ax_main.yaxis.set_major_locator(MaxNLocator(nbins=16, prune=None))
+ self.ax_overview.xaxis.set_major_locator(mdates.AutoDateLocator())
+ self.ax_overview.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d %H:%M:%S'))
+ self.ax_overview.yaxis.set_major_locator(MaxNLocator(nbins=16, prune=None))
+ # 刻度数量更多
+ self.ax_main.xaxis.set_major_locator(MaxNLocator(nbins=16, prune=None))
+ self.ax_overview.xaxis.set_major_locator(MaxNLocator(nbins=16, prune=None))
+ # 设置标签
+ self.ax_main.set_title('历史趋势图', fontsize=12, fontweight='bold')
+ self.ax_main.set_ylabel('数值')
+ self.ax_overview.set_xlabel('时间')
+ self.ax_overview.set_ylabel('数值')
+ # 添加图例
+ if self.selectedVars:
+ self.ax_main.legend(loc='upper right')
+ # 自动调整布局
+ self.figure.tight_layout()
+ # 刷新画布
+ self.canvas.draw()
+
+ def _process_break_line_data(self, x_raw, y_raw):
+ """处理断线数据:间隔大于2分钟的点不连线"""
+ x, y = [], []
+ prev_time = None
+ for xi, yi in zip(x_raw, y_raw):
+ if prev_time is not None and (xi - prev_time).total_seconds() > 60:
+ # 断线时只在y中插入None,x继续插入当前时间戳
+ x.append(xi)
+ y.append(None)
+ x.append(xi)
+ y.append(yi)
+ prev_time = xi
+ return x, y
+
+ def filterVarList(self, text):
+ # 本地过滤变量列表
+ for i in range(self.varListWidget.count()):
+ item = self.varListWidget.item(i)
+ if item is not None:
+ item.setHidden(text.lower() not in item.text().lower())
+
+ def onVarSelected(self):
+ # 当选中一个变量时(非双击)
+ selectedItems = self.varListWidget.selectedItems()
+ if selectedItems:
+ self.statusLabel.setText(f"已选择: {selectedItems[0].text()}")
+
+ def showVarTrend(self, item):
+ # 显示选中变量的趋势图(与多变量流程一致)
+ varName = item.text()
+ self.currentVarName = varName
+ self.statusLabel.setText(f"加载 {varName} 历史趋势...")
+ # 清空多变量数据,只保留当前变量
+ self.selectedVars = [varName]
+ self.multiVarData = {}
+ self.varColors = {}
+ self.loadVarData(varName)
+ # 更新UI信息
+ if varName in self.multiVarData:
+ data = self.multiVarData[varName]['y']
+ xdata = self.multiVarData[varName]['x']
+ else:
+ data = []
+ xdata = []
+ self.varNameLabel.setText(f"当前变量: {varName}")
+ self.dataCountLabel.setText(f"数据点: {len(data)}")
+ if xdata:
+ import datetime
+ import pandas as pd
+ def to_dt(t):
+ if isinstance(t, datetime.datetime):
+ return t
+ elif isinstance(t, pd.Timestamp):
+ return t.to_pydatetime()
+ elif isinstance(t, (int, float)):
+ return datetime.datetime.fromtimestamp(t)
+ else:
+ return datetime.datetime.now()
+ start_time = to_dt(xdata[0]).strftime('%Y-%m-%d %H:%M')
+ end_time = to_dt(xdata[-1]).strftime('%Y-%m-%d %H:%M')
+ self.timeRangeLabel.setText(f"时间范围: {start_time} - {end_time}")
+ else:
+ self.timeRangeLabel.setText("时间范围: 无数据")
+ # 刷新多变量趋势图
+ self.updateMultiVarChart()
+ self.resetZoom()
+ self.statusLabel.setText(f"{varName} 趋势已显示")
+
+ def updateChart(self):
+ # 用当前数据更新图表
+ self.ax_main.clear()
+ self.ax_overview.clear()
+
+ # 设置网格
+ self.ax_main.grid(True, alpha=0.3)
+ self.ax_overview.grid(True, alpha=0.3)
+
+ # 绘制数据
+ if self.xData and self.yData:
+ self.ax_main.plot(self.xData, self.yData,
+ color='blue',
+ linewidth=2,
+ marker='o',
+ markersize=4,
+ label=self.currentVarName)
+
+ # 概览图(降采样)
+ step = max(1, len(self.xData) // 10)
+ x_overview = self.xData[::step]
+ y_overview = self.yData[::step]
+ self.ax_overview.plot(x_overview, y_overview,
+ color='blue',
+ linewidth=1,
+ alpha=0.7)
+
+ # 设置时间格式
+ self.ax_main.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
+ self.ax_overview.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S'))
+
+ # 设置标签
+ self.ax_main.set_title('历史趋势图', fontsize=12, fontweight='bold')
+ self.ax_main.set_ylabel('数值')
+ self.ax_overview.set_xlabel('时间')
+ self.ax_overview.set_ylabel('数值')
+
+ # 添加图例
+ if self.currentVarName:
+ self.ax_main.legend(loc='upper right')
+
+ # 自动调整布局
+ self.figure.tight_layout()
+
+ # 刷新画布
+ self.canvas.draw()
+
+ def on_mouse_press(self, event):
+ # 鼠标左键按下,准备平移
+ if event.inaxes == self.ax_main and event.button == 1 and event.xdata is not None:
+ self._is_panning = True
+ self._pan_start_x = event.xdata
+ self._pan_start_xlim = self.ax_main.get_xlim()
+
+ def on_mouse_release(self, event):
+ # 鼠标左键松开,结束平移
+ if self._is_panning:
+ self._is_panning = False
+ self._pan_start_x = None
+ self._pan_start_xlim = None
+ # 拖拽结束后再重绘
+ self.canvas.draw_idle()
+
+ def on_mouse_move(self, event):
+ # 鼠标移动事件,显示悬浮气泡+平移主图
+ if self._is_panning and event.inaxes == self.ax_main and event.xdata is not None and self._pan_start_x is not None:
+ dx = event.xdata - self._pan_start_x
+ xlim0, xlim1 = self._pan_start_xlim
+ self.ax_main.set_xlim(xlim0 - dx, xlim1 - dx)
+ # 拖拽时不重绘,提高流畅度
+ # self.canvas.draw_idle()
+ # 悬浮气泡逻辑(不变)
+ if event.inaxes == self.ax_main and event.xdata is not None and event.ydata is not None:
+ info_text = f"时间: {mdates.num2date(event.xdata).strftime('%Y-%m-%d %H:%M:%S')}
"
+ if self.selectedVars:
+ for varName in self.selectedVars:
+ if varName in self.multiVarData:
+ data = self.multiVarData[varName]
+ if data['x'] and data['y']:
+ closest_idx = self.find_closest_point(data['x'], event.xdata)
+ if closest_idx is not None and closest_idx < len(data['y']):
+ value = data['y'][closest_idx]
+ info_text += f"{varName}: {value:.3f}
"
+ self.infoBubble.setText(f"{info_text}")
+ x = int(event.guiEvent.x())
+ y = int(event.guiEvent.y())
+ self.infoBubble.move(x + 16, y + 8)
+ self.infoBubble.adjustSize()
+ self.infoBubble.setVisible(True)
+ else:
+ self.infoBubble.setVisible(False)
+ # self.canvas.draw() # 性能优化:去除重绘
+
+ def on_mouse_leave(self, event):
+ # 鼠标离开主图时隐藏气泡
+ self.infoBubble.setVisible(False)
+
+ def find_closest_point(self, x_data, target_x):
+ # 找到最接近目标X值的数据点索引
+ if not x_data:
+ return None
+
+ # 将datetime转换为matplotlib日期
+ if isinstance(x_data[0], datetime.datetime):
+ x_nums = mdates.date2num(x_data)
+ else:
+ x_nums = x_data
+
+ # 使用二分查找
+ left = 0
+ right = len(x_nums) - 1
+ while left <= right:
+ mid = (left + right) // 2
+ if x_nums[mid] == target_x:
+ return mid
+ elif x_nums[mid] < target_x:
+ left = mid + 1
+ else:
+ right = mid - 1
+ # 找到最接近的点
+ if left >= len(x_nums):
+ return len(x_nums) - 1
+ elif right < 0:
+ return 0
+ else:
+ if abs(x_nums[left] - target_x) < abs(x_nums[right] - target_x):
+ return left
+ else:
+ return right
+
+ def on_overview_xlim_changed(self, event_ax):
+ # 当概览图范围变化时更新主图
+ if event_ax == self.ax_overview:
+ x1, x2 = self.ax_overview.get_xlim()
+ self.ax_main.set_xlim(x1)
+ self.canvas.draw()
+
+ def resetZoom(self):
+ # 重置到完整视图
+ if self.xData:
+ self.ax_main.set_xlim(min(self.xData), max(self.xData))
+ self.ax_overview.set_xlim(min(self.xData), max(self.xData))
+ self.canvas.draw()
+
+ def zoomBack(self):
+ # 退到上一个视图
+ if self.history and self.historyIndex > 0:
+ self.historyIndex -= 1
+ self.applyZoom(self.history[self.historyIndex])
+ self.backBtn.setEnabled(self.historyIndex > 0)
+ self.forwardBtn.setEnabled(True)
+
+ def zoomForward(self):
+ # 进到下一个视图
+ if self.history and self.historyIndex < len(self.history) - 1:
+ self.historyIndex += 1
+ self.applyZoom(self.history[self.historyIndex])
+ self.forwardBtn.setEnabled(self.historyIndex < len(self.history) - 1)
+ self.backBtn.setEnabled(True)
+ def applyZoom(self, zoom_range):
+ # 应用指定的缩放范围
+ x1, x2 = zoom_range
+ self.ax_main.set_xlim(x1, x2)
+ self.canvas.draw()
+
+ def enableRectZoom(self):
+ # 启用框选缩放模式
+ self.statusLabel.setText("模式: 框选缩放 - 在概览图中拖动选择区域")
+
+ def enablePanMode(self):
+ # 启用平移模式
+ self.statusLabel.setText("模式: 平移视图 - 在主图中拖动移动视图")
+ def onProjectChanged(self):
+ # 项目更改时处理
+ self.exchangeProject()
+ def clearAllVars(self):
+ # 清除趋势图中的所有变量
+ self.selectedVars = []
+ self.multiVarData = {}
+ self.varColors = {}
+ self.ax_main.clear()
+ self.ax_overview.clear()
+ self.ax_main.grid(True, alpha=0.3)
+ self.ax_overview.grid(True, alpha=0.3)
+ self.canvas.draw()
+ self.statusLabel.setText("趋势图已清除")
+ self.varNameLabel.setText("当前变量: 无")
+ self.dataCountLabel.setText("数据点: 0")
+ self.timeRangeLabel.setText("时间范围: 无数据")
+ self.resetZoom()
+
+ def on_select(self, xmin, xmax):
+ # SpanSelector回调,记录选中区间并更新主趋势图
+ self._selected_range = (xmin, xmax)
+ self.updateMultiVarChart()
+
+ def on_overview_double_click(self, event):
+ # 双击下方缩略图恢复全局视图
+ if event.dblclick and event.inaxes == self.ax_overview:
+ self.ax_main.set_xlim(auto=True)
+ self.canvas.draw_idle()
+
+ def on_mouse_wheel(self, event):
+ # 鼠标滚轮缩放主趋势图X轴
+ if event.inaxes == self.ax_main and event.xdata is not None:
+ xlim = self.ax_main.get_xlim()
+ x_center = event.xdata
+ # 缩放因子,滚轮向上放大,向下缩小
+ scale_factor = 0.8 if event.button == 'up' else 1.25
+ x_left = x_center - (x_center - xlim[0]) * scale_factor
+ x_right = x_center + (xlim[1] - x_center) * scale_factor
+ self.ax_main.set_xlim(x_left, x_right)
+ self.canvas.draw_idle()
+
+ def onQuickRangeChanged(self, idx):
+ from PyQt5.QtCore import QDateTime
+ now = QDateTime.currentDateTime()
+ if idx == 0: # 最近一天
+ self.startTimeEdit.setDateTime(now.addDays(-1))
+ self.endTimeEdit.setDateTime(now)
+ elif idx == 1: # 最近6小时
+ self.startTimeEdit.setDateTime(now.addSecs(-6*3600))
+ self.endTimeEdit.setDateTime(now)
+ elif idx == 2: # 最近1小时
+ self.startTimeEdit.setDateTime(now.addSecs(-3600))
+ self.endTimeEdit.setDateTime(now)
+ elif idx == 3: # 最近30分钟
+ self.startTimeEdit.setDateTime(now.addSecs(-1800))
+ self.endTimeEdit.setDateTime(now)
+ # idx==4为自定义,不做处理
+ # 自动触发查询
+ if idx != 4:
+ self.onTimeRangeQuery()
+
+# 使用示例
+if __name__ == "__main__":
+ app = QApplication(sys.argv)
+ window = TrendWidgets()
+ window.show()
+ sys.exit(app.exec_())
\ No newline at end of file
diff --git a/UI/VarManages/AnalogModel.py b/UI/VarManages/AnalogModel.py
index 07c0cb8..5a60b87 100644
--- a/UI/VarManages/AnalogModel.py
+++ b/UI/VarManages/AnalogModel.py
@@ -107,6 +107,18 @@ class AnalogButtonDelegate(TcRtdButtonDelegate):
def __init__(self, parent=None):
super(AnalogButtonDelegate, self).__init__(parent)
+ self.trendWindows = {} # 保存所有打开的趋势窗口
+
+ def trend_action(self):
+ sender = self.sender()
+ model = self.parent().model
+ varName = model.datas[sender.index[0]][3]
+ # 以变量名为key,避免重复打开
+ if varName not in self.trendWindows or self.trendWindows[varName] is None:
+ self.trendWindows[varName] = ActualTrend(varName=varName)
+ self.trendWindows[varName].show()
+ self.trendWindows[varName].raise_()
+ self.trendWindows[varName].activateWindow()
def edit_action(self):
sender = self.sender()
diff --git a/UI/VarManages/TCRTDModel.py b/UI/VarManages/TCRTDModel.py
index 956ae94..55b5137 100644
--- a/UI/VarManages/TCRTDModel.py
+++ b/UI/VarManages/TCRTDModel.py
@@ -6,6 +6,7 @@ from PyQt5.QtWidgets import QHBoxLayout, QWidget, QMessageBox, QComboBox
from protocol.TCP.TemToMv import temToMv
from model.ProjectModel.VarManage import *
from UI.VarManages.ModbusModel import *
+from UI.TrendManage.ActualTrendWidget import ActualTrend
from utils import Globals
import re
@@ -109,6 +110,7 @@ class TcRtdButtonDelegate(VarButtonDelegate):
def __init__(self, parent=None):
super(TcRtdButtonDelegate, self).__init__(parent)
+ self.trendWindows = {} # 保存所有打开的趋势窗口
def paint(self, painter, option, index):
if not self.parent().indexWidget(index):
@@ -127,15 +129,24 @@ class TcRtdButtonDelegate(VarButtonDelegate):
clicked=self.edit_action
)
+ button3 = QPushButton(
+ qtawesome.icon('fa.line-chart', color='#393c4e'),
+ "",
+ self.parent(),
+ clicked=self.trend_action
+ )
+
button1.clicked.connect(self.start_action)
button2.clicked.connect(self.edit_action)
+ button3.clicked.connect(self.trend_action)
button2.oldName = False
button2.isEdit = True
button1.index = [index.row(), index.column()]
button2.index = [index.row(), index.column()]
+ button3.index = [index.row(), index.column()]
data = self.parent().model.datas[index.row()]
@@ -149,6 +160,7 @@ class TcRtdButtonDelegate(VarButtonDelegate):
h_box_layout = QHBoxLayout()
h_box_layout.addWidget(button1)
h_box_layout.addWidget(button2)
+ h_box_layout.addWidget(button3)
h_box_layout.setContentsMargins(2, 0, 0, 2)
h_box_layout.setAlignment(Qt.AlignCenter)
widget = QWidget()
@@ -262,6 +274,17 @@ class TcRtdButtonDelegate(VarButtonDelegate):
varMes.append('')
model.insert_data(varMes, rowIndex)
model.remove_row(rowIndex + 1)
+
+ def trend_action(self):
+ sender = self.sender()
+ model = self.parent().model
+ varName = model.datas[sender.index[0]][3]
+ # 以变量名为key,避免重复打开
+ if varName not in self.trendWindows or self.trendWindows[varName] is None:
+ self.trendWindows[varName] = ActualTrend(varName=varName)
+ self.trendWindows[varName].show()
+ self.trendWindows[varName].raise_()
+ self.trendWindows[varName].activateWindow()
class TcRtdTypeDelegate(TcRtdButtonDelegate):
def __init__(self, parent=None):
diff --git a/model/HistoryDBModel/HistoryDBManage.py b/model/HistoryDBModel/HistoryDBManage.py
index 888d11c..c1c3b8b 100644
--- a/model/HistoryDBModel/HistoryDBManage.py
+++ b/model/HistoryDBModel/HistoryDBManage.py
@@ -88,9 +88,8 @@ class HistoryDBManage:
pass
def writeVarValue(self, varName, value):
- # 入队时记录当前时间
- enqueue_time = datetime.now(timezone(timedelta(hours=8)))
- # print(enqueue_time)
+ # 入队时记录中国时区时间
+ enqueue_time = datetime.now()
self.writeQueue.put((varName, value, enqueue_time))
def _writeWorker(self):
@@ -105,39 +104,62 @@ class HistoryDBManage:
point = Point(self.table).tag("varName", varName).field("value", float(value)).time(enqueue_time)
self.client.write(point)
- def queryVarHistory(self, varName, startTime=None, endTime=None, limit=1000):
+ def queryVarHistory(self, varName, startTime=None, endTime=None):
where = f'"varName" = \'{varName}\''
if startTime:
where += f" AND time >= '{startTime}'"
if endTime:
where += f" AND time <= '{endTime}'"
- sql = f'SELECT time, value FROM "{self.table}" WHERE {where} ORDER BY time LIMIT {limit}'
+ # 关键:用tz()函数将time字段转为中国时区
+ sql = f'SELECT tz(time, \'Asia/Shanghai\') AS time, value FROM "{self.table}" WHERE {where} ORDER BY time ASC LIMIT 100000'
+ # print(f"执行SQL查询: {sql}")
try:
df = self.client.query(sql, mode="pandas")
import pandas as pd
if isinstance(df, pd.DataFrame):
data = df["value"].tolist() if "value" in df.columns else []
timeList = df["time"].tolist() if "time" in df.columns else []
+ print(f"查询到 {len(data)} 个数据点")
else:
+ # print(f"查询结果不是DataFrame: {type(df)}")
data, timeList = [], []
except Exception as e:
print(f"查询结果处理失败: {e}")
data, timeList = [], []
return data, timeList
+ def getAllVarNames(self):
+ """获取所有去重后的变量名列表"""
+ sql = f'SELECT DISTINCT("varName") FROM "{self.table}"'
+ try:
+ df = self.client.query(sql, mode="pandas")
+ import pandas as pd
+ if isinstance(df, pd.DataFrame) and 'varName' in df.columns:
+ return df['varName'].tolist()
+ else:
+ return []
+ except Exception as e:
+ print(f"获取变量名失败: {e}")
+ return []
+
@classmethod
def deleteTable(cls, table):
token = cls.getAPIToken()
host = 'http://localhost:8181'
- url = f"{host.rstrip('/')}/api/v3/configure/table?db={'dcs'}&table={table}"
+ import datetime
+ # 获取当前UTC时间+1分钟,去除微秒,Z结尾
+ now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc, microsecond=0)
+ hardDeleteAt = (now + datetime.timedelta(minutes=1)).isoformat().replace('+00:00', 'Z')
+ url = f"{host.rstrip('/')}/api/v3/configure/table?db={'dcs'}&table={table}&hard_delete_at={hardDeleteAt}"
headers = {
'Authorization': f'Bearer {token}'
}
response = requests.delete(url, headers=headers)
+ print(hardDeleteAt)
if response.status_code != 200:
print(f"删除失败: {response.status_code} {response.text}")
else:
- print(f"已删除表 {table} 的所有历史数据。")
+ print(f"已硬删除表 {table} 的所有历史数据。")
def stopWriteThread(self):
self._stopWriteThread.set()
diff --git a/model/ProjectModel/ProjectManage.py b/model/ProjectModel/ProjectManage.py
index 257b405..8434602 100644
--- a/model/ProjectModel/ProjectManage.py
+++ b/model/ProjectModel/ProjectManage.py
@@ -107,8 +107,9 @@ class ProjectManage(object):
try:
if name == Globals.getValue('currentPro'):
self.closePopen()
+ Globals.getValue('protocolManage').shutdown()
Globals.getValue('currentProDB').close()
- Globals.setValue('currentPro', None)
+ Globals.setValue('currentPro', None)
QTimer.singleShot(1000, lambda:shutil.rmtree(os.path.join('project', name)))
except OSError as e:
print(e)
diff --git a/model/ProjectModel/VarManage.py b/model/ProjectModel/VarManage.py
index 1b2fa08..bb96090 100644
--- a/model/ProjectModel/VarManage.py
+++ b/model/ProjectModel/VarManage.py
@@ -17,11 +17,10 @@ from protocol.ProtocolManage import ProtocolManage
# 获取ProtocolManage单例或全局对象(假设为全局唯一,实际项目可根据实际情况调整)
-def getProtocolManager():
+def refreshCache():
protocolManagerInstance = Globals.getValue('protocolManage')
- if protocolManagerInstance is None:
- protocolManagerInstance = ProtocolManage()
- return protocolManagerInstance
+ if protocolManagerInstance:
+ protocolManagerInstance.refreshVarCache()
class ModbusVarManage(object):
@@ -55,7 +54,7 @@ class ModbusVarManage(object):
modbusVarType.createVar(varName = varName, varType = varType, des = des, address = address,
slaveID = slaveID, min = min, max = max, order = order, varModel = varModel)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
# def importModbusVar(self, path):
@@ -162,7 +161,7 @@ class ModbusVarManage(object):
modbusVarType.createVar(varName = varName, varType = varType, des = des, address = address,
slaveID = slaveID, min = min, max = max, order = order, varModel = varModel)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
@@ -172,7 +171,7 @@ class ModbusVarManage(object):
# print(name)
self.getVarClass(modbusType).deleteVar(name = name)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
@@ -201,7 +200,7 @@ class ModbusVarManage(object):
self.getVarClass(modbusType).update(varName = Nname, description = des, varType = varType, address = address, slaveID = slaveID,
min = min, max = max, order = order, varModel = varModel).where(self.getVarClass(modbusType).varName == name).execute()
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def editOrder(self, name, order, modbusType):
@@ -209,7 +208,7 @@ class ModbusVarManage(object):
order = str(order)
self.getVarClass(modbusType).update(order = order).where(self.getVarClass(modbusType).varName == name).execute()
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def editVarModel(self, name, varModel, modbusType):
@@ -217,7 +216,7 @@ class ModbusVarManage(object):
varModel = str(varModel)
self.getVarClass(modbusType).update(varModel = varModel).where(self.getVarClass(modbusType).varName == name).execute()
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def getAllVar(self, modbusType):
@@ -255,7 +254,7 @@ class HartVarManage(object):
hartVarType = HartVar()
hartVarType.createVar(varName = varName, des = des, varModel = varModel)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def getAllVar(self):
@@ -276,7 +275,7 @@ class HartVarManage(object):
# print(name)
HartVar.deleteVar(name = name)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
@@ -298,7 +297,7 @@ class HartVarManage(object):
else:
HartVar.update(varName = Nname, description = des, varModel = varModel).where(HartVar.varName == name).execute()
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def editVarModel(self, name, varModel):
@@ -306,7 +305,7 @@ class HartVarManage(object):
print('修改变量模型',name)
HartVar.update(varModel = str(varModel)).where(HartVar.varName == name).execute()
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
@@ -332,7 +331,7 @@ class HartVarManage(object):
hartVarType = HartVar()
hartVarType.createVar(varName = varName, des = des, varModel = varModel)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
class TcRtdManage(object):
@@ -352,7 +351,7 @@ class TcRtdManage(object):
tcRtdVarType = TcRtdVar()
tcRtdVarType.createVar(varName = varName, channelNumber=channelNumber, des = des, varType = varType, min = min, max = max, compensationVar = compensationVar, varModel = varModel)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def getAllVar(self):
@@ -373,7 +372,7 @@ class TcRtdManage(object):
# print(name)
TcRtdVar.deleteVar(name = name)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
@@ -399,7 +398,7 @@ class TcRtdManage(object):
else:
TcRtdVar.update(varName=Nname, channelNumber = channelNumber, description=des, varType=varType, min=min, max=max, compensationVar = compensationVar).where(TcRtdVar.varName == name).execute()
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def getByName(self, name):
@@ -425,14 +424,14 @@ class TcRtdManage(object):
name = str(name)
TcRtdVar.update(varType = str(varType)).where(TcRtdVar.varName == name).execute()
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def editVarModel(self, name, varModel):
name = str(name)
TcRtdVar.update(varModel = str(varModel)).where(TcRtdVar.varName == name).execute()
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def importVarForm(self, varName, channelNumber, varType, des, min, max, compensationVar, varModel):
@@ -443,7 +442,7 @@ class TcRtdManage(object):
tcRtdVarType = TcRtdVar()
tcRtdVarType.createVar(varName = varName, channelNumber=channelNumber, des = des, varType = varType, min = min, max = max, compensationVar = compensationVar, varModel = varModel)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
class AnalogManage(object):
def __init__(self):
@@ -462,7 +461,7 @@ class AnalogManage(object):
analogVarType = AnalogVar()
analogVarType.createVar(varName = varName, channelNumber = channelNumber, des = des, varType = varType, min = min, max = max, varModel = varModel)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def getAllVar(self):
@@ -483,7 +482,7 @@ class AnalogManage(object):
# print(name)
AnalogVar.deleteVar(name = name)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
@@ -507,14 +506,14 @@ class AnalogManage(object):
else:
AnalogVar.update(varName=Nname, channelNumber =channelNumber, description=des, varType=varType, min=min, max=max).where(AnalogVar.varName == name).execute()
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def editVarModel(self, name, varModel):
name = str(name)
AnalogVar.update(varModel = str(varModel)).where(AnalogVar.varName == name).execute()
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def getByName(self, name):
# 查询指定变量信息
@@ -565,7 +564,7 @@ class AnalogManage(object):
analogVarType = AnalogVar()
analogVarType.createVar(varName = varName, channelNumber = channelNumber, des = des, varType = varType, min = min, max = max, varModel = varModel)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@@ -585,7 +584,7 @@ class HartSimulateVarManage(object):
hartSimulateVarType = HartSimulateVar()
hartSimulateVarType.createVar(varName = varName, des = des,varModel = varModel)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def getAllVar(self):
@@ -606,7 +605,7 @@ class HartSimulateVarManage(object):
# print(name)
HartSimulateVar.deleteVar(name = name)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
@@ -627,14 +626,14 @@ class HartSimulateVarManage(object):
else:
HartSimulateVar.update(varName = Nname, description = des, varModel = varModel).where(HartSimulateVar.varName == name).execute()
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def editVarModel(self, name, varModel):
name = str(name)
HartSimulateVar.update(varModel = str(varModel)).where(HartSimulateVar.varName == name).execute()
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
@classmethod
def getByName(self, name):
# 查询指定变量信息
@@ -658,7 +657,7 @@ class HartSimulateVarManage(object):
hartSimulateVarType = HartSimulateVar()
hartSimulateVarType.createVar(varName = varName, des = des,varModel = varModel)
# 操作后刷新缓存
- getProtocolManager().refreshVarCache()
+ refreshCache()
class GlobalVarManager(object):
diff --git a/protocol/ProtocolManage.py b/protocol/ProtocolManage.py
index 9fe16ad..da94164 100644
--- a/protocol/ProtocolManage.py
+++ b/protocol/ProtocolManage.py
@@ -229,6 +229,7 @@ class ProtocolManage(object):
def _readVariableValueOriginal(self, variableName):
# 完全保留原有读取逻辑
+ # print(12)
varInfo = self.lookupVariable(variableName)
value = None
if not varInfo:
@@ -315,6 +316,10 @@ class ProtocolManage(object):
def shutdown(self):
self.tcpVarManager.shutdown()
+ # 关闭后台读取线程
+ if hasattr(self, 'readThreadStop') and hasattr(self, 'readThread'):
+ self.readThreadStop.set()
+ self.readThread.join(timeout=1)