PySide6 动态加载 UI 文件与多线程查询应用框架教程

本教程旨在说明如何使用 QUiLoader 动态加载 .ui 设计文件,结合自定义控件,并通过多线程执行耗时任务(如数据库查询),避免阻塞 UI 线程,最后展示一个典型的桌面应用结构。

1. 环境准备

确保已安装 PySide6:
pip install pyside6

2. UI 设计与准备

  1. 使用 Qt Designer: 创建你的用户界面 .ui 文件(例如 laohua_main.ui)。
  2. 放置控件: 在界面上放置所需的控件,如 QTableWidget, QLineEdit, QPushButton, QComboBox 等。
  3. 设置 Object Name: 非常重要:为每个需要在代码中访问的控件设置唯一的 objectName(例如,sn_table_widget, line_edit, button_search)。这些名称将在代码中用来引用控件。
  4. 自定义控件 (可选): 如果你有一个继承自 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. 关键点总结

  • QUiLoaderregisterCustomWidget: 这是动态加载 UI 并使用自定义控件的关键。必须在 load() 之前调用 registerCustomWidget
  • 类定义位置: 自定义控件类必须在 QUiLoader 尝试加载 UI 之前就存在于 Python 解释器中。如果定义在同一个文件,放在 MainWindow 类之前即可。
  • Qt Designer 提升: 确保在 Designer 中正确提升了控件,并设置了正确的头文件名。
  • 多线程 (QThread): 用于执行耗时操作(如查询),避免 UI 冻结。通过 Signal 将结果安全地传递回主线程进行 UI 更新。
  • 信号与槽: 这是 PySide6 中事件驱动编程的核心机制,用于连接 UI 控件的操作和对应的处理函数。
  • UI 访问: 通过 self.re_ui.<objectName> 来访问在 .ui 文件中定义的控件。