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,
+ )