feeding 下料

This commit is contained in:
2025-11-12 09:23:34 +08:00
49 changed files with 1946 additions and 165 deletions

4
.gitignore vendored
View File

@ -33,3 +33,7 @@ PyQt_Fluent_Widgets.egg-info/
PySide6_Fluent_Widgets.egg-info/ PySide6_Fluent_Widgets.egg-info/
PyQt6_Fluent_Widgets.egg-info/ PyQt6_Fluent_Widgets.egg-info/
PySide2_Fluent_Widgets.egg-info/ PySide2_Fluent_Widgets.egg-info/
/hardware/__pycache__
__pycache__
/core/__pycache__
/vision/__pycache__

View File

@ -1,21 +1,21 @@
# camera_config.ini # camera_config.ini
# 相关的摄像头的配置文件 # 相关的摄像头的配置文件
[上位料斗] [上位料斗]
ip = 192.168.1.50 ip = 192.168.250.60
port = 554 port = 554
username = admin username = admin
password = XJ123456 password = XJ123456
channel = 101 channel = 101
[下位料斗] [下位料斗]
ip = 192.168.1.51 ip = 192.168.250.61
port = 554 port = 554
username = admin username = admin
password = XJ123456 password = XJ123456
channel = 101 channel = 101
[模具车] [模具车]
ip = 192.168.1.51 ip = 192.168.250.61
port = 554 port = 554
username = admin username = admin
password = XJ123456 password = XJ123456

View File

@ -2,6 +2,7 @@
from PySide6.QtCore import Qt, QPropertyAnimation, QRect, QParallelAnimationGroup, QEasingCurve from PySide6.QtCore import Qt, QPropertyAnimation, QRect, QParallelAnimationGroup, QEasingCurve
from view.widgets.system_center_dialog import SystemCenterDialog from view.widgets.system_center_dialog import SystemCenterDialog
from view.widgets.bottom_control_widget import BottomControlWidget from view.widgets.bottom_control_widget import BottomControlWidget
from view.widgets.system_diagnostics_dialog import SystemDiagnosticsDialog
""" """
控制主界面底部的所有按钮, 包括系统诊断、系统中心等的行为。 控制主界面底部的所有按钮, 包括系统诊断、系统中心等的行为。
@ -19,12 +20,20 @@ class BottomControlController:
self.system_center_dialog.hide() # 初始隐藏 (必须) self.system_center_dialog.hide() # 初始隐藏 (必须)
self._init_system_center_dialog_hide_animations() 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() self._bind_dialog_signals()
def _bind_buttons(self): def _bind_buttons(self):
# 底部系统中心按钮 → 触发弹窗显示/隐藏 # 底部系统中心按钮 → 触发弹窗显示/隐藏
self.bottom_control_widget.center_btn.clicked.connect(self.toggle_system_center_dialog) 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): def _bind_dialog_signals(self):
"""绑定弹窗按钮的信号""" """绑定弹窗按钮的信号"""
self.system_center_dialog.sys_setting_clicked.connect(self.handle_sys_setting) self.system_center_dialog.sys_setting_clicked.connect(self.handle_sys_setting)
@ -53,7 +62,7 @@ class BottomControlController:
self.hide_anim_group.finished.connect(self.system_center_dialog.hide) self.hide_anim_group.finished.connect(self.system_center_dialog.hide)
def toggle_system_center_dialog(self): def toggle_system_center_dialog(self):
"""切换弹窗的显示/隐藏状态""" """切换系统中心弹窗的显示/隐藏状态"""
if self.system_center_dialog.isVisible(): if self.system_center_dialog.isVisible():
# 已显示 → 隐藏 # 已显示 → 隐藏
# self.system_center_dialog.hide() # self.system_center_dialog.hide()
@ -94,7 +103,7 @@ class BottomControlController:
# 设置弹窗位置 # 设置弹窗位置
self.system_center_dialog.move(dialog_x, dialog_y) self.system_center_dialog.move(dialog_x, dialog_y)
# ------------------- 业务逻辑方法------------------- # ------------------- 系统中心弹窗业务逻辑-------------------
def handle_sys_setting(self): def handle_sys_setting(self):
"""系统设置按钮的业务逻辑""" """系统设置按钮的业务逻辑"""
# print("执行系统设置逻辑:如打开系统配置窗口、修改参数等") # print("执行系统设置逻辑:如打开系统配置窗口、修改参数等")
@ -109,3 +118,77 @@ class BottomControlController:
def handle_user_center(self): def handle_user_center(self):
"""用户中心按钮的业务逻辑""" """用户中心按钮的业务逻辑"""
# print("执行用户中心逻辑:如切换用户、修改密码等") # 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()

View File

@ -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)

View File

@ -1,95 +1,47 @@
from re import U from PySide6.QtCore import QTimer, Signal, QObject # 导入Qt核心类
from PySide6.QtWidgets import QApplication # 用于获取主线程
import threading
from hardware import transmitter from hardware import transmitter
from view.main_window import MainWindow from view.main_window import MainWindow
import threading
from .camera_controller import CameraController from .camera_controller import CameraController
from .bottom_control_controller import BottomControlController from .bottom_control_controller import BottomControlController
from hardware.transmitter import TransmitterController from .hopper_controller import HopperController
from hardware.relay import RelayController
class MainController: class MainController:
def __init__(self): def __init__(self):
# 主界面 # 主界面
self.main_window = MainWindow() 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
# 初始化子界面
self._initSubViews() self._initSubViews()
# 初始化子控制器
self._initSubControllers() self._initSubControllers()
# self.__connectSignals()
def _onTimer(self):
# 定时任务逻辑
loc_tra=TransmitterController(RelayController())
upper_weight=loc_tra.read_data(1)
lower_weight=loc_tra.read_data(2)
if upper_weight is None:
upper_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.timer = threading.Timer(5.0, self._onTimer)
self.timer.start()
pass
def _onTimer2(self):
print(str(self.angle))
# 定时任务逻辑
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
self.main_window.hopper_widget.setLowerHopperOpeningAngle(self.angle)
# 重新启动定时器以实现重复执行
self.timer2 = threading.Timer(1.0, self._onTimer2)
self.timer2.start()
pass
def showMainWindow(self): def showMainWindow(self):
self.main_window.show() 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("task1","15:44 PM")
self.main_window.dispatch_task_widget.set_task_time("task2","17:37 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("task1","15:38 PM")
self.main_window.segment_task_widget.set_task_time("task2","17:24 PM") self.main_window.segment_task_widget.set_task_time("task2","17:24 PM")
def _initSubControllers(self): def _initSubControllers(self):
# 振捣视频控制 # 右侧视频显示控制模块
self.camera_controller = CameraController( self.camera_controller = CameraController(
video_view=self.main_window.vibration_video video_view=self.main_window.vibration_video
) )
# 底部控制(按钮)控制器
# 底部按钮控制模块
self.bottom_control_controller = BottomControlController( self.bottom_control_controller = BottomControlController(
bottom_control_widget=self.main_window.bottom_control_widget, bottom_control_widget=self.main_window.bottom_control_widget,
main_window=self.main_window 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): def _initSubViews(self):
pass pass

BIN
db/three.db Normal file

Binary file not shown.

View File

@ -20,7 +20,8 @@ class TransmitterController:
} }
} }
def read_data(self, transmitter_id): # 备份 modbus 读取数据)
def read_data_bak(self, transmitter_id):
"""读取变送器数据""" """读取变送器数据"""
try: try:
if transmitter_id not in self.config: if transmitter_id not in self.config:
@ -67,3 +68,103 @@ class TransmitterController:
return None return None
finally: finally:
self.relay_controller.modbus_client.close() 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 = 2 # 超时时间为 2秒
BUFFER_SIZE= 1024
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(BUFFER_SIZE)
if data:
# print(f"收到原始数据:{data}")
# 提取出完整的一个数据包 (\r\n结尾)
packet = self.get_latest_valid_packet(data)
if not packet:
print("未获取到有效数据包!!")
return None
# 解析重量
weight = self.parse_weight(packet)
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 get_latest_valid_packet(self, raw_data):
"""
解决TCP粘包:
从原始数据中,筛选所有有效包,返回最新的一个有效包
有效包标准: 1. 能UTF-8解码 2. 按逗号拆分≥3个字段 3. 第三个字段含数字(重量)
"""
DELIMITER = b'\r\n'
# 1. 按分隔符拆分,过滤空包
packets = [p for p in raw_data.split(DELIMITER) if p]
if not packets:
return None
valid_packets = []
for packet in packets:
try:
# 过滤无效ASCII字符只保留可见字符
valid_chars = [c for c in packet if 32 <= c <= 126]
filtered_packet = bytes(valid_chars)
# 2. 验证解码
data_str = filtered_packet.decode('utf-8').strip()
# 3. 验证字段数量
parts = data_str.split(',')
if len(parts) < 3:
continue
# 4. 验证重量字段含数字
weight_part = parts[2].strip()
if not any(char.isdigit() for char in weight_part):
continue
# 满足所有条件,加入有效包列表
valid_packets.append(packet)
except (UnicodeDecodeError, IndexError):
# 解码失败或字段异常,跳过该包
continue
# 返回最后一个有效包最新无有效包则返回None
return valid_packets[-1] if valid_packets else None
def parse_weight(self, packet_data):
"""解析重量函数:提取重量数值(如从 b'ST,NT,+0000175\r\n' 中提取 175)"""
try:
data_str = packet_data.decode('utf-8').strip()
parts = data_str.split(',')
# 确保有完整的数据包,三个字段
if len(parts) < 3:
print(f"parse_weight: 包格式错误(字段不足):{data_str}")
return None
weight_part = parts[2].strip()
return int(''.join(filter(str.isdigit, weight_part)))
except (IndexError, ValueError, UnicodeDecodeError) as e:
# print(f"数据解析失败:{e},原始数据包:{packet_data}")
return None

BIN
images/关闭图标.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
images/详情标题.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

32
test.py Normal file
View File

@ -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)}")

View File

@ -93,3 +93,25 @@ class ImagePaths:
# 功能:主界面相关 # 功能:主界面相关
MAIN_INTERFACE_BACKGROUND = ":/icons/images/主界面背景.png" 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"
# 功能:管片任务详情按钮弹窗
SEGMENT_DETAILS_POPUP_BG = "images/详情弹出背景.png"
SEGMENT_DETAILS_TITLE_BG = "images/详情标题.png"
SEGMENT_DETAILS_INFO_BAR = "images/管片任务信息栏.png"
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"

View File

@ -21,6 +21,9 @@ from .widgets.bottom_control_widget import BottomControlWidget
import resources.resources_rc import resources.resources_rc
from utils.image_paths import ImagePaths from utils.image_paths import ImagePaths
from .widgets.segment_details_dialog import SegmentDetailsDialog
from .widgets.dispatch_details_dialog import DispatchDetailsDialog
class MainWindow(QWidget): class MainWindow(QWidget):
def __init__(self): def __init__(self):
@ -34,21 +37,30 @@ class MainWindow(QWidget):
# 安装事件过滤器,处理计划方量的 QLineEdit的失去焦点事件 # 安装事件过滤器,处理计划方量的 QLineEdit的失去焦点事件
self.installEventFilter(self) self.installEventFilter(self)
# 连接槽函数
def connectSignalToSlot(self): def connectSignalToSlot(self):
# 可添加信号槽连接 # 可添加信号槽连接
# self.system_button_widget.buttons["系统启动"].clicked.connect(self.handleSystemStart) # self.system_button_widget.buttons["系统启动"].clicked.connect(self.handleSystemStart)
# self.system_button_widget.buttons["系统停止"].clicked.connect(self.handleSystemStop) # 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) # 管片任务详情按钮
# 派单任务详情
self.dispatch_task_widget.task_details_signal.connect(self.handleDispatchTaskDetails) # 派单任务详情按钮
def handleSystemStart(self): def handleSystemStart(self):
# 测试 # 测试系统开启,进度条动画
self.production_progress.testProgress(60) self.production_progress.testProgress(60)
self.arc_progress.testProgress(60) self.arc_progress.testProgress(60)
def handleSystemStop(self): def handleSystemStop(self):
# 测试 # 测试系统停止,进度条动画
self.production_progress.animation.stop() self.production_progress.animation.stop()
self.arc_progress.animation.stop() self.arc_progress.animation.stop()
@ -63,7 +75,8 @@ class MainWindow(QWidget):
# self.setStyleSheet("background-color: #ffffff;") # #001558 # self.setStyleSheet("background-color: #ffffff;") # #001558
# Qt.FramelessWindowHint # Qt.FramelessWindowHint
self.setWindowFlags(Qt.FramelessWindowHint) # 没有顶部的白色边框
self.setWindowFlags(Qt.FramelessWindowHint) # 无边框
# 设置主界面背景图片 # 设置主界面背景图片
try: try:
@ -100,6 +113,7 @@ class MainWindow(QWidget):
self.dispatch_task_widget.set_task_id("task2", "PD0002") self.dispatch_task_widget.set_task_id("task2", "PD0002")
self.dispatch_task_widget.set_task_id("task3", "PD0003") self.dispatch_task_widget.set_task_id("task3", "PD0003")
# 读取数据库,初始化 管片任务的数据
from busisness.blls import ArtifactBll, PDRecordBll from busisness.blls import ArtifactBll, PDRecordBll
artifact_dal = ArtifactBll() artifact_dal = ArtifactBll()
artifacts = artifact_dal.get_artifact_task() artifacts = artifact_dal.get_artifact_task()
@ -197,6 +211,8 @@ class MainWindow(QWidget):
# 以下为模拟: # 以下为模拟:
# 假设两秒种之后,移动到了搅拌机下 (这里需要根据实际情况修改) # 假设两秒种之后,移动到了搅拌机下 (这里需要根据实际情况修改)
QTimer.singleShot(2000, self.conveyor_system_widget.moveHopperBelowMixer) 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)) QTimer.singleShot(2100, lambda: self.conveyor_system_widget.right_btn.setEnabled(True))
@ -207,6 +223,10 @@ class MainWindow(QWidget):
self.hopper_widget.upper_clamp_widget.set_angle(0) # 上料斗向右移动到目的地时夹爪的角度一定是0 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) # 料斗向右移动完成,隐藏料斗 QTimer.singleShot(1900, self.conveyor_system_widget.hideHopper) # 料斗向右移动完成,隐藏料斗
@ -214,6 +234,42 @@ class MainWindow(QWidget):
# 料斗右移完成,恢复料斗左移按钮 # 料斗右移完成,恢复料斗左移按钮
QTimer.singleShot(2100, lambda: self.conveyor_system_widget.left_btn.setEnabled(True)) 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()
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的坐标 # 更新 派单任务widget的坐标
def update_dispatch_task_position(self): def update_dispatch_task_position(self):
# 方法1获取模具车控件左上角坐标相对于父控件 # 方法1获取模具车控件左上角坐标相对于父控件

View File

@ -6,6 +6,7 @@ from view.widgets.switch_button import SwitchButton
import resources.resources_rc import resources.resources_rc
from utils.image_paths import ImagePaths from utils.image_paths import ImagePaths
# 底部的控制控件,包括 系统诊断、系统中心等按钮
class BottomControlWidget(QWidget): class BottomControlWidget(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -262,8 +263,9 @@ class BottomControlWidget(QWidget):
text_label.setStyleSheet("color: #3bfff8; font-size: 20px;") text_label.setStyleSheet("color: #3bfff8; font-size: 20px;")
layout.addWidget(text_label, alignment=Qt.AlignVCenter) layout.addWidget(text_label, alignment=Qt.AlignVCenter)
# 开关 # 开关(初始的时候,自动模式是打开的)
self.auto_switch = SwitchButton() self.auto_switch = SwitchButton()
self.auto_switch.setChecked(True) # 设置自动模式初始状态
layout.addWidget(self.auto_switch, alignment=Qt.AlignVCenter | Qt.AlignRight) layout.addWidget(self.auto_switch, alignment=Qt.AlignVCenter | Qt.AlignRight)
return widget return widget

View File

@ -18,6 +18,9 @@ class ConveyorSystemWidget(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("料斗与传送带界面") self.setWindowTitle("料斗与传送带界面")
self._last_upper_hopper_weight = None # 上一次的上料斗重量初始为None
self.setFixedSize(443, 190) self.setFixedSize(443, 190)
self.init_ui() self.init_ui()
self._bind() self._bind()
@ -74,13 +77,52 @@ class ConveyorSystemWidget(QWidget):
inner_img = ImagePaths.HOPPER2 inner_img = ImagePaths.HOPPER2
inner_pixmap = QPixmap(inner_img) inner_pixmap = QPixmap(inner_img)
if not inner_pixmap.isNull(): if not inner_pixmap.isNull():
upper_inner_label = QLabel(upper_bg_widget) self.upper_inner_label = QLabel(upper_bg_widget)
upper_inner_label.setPixmap(inner_pixmap) self.upper_inner_label.setPixmap(inner_pixmap)
upper_inner_label.setFixedSize(inner_pixmap.width(), inner_pixmap.height()) self.upper_inner_label.setFixedSize(inner_pixmap.width(), inner_pixmap.height())
upper_inner_label.move(14, 9) # 保持原位置 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 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: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): def create_conveyor(self):
"""创建传送带组件包含左右齿轮group容器背景为传送带图片""" """创建传送带组件包含左右齿轮group容器背景为传送带图片"""
group = QWidget() group = QWidget()

View File

@ -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())

View File

@ -29,6 +29,10 @@ class HopperWidget(QWidget):
self.upper_arch_breaking_status = False # 初始为不破拱状态 self.upper_arch_breaking_status = False # 初始为不破拱状态
self.lower_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 # 料斗控制界面的固定大小为 332x482
# 需要根据具体的料斗的图片来调整 # 需要根据具体的料斗的图片来调整
# self.setFixedSize(356, 496) # self.setFixedSize(356, 496)
@ -90,17 +94,20 @@ class HopperWidget(QWidget):
self.upper_bg_widget = QWidget() self.upper_bg_widget = QWidget()
self.upper_bg_widget.setFixedSize(outer_width, outer_height) 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) layout.addWidget(self.upper_bg_widget, alignment=Qt.AlignCenter)
# 内框图片(上位) # 内框图片(上位)
inner_img = ImagePaths.HOPPER2 inner_img = ImagePaths.HOPPER2
inner_pixmap = QPixmap(inner_img) inner_pixmap = QPixmap(inner_img)
if not inner_pixmap.isNull(): if not inner_pixmap.isNull():
self.upper_inner_label = QLabel(self.upper_bg_widget) self.upper_inner_label = QLabel(self.upper_bg_widget)
self.upper_inner_label.setPixmap(inner_pixmap) 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.move(14, 9)
self.upper_inner_label.setAlignment(Qt.AlignBottom)
# 状态图片(上位,绿色) # 状态图片(上位,绿色)
status_img = ImagePaths.HOPPER_STATUS_GREEN status_img = ImagePaths.HOPPER_STATUS_GREEN
@ -162,17 +169,6 @@ class HopperWidget(QWidget):
self.upper_arch_btn.clicked.connect(self.onUpperArchBreaking) self.upper_arch_btn.clicked.connect(self.onUpperArchBreaking)
self.lower_arch_btn.clicked.connect(self.onLowerArchBreaking) 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() @Slot()
def onUpperArchBreaking(self): def onUpperArchBreaking(self):
if self.upper_arch_breaking_status == False: # 不破拱状态 if self.upper_arch_breaking_status == False: # 不破拱状态
@ -251,7 +247,10 @@ class HopperWidget(QWidget):
self.lower_inner_label = QLabel(self.lower_bg_widget) self.lower_inner_label = QLabel(self.lower_bg_widget)
self.lower_inner_label.setPixmap(inner_pixmap) self.lower_inner_label.setPixmap(inner_pixmap)
self.lower_inner_label.setFixedSize(inner_pixmap.width(), inner_pixmap.height()) 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.move(14, 9)
self.lower_inner_label.setAlignment(Qt.AlignBottom)
# 状态图片(下位) # 状态图片(下位)
status_img = ImagePaths.HOPPER_STATUS_GREEN status_img = ImagePaths.HOPPER_STATUS_GREEN
@ -313,10 +312,48 @@ class HopperWidget(QWidget):
return group 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()
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("current_weight",current_weight, "label_y", label_y)
# 6、强制刷新UI确保立即显示变化
self.upper_inner_label.update()
# 上料斗重量设置 # 上料斗重量设置
def setUpperHopperWeight(self, weight:float): def setUpperHopperWeight(self, weight:int):
# 仅当重量变化时,才更新标签和进度
if weight != self._last_upper_hopper_weight:
# 1、更新上料斗重量标签显示最新重量
self.upper_weight_label.setText(f"{weight}kg") self.upper_weight_label.setText(f"{weight}kg")
# 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): def setUpperHopperVolume(self, volume: float):
"""Args: """Args:
@ -324,22 +361,65 @@ class HopperWidget(QWidget):
""" """
self.upper_extra_label.setText(f"{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): def setLowerHopperWeight(self, weight:int):
# 仅当重量变化时,才更新标签和进度
if weight != self._last_lower_hopper_weight:
# 1、更新下料斗显示标签显示的重量
self.lower_weight_label.setText(f"{weight}kg") self.lower_weight_label.setText(f"{weight}kg")
# 下料斗开合角度设置 # 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、处理边界值
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()
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()
# 下料斗开合角度设置 (包括 夹爪和标签)
def setLowerHopperOpeningAngle(self, angle: float): def setLowerHopperOpeningAngle(self, angle: float):
"""Args: """Args:
angle : 传入多少度 (单位°) angle : 传入多少度 (单位°)
""" """
self.lower_extra_label.setText(f"开: {angle}°") self.lower_extra_label.setText(f"开: {angle}°") # 设置下料斗角度标签
self.lower_clamp_widget.set_angle(angle) self.lower_clamp_widget.set_angle(angle) # 设置下料斗夹爪开合角度
# ------------------------------ # ------------------------------
# 设置上料斗状态0=绿1=黄2=红) # 设置上料斗状态0=绿1=黄2=红)
# ------------------------------ # ------------------------------
def setUpperArchStatus(self, status: int): def setUpperHopperStatus(self, status: int):
""" """
设置上料斗状态图片 设置上料斗状态图片
Args: Args:
@ -361,17 +441,12 @@ class HopperWidget(QWidget):
# 加载并缩放图片 # 加载并缩放图片
status_pixmap = QPixmap(img_path) status_pixmap = QPixmap(img_path)
if not status_pixmap.isNull(): if not status_pixmap.isNull():
status_pixmap = status_pixmap.scaled(
22, 22,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.upper_status_label.setPixmap(status_pixmap) self.upper_status_label.setPixmap(status_pixmap)
# ------------------------------ # ------------------------------
# 设置下料斗状态0=绿1=黄2=红) # 设置下料斗状态0=绿1=黄2=红)
# ------------------------------ # ------------------------------
def setLowerArchStatus(self, status: int): def setLowerHopperStatus(self, status: int):
""" """
设置下料斗状态图片 设置下料斗状态图片
Args: Args:
@ -390,11 +465,6 @@ class HopperWidget(QWidget):
status_pixmap = QPixmap(img_path) status_pixmap = QPixmap(img_path)
if not status_pixmap.isNull(): if not status_pixmap.isNull():
status_pixmap = status_pixmap.scaled(
22, 22,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
self.lower_status_label.setPixmap(status_pixmap) self.lower_status_label.setPixmap(status_pixmap)
# 隐藏上料斗 (用于上料斗移动) # 隐藏上料斗 (用于上料斗移动)
@ -420,7 +490,7 @@ if __name__ == "__main__":
window.setLowerHopperWeight(2000) window.setLowerHopperWeight(2000)
window.setUpperHopperVolume(3.0) window.setUpperHopperVolume(3.0)
window.setLowerHopperOpeningAngle(45) window.setLowerHopperOpeningAngle(45)
window.setUpperArchStatus(2) window.setUpperHopperStatus(2)
window.setLowerArchStatus(1) window.setLowerHopperStatus(1)
window.show() window.show()
sys.exit(app.exec()) sys.exit(app.exec())

View File

@ -1,14 +1,88 @@
from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout
from PySide6.QtGui import QPixmap, QFont from PySide6.QtGui import QPixmap, QFont, QTransform
from PySide6.QtCore import Qt from PySide6.QtCore import Qt, QPropertyAnimation, QEasingCurve, Property
import resources.resources_rc import resources.resources_rc
from utils.image_paths import ImagePaths 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): class MixerWidget(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
# 两个搅拌桨的转动的动画引用
self.animations = [] # 保存动画引用
# 初始化布局 # 初始化布局
layout = QHBoxLayout(self) layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0) layout.setContentsMargins(0, 0, 0, 0)
@ -28,35 +102,71 @@ class MixerWidget(QWidget):
""") """)
layout.addWidget(self.text_label, alignment=Qt.AlignLeft) layout.addWidget(self.text_label, alignment=Qt.AlignLeft)
# 2. 创建搅拌机设备及搅拌桨图标 # 2. 创建搅拌机设备
self.device_label = QLabel() self.device_label = QLabel()
device_pixmap = QPixmap(ImagePaths.MIXER) device_pixmap = QPixmap(ImagePaths.MIXER)
self.device_label.setPixmap(device_pixmap) self.device_label.setPixmap(device_pixmap)
layout.addWidget(self.device_label, alignment=Qt.AlignLeft) layout.addWidget(self.device_label, alignment=Qt.AlignLeft)
# 3. 叠加两个搅拌桨图标 # 3. 初始化两个搅拌桨
self.blade1 = QLabel(self.device_label) # 从左往右第一个搅拌桨 self._init_blades()
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
)
self.blade2 = QLabel(self.device_label) def _init_blades(self):
blade2_pixmap = QPixmap(ImagePaths.MIXER_PADDLE) # 从左往右第二个搅拌桨 blade_pixmap = QPixmap(ImagePaths.MIXER_PADDLE)
self.blade2.setPixmap(blade2_pixmap) if blade_pixmap.isNull():
self.blade2.move( return
(device_pixmap.width() - blade2_pixmap.width()) // 2 + 31,
(device_pixmap.height() - blade2_pixmap.height()) // 2 - 4
)
# 测试代码 # 设备背景的尺寸(用于计算固定中心点)
if __name__ == "__main__": device_pixmap = self.device_label.pixmap()
import sys if not device_pixmap:
from PySide6.QtWidgets import QApplication, QMainWindow return
device_w = device_pixmap.width()
device_h = device_pixmap.height()
app = QApplication(sys.argv) # --------------------------
mixer_widget = MixerWidget() # 左搅拌桨:计算固定中心点
mixer_widget.show() # --------------------------
sys.exit(app.exec()) 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)
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()
# 搅拌桨开始搅拌
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()

View File

@ -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())

View File

@ -60,10 +60,10 @@ class SystemCenterDialog(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.init_ui() self._init_ui()
self.init_animations() # 初始化动画 self.init_animations() # 初始化动画
def init_ui(self): def _init_ui(self):
# 弹窗基础设置 # 弹窗基础设置
self.setWindowTitle("系统中心") self.setWindowTitle("系统中心")
self.setWindowFlags(Qt.FramelessWindowHint) # 隐藏默认边框 self.setWindowFlags(Qt.FramelessWindowHint) # 隐藏默认边框

View File

@ -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)
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())

View File

@ -5,6 +5,8 @@ from PySide6.QtCore import Qt, QTimer, QDateTime
import resources.resources_rc import resources.resources_rc
from utils.image_paths import ImagePaths from utils.image_paths import ImagePaths
"""主界面最上方的导航栏"""
# 自定义消息容器, 显示系统消息 # 自定义消息容器, 显示系统消息
class MsgContainer(QWidget): class MsgContainer(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):

View File

@ -1,14 +1,19 @@
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QMessageBox, QApplication) QPushButton, QMessageBox, QApplication)
from PySide6.QtCore import Qt from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QPainter, QPixmap, QFont from PySide6.QtGui import QPainter, QPixmap, QFont
import sys import sys
import resources.resources_rc import resources.resources_rc
from utils.image_paths import ImagePaths from utils.image_paths import ImagePaths
# 任务控件,如:管片任务、派单任务 """
任务控件,如:管片任务、派单任务
"""
class TaskWidget(QWidget): class TaskWidget(QWidget):
# 任务详情信号: task1表示第一条任务
task_details_signal = Signal(str)
def __init__(self, taskTitle:str, parent=None): def __init__(self, taskTitle:str, parent=None):
super().__init__(parent) super().__init__(parent)
# 设置Widget大小与背景图一致 # 设置Widget大小与背景图一致
@ -153,8 +158,12 @@ class TaskWidget(QWidget):
def _show_detail_dialog(self, task_name): 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)
# -------------------------- # --------------------------
# 对外接口:修改任务属性 # 对外接口:修改任务属性
@ -178,6 +187,33 @@ class TaskWidget(QWidget):
task_id_label = self.task_controls[task_name]["task_id_label"] task_id_label = self.task_controls[task_name]["task_id_label"]
task_id_label.setText(new_id) 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__": if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
widget = TaskWidget("管片任务") widget = TaskWidget("管片任务")

View File

@ -4,13 +4,36 @@ from PySide6.QtCore import Qt
from PySide6.QtGui import QDoubleValidator from PySide6.QtGui import QDoubleValidator
import sys 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): class ValueAdjuster(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.min_value = 0 # 最小值 self.min_value = 0.0 # 最小值
self.max_value = 99 # 最大值 self.max_value = 99.0 # 最大值
self.value = 2.5 # 初始值 self.value = 2.5 # 初始值 (需要显示一位数字)
self.setFixedSize(102, 32) self.setFixedSize(102, 32)
@ -21,7 +44,9 @@ class ValueAdjuster(QWidget):
self.minus_btn.setCursor(Qt.PointingHandCursor) 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) self.line_edit.setFixedSize(40, 26)
# 加号按钮 # 加号按钮
@ -31,8 +56,8 @@ class ValueAdjuster(QWidget):
# 配置QLineEdit支持数字输入+居中显示 # 配置QLineEdit支持数字输入+居中显示
self.line_edit.setAlignment(Qt.AlignCenter) # 文本居中 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) # 监听输入变化 self.line_edit.textChanged.connect(self.on_text_changed) # 监听输入变化
# 设置样式表(保持与按钮风格统一) # 设置样式表(保持与按钮风格统一)
@ -112,7 +137,7 @@ class ValueAdjuster(QWidget):
self.line_edit.setText(f"{self.value:.1f}") self.line_edit.setText(f"{self.value:.1f}")
def on_text_changed(self, text): def on_text_changed(self, text):
"""监听输入框文本变化更新内部value""" """监听输入框文本变化, 更新内部value"""
if not text: if not text:
return return
try: try:
@ -126,7 +151,7 @@ class ValueAdjuster(QWidget):
except ValueError: except ValueError:
pass pass
# 获取具体的方量数值 # 获取具体的方量数值float类型 (一位小数)
def get_value(self): def get_value(self):
return self.value return self.value