PySide6 动态加载 UI 文件与多线程查询应用框架教程
本教程旨在说明如何使用 QUiLoader 动态加载 .ui 设计文件,结合自定义控件,并通过多线程执行耗时任务(如数据库查询),避免阻塞 UI 线程,最后展示一个典型的桌面应用结构。
1. 环境准备
确保已安装 PySide6:pip install pyside6
2. UI 设计与准备
- 使用 Qt Designer: 创建你的用户界面
.ui文件(例如laohua_main.ui)。 - 放置控件: 在界面上放置所需的控件,如
QTableWidget,QLineEdit,QPushButton,QComboBox等。 - 设置 Object Name: 非常重要:为每个需要在代码中访问的控件设置唯一的
objectName(例如,sn_table_widget,line_edit,button_search)。这些名称将在代码中用来引用控件。 自定义控件 (可选): 如果你有一个继承自
QTableWidget的自定义类SNTableWidget,你需要在 Qt Designer 中将QTableWidget提升 (Promote) 为SNTableWidget。在“提升为”对话框中:- 提升的类名称:
SNTableWidget - 头文件: 如果
SNTableWidget定义在与主程序相同的.py文件中,填写该文件名(不含.py,例如main_program)。如果在单独的文件中,填写该文件名。 - 全局包含: 取消勾选。
- 提升的类名称:
3. 主程序结构 (main.py)
import sys
from PySide6.QtWidgets import QApplication, QMainWindow, QTableWidget, QTableWidgetItem, QMessageBox, QFile, QAbstractButton
from PySide6.QtCore import QThread, Signal, QIODevice
from PySide6.QtUiTools import QUiLoader
# resource_path 是一个用于定位资源文件的函数
from pathlib import Path # 示例,实际请替换为你的 resource_path 函数
def resource_path(relative_path):
""" 获取资源的绝对路径,适配打包后的环境 """
try:
base_path = sys._MEIPASS # 打包后,PyInstaller 会把文件解压到这个临时目录
except Exception:
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
# --- 1. 定义自定义控件 ---
# 必须在加载 UI 之前定义,因为 QUiLoader 需要知道这个类
class SNTableWidget(QTableWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setColumnCount(1) # 示例列数
self.setRowCount(0) # 初始为空
def set_sn_list(self, sn_list):
"""自定义方法:根据SN列表填充表格"""
self.setRowCount(len(sn_list))
for i, sn in enumerate(sn_list):
item = QTableWidgetItem(sn)
self.setItem(i, 0, item)
def get_all_groups_with_time(self):
"""自定义方法:获取分组和时间信息 (示例)"""
# 实现你的逻辑...
return {}
# --- 自定义控件定义结束 ---
# --- 2. 定义后台工作线程 ---
class QueryWorker(QThread):
query_finished = Signal(list) # 成功时发送结果
query_error = Signal(str) # 失败时发送错误信息
def __init__(self, search_type, search_term, proces, parent=None):
super().__init__(parent)
self.search_type = search_type
self.search_term = search_term
self.proces = proces
def run(self):
try:
# --- 模拟耗时的查询操作 ---
import time
time.sleep(2) # 模拟网络/数据库延迟
# result = your_database_query_function(...) # 真实查询逻辑
# 示例结果
result = [f"SN{i:03d}" for i in range(10)]
# --- 查询操作结束 ---
self.query_finished.emit(result) # 发送成功信号
except Exception as e:
self.query_error.emit(str(e)) # 发送错误信号
# --- 3. 定义主窗口 ---
class MainWindow:
def __init__(self, app):
# 注意:如果你希望 MainWindow 本身是一个窗口,应继承 QMainWindow
# super().__init__()
self.app = app
# --- 3.1 动态加载 UI 文件 ---
loader = QUiLoader()
# 提前注册自定义类
loader.registerCustomWidget(SNTableWidget)
re_ui_file_path = resource_path("ui/laohua_main.ui") #加载UI文件路径
re_ui_file = QFile(str(re_ui_file_path)) # QFile 需要字符串路径
if not re_ui_file.open(QIODevice.ReadOnly):
print(f"无法打开 UI 文件: {re_ui_file.errorString()}")
sys.exit(1)
# 加载UI文件中的控件
self.re_ui = loader.load(re_ui_file)
re_ui_file.close()
# --- UI 加载完成 ---
# --- 3.2 初始化 UI 控件 ---
self.re_ui.com_box.addItems(["SN号", "订单号"])
self.re_ui.com_box_proces.addItems([
"*", "条码登记", "条码关联", "耐压测试", "ATE测试",
"老化测试", "组网测试", "ATE测试2", "包装", "称重"
])
# --- 3.3 连接信号与槽 ---
self.re_ui.button_search.clicked.connect(self._on_button_search_clicked)
self.re_ui.button_Grouping.clicked.connect(self._on_button_Grouping_clicked)
self.re_ui.button_Generate_data.clicked.connect(self._on_button_Generate_data_clicked)
self.re_ui.button_to_MES.clicked.connect(self._on_button_to_MES_clicked)
# --- 3.4 初始化自定义控件 ---
# 假设 UI 中的 QTableWidget 已被成功提升为 SNTableWidget
# 并且其 objectName 是 'sn_table_widget'
if hasattr(self.re_ui, 'sn_table_widget') and isinstance(self.re_ui.sn_table_widget, SNTableWidget):
self.re_ui.sn_table_widget.set_sn_list([]) # 清空初始数据
else:
print("警告: UI 中的 'sn_table_widget' 可能未正确提升为 SNTableWidget 或不存在。")
# 如果提升失败,你需要使用布局替换方法,但这在 QUiLoader 中较难实现,
# 推荐使用 pyside6-uic 预编译 UI 的方式。
# --- 3.5 线程实例变量 ---
self.query_thread = None # 用于持有当前运行的查询线程引用
# --- 4. 按钮事件处理函数 ---
def _on_button_search_clicked(self):
print("1 - 查询按钮被点击,启动后台线程...")
search_type = self.re_ui.com_box.currentText()
search_term = self.re_ui.line_edit.text().strip()
proces = self.re_ui.com_box_proces.currentText()
if not search_term:
QMessageBox.warning(self.re_ui, "警告", "请输入要查询的内容!")
return
# 禁用按钮,防止重复点击
self.re_ui.button_search.setEnabled(False)
self.re_ui.button_search.setText("查询中...")
# 创建并启动查询线程
self.query_thread = QueryWorker(search_type, search_term, proces)
self.query_thread.query_finished.connect(self._on_query_success_on_button_search)
self.query_thread.query_error.connect(self._on_query_error_on_button_search)
self.query_thread.start()
def _on_query_success_on_button_search(self, result_data):
print(f"查询完成,获得 {len(result_data)} 个SN。")
# 重新启用按钮
self.re_ui.button_search.setEnabled(True)
self.re_ui.button_search.setText("查询")
# 更新全局数据(示例)
global sample_sn_list
sample_sn_list = result_data
# 清空旧表格数据
self.re_ui.sn_table_widget.set_sn_list([])
# 显示结果(例如,弹出对话框让用户确认)
# dialog = SNListDialog(sample_sn_list, parent=self.re_ui) # 假设 SNListDialog 存在
# dialog.exec()
def _on_query_error_on_button_search(self, error_message):
# 重新启用按钮
self.re_ui.button_search.setEnabled(True)
self.re_ui.button_search.setText("查询")
# 显示错误信息
QMessageBox.critical(self.re_ui, "查询失败", f"查询过程中发生错误:\n{error_message}")
def _on_button_Grouping_clicked(self):
print("2 - 计算分组按钮被点击")
if not sample_sn_list:
QMessageBox.warning(self.re_ui, "警告", "SN数据为空!")
return
# 将全局数据填充到自定义表格中
self.re_ui.sn_table_widget.set_sn_list(sample_sn_list)
# 其他按钮的处理函数...
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow(app)
window.re_ui.show() # 显示加载的 UI 窗口
sys.exit(app.exec())
# 示例全局变量
sample_sn_list = []4. 关键点总结
QUiLoader与registerCustomWidget: 这是动态加载 UI 并使用自定义控件的关键。必须在load()之前调用registerCustomWidget。- 类定义位置: 自定义控件类必须在
QUiLoader尝试加载 UI 之前就存在于 Python 解释器中。如果定义在同一个文件,放在MainWindow类之前即可。 - Qt Designer 提升: 确保在 Designer 中正确提升了控件,并设置了正确的头文件名。
- 多线程 (
QThread): 用于执行耗时操作(如查询),避免 UI 冻结。通过Signal将结果安全地传递回主线程进行 UI 更新。 - 信号与槽: 这是 PySide6 中事件驱动编程的核心机制,用于连接 UI 控件的操作和对应的处理函数。
- UI 访问: 通过
self.re_ui.<objectName>来访问在.ui文件中定义的控件。
评论已关闭