jiateng_ws/widgets/main_window.py

776 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import sys
import logging
import json
from datetime import datetime
from pathlib import Path
# 导入PySide6
from PySide6.QtWidgets import (
QWidget, QMessageBox, QTableWidgetItem, QStackedWidget, QLabel, QMainWindow,
QTableWidget, QMenu
)
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QBrush, QColor
# 导入UI
from ui.main_window_ui import MainWindowUI
# 导入相机显示组件和设置组件
from widgets.camera_display_widget import CameraDisplayWidget
from widgets.camera_settings_widget import CameraSettingsWidget
# 导入检验配置管理器
from utils.inspection_config_manager import InspectionConfigManager
class MainWindow(MainWindowUI):
"""主窗口"""
def __init__(self, user_id=None, user_name=None, corp_name=None, corp_id=None, position_id=None):
super().__init__()
self.user_id = user_id
self.user_name = user_name
self.corp_name = corp_name
self.corp_id = corp_id
self.position_id = position_id
# 设置窗口标题
if user_name and corp_name:
self.setWindowTitle(f"腾智微丝产线包装系统 - {user_name} ({corp_name})")
# 加载配置文件
self.config = self.load_config()
self.camera_enabled = self.config.get('camera', {}).get('enabled', False)
# 初始化检验配置管理器
self.inspection_manager = InspectionConfigManager.get_instance()
# 只有在相机启用时创建相机显示组件
if self.camera_enabled:
# 创建相机显示组件并添加到上料区
self.camera_display = CameraDisplayWidget()
self.material_content_layout.addWidget(self.camera_display)
else:
# 在上料区添加占位标签
self.material_placeholder = QLabel("相机功能已禁用")
self.material_placeholder.setAlignment(Qt.AlignCenter)
self.material_placeholder.setStyleSheet("color: #888888; background-color: #f0f0f0;")
self.material_content_layout.addWidget(self.material_placeholder)
# 为下料区添加占位标签,确保它保持为空
self.output_placeholder = QLabel("下料区 - 暂无内容")
self.output_placeholder.setAlignment(Qt.AlignCenter)
self.output_placeholder.setStyleSheet("color: #888888; background-color: #f0f0f0;")
self.output_content_layout.addWidget(self.output_placeholder)
# 创建堆叠部件
self.stacked_widget = QStackedWidget()
self.stacked_widget.addWidget(self.central_widget) # 主页面
# 不在这里直接初始化相机设置组件
# 延迟创建保证创建的时候SettingsUI的所有控件都已经准备好
self.camera_settings = None
# 设置中央部件为堆叠部件
self.setCentralWidget(self.stacked_widget)
# 连接信号和槽
self.connect_signals()
# 默认显示主页面
self.stacked_widget.setCurrentIndex(0)
# 初始化数据
self.initialize_data()
# 配置检验列 - 使用检验配置管理器获取启用的列数和标题
self.update_inspection_columns()
# 设置表格上下文菜单
self.process_table.setContextMenuPolicy(Qt.CustomContextMenu)
self.process_table.customContextMenuRequested.connect(self.show_table_context_menu)
# 加载未完成的检验数据
self.load_unfinished_inspection_data()
logging.info(f"主窗口已创建,用户: {user_name}")
def load_config(self):
"""加载配置文件"""
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "app_config.json")
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
logging.info(f"已加载配置文件: {config_path}")
return config
except Exception as e:
logging.error(f"加载配置文件失败: {e}")
return {}
def connect_signals(self):
# 连接菜单动作
self.main_action.triggered.connect(self.show_main_page)
self.settings_action.triggered.connect(self.show_settings_page)
# 工程号输入框回车事件
self.order_edit.returnPressed.connect(self.handle_order_enter)
# 托盘号输入框回车和切换事件,触发未加载数据查询
# QComboBox没有returnPressed信号只有currentTextChanged和activated信号
self.tray_edit.currentTextChanged.connect(self.load_unfinished_inspection_data)
self.tray_edit.activated.connect(self.load_unfinished_inspection_data) # 当用户选择一项时触发
# 连接按钮事件
self.input_button.clicked.connect(self.handle_input)
self.output_button.clicked.connect(self.handle_output)
self.start_button.clicked.connect(self.handle_start)
self.stop_button.clicked.connect(self.handle_stop)
# 只有在相机启用时连接相机信号
if self.camera_enabled and hasattr(self, 'camera_display'):
self.camera_display.signal_camera_status.connect(self.handle_camera_status)
def initialize_data(self):
"""初始化界面数据"""
# 设置订单和批号
self.order_edit.setText("ORD-2025-001")
self.tray_edit.setCurrentText("托盘1")
# 初始化项目表格数据示例
project_data = [
["100", "50", "200", "95%"],
["3000", "1500", "6000", "92%"],
["36000", "18000", "72000", "90%"],
["120000", "60000", "240000", "91%"]
]
for row in range(4):
for col in range(0, 4): # 从第2列开始跳过项目列
item = QTableWidgetItem(project_data[row][col])
# item.setTextAlignment(Qt.AlignCenter) # 设置文本居中对齐
self.project_table.setItem(row, col, item)
# 初始化任务表格数据示例 - 注意现在表格有3行第0行是一级标题第1行是二级标题第2行是数据行
# 数据应该填充在第2行索引为2
data = ["200", "95", "0", "0"] # 分别对应:总生产数量、总生产公斤、已完成数量、已完成公斤
for col, value in enumerate(data):
item = QTableWidgetItem(value)
item.setTextAlignment(Qt.AlignCenter) # 设置文本居中对齐
self.task_table.setItem(2, col, item)
def update_inspection_columns(self):
"""更新检验列配置 - 使用检验配置管理器获取启用的列数和标题"""
try:
# 获取已启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 获取启用的列数
column_count = len(enabled_configs)
if column_count == 0:
# 如果没有启用的列,至少显示一列
column_count = 1
headers = ["检验项"]
else:
# 如果有启用的列,使用配置的标题
headers = [config['display_name'] for config in enabled_configs]
# 设置检验列
self.set_inspection_columns(column_count, headers)
logging.info(f"已更新检验列配置:{column_count}列, 标题: {headers}")
except Exception as e:
logging.error(f"更新检验列配置失败: {str(e)}")
# 如果更新失败,使用默认配置
self.set_inspection_columns(1, ["检验项"])
def show_main_page(self):
self.stacked_widget.setCurrentWidget(self.central_widget)
# 更新检验列配置
self.update_inspection_columns()
# 加载未完成的检验数据
self.load_unfinished_inspection_data()
# 只有在相机启用时处理相机显示
if self.camera_enabled and hasattr(self, 'camera_display'):
# 如果相机已连接,直接开始显示相机画面
if self.camera_display.camera_manager.isOpen:
if not self.camera_display.camera_manager.isGrabbing:
self.camera_display.start_display()
logging.info("显示主页面")
def show_settings_page(self):
"""显示设置页面"""
# 延迟创建设置组件
if not hasattr(self, 'settings_widget'):
from widgets.settings_widget import SettingsWidget
self.settings_widget = SettingsWidget(self)
self.stacked_widget.addWidget(self.settings_widget)
# 切换到设置页面
self.stacked_widget.setCurrentWidget(self.settings_widget)
logging.info("显示设置页面")
def handle_input(self):
"""处理上料按钮点击事件"""
logging.info("上料按钮被点击")
QMessageBox.information(self, "操作提示", "开始上料操作")
# 这里添加上料相关的业务逻辑
def handle_output(self):
"""处理下料按钮点击事件"""
logging.info("下料按钮被点击")
QMessageBox.information(self, "操作提示", "开始下料操作")
# 这里添加下料相关的业务逻辑
def handle_start(self):
"""处理开始按钮点击事件"""
logging.info("开始按钮被点击")
# 只有在相机启用时处理相机显示
if self.camera_enabled and hasattr(self, 'camera_display'):
# 开始显示相机画面
if self.camera_display.camera_manager.isOpen:
self.camera_display.start_display()
QMessageBox.information(self, "操作提示", "生产线已启动")
# 这里添加启动生产线的业务逻辑
def handle_stop(self):
"""处理暂停按钮点击事件"""
logging.info("暂停按钮被点击")
# 只有在相机启用时处理相机显示
if self.camera_enabled and hasattr(self, 'camera_display'):
# 停止显示相机画面
self.camera_display.stop_display()
QMessageBox.information(self, "操作提示", "生产线已暂停")
# 这里添加暂停生产线的业务逻辑
def handle_camera_status(self, is_connected, message):
"""处理相机状态变化"""
if is_connected:
logging.info("相机已连接并显示")
else:
logging.warning(f"相机显示问题: {message}")
def handle_camera_connection(self, is_connected, message):
"""处理相机连接状态变化"""
if is_connected:
logging.info("相机已连接")
# 如果当前在主页面,直接开始显示相机画面
if self.stacked_widget.currentWidget() == self.central_widget:
self.camera_display.start_display()
else:
if message:
logging.warning(f"相机连接失败: {message}")
else:
logging.info("相机已断开")
# 如果相机断开,确保停止显示
self.camera_display.stop_display()
def handle_camera_params_changed(self, exposure_time, gain, frame_rate):
"""处理相机参数变化"""
logging.info(f"相机参数已更新: 曝光={exposure_time:.1f}μs, 增益={gain:.1f}dB, 帧率={frame_rate:.1f}fps")
# 这里可以添加对相机参数变化的处理逻辑
def handle_camera_error(self, error_msg):
"""处理相机错误"""
logging.error(f"相机错误: {error_msg}")
QMessageBox.warning(self, "相机错误", error_msg)
def closeEvent(self, event):
"""窗口关闭事件"""
# 只有在相机启用时处理相机关闭
if self.camera_enabled and hasattr(self, 'camera_display'):
# 停止相机显示
self.camera_display.stop_display()
# 接受关闭事件
event.accept()
def handle_order_enter(self):
"""处理工程号输入框按下回车事件"""
logging.info("工程号输入框按下回车事件")
# 获取当前输入的工程号
order_text = self.order_edit.text().strip()
if order_text:
logging.info(f"输入的工程号: {order_text}")
# 在微丝产线表格中添加一条新记录
self.add_new_inspection_row(order_text)
else:
logging.warning("工程号为空")
QMessageBox.warning(self, "输入提示", "请输入有效的工程号")
# 处理完后可以清除焦点,让输入框失去焦点
self.central_widget.setFocus()
def add_new_inspection_row(self, order_id):
"""在微丝产线表格中添加一条新记录
Args:
order_id: 工程号
"""
try:
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 固定的数据起始行索引
data_start_row = 2 # 数据从第3行开始
# 在指定行索引插入新行
self.process_table.insertRow(data_start_row)
# 更新序号 - 所有现有行序号+1
for row in range(data_start_row + 1, self.process_table.rowCount()):
seq_item = self.process_table.item(row, 0)
if seq_item:
try:
current_seq = int(seq_item.text())
seq_item.setText(str(current_seq + 1))
except ValueError:
pass
# 添加工程号到表格的第二列
item = QTableWidgetItem(order_id)
item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(data_start_row, 1, item)
# 添加序号到表格的第一列 - 新行始终是第1条
item = QTableWidgetItem("1")
item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(data_start_row, 0, item)
# 检验列设置为可编辑状态
for i, config in enumerate(enabled_configs):
col_index = 2 + i # 检验列从第3列开始
# 创建空的可编辑单元格
item = QTableWidgetItem("")
item.setTextAlignment(Qt.AlignCenter)
# 设置单元格属性以标识其关联的检验项
item.setData(Qt.UserRole, config.get('id'))
self.process_table.setItem(data_start_row, col_index, item)
# 包装列设置为可编辑状态
packaging_start_col = 2 + len(enabled_configs)
for i in range(2): # 贴标和称重
col_index = packaging_start_col + i
item = QTableWidgetItem("")
item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(data_start_row, col_index, item)
# 设置表格为可编辑状态
self.process_table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed)
# 连接单元格内容变更信号
# 断开之前的连接(如果有)
try:
self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed)
except:
pass # 如果没有连接过,会抛出异常,忽略即可
# 重新连接信号
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
# 选中新添加的行
self.process_table.selectRow(data_start_row)
# 限制最大行数
self.limit_table_rows(10) # 最多保留10行数据
logging.info(f"已添加工程号 {order_id} 的新记录显示在第1条")
except Exception as e:
logging.error(f"添加新记录失败: {str(e)}")
QMessageBox.warning(self, "添加失败", f"添加新记录失败: {str(e)}")
def limit_table_rows(self, max_rows):
"""限制表格最大行数
Args:
max_rows: 最大行数(不包括表头行)
"""
try:
# 计算数据总行数
data_rows = self.process_table.rowCount() - 2 # 减去表头行
# 如果超过最大行数,删除多余的行
if data_rows > max_rows:
# 要删除的行数
rows_to_remove = data_rows - max_rows
# 从最后一行开始删除
for i in range(rows_to_remove):
self.process_table.removeRow(self.process_table.rowCount() - 1)
logging.info(f"已限制表格最大行数为 {max_rows} 行数据,删除了 {rows_to_remove}")
except Exception as e:
logging.error(f"限制表格行数失败: {str(e)}")
def handle_inspection_cell_changed(self, row, column):
"""处理检验单元格内容变更
Args:
row: 行索引
column: 列索引
"""
try:
# 只处理数据行的检验列变更
if row < 2: # 忽略表头行
return
# 忽略首尾两列(序号和工程号)
if column < 2:
return
# 获取工程号
order_id_item = self.process_table.item(row, 1)
if not order_id_item:
return
order_id = order_id_item.text().strip()
if not order_id:
return
# 获取托盘号
tray_id = self.tray_edit.currentText()
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 判断是否是检验列(非包装列)
packaging_start_col = 2 + len(enabled_configs)
if column >= 2 and column < packaging_start_col:
# 是检验列
config_index = column - 2
if config_index < len(enabled_configs):
config = enabled_configs[config_index]
# 获取单元格内容
cell_item = self.process_table.item(row, column)
if not cell_item:
return
value = cell_item.text().strip()
# 显示临时状态消息
self.statusBar().showMessage(f"正在保存检验数据: {config['display_name']}={value}", 1000)
# 验证数据有效性
if self.validate_inspection_value(config, value):
# 设置单元格颜色为通过
cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色
status = 'pass'
else:
# 设置单元格颜色为警告
cell_item.setBackground(QBrush(QColor("#fff9c4"))) # 浅黄色
status = 'warning'
# 保存到数据库
self.save_inspection_data(order_id, tray_id, config['position'], config['id'], value, status)
# 触发查询, 更新页面记录回显
self.load_unfinished_inspection_data()
except Exception as e:
logging.error(f"处理检验单元格变更失败: {str(e)}")
self.statusBar().showMessage(f"处理检验数据失败: {str(e)[:50]}...", 3000)
def validate_inspection_value(self, config, value):
"""验证检验值是否有效
Args:
config: 检验配置
value: 检验值
Returns:
bool: 是否有效
"""
try:
# 检查值是否为空
if not value and config.get('required', False):
return False
# 根据数据类型验证
data_type = config.get('data_type')
if data_type == 'number':
# 数值类型验证
try:
num_value = float(value)
min_value = config.get('min_value')
max_value = config.get('max_value')
if min_value is not None and num_value < min_value:
return False
if max_value is not None and num_value > max_value:
return False
return True
except ValueError:
return False
elif data_type == 'enum':
# 枚举类型验证
enum_values = config.get('enum_values')
if enum_values and isinstance(enum_values, list):
return value in enum_values
return False
# 文本类型不做特殊验证
return True
except Exception as e:
logging.error(f"验证检验值失败: {str(e)}")
return False
def save_inspection_data(self, order_id, tray_id, position, config_id, value, status):
"""保存检验数据到数据库
Args:
order_id: 工程号
position: 位置序号
config_id: 配置ID
value: 检验值
status: 状态
"""
try:
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
# 记录保存前的详细日志
logging.info(f"正在保存检验数据: 工程号={order_id}, 托盘号={tray_id}, 位置={position}, 配置ID={config_id}, 值={value}, 状态={status}")
# 构建数据
data = [{
'position': position,
'config_id': config_id,
'value': value,
'status': status,
'remark': '',
'tray_id': tray_id
}]
# 保存到数据库
result = inspection_dao.save_inspection_data(order_id, data)
# 判断,如果保存成功后,且当前工程号中,全部为 pass 说明该工序已经完成,需要流转到下一步,回显到包装记录中
if result:
logging.info(f"已成功保存工程号 {order_id} 的检验数据,位置: {position}, 值: {value}")
# 显示临时状态消息
self.statusBar().showMessage(f"已保存检验数据:{value}", 3000)
else:
logging.warning(f"保存工程号 {order_id} 的检验数据失败")
# 显示错误消息
self.statusBar().showMessage(f"保存检验数据失败", 3000)
except Exception as e:
logging.error(f"保存检验数据失败: {str(e)}")
# 显示错误消息
self.statusBar().showMessage(f"保存检验数据错误: {str(e)[:50]}...", 3000)
def show_table_context_menu(self, pos):
"""显示表格上下文菜单
Args:
pos: 鼠标位置
"""
try:
# 获取当前单元格
cell_index = self.process_table.indexAt(pos)
if not cell_index.isValid():
return
row = cell_index.row()
column = cell_index.column()
# 只对数据行和检验列显示上下文菜单
if row < 2: # 忽略表头行
return
# 获取工程号
order_id_item = self.process_table.item(row, 1)
if not order_id_item:
return
order_id = order_id_item.text().strip()
if not order_id:
return
# 获取托盘号
tray_id = self.tray_edit.currentText()
# 创建上下文菜单
menu = QMenu(self)
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 判断是否是检验列(非包装列)
packaging_start_col = 2 + len(enabled_configs)
if column >= 2 and column < packaging_start_col:
# 是检验列
config_index = column - 2
if config_index < len(enabled_configs):
config = enabled_configs[config_index]
position = config.get('position')
# 添加查询数据库菜单项
check_action = menu.addAction("检查数据库记录")
check_action.triggered.connect(lambda: self.check_database_record(order_id, position, tray_id))
# 显示菜单
menu.exec_(self.process_table.viewport().mapToGlobal(pos))
except Exception as e:
logging.error(f"显示表格上下文菜单失败: {str(e)}")
def check_database_record(self, order_id, position, tray_id):
"""检查数据库记录
Args:
order_id: 工程号
position: 位置序号
"""
try:
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
# 获取检验数据
inspection_data = inspection_dao.get_inspection_data_by_order(order_id, tray_id)
# 查找对应位置的数据
matching_data = None
for data in inspection_data:
if data.get('position') == position:
matching_data = data
break
# 显示结果
if matching_data:
value = matching_data.get('value')
status = matching_data.get('status')
message = f"数据库记录:\n\n"
message += f"工程号: {order_id}\n"
message += f"位置: {position}\n"
message += f"值: {value}\n"
message += f"状态: {status}\n"
QMessageBox.information(self, "数据库记录", message)
else:
QMessageBox.warning(self, "数据库记录", f"未找到工程号 {order_id} 位置 {position} 的数据")
except Exception as e:
logging.error(f"检查数据库记录失败: {str(e)}")
QMessageBox.warning(self, "查询失败", f"检查数据库记录失败: {str(e)}")
def load_unfinished_inspection_data(self):
"""加载未完成的检验数据并显示在表格中"""
try:
# 使用InspectionDAO获取未完成的检验数据
from dao.inspection_dao import InspectionDAO
inspection_dao = InspectionDAO()
# 获取托盘号
tray_id = self.tray_edit.currentText()
# 使用get_inspection_data_unfinished获取未完成的数据
unfinished_data = inspection_dao.get_inspection_data_unfinished(tray_id)
if not unfinished_data:
logging.info("没有未完成的检验数据")
# 清空表格现有数据行
while self.process_table.rowCount() > 2:
self.process_table.removeRow(2)
return
logging.info(f"已加载未完成的检验数据,共 {len(unfinished_data)} 条记录")
# 获取启用的检验配置
enabled_configs = self.inspection_manager.get_enabled_configs()
# 按工程号分组
orders_data = {}
for data in unfinished_data:
order_id = data['order_id']
if order_id not in orders_data:
orders_data[order_id] = []
orders_data[order_id].append(data)
# 断开单元格变更信号,避免加载过程中触发保存
try:
self.process_table.cellChanged.disconnect(self.handle_inspection_cell_changed)
except:
pass
# 清空表格现有数据行
while self.process_table.rowCount() > 2:
self.process_table.removeRow(2)
# 添加数据到表格
row_idx = 2 # 从第3行开始添加数据
for order_id, items in orders_data.items():
# 添加新行
self.process_table.insertRow(row_idx)
# 添加序号到第一列
seq_item = QTableWidgetItem(str(row_idx - 1))
seq_item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(row_idx, 0, seq_item)
# 添加工程号到第二列
order_item = QTableWidgetItem(order_id)
order_item.setTextAlignment(Qt.AlignCenter)
self.process_table.setItem(row_idx, 1, order_item)
# 添加检验数据
for item in items:
position = item['position']
value = item['value'] if item['value'] else ""
status = item['status']
config_id = item['config_id']
# 找到对应的列索引
col_index = None
for i, config in enumerate(enabled_configs):
if config.get('position') == position:
col_index = 2 + i # 检验列从第3列开始
break
if col_index is not None:
# 创建单元格并设置值
cell_item = QTableWidgetItem(str(value))
cell_item.setTextAlignment(Qt.AlignCenter)
# 存储配置ID用于保存时确定是哪个检验项
cell_item.setData(Qt.UserRole, config_id)
# 根据状态设置单元格颜色
if status == 'fail':
cell_item.setBackground(QBrush(QColor("#ffcdd2"))) # 浅红色
elif status == 'warning':
cell_item.setBackground(QBrush(QColor("#fff9c4"))) # 浅黄色
elif status == 'pass':
cell_item.setBackground(QBrush(QColor("#c8e6c9"))) # 浅绿色
# 设置单元格
self.process_table.setItem(row_idx, col_index, cell_item)
row_idx += 1
# 设置表格为可编辑状态
self.process_table.setEditTriggers(QTableWidget.DoubleClicked | QTableWidget.EditKeyPressed)
# 重新连接单元格变更信号
self.process_table.cellChanged.connect(self.handle_inspection_cell_changed)
except Exception as e:
logging.error(f"加载未完成的检验数据失败: {str(e)}")
QMessageBox.warning(self, "加载失败", f"加载未完成的检验数据失败: {str(e)}")