From a5ec3feadda063b068d134497b39e989384894f6 Mon Sep 17 00:00:00 2001 From: zcwBit Date: Wed, 16 Jul 2025 15:29:26 +0800 Subject: [PATCH] =?UTF-8?q?0716=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UI/Main/Main.py | 67 +------ UI/ProjectManages/ProjectModel.py | 27 ++- UI/TrendManage/TrendWidget.py | 194 ++++++------------- model/ClientModel/Client.py | 2 + model/HistoryDBModel/HistoryDBManage.py | 247 ++++++++++++++---------- model/ProjectModel/ProjectManage.py | 9 +- model/ProjectModel/VarManage.py | 66 +++++++ protocol/ProtocolManage.py | 148 ++++++++------ protocol/TCP/TCPVarManage.py | 15 +- utils/Globals.py | 3 +- windoweffect/__init__.py | 2 + windoweffect/c_structures.py | 135 +++++++++++++ windoweffect/window_effect.py | 222 +++++++++++++++++++++ 13 files changed, 759 insertions(+), 378 deletions(-) create mode 100644 windoweffect/__init__.py create mode 100644 windoweffect/c_structures.py create mode 100644 windoweffect/window_effect.py diff --git a/UI/Main/Main.py b/UI/Main/Main.py index d2e26dc..86d497e 100644 --- a/UI/Main/Main.py +++ b/UI/Main/Main.py @@ -14,7 +14,6 @@ from ctypes.wintypes import MSG from win32 import win32api, win32gui from win32.lib import win32con from model.ProjectModel.VarManage import GlobalVarManager -from windoweffect import WindowEffect, MINMAXINFO, NCCALCSIZE_PARAMS from UI.Main.MainLeft import MainLeft from UI.Main.MainTop import MainTop from ..ProjectManages.ProjectWidget import ProjectWidgets @@ -368,71 +367,7 @@ class MainWindow(QMainWindow): event.accept() - def nativeEvent(self, eventType, message): - """ 接受windows发送的信息 """ - msg = MSG.from_address(message.__int__()) - if msg.message == win32con.WM_NCHITTEST: - # 解决 issue #2 and issue #7 - r = self.devicePixelRatioF() - xPos = (win32api.LOWORD(msg.lParam) - - self.frameGeometry().x()*r) % 65536 - yPos = win32api.HIWORD(msg.lParam) - self.frameGeometry().y()*r - w, h = self.width()*r, self.height()*r - lx = xPos < self.BORDER_WIDTH - rx = xPos + 9 > w - self.BORDER_WIDTH - ty = yPos < self.BORDER_WIDTH - by = yPos > h - self.BORDER_WIDTH - if lx and ty: - return True, win32con.HTTOPLEFT - elif rx and by: - return True, win32con.HTBOTTOMRIGHT - elif rx and ty: - return True, win32con.HTTOPRIGHT - elif lx and by: - return True, win32con.HTBOTTOMLEFT - elif ty: - return True, win32con.HTTOP - elif by: - return True, win32con.HTBOTTOM - elif lx: - return True, win32con.HTLEFT - elif rx: - return True, win32con.HTRIGHT - elif msg.message == win32con.WM_NCCALCSIZE: - if self.__isWindowMaximized(msg.hWnd): - self.__monitorNCCALCSIZE(msg) - return True, 0 - elif msg.message == win32con.WM_GETMINMAXINFO: - if self.__isWindowMaximized(msg.hWnd): - window_rect = win32gui.GetWindowRect(msg.hWnd) - if not window_rect: - return False, 0 - - # get the monitor handle - monitor = win32api.MonitorFromRect(window_rect) - if not monitor: - return False, 0 - - # get the monitor info - __monitorInfo = win32api.GetMonitorInfo(monitor) - monitor_rect = __monitorInfo['Monitor'] - work_area = __monitorInfo['Work'] - - # convert lParam to MINMAXINFO pointer - info = cast(msg.lParam, POINTER(MINMAXINFO)).contents - - # adjust the size of window - info.ptMaxSize.x = work_area[2] - work_area[0] - info.ptMaxSize.y = work_area[3] - work_area[1] - info.ptMaxTrackSize.x = info.ptMaxSize.x - info.ptMaxTrackSize.y = info.ptMaxSize.y - - # modify the upper left coordinate - info.ptMaxPosition.x = abs(window_rect[0] - monitor_rect[0]) - info.ptMaxPosition.y = abs(window_rect[1] - monitor_rect[1]) - return True, 1 - - return QWidget.nativeEvent(self, eventType, message) + def __isWindowMaximized(self, hWnd) -> bool: diff --git a/UI/ProjectManages/ProjectModel.py b/UI/ProjectManages/ProjectModel.py index 6176267..dee4201 100644 --- a/UI/ProjectManages/ProjectModel.py +++ b/UI/ProjectManages/ProjectModel.py @@ -216,23 +216,36 @@ class ProjectButtonDelegate(QItemDelegate): "请先保存工程", QMessageBox.Yes) return - self.loginWidget = LoginWidget() - self.loginWidget.show() - ProjectManage.switchProject(str(self.parent().model.datas[sender.index[0]][1])) - #初始化读取数据库数据 + + # 初始化读取数据库数据,添加进度条 + progress = QtWidgets.QProgressDialog() + progress.setLabelText("正在加载工程数据...") + progress.setCancelButton(None) + progress.setMinimum(0) + progress.setMaximum(0) + progress.setWindowModality(QtCore.Qt.ApplicationModal) + progress.setMinimumDuration(0) + progress.show() + QtWidgets.QApplication.processEvents() + ProjectManage.switchProject(str(self.parent().model.datas[sender.index[0]][1])) modelLists = ['ModbusTcpMasterTable', 'ModbusTcpSlaveTable', 'ModbusRtuMasterTable', \ 'ModbusRtuSlaveTable', 'HartTable', 'TcRtdTable', 'AnalogTable', 'HartSimulateTable', 'userTable'] for l in modelLists: Globals.getValue(l).model.initTable() + QtWidgets.QApplication.processEvents() profibusTabWidgets = ['DP主站', 'DP从站', 'PA主站', 'PA从站'] for widget in profibusTabWidgets: - Globals.getValue(widget).switchProject() - - Globals.getValue('HistoryWidget').exchangeProject() + Globals.getValue(widget).switchProject() + QtWidgets.QApplication.processEvents() + progress.close() + QtWidgets.QApplication.processEvents() + self.parent().proxy.invalidate() + self.loginWidget = LoginWidget() + self.loginWidget.show() def edit_action(self): diff --git a/UI/TrendManage/TrendWidget.py b/UI/TrendManage/TrendWidget.py index c314be5..c4e1502 100644 --- a/UI/TrendManage/TrendWidget.py +++ b/UI/TrendManage/TrendWidget.py @@ -65,172 +65,94 @@ class HistoryTrend(object): class TrendWidgets(QWidget): def __init__(self, parent=None): super(TrendWidgets, self).__init__(parent) - self.memLsit = [] # 所有mem列表 - - self.setAttribute(Qt.WA_StyledBackground, True) - + # self.setAttribute(Qt.WA_StyledBackground, True) # 移除无效属性 self.gridLayout = QtWidgets.QGridLayout(self) self.gridLayout.setObjectName("gridLayout") - self.timeBox = QtWidgets.QComboBox() - self.timeBox.setObjectName("timeBox") - self.timeBox.activated.connect(self.refreshList) - - self.listview = QListView() - self.listview.setObjectName("trendListView") - - self.model = QStandardItemModel() - - - self.listview.setContextMenuPolicy(Qt.CustomContextMenu) - self.listview.customContextMenuRequested.connect(self.listContext) - self.model.itemChanged.connect(self.itemCheckstate) - - # self.widget = QtWidgets.QWidget(self) + # 变量列表 + 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.proxy = QtCore.QSortFilterProxyModel(self) - # self.listview.proxy = self.proxy - self.proxy.setSourceModel(self.model) - self.listview.setModel(self.proxy) - - self.delBtn = QtWidgets.QPushButton(QIcon(':/static/delete.png'), '删除记录', self) - self.delBtn.setObjectName('delBtn') - self.delBtn.setIconSize(QSize(22, 22)) - self.delBtn.clicked.connect(self.deleteMem) - - self.gridLayout.addWidget(self.timeBox, 0, 0, 1, 3) - self.gridLayout.addWidget(self.delBtn, 0, 3, 1, 1) - self.gridLayout.addWidget(self.listview, 1, 0, 14, 4) self.gridLayout.addWidget(self.trendWebView, 0, 4, 15, 22) self.gridLayout.setSpacing(10) self.gridLayout.setContentsMargins(20, 20, 20, 20) + Globals.setValue('HistoryWidget', self) - - - Globals.setValue('HistoryWidget', self) + + def exchangeProject(self): + self.historyDB = Globals.getValue('historyDBManage') + self.refreshVarList() def deleteMem(self): - if not self.memLsit: - return - self.historyDB.mem = self.memLsit[self.timeBox.currentIndex()] - self.historyDB.deleteMem() - InfluxMem.deleteMem(self.historyDB.mem) - self.getMems() - self.exchangeProject() - self.model.clear() + pass # 历史mem相关功能已废弃 def itemCheckstate(self): - # 勾选变量复选框事件 - allItems = self.model.findItems('*', Qt.MatchWildcard) - itemCheckedList = [] - for item in allItems: - if item.checkState() == Qt.Checked: - itemCheckedList.append(item.text()) - if not itemCheckedList: - return - self.lineTrend = HistoryTrend(self.trendWebView) - self.createHtml(itemCheckedList) - - - + pass # 历史mem相关功能已废弃 @QtCore.pyqtSlot(str) def on_lineEdit_textChanged(self, text): - # 搜索框文字变化函数 - search = QtCore.QRegExp(text, - QtCore.Qt.CaseInsensitive, - QtCore.QRegExp.RegExp - ) - - self.proxy.setFilterKeyColumn(0) - self.proxy.setFilterRegExp(search) - - + pass # 历史mem相关功能已废弃 def listContext(self, position): - # 点击右键删除事件 - menu = QtWidgets.QMenu() - listDel = QtWidgets.QAction('删除') - listDel.triggered.connect(self.delVarRecard) - menu.addAction(listDel) - menu.exec_(self.listview.mapToGlobal(position)) - + pass # 历史mem相关功能已废弃 def delVarRecard(self): - # 删除变量记录 - # count = self.listWidget.count() - # cb_list = [self.listWidget.itemWidget(self.listWidget.item(i)) - # for i in range(count)] - index = self.listview.currentIndex() - varName = self.model.item(index.row()).text() - self.model.removeRow(index.row()) - # self.memName = self.timeBox.currentText() - self.historyDB.mem = self.memLsit[self.timeBox.currentIndex()] - self.historyDB.deleteFun(str(varName)) + pass # 历史mem相关功能已废弃 - def exchangeProject(self): - self.timeBox.clear() - self.memLsit = [] - self.getMems() - self.refreshList(0) - self.trendWebView.setHtml('') def getMems(self): - # 获取所有的趋势表 - mems = InfluxMem.get_all() - self.proName = Globals.getValue('currentPro') - if not self.proName: - return - self.timeBox.clear() - if mems is 'Error': - return - for x in mems: - time = datetime.datetime.fromtimestamp(float(x.mem)).strftime("%Y-%m-%d %H:%M:%S") - if self.timeBox.findText(time) == -1: - self.timeBox.addItem(time) - self.memLsit.append(x.mem) - self.historyDB = HistoryDBManage(bucket = self.proName, isCelery = False) - # self.refreshList(self.timeBox.currentIndex()) + pass # 历史mem相关功能已废弃 def refreshList(self, index): - # 更新变量列表 - self.model.clear() - if self.memLsit == []: - return - self.historyDB.mem = self.memLsit[index] - # self.historyDB.startTime = int(float(self.memLsit[index])) - # print(self.memLsit[index]) - tagSet = self.historyDB.queryVarName() - # if not tagSet: - # QMessageBox.information(self, '提示', '当前工程历史趋势已损坏') - # return - for tag in tagSet: - item = QStandardItem(str(tag)) - item.setCheckable(True) - self.model.appendRow(item) - self.lineTrend = HistoryTrend(self.trendWebView) - - + pass # 历史mem相关功能已废弃 def createHtml(self, varNames): - # 创建趋势图网页 - self.historyDB.mem = self.memLsit[self.timeBox.currentIndex()] - xAxisList = [] - allData = [] - for var in varNames: - varData, xAxis = self.historyDB.queryFun(str(var)) - xAxisList.append(xAxis) - allData.append(varData) - xAxis = max(xAxisList, key=len) - self.lineTrend.addXAxis(xAxis) - [self.lineTrend.addYAxis(varNames[index], varData) for index, varData in enumerate(allData)] - # self.trendWebView.reload() + 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 = [] + for name in varNames: + item = QListWidgetItem(str(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('

无历史数据

') + 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)], + ) + html = line.render_embed() page = QWebEnginePage(self.trendWebView) - page.setHtml('\n\n' + self.lineTrend.html) + page.setHtml('\n\n' + html) self.trendWebView.setPage(page) self.trendWebView.reload() diff --git a/model/ClientModel/Client.py b/model/ClientModel/Client.py index 2d0d35f..8bea13c 100644 --- a/model/ClientModel/Client.py +++ b/model/ClientModel/Client.py @@ -47,6 +47,8 @@ class Client(object): from model.ProjectModel.ProjectManage import ProjectManage popen = Globals.getValue('popen') popenBeat = Globals.getValue('beatPopen') + histroyMan = Globals.getValue('historyDBManage') + histroyMan.close() if popen: popen.kill() if popenBeat: diff --git a/model/HistoryDBModel/HistoryDBManage.py b/model/HistoryDBModel/HistoryDBManage.py index 87d981f..888d11c 100644 --- a/model/HistoryDBModel/HistoryDBManage.py +++ b/model/HistoryDBModel/HistoryDBManage.py @@ -1,13 +1,14 @@ import os import time -from datetime import datetime +from datetime import datetime, timezone, timedelta -from influxdb_client import InfluxDBClient, Point, WritePrecision, BucketsApi -from influxdb_client.client.write_api import SYNCHRONOUS -from influxdb_client.client.util import date_utils -from influxdb_client.client.util.date_utils import DateHelper +from influxdb_client_3 import InfluxDBClient3, Point, WriteOptions, write_client_options import dateutil.parser from dateutil import tz +import pandas as pd +import requests +import queue +import threading @@ -17,106 +18,146 @@ def parseDate(date_string: str): return dateutil.parser.parse(date_string).astimezone(tz.gettz('ETC/GMT-8')) -class HistoryDBManage(): - org = "DCS" - # bucket = "history" - url = "http://localhost:8086" - - def __init__(self, bucket, mem = None, isCelery = True): - self.getToken(isCelery) - self.client = InfluxDBClient(url = self.url, token = self.token, org = self.org) - self.writeApi = self.client.write_api(write_options = SYNCHRONOUS) - self.deleteApi = self.client.delete_api() - self.queryApi = self.client.query_api() - self.bucketApi = self.client.buckets_api() - self.mem = mem - self.bucket = bucket - date_utils.date_helper = DateHelper() - date_utils.date_helper.parse_date = parseDate - # if self.mem: - self.startTime = 1680534 - - def __del__(self): - self.client.close() - - def getToken(self, isCelery): - if not isCelery: - with open('Static/InfluxDB.api', 'r', encoding='utf-8') as f: - self.token = f.read() - else: - with open('../../../Static/InfluxDB.api', 'r', encoding='utf-8') as f: - self.token = f.read() - - - - def writeFun(self, varName, value): - try: - value = int(value) - point = Point(self.mem).tag("varName", varName).field("value", value).time(datetime.utcnow(), WritePrecision.NS) - self.writeApi.write(self.bucket, self.org, point) - except Exception as e: - print(e) - BucketsApi(self.client).create_bucket(bucket_name = self.bucket, org = self.org) - - def queryFun(self, varName): - # startTime = time.mktime(time.strptime("%Y-%m-%d %H:%M:%S", mem)) - data = [] - timeList = [] - query = ' from(bucket:"{}")\ - |> range(start: {})\ - |> filter(fn:(r) => r._measurement == "{}")\ - |> filter(fn:(r) => r.varName == "{}")\ - |> filter(fn:(r) => r._field == "value" )'.format(self.bucket, self.startTime, self.mem, varName) - results = self.queryApi.query(query, org = self.org) - for result in results: - for record in result.records: - data.append(record['_value']) - timeList.append(record['_time'].strftime("%H:%M:%S:%f")[:-3]) - return data, timeList - - def deleteMem(self): - self.deleteApi.delete(start = '1970-01-01T00:00:00Z', stop = '2099-01-01T00:00:00Z', predicate = '_measurement={}'.format(self.mem), bucket = self.bucket, org = self.org, ) - - def deleteFun(self, varName): - self.deleteApi.delete(start = '1970-01-01T00:00:00Z', stop = '2099-01-01T00:00:00Z', predicate = '_measurement={} AND varName={}'.format(self.mem, varName), bucket = self.bucket, org = self.org, ) - - def deleteBucket(self): - bucket = self.bucketApi.find_bucket_by_name(self.bucket) - self.bucketApi.delete_bucket(bucket) - - def queryMem(self): - query = 'from(bucket: "{}") |> range(start: 0) |> keys(column: " _measurement")'.format(self.bucket) - result = self.queryApi.query(query) - return result - - def queryVarName(self): - tagSet = set() - query = ' from(bucket:"{}")\ - |> range(start: {})\ - |> filter(fn:(r) => r._measurement == "{}")\ - |> filter(fn:(r) => r._field == "value")'.format(self.bucket, self.startTime, self.mem) - try: - results = self.queryApi.query(query, org = self.org) - except Exception as e: - BucketsApi(self.client).create_bucket(bucket_name = self.bucket, org = self.org) - return tagSet - for result in results: - for record in result.records: - tagSet.add(record['varName']) - return tagSet +class HistoryDBManage: + def __init__(self, database='dcs', table='history', host="http://localhost:8181", token="", org="dcs"): + token = self.getAPIToken() if not token else token + self.database = database + self.table = table + self.host = host + self.token = token + self.org = org + + # 写入回调 + def onSuccess(conf, data, exception=None): + now_ms = datetime.now(timezone(timedelta(hours=8))) + # 统一转为字符串 + if isinstance(data, bytes): + data = data.decode(errors="ignore") + elif not isinstance(data, str): + data = str(data) + for line in data.split('\n'): + # print(line) + # m = re.search(r'enqueue_time_ms=([0-9]+)', line) + m = line.split(' ')[-1][:-3] + if m: + enqueue_time_ms = int(m) + # 转为datetime对象 + # print(enqueue_time_ms / 1000) + enqueue_time_dt = datetime.fromtimestamp(enqueue_time_ms /1000000, tz=timezone(timedelta(hours=8))) + # delay = now_ms - enqueue_time_ms + print(f"写入延迟: {1} ms (enqueue_time={enqueue_time_dt}, now={now_ms})") + def onError(conf, data, exception=None): + print("InfluxDB写入失败") + def onRetry(conf, data, exception=None): + print("InfluxDB写入重试") + + # 官方推荐批量写入配置 + self.client = InfluxDBClient3( + host=host, + token=token, + org=org, + database=database, + write_client_options=write_client_options( + write_options=WriteOptions(batch_size=5000, flush_interval=2000), # flush_interval单位ms + # success_callback=onSuccess, + error_callback=onError, + retry_callback=onRetry + ) + ) + + self.writeQueue = queue.Queue() + self._stopWriteThread = threading.Event() + self.writeThread = threading.Thread(target=self._writeWorker, daemon=True) + self.writeThread.start() + + @classmethod + def getAPIToken(cls): + try: + with open('Static/InfluxDB.api', 'r', encoding='utf-8') as f: + token = f.read() + except Exception as e: + print(f"读取token文件失败: {e}") + token = "" + return token + + def createDatabaseIfNotExists(self): + try: + sql = f'SELECT 1 FROM "{self.table}" LIMIT 1' + self.client.query(sql) + except Exception: + pass + + def writeVarValue(self, varName, value): + # 入队时记录当前时间 + enqueue_time = datetime.now(timezone(timedelta(hours=8))) + # print(enqueue_time) + self.writeQueue.put((varName, value, enqueue_time)) + + def _writeWorker(self): + while not self._stopWriteThread.is_set(): + try: + varName, value, enqueue_time = self.writeQueue.get() + # print(self.writeQueue.qsize()) + except queue.Empty: + # print(1111) + continue + # 用Point对象构建数据点,时间用入队时间 + 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): + 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}' + 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 [] + else: + data, timeList = [], [] + except Exception as e: + print(f"查询结果处理失败: {e}") + data, timeList = [], [] + return data, timeList + + @classmethod + def deleteTable(cls, table): + token = cls.getAPIToken() + host = 'http://localhost:8181' + url = f"{host.rstrip('/')}/api/v3/configure/table?db={'dcs'}&table={table}" + headers = { + 'Authorization': f'Bearer {token}' + } + response = requests.delete(url, headers=headers) + if response.status_code != 200: + print(f"删除失败: {response.status_code} {response.text}") + else: + print(f"已删除表 {table} 的所有历史数据。") + + def stopWriteThread(self): + self._stopWriteThread.set() + self.writeThread.join(timeout=1) + + def close(self): + self.stopWriteThread() + self.client.close() if __name__ == '__main__': - t = time.time() - # t = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(t)) - print(t) - h = HistoryDBManage(mem = t, bucket = 'history') - h.writeFun('test1', 1) - h.writeFun('test1', 2) - h.writeFun('test1', 3) - h.queryFun('test1') - h.queryMem() - h.deleteMem() - # h.deleteBucket() + db = HistoryDBManage( + database="dcs", + table="p1", + host="http://localhost:8181", + token="apiv3_ynlNTgq_OX164srSzjYXetWZJGOpgokFJbp_JaToWYlzwIPAZboPxKt4ss6vD1_4jj90QOIDnRDodQSJ66m3_g", + org="dcs" + ) + data, times = db.queryVarHistory("有源/无源4-20mA输入通道1") + print(data, times) + db.close() \ No newline at end of file diff --git a/model/ProjectModel/ProjectManage.py b/model/ProjectModel/ProjectManage.py index bcaa236..257b405 100644 --- a/model/ProjectModel/ProjectManage.py +++ b/model/ProjectModel/ProjectManage.py @@ -113,6 +113,8 @@ class ProjectManage(object): except OSError as e: print(e) Project.deleteProject(name = name) + histroyTableName = 'p' + name + HistoryDBManage.deleteTable(table=histroyTableName) @classmethod def switchProject(self, name): @@ -140,7 +142,9 @@ class ProjectManage(object): Globals.setValue('currentProDB', projectDB) Globals.setValue('currentProType', 1)#切换工程 - protocolManage = ProtocolManage() + histroyTableName = 'p' + name + historyDBManage = HistoryDBManage(table=histroyTableName) + Globals.setValue('historyDBManage', historyDBManage) # protocolManage.writeVariableValue('无源4-20mA输出通道1', 150) # protocolManage.writeVariableValue('有源24V数字输出通道12', 1) # protocolManage.writeVariableValue('热偶输出2', 40) @@ -148,7 +152,10 @@ class ProjectManage(object): # print(protocolManage.readVariableValue('有源/无源4-20mA输入通道2')) # print(protocolManage.readVariableValue('无源24V数字输入通道2')) # print(a) + # print(Globals.getValue('historyDBManage')) + protocolManage = ProtocolManage() Globals.setValue('protocolManage', protocolManage) + Globals.getValue('HistoryWidget').exchangeProject() # if Globals.getValue('FFThread').isRunning(): # Globals.getValue('FFThread').reStart() diff --git a/model/ProjectModel/VarManage.py b/model/ProjectModel/VarManage.py index e01e63e..1b2fa08 100644 --- a/model/ProjectModel/VarManage.py +++ b/model/ProjectModel/VarManage.py @@ -9,9 +9,21 @@ import openpyxl from utils.DBModels.ProtocolModel import ModbusTcpMasterVar, ModbusRtuMasterVar, ModbusRtuSlaveVar,\ ModbusTcpSlaveVar, HartVar, TcRtdVar, AnalogVar, HartSimulateVar from PyQt5.Qt import QFileDialog, QMessageBox +from protocol.ProtocolManage import ProtocolManage + # ID 从站地址 变量名 变量描述 变量类型 寄存器地址 工程量下限 工程量上限 +# 获取ProtocolManage单例或全局对象(假设为全局唯一,实际项目可根据实际情况调整) + + +def getProtocolManager(): + protocolManagerInstance = Globals.getValue('protocolManage') + if protocolManagerInstance is None: + protocolManagerInstance = ProtocolManage() + return protocolManagerInstance + + class ModbusVarManage(object): ModBusVarClass = { 'ModbusTcpMaster': ModbusTcpMasterVar, @@ -42,6 +54,8 @@ class ModbusVarManage(object): modbusVarType =self.getVarClass(modbusType)() modbusVarType.createVar(varName = varName, varType = varType, des = des, address = address, slaveID = slaveID, min = min, max = max, order = order, varModel = varModel) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() # def importModbusVar(self, path): @@ -147,6 +161,8 @@ class ModbusVarManage(object): modbusVarType =self.getVarClass(modbusType)() modbusVarType.createVar(varName = varName, varType = varType, des = des, address = address, slaveID = slaveID, min = min, max = max, order = order, varModel = varModel) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod @@ -155,6 +171,8 @@ class ModbusVarManage(object): name = str(name) # print(name) self.getVarClass(modbusType).deleteVar(name = name) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod @@ -182,18 +200,24 @@ class ModbusVarManage(object): else: 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() @classmethod def editOrder(self, name, order, modbusType): name = str(name) order = str(order) self.getVarClass(modbusType).update(order = order).where(self.getVarClass(modbusType).varName == name).execute() + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def editVarModel(self, name, varModel, modbusType): name = str(name) varModel = str(varModel) self.getVarClass(modbusType).update(varModel = varModel).where(self.getVarClass(modbusType).varName == name).execute() + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def getAllVar(self, modbusType): @@ -230,6 +254,8 @@ class HartVarManage(object): else: hartVarType = HartVar() hartVarType.createVar(varName = varName, des = des, varModel = varModel) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def getAllVar(self): @@ -249,6 +275,8 @@ class HartVarManage(object): name = str(name) # print(name) HartVar.deleteVar(name = name) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod @@ -269,12 +297,16 @@ class HartVarManage(object): return else: HartVar.update(varName = Nname, description = des, varModel = varModel).where(HartVar.varName == name).execute() + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def editVarModel(self, name, varModel): name = str(name) print('修改变量模型',name) HartVar.update(varModel = str(varModel)).where(HartVar.varName == name).execute() + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod @@ -299,6 +331,8 @@ class HartVarManage(object): else: hartVarType = HartVar() hartVarType.createVar(varName = varName, des = des, varModel = varModel) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() class TcRtdManage(object): @@ -317,6 +351,8 @@ class TcRtdManage(object): else: tcRtdVarType = TcRtdVar() tcRtdVarType.createVar(varName = varName, channelNumber=channelNumber, des = des, varType = varType, min = min, max = max, compensationVar = compensationVar, varModel = varModel) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def getAllVar(self): @@ -336,6 +372,8 @@ class TcRtdManage(object): name = str(name) # print(name) TcRtdVar.deleteVar(name = name) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod @@ -360,6 +398,8 @@ class TcRtdManage(object): return else: TcRtdVar.update(varName=Nname, channelNumber = channelNumber, description=des, varType=varType, min=min, max=max, compensationVar = compensationVar).where(TcRtdVar.varName == name).execute() + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def getByName(self, name): @@ -384,11 +424,15 @@ class TcRtdManage(object): def editvarType(self, name, varType): name = str(name) TcRtdVar.update(varType = str(varType)).where(TcRtdVar.varName == name).execute() + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def editVarModel(self, name, varModel): name = str(name) TcRtdVar.update(varModel = str(varModel)).where(TcRtdVar.varName == name).execute() + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def importVarForm(self, varName, channelNumber, varType, des, min, max, compensationVar, varModel): @@ -398,6 +442,8 @@ class TcRtdManage(object): else: tcRtdVarType = TcRtdVar() tcRtdVarType.createVar(varName = varName, channelNumber=channelNumber, des = des, varType = varType, min = min, max = max, compensationVar = compensationVar, varModel = varModel) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() class AnalogManage(object): def __init__(self): @@ -415,6 +461,8 @@ class AnalogManage(object): # else: analogVarType = AnalogVar() analogVarType.createVar(varName = varName, channelNumber = channelNumber, des = des, varType = varType, min = min, max = max, varModel = varModel) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def getAllVar(self): @@ -434,6 +482,8 @@ class AnalogManage(object): name = str(name) # print(name) AnalogVar.deleteVar(name = name) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod @@ -456,11 +506,15 @@ class AnalogManage(object): return else: AnalogVar.update(varName=Nname, channelNumber =channelNumber, description=des, varType=varType, min=min, max=max).where(AnalogVar.varName == name).execute() + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def editVarModel(self, name, varModel): name = str(name) AnalogVar.update(varModel = str(varModel)).where(AnalogVar.varName == name).execute() + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def getByName(self, name): # 查询指定变量信息 @@ -510,6 +564,8 @@ class AnalogManage(object): else: analogVarType = AnalogVar() analogVarType.createVar(varName = varName, channelNumber = channelNumber, des = des, varType = varType, min = min, max = max, varModel = varModel) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @@ -528,6 +584,8 @@ class HartSimulateVarManage(object): else: hartSimulateVarType = HartSimulateVar() hartSimulateVarType.createVar(varName = varName, des = des,varModel = varModel) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def getAllVar(self): @@ -547,6 +605,8 @@ class HartSimulateVarManage(object): name = str(name) # print(name) HartSimulateVar.deleteVar(name = name) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod @@ -566,11 +626,15 @@ class HartSimulateVarManage(object): return else: HartSimulateVar.update(varName = Nname, description = des, varModel = varModel).where(HartSimulateVar.varName == name).execute() + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def editVarModel(self, name, varModel): name = str(name) HartSimulateVar.update(varModel = str(varModel)).where(HartSimulateVar.varName == name).execute() + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() @classmethod def getByName(self, name): # 查询指定变量信息 @@ -593,6 +657,8 @@ class HartSimulateVarManage(object): else: hartSimulateVarType = HartSimulateVar() hartSimulateVarType.createVar(varName = varName, des = des,varModel = varModel) + # 操作后刷新缓存 + getProtocolManager().refreshVarCache() class GlobalVarManager(object): diff --git a/protocol/ProtocolManage.py b/protocol/ProtocolManage.py index 102b39d..9fe16ad 100644 --- a/protocol/ProtocolManage.py +++ b/protocol/ProtocolManage.py @@ -7,6 +7,9 @@ from protocol.TCP.TCPVarManage import * from protocol.TCP.TemToMv import temToMv from protocol.RPC.RpcClient import RpcClient from protocol.RPC.RpcServer import RpcServer +from utils import Globals +import threading +import time class ProtocolManage(object): """通讯变量查找类,用于根据变量名在数据库模型中检索变量信息""" @@ -25,31 +28,61 @@ class ProtocolManage(object): self.writeRTD = [0] * 8 self.RpcClient = None self.RpcServer = None + self.varInfoCache = {} # 保持驼峰命名 + self.historyDBManage = Globals.getValue('historyDBManage') + self.variableValueCache = {} # {varName: value} + self.refreshVarCache() + self.cacheLock = threading.Lock() + self.readThreadStop = threading.Event() + self.readThread = threading.Thread(target=self._backgroundReadAllVariables, daemon=True) + self.readThread.start() + def clearVarCache(self): + """清空变量信息缓存""" + self.varInfoCache.clear() - @classmethod - def lookupVariable(cls, variableName): + def refreshVarCache(self): + """重新加载所有变量信息到缓存(可选实现)""" + self.varInfoCache.clear() + for modelClass in self.MODEL_CLASSES: + try: + for varInstance in modelClass.select(): + varName = getattr(varInstance, 'varName', None) + if varName: + varData = {} + for field in varInstance._meta.sorted_fields: + fieldName = field.name + varData[fieldName] = getattr(varInstance, fieldName) + self.varInfoCache[varName] = { + 'modelType': modelClass.__name__, + 'variableData': varData + } + except Exception as e: + print(f"刷新缓存时出错: {modelClass.__name__}: {e}") + + def lookupVariable(self, variableName): """ - 根据变量名检索变量信息 - + 根据变量名检索变量信息(优先查缓存) :param variableName: 要查询的变量名 :return: 包含变量信息和模型类型的字典,如果未找到返回None """ - for modelClass in cls.MODEL_CLASSES: + if variableName in self.varInfoCache: + # print(111) + return self.varInfoCache[variableName] + for modelClass in self.MODEL_CLASSES: varInstance = modelClass.getByName(variableName) if varInstance: - # 获取变量字段信息 varData = {} for field in varInstance._meta.sorted_fields: fieldName = field.name varData[fieldName] = getattr(varInstance, fieldName) - - return { - 'model_type': modelClass.__name__, - 'variable_data': varData + result = { + 'modelType': modelClass.__name__, + 'variableData': varData } - + self.varInfoCache[variableName] = result # 写入缓存 + return result return None def setClentMode(self, clentName, rabbitmqHost='localhost'): @@ -105,8 +138,8 @@ class ProtocolManage(object): return False return False - modelType = varInfo['model_type'] - info = varInfo['variable_data'] + modelType = varInfo['modelType'] + info = varInfo['variableData'] # print(info) @@ -181,51 +214,46 @@ class ProtocolManage(object): print(f"写入变量值失败: {str(e)}") return False - # 添加读取函数 - def readVariableValue(self, variableName): - """ - 根据变量名读取变量值,根据变量类型进行不同处理(留空) - - :param variableName: 变量名 - :return: 读取的值或None(失败时) - """ - # + def _backgroundReadAllVariables(self, interval=0.5): + while not self.readThreadStop.is_set(): + allVarNames = list(self.getAllVariableNames()) + for varName in allVarNames: + value = self._readVariableValueOriginal(varName) + with self.cacheLock: + self.variableValueCache[varName] = value + time.sleep(interval) + + def getAllVariableNames(self): + # 直接从缓存获取所有变量名,降低与数据库模型的耦合 + return list(self.varInfoCache.keys()) + + def _readVariableValueOriginal(self, variableName): + # 完全保留原有读取逻辑 varInfo = self.lookupVariable(variableName) + value = None if not varInfo: if self.RpcServer: - print(variableName, 1111111111111111111) existsVar, clientNames = self.RpcServer.existsVar(variableName) if existsVar: - # print(clientNames, 1111111111111111) - value =float(self.RpcServer.getVarValue(clientNames[0], variableName)['value']) - return value + value = float(self.RpcServer.getVarValue(clientNames[0], variableName)['value']) + # return value else: return None return None - - modelType = varInfo['model_type'] - info = varInfo['variable_data'] - + modelType = varInfo['modelType'] + info = varInfo['variableData'] try: # 拆分为独立的协议条件判断 if modelType == 'ModbusTcpMasterVar': - # 读取操作(留空) pass - elif modelType == 'ModbusTcpSlaveVar': pass - elif modelType == 'ModbusRtuMasterVar': pass - elif modelType == 'ModbusRtuSlaveVar': pass - - # HART协议变量处理 elif modelType == 'HartVar': pass - - # 温度/RTD变量处理 elif modelType == 'TcRtdVar': channel = int(info['channelNumber']) - 1 varType = info['varType'] @@ -236,44 +264,47 @@ class ProtocolManage(object): value = self.tcpVarManager.simRTDData[channel] elif varType in ['R', 'S', 'B', 'J', 'T', 'E', 'K', 'N', 'C', 'A']: value = self.tcpVarManager.simTCData[channel] - if self.RpcClient: - self.RpcClient.setVarContent(variableName, value, info['min'], info['max'], info['varType']) - return value + # if self.RpcClient: + # self.RpcClient.setVarContent(variableName, value, info['min'], info['max'], info['varType']) + # return value else: if varType == 'PT100': value = self.writeRTD[channel] elif varType in ['R', 'S', 'B', 'J', 'T', 'E', 'K', 'N', 'C', 'A']: value = self.writeTC[channel] - if self.RpcClient: - self.RpcClient.setVarContent(variableName, value, info['min'], info['max'], info['varType']) - return value - - # 模拟量变量处理 + # if self.RpcClient: + # self.RpcClient.setVarContent(variableName, value, info['min'], info['max'], info['varType']) + # return value elif modelType == 'AnalogVar': channel = int(info['channelNumber']) - 1 varType = info['varType'] - # print(varType, channel) varModel = info['varModel'] model = self.getModelType(varModel) value = self.tcpVarManager.readValue(varType, channel, model=model) if varType in ['AI','AO']: - # print(value) value = self.getRealAI(value, info['max'], info['min']) + + # return value + elif modelType == 'HartSimulateVar': + pass + if value is not None: if self.RpcClient: self.RpcClient.setVarContent(variableName, value, info['min'], info['max'], info['varType']) + self.historyDBManage.writeVarValue(variableName, value) + # print('sucess') return value - # print(1) - - # HART模拟变量处理 - elif modelType == 'HartSimulateVar': - pass - # print(1111111111111111) - - - return None # 暂时返回None + else: + return None except Exception as e: print(f"读取变量值失败: {str(e)}") - return str(e) + return None + + def readVariableValue(self, variableName): + # 优先从缓存读取,未命中则走原有逻辑 + with self.cacheLock: + if variableName in self.variableValueCache: + return self.variableValueCache[variableName] + return self._readVariableValueOriginal(variableName) # def sendTrigger(self, variableName, value, timeoutMS): @@ -284,6 +315,7 @@ class ProtocolManage(object): def shutdown(self): self.tcpVarManager.shutdown() + def getRealAO(self, value,highValue, lowValue): diff --git a/protocol/TCP/TCPVarManage.py b/protocol/TCP/TCPVarManage.py index 8a46c52..059b712 100644 --- a/protocol/TCP/TCPVarManage.py +++ b/protocol/TCP/TCPVarManage.py @@ -45,6 +45,8 @@ class TCPVarManager: """ try: result = self.communicator.readAIDI() + self.AIDATA = result[0] + self.DIDATA = result[1] # print(result) if result is None: logging.warning("Failed to read AI/DI data") @@ -61,12 +63,12 @@ class TCPVarManager: :param callback: 数据回调函数 """ self.stop_event = threading.Event() - self.readThread = threading.Thread( - target=self._read_worker, - args=(interval, callback), - daemon=True - ) - self.readThread.start() + # self.readThread = threading.Thread( + # target=self._read_worker, + # args=(interval, callback), + # daemon=True + # ) + # self.readThread.start() logging.info(f"Started periodic read every {interval*1000}ms") def _read_worker(self, interval, callback): @@ -225,6 +227,7 @@ class TCPVarManager: def readValue(self, variableType, channel, model = localModel): # channel = channel + self.readAiDi() if variableType == "AI": # print(self.AIDATA) if model == SimModel: diff --git a/utils/Globals.py b/utils/Globals.py index 1069cd9..b39a19c 100644 --- a/utils/Globals.py +++ b/utils/Globals.py @@ -22,7 +22,7 @@ def _init():#初始化 _globalDict['AnalogThread'] = 0 _globalDict['FFSimulateThread'] = 0 _globalDict['HartSimulateThread'] = 0 - _globalDict['HistoryWidget'] = 0 + _globalDict['HistoryWidget'] = None _globalDict['projectNumber'] = None _globalDict['username'] = None _globalDict['MainWindow'] = None @@ -32,6 +32,7 @@ def _init():#初始化 _globalDict['blockManage'] = None _globalDict['protocolManage'] = None + _globalDict['historyDBManage'] = None # 确保初始化 _init() diff --git a/windoweffect/__init__.py b/windoweffect/__init__.py new file mode 100644 index 0000000..a3fb4ed --- /dev/null +++ b/windoweffect/__init__.py @@ -0,0 +1,2 @@ +from .window_effect import WindowEffect +from .c_structures import * \ No newline at end of file diff --git a/windoweffect/c_structures.py b/windoweffect/c_structures.py new file mode 100644 index 0000000..1553383 --- /dev/null +++ b/windoweffect/c_structures.py @@ -0,0 +1,135 @@ +# coding:utf-8 + +from ctypes import POINTER, Structure, c_int +from ctypes.wintypes import DWORD, HWND, ULONG, POINT, RECT, UINT +from enum import Enum + + +class WINDOWCOMPOSITIONATTRIB(Enum): + WCA_UNDEFINED = 0 + WCA_NCRENDERING_ENABLED = 1 + WCA_NCRENDERING_POLICY = 2 + WCA_TRANSITIONS_FORCEDISABLED = 3 + WCA_ALLOW_NCPAINT = 4 + WCA_CAPTION_BUTTON_BOUNDS = 5 + WCA_NONCLIENT_RTL_LAYOUT = 6 + WCA_FORCE_ICONIC_REPRESENTATION = 7 + WCA_EXTENDED_FRAME_BOUNDS = 8 + WCA_HAS_ICONIC_BITMAP = 9 + WCA_THEME_ATTRIBUTES = 10 + WCA_NCRENDERING_EXILED = 11 + WCA_NCADORNMENTINFO = 12 + WCA_EXCLUDED_FROM_LIVEPREVIEW = 13 + WCA_VIDEO_OVERLAY_ACTIVE = 14 + WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15 + WCA_DISALLOW_PEEK = 16 + WCA_CLOAK = 17 + WCA_CLOAKED = 18 + WCA_ACCENT_POLICY = 19 + WCA_FREEZE_REPRESENTATION = 20 + WCA_EVER_UNCLOAKED = 21 + WCA_VISUAL_OWNER = 22 + WCA_LAST = 23 + + +class ACCENT_STATE(Enum): + """ Client area status enumeration class """ + ACCENT_DISABLED = 0 + ACCENT_ENABLE_GRADIENT = 1 + ACCENT_ENABLE_TRANSPARENTGRADIENT = 2 + ACCENT_ENABLE_BLURBEHIND = 3 # Aero effect + ACCENT_ENABLE_ACRYLICBLURBEHIND = 4 # Acrylic effect + ACCENT_ENABLE_HOSTBACKDROP = 5 # Mica effect + ACCENT_INVALID_STATE = 6 + + +class ACCENT_POLICY(Structure): + """ Specific attributes of client area """ + + _fields_ = [ + ("AccentState", DWORD), + ("AccentFlags", DWORD), + ("GradientColor", DWORD), + ("AnimationId", DWORD), + ] + + +class WINDOWCOMPOSITIONATTRIBDATA(Structure): + _fields_ = [ + ("Attribute", DWORD), + # Pointer() receives any ctypes type and returns a pointer type + ("Data", POINTER(ACCENT_POLICY)), + ("SizeOfData", ULONG), + ] + + +class DWMNCRENDERINGPOLICY(Enum): + DWMNCRP_USEWINDOWSTYLE = 0 + DWMNCRP_DISABLED = 1 + DWMNCRP_ENABLED = 2 + DWMNCRP_LAS = 3 + + +class DWMWINDOWATTRIBUTE(Enum): + DWMWA_NCRENDERING_ENABLED = 1 + DWMWA_NCRENDERING_POLICY = 2 + DWMWA_TRANSITIONS_FORCEDISABLED = 3 + DWMWA_ALLOW_NCPAINT = 4 + DWMWA_CAPTION_BUTTON_BOUNDS = 5 + DWMWA_NONCLIENT_RTL_LAYOUT = 6 + DWMWA_FORCE_ICONIC_REPRESENTATION = 7 + DWMWA_FLIP3D_POLICY = 8 + DWMWA_EXTENDED_FRAME_BOUNDS = 9 + DWMWA_HAS_ICONIC_BITMAP = 10 + DWMWA_DISALLOW_PEEK = 11 + DWMWA_EXCLUDED_FROM_PEEK = 12 + DWMWA_CLOAK = 13 + DWMWA_CLOAKED = 14 + DWMWA_FREEZE_REPRESENTATION = 15 + DWMWA_PASSIVE_UPDATE_MODE = 16 + DWMWA_USE_HOSTBACKDROPBRUSH = 17 + DWMWA_USE_IMMERSIVE_DARK_MODE = 18 + DWMWA_WINDOW_CORNER_PREFERENCE = 19 + DWMWA_BORDER_COLOR = 20 + DWMWA_CAPTION_COLOR = 21 + DWMWA_TEXT_COLOR = 22 + DWMWA_VISIBLE_FRAME_BORDER_THICKNESS = 23 + DWMWA_LAST = 24 + + +class MARGINS(Structure): + _fields_ = [ + ("cxLeftWidth", c_int), + ("cxRightWidth", c_int), + ("cyTopHeight", c_int), + ("cyBottomHeight", c_int), + ] + + +class MINMAXINFO(Structure): + _fields_ = [ + ("ptReserved", POINT), + ("ptMaxSize", POINT), + ("ptMaxPosition", POINT), + ("ptMinTrackSize", POINT), + ("ptMaxTrackSize", POINT), + ] + + +class PWINDOWPOS(Structure): + _fields_ = [ + ('hWnd', HWND), + ('hwndInsertAfter', HWND), + ('x', c_int), + ('y', c_int), + ('cx', c_int), + ('cy', c_int), + ('flags', UINT) + ] + + +class NCCALCSIZE_PARAMS(Structure): + _fields_ = [ + ('rgrc', RECT*3), + ('lppos', POINTER(PWINDOWPOS)) + ] diff --git a/windoweffect/window_effect.py b/windoweffect/window_effect.py new file mode 100644 index 0000000..aa8537c --- /dev/null +++ b/windoweffect/window_effect.py @@ -0,0 +1,222 @@ +# coding:utf-8 +import sys + +from ctypes import POINTER, c_bool, c_int, pointer, sizeof, WinDLL, byref +from ctypes.wintypes import DWORD, LONG, LPCVOID + +from win32 import win32api, win32gui +from win32.lib import win32con + +from .c_structures import ( + ACCENT_POLICY, + ACCENT_STATE, + MARGINS, + DWMNCRENDERINGPOLICY, + DWMWINDOWATTRIBUTE, + WINDOWCOMPOSITIONATTRIB, + WINDOWCOMPOSITIONATTRIBDATA, +) + + +class WindowEffect: + """ A class that calls Windows API to realize window effect """ + + def __init__(self): + # Declare the function signature of the API + self.user32 = WinDLL("user32") + self.dwmapi = WinDLL("dwmapi") + self.SetWindowCompositionAttribute = self.user32.SetWindowCompositionAttribute + self.DwmExtendFrameIntoClientArea = self.dwmapi.DwmExtendFrameIntoClientArea + self.DwmSetWindowAttribute = self.dwmapi.DwmSetWindowAttribute + self.SetWindowCompositionAttribute.restype = c_bool + self.DwmExtendFrameIntoClientArea.restype = LONG + self.DwmSetWindowAttribute.restype = LONG + self.SetWindowCompositionAttribute.argtypes = [ + c_int, + POINTER(WINDOWCOMPOSITIONATTRIBDATA), + ] + self.DwmSetWindowAttribute.argtypes = [c_int, DWORD, LPCVOID, DWORD] + self.DwmExtendFrameIntoClientArea.argtypes = [c_int, POINTER(MARGINS)] + + # Initialize structure + self.accentPolicy = ACCENT_POLICY() + self.winCompAttrData = WINDOWCOMPOSITIONATTRIBDATA() + self.winCompAttrData.Attribute = WINDOWCOMPOSITIONATTRIB.WCA_ACCENT_POLICY.value + self.winCompAttrData.SizeOfData = sizeof(self.accentPolicy) + self.winCompAttrData.Data = pointer(self.accentPolicy) + + def setAcrylicEffect(self, hWnd, gradientColor: str = "F2F2F299", isEnableShadow: bool = True, animationId: int = 0): + """ Add the acrylic effect to the window + + Parameters + ---------- + hWnd: int or `sip.voidptr` + Window handle + + gradientColor: str + Hexadecimal acrylic mixed color, corresponding to four RGBA channels + + isEnableShadow: bool + Enable window shadows + + animationId: int + Turn on matte animation + """ + hWnd = int(hWnd) + # Acrylic mixed color + gradientColor = ( + gradientColor[6:] + + gradientColor[4:6] + + gradientColor[2:4] + + gradientColor[:2] + ) + gradientColor = DWORD(int(gradientColor, base=16)) + # matte animation + animationId = DWORD(animationId) + # window shadow + accentFlags = DWORD(0x20 | 0x40 | 0x80 | + 0x100) if isEnableShadow else DWORD(0) + self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_ENABLE_ACRYLICBLURBEHIND.value + self.accentPolicy.GradientColor = gradientColor + self.accentPolicy.AccentFlags = accentFlags + self.accentPolicy.AnimationId = animationId + # enable acrylic effect + self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData)) + + def setMicaEffect(self, hWnd): + """ Add the mica effect to the window (Win11 only) + + Parameters + ---------- + hWnd: int or `sip.voidptr` + Window handle + """ + if sys.getwindowsversion().build < 22000: + raise Exception("The mica effect is only available on Win11") + + hWnd = int(hWnd) + margins = MARGINS(-1, -1, -1, -1) + self.DwmExtendFrameIntoClientArea(hWnd, byref(margins)) + self.DwmSetWindowAttribute(hWnd, 1029, byref(c_int(1)), 4) + self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_ENABLE_HOSTBACKDROP.value + self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData)) + + def setAeroEffect(self, hWnd): + """ Add the aero effect to the window + + Parameters + ---------- + hWnd: int or `sip.voidptr` + Window handle + """ + hWnd = int(hWnd) + self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_ENABLE_BLURBEHIND.value + self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData)) + + def removeBackgroundEffect(self, hWnd): + """ Remove background effect + + Parameters + ---------- + hWnd: int or `sip.voidptr` + Window handle + """ + hWnd = int(hWnd) + self.accentPolicy.AccentState = ACCENT_STATE.ACCENT_DISABLED.value + self.SetWindowCompositionAttribute(hWnd, pointer(self.winCompAttrData)) + + @staticmethod + def moveWindow(hWnd): + """ Move the window + + Parameters + ---------- + hWnd: int or `sip.voidptr` + Window handle + """ + hWnd = int(hWnd) + win32gui.ReleaseCapture() + win32api.SendMessage( + hWnd, win32con.WM_SYSCOMMAND, win32con.SC_MOVE + win32con.HTCAPTION, 0 + ) + + def addShadowEffect(self, hWnd): + """ Add DWM shadow to window + + Parameters + ---------- + hWnd: int or `sip.voidptr` + Window handle + """ + hWnd = int(hWnd) + margins = MARGINS(-1, -1, -1, -1) + self.DwmExtendFrameIntoClientArea(hWnd, byref(margins)) + + def addMenuShadowEffect(self, hWnd): + """ Add DWM shadow to menu + + Parameters + ---------- + hWnd: int or `sip.voidptr` + Window handle + """ + hWnd = int(hWnd) + self.DwmSetWindowAttribute( + hWnd, + DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY.value, + byref(c_int(DWMNCRENDERINGPOLICY.DWMNCRP_ENABLED.value)), + 4, + ) + margins = MARGINS(-1, -1, -1, -1) + self.DwmExtendFrameIntoClientArea(hWnd, byref(margins)) + + def removeShadowEffect(self, hWnd): + """ Remove DWM shadow from the window + + Parameters + ---------- + hWnd: int or `sip.voidptr` + Window handle + """ + hWnd = int(hWnd) + self.DwmSetWindowAttribute( + hWnd, + DWMWINDOWATTRIBUTE.DWMWA_NCRENDERING_POLICY.value, + byref(c_int(DWMNCRENDERINGPOLICY.DWMNCRP_DISABLED.value)), + 4, + ) + + @staticmethod + def removeMenuShadowEffect(hWnd): + """ Remove shadow from pop-up menu + + Parameters + ---------- + hWnd: int or `sip.voidptr` + Window handle + """ + hWnd = int(hWnd) + style = win32gui.GetClassLong(hWnd, win32con.GCL_STYLE) + style &= ~0x00020000 # CS_DROPSHADOW + win32api.SetClassLong(hWnd, win32con.GCL_STYLE, style) + + @staticmethod + def addWindowAnimation(hWnd): + """ Enables the maximize and minimize animation of the window + + Parameters + ---------- + hWnd : int or `sip.voidptr` + Window handle + """ + hWnd = int(hWnd) + style = win32gui.GetWindowLong(hWnd, win32con.GWL_STYLE) + win32gui.SetWindowLong( + hWnd, + win32con.GWL_STYLE, + style + | win32con.WS_MAXIMIZEBOX + | win32con.WS_CAPTION + | win32con.CS_DBLCLKS + | win32con.WS_THICKFRAME, + )