最近有一个制作用户界面的任务,用了pyqt来完成,这里快速小结记录下。

时间紧任务重,我之前用得最多的是 pysimplegui,有丰富的gallery,稍微修改就能用,但是这个项目已经停了。如果要发布软件给别人用,可能会比较折腾。所以我就只有 pyqt 这个比较熟悉的框架可以用了。pyqt基于qt,历史悠久,功能丰富,性能强大且稳定,最重要的是资料很丰富,像 picasso,cellpose 提供的用户界面都是这个框架制作的,再加上大语言模型帮忙生成代码,估计用户界面开发的速度应该不至于太慢。

用户界面设计

pyqt安装之后,通过everything可以找到一个designer.exe的工具,可以快速完成窗口控件(注意按规范命名objectName)和布局设计。

2025-04-08T10:06:43.png

设计稿可以保存为后缀为 ui 的文件,然后需要在命令行中使用 pyuic5 转换为 python 文件,示例命令如下:

pyuic5 -o design.py design.ui

然后这个 design.py 中的内容不要进行任何修改,在另外一个 main.py 中 import 转换好的UI设计对象即可。

主程序的基本代码结构

这里相当于抽提出来一个简单的模板

from design import UI_MainWindow
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import *

class MyWindow(QMainWindow):
    # 主窗口类
    def __init__(self):
        super().__init__()
        # 应用主窗口设计
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        # 连接槽函数
        self.connectAction()
    
    def connectAction(self):
        # 菜单之类的都可以有一个 triggered 的状态,然后这些状态可以和槽函数连接起来
        self.ui.actionOpen.triggered.connect(self.open)
        # 按钮都有clicked的状态,可以与槽函数连接
        self.ui.btn_process.clicked.connect(self.process)
        # 滑块控件有valueChanged的状态
        self.ui.slider.valueChanged.connect(self.slice)

    # ------- 定义槽函数 ------ #
    def open(self):
        pass

if __name__=='__main__':
   app = QApplication(sys.argv)
   window = MyWindow()
   window.show()
   sys.exit(app.exec_())

子窗口的接入方式

本次任务中,有一个按钮点击后要打开一个子窗口进行操作。这里涉及到一些继承。一般的子窗口同样可以使用designer设计。

pyuic5 -o dialog.py dialog.ui

from dialog import Ui_QDialog

class MyChildWindow(QDialog):
    def __init__(self, parent=None):
        super(LineProfile, self).__init__(parent)
        self.ui = Ui_QDialog()
        self.ui.setupUi(self)
        self.connectAction()

    def connectAction(self):
        # 类似地把UI中的各种信号按钮和槽函数连接起来
        pass

    def func(self):
        # do something, if finished, close
        self.close()

可以看到子窗口中可以在初始化的时候,指定 parent。具体在主窗口的槽函数中可以这样定义:

def open(self):
    self.dialog_window = MyChildWindow(self)
    self.dialog_window.exec_()  # 阻塞
    # self.dialog_window.show()  # 非阻塞

自定义窗口样式

本次任务中还是涉及一个窗口需要在显示的图片上绘制线段。这种窗口感觉在 designer 中无法直接弄,因为有些鼠标事件需要重新定义。

class DrawingView(QGraphicsView):
    def __init__(self, crops_dir):
        super().__init__()
        self.crops_dir = crops_dir
        fp = os.path.join(crops_dir, 'avg.tif')
        self.pixmap = QPixmap(fp)
        self.scene = QGraphicsScene(self)
        self.setScene(self.scene)
        self.start_point = None  # 存储线段起点
        self.end_point = None   # 存储线段终点
        self.temp_line = None   # 临时线段项
        self.is_dragging = False  # 拖拽标志
        self.display_average_particle()

    def display_average_particle(self):
        self.scene.clear()
        self.scene.addPixmap(self.pixmap)
        self.fitInView(self.scene.itemsBoundingRect(), Qt.KeepAspectRatio)

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.clear_temp_line()
            pos = self.mapToScene(event.pos())
            # 判断是否有线段在拖拽中,若有则重置
            if self.is_dragging:
                self.cancel_dragging()
            # 记录起点并开始拖拽
            self.start_point = QPointF(pos)
            pen = QPen(Qt.yellow, 2)
            self.temp_line = self.scene.addLine(
                self.start_point.x(),
                self.start_point.y(),
                self.start_point.x(),
                self.start_point.y(),
                pen)
            self.is_dragging = True

    def mouseMoveEvent(self, event):
        if event.buttons() and Qt.LeftButton and self.is_dragging:
            pos = self.mapToScene(event.pos())
            # 更新终点
            self.end_point = QPointF(pos)
            # 更新线段的坐标
            pen = QPen(Qt.yellow, 2)
            self.temp_line.setPen(pen)
            self.temp_line.setLine(
                self.start_point.x(),
                self.start_point.y(),
                self.end_point.x(),
                self.end_point.y()
            )

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton and self.is_dragging:
            # 确认绘制
            pen = QPen(Qt.yellow, 2)
            self.temp_line.setPen(pen)
            self.is_dragging = False

    def reset_line(self):
        self.start_point = None
        self.end_point = None
        if self.temp_line:
            self.scene.removeItem(self.temp_line)
            self.temp_line = None
        self.display_average_particle()

    def accept_selection(self):
        if self.start_point and self.end_point:
            # print(f"Selected line coordinates: {self.start_point}, {self.end_point}")
            x1 = self.start_point.x()
            y1 = self.start_point.y()
            x2 = self.end_point.x()
            y2 = self.end_point.y()
            saveLineROI(self.crops_dir, x1, y1, x2, y2)
            self.parent().close()
        else:
            print("No selection made")

    def clear_temp_line(self):
        if self.temp_line is not None:
            self.scene.removeItem(self.temp_line)
            self.temp_line = None

这段代码95%是在Deepseek的帮助下完成的,反复迭代了几次,然后根据自己的需求做了少量修改。然而这个还只是一个UI,接下来还要一段代码装入子窗口:

class DRAW_ROI(QtWidgets.QDialog):
    def __init__(self, crops_dir, parent=None):
        super(DRAW_ROI, self).__init__(parent)
        self.view = DrawingView(crops_dir)
        self.setup_ui()

    def setup_ui(self):
        # 创建主窗口
        self.setWindowTitle("Draw line")
        # 创建垂直布局
        main_layout = QVBoxLayout()
        # 添加绘图视图到布局
        main_layout.addWidget(self.view)
        # 创建按钮布局
        button_layout = QHBoxLayout()
        # 创建 OK 和 Cancel 按钮
        self.ok_button = QPushButton("OK")
        self.cancel_button = QPushButton("Clear")
        # 将按钮添加到按钮布局
        button_layout.addWidget(self.ok_button)
        button_layout.addWidget(self.cancel_button)
        main_layout.addLayout(button_layout)
        self.setLayout(main_layout)
        # 连接按钮信号和槽函数
        self.ok_button.clicked.connect(self.view.accept_selection)
        self.cancel_button.clicked.connect(self.view.reset_line)

    def cancel_dragging(self):
        if self.is_dragging:
            self.start_point = None
            self.end_point = None
            if self.temp_line:
                self.scene.removeItem(self.temp_line)
                self.temp_line = None
            self.is_dragging = False

使用pyqt时需要对一些widget对象的方法进行重写,这个就难度很大,不过因为资料多,使用LLM大语言模型提供帮助效果其实还不错。

最后修改:2025 年 05 月 21 日
请大力赞赏以支持本站持续运行!