From 5d58b881869e0e29024a0d893bf877e3464b50c1 Mon Sep 17 00:00:00 2001 From: yanganjie Date: Mon, 3 Nov 2025 13:55:56 +0800 Subject: [PATCH 01/16] 1 --- db/three.db | Bin 53248 -> 53248 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/db/three.db b/db/three.db index cb2afc3f5746f3058c061dd9cb8ade4bc64a6337..79d639ee09739771356cd9e1f8a069d8ff490d8a 100644 GIT binary patch delta 885 zcmZozz}&Eac>{}r2nWA71OG$*{rq$JtNA1O&G}XN#Wyzo=i}qyV3uVJa0yCH&M!(~ z-mI#BM}U_vj)9qHHv`XZetq7)jg4!0>Kg+XMHv{B8@ZKv*=5y*fOdut?h6cLEh6;v;Rz{XqMuvKZ#wLaaW~eg8CLkGeOA||DvqoQLxWlE@1wjty zL^vF59gz;#CdTDIfX69YXXBQr}gO9lpp zixV5q);Ibwp*tHXyh(9339b(Ih;lO4HF1wpn5=kS))X_mjSciHjSPVf#qfZ!p`Njc zrG>fSrU1qQUS1Di`srif>EmPIjo#SUz$4LU&Lqj8&L+zQNj&Vz92|_C9LC0zTjk^H znc>D6C>WYrnV47^8xSY-Y{vSh6P5uH-wp<0VAvspMkN-Am5^wMS;=55EG!K4qK}cW zfrX)wxry`Sl5;*HD4sR2GPJNVvY33}oUFJhGd5F{}r5Iesr1OG$*{rq$JtNA1O&G}U~HuCdtR@J{Fu$eJ{Szt3uz~6jc z1_lOZK5qv8T)sPe-WwZj`NSLfSR@(L*<_id)dl4x1=*E3I5;^u7$z5;jhlS&tgzy< v8S9@;SO!Fme9YJsGE9~@7dv^*IT?vY0Vaq7S#@C~lNcs1I}< Date: Tue, 4 Nov 2025 09:45:14 +0800 Subject: [PATCH 02/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=91=84=E5=83=8F?= =?UTF-8?q?=E5=A4=B4=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/camera_config.ini | 6 +++--- controller/main_controller.py | 8 +++++--- view/main_window.py | 2 +- view/widgets/hopper_widget.py | 14 +++++++++++--- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/config/camera_config.ini b/config/camera_config.ini index 9d66244..b6b58d0 100644 --- a/config/camera_config.ini +++ b/config/camera_config.ini @@ -1,21 +1,21 @@ # camera_config.ini # 相关的摄像头的配置文件 [上位料斗] -ip = 192.168.1.50 +ip = 192.168.250.60 port = 554 username = admin password = XJ123456 channel = 101 [下位料斗] -ip = 192.168.1.51 +ip = 192.168.250.61 port = 554 username = admin password = XJ123456 channel = 101 [模具车] -ip = 192.168.1.51 +ip = 192.168.250.61 port = 554 username = admin password = XJ123456 diff --git a/controller/main_controller.py b/controller/main_controller.py index 6c0f506..060fe1c 100644 --- a/controller/main_controller.py +++ b/controller/main_controller.py @@ -32,7 +32,8 @@ class MainController: def _onTimer(self): # 定时任务逻辑 - loc_tra=TransmitterController(RelayController()) + # 变送器是 192.168.250.63,端口 502 + loc_tra=TransmitterController(RelayController(host="192.168.250.63", port=502)) upper_weight=loc_tra.read_data(1) lower_weight=loc_tra.read_data(2) if upper_weight is None: @@ -49,7 +50,7 @@ class MainController: pass def _onTimer2(self): - print(str(self.angle)) + # print(str(self.angle)) # 定时任务逻辑 if self.is_add: self.angle+=1 @@ -71,7 +72,8 @@ class MainController: def showMainWindow(self): - self.main_window.show() + # self.main_window.show() + self.main_window.showFullScreen() self.main_window.dispatch_task_widget.set_task_time("task1","15:44 PM") self.main_window.dispatch_task_widget.set_task_time("task2","17:37 PM") self.main_window.segment_task_widget.set_task_time("task1","15:38 PM") diff --git a/view/main_window.py b/view/main_window.py index 1e1ccff..127be50 100644 --- a/view/main_window.py +++ b/view/main_window.py @@ -63,7 +63,7 @@ class MainWindow(QWidget): # self.setStyleSheet("background-color: #ffffff;") # #001558 # Qt.FramelessWindowHint - self.setWindowFlags(Qt.FramelessWindowHint) + # self.setWindowFlags(Qt.FramelessWindowHint) # 设置主界面背景图片 try: diff --git a/view/widgets/hopper_widget.py b/view/widgets/hopper_widget.py index 557094f..3390208 100644 --- a/view/widgets/hopper_widget.py +++ b/view/widgets/hopper_widget.py @@ -323,18 +323,26 @@ class HopperWidget(QWidget): volume : 传入多少方 """ self.upper_extra_label.setText(f"{volume}方(预估)") + + # 上料斗夹爪开合角度设置 + def setUpperHopperClampAngle(self, angle: float): + """ + Args: + angle: 传入多少角度(单位°) + """ + self.upper_clamp_widget.set_angle(angle) # 下料斗重量设置 def setLowerHopperWeight(self, weight:float): self.lower_weight_label.setText(f"{weight}kg") - # 下料斗开合角度设置 + # 下料斗开合角度设置 (包括 夹爪和标签) def setLowerHopperOpeningAngle(self, angle: float): """Args: angle : 传入多少度 (单位°) """ - self.lower_extra_label.setText(f"开: {angle}°") - self.lower_clamp_widget.set_angle(angle) + self.lower_extra_label.setText(f"开: {angle}°") # 设置下料斗角度标签 + self.lower_clamp_widget.set_angle(angle) # 设置下料斗夹爪开合角度 # ------------------------------ # 设置上料斗状态(0=绿,1=黄,2=红) From 10a35867c5994be02e76598afa5a7002de6e867c Mon Sep 17 00:00:00 2001 From: yanganjie Date: Tue, 4 Nov 2025 10:21:39 +0800 Subject: [PATCH 03/16] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=8F=98=E9=80=81?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 test.py diff --git a/test.py b/test.py new file mode 100644 index 0000000..de26e5b --- /dev/null +++ b/test.py @@ -0,0 +1,32 @@ +import socket + +# 设备信息 +IP = "192.168.250.63" +PORT = 502 +TIMEOUT = 5 # 超时时间(秒) + +# 创建TCP socket +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.settimeout(TIMEOUT) # 设置超时,避免一直阻塞 + # 连接设备 + s.connect((IP, PORT)) + print(f"✅ 已通过TCP连接到 {IP}:{PORT}") + + # 尝试接收数据(不发送任何请求,纯等待) + print("等待设备发送数据...(若5秒内无响应则超时)") + data = s.recv(1024) # 最多接收1024字节 + + if data: + # 打印收到的原始数据(16进制和字节列表) + # print(f"收到数据(16进制):{data.hex()}") + print(f"收到数据(字节列表):{list(data)}") + else: + print("❌ 未收到任何数据(设备未主动发送)") + + except ConnectionRefusedError: + print(f"❌ 连接失败:{IP}:{PORT} 拒绝连接(设备离线/端口错误)") + except socket.timeout: + print(f"❌ 超时:{TIMEOUT}秒内未收到设备数据(设备未主动发送)") + except Exception as e: + print(f"❌ 发生错误:{str(e)}") \ No newline at end of file From 27e1ee6ec55508dd6b9489427dbffc59a2896bb0 Mon Sep 17 00:00:00 2001 From: yanganjie Date: Tue, 4 Nov 2025 10:35:12 +0800 Subject: [PATCH 04/16] =?UTF-8?q?=E6=8C=81=E7=BB=AD=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=E5=8F=98=E9=80=81=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test2.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 test2.py diff --git a/test2.py b/test2.py new file mode 100644 index 0000000..688a580 --- /dev/null +++ b/test2.py @@ -0,0 +1,48 @@ +import socket + +# 设备信息 +IP = "192.168.250.63" +PORT = 502 +TIMEOUT = 10 # 超时时间(秒) + +def parse_weight(raw_data): + """解析原始数据,提取重量(如从 b'ST,NT, +0000175\r\n' 中提取 175)""" + try: + # 1. 字节串解码为字符串(去除 b'' 包裹) + data_str = raw_data.decode('utf-8').strip() # strip() 去除首尾的换行符\r\n和空格 + # 此时 data_str 应为 'ST,NT, +0000175' + + # 2. 按逗号分割字符串,取第三个字段(包含重量的部分) + parts = data_str.split(',') # 分割后为 ['ST', 'NT', '+0000175'] + weight_part = parts[2].strip() # 得到 '+0000175' + + # 3. 提取纯数字(去除可能的正负号和前导零) + # 方法:替换掉非数字字符,再转换为整数 + weight_num = int(''.join(filter(str.isdigit, weight_part))) + return weight_num + except (IndexError, ValueError, UnicodeDecodeError) as e: + print(f"解析失败:{e},原始数据:{raw_data}") + return None + +# 创建TCP连接并持续读取 +with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.settimeout(TIMEOUT) + s.connect((IP, PORT)) + print(f"✅ 已连接到 {IP}:{PORT},开始持续读取重量...\n") + + while True: # 循环读取 + data = s.recv(1024) # 接收数据 + if data: + print(f"原始数据:{data}") + weight = parse_weight(data) + if weight is not None: + print(f"解析出重量:{weight}\n") + else: + print("❌ 未收到数据,等待下一次...\n") + except ConnectionRefusedError: + print(f"❌ 连接失败:{IP}:{PORT} 拒绝连接") + except socket.timeout: + print(f"❌ 超时:{TIMEOUT}秒内未收到数据") + except Exception as e: + print(f"❌ 发生错误:{e}") \ No newline at end of file From eebd6e4e36a38a8b96d4fbe980e1a48ccd81de71 Mon Sep 17 00:00:00 2001 From: yanganjie Date: Tue, 4 Nov 2025 10:39:03 +0800 Subject: [PATCH 05/16] =?UTF-8?q?=E8=AF=BB=E5=8F=96=E5=8F=98=E9=80=81?= =?UTF-8?q?=E5=99=A8=EF=BC=8C=E8=8E=B7=E5=8F=96=E6=97=B6=E9=97=B4=E9=97=B4?= =?UTF-8?q?=E9=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test2.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 14 deletions(-) diff --git a/test2.py b/test2.py index 688a580..6dd4c8d 100644 --- a/test2.py +++ b/test2.py @@ -1,43 +1,100 @@ +# import socket + +# # 设备信息 +# IP = "192.168.250.63" +# PORT = 502 +# TIMEOUT = 10 # 超时时间(秒) + +# def parse_weight(raw_data): +# """解析原始数据,提取重量(如从 b'ST,NT, +0000175\r\n' 中提取 175)""" +# try: +# # 1. 字节串解码为字符串(去除 b'' 包裹) +# data_str = raw_data.decode('utf-8').strip() # strip() 去除首尾的换行符\r\n和空格 +# # 此时 data_str 应为 'ST,NT, +0000175' + +# # 2. 按逗号分割字符串,取第三个字段(包含重量的部分) +# parts = data_str.split(',') # 分割后为 ['ST', 'NT', '+0000175'] +# weight_part = parts[2].strip() # 得到 '+0000175' + +# # 3. 提取纯数字(去除可能的正负号和前导零) +# # 方法:替换掉非数字字符,再转换为整数 +# weight_num = int(''.join(filter(str.isdigit, weight_part))) +# return weight_num +# except (IndexError, ValueError, UnicodeDecodeError) as e: +# print(f"解析失败:{e},原始数据:{raw_data}") +# return None + +# # 创建TCP连接并持续读取 +# with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: +# try: +# s.settimeout(TIMEOUT) +# s.connect((IP, PORT)) +# print(f"✅ 已连接到 {IP}:{PORT},开始持续读取重量...\n") + +# while True: # 循环读取 +# data = s.recv(1024) # 接收数据 +# if data: +# print(f"原始数据:{data}") +# weight = parse_weight(data) +# if weight is not None: +# print(f"解析出重量:{weight}\n") +# else: +# print("❌ 未收到数据,等待下一次...\n") +# except ConnectionRefusedError: +# print(f"❌ 连接失败:{IP}:{PORT} 拒绝连接") +# except socket.timeout: +# print(f"❌ 超时:{TIMEOUT}秒内未收到数据") +# except Exception as e: +# print(f"❌ 发生错误:{e}") + import socket +import time # 导入时间模块,用于记录时间戳 # 设备信息 IP = "192.168.250.63" PORT = 502 TIMEOUT = 10 # 超时时间(秒) + def parse_weight(raw_data): """解析原始数据,提取重量(如从 b'ST,NT, +0000175\r\n' 中提取 175)""" try: - # 1. 字节串解码为字符串(去除 b'' 包裹) - data_str = raw_data.decode('utf-8').strip() # strip() 去除首尾的换行符\r\n和空格 - # 此时 data_str 应为 'ST,NT, +0000175' - - # 2. 按逗号分割字符串,取第三个字段(包含重量的部分) - parts = data_str.split(',') # 分割后为 ['ST', 'NT', '+0000175'] - weight_part = parts[2].strip() # 得到 '+0000175' - - # 3. 提取纯数字(去除可能的正负号和前导零) - # 方法:替换掉非数字字符,再转换为整数 - weight_num = int(''.join(filter(str.isdigit, weight_part))) + data_str = raw_data.decode("utf-8").strip() + parts = data_str.split(",") + weight_part = parts[2].strip() + weight_num = int("".join(filter(str.isdigit, weight_part))) return weight_num except (IndexError, ValueError, UnicodeDecodeError) as e: print(f"解析失败:{e},原始数据:{raw_data}") return None + # 创建TCP连接并持续读取 with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: try: s.settimeout(TIMEOUT) s.connect((IP, PORT)) print(f"✅ 已连接到 {IP}:{PORT},开始持续读取重量...\n") - + + last_receive_time = None # 记录上一次收到有效数据的时间戳(初始为None) + while True: # 循环读取 data = s.recv(1024) # 接收数据 if data: print(f"原始数据:{data}") weight = parse_weight(data) if weight is not None: - print(f"解析出重量:{weight}\n") + current_time = time.time() # 当前时间戳(秒,带小数) + print(f"解析出重量:{weight}") + + # 计算与上一次有效数据的时间间隔 + if last_receive_time is not None: + interval = current_time - last_receive_time # 间隔时间(秒) + print(f"与上一次数据的间隔:{interval:.3f} 秒\n") # 保留3位小数 + else: + print("(首次收到数据,无间隔记录)\n") + + last_receive_time = current_time # 更新上一次时间戳 else: print("❌ 未收到数据,等待下一次...\n") except ConnectionRefusedError: @@ -45,4 +102,4 @@ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: except socket.timeout: print(f"❌ 超时:{TIMEOUT}秒内未收到数据") except Exception as e: - print(f"❌ 发生错误:{e}") \ No newline at end of file + print(f"❌ 发生错误:{e}") From ff2af1d5818956f2de359513203cbcfb89d981f2 Mon Sep 17 00:00:00 2001 From: yanganjie Date: Tue, 4 Nov 2025 11:03:39 +0800 Subject: [PATCH 06/16] =?UTF-8?q?=E5=88=A0=E9=99=A4test2.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test2.py | 105 ------------------------------------------------------- 1 file changed, 105 deletions(-) delete mode 100644 test2.py diff --git a/test2.py b/test2.py deleted file mode 100644 index 6dd4c8d..0000000 --- a/test2.py +++ /dev/null @@ -1,105 +0,0 @@ -# import socket - -# # 设备信息 -# IP = "192.168.250.63" -# PORT = 502 -# TIMEOUT = 10 # 超时时间(秒) - -# def parse_weight(raw_data): -# """解析原始数据,提取重量(如从 b'ST,NT, +0000175\r\n' 中提取 175)""" -# try: -# # 1. 字节串解码为字符串(去除 b'' 包裹) -# data_str = raw_data.decode('utf-8').strip() # strip() 去除首尾的换行符\r\n和空格 -# # 此时 data_str 应为 'ST,NT, +0000175' - -# # 2. 按逗号分割字符串,取第三个字段(包含重量的部分) -# parts = data_str.split(',') # 分割后为 ['ST', 'NT', '+0000175'] -# weight_part = parts[2].strip() # 得到 '+0000175' - -# # 3. 提取纯数字(去除可能的正负号和前导零) -# # 方法:替换掉非数字字符,再转换为整数 -# weight_num = int(''.join(filter(str.isdigit, weight_part))) -# return weight_num -# except (IndexError, ValueError, UnicodeDecodeError) as e: -# print(f"解析失败:{e},原始数据:{raw_data}") -# return None - -# # 创建TCP连接并持续读取 -# with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: -# try: -# s.settimeout(TIMEOUT) -# s.connect((IP, PORT)) -# print(f"✅ 已连接到 {IP}:{PORT},开始持续读取重量...\n") - -# while True: # 循环读取 -# data = s.recv(1024) # 接收数据 -# if data: -# print(f"原始数据:{data}") -# weight = parse_weight(data) -# if weight is not None: -# print(f"解析出重量:{weight}\n") -# else: -# print("❌ 未收到数据,等待下一次...\n") -# except ConnectionRefusedError: -# print(f"❌ 连接失败:{IP}:{PORT} 拒绝连接") -# except socket.timeout: -# print(f"❌ 超时:{TIMEOUT}秒内未收到数据") -# except Exception as e: -# print(f"❌ 发生错误:{e}") - -import socket -import time # 导入时间模块,用于记录时间戳 - -# 设备信息 -IP = "192.168.250.63" -PORT = 502 -TIMEOUT = 10 # 超时时间(秒) - - -def parse_weight(raw_data): - """解析原始数据,提取重量(如从 b'ST,NT, +0000175\r\n' 中提取 175)""" - try: - data_str = raw_data.decode("utf-8").strip() - parts = data_str.split(",") - weight_part = parts[2].strip() - weight_num = int("".join(filter(str.isdigit, weight_part))) - return weight_num - except (IndexError, ValueError, UnicodeDecodeError) as e: - print(f"解析失败:{e},原始数据:{raw_data}") - return None - - -# 创建TCP连接并持续读取 -with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - try: - s.settimeout(TIMEOUT) - s.connect((IP, PORT)) - print(f"✅ 已连接到 {IP}:{PORT},开始持续读取重量...\n") - - last_receive_time = None # 记录上一次收到有效数据的时间戳(初始为None) - - while True: # 循环读取 - data = s.recv(1024) # 接收数据 - if data: - print(f"原始数据:{data}") - weight = parse_weight(data) - if weight is not None: - current_time = time.time() # 当前时间戳(秒,带小数) - print(f"解析出重量:{weight}") - - # 计算与上一次有效数据的时间间隔 - if last_receive_time is not None: - interval = current_time - last_receive_time # 间隔时间(秒) - print(f"与上一次数据的间隔:{interval:.3f} 秒\n") # 保留3位小数 - else: - print("(首次收到数据,无间隔记录)\n") - - last_receive_time = current_time # 更新上一次时间戳 - else: - print("❌ 未收到数据,等待下一次...\n") - except ConnectionRefusedError: - print(f"❌ 连接失败:{IP}:{PORT} 拒绝连接") - except socket.timeout: - print(f"❌ 超时:{TIMEOUT}秒内未收到数据") - except Exception as e: - print(f"❌ 发生错误:{e}") From ed73abaf3f48b358403cb3b6ad19147f1a26bc39 Mon Sep 17 00:00:00 2001 From: yanganjie Date: Tue, 4 Nov 2025 11:40:52 +0800 Subject: [PATCH 07/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E4=B8=8A?= =?UTF-8?q?=E6=96=99=E6=96=97=E5=8F=98=E9=80=81=E5=99=A8=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=EF=BC=8C=E6=98=BE=E7=A4=BA=E5=88=B0=E7=95=8C=E9=9D=A2=EF=BC=8C?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=97=B4=E9=9A=94=E4=B8=BA5=E7=A7=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/main_controller.py | 19 +++++++------ hardware/transmitter.py | 52 ++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/controller/main_controller.py b/controller/main_controller.py index 060fe1c..c97ed15 100644 --- a/controller/main_controller.py +++ b/controller/main_controller.py @@ -33,17 +33,18 @@ class MainController: def _onTimer(self): # 定时任务逻辑 # 变送器是 192.168.250.63,端口 502 - loc_tra=TransmitterController(RelayController(host="192.168.250.63", port=502)) - upper_weight=loc_tra.read_data(1) - lower_weight=loc_tra.read_data(2) - if upper_weight is None: - upper_weight=0 + loc_tra=TransmitterController(RelayController()) + upper_weight=loc_tra.read_data(1) # 目前只有上料斗安装了变送器 + # lower_weight=loc_tra.read_data(2) + if upper_weight is not None: + # upper_weight=0 + self.main_window.hopper_widget.setUpperHopperWeight(upper_weight) - if lower_weight is None: - lower_weight=0 + # if lower_weight is None: + # lower_weight=0 - self.main_window.hopper_widget.setUpperHopperWeight(upper_weight) - self.main_window.hopper_widget.setLowerHopperWeight(lower_weight) + # self.main_window.hopper_widget.setUpperHopperWeight(upper_weight) + # self.main_window.hopper_widget.setLowerHopperWeight(lower_weight) # 重新启动定时器以实现重复执行 self.timer = threading.Timer(5.0, self._onTimer) self.timer.start() diff --git a/hardware/transmitter.py b/hardware/transmitter.py index 1bf6a19..dd72798 100644 --- a/hardware/transmitter.py +++ b/hardware/transmitter.py @@ -20,7 +20,7 @@ class TransmitterController: } } - def read_data(self, transmitter_id): + def read_data_back(self, transmitter_id): """读取变送器数据""" try: if transmitter_id not in self.config: @@ -67,3 +67,53 @@ class TransmitterController: return None finally: self.relay_controller.modbus_client.close() + + # 直接读取 变送器返回的数据 + def read_data(self, transmitter_id): + """ + Args: transmitter_id 为1 表示上料斗, 为2 表示下料斗 + return: 读取成功返回重量 weight: int, 失败返回 None + """ + if transmitter_id == 1: + # 上料斗变送器的信息: + IP = "192.168.250.63" + PORT = 502 + TIMEOUT = 5 # 超时时间为 5秒 + weight = None + + import socket + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + try: + s.settimeout(TIMEOUT) + s.connect((IP, PORT)) + print(f"✅ 连接 {IP}:{PORT} 成功") + + # 接收数据(变送器主动推送,单次recv即可获取一条完整数据) + data = s.recv(1024) + if data: + # print(f"收到原始数据:{data}") + weight = self.parse_weight(data) + else: + print("❌ 未收到设备数据") + + except ConnectionRefusedError: + print(f"❌ 连接失败:{IP}:{PORT} 拒绝连接(设备离线/端口错误)") + except socket.timeout: + print(f"❌ 超时:{TIMEOUT}秒内未收到数据") + except Exception as e: + print(f"❌ 读取异常:{e}") + + # 成功返回重量(int),失败返回None + return weight + + def parse_weight(raw_data): + """解析函数:提取重量数值(如从 b'ST,NT,+0000175\r\n' 中提取 175)""" + try: + data_str = raw_data.decode('utf-8').strip() + parts = data_str.split(',') + weight_part = parts[2].strip() + return int(''.join(filter(str.isdigit, weight_part))) + except (IndexError, ValueError, UnicodeDecodeError) as e: + # print(f"数据解析失败:{e},原始数据:{raw_data}") + # 注意:可能会出现解析失败的情况,此时返回None,界面不用更新数据就行 + return None \ No newline at end of file From b2349365048c9e8c0cd36edeacfee62b400c114e Mon Sep 17 00:00:00 2001 From: yanganjie Date: Tue, 4 Nov 2025 15:00:13 +0800 Subject: [PATCH 08/16] =?UTF-8?q?=E4=B8=8A=E6=96=99=E6=96=97=E5=8F=98?= =?UTF-8?q?=E9=80=81=E5=99=A8=E9=87=8D=E9=87=8F=E6=95=B0=E6=8D=AE=E8=AF=BB?= =?UTF-8?q?=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- hardware/__pycache__/__init__.cpython-39.pyc | Bin 420 -> 0 bytes hardware/__pycache__/inverter.cpython-39.pyc | Bin 2356 -> 0 bytes hardware/__pycache__/relay.cpython-39.pyc | Bin 2711 -> 0 bytes .../__pycache__/transmitter.cpython-39.pyc | Bin 1877 -> 0 bytes hardware/transmitter.py | 83 ++++++++++++++---- 6 files changed, 70 insertions(+), 17 deletions(-) delete mode 100644 hardware/__pycache__/__init__.cpython-39.pyc delete mode 100644 hardware/__pycache__/inverter.cpython-39.pyc delete mode 100644 hardware/__pycache__/relay.cpython-39.pyc delete mode 100644 hardware/__pycache__/transmitter.cpython-39.pyc diff --git a/.gitignore b/.gitignore index 6fb6717..4570eb6 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ dist/ PyQt_Fluent_Widgets.egg-info/ PySide6_Fluent_Widgets.egg-info/ PyQt6_Fluent_Widgets.egg-info/ -PySide2_Fluent_Widgets.egg-info/ \ No newline at end of file +PySide2_Fluent_Widgets.egg-info/ +/hardware/__pycache__ +__pycache__ diff --git a/hardware/__pycache__/__init__.cpython-39.pyc b/hardware/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index c70084f396446f3191673c9efcae64ae3db466fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 420 zcmYe~<>g`k0!``P8M=%N439w^WWWUEH~?|636Mx(h+;@#Okv7l%w>vVVg#|7bC`2k zqFBIemK@exwkS46hIED$)?v%)44UkvHbAXh&lj$FvU}UJzU5Clwmn<1@af#? zTu*yipH5i)th3?SoKA@Ji*@^+E@^*0s|BjAZ|T$i#au6e4r9<{yu}}snv+=RoS#=x zl%JE6S_Bjl^2{qsEh^%*lz5U&&C!4l)fy{IYSeib>Ng&o9c3Daj~G zO^r{6xHP`FvbZEQH>M=DxTH8nPcJ4Tu_&cHu_!erK0Y%qvm`!Vub}c4hfQvNN@-52 R9mtKv96*AFg@+LcnE>7yhLZpQ diff --git a/hardware/__pycache__/inverter.cpython-39.pyc b/hardware/__pycache__/inverter.cpython-39.pyc deleted file mode 100644 index d3fae76e081cb31adde0e40b15bb793a2b91c6c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2356 zcmZ{m|8EpU6u@U@zuew+?>Gu-O8s(vNz4&#_`w*{5JQ3@n%G}TV!T|kS#PFg%XP2J z?iKE`M<6AUkbn>&iWJil3HXCFMkGc8{1?XTkILP(zxj)aLKNTIz1z05?q=WIyf^da z?d?A2ET;VFTD%ZFU zt;SQ}+Xx5WcV~t4h1^?neOaqkg{+{T{)N9&@a%;r@*B{ASP(Ht@Ql%-XK1x~9dwKn z@T-qa#ts^LRw(?EC|0(CDC(i%Rqa_}IXoPkkm7J%xRr(_#iZjw7-7!yZRscdQr7cp zGf6juE}AQ~>1o^Lp&40wy`mE4rKs8sF!x_3%ud@!tz?~02h)TzS!AK=iRy$LfSA4| zatkyMcfT`Qo_PI8O&%)y2c-~wH=-a3z53nqw^`o;8(*Hp8_c z5}+o-^&!GjXOx!e4+N@j&M~A_Otlvd;J@Kw?E`EoF=*@S*e8|3B{VCYF<(v1qcW<^o z`*P*#`Szt7AiHNj{qt3@biP>V{BUvk{@0ze*SdGlckj%1?_BR*_z_0Mh;`=Av`;VY zbMi3S`Qz8ek0l}?@tXjLT9m)_?a3Ok>-IO_FW(Ms#G1}`e6#v*A*2%)MlgueXUZhc|t;s zA)$f_1=1aa^jAmmm=m`*B*M9Pc0%t2F1ufmd&Io6O7NJUv?3XBa#jCIalZ=brIWODo5Qv1X_)K)F3(okZdcGoeh z5V?z|I-j3;aCd&Ruuo!nlrN(&P^3{rcqw7Z<{XmDlP{nW&x|@&XY_sgMzWN|Gfv|7 zj;$x=JKtSRtnr$0a6K#!)Sp2w{c;dTzKUB$K8+6XIFOjT5=)50DRLME(j%Wmf$YY> zs1Y!sZddR)mIXAthyvqXLk2>NXX_iF_C5eX6a(|VPsR?jqee_17eIxU!8X33XRwHp zl=-*HHZZbd6GnQ=jp12qS*|@TEGx`d7L<~D6@QzSb+~R!j+q#J`HciT)Ou2CbR|IfsW_9q&#nY+-~9f$HZ)*TOpsr&~fiiIHn diff --git a/hardware/__pycache__/relay.cpython-39.pyc b/hardware/__pycache__/relay.cpython-39.pyc deleted file mode 100644 index 3bd4bf5cbd9b4152ee5252b77df9b2fa674870d9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2711 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 Date: Tue, 4 Nov 2025 16:20:50 +0800 Subject: [PATCH 09/16] =?UTF-8?q?main=5Fcontroller:=20add=20=E9=87=8D?= =?UTF-8?q?=E9=87=8F=E8=AF=BB=E5=8F=96=E7=BA=BF=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/main_controller.py | 121 +++++++++++++------------- hardware/transmitter.py | 6 +- view/widgets/bottom_control_widget.py | 4 +- view/widgets/system_nav_bar.py | 2 + 4 files changed, 68 insertions(+), 65 deletions(-) diff --git a/controller/main_controller.py b/controller/main_controller.py index c97ed15..586def9 100644 --- a/controller/main_controller.py +++ b/controller/main_controller.py @@ -1,98 +1,97 @@ -from re import U -from hardware import transmitter -from view.main_window import MainWindow +from PySide6.QtCore import QTimer, Signal, QObject # 导入Qt核心类 +from PySide6.QtWidgets import QApplication # 用于获取主线程 import threading +from hardware import transmitter +from view.main_window import MainWindow from .camera_controller import CameraController from .bottom_control_controller import BottomControlController from hardware.transmitter import TransmitterController from hardware.relay import RelayController +# 定义信号类(用于后台线程向 UI主线程传递数据) +class Signals(QObject): + weight_updated = Signal(int) # 传递上料斗重量 + class MainController: def __init__(self): # 主界面 self.main_window = MainWindow() - # 定时器 - self.timer = threading.Timer(5.0, self._onTimer) - self.timer.start() # 每5秒触发一次 - - self.timer2=threading.Timer(1.0, self._onTimer2) - self.timer2.start() # 每秒触发一次 - self.angle=10 - self.max_angle=60 - self.min_angle=10 - self.is_add=True - # 初始化子界面 + + # 1. 用于更新下料斗夹爪角度的QTimer + self.timer_angle = QTimer() + self.timer_angle.setInterval(1000) # 1秒触发一次 + self.timer_angle.timeout.connect(self._onTimer2) # 连接到角度更新函数 + self.timer_angle.start() + + # 下料斗夹具角度相关参数 + self.angle = 10 + self.max_angle = 60 + self.min_angle = 10 + self.is_add = True + + # 2. 用于更新重量的 QTimer + self.timer_weight = QTimer() + self.timer_weight.setInterval(2000) # 2秒触发一次 + self.timer_weight.timeout.connect(self._read_weight_in_background) + self.timer_weight.start() + + # 信号实例(用于后台线程传递数据到主线程) + self.signals = Signals() + self.signals.weight_updated.connect(self._update_upper_weight) # 主线程更新UI + + # 初始化子界面和控制器 self._initSubViews() - - # 初始化子控制器 self._initSubControllers() - - # self.__connectSignals() - - def _onTimer(self): - # 定时任务逻辑 - # 变送器是 192.168.250.63,端口 502 - loc_tra=TransmitterController(RelayController()) - upper_weight=loc_tra.read_data(1) # 目前只有上料斗安装了变送器 - # lower_weight=loc_tra.read_data(2) - if upper_weight is not None: - # upper_weight=0 - self.main_window.hopper_widget.setUpperHopperWeight(upper_weight) + def _read_weight_in_background(self): + """在后台线程中读取重量""" + def weight_task(): + loc_tra = TransmitterController(RelayController()) + # 上料斗重量 (目前只有上料斗安装变送器, 可以读取到重量) + upper_weight = loc_tra.read_data(1) + # 发送信号到主线程更新UI + if upper_weight is not None: + self.signals.weight_updated.emit(upper_weight) - # if lower_weight is None: - # lower_weight=0 - - # self.main_window.hopper_widget.setUpperHopperWeight(upper_weight) - # self.main_window.hopper_widget.setLowerHopperWeight(lower_weight) - # 重新启动定时器以实现重复执行 - self.timer = threading.Timer(5.0, self._onTimer) - self.timer.start() - pass - + # 启动后台线程执行重量读取操作 + threading.Thread(target=weight_task, daemon=True).start() + + def _update_upper_weight(self, weight): + """主线程中 更新界面上料斗重量数据""" + self.main_window.hopper_widget.setUpperHopperWeight(weight) + def _onTimer2(self): - # print(str(self.angle)) - # 定时任务逻辑 + """QTimer触发的 料斗夹爪角度更新""" if self.is_add: - self.angle+=1 + self.angle += 1 else: - self.angle-=1 + self.angle -= 1 - if self.angle>self.max_angle: - self.is_add=False - self.angle=self.max_angle - if self.angle<=self.min_angle: - self.is_add=True - self.angle=10 - + if self.angle > self.max_angle: + self.is_add = False + self.angle = self.max_angle + if self.angle <= self.min_angle: + self.is_add = True + self.angle = 10 + + # 直接更新UI self.main_window.hopper_widget.setLowerHopperOpeningAngle(self.angle) - # 重新启动定时器以实现重复执行 - self.timer2 = threading.Timer(1.0, self._onTimer2) - self.timer2.start() - pass - - + def showMainWindow(self): - # self.main_window.show() self.main_window.showFullScreen() self.main_window.dispatch_task_widget.set_task_time("task1","15:44 PM") self.main_window.dispatch_task_widget.set_task_time("task2","17:37 PM") self.main_window.segment_task_widget.set_task_time("task1","15:38 PM") self.main_window.segment_task_widget.set_task_time("task2","17:24 PM") - - def _initSubControllers(self): - # 振捣视频控制 self.camera_controller = CameraController( video_view=self.main_window.vibration_video ) - # 底部控制(按钮)控制器 self.bottom_control_controller = BottomControlController( bottom_control_widget=self.main_window.bottom_control_widget, main_window=self.main_window ) - def _initSubViews(self): pass \ No newline at end of file diff --git a/hardware/transmitter.py b/hardware/transmitter.py index 1bd2ef4..d22b033 100644 --- a/hardware/transmitter.py +++ b/hardware/transmitter.py @@ -79,7 +79,7 @@ class TransmitterController: # 上料斗变送器的信息: IP = "192.168.250.63" PORT = 502 - TIMEOUT = 5 # 超时时间为 5秒 + TIMEOUT = 2 # 超时时间为 2秒 BUFFER_SIZE= 1024 weight = None @@ -106,9 +106,9 @@ class TransmitterController: print("未收到设备数据") except ConnectionRefusedError: - print(f"连接失败:{IP}:{PORT} 拒绝连接(设备离线/端口错误)") + print(f"变送器连接失败:{IP}:{PORT} 拒绝连接(设备离线/端口错误)") except socket.timeout: - print(f"超时:{TIMEOUT}秒内未收到数据") + print(f"读取变送器数据超时:{TIMEOUT}秒内未收到数据") except Exception as e: print(f"读取异常:{e}") diff --git a/view/widgets/bottom_control_widget.py b/view/widgets/bottom_control_widget.py index a59f05f..051d74e 100644 --- a/view/widgets/bottom_control_widget.py +++ b/view/widgets/bottom_control_widget.py @@ -6,6 +6,7 @@ from view.widgets.switch_button import SwitchButton import resources.resources_rc from utils.image_paths import ImagePaths +# 底部的控制控件,包括 系统诊断、系统中心等按钮 class BottomControlWidget(QWidget): def __init__(self, parent=None): super().__init__(parent) @@ -262,8 +263,9 @@ class BottomControlWidget(QWidget): text_label.setStyleSheet("color: #3bfff8; font-size: 20px;") layout.addWidget(text_label, alignment=Qt.AlignVCenter) - # 开关 + # 开关(初始的时候,自动模式是打开的) self.auto_switch = SwitchButton() + self.auto_switch.setChecked(True) # 设置自动模式初始状态 layout.addWidget(self.auto_switch, alignment=Qt.AlignVCenter | Qt.AlignRight) return widget diff --git a/view/widgets/system_nav_bar.py b/view/widgets/system_nav_bar.py index 4664fa1..9090473 100644 --- a/view/widgets/system_nav_bar.py +++ b/view/widgets/system_nav_bar.py @@ -5,6 +5,8 @@ from PySide6.QtCore import Qt, QTimer, QDateTime import resources.resources_rc from utils.image_paths import ImagePaths +"""主界面最上方的导航栏""" + # 自定义消息容器, 显示系统消息 class MsgContainer(QWidget): def __init__(self, parent=None): From d2603cec4d3a576cc29276eb318ae3ccff97a28d Mon Sep 17 00:00:00 2001 From: yanganjie Date: Thu, 6 Nov 2025 10:55:29 +0800 Subject: [PATCH 10/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=87=8D=E9=87=8F?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=98=BE=E7=A4=BA=EF=BC=8Chopper=5Fcontrolle?= =?UTF-8?q?r=20=E6=8F=90=E4=BE=9B=E4=BA=86=E6=96=99=E6=96=97=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E7=9A=84=E6=95=B0=E6=8D=AE=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + controller/hopper_controller.py | 153 ++++++++++++++++++ controller/main_controller.py | 73 ++------- core/__pycache__/__init__.cpython-39.pyc | Bin 349 -> 0 bytes core/__pycache__/state.cpython-39.pyc | Bin 846 -> 0 bytes core/__pycache__/system.cpython-39.pyc | Bin 4781 -> 0 bytes view/widgets/conveyor_system_widget.py | 43 ++++- view/widgets/hopper_widget.py | 101 ++++++++---- vision/__pycache__/__init__.cpython-39.pyc | Bin 569 -> 0 bytes .../alignment_detector.cpython-39.pyc | Bin 786 -> 0 bytes .../__pycache__/anger_caculate.cpython-39.pyc | Bin 2191 -> 0 bytes .../__pycache__/angle_detector.cpython-39.pyc | Bin 857 -> 0 bytes vision/__pycache__/camera.cpython-39.pyc | Bin 2226 -> 0 bytes vision/__pycache__/detector.cpython-39.pyc | Bin 2603 -> 0 bytes .../overflow_detector.cpython-39.pyc | Bin 1301 -> 0 bytes .../resize_tuili_image_main.cpython-39.pyc | Bin 3925 -> 0 bytes 16 files changed, 278 insertions(+), 94 deletions(-) create mode 100644 controller/hopper_controller.py delete mode 100644 core/__pycache__/__init__.cpython-39.pyc delete mode 100644 core/__pycache__/state.cpython-39.pyc delete mode 100644 core/__pycache__/system.cpython-39.pyc delete mode 100644 vision/__pycache__/__init__.cpython-39.pyc delete mode 100644 vision/__pycache__/alignment_detector.cpython-39.pyc delete mode 100644 vision/__pycache__/anger_caculate.cpython-39.pyc delete mode 100644 vision/__pycache__/angle_detector.cpython-39.pyc delete mode 100644 vision/__pycache__/camera.cpython-39.pyc delete mode 100644 vision/__pycache__/detector.cpython-39.pyc delete mode 100644 vision/__pycache__/overflow_detector.cpython-39.pyc delete mode 100644 vision/__pycache__/resize_tuili_image_main.cpython-39.pyc diff --git a/.gitignore b/.gitignore index 4570eb6..62b0bd5 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ PyQt6_Fluent_Widgets.egg-info/ PySide2_Fluent_Widgets.egg-info/ /hardware/__pycache__ __pycache__ +/core/__pycache__ +/vision/__pycache__ diff --git a/controller/hopper_controller.py b/controller/hopper_controller.py new file mode 100644 index 0000000..029ed4d --- /dev/null +++ b/controller/hopper_controller.py @@ -0,0 +1,153 @@ +from PySide6.QtCore import QTimer, Signal, QObject, Slot +import threading +from hardware.transmitter import TransmitterController +from hardware.relay import RelayController +from view.widgets.hopper_widget import HopperWidget +from view.widgets.conveyor_system_widget import ConveyorSystemWidget + +# 信号类:后台线程向主线程传递数据 +class HopperSignals(QObject): + upper_weight_updated = Signal(int) # 上料斗重量更新信号 + +class HopperController: + def __init__(self, hopper_view:HopperWidget, conveyor_view:ConveyorSystemWidget): + self.hopper_view = hopper_view + self.conveyor_view = conveyor_view # 控制传送带中的上料斗 + + self.signals = HopperSignals() # 信号 + + # 下料斗夹爪测试用例数据 + # 注意:目前只控制 下料斗的夹爪角度变化 + self.angle = 10 # 夹爪当前角度 + self.max_angle = 60 # 夹爪最大张开角度 + self.min_angle = 10 # 夹爪最小张开角度 + self.is_add = True # 角度增加/减小 控制 + self.timer_angle = QTimer() # 角度更新定时器 + self.timer_angle.setInterval(1000) # 1秒更新一次角度 + + # 重量读取定时器 + self.timer_weight = QTimer() # 重量读取定时器 + self.timer_weight.setInterval(2000) # 每2秒读取一次重量 + + # 绑定信号 + self._connect_signals() + + # 开启定时器 + self.timer_angle.start() + self.timer_weight.start() + + def _connect_signals(self): + # 更新上料斗重量 + self.signals.upper_weight_updated.connect(self.onUpdateUpperHopperWeight) + + # 上料斗重量定时读取 + self.timer_weight.timeout.connect(self.handleReadUpperHopperWeight) + + # 下料斗夹爪定时更新 + self.timer_angle.timeout.connect(self.handleLowerClampAngleUpdate) + + # 上料斗 "开"按钮点击 + self.hopper_view.upper_open_btn.clicked.connect(self.onUpperClampOpenBottonClicked) + + # 上料斗 "破拱"按钮 + self.hopper_view.upper_arch_breaking_signal.connect(self.onUpperArchBreaking) + + # 下料斗 "开"按钮点击 + self.hopper_view.lower_open_btn.clicked.connect(self.onLowerClampOpenBottonClicked) + + # 下料斗 "破拱"按钮 + self.hopper_view.lower_arch_breaking_signal.connect(self.onLowerArchBreaking) + + def handleLowerClampAngleUpdate(self): + """处理下料斗夹爪开合""" + # 角度增减逻辑 + if self.is_add: + self.angle += 1 + else: + self.angle -= 1 + + # 边界控制 + if self.angle > self.max_angle: + self.is_add = False + self.angle = self.max_angle + if self.angle <= self.min_angle: + self.is_add = True + self.angle = self.min_angle + + # 更新下料斗夹爪角度 + self.onUpdateLowerClampAngle(self.angle) + + @Slot(int) + def onUpdateUpperHopperWeight(self, weight:int): + "更新上料斗重量" + self.hopper_view.setUpperHopperWeight(weight) + + # 注意:此时需要同步更新传送带中的上料斗的重量 + self.conveyor_view.setConveyorHopperWeight(weight) + + @Slot(int) + def onUpdateLowerHopperWeight(self, weight:int): + "更新下料斗重量" + self.hopper_view.setLowerHopperWeight(weight) + + @Slot() + def handleReadUpperHopperWeight(self): + # 后台读取上料斗重量 + def upper_weight_task(): + loc_tra = TransmitterController(RelayController()) + # 上料斗重量 (目前只有上料斗安装变送器, 可以读取到重量) + upper_weight = loc_tra.read_data(1) + # 发送信号到主线程更新UI + if upper_weight is not None: + self.signals.upper_weight_updated.emit(upper_weight) + + threading.Thread(target=upper_weight_task, daemon=True).start() + + @Slot(float) + def onUpdateLowerClampAngle(self, angle:float): + """更新下料斗夹爪角度""" + self.hopper_view.setLowerHopperOpeningAngle(angle) + + @Slot(float) + def onUpdateUpperClampAngle(self, angle:float): + """更新上料斗夹爪角度""" + self.hopper_view.setUpperHopperClampAngle(angle) + + @Slot() + def onUpperClampOpenBottonClicked(self): + # 上料斗 夹爪 "开"按钮点击 + print("hopper_controller: onUpperClampOpenBottonClicked") + # 测试上料斗夹爪,6秒打开60度 + self.hopper_view.upper_clamp_widget.testAnimation(target_angle=60, duration=6) + + @Slot(bool) + def onUpperArchBreaking(self, status:bool): + """上料斗破拱: status 为True表示 开启破拱, 为False表示 关闭破拱""" + print("hopper_controller: onUpperArchBreaking ", status) + + @Slot(int) + def onUpperHopperStatusChanged(self, status:int): + """上料斗状态改变: status为 0=绿(正常), 1=黄(警告), 2=红(异常) """ + # 料斗中的状态指示器 + self.hopper_view.setUpperHopperStatus(status) + + @Slot(float) + def onUpdateUpperHopperVolume(self, volume: float): + """更新上料斗显示的方量,如: 2.0""" + self.hopper_view.setUpperHopperVolume(volume) + + @Slot() + def onLowerClampOpenBottonClicked(self): + # 下料斗 夹爪 "开"按钮点击 + print("hopper_controller: onLowerClampOpenBottonClicked") + + @Slot(bool) + def onLowerArchBreaking(self, status:bool): + """下料斗破拱: status 为True表示 开启破拱, 为False表示 关闭破拱""" + print("hopper_controller: onLowerArchBreaking ", status) + + @Slot(int) + def onLowerHopperStatusChanged(self, status:int): + """下料斗状态改变: status为 0=绿(正常), 1=黄(警告), 2=红(异常) """ + # 料斗中的状态指示器 + self.hopper_view.setLowerHopperStatus(status) \ No newline at end of file diff --git a/controller/main_controller.py b/controller/main_controller.py index 586def9..e45aeb9 100644 --- a/controller/main_controller.py +++ b/controller/main_controller.py @@ -5,78 +5,17 @@ from hardware import transmitter from view.main_window import MainWindow from .camera_controller import CameraController from .bottom_control_controller import BottomControlController -from hardware.transmitter import TransmitterController -from hardware.relay import RelayController - -# 定义信号类(用于后台线程向 UI主线程传递数据) -class Signals(QObject): - weight_updated = Signal(int) # 传递上料斗重量 +from .hopper_controller import HopperController class MainController: def __init__(self): # 主界面 self.main_window = MainWindow() - # 1. 用于更新下料斗夹爪角度的QTimer - self.timer_angle = QTimer() - self.timer_angle.setInterval(1000) # 1秒触发一次 - self.timer_angle.timeout.connect(self._onTimer2) # 连接到角度更新函数 - self.timer_angle.start() - - # 下料斗夹具角度相关参数 - self.angle = 10 - self.max_angle = 60 - self.min_angle = 10 - self.is_add = True - - # 2. 用于更新重量的 QTimer - self.timer_weight = QTimer() - self.timer_weight.setInterval(2000) # 2秒触发一次 - self.timer_weight.timeout.connect(self._read_weight_in_background) - self.timer_weight.start() - - # 信号实例(用于后台线程传递数据到主线程) - self.signals = Signals() - self.signals.weight_updated.connect(self._update_upper_weight) # 主线程更新UI - # 初始化子界面和控制器 self._initSubViews() self._initSubControllers() - def _read_weight_in_background(self): - """在后台线程中读取重量""" - def weight_task(): - loc_tra = TransmitterController(RelayController()) - # 上料斗重量 (目前只有上料斗安装变送器, 可以读取到重量) - upper_weight = loc_tra.read_data(1) - # 发送信号到主线程更新UI - if upper_weight is not None: - self.signals.weight_updated.emit(upper_weight) - - # 启动后台线程执行重量读取操作 - threading.Thread(target=weight_task, daemon=True).start() - - def _update_upper_weight(self, weight): - """主线程中 更新界面上料斗重量数据""" - self.main_window.hopper_widget.setUpperHopperWeight(weight) - - def _onTimer2(self): - """QTimer触发的 料斗夹爪角度更新""" - if self.is_add: - self.angle += 1 - else: - self.angle -= 1 - - if self.angle > self.max_angle: - self.is_add = False - self.angle = self.max_angle - if self.angle <= self.min_angle: - self.is_add = True - self.angle = 10 - - # 直接更新UI - self.main_window.hopper_widget.setLowerHopperOpeningAngle(self.angle) - def showMainWindow(self): self.main_window.showFullScreen() self.main_window.dispatch_task_widget.set_task_time("task1","15:44 PM") @@ -85,13 +24,23 @@ class MainController: self.main_window.segment_task_widget.set_task_time("task2","17:24 PM") def _initSubControllers(self): + # 右侧视频显示控制模块 self.camera_controller = CameraController( video_view=self.main_window.vibration_video ) + + # 底部按钮控制模块 self.bottom_control_controller = BottomControlController( bottom_control_widget=self.main_window.bottom_control_widget, main_window=self.main_window ) + # 料斗控制模块(包括 夹爪开合、拱等按钮) + self.hopper_controller = HopperController( + hopper_view = self.main_window.hopper_widget, + conveyor_view = self.main_window.conveyor_system_widget + ) + + def _initSubViews(self): pass \ No newline at end of file diff --git a/core/__pycache__/__init__.cpython-39.pyc b/core/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 893971ba73ea89b906fd4847fab9162fad3a1e08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 349 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 diff --git a/view/widgets/conveyor_system_widget.py b/view/widgets/conveyor_system_widget.py index 9f8b5c2..b1c1942 100644 --- a/view/widgets/conveyor_system_widget.py +++ b/view/widgets/conveyor_system_widget.py @@ -74,12 +74,47 @@ class ConveyorSystemWidget(QWidget): inner_img = ImagePaths.HOPPER2 inner_pixmap = QPixmap(inner_img) if not inner_pixmap.isNull(): - upper_inner_label = QLabel(upper_bg_widget) - upper_inner_label.setPixmap(inner_pixmap) - upper_inner_label.setFixedSize(inner_pixmap.width(), inner_pixmap.height()) - upper_inner_label.move(14, 9) # 保持原位置 + self.upper_inner_label = QLabel(upper_bg_widget) + self.upper_inner_label.setPixmap(inner_pixmap) + self.upper_inner_label.setFixedSize(inner_pixmap.width(), inner_pixmap.height()) + self.upper_inner_label.setScaledContents(False) + self.upper_inner_label.setStyleSheet("background: none;") + self.upper_inner_label.move(14, 9) + self.upper_inner_label.setAlignment(Qt.AlignBottom) return group + + def _update_upper_inner_height(self, total_weight, current_weight: float): + """根据当前重量占比, 更新upper_inner_label的高度, 实现动态进度的效果""" + # 1、处理边界值(超过总重量按100%,低于0按0%) + clamped_weight = max(0.0, min(current_weight, total_weight)) + + # 2、计算占比(0~1之间) + weight_ratio = clamped_weight / (total_weight * 1.0) + + # 3、根据占比计算实际高度 + inner_img_height = 100 # 内部的料斗阴影的高度为100px + target_height = int(weight_ratio * inner_img_height) + + # print("target_height: ", target_height) + + # 4、设置标签高度(动态变化) + self.upper_inner_label.setFixedHeight(target_height) + + # 5、计算标签位置(确保标签底部与父容器底部对齐) + container_bottom = 117 # 容器的高固定为了 117px (背景图片"料斗1"的高) + label_y = container_bottom - target_height - 8 # 标签顶部y坐标 (减去底部8px) + self.upper_inner_label.move(14, label_y) # x固定,y动态计算 + # print("label_y", label_y) + + # 6、强制刷新UI,确保立即显示变化 + self.upper_inner_label.update() + + def setConveyorHopperWeight(self, weight:float): + # 1、更新传送带中的 上料斗内部进度显示 + # 假设上料斗装满之后,总的重量为 5100kg + total_weight = 5100 + self._update_upper_inner_height(total_weight, weight) def create_conveyor(self): """创建传送带组件(包含左右齿轮,group容器背景为传送带图片)""" diff --git a/view/widgets/hopper_widget.py b/view/widgets/hopper_widget.py index 3390208..1dfc5f3 100644 --- a/view/widgets/hopper_widget.py +++ b/view/widgets/hopper_widget.py @@ -89,9 +89,9 @@ class HopperWidget(QWidget): # 背景容器(上位) self.upper_bg_widget = QWidget() self.upper_bg_widget.setFixedSize(outer_width, outer_height) - self.upper_bg_widget.setStyleSheet(f"background-image: url({outer_img}); background-repeat: no-repeat; background-position: center;") + self.upper_bg_widget.setStyleSheet(f"background-image: url({outer_img});background-repeat: no-repeat; background-position: center;") + # self.upper_bg_widget.setStyleSheet(f"background-color:red; background-repeat: no-repeat; background-position: center;") layout.addWidget(self.upper_bg_widget, alignment=Qt.AlignCenter) - # 内框图片(上位) inner_img = ImagePaths.HOPPER2 @@ -99,8 +99,11 @@ class HopperWidget(QWidget): if not inner_pixmap.isNull(): self.upper_inner_label = QLabel(self.upper_bg_widget) self.upper_inner_label.setPixmap(inner_pixmap) - self.upper_inner_label.setFixedSize(inner_pixmap.width(), inner_pixmap.height()) + self.upper_inner_label.setFixedSize(inner_pixmap.width(), inner_pixmap.height()) # 初始宽高 + self.upper_inner_label.setScaledContents(False) # 禁用缩放(避免图片拉伸) + self.upper_inner_label.setStyleSheet("background: none;") self.upper_inner_label.move(14, 9) + self.upper_inner_label.setAlignment(Qt.AlignBottom) # 状态图片(上位,绿色) status_img = ImagePaths.HOPPER_STATUS_GREEN @@ -162,17 +165,6 @@ class HopperWidget(QWidget): self.upper_arch_btn.clicked.connect(self.onUpperArchBreaking) self.lower_arch_btn.clicked.connect(self.onLowerArchBreaking) - self.upper_open_btn.clicked.connect(self.onUpperClampOpen) - self.lower_open_btn.clicked.connect(self.onLowerClampOpen) - - @Slot() - def onUpperClampOpen(self): - self.upper_clamp_widget.testAnimation(target_angle=60, duration=6) # 测试,6秒打开60度 - - @Slot() - def onLowerClampOpen(self): - self.lower_clamp_widget.testAnimation(target_angle=25, duration=6) # 测试,6秒打开30度 - @Slot() def onUpperArchBreaking(self): if self.upper_arch_breaking_status == False: # 不破拱状态 @@ -251,7 +243,10 @@ class HopperWidget(QWidget): self.lower_inner_label = QLabel(self.lower_bg_widget) self.lower_inner_label.setPixmap(inner_pixmap) self.lower_inner_label.setFixedSize(inner_pixmap.width(), inner_pixmap.height()) + self.lower_inner_label.setScaledContents(False) # 禁用图片缩放 + self.lower_inner_label.setStyleSheet("background: none;") self.lower_inner_label.move(14, 9) + self.lower_inner_label.setAlignment(Qt.AlignBottom) # 状态图片(下位) status_img = ImagePaths.HOPPER_STATUS_GREEN @@ -313,10 +308,42 @@ class HopperWidget(QWidget): return group + def _update_upper_inner_height(self, total_weight, current_weight: float): + """根据当前重量占比, 更新upper_inner_label的高度, 实现动态进度的效果""" + # 1、处理边界值(超过总重量按100%,低于0按0%) + clamped_weight = max(0.0, min(current_weight, total_weight)) + + # 2、计算占比(0~1之间) + weight_ratio = clamped_weight / (total_weight * 1.0) + + # 3、根据占比计算实际高度 + inner_img_height = 100 # 内部的料斗阴影的高度为100px + target_height = int(weight_ratio * inner_img_height) + + # print("target_height: ", target_height) + + # 4、设置标签高度(动态变化) + self.upper_inner_label.setFixedHeight(target_height) + + # 5、计算标签位置(确保标签底部与父容器底部对齐) + container_bottom = self.upper_bg_widget.y() + self.upper_bg_widget.height() + label_y = container_bottom - target_height - 8 # 标签顶部y坐标 (减去底部8px) + self.upper_inner_label.move(14, label_y) # x固定,y动态计算 + # print("label_y", label_y) + + # 6、强制刷新UI,确保立即显示变化 + self.upper_inner_label.update() + # 上料斗重量设置 def setUpperHopperWeight(self, weight:float): + # 1、更新上料斗重量标签,显示最新重量 self.upper_weight_label.setText(f"{weight}kg") + # 2、更新上料斗内部进度显示 + # 假设上料斗装满之后,总的重量为 5100kg + total_weight = 5100 + self._update_upper_inner_height(total_weight, weight) + # 上料斗方量设置 def setUpperHopperVolume(self, volume: float): """Args: @@ -334,8 +361,36 @@ class HopperWidget(QWidget): # 下料斗重量设置 def setLowerHopperWeight(self, weight:float): + # 1、更新下料斗显示标签,显示的重量 self.lower_weight_label.setText(f"{weight}kg") + # 2、更新下料斗的进度显示 + # 假设下料斗装满之后 总重量为 5100kg + total_weight = 5100 + self._update_lower_inner_height(total_weight, weight) + + def _update_lower_inner_height(self, total_weight, current_weight: float): + # 1、处理边界值 + clamped_weight = max(0.0, min(current_weight, total_weight)) + + # 2、计算占比 + weight_ratio = clamped_weight / (total_weight * 1.0) + + # 3、根据占比计算当前的实际高度 + inner_img_height = 100 # 内部料斗阴影的高度为100px + target_height = int(weight_ratio * inner_img_height) + + # 4、设置内部阴影标签的高度 + self.lower_inner_label.setFixedHeight(target_height) + + # 5、计算标签位置 + container_bottom = self.lower_bg_widget.y() + self.lower_bg_widget.height() + label_y = container_bottom - target_height - 8 + self.lower_inner_label.move(14, label_y) + + # 6、强制刷新UI,确保立即显示变化 + self.lower_inner_label.update() + # 下料斗开合角度设置 (包括 夹爪和标签) def setLowerHopperOpeningAngle(self, angle: float): """Args: @@ -347,7 +402,7 @@ class HopperWidget(QWidget): # ------------------------------ # 设置上料斗状态(0=绿,1=黄,2=红) # ------------------------------ - def setUpperArchStatus(self, status: int): + def setUpperHopperStatus(self, status: int): """ 设置上料斗状态图片 Args: @@ -369,17 +424,12 @@ class HopperWidget(QWidget): # 加载并缩放图片 status_pixmap = QPixmap(img_path) if not status_pixmap.isNull(): - status_pixmap = status_pixmap.scaled( - 22, 22, - Qt.KeepAspectRatio, - Qt.SmoothTransformation - ) self.upper_status_label.setPixmap(status_pixmap) # ------------------------------ # 设置下料斗状态(0=绿,1=黄,2=红) # ------------------------------ - def setLowerArchStatus(self, status: int): + def setLowerHopperStatus(self, status: int): """ 设置下料斗状态图片 Args: @@ -398,11 +448,6 @@ class HopperWidget(QWidget): status_pixmap = QPixmap(img_path) if not status_pixmap.isNull(): - status_pixmap = status_pixmap.scaled( - 22, 22, - Qt.KeepAspectRatio, - Qt.SmoothTransformation - ) self.lower_status_label.setPixmap(status_pixmap) # 隐藏上料斗 (用于上料斗移动) @@ -428,7 +473,7 @@ if __name__ == "__main__": window.setLowerHopperWeight(2000) window.setUpperHopperVolume(3.0) window.setLowerHopperOpeningAngle(45) - window.setUpperArchStatus(2) - window.setLowerArchStatus(1) + window.setUpperHopperStatus(2) + window.setLowerHopperStatus(1) window.show() sys.exit(app.exec()) \ No newline at end of file diff --git a/vision/__pycache__/__init__.cpython-39.pyc b/vision/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index f4f43ef0f33886b7747eef399e45212dc3c1fcc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 569 zcmYjNJx?1k81`K*m&-Mv!cZapK^A^Mi=YyyQ`H5jgB9u3>1-}n+2_c23CdOpiuh7X zyD%Zt7AYWBVC&!LkQMnFW~MS>J1AhwkKWJcef&mKQz6pl&imW`?;vzzoae{Y&RhNR z_ZkSoh&aS$4tKHZ7`wtM+{2!=JyzvD_N`rIH6Gx=+CHoE5Qh#*5UKrg@Cy>Gp=Q0i ztSQ6MV0$#!Ri|734!6s*pX&H1R0m(w;YIm>5pliifok@G%kdRyh>;RaaT&36+ncL5M>FY zIx|mf6^I^{>r8CGx)dBz9wjC__n7U`b{aDZ5lfSdYm#PFngz*<0nb3yfUn_0voWlJ zo)QCKKeqC0F%HITLfSSwZ8g9oXlU@vM93s=nDOjn^izYGW=2Jf0h}XQ*S>}OH>W64@l8N@gljDdM%=Yw#Qy7h$XO$JCk(V{Rp$Or70UJ)FRk} z1utS%612s7DMCdJr2Y}&Wv;q0xp?vB$+y{-<_+(+@4fxad%w3V85=7guxt4ri{Dj* zex}K=DUjR-4@UtgqWCddKvbDWCDl&fomdWnw9_5%unn-b9%G6l#ESy>8yKVeh^q4_ zh8|FzgsL`F#WC8z4JB5nK8}{~lG0RLsHuVLE!0qj5v$EyE9GdyjMZ2hPh&mSsIjc@ zIM!)yS>rLxFz1gaX-MSjhZr>sjMlEeynJlzpx4J*9)OO9@yJLuWziIxf(SM%MAy@8 zCcygZFMHpfBs)jR#%KThw)c{Qul>&UfIAmmhHF{jl>26@ z`|3mTwAt%)W~1{!=p8*zHg~g@zrM78@3;F_bpcc&*x z1*zA#ZLtkt(wY=cF#r64$Bv9`l9e1(dlm$5|M;B3u}$5zeTU%` zOk%c=_yy4XZ|3fFxmLFq3HRW_60=2lmNDx1RSWon2d)*?L&3cAiW7p2^66O01keLM z2}=~S#9CpCj_rcR6LINZ7hVNGnxdH4#72gyiHqte9#zj_1I`&Z{1UL{O4)OuZV1v3 REX^CRYUHw;Y1hBt=sz%X_{#tQ diff --git a/vision/__pycache__/anger_caculate.cpython-39.pyc b/vision/__pycache__/anger_caculate.cpython-39.pyc deleted file mode 100644 index 6913261bfb28eabca9f84b2f10bd125ffe950d0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2191 zcmaJ?O-vg{6rR~1+Zgi?hNPA1NbaLu50&DzL8`=|?IowaSpy+W+p*@&d$aTY zzIkt~)YJqKjHl_J``!y7^n?rhhYZ3yFs*hVDpIkIVwDxguqO*K5mrH$(sE43NR?Gl zmBw()r{XRY^=*M3>WpG0eSGTVsU)|zJKBKp!+am6^%F*jg8eKewG=tBagbt9ir9$a zQ4Bi)8>_-79>G-bz8t24<1-FZ(UVkL$ct1gV8=(ncNP)dmQ>i8Ml+xVkx5<1vmtvn zWH{wh{c1q1QiBt+_w9#(pZ&d4ELaoSYqD@>1 zbFMnyL~CeMk5GWzKmF5grqybL+Bk+Fi>3(?yjOEyYV!dE98f+5Y&R_(_=K59l>{r{X zw>^pPq)k=RJ0e?OjJcoXH`mt6x5s#Cl;#WW>_vBBwzz(Ad-c3Kd4Fs8!uINg=k?+l z#qR5faD_-|W^`+OV)NmBcYTa&tX=g?7nhbhO+zc){IR&UQNA|6-$p8}^lBXucY56& zoh)5nEU$1)Bb(0TA_^f&?uElU4nd=QVSK+Y&(#0S9>R!OT~>D_q$u}(p)J& zUV1QJiDM6^@#XzEDMqiZ#Z|4hBT`;lC~g$o<+;+G;jM3P6_@T7m%b?#&H;4ArIpf+ zNAA=W_wJ+3d!KV0mggXg?bS=}+(>EmPU*|x(hnEi#Vf_-%N51)^a8lw+_>gW&qg>@ zdk`yzVcLqVsqs`A+`=7f-aF@By0I_L&g^T@uI|R!t-?xaa;msK?SWo8f5Th7gx$#z zz&6_q2TM2er3cqH@2|L%J61X(QK9oiD;}I+#as7TIguI`5Oj+-emllQGm(f&OycM= z8N#&AFezyoJxt;O5U4x!Av`w$Z75~gadRMhkcV-3zC3lVymA!)aUXtPxrWX02T+D! z9cAC-XDUFQUwjcO0COjig+h7ZW(Dde!y`%VZFg{<8|#8;1%UA9>;Q<6uNT(W2;#y+ zgul2RdHU}0j_yn@*{6_nhM4_Y((XQ?X==*ojYCQ{G4;5WvurKhJ(#jmrqK;Gp^fDn@~F*(@I ze5o|i6qU&tk}_;2WpvYKf@!fpS{Z;)i3NMm*US&Rp`eka7XJ5J z<}g_&fuNcI_Tc~y3ITD41F07KKyJi*Pr_jw!nL9gH^Y~JAOUT}FT*d0>*35nF@)b=MVM*z*Nt{vs& M=5-2qhhIqe8}-u;9smFU diff --git a/vision/__pycache__/angle_detector.cpython-39.pyc b/vision/__pycache__/angle_detector.cpython-39.pyc deleted file mode 100644 index 652bdbfbc90c28a0d471a787b0419d3c082ddb43..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 857 zcmZWnO=}ZD7@nEk&6lkwEgnR2%_a04L=->pAUW7WF$9L~&X{i5FJ>oVN+N|8Ybdp# zs9>=+_|bat1C>G>lDKgVqn3fu{Ao!TX&US38Ce7$)940bTFba&Fm3AZY!Xev z3|#5;Z?Q@qVt?W3@ayt$>+W!?J@~jc*n2*_zZ0!Jj=nsM+N;s>?qIXK|73f(^&r}P zo!lq|rZC;S>oe|@^8M{jw6PMs-T2vCIl67TW{sEf(RwdhT+xL_a2-}l|l!z-L^cW0)N;at# zWxS>^RhYj)BCgS|3ztgOpkd9L!VQE!$1PdC!lBGwjat4Zh40X?5lZe>Z`mOTR1bM1 z9Q8#pXsC%Am(;2Y!97Wt?~BoYkp=oNwvIA{5S+B%IU|LSLla~0y`YuLjZPi8yMFfh(!5o4bup+H2&nDf-UN5t| zp|&=tQi5np1FhP?AqgZ>Au5NehysN+;jiGbSF}!Y;+_-l?QWb;G1k0!^YPxy{@!oi zxZK&9B2dmG|GrXI2>Ay)`-=pfVW?Ig2u?Vyk`j53aK@!M!lhYOqMVMBth^2Lq*y_@ zzr>+44Ar^-LL;!6lo+R+&5@GCyLg-@=4eUg-8{)tpv8CxPs3cCck&Fh2`+>898(q;A-vUae}PLc!r~L7hPzfokPIaN-gbP74zD0tx2@DRCL3%wr&9JPtC>3HVPG zJ*Jz@bm8J)u5kWBu8=!d&|e3SUaZV(y5T7wyb}qg% zG;TIvQq)XQpVBJ!_$5u_x-qF%LPS)nVcA-3d|J2kx-lNQ%b5*NQB~d0ZB@lcSQ!w6 z#`pc2FO3{RHn(dbF!)7*hLr>wMg{dY+89?CaV9Pk-7aEHT}kx0h&OX(@tTX6OKwc$ zT*O^=5%*ZY8s?k<^HhISwjOS5u0L$une%7A_U}L5{(K&~?b%{BE`Xv$BvBx3MF)yB zicS#Ogy=#&ScGTyxH-wR!Rg zYMv$H_7P=|JV+T&s!R`-(wB9v)kn&v z?FdaEy-SL2ePC*a#=V#+bi?+N?_RBFrVW=XD+e?yrK3Bm8(h0uQt;bVM^rtk9VS&L zP{T3RO1Qyk@Nc~Ug2;^KXqu&H2G|wW3~io1j#+!=2NECh3U5NdD;JdJ+rSP;d=3$P zHl)6Y7m%WHe1)x&aW+F|m`lZ=%f=x2!76EQ)fh3(0$ZfUbek;{n0SyaG++z(et%XOMPv?rx^SE#~Uah5{+OZ91 zLT&`~@)tKS9Xwfc@GgeEXp1Po5D<)+I0?d&fpJ`%0yT=0Ckf5=ViN+>91`zHbfUPZ z!IC_zuugy=jL{TTXq;v6<}(F)8TcI&p92mK#OJ`ngkGcQP|+L2gRT+^C<1mY`0(v7 zZno}iw1L_Bb*}a2?P8V%ALu27R@KVzh+rB&^ce!7ABJinCkT+LdEy9>B6~=r609jg z39@^Z1{o`>;A?>ol&X3uRjt*zQ$;!y~+fUMv)f^dD8&TFw9f diff --git a/vision/__pycache__/detector.cpython-39.pyc b/vision/__pycache__/detector.cpython-39.pyc deleted file mode 100644 index 1a9c6da958bf49137a30c8a90e8dc4a7cef8a004..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2603 zcma)8|8Ltw6u&z=iQ}YQRbY$`Mt_@vP_>_?QP4nQ41}gk0>m_{k`>u*>&UUo*(poZ zqDd_q-BebAGzJ^AgMI0Km?i|MtfS+v;1~Z2&eDGYF^Tuiaq2dmw2S=i?!9;S-sj!B zcd7LCsRY`={9ls;MMD0@NxDfec?&-OeIO<=sY5jEnL}$#V-&}dBh_S07Gv4T)UsMu zj5AKIrf3Q!?-Da>=H`i+n`2tuq#u%!vI+Z60f;Lce#y3Us5+x#b4B>3LkQ zxD$>Ak|EOyEF;jpX^W3L-lzIF_iDObt4yGDUq?D^P1=TI=@rMGaBHxMffmxOi{&tI zEuvAAXw0N0n-G70QO%OyEfd@BfScft<~0OqG?dn>NJclE*01>fW)M;w0Sfj zRl34vNL8xJ*Qq}eWX#U&D*2wxQOFc&mb!z$mt(Dje~P$=WfEi?XDFG4Oluwhoy-hN zISsir%RXbS8uy3DA<{gNvYG>{H(+H>?4C=nc83Ec=FZ_cFH$f@6W}1d3x0 z^ZiJ$jHbd=?7S`Fw2p$z$M_PNQJFHz9yj;zK%Um`c^PC_Dv)Eu0Xfthz)q^HD$OBw zy8V{M*rTftqq$3+RK0R9TDsL*{}C?A7Fe0%#v&==9-NIw9+FP>((d2n~nd;Tz#Sh-wM@F8?7HA~k+RmUq* zhd}CKQP)4ISDaYF`_b$^B>2knmyrmkwpu=n}U8 diff --git a/vision/__pycache__/overflow_detector.cpython-39.pyc b/vision/__pycache__/overflow_detector.cpython-39.pyc deleted file mode 100644 index 1d303ac14fc4ca28ca32f9736ad6782da2b0fb58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1301 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 Date: Thu, 6 Nov 2025 14:05:39 +0800 Subject: [PATCH 11/16] =?UTF-8?q?fix=20bug(=E8=A7=A3=E5=86=B3=E4=BA=86?= =?UTF-8?q?=E6=96=99=E6=96=97=E8=BF=9B=E5=BA=A6=E6=98=BE=E7=A4=BA=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E7=9B=AE=E5=89=8D=E7=9A=84=E6=96=99=E6=96=97?= =?UTF-8?q?=E8=A3=85=E6=BB=A1=E6=80=BB=E9=87=8D=E9=87=8F=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E4=B8=BA6000kg)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- view/widgets/conveyor_system_widget.py | 4 ++-- view/widgets/hopper_widget.py | 17 ++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/view/widgets/conveyor_system_widget.py b/view/widgets/conveyor_system_widget.py index b1c1942..2eccf44 100644 --- a/view/widgets/conveyor_system_widget.py +++ b/view/widgets/conveyor_system_widget.py @@ -112,8 +112,8 @@ class ConveyorSystemWidget(QWidget): def setConveyorHopperWeight(self, weight:float): # 1、更新传送带中的 上料斗内部进度显示 - # 假设上料斗装满之后,总的重量为 5100kg - total_weight = 5100 + # 假设上料斗装满之后,总的重量为 5100kg (褚工说设置为 6000kg 11/6) + total_weight = 6000 self._update_upper_inner_height(total_weight, weight) def create_conveyor(self): diff --git a/view/widgets/hopper_widget.py b/view/widgets/hopper_widget.py index 1dfc5f3..44dd615 100644 --- a/view/widgets/hopper_widget.py +++ b/view/widgets/hopper_widget.py @@ -326,10 +326,11 @@ class HopperWidget(QWidget): self.upper_inner_label.setFixedHeight(target_height) # 5、计算标签位置(确保标签底部与父容器底部对齐) - container_bottom = self.upper_bg_widget.y() + self.upper_bg_widget.height() + # container_bottom = self.upper_bg_widget.y() + self.upper_bg_widget.height() + container_bottom = 117 # 容器的高固定为了 117px (背景图片"料斗1"的高) label_y = container_bottom - target_height - 8 # 标签顶部y坐标 (减去底部8px) self.upper_inner_label.move(14, label_y) # x固定,y动态计算 - # print("label_y", label_y) + # print("current_weight",current_weight, "label_y", label_y) # 6、强制刷新UI,确保立即显示变化 self.upper_inner_label.update() @@ -340,8 +341,8 @@ class HopperWidget(QWidget): self.upper_weight_label.setText(f"{weight}kg") # 2、更新上料斗内部进度显示 - # 假设上料斗装满之后,总的重量为 5100kg - total_weight = 5100 + # 假设上料斗装满之后,总的重量为 5100kg (褚工说设置为 6000kg 11/6) + total_weight = 6000 self._update_upper_inner_height(total_weight, weight) # 上料斗方量设置 @@ -365,8 +366,8 @@ class HopperWidget(QWidget): self.lower_weight_label.setText(f"{weight}kg") # 2、更新下料斗的进度显示 - # 假设下料斗装满之后 总重量为 5100kg - total_weight = 5100 + # 假设下料斗装满之后 总重量为 5100kg (褚工说设置为 6000kg 11/6) + total_weight = 6000 self._update_lower_inner_height(total_weight, weight) def _update_lower_inner_height(self, total_weight, current_weight: float): @@ -384,9 +385,11 @@ class HopperWidget(QWidget): self.lower_inner_label.setFixedHeight(target_height) # 5、计算标签位置 - container_bottom = self.lower_bg_widget.y() + self.lower_bg_widget.height() + # container_bottom = self.lower_bg_widget.y() + self.lower_bg_widget.height() + container_bottom = 117 # 容器的高固定为了 117px (背景图片"料斗1"的高) label_y = container_bottom - target_height - 8 self.lower_inner_label.move(14, label_y) + # print("_update_lower_inner_height", container_bottom) # 6、强制刷新UI,确保立即显示变化 self.lower_inner_label.update() From 75df5d79efa222812564cf26dbed6a3853ffdb7c Mon Sep 17 00:00:00 2001 From: yanganjie Date: Thu, 6 Nov 2025 15:10:58 +0800 Subject: [PATCH 12/16] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=99=E6=96=97?= =?UTF-8?q?=E9=87=8D=E9=87=8F=E6=9B=B4=E6=96=B0=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- view/widgets/conveyor_system_widget.py | 17 ++++++++--- view/widgets/hopper_widget.py | 42 +++++++++++++++++--------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/view/widgets/conveyor_system_widget.py b/view/widgets/conveyor_system_widget.py index 2eccf44..19da6e3 100644 --- a/view/widgets/conveyor_system_widget.py +++ b/view/widgets/conveyor_system_widget.py @@ -18,6 +18,9 @@ class ConveyorSystemWidget(QWidget): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("料斗与传送带界面") + + self._last_upper_hopper_weight = None # 上一次的上料斗重量(初始为None) + self.setFixedSize(443, 190) self.init_ui() self._bind() @@ -110,11 +113,15 @@ class ConveyorSystemWidget(QWidget): # 6、强制刷新UI,确保立即显示变化 self.upper_inner_label.update() - def setConveyorHopperWeight(self, weight:float): - # 1、更新传送带中的 上料斗内部进度显示 - # 假设上料斗装满之后,总的重量为 5100kg (褚工说设置为 6000kg 11/6) - total_weight = 6000 - self._update_upper_inner_height(total_weight, weight) + def setConveyorHopperWeight(self, weight:int): + if weight != self._last_upper_hopper_weight: + # 1、更新传送带中的 上料斗内部进度显示 + # 假设上料斗装满之后,总的重量为 5100kg (褚工说设置为 6000kg 11/6) + total_weight = 6000 + self._update_upper_inner_height(total_weight, weight) + + # 2、将self._last_upper_hopper_weight设置为当前重量 + self._last_upper_hopper_weight = weight def create_conveyor(self): """创建传送带组件(包含左右齿轮,group容器背景为传送带图片)""" diff --git a/view/widgets/hopper_widget.py b/view/widgets/hopper_widget.py index 44dd615..bc52c42 100644 --- a/view/widgets/hopper_widget.py +++ b/view/widgets/hopper_widget.py @@ -29,6 +29,10 @@ class HopperWidget(QWidget): self.upper_arch_breaking_status = False # 初始为不破拱状态 self.lower_arch_breaking_status = False # 初始为不破拱状态 + # 上一次获取到的料斗的当前重量 + self._last_upper_hopper_weight = None # 上一次的上料斗重量(初始为None) + self._last_lower_hopper_weight = None # 上一次的下料斗重量(初始为None) + # 料斗控制界面的固定大小为 332x482, # 需要根据具体的料斗的图片来调整 # self.setFixedSize(356, 496) @@ -336,14 +340,19 @@ class HopperWidget(QWidget): self.upper_inner_label.update() # 上料斗重量设置 - def setUpperHopperWeight(self, weight:float): - # 1、更新上料斗重量标签,显示最新重量 - self.upper_weight_label.setText(f"{weight}kg") + def setUpperHopperWeight(self, weight:int): + # 仅当重量变化时,才更新标签和进度 + if weight != self._last_upper_hopper_weight: + # 1、更新上料斗重量标签,显示最新重量 + self.upper_weight_label.setText(f"{weight}kg") - # 2、更新上料斗内部进度显示 - # 假设上料斗装满之后,总的重量为 5100kg (褚工说设置为 6000kg 11/6) - total_weight = 6000 - self._update_upper_inner_height(total_weight, weight) + # 2、更新上料斗内部进度显示 + # 假设上料斗装满之后,总的重量为 5100kg (褚工说设置为 6000kg 11/6) + total_weight = 6000 + self._update_upper_inner_height(total_weight, weight) + + # 3、设置_last_upper_hopper_weight 为当前重量 + self._last_upper_hopper_weight = weight # 上料斗方量设置 def setUpperHopperVolume(self, volume: float): @@ -361,14 +370,19 @@ class HopperWidget(QWidget): self.upper_clamp_widget.set_angle(angle) # 下料斗重量设置 - def setLowerHopperWeight(self, weight:float): - # 1、更新下料斗显示标签,显示的重量 - self.lower_weight_label.setText(f"{weight}kg") + def setLowerHopperWeight(self, weight:int): + # 仅当重量变化时,才更新标签和进度 + if weight != self._last_lower_hopper_weight: + # 1、更新下料斗显示标签,显示的重量 + self.lower_weight_label.setText(f"{weight}kg") - # 2、更新下料斗的进度显示 - # 假设下料斗装满之后 总重量为 5100kg (褚工说设置为 6000kg 11/6) - total_weight = 6000 - self._update_lower_inner_height(total_weight, weight) + # 2、更新下料斗的进度显示 + # 假设下料斗装满之后 总重量为 5100kg (褚工说设置为 6000kg 11/6) + total_weight = 6000 + self._update_lower_inner_height(total_weight, weight) + + # 3、设置_last_lower_hopper_weight 为当前重量 + self._last_lower_hopper_weight = weight def _update_lower_inner_height(self, total_weight, current_weight: float): # 1、处理边界值 From 2b93713e7f09216f921e451859bf61f58ead2871 Mon Sep 17 00:00:00 2001 From: yanganjie Date: Thu, 6 Nov 2025 19:03:56 +0800 Subject: [PATCH 13/16] =?UTF-8?q?=E6=90=85=E6=8B=8C=E6=A1=A8=E6=97=8B?= =?UTF-8?q?=E8=BD=AC=EF=BC=88=E7=9B=AE=E5=89=8D=E4=B8=8A=E6=96=99=E6=96=97?= =?UTF-8?q?=E5=B7=A6=E7=A7=BB=E5=88=B0=E6=90=85=E6=8B=8C=E6=A5=BC=E4=B8=8B?= =?UTF-8?q?=E6=97=B6=E8=A7=A6=E5=8F=91=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- view/main_window.py | 10 ++- view/widgets/mixer_widget.py | 162 +++++++++++++++++++++++++++++------ 2 files changed, 144 insertions(+), 28 deletions(-) diff --git a/view/main_window.py b/view/main_window.py index 127be50..705c577 100644 --- a/view/main_window.py +++ b/view/main_window.py @@ -197,6 +197,8 @@ class MainWindow(QWidget): # 以下为模拟: # 假设两秒种之后,移动到了搅拌机下 (这里需要根据实际情况修改) QTimer.singleShot(2000, self.conveyor_system_widget.moveHopperBelowMixer) + # 移动到搅拌楼下,搅拌桨就开始旋转 + QTimer.singleShot(2100, self.mixer_widget.startBladeMix) # 料斗左移完成,恢复料斗右移按钮 QTimer.singleShot(2100, lambda: self.conveyor_system_widget.right_btn.setEnabled(True)) @@ -205,8 +207,12 @@ class MainWindow(QWidget): # 演示效果 self.conveyor_system_widget.moveHopperToTransition() # 移动到过渡的位置 self.hopper_widget.upper_clamp_widget.set_angle(0) # 上料斗向右移动到目的地时,夹爪的角度一定是0 - # 按钮状态:点击料斗右移按钮后,禁用料斗左移按钮 - self.conveyor_system_widget.left_btn.setEnabled(False) + # 按钮状态:点击料斗右移按钮后,禁用料斗左移按钮 + self.conveyor_system_widget.left_btn.setEnabled(False) + + # 开始右移,搅拌桨就停止转动 + self.mixer_widget.stopBladeMix() + # 以下为模拟: # 假设两秒后,传送带中 料斗向右移动完成 (这里需要根据实际情况修改) QTimer.singleShot(1900, self.conveyor_system_widget.hideHopper) # 料斗向右移动完成,隐藏料斗 diff --git a/view/widgets/mixer_widget.py b/view/widgets/mixer_widget.py index bea9c29..921a17b 100644 --- a/view/widgets/mixer_widget.py +++ b/view/widgets/mixer_widget.py @@ -1,13 +1,87 @@ from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout -from PySide6.QtGui import QPixmap, QFont -from PySide6.QtCore import Qt +from PySide6.QtGui import QPixmap, QFont, QTransform +from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, Property import resources.resources_rc from utils.image_paths import ImagePaths +class BladeLabel(QLabel): + def __init__(self, parent=None): + super().__init__(parent) + self._rotation = 0.0 + self._original_pixmap = None + self._original_center_x = 0.0 # 原始图片自身中心点x + self._original_center_y = 0.0 # 原始图片自身中心点y + self._fixed_center_in_parent_x = 0 # 父容器中的固定中心点x(关键) + self._fixed_center_in_parent_y = 0 # 父容器中的固定中心点y(关键) + # self.setFixedSize(50, 54) + + def set_original_pixmap(self, pixmap, fixed_center_x, fixed_center_y): + """ + :param pixmap: 原始图片 + :param fixed_center_x: 父容器中固定的中心点x坐标(绝对位置) + :param fixed_center_y: 父容器中固定的中心点y坐标(绝对位置) + """ + self._original_pixmap = pixmap + if pixmap.isNull(): + print("错误:搅拌桨图片加载失败!") + return + # 记录原始图片自身的中心点(用于旋转计算) + self._original_center_x = 28 # 图片的中心点为 28,28 + self._original_center_y = 28 + # 记录在父容器中的固定中心点(旋转时始终对齐这个点) + self._fixed_center_in_parent_x = fixed_center_x + self._fixed_center_in_parent_y = fixed_center_y + # 初始显示图片 + self.setPixmap(pixmap) + # 初始位置:让原始图片的中心点与固定中心点对齐 + self._update_position(pixmap.width(), pixmap.height()) + + def _update_position(self, current_w, current_h): + """根据当前图片尺寸,计算位置使中心点与固定坐标对齐""" + # 当前图片的中心点坐标(自身坐标系) + current_center_x = current_w / 2 + current_center_y = current_h / 2 + # 计算左上角坐标:固定中心点 - 当前图片中心点 + x = self._fixed_center_in_parent_x - current_center_x + y = self._fixed_center_in_parent_y - current_center_y + self.move(round(x), round(y)) # 取整避免浮点数位置偏差 + self.setFixedSize(current_w, current_h) + + def get_rotation(self): + return self._rotation + + def set_rotation(self, angle): + self._rotation = angle + if self._original_pixmap is None: + return + + # 生成旋转后的图片(保持旋转中心为原始图片中心) + transform = QTransform() + transform.translate(self._original_center_x, self._original_center_y) + transform.rotate(angle) + transform.translate(-self._original_center_x, -self._original_center_y) + rotated_pixmap = self._original_pixmap.transformed(transform, Qt.SmoothTransformation) + + # 强制对齐固定中心点(关键:无论尺寸如何变化,中心点不变) + self._update_position(rotated_pixmap.width(), rotated_pixmap.height()) + self.setPixmap(rotated_pixmap) + + rotation = Property(float, get_rotation, set_rotation) + + def reset_to_original(self): + self._rotation = 0.0 # 重置旋转角度为0° + if self._original_pixmap is not None: + self.setPixmap(self._original_pixmap) # 恢复原始图片 + # 恢复初始位置(基于原始图片尺寸) + self._update_position(self._original_pixmap.width(), self._original_pixmap.height()) + class MixerWidget(QWidget): def __init__(self, parent=None): super().__init__(parent) + + # 两个搅拌桨的转动的动画引用 + self.animations = [] # 保存动画引用 # 初始化布局 layout = QHBoxLayout(self) @@ -28,35 +102,71 @@ class MixerWidget(QWidget): """) layout.addWidget(self.text_label, alignment=Qt.AlignLeft) - # 2. 创建搅拌机设备及搅拌桨图标 + # 2. 创建搅拌机设备 self.device_label = QLabel() device_pixmap = QPixmap(ImagePaths.MIXER) self.device_label.setPixmap(device_pixmap) layout.addWidget(self.device_label, alignment=Qt.AlignLeft) - # 3. 叠加两个搅拌桨图标 - self.blade1 = QLabel(self.device_label) # 从左往右第一个搅拌桨 - blade1_pixmap = QPixmap(ImagePaths.MIXER_PADDLE) - self.blade1.setPixmap(blade1_pixmap) - self.blade1.move( - (device_pixmap.width() - blade1_pixmap.width()) // 2 - 26, - (device_pixmap.height() - blade1_pixmap.height()) // 2 - 4 - ) + # 3. 初始化两个搅拌桨 + self._init_blades() + + def _init_blades(self): + blade_pixmap = QPixmap(ImagePaths.MIXER_PADDLE) + if blade_pixmap.isNull(): + return - self.blade2 = QLabel(self.device_label) - blade2_pixmap = QPixmap(ImagePaths.MIXER_PADDLE) # 从左往右第二个搅拌桨 - self.blade2.setPixmap(blade2_pixmap) - self.blade2.move( - (device_pixmap.width() - blade2_pixmap.width()) // 2 + 31, - (device_pixmap.height() - blade2_pixmap.height()) // 2 - 4 - ) + # 设备背景的尺寸(用于计算固定中心点) + device_pixmap = self.device_label.pixmap() + if not device_pixmap: + return + device_w = device_pixmap.width() + device_h = device_pixmap.height() + + # -------------------------- + # 左搅拌桨:计算固定中心点 + # -------------------------- + left_center_x = (device_w // 2) - 26 # 左桨中心点x(示例值,需根据实际调整) + left_center_y = device_h // 2 - 5 # 左桨中心点y(示例值,需根据实际调整) + self.blade1 = BladeLabel(self.device_label) + self.blade1.set_original_pixmap(blade_pixmap, left_center_x, left_center_y) + + # -------------------------- + # 右搅拌桨:计算固定中心点 + # -------------------------- + right_center_x = (device_w // 2) + 30 # 右桨中心点x(示例值,需根据实际调整) + right_center_y = device_h // 2 - 5 # 右桨中心点y(与左桨对齐) + self.blade2 = BladeLabel(self.device_label) + self.blade2.set_original_pixmap(blade_pixmap, right_center_x, right_center_y) -# 测试代码 -if __name__ == "__main__": - import sys - from PySide6.QtWidgets import QApplication, QMainWindow + def _start_animation(self, blade: BladeLabel, duration: int, reverse: bool = False): + """ + Args: + blade: 所需旋转的搅拌桨标签 + duration 一次搅拌桨旋转所需的时间,值越小,旋转越快 + reverse: 是否反转(逆时针转) + """ + animation = QPropertyAnimation(blade, b"rotation") + animation.setStartValue(360 if reverse else 0) + animation.setEndValue(0 if reverse else 360) + animation.setDuration(duration) + animation.setEasingCurve(QEasingCurve.Linear) + animation.setLoopCount(-1) + self.animations.append(animation) + animation.start() - app = QApplication(sys.argv) - mixer_widget = MixerWidget() - mixer_widget.show() - sys.exit(app.exec()) \ No newline at end of file + # 搅拌桨开始搅拌 + def startBladeMix(self, duration=700): + self.animations.clear() + # 备注:duration控制搅拌桨旋转的速度,值越小旋转得越快 + self._start_animation(self.blade1, duration) + self._start_animation(self.blade2, duration) + + def stopBladeMix(self): + for animation in self.animations: + animation.stop() + + if self.blade1: + self.blade1.reset_to_original() + if self.blade2: + self.blade2.reset_to_original() From 4c48332c06c22d4f3502fac34aed0c606aab4a16 Mon Sep 17 00:00:00 2001 From: yanganjie Date: Fri, 7 Nov 2025 15:45:54 +0800 Subject: [PATCH 14/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E8=AF=8A=E6=96=AD=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/bottom_control_controller.py | 89 ++++- controller/main_controller.py | 1 + images/系统诊断下拉箭头.png | Bin 0 -> 172 bytes images/系统诊断小框.png | Bin 0 -> 213 bytes images/系统诊断弹出背景.png | Bin 0 -> 6260 bytes images/系统诊断毫秒背景.png | Bin 0 -> 735 bytes images/系统诊断状态红.png | Bin 0 -> 257 bytes images/系统诊断状态绿.png | Bin 0 -> 251 bytes images/系统诊断状态黄.png | Bin 0 -> 265 bytes utils/image_paths.py | 11 +- view/main_window.py | 3 +- view/widgets/system_center_dialog.py | 4 +- view/widgets/system_diagnostics_dialog.py | 385 ++++++++++++++++++++++ 13 files changed, 486 insertions(+), 7 deletions(-) create mode 100644 images/系统诊断下拉箭头.png create mode 100644 images/系统诊断小框.png create mode 100644 images/系统诊断弹出背景.png create mode 100644 images/系统诊断毫秒背景.png create mode 100644 images/系统诊断状态红.png create mode 100644 images/系统诊断状态绿.png create mode 100644 images/系统诊断状态黄.png create mode 100644 view/widgets/system_diagnostics_dialog.py diff --git a/controller/bottom_control_controller.py b/controller/bottom_control_controller.py index 6309dd1..bd1c000 100644 --- a/controller/bottom_control_controller.py +++ b/controller/bottom_control_controller.py @@ -2,6 +2,7 @@ from PySide6.QtCore import Qt, QPropertyAnimation, QRect, QParallelAnimationGroup, QEasingCurve from view.widgets.system_center_dialog import SystemCenterDialog from view.widgets.bottom_control_widget import BottomControlWidget +from view.widgets.system_diagnostics_dialog import SystemDiagnosticsDialog """ 控制主界面底部的所有按钮, 包括系统诊断、系统中心等的行为。 @@ -19,11 +20,19 @@ class BottomControlController: self.system_center_dialog.hide() # 初始隐藏 (必须) self._init_system_center_dialog_hide_animations() + # 系统诊断弹窗 + self.system_diagnostics_dialog = SystemDiagnosticsDialog(self.main_window) + self.system_diagnostics_dialog.hide() + self._init_system_diagnostics_dialog_hide_animations() + self._bind_dialog_signals() def _bind_buttons(self): # 底部系统中心按钮 → 触发弹窗显示/隐藏 self.bottom_control_widget.center_btn.clicked.connect(self.toggle_system_center_dialog) + + # 底部系统诊断按钮 → 触发弹窗显示/隐藏 + self.bottom_control_widget.diagnosis_btn.clicked.connect(self.toggle_system_diagnostics_dialog) def _bind_dialog_signals(self): """绑定弹窗按钮的信号""" @@ -53,7 +62,7 @@ class BottomControlController: self.hide_anim_group.finished.connect(self.system_center_dialog.hide) def toggle_system_center_dialog(self): - """切换弹窗的显示/隐藏状态""" + """切换系统中心弹窗的显示/隐藏状态""" if self.system_center_dialog.isVisible(): # 已显示 → 隐藏 # self.system_center_dialog.hide() @@ -94,7 +103,7 @@ class BottomControlController: # 设置弹窗位置 self.system_center_dialog.move(dialog_x, dialog_y) - # ------------------- 业务逻辑方法------------------- + # ------------------- 系统中心弹窗业务逻辑------------------- def handle_sys_setting(self): """系统设置按钮的业务逻辑""" # print("执行系统设置逻辑:如打开系统配置窗口、修改参数等") @@ -108,4 +117,78 @@ class BottomControlController: def handle_user_center(self): """用户中心按钮的业务逻辑""" - # print("执行用户中心逻辑:如切换用户、修改密码等") \ No newline at end of file + # print("执行用户中心逻辑:如切换用户、修改密码等") + + # ------------------- 系统诊断弹窗逻辑------------------- + def _init_system_diagnostics_dialog_hide_animations(self): + """初始化系统诊断弹窗隐藏动画(与显示动画反向:滑出+淡出)""" + # 1. 淡出动画(与显示动画时长一致) + self.dia_hide_opacity_anim = QPropertyAnimation( + self.system_diagnostics_dialog, b"windowOpacity", self.system_diagnostics_dialog + ) + self.dia_hide_opacity_anim.setDuration(300) # 显示动画为400ms + self.dia_hide_opacity_anim.setStartValue(1.0) + self.dia_hide_opacity_anim.setEndValue(0.0) + + # 2. 位置动画(从当前位置滑出到下方100px,与显示动画反向) + self.dia_hide_pos_anim = QPropertyAnimation( + self.system_diagnostics_dialog, b"geometry", self.system_diagnostics_dialog + ) + self.dia_hide_pos_anim.setDuration(300) + self.dia_hide_pos_anim.setEasingCurve(QEasingCurve.InQuart) # 滑出曲线与显示反向 + + # 3. 组合动画(同时执行滑出和淡出) + self.dia_hide_anim_group = QParallelAnimationGroup(self.system_diagnostics_dialog) + self.dia_hide_anim_group.addAnimation(self.dia_hide_opacity_anim) + self.dia_hide_anim_group.addAnimation(self.dia_hide_pos_anim) + # 动画结束后强制隐藏弹窗 + self.dia_hide_anim_group.finished.connect(self.system_diagnostics_dialog.hide) + + def toggle_system_diagnostics_dialog(self): + """切换系统诊断弹窗的显示/隐藏状态""" + if self.system_diagnostics_dialog.isVisible(): + # 已显示 → 执行隐藏动画 + self._start_diagnostics_hide_animation() + else: + # 未显示 → 计算位置并显示(触发显示动画) + self._calc_system_diagnostics_dialog_position() + self.system_diagnostics_dialog.show() + + def _calc_system_diagnostics_dialog_position(self): + """计算系统诊断弹窗位置(显示在诊断按钮上方,与中心弹窗布局一致)""" + btn = self.bottom_control_widget.diagnosis_btn # 诊断按钮 + bottom_widget = self.bottom_control_widget + + # 计算按钮在主窗口中的绝对位置 + bottom_pos_rel_main = bottom_widget.pos() # 底部控件相对于主窗口的位置 + btn_pos_rel_bottom = btn.pos() # 诊断按钮相对于底部控件的位置 + btn_pos_rel_main = bottom_pos_rel_main + btn_pos_rel_bottom # 诊断按钮在主窗口中的绝对位置 + + # 计算弹窗坐标(显示在按钮上方,水平居中对齐) + btn_width = btn.width() + dialog_size = self.system_diagnostics_dialog.size() + # 水平方向:与按钮居中对齐 + # dialog_x = btn_pos_rel_main.x() + (btn_width - dialog_size.width()) // 2 + # dialog_x = btn_pos_rel_main.x() + (btn_width - dialog_size.width()) + dialog_x = btn_pos_rel_main.x() # 与系统诊断按钮的左边平齐 + # 垂直方向:在按钮上方(与按钮保持10px间距) + # dialog_y = btn_pos_rel_main.y() - dialog_size.height() - 10 + dialog_y = btn_pos_rel_main.y() - dialog_size.height() + + # 设置弹窗位置(动画会基于此位置执行滑入效果) + self.system_diagnostics_dialog.move(dialog_x, dialog_y) + + def _start_diagnostics_hide_animation(self): + """启动系统诊断弹窗的隐藏动画(滑出+淡出)""" + current_geo = self.system_diagnostics_dialog.geometry() # 当前位置和尺寸 + # 计算隐藏动画终点(当前位置下方100px,与显示动画起点对应) + end_rect = QRect( + current_geo.x(), + current_geo.y() + 100, # 向下滑出100px + current_geo.width(), + current_geo.height() + ) + # 设置动画参数并启动 + self.dia_hide_pos_anim.setStartValue(current_geo) + self.dia_hide_pos_anim.setEndValue(end_rect) + self.dia_hide_anim_group.start() \ No newline at end of file diff --git a/controller/main_controller.py b/controller/main_controller.py index e45aeb9..0b771fa 100644 --- a/controller/main_controller.py +++ b/controller/main_controller.py @@ -18,6 +18,7 @@ class MainController: def showMainWindow(self): self.main_window.showFullScreen() + # self.main_window.show() self.main_window.dispatch_task_widget.set_task_time("task1","15:44 PM") self.main_window.dispatch_task_widget.set_task_time("task2","17:37 PM") self.main_window.segment_task_widget.set_task_time("task1","15:38 PM") diff --git a/images/系统诊断下拉箭头.png b/images/系统诊断下拉箭头.png new file mode 100644 index 0000000000000000000000000000000000000000..4322476fa8abbe720214f871b2d3d9f339ef7cb7 GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CJ!3HGRcAO0XQpKJwjv*3LdnYtfy0k-?BHMFWK!WXFa#3>DgaL9oBpa6?QzDA;iUX=HefwJ9F2| ViRP}jngz6;!PC{xWt~$(696V>J<_KIEGZrd3)E9_ppNi%fVjJ zRcZ+e?gv=TDoty8_$!<>L4NVS_vzO@OW%0&+kSiA?oH=Q&n5bC9a2)%5uzO-$Fbr5 anLSV6v)gZ)R_z3ICxfS}pUXO@geCyl{6mcZ literal 0 HcmV?d00001 diff --git a/images/系统诊断弹出背景.png b/images/系统诊断弹出背景.png new file mode 100644 index 0000000000000000000000000000000000000000..958bf7800ee22132f54e4d4c438fb5fa695bfae2 GIT binary patch literal 6260 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 literal 0 HcmV?d00001 diff --git a/images/系统诊断毫秒背景.png b/images/系统诊断毫秒背景.png new file mode 100644 index 0000000000000000000000000000000000000000..d3dd2956aa6c84272bdd25e3680527338bf767d6 GIT binary patch literal 735 zcmV<50wDc~P)s&fD2mU@kOCz{X>=R6Bdt^d3x~+fE<7= z08Lm{q&*02o4B^HJSkSzBM?2e-H6jLu=&{?o=VcnD75`LZ7oR;@#slRtiG5yF4wLI z0rB*CM5{MAUwS~_eA8MPYmjT&!gAEMAuN!iCc6bsCBFm6z;gT0!H$TP_49TR?FZ8} z;;QNHe}Y8mVB79YbVCT}&Oj7q5TDq1H({6CX%NVn++8*JC`d>=dwte9`bRO{Llnf1 z%qOp%?E{zWJ3pFdKLc1dGQiH!4t^SjX)t6yghq!+?&r`{5a|oeh4MM%I!j~RG zlTT`T2WHRV!>$=ZY4z)H*f^-d(~F<`9S2#V3m__#Y*?&qjN$EjpD66fdbQcqhFfGx z+!)Y>#_PCsrOM`}_gOqf13FrXBLljAg0!%XOwRx~F`|*_F90+p={+(9@EeIC1RZ<@ RR0;q9002ovPDHLkV1f|yQC9!} literal 0 HcmV?d00001 diff --git a/images/系统诊断状态红.png b/images/系统诊断状态红.png new file mode 100644 index 0000000000000000000000000000000000000000..716cef9842103892e4b767ad79953cf682bfc7dc GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4uAB#T}@sR2?aJY5_^G$u}+=$OUiDBv3Zcit10 zU4`N*Eq4@mUGCO6BVd|dDA-)Vn*Xpu!qI=hgA?s6cUHV)cP(}PyQnVIL_c=j4+b>{ z`3y$(1m-^sU*jW9?uI{CPvEO5nyvbQbq<%vx}xPDWgAyEq$h}+y}jFgl|09~m*O7Z zCLG?PpgQ~9!te=Cbe~@MRuPx2?z}}@^Xd{8x18234ob@VbXQHB_FrV}1hcr7=U@j_FhwPxelF{r5}E+1hGPK$ literal 0 HcmV?d00001 diff --git a/images/系统诊断状态黄.png b/images/系统诊断状态黄.png new file mode 100644 index 0000000000000000000000000000000000000000..b57e019edd42166a90ba063b95e774c9bb2a8872 GIT binary patch literal 265 zcmV+k0rvihP)>=H@{&#OTF?2MrygfjY^h7(IAMO}2I;ptCy3gkc196!Z}Zy_>le z=C(37{&Cjkmb)I+rW0V=&|wX{>f#oHVp(l`4@_h$&;fp`#czPm8wy+jOvWJmf)~H| P00000NkvXXu0mjf#(iqd literal 0 HcmV?d00001 diff --git a/utils/image_paths.py b/utils/image_paths.py index fa1495d..ffdda75 100644 --- a/utils/image_paths.py +++ b/utils/image_paths.py @@ -92,4 +92,13 @@ class ImagePaths: ROUND_BTN_BG2 = ":/icons/images/圆形按钮背景2.png" # 功能:主界面相关 - MAIN_INTERFACE_BACKGROUND = ":/icons/images/主界面背景.png" \ No newline at end of file + MAIN_INTERFACE_BACKGROUND = ":/icons/images/主界面背景.png" + + # 功能: 系统诊断弹窗 + SYSTEM_DIAGNOSTICS_POPUP_BG = "images/系统诊断弹出背景.png" + SYSTEM_DIAGNOSTICS_BOX = "images/系统诊断小框.png" + SYSTEM_DIAGNOSTICS_STATUS_GREEN = "images/系统诊断状态绿.png" + SYSTEM_DIAGNOSTICS_STATUS_YELLOW = "images/系统诊断状态黄.png" + SYSTEM_DIAGNOSTICS_STATUS_RED = "images/系统诊断状态红.png" + SYSTEM_DIAGNOSTICS_MS_BG = "images/系统诊断毫秒背景.png" + SYSTEM_DIAGNOSTICS_DROPDOWN_ARROW = "images/系统诊断下拉箭头.png" \ No newline at end of file diff --git a/view/main_window.py b/view/main_window.py index 705c577..cb91984 100644 --- a/view/main_window.py +++ b/view/main_window.py @@ -63,7 +63,8 @@ class MainWindow(QWidget): # self.setStyleSheet("background-color: #ffffff;") # #001558 # Qt.FramelessWindowHint - # self.setWindowFlags(Qt.FramelessWindowHint) + # 没有顶部的白色边框 + self.setWindowFlags(Qt.FramelessWindowHint) # 无边框 # 设置主界面背景图片 try: diff --git a/view/widgets/system_center_dialog.py b/view/widgets/system_center_dialog.py index 2e9b3f3..be444c9 100644 --- a/view/widgets/system_center_dialog.py +++ b/view/widgets/system_center_dialog.py @@ -60,10 +60,10 @@ class SystemCenterDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) - self.init_ui() + self._init_ui() self.init_animations() # 初始化动画 - def init_ui(self): + def _init_ui(self): # 弹窗基础设置 self.setWindowTitle("系统中心") self.setWindowFlags(Qt.FramelessWindowHint) # 隐藏默认边框 diff --git a/view/widgets/system_diagnostics_dialog.py b/view/widgets/system_diagnostics_dialog.py new file mode 100644 index 0000000..8c89c48 --- /dev/null +++ b/view/widgets/system_diagnostics_dialog.py @@ -0,0 +1,385 @@ +from PySide6.QtWidgets import ( + QApplication, + QMainWindow, + QWidget, + QVBoxLayout, + QGridLayout, + QLabel, + QHBoxLayout, + QListWidget, + QListWidgetItem, + QSpacerItem, + QSizePolicy, + QLineEdit, + QDialog, +) +from PySide6.QtGui import QPixmap, QFont, QColor, QTransform, QPainter +from PySide6.QtCore import ( + Qt, + QPoint, + QEvent, + QPropertyAnimation, + QEasingCurve, + QRect, + QParallelAnimationGroup, +) +import sys + +from utils.image_paths import ImagePaths + +class CustomDropdown(QWidget): + """自定义下拉框组件""" + + def __init__(self, options, arrow_img_path, parent=None): + super().__init__(parent) + self.options = options + self.arrow_img_path = arrow_img_path + self.is_expanded = False + + # 主布局(标签 + 箭头) + self.main_layout = QHBoxLayout(self) + self.main_layout.setContentsMargins(0, 0, 0, 0) + self.main_layout.setSpacing(0) + self.main_layout.setAlignment(Qt.AlignLeft) + self.setFixedSize(63, 19) # 需要根据下拉框需要显示的文字来修改 + # self.setFixedHeight(19) + + # 1. 结果显示标签(QLabel,无clicked信号) + self.result_label = QLabel(options[0]) + self.result_label.setStyleSheet( + """ + background-image: url(""); + color: #16ffff; + background-color: transparent; + border: none; + padding: 0px; + font-size: 18px; + """ + ) + # self.result_label.setCursor(Qt.PointingHandCursor) # 手型光标提示可点击 + self.main_layout.addWidget( + self.result_label, alignment=Qt.AlignVCenter | Qt.AlignLeft + ) + + # 2. 可点击的箭头标签(QLabel) + self.arrow_label = QLabel() + self.arrow_pixmap = QPixmap(arrow_img_path) + self.arrow_label.setStyleSheet("background-image: url(" ");") + self.arrow_label.setPixmap( + self.arrow_pixmap.scaled(12, 9, Qt.KeepAspectRatio, Qt.SmoothTransformation) + ) + self.arrow_label.setCursor(Qt.PointingHandCursor) + self.main_layout.addWidget(self.arrow_label, alignment=Qt.AlignTop) + + # 3. 下拉选项列表(默认选中第一个) + self.list_widget = QListWidget() + self.list_widget.setWindowFlags(Qt.Popup) + + # 设置选项字体 + font = QFont() + font.setPixelSize(16) + + # 添加所有的下拉选项 + for option in options: + item = QListWidgetItem(option) + item.setTextAlignment(Qt.AlignLeft) + item.setFont(font) + self.list_widget.addItem(item) + self.list_widget.setCurrentRow(0) # 默认选中第一项 + self.list_widget.itemClicked.connect(self.select_option) + + # 双保险监听:全局焦点变化 + 事件过滤 + self.app = QApplication.instance() + self.app.focusChanged.connect(self.on_focus_changed) + self.list_widget.installEventFilter(self) + + def mousePressEvent(self, event): + """重写鼠标点击事件,实现QLabel点击功能""" + # 判断点击是否在result_label或arrow_label区域内 + # if self.result_label.underMouse() or self.arrow_label.underMouse(): + # self.toggle_expand() + if self.arrow_label.underMouse(): + self.toggle_expand() + super().mousePressEvent(event) # 传递事件,不影响其他组件 + + def toggle_expand(self): + """切换下拉框展开/收起 + 箭头旋转""" + if self.is_expanded: + self.list_widget.hide() + # 箭头恢复向下 + self.arrow_label.setPixmap( + self.arrow_pixmap.scaled( + 12, 9, Qt.KeepAspectRatio, Qt.SmoothTransformation + ) + ) + else: + # 计算下拉框位置(在标签下方对齐) + label_pos = self.result_label.mapToGlobal( + QPoint(0, self.result_label.height()) + ) + self.list_widget.setGeometry( + label_pos.x(), label_pos.y(), self.result_label.width() + 10, 80 + ) + self.list_widget.show() + self.list_widget.setFocus() + # 箭头旋转180度(向上) + transform = QTransform().rotate(180) + rotated_pixmap = self.arrow_pixmap.transformed( + transform, Qt.SmoothTransformation + ) + self.arrow_label.setPixmap(rotated_pixmap) + self.is_expanded = not self.is_expanded + + def select_option(self, item): + """选择选项后更新标签 + 收起下拉框""" + self.result_label.setText(item.text()) + self.list_widget.hide() + self.arrow_label.setPixmap(self.arrow_pixmap) + self.is_expanded = False + + def on_focus_changed(self, old_widget, new_widget): + """焦点变化时关闭下拉框""" + if self.is_expanded: + is_focus_on_self = ( + new_widget == self + or new_widget == self.result_label + or new_widget == self.arrow_label + or (self.list_widget.isAncestorOf(new_widget) if new_widget else False) + ) + if not is_focus_on_self: + self.list_widget.hide() + self.arrow_label.setPixmap(self.arrow_pixmap) + self.is_expanded = False + + def eventFilter(self, obj, event): + """点击外部关闭下拉框""" + if obj == self.list_widget and event.type() == QEvent.MouseButtonPress: + self.list_widget.hide() + self.arrow_label.setPixmap( + self.arrow_pixmap.scaled( + 12, 9, Qt.KeepAspectRatio, Qt.SmoothTransformation + ) + ) + self.is_expanded = False + return True + return super().eventFilter(obj, event) + + def setFont(self, font): + """设置字体""" + self.result_label.setFont(font) + for i in range(self.list_widget.count()): + self.list_widget.item(i).setFont(font) + + # 获取当前选中的设备名 + def get_selected_device(self): + return self.result_label.text() + + +class SystemDiagnosticsDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setAttribute(Qt.WA_TranslucentBackground) + self.setWindowOpacity(0.0) + self._init_ui() + self.init_animations() + + def _init_ui(self): + # 无边框模式 + self.setWindowFlags(Qt.FramelessWindowHint) + + # 加载系统诊断弹窗的背景图片 + self.bg_pixmap = QPixmap(ImagePaths.SYSTEM_DIAGNOSTICS_POPUP_BG) + if self.bg_pixmap.isNull(): + print("错误: 系统诊断弹窗背景图加载失败!请检查路径是否正确") + else: + # 窗口尺寸与图片尺寸完全一致 + self.setFixedSize(self.bg_pixmap.size()) + + # 网格布局(8行4列小框) + grid_layout = QGridLayout(self) + grid_layout.setContentsMargins(24, 28, 20, 24) + + # 图片路径(替换为实际路径) + box_image_path = ImagePaths.SYSTEM_DIAGNOSTICS_BOX + circle_normal_path = ImagePaths.SYSTEM_DIAGNOSTICS_STATUS_GREEN # 正常状态 + circle_warning_path = ImagePaths.SYSTEM_DIAGNOSTICS_STATUS_YELLOW # 警告状态 + circle_error_path = ImagePaths.SYSTEM_DIAGNOSTICS_STATUS_RED # 异常状态 + ms_box_path = ImagePaths.SYSTEM_DIAGNOSTICS_MS_BG + dropdown_arrow_path = ImagePaths.SYSTEM_DIAGNOSTICS_DROPDOWN_ARROW + + # 字体设置 + ms_font = QFont() + ms_font.setPixelSize(14) + ms_color = QColor("#14abea") + + # 生成小框 + for row in range(8): + for col in range(4): + box_container = QWidget() + box_container.setObjectName(f"box_{row}_{col}") + box_container.setStyleSheet( + f""" + background-image: url("{box_image_path}"); + background-repeat: no-repeat; + """ + ) + box_layout = QHBoxLayout(box_container) + box_layout.setSpacing(0) + + # ========== 状态圆圈(支持状态切换) ========== + circle_label = QLabel() + circle_label.status = "normal" + circle_label.pixmaps = { + "normal": QPixmap(circle_normal_path), + "warning": QPixmap(circle_warning_path), + "error": QPixmap(circle_error_path), + } + circle_label.setPixmap(circle_label.pixmaps["normal"]) + circle_label.setStyleSheet("background: none;") + + # ========== 自定义下拉框(支持获取设备名) ========== + led_dropdown = CustomDropdown( + options=["LED1", "LED2", "LED3"], arrow_img_path=dropdown_arrow_path + ) + + # ========== 秒数输入框(获取毫秒值) ========== + ms_container = QWidget() + ms_layout = QHBoxLayout(ms_container) + ms_layout.setContentsMargins(6, 0, 0, 0) + ms_edit = QLineEdit("5ms") + ms_edit.setFont(ms_font) + ms_edit.setStyleSheet( + f""" + background: none; + color: {ms_color.name()}; + border: none; + outline: none; + background-color: transparent; + """ + ) + ms_container.setStyleSheet( + f""" + background-image: url("{ms_box_path}"); + background-repeat: no-repeat; + """ + ) + ms_layout.addWidget(ms_edit) + + # 保存组件引用 (动态增加) + box_container.circle = circle_label + box_container.dropdown = led_dropdown + box_container.ms_edit = ms_edit + + # 间距调整 + spacer1 = QSpacerItem(5, 1, QSizePolicy.Fixed, QSizePolicy.Minimum) + spacer2 = QSpacerItem(5, 1, QSizePolicy.Fixed, QSizePolicy.Minimum) + spacer3 = QSpacerItem(8, 1, QSizePolicy.Fixed, QSizePolicy.Minimum) + # box_layout.addItem(spacer1) + box_layout.addWidget(circle_label) + box_layout.addItem(spacer2) + box_layout.addWidget(led_dropdown) + # box_layout.addItem(spacer3) + box_layout.addWidget(ms_container) + + grid_layout.addWidget(box_container, row, col) + + def init_animations(self): + """初始化显示动画:从下方滑入 + 淡入""" + # 1. 透明度动画(从0→1,与系统中心一致但时长不同) + self.opacity_anim = QPropertyAnimation(self, b"windowOpacity") + self.opacity_anim.setDuration(400) + self.opacity_anim.setStartValue(0.0) + self.opacity_anim.setEndValue(1.0) + self.opacity_anim.setEasingCurve(QEasingCurve.OutCubic) # 缓动曲线不同 + + # 2. 位置动画(从下方100px滑入目标位置,核心差异点) + self.pos_anim = QPropertyAnimation(self, b"geometry") + self.pos_anim.setDuration(400) + self.pos_anim.setEasingCurve(QEasingCurve.OutQuart) # 滑入效果更自然 + + # 3. 组合动画(同时执行滑入和淡入) + self.anim_group = QParallelAnimationGroup(self) + self.anim_group.addAnimation(self.opacity_anim) + self.anim_group.addAnimation(self.pos_anim) + + def showEvent(self, event): + super().showEvent(event) # 先调用父类方法 + + # 动态计算动画起点(在当前位置下方100px,保持宽度和高度不变) + current_geometry = self.geometry() # 当前位置和尺寸(需提前用move设置) + # 起点:y坐标增加100px(从下方滑入),x和尺寸不变 + start_rect = QRect( + current_geometry.x(), + current_geometry.y() + 100, # 下方100px + current_geometry.width(), + current_geometry.height() + ) + # 设置动画起点和终点 + self.pos_anim.setStartValue(start_rect) + self.pos_anim.setEndValue(current_geometry) # 终点:目标位置 + + # 启动动画 + self.anim_group.start() + + def paintEvent(self, event): + """重写绘制事件,手动在透明背景上绘制图片""" + if not self.bg_pixmap.isNull(): + painter = QPainter(self) + # 绘制背景图(完全覆盖窗口,无间隙) + painter.drawPixmap(self.rect(), self.bg_pixmap) + # 必须调用父类方法,确保子控件正常绘制 + super().paintEvent(event) + + """ + 注意: row表示行号、col表示列号。都是从 0开始, 比如: 0行0列 + """ + + # ========== 对外接口:设置设备状态 ========== + def set_circle_status(self, row, col, status): + """设置指定行列的状态(绿-黄-红) (normal/warning/error)""" + box = self.findChild(QWidget, f"box_{row}_{col}") + if box and hasattr(box, "circle"): + box.circle.setPixmap(box.circle.pixmaps[status]) + box.circle.status = status + + # ========== 对外接口:获取选中的设备名 ========== + def get_selected_device(self, row, col): + """获取指定行列的选中设备名""" + box = self.findChild(QWidget, f"box_{row}_{col}") + if box and hasattr(box, "dropdown"): + return box.dropdown.get_selected_device() + return None + + # ========== 对外接口:获取毫秒值 ========== + def get_ms_value(self, row, col): + """获取指定行列的毫秒值(如“5ms”)""" + box = self.findChild(QWidget, f"box_{row}_{col}") + if box and hasattr(box, "ms_edit"): + # return box.ms_edit.text() + text = box.ms_edit.text().strip() + # 用正则提取数字(支持整数/小数,如"5"、"3.8"、"10.2ms") + import re + + number_match = re.search(r"(\d+(?:\.\d+)?)", text) + if number_match: + return number_match.group(1) + + return None + + +if __name__ == "__main__": + app = QApplication(sys.argv) + dialog = SystemDiagnosticsDialog() + dialog.show() + + # 1. 设置0行0列的状态为“警告”状态 + dialog.set_circle_status(0, 0, "warning") + + # 2. 获取1行2列的选中设备名 + device = dialog.get_selected_device(1, 2) + print(f"选中设备:{device}") + + # 3. 获取3行1列的毫秒值 + ms = dialog.get_ms_value(3, 1) + print(f"毫秒值:{ms}") + sys.exit(app.exec()) From 52d753267bafa5f8b606d61bb9e5600060d0318a Mon Sep 17 00:00:00 2001 From: yanganjie Date: Fri, 7 Nov 2025 21:58:20 +0800 Subject: [PATCH 15/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E8=AF=8A=E6=96=AD=E5=BC=B9=E7=AA=97=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=20=E7=AE=A1=E7=89=87=E4=BB=BB=E5=8A=A1=E7=9A=84=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E6=8C=89=E9=92=AE=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- images/关闭图标.png | Bin 0 -> 229 bytes images/管片任务信息栏.png | Bin 0 -> 369 bytes images/详情弹出背景.png | Bin 0 -> 19062 bytes images/详情标题.png | Bin 0 -> 1075 bytes utils/image_paths.py | 8 +- view/main_window.py | 30 ++- view/widgets/segment_details_dialog.py | 315 ++++++++++++++++++++++ view/widgets/system_diagnostics_dialog.py | 10 +- view/widgets/task_widget.py | 17 +- 9 files changed, 365 insertions(+), 15 deletions(-) create mode 100644 images/关闭图标.png create mode 100644 images/管片任务信息栏.png create mode 100644 images/详情弹出背景.png create mode 100644 images/详情标题.png create mode 100644 view/widgets/segment_details_dialog.py diff --git a/images/关闭图标.png b/images/关闭图标.png new file mode 100644 index 0000000000000000000000000000000000000000..9b090d67965bc9643f057743a444b62ec207dee1 GIT binary patch literal 229 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@RheHJ&bxAr`$;uWaOEGURD^sBq{Z z_s+SGe;WAh`n_rUofS>H{t3N3xYBpBh?R+_Jr}Rw1D-n#auc4&HE0)HI^24Gm%5>= z+|zeU8e<(j61r?o%hp~j^Gn%a@ACL`6Gxb9og%;SM5&ZS-y=;~5yF{{L2*XOzAoRr z-fR{8d1}4u664j)@o%=dZSk+-=l)8VEO%v@oj9)U!c1gJYD@<);T3K0RY{-S^oe4 literal 0 HcmV?d00001 diff --git a/images/管片任务信息栏.png b/images/管片任务信息栏.png new file mode 100644 index 0000000000000000000000000000000000000000..290f6ab9053d406bd3e7b3bd3fdf9f9f21ac0cd1 GIT binary patch literal 369 zcmeAS@N?(olHy`uVBq!ia0y~yV7vfiJ8`f9$>%NYe}DoOo-U3d6?5KRHRL_SAmVy4 zzKKOAi9yk{nOBGI)bII5Zeon79}k8<-pvhAsE>n%nOz7%URg>V9!@<*%Ca=>RY&89ZJ6T-G@y GGywnulvREJ literal 0 HcmV?d00001 diff --git a/images/详情弹出背景.png b/images/详情弹出背景.png new file mode 100644 index 0000000000000000000000000000000000000000..7c003c296102c6b712a95145c0c41a34fd28cfd3 GIT binary patch literal 19062 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 literal 0 HcmV?d00001 diff --git a/utils/image_paths.py b/utils/image_paths.py index ffdda75..a62027b 100644 --- a/utils/image_paths.py +++ b/utils/image_paths.py @@ -101,4 +101,10 @@ class ImagePaths: SYSTEM_DIAGNOSTICS_STATUS_YELLOW = "images/系统诊断状态黄.png" SYSTEM_DIAGNOSTICS_STATUS_RED = "images/系统诊断状态红.png" SYSTEM_DIAGNOSTICS_MS_BG = "images/系统诊断毫秒背景.png" - SYSTEM_DIAGNOSTICS_DROPDOWN_ARROW = "images/系统诊断下拉箭头.png" \ No newline at end of file + SYSTEM_DIAGNOSTICS_DROPDOWN_ARROW = "images/系统诊断下拉箭头.png" + + # 功能:管片任务详情按钮弹窗 + SEGMENT_DETAILS_POPUP_BG = "images/详情弹出背景.png" + SEGMENT_DETAILS_TITLE_BG = "images/详情标题.png" + SEGMENT_DETAILS_INFO_BAR = "images/管片任务信息栏.png" + SEGMENT_DETAILS_CLOSE_ICON = "images/关闭图标.png" \ No newline at end of file diff --git a/view/main_window.py b/view/main_window.py index cb91984..e2a500a 100644 --- a/view/main_window.py +++ b/view/main_window.py @@ -21,6 +21,8 @@ from .widgets.bottom_control_widget import BottomControlWidget import resources.resources_rc from utils.image_paths import ImagePaths +from .widgets.segment_details_dialog import SegmentDetailsDialog + class MainWindow(QWidget): def __init__(self): @@ -34,21 +36,27 @@ class MainWindow(QWidget): # 安装事件过滤器,处理计划方量的 QLineEdit的失去焦点事件 self.installEventFilter(self) + # 连接槽函数 def connectSignalToSlot(self): # 可添加信号槽连接 # self.system_button_widget.buttons["系统启动"].clicked.connect(self.handleSystemStart) # self.system_button_widget.buttons["系统停止"].clicked.connect(self.handleSystemStop) - pass - self.conveyor_system_widget.left_btn.clicked.connect(self.handleHopperMoveLeft) - self.conveyor_system_widget.right_btn.clicked.connect(self.handleHopperMoveRight) + + # 传送带部分的按钮 + self.conveyor_system_widget.left_btn.clicked.connect(self.handleHopperMoveLeft) # 传送带下的左移按钮 + self.conveyor_system_widget.right_btn.clicked.connect(self.handleHopperMoveRight) # 传送带下的右移按钮 + + # 管片任务详情 + self.segment_task_widget.task_details_signal.connect(self.handleSegmentTaskDetails) # 管片任务详情按钮 + def handleSystemStart(self): - # 测试 + # 测试系统开启,进度条动画 self.production_progress.testProgress(60) self.arc_progress.testProgress(60) def handleSystemStop(self): - # 测试 + # 测试系统停止,进度条动画 self.production_progress.animation.stop() self.arc_progress.animation.stop() @@ -101,6 +109,7 @@ class MainWindow(QWidget): self.dispatch_task_widget.set_task_id("task2", "PD0002") self.dispatch_task_widget.set_task_id("task3", "PD0003") + # 读取数据库,初始化 管片任务的数据 from busisness.blls import ArtifactBll, PDRecordBll artifact_dal = ArtifactBll() artifacts = artifact_dal.get_artifact_task() @@ -221,6 +230,17 @@ class MainWindow(QWidget): # 料斗右移完成,恢复料斗左移按钮 QTimer.singleShot(2100, lambda: self.conveyor_system_widget.left_btn.setEnabled(True)) + def handleSegmentTaskDetails(self, segment_task_name:str): + # 管片任务名 task1、task2、task3 (分别对应第一条管片任务、 第二条管片任务...) + print("main_window: handleSegmentTaskDetails", segment_task_name) + + # 显示管片任务详情对话框 + segment_details_dialog = SegmentDetailsDialog(self) + # 这里可以设置对话框显示的内容 如 set_segment_id + # segment_details_dialog.set_segment_id("9999999999") + segment_details_dialog.show() + + # 更新 派单任务widget的坐标 def update_dispatch_task_position(self): # 方法1:获取模具车控件左上角坐标(相对于父控件) diff --git a/view/widgets/segment_details_dialog.py b/view/widgets/segment_details_dialog.py new file mode 100644 index 0000000..1a31784 --- /dev/null +++ b/view/widgets/segment_details_dialog.py @@ -0,0 +1,315 @@ +from PySide6.QtWidgets import ( + QApplication, + QDialog, + QVBoxLayout, + QHBoxLayout, + QGridLayout, + QLabel, + QWidget, + QPushButton +) +from PySide6.QtGui import QPixmap, QFont, QPainter, QIcon +from PySide6.QtCore import Qt +import sys + +from utils.image_paths import ImagePaths + +""" + 管片任务详情的弹出窗口: 点击管片任务的详情按钮之后弹出 +""" + +class SegmentDetailsDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setAttribute(Qt.WA_TranslucentBackground) + + # 初始化存储需要修改的控件 + self.id_value_label = None # 管片ID值标签 + self.left_cells = [] # 左列单元格列表(每个元素是包含label和value的widget) + self.right_cells = [] # 右列单元格列表 + + self._init_ui() + + def _init_ui(self): + # 基础设置:无边框+窗口尺寸由背景图决定 + self.setWindowFlags(Qt.FramelessWindowHint) + self._load_background() + + # 主布局: + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(32, 20, 32, 50) + main_layout.setSpacing(0) + + # 1. 顶部区域(标题 + 关闭按钮) + self._add_top_area(main_layout) + + # 2. 管片ID区域(保存ID值标签引用) + self._add_segment_id_area(main_layout) + + # 3. 网格信息区域(保存左右列单元格引用) + self._add_grid_info_area(main_layout) + + def _load_background(self): + self.bg_pixmap = QPixmap(ImagePaths.SEGMENT_DETAILS_POPUP_BG) + if self.bg_pixmap.isNull(): + print("错误:详情弹出背景.png 加载失败,请检查路径!") + self.setFixedSize(800, 600) + else: + self.setFixedSize(self.bg_pixmap.size()) + + def _add_top_area(self, parent_layout): + """创建包含标题和关闭按钮的顶部水平布局""" + top_layout = QHBoxLayout() + top_layout.setContentsMargins(0, 0, 0, 36) # 保持原标题下方36px间距 + top_layout.setSpacing(0) + + # 左侧弹簧(让标题居中) + top_layout.addStretch() + + # 标题标签(复用原标题逻辑) + title_label = QLabel("管片任务") + font = QFont() + font.setPixelSize(24) + font.setLetterSpacing(QFont.AbsoluteSpacing, 2) + font.setBold(True) + title_label.setFont(font) + title_label.setStyleSheet("color: #13fffc; font-weight: Bold;") + title_label.setAlignment(Qt.AlignCenter) + top_layout.addWidget(title_label) + + # 右侧:关闭按钮 + self._create_close_button(top_layout) + + parent_layout.addLayout(top_layout) + + # 新增:创建关闭按钮 + def _create_close_button(self, parent_layout): + """创建36x36关闭按钮""" + self.close_btn = QPushButton() + self.close_btn.setFixedSize(36, 36) # 固定尺寸18x18 + + # 加载关闭图标 + close_icon = QPixmap(ImagePaths.SEGMENT_DETAILS_CLOSE_ICON) + if not close_icon.isNull(): + # 设置图标并自适应按钮大小 + self.close_btn.setIcon(QIcon(close_icon)) + + # 样式设置:默认透明背景,悬停红色背景 + self.close_btn.setStyleSheet(""" + QPushButton { + background-color: transparent; + border: none; + padding: 0px; + } + QPushButton:hover { + background-color: red; + border-radius: 2px; + } + """) + + # 点击事件:关闭窗口 + self.close_btn.clicked.connect(self.close) + + # 添加到布局(与标题保持间距) + parent_layout.addStretch() # 右侧弹簧,确保按钮靠右 + parent_layout.addWidget(self.close_btn) + + + def _add_segment_id_area(self, parent_layout): + id_layout = QHBoxLayout() + + # 左侧:管片ID标签 + id_label = QLabel("管片ID") + id_label.setFixedSize(318, 32) + id_font = QFont() + id_font.setPixelSize(18) + id_font.setLetterSpacing(QFont.AbsoluteSpacing, 2) + id_font.setBold(True) + id_label.setFont(id_font) + id_label.setStyleSheet(f""" + background-image: url({ImagePaths.SEGMENT_DETAILS_TITLE_BG}); + background-repeat: no-repeat; + background-position: center; + color: #13ffff; + """) + id_label.setContentsMargins(16, 0, 0, 0) + id_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + + # 右侧:管片ID值(保存引用到实例变量) + self.id_value_label = QLabel("346482967298119") + value_font = QFont() + value_font.setPixelSize(18) + value_font.setBold(True) + value_font.setLetterSpacing(QFont.AbsoluteSpacing, 2) + self.id_value_label.setFont(value_font) + self.id_value_label.setStyleSheet("color: #13ffff;") + self.id_value_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + + id_layout.addWidget(id_label) + id_layout.addStretch() + id_layout.addWidget(self.id_value_label) + id_layout.setContentsMargins(0, 0, 0, 16) + parent_layout.addLayout(id_layout) + + def _add_grid_info_area(self, parent_layout): + grid_layout = QGridLayout() + grid_layout.setSpacing(12) + + # 初始化显示的数据 + # 左侧信息条目 + left_info_items = [ + ("管片编号", "QR1B32000153AD"), + ("管片副标识", "QR1B32000153AD"), + ("生产环号", "QR1B32000153AD"), + ("模具编号", "QR1B32000153AD"), + ("骨架编号", "QR1B32000153AD"), + ("环类型编号", "QR1B32000153AD"), + ("尺寸规格", "QR1B32000153AD"), + ] + + # 右侧信息条目 + right_info_items = [ + ("分块号", "QR3143243423543254"), + ("出洞环标记", "QR3143243423543254"), + ("注浆管标记", "QR3143243423543254"), + ("聚丙烯纤维标记", "QR3143243423543254"), + ("浇筑方量", "QR3143243423543254"), + ("任务单号", "QR3143243423543254"), + ("埋深", "QR3143243423543254"), + ] + + # 填充左列并保存单元格引用 + self.left_cells.clear() # 清空列表 + for row, (label_text, value_text) in enumerate(left_info_items): + cell_widget = self._create_info_cell(label_text, value_text) + self.left_cells.append(cell_widget) # 保存到列表 + grid_layout.addWidget(cell_widget, row, 0) + + # 填充右列并保存单元格引用 + self.right_cells.clear() # 清空列表 + for row, (label_text, value_text) in enumerate(right_info_items): + cell_widget = self._create_info_cell(label_text, value_text) + self.right_cells.append(cell_widget) # 保存到列表 + grid_layout.addWidget(cell_widget, row, 1) + + parent_layout.addLayout(grid_layout) + + def _create_info_cell(self, label_text, value_text): + cell_widget = QWidget() + cell_bg = QPixmap(ImagePaths.SEGMENT_DETAILS_INFO_BAR) + if not cell_bg.isNull(): + cell_widget.setFixedSize(cell_bg.size()) + cell_widget.setStyleSheet(f""" + background-image: url({ImagePaths.SEGMENT_DETAILS_INFO_BAR}); + background-repeat: no-repeat; + background-position: Center; + """) + else: + print("警告:管片任务信息栏.png 加载失败,使用默认背景!") + cell_widget.setStyleSheet("background-color: #0a2463;") + + cell_layout = QHBoxLayout(cell_widget) + cell_layout.setContentsMargins(2, 0, 0, 0) + + # 左侧标签(保存到cell_widget的属性中) + label = QLabel(label_text) + label.setFixedSize(136, 60) + label_font = QFont() + label_font.setPixelSize(16) + label_font.setLetterSpacing(QFont.AbsoluteSpacing, 2) + label.setFont(label_font) + label.setStyleSheet("background: none; background-color: #1369b4; color: #fffffd; font-weight:Bold;") + label.setAlignment(Qt.AlignCenter) + cell_widget.label = label + + # 右侧值(保存到cell_widget的属性中) + value = QLabel(value_text) + value_font = QFont() + value_font.setPixelSize(18) + value.setFont(value_font) + value.setStyleSheet("background: none; color: #9fbfd4;") + value.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) + cell_widget.value = value + + cell_layout.addWidget(label) + cell_layout.addSpacing(60) + cell_layout.addWidget(value) + + return cell_widget + + def paintEvent(self, event): + if not self.bg_pixmap.isNull(): + painter = QPainter(self) + painter.drawPixmap(self.rect(), self.bg_pixmap) + super().paintEvent(event) + + # ------------------- 对外修改接口 ------------------- + # --------------修改管片任务详情中显示的值 ------------ + def set_segment_id(self, new_id): + """修改管片ID的值""" + if self.id_value_label: + self.id_value_label.setText(str(new_id)) + + def set_left_label(self, row, new_label_text:str): + """ + 修改左列网格的标签文本 ("生产环号") + Args: + row: 左列网格行号(0-6,共7行) + new_label_text: 新的标签文字(如“管片编号”) + """ + if 0 <= row < len(self.left_cells): + cell = self.left_cells[row] + cell.label.setText(new_label_text) + + def set_left_value(self, row, new_value_text:str): + """ + 修改左列网格的值 + Args: + row: 左列网格行号(0-6,共7行) + new_value_text: 新的值(如“FB789”) + """ + if 0 <= row < len(self.left_cells): + cell = self.left_cells[row] + cell.value.setText(new_value_text) + + def set_right_label(self, row, new_label_text:str): + """ + 修改右列网格的标签文本 ("任务单号") + Args: + row: 右列网格行号(0-6,共7行) + new_label_text: 新的标签文字(如“分块号”) + """ + if 0 <= row < len(self.right_cells): + cell = self.right_cells[row] + cell.label.setText(new_label_text) + + def set_right_value(self, row, new_value_text:str): + """ + 修改右列网格的值 + Args: + row: 右列网格行号(0-6,共7行) + new_value_text: 新的值(如“FB789”) + """ + if 0 <= row < len(self.left_cells): + cell = self.right_cells[row] + cell.value.setText(new_value_text) + + +# 测试代码 +if __name__ == "__main__": + app = QApplication(sys.argv) + dialog = SegmentDetailsDialog() + dialog.show() + + # 测试修改接口 + dialog.set_segment_id("999999999999999") # 修改管片ID值 + + # 左列修改 + dialog.set_left_label(0, "新管片编号") # 修改左列第0行的标签文本 + dialog.set_left_value(0, "QR6666666666666") # 修改左列第0行的值 + + # 右列修改 + dialog.set_right_label(0, "新分块号") # 修改右列第0行的标签文本 + dialog.set_right_value(0, "QR99999999999999999") # 修改右列第0行的值 + + sys.exit(app.exec()) \ No newline at end of file diff --git a/view/widgets/system_diagnostics_dialog.py b/view/widgets/system_diagnostics_dialog.py index 8c89c48..7295384 100644 --- a/view/widgets/system_diagnostics_dialog.py +++ b/view/widgets/system_diagnostics_dialog.py @@ -27,6 +27,10 @@ import sys from utils.image_paths import ImagePaths +""" + 系统诊断按钮的弹窗: 可以显示设备的状态 +""" + class CustomDropdown(QWidget): """自定义下拉框组件""" @@ -107,11 +111,7 @@ class CustomDropdown(QWidget): if self.is_expanded: self.list_widget.hide() # 箭头恢复向下 - self.arrow_label.setPixmap( - self.arrow_pixmap.scaled( - 12, 9, Qt.KeepAspectRatio, Qt.SmoothTransformation - ) - ) + self.arrow_label.setPixmap(self.arrow_pixmap) else: # 计算下拉框位置(在标签下方对齐) label_pos = self.result_label.mapToGlobal( diff --git a/view/widgets/task_widget.py b/view/widgets/task_widget.py index f3ca49e..e9f7556 100644 --- a/view/widgets/task_widget.py +++ b/view/widgets/task_widget.py @@ -1,14 +1,19 @@ from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QMessageBox, QApplication) -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QPainter, QPixmap, QFont import sys import resources.resources_rc from utils.image_paths import ImagePaths -# 任务控件,如:管片任务、派单任务 +""" + 任务控件,如:管片任务、派单任务 +""" class TaskWidget(QWidget): + # 任务详情信号: task1表示第一条任务 + task_details_signal = Signal(str) + def __init__(self, taskTitle:str, parent=None): super().__init__(parent) # 设置Widget大小与背景图一致 @@ -153,8 +158,12 @@ class TaskWidget(QWidget): def _show_detail_dialog(self, task_name): """显示任务详情弹窗""" - QMessageBox.information(self, "任务详情", f"任务 {task_name} 的详细信息...") - + # QMessageBox.information(self, "任务详情", f"任务 {task_name} 的详细信息...") + """ + task1 表示第一条任务, 依次类推 + """ + # 发送任务详情信号 + self.task_details_signal.emit(task_name) # -------------------------- # 对外接口:修改任务属性 From aa7dd7974a3b877c3762c704363f3fbd46dc48b0 Mon Sep 17 00:00:00 2001 From: yanganjie Date: Sat, 8 Nov 2025 18:25:16 +0800 Subject: [PATCH 16/16] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=20=E6=B4=BE?= =?UTF-8?q?=E5=8D=95=E4=BB=BB=E5=8A=A1=E8=AF=A6=E6=83=85=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- images/派单任务信息栏1.png | Bin 0 -> 510 bytes images/派单任务信息栏2.png | Bin 0 -> 509 bytes utils/image_paths.py | 9 +- view/main_window.py | 29 ++ view/widgets/dispatch_details_dialog.py | 391 ++++++++++++++++++++++++ view/widgets/task_widget.py | 27 ++ view/widgets/value_adjuster.py | 43 ++- 7 files changed, 489 insertions(+), 10 deletions(-) create mode 100644 images/派单任务信息栏1.png create mode 100644 images/派单任务信息栏2.png create mode 100644 view/widgets/dispatch_details_dialog.py diff --git a/images/派单任务信息栏1.png b/images/派单任务信息栏1.png new file mode 100644 index 0000000000000000000000000000000000000000..69915346e682b4c08f7f63bbb222998c2f153874 GIT binary patch literal 510 zcmeAS@N?(olHy`uVBq!ia0y~yU|tPmJ8`f9$&HQ%OUiYI2wc18(M+f{WR67cSKE3b-JA()x_XjW_mtQ@1^q0czxktY2>PZnf`@FO~1-u`n?F zx&PYIJRfM}0=G3x3=9*L!x$MDP6&c*deRXJB#Vw{0ZGdwQ3i$vnT4!-N|cu9C)|Srhs>^7&-fDFe9-p00i_>zopr0Ju|qSO5S3 literal 0 HcmV?d00001 diff --git a/utils/image_paths.py b/utils/image_paths.py index a62027b..32fc454 100644 --- a/utils/image_paths.py +++ b/utils/image_paths.py @@ -107,4 +107,11 @@ class ImagePaths: SEGMENT_DETAILS_POPUP_BG = "images/详情弹出背景.png" SEGMENT_DETAILS_TITLE_BG = "images/详情标题.png" SEGMENT_DETAILS_INFO_BAR = "images/管片任务信息栏.png" - SEGMENT_DETAILS_CLOSE_ICON = "images/关闭图标.png" \ No newline at end of file + SEGMENT_DETAILS_CLOSE_ICON = "images/关闭图标.png" + + # 功能: 派单任务详情按钮弹窗 + DESPATCH_DETAILS_POPUP_BG = "images/详情弹出背景.png" + DESPATCH_DETAILS_TITLE_BG = "images/详情标题.png" + DESPATCH_DETAILS_INFO_BAR_NORMAL = "images/派单任务信息栏1.png" + DESPATCH_DETAILS_INFO_BAR_HOVER = "images/派单任务信息栏2.png" + DESPATCH_DETAILS_CLOSE_ICON = "images/关闭图标.png" \ No newline at end of file diff --git a/view/main_window.py b/view/main_window.py index e2a500a..2e6934b 100644 --- a/view/main_window.py +++ b/view/main_window.py @@ -22,6 +22,7 @@ import resources.resources_rc from utils.image_paths import ImagePaths from .widgets.segment_details_dialog import SegmentDetailsDialog +from .widgets.dispatch_details_dialog import DispatchDetailsDialog class MainWindow(QWidget): @@ -49,6 +50,9 @@ class MainWindow(QWidget): # 管片任务详情 self.segment_task_widget.task_details_signal.connect(self.handleSegmentTaskDetails) # 管片任务详情按钮 + # 派单任务详情 + self.dispatch_task_widget.task_details_signal.connect(self.handleDispatchTaskDetails) # 派单任务详情按钮 + def handleSystemStart(self): # 测试系统开启,进度条动画 @@ -239,6 +243,31 @@ class MainWindow(QWidget): # 这里可以设置对话框显示的内容 如 set_segment_id # segment_details_dialog.set_segment_id("9999999999") segment_details_dialog.show() + + def handleDispatchTaskDetails(self, dispatch_task_name:str): + # 派单任务名 task1、task2、task3 (分别对应第一条派单任务、 第二条派单任务...) + print("main_window: handleDispatchTaskDetails", dispatch_task_name) + + # 显示派单任务详情对话框 + dispatch_details_dialog = DispatchDetailsDialog(dispatch_task_name, self) + + # 这里可以设置对话框显示的内容 如 set_segment_id + # dispatch_details_dialog.set_segment_id("9999999999") + # 设置派单任务详情中的方量的值 + current_volume = self.dispatch_task_widget.get_task_volume(dispatch_task_name) + dispatch_details_dialog.set_row_value(4, str(current_volume)) # 派单方量的值的行号为4,第五行 + + # 派单任务详情页面中确定修改了派单任务的方量 + # 备注:褚工说管片任务和派单任务中的方量都只有一位小数,料斗上的方量显示两位 2025/11/8 + dispatch_details_dialog.confirm_modify_volume.connect(self.handleModifyDispatchTaskVolume) + dispatch_details_dialog.show() + + def handleModifyDispatchTaskVolume(self, dispatch_task_name:str, modifyed_volume:float): + """派单任务详情页面中, 修改了派单任务的方量""" + # 修改相应的派单任务条目显示的 派单任务方量 + self.dispatch_task_widget.set_task_volume(dispatch_task_name, modifyed_volume) + + # 其他操作,可能需要修改数据库的派单任务方量 # 更新 派单任务widget的坐标 diff --git a/view/widgets/dispatch_details_dialog.py b/view/widgets/dispatch_details_dialog.py new file mode 100644 index 0000000..7a535a4 --- /dev/null +++ b/view/widgets/dispatch_details_dialog.py @@ -0,0 +1,391 @@ +from PySide6.QtWidgets import ( + QApplication, + QDialog, + QVBoxLayout, + QHBoxLayout, + QGridLayout, + QLabel, + QWidget, + QPushButton, +) +from PySide6.QtGui import QPixmap, QFont, QPainter, QIcon +from PySide6.QtCore import Qt, QEvent, Signal +import sys + +from utils.image_paths import ImagePaths + +from view.widgets.value_adjuster import ValueAdjuster + +""" + 派单任务的详情按钮点击之后弹出, 显示派单任务的详情 +""" + +class DispatchDetailsDialog(QDialog): + # 确认修改了派单任务的方量,发送任务名(task1、task2等)和最终确认修改的方量值 + confirm_modify_volume = Signal(str, float) + + def __init__(self, dispatch_task_name:str, parent=None): + super().__init__(parent) + self.setAttribute(Qt.WA_TranslucentBackground) + + # 派单任务名 (task1、task2、 task3) + self.dispatch_task_name = dispatch_task_name + + # 初始化存储需要修改的控件 + self.id_value_label = None # 对应管片ID值标签 + self.rows = [] # 所有行的单元格列表(包含label、value) + + # 派单方量调整控件,用于修改派单方量 + self.volume_value_adjuster = None + + self._init_ui() + + def _init_ui(self): + self.setWindowFlags(Qt.FramelessWindowHint) + self._load_background() + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(32, 20, 32, 50) + main_layout.setSpacing(0) + + # 1. 顶部区域(标题 + 关闭按钮) + self._add_top_area(main_layout) + + # 2. 对应管片ID区域 + self._add_segment_id_area(main_layout) + + # 3. 网格信息区域(单列7行) + self._add_grid_info_area(main_layout) + + # 4. 修改方量按钮 + self.modify_btn = QPushButton("修改方量", parent=self) + self.modify_btn.setFixedSize(89, 32) + self.modify_btn.setStyleSheet( + """ + QPushButton { + background-color: #001c82; + color: #9fbfd4; + border: 1px solid #017cbc; + font-size: 18px; + font-weight: Bold; + } + QPushButton:hover { + color: #2dcedb; + } + """ + ) + # modify_btn.move(860, 446) # 移动到第五行,派单方量的位置 + self.modify_btn.move(860, 442) # 移动到第五行,派单方量的位置 + self.modify_btn.clicked.connect(self.onModifyVolume) + + # 确认修改方量按钮,表示 派单方量的修改已经确定 + self.confirm_btn = QPushButton("确定", parent=self) + self.confirm_btn.setStyleSheet( + """ + QPushButton { + background-color: #001c82; + color: #9fbfd4; + border: 1px solid #017cbc; + font-size: 18px; + font-weight: Bold; + } + QPushButton:hover { + color: #2dcedb; + } + """ + ) + self.confirm_btn.move(860, 442) + self.confirm_btn.hide() # 初始隐藏 + self.confirm_btn.setFixedSize(42, 32) + self.confirm_btn.clicked.connect(self.onConfirmModifyVolume) + + # 取消修改方量按钮,表示 派单方量的修改已经取消 + self.cancel_btn = QPushButton("取消", parent=self) + self.cancel_btn.setStyleSheet( + """ + QPushButton { + background-color: #001c82; + color: #9fbfd4; + border: 1px solid #017cbc; + font-size: 18px; + font-weight: Bold; + } + QPushButton:hover { + color: #2dcedb; + } + """ + ) + self.cancel_btn.hide() + self.cancel_btn.setFixedSize(42, 32) + self.cancel_btn.move(907, 442) + self.cancel_btn.clicked.connect(self.onCancelModifyVolume) + + def _load_background(self): + self.bg_pixmap = QPixmap(ImagePaths.DESPATCH_DETAILS_POPUP_BG) + if self.bg_pixmap.isNull(): + print("错误:派单任务背景.png 加载失败,请检查路径!") + self.setFixedSize(800, 600) + else: + self.setFixedSize(self.bg_pixmap.size()) + + def _add_top_area(self, parent_layout): + top_layout = QHBoxLayout() + top_layout.setContentsMargins(0, 0, 0, 36) + + top_layout.addStretch() + + # 标题改为“任务派单” + title_label = QLabel("派单任务") + font = QFont() + font.setPixelSize(24) + font.setLetterSpacing(QFont.AbsoluteSpacing, 2) + font.setBold(True) + title_label.setFont(font) + title_label.setStyleSheet("color: #13fffc; font-weight: Bold;") + title_label.setAlignment(Qt.AlignCenter) + top_layout.addWidget(title_label) + + # 关闭按钮(保持原逻辑) + self._create_close_button(top_layout) + + parent_layout.addLayout(top_layout) + + def _create_close_button(self, parent_layout): + self.close_btn = QPushButton() + self.close_btn.setFixedSize(36, 36) + + close_icon = QPixmap(ImagePaths.DESPATCH_DETAILS_CLOSE_ICON) + if not close_icon.isNull(): + self.close_btn.setIcon(QIcon(close_icon)) + + self.close_btn.setStyleSheet( + """ + QPushButton { + background-color: transparent; + border: none; + padding: 0px; + } + QPushButton:hover { + background-color: red; + border-radius: 2px; + } + """ + ) + self.close_btn.clicked.connect(self.close) + + parent_layout.addStretch() + parent_layout.addWidget(self.close_btn) + + def _add_segment_id_area(self, parent_layout): + id_layout = QHBoxLayout() + + id_label = QLabel("对应管片ID") # 标签文字修改 + id_label.setFixedSize(318, 32) + id_font = QFont() + id_font.setPixelSize(18) + id_font.setLetterSpacing(QFont.AbsoluteSpacing, 2) + id_font.setBold(True) + id_label.setFont(id_font) + id_label.setStyleSheet( + f""" + background-image: url({ImagePaths.DESPATCH_DETAILS_TITLE_BG}); + background-repeat: no-repeat; + background-position: center; + color: #13ffff; + """ + ) + id_label.setContentsMargins(16, 0, 0, 0) + id_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + + self.id_value_label = QLabel("222232454352452") # 初始管片ID值 + value_font = QFont() + value_font.setPixelSize(18) + value_font.setBold(True) + value_font.setLetterSpacing(QFont.AbsoluteSpacing, 2) + self.id_value_label.setFont(value_font) + self.id_value_label.setStyleSheet("color: #13ffff;") + self.id_value_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + + id_layout.addWidget(id_label) + id_layout.addStretch() + id_layout.addWidget(self.id_value_label) + id_layout.setContentsMargins(0, 0, 0, 16) + parent_layout.addLayout(id_layout) + + def _add_grid_info_area(self, parent_layout): + grid_layout = QGridLayout() + grid_layout.setSpacing(12) + + # 初始化信息条目(7行) + info_items = [ + ("创建时间", "2025年10月10日 10:10:10"), + ("派单时间", "2025年10月10日 10:10:10"), + ("任务编号", "20251010-10"), + ("配比编号", "20251010-10"), + ("派单方量", "2.0"), + ("派单状态", "未下发"), + ("派单类型", "自动派单"), + ] + + self.rows.clear() + for row, (label_text, value_text) in enumerate(info_items): + cell_widget = self._create_info_cell(label_text, value_text) + self.rows.append(cell_widget) + grid_layout.addWidget(cell_widget, row, 0) + + parent_layout.addLayout(grid_layout) + + def _create_info_cell(self, label_text, value_text): + cell_widget = QWidget() + cell_bg = QPixmap(ImagePaths.DESPATCH_DETAILS_INFO_BAR_NORMAL) # 正常背景图 + cell_widget.setObjectName("infoCell") + if not cell_bg.isNull(): + cell_widget.setFixedSize(cell_bg.size()) + cell_widget.setStyleSheet( + f""" + QWidget {{ + background-image: url({ImagePaths.DESPATCH_DETAILS_INFO_BAR_NORMAL}); + background-repeat: no-repeat; + background-position: Center; + }} + QWidget:hover {{ + background-image: url({ImagePaths.DESPATCH_DETAILS_INFO_BAR_HOVER}); + }} + QWidget QLabel#valueLabel {{ + color: #9fbfd4; + background: none; + }} + """ + ) + + cell_layout = QHBoxLayout(cell_widget) + cell_layout.setContentsMargins(0, 0, 0, 0) + + # 左侧标签 + label = QLabel(label_text) + label.setFixedSize(136, 60) + label_font = QFont() + label_font.setPixelSize(16) + label_font.setLetterSpacing(QFont.AbsoluteSpacing, 2) + label.setFont(label_font) + label.setStyleSheet("background: none;color: #fffffd; font-weight:Bold;") + label.setAlignment(Qt.AlignCenter) + cell_widget.label = label + + # 右侧值标签(设置objectName以便样式选择) + value = QLabel(value_text) + value.setObjectName("valueLabel") + value_font = QFont() + value_font.setPixelSize(20) + value.setFont(value_font) + value.setAlignment(Qt.AlignCenter) + cell_widget.value = value + + cell_layout.addWidget(label) # 左侧的标题标签 + cell_layout.addSpacing(60) + cell_layout.addWidget(value) # 右侧的值标签 + + cell_widget.installEventFilter(self) + return cell_widget + + # 实现事件过滤器,动态修改右侧值颜色 + def eventFilter(self, obj, event): + # 只处理父控件(infoCell)的事件 + if obj.objectName() == "infoCell": + # 鼠标进入父控件 → 改#13f0f3 + if event.type() == QEvent.Enter: + if hasattr(obj, "value"): # 确保存在value控件 + obj.value.setStyleSheet("background: none; color: #13f0f3;") + # 鼠标离开父控件 → 恢复默认色 + elif event.type() == QEvent.Leave: + if hasattr(obj, "value"): + obj.value.setStyleSheet("background: none; color: #9fbfd4;") + return super().eventFilter(obj, event) + + def onModifyVolume(self): + """修改派单方量的逻辑""" + volume_label = self.rows[4].value + current_value = float(volume_label.text()) + + # 1、调整派单方量,创建派单方量调整控件 + if not self.volume_value_adjuster: + self.volume_value_adjuster = ValueAdjuster(self) + self.volume_value_adjuster.move(551, 442) # 移动到当前显示派单方量的标签处 + + # 2、更新派单方量调整控件的值, 并显示 + self.volume_value_adjuster.set_value(current_value) + self.volume_value_adjuster.show() + + # 3、显示确定按钮、显示取消按钮、隐藏修改方量按钮 + self.confirm_btn.show() + self.cancel_btn.show() + self.modify_btn.hide() + + def onConfirmModifyVolume(self): + """确定 修改派单方量""" + # 显示相关的: + # 1、隐藏确认按钮、隐藏取消按钮、显示修改方量按钮 + self.confirm_btn.hide() + self.cancel_btn.hide() + self.modify_btn.show() + + # 2、修改 派单方量标签的值 + volume_label = self.rows[4].value + # modifyed_value 为float类型, 一位小数 + modifyed_value = self.volume_value_adjuster.get_value() + volume_label.setText(str(modifyed_value)) + + # 3、发送派单方量确定修改的信号 (发送派单任务名 + 确认修改之后的派单方量) + self.confirm_modify_volume.emit(self.dispatch_task_name, modifyed_value) + + # 4、关闭派单方量调整控件 + self.volume_value_adjuster.close() + + def onCancelModifyVolume(self): + # 显示相关的: + # 1、隐藏确认按钮、隐藏取消按钮、显示修改方量按钮 + self.confirm_btn.hide() + self.cancel_btn.hide() + self.modify_btn.show() + + # 2、关闭派单方量调整控件 + self.volume_value_adjuster.close() + + def paintEvent(self, event): + if not self.bg_pixmap.isNull(): + painter = QPainter(self) + painter.drawPixmap(self.rect(), self.bg_pixmap) + super().paintEvent(event) + + # ------------------- 对外修改接口 ------------------- + # row 对应行号(0-6),从0开始 + # -------------------------------------------------- + def set_segment_id(self, new_id): + """修改上方的 对应的管片ID的值""" + if self.id_value_label: + self.id_value_label.setText(str(new_id)) + + def set_row_label(self, row, new_label_text: str): + """修改左侧的显示的标签的文本,如: 创建时间、派单时间等""" + if 0 <= row < len(self.rows): + self.rows[row].label.setText(new_label_text) + + def set_row_value(self, row, new_value_text: str): + """修改右侧的显示的值, 如: 2025年9月9日 9:9:9""" + if 0 <= row < len(self.rows): + self.rows[row].value.setText(new_value_text) + + +# 测试代码 +if __name__ == "__main__": + app = QApplication(sys.argv) + dialog = DispatchDetailsDialog() + + # 测试修改接口 + dialog.set_segment_id("999999999999999") + dialog.set_row_label(0, "新创建时间") + dialog.set_row_value(0, "2025年09月09日 09:09:09") + dialog.set_row_value(4, "3.0") # 初始派单方量修改 + + dialog.show() + sys.exit(app.exec()) diff --git a/view/widgets/task_widget.py b/view/widgets/task_widget.py index e9f7556..d0ba1d0 100644 --- a/view/widgets/task_widget.py +++ b/view/widgets/task_widget.py @@ -187,6 +187,33 @@ class TaskWidget(QWidget): task_id_label = self.task_controls[task_name]["task_id_label"] task_id_label.setText(new_id) + def get_task_volume(self, task_name:str): + """ + 获取指定任务的方量, 传入任务名,如 task1、task2、task3 + return: 返回 float类型一位小数的方量值 + """ + if task_name in self.task_controls: + volume_label = self.task_controls[task_name]["volume_label"] + + # 提取 volume_label中显示的 "方量 200" 中的数字部分 + # 1. 去除前后空格,按空格分割字符串 + volume_text = volume_label.text().strip() + parts = volume_text.split() + + # 2. 取分割后的数字部分 + if len(parts) >= 2: + number_str = parts[1] # 得到 "200" + else: + # 格式异常(没有数字部分),返回None + return None + + # 褚工说任务中显示的方量只有一位小数 + try: + volume_value = round(float(number_str), 1) + return volume_value + except ValueError: + return None + if __name__ == "__main__": app = QApplication(sys.argv) widget = TaskWidget("管片任务") diff --git a/view/widgets/value_adjuster.py b/view/widgets/value_adjuster.py index a2a8393..153a294 100644 --- a/view/widgets/value_adjuster.py +++ b/view/widgets/value_adjuster.py @@ -4,13 +4,36 @@ from PySide6.QtCore import Qt from PySide6.QtGui import QDoubleValidator import sys -# 调整计划方量 +""" + 调整计划方量, 左侧减按钮, 右侧加按钮 + 这里的 最小值、最大值、初始值 需要读取配置文件来决定 +""" + +class CustomLineEdit(QLineEdit): + def __init__(self, default_text: str, parent=None): + super().__init__(parent) + self.default_text = default_text # 保存初始化时的默认文本 + self.setText(self.default_text) # 初始化为默认文本 + + def focusOutEvent(self, event): + super().focusOutEvent(event) # 先执行父类的焦点离开逻辑 + + # 检查文本是否为空(或仅含空格) + current_text = self.text().strip() + if not current_text: + self.setText(self.default_text) # 为空则恢复默认值 + else: # 不为空,显示一位小数 + value = round(float(current_text), 1) + self.setText(f"{value:.1f}") + + self.setCursorPosition(0) # 光标移到最前面 (保证数值显示完整) + class ValueAdjuster(QWidget): def __init__(self, parent=None): super().__init__(parent) - self.min_value = 0 # 最小值 - self.max_value = 99 # 最大值 - self.value = 2.5 # 初始值 + self.min_value = 0.0 # 最小值 + self.max_value = 99.0 # 最大值 + self.value = 2.5 # 初始值 (需要显示一位数字) self.setFixedSize(102, 32) @@ -21,7 +44,9 @@ class ValueAdjuster(QWidget): self.minus_btn.setCursor(Qt.PointingHandCursor) # 中间的编辑栏 - self.line_edit = QLineEdit(f"{self.value:.1f}") # 显示1位小数 + # 支持显示两位小数 + # self.line_edit = QLineEdit(f"{self.value:.1f}") # 显示1位小数 + self.line_edit = CustomLineEdit(f"{self.value:.1f}") # 显示1位小数 self.line_edit.setFixedSize(40, 26) # 加号按钮 @@ -31,8 +56,8 @@ class ValueAdjuster(QWidget): # 配置QLineEdit:支持数字输入+居中显示 self.line_edit.setAlignment(Qt.AlignCenter) # 文本居中 - # 限制输入为浮点数(支持负数,范围可自定义) - self.line_edit.setValidator(QDoubleValidator(0, 99, 1, self)) # 最多1位小数 + # 限制输入为浮点数(范围可自定义) + self.line_edit.setValidator(QDoubleValidator(self.min_value, self.max_value, 1, self)) # 最多1位小数 (必选) self.line_edit.textChanged.connect(self.on_text_changed) # 监听输入变化 # 设置样式表(保持与按钮风格统一) @@ -112,7 +137,7 @@ class ValueAdjuster(QWidget): self.line_edit.setText(f"{self.value:.1f}") def on_text_changed(self, text): - """监听输入框文本变化,更新内部value""" + """监听输入框文本变化, 更新内部value""" if not text: return try: @@ -126,7 +151,7 @@ class ValueAdjuster(QWidget): except ValueError: pass - # 获取具体的方量数值 + # 获取具体的方量数值,float类型 (一位小数) def get_value(self): return self.value