我的 Qt for Python 笔记

本文最后更新于:2020年11月19日 上午

盒子布局

官方文档:https://doc.qt.io/qtforpython/PySide2/QtWidgets/QGridLayout.html

grid = QGridLayout()  # 实例化一个格子布局

比较常用的几个函数:

def addLayout (arg__1, row, column, rowSpan, columnSpan[, alignment=Qt.Alignment()])
def addLayout (arg__1, row, column[, alignment=Qt.Alignment()])
def addWidget (arg__1, row, column, rowSpan, columnSpan[, alignment=Qt.Alignment()])
def addWidget (arg__1, row, column[, alignment=Qt.Alignment()])

实际使用中一般是这样:

grid.addLayout(hbox, 0, 0)  # 第一行第一列,占一个单元格
grid.addWidget(w1, 0, 1)  # 第一行第二列,占一个单元格
grid.addWidget(w2, 1, 0, 1, 2)  # 第二行第一列,占一行两列

上面的代码,实际构造出来的 grid 就是一个两行两列的格子,只不过第二行的两列是合并为了一个单元格。

还有两个很重要的函数:

def setColumnStretch (column, stretch)
def setRowStretch (row, stretch)

用于设置某一行(列)的宽(高)度占比,如果使用这个函数理论上就应该为每一行(列)进行设置,否则默认的占比为 0 ,就可能导致某一行(列)无法显示。例如上述二行二列的格子:

grid.setColumnStretch(0, 1)  # 第一列占比1/3
grid.setColumnStretch(1, 2)  # 第一列占比2/3

删除组件

事实上删除组件是不支持的~Qt没有任何有关删除某个组件的接口函数!

但组件是可以显示或隐藏的,想想其实隐藏可能也更好一些~

widget.setVisiable(b)  # 传入bool量用于显示或隐藏

实际开发中,一个组件隐藏后只是看不见,但它还是占空间的。之所以想要删除组件其实也是为了让它不再占用空间,不过虽然没有删除组件的功能,但仍然可以通过隐藏实现同样的效果。

这就需要借助横向盒子布局或者纵向盒子布局(格子布局理论上也是可以的,但需要限制其独占一行或一列),不管是哪种布局,默认的拉伸因子都是 0 ,以如下代码为例:

hbox = QHBoxLayout()
hbox.addWidget(w1, 1)  # 添加组件w1,拉伸因子为1
hbox.addWidget(w2, 1)  # 添加组件w2,拉伸因子为1

也可以通过以下函数设置某一组件或子布局的拉伸因子:

PySide2.QtWidgets.QBoxLayout.setStretchFactor(l, stretch)  # 设置子布局的拉伸因子
PySide2.QtWidgets.QBoxLayout.setStretchFactor(w, stretch)  # 设置组件的拉伸因子

如果需要隐藏某组件并使其不再占据空间,只需要两步,以 w1 为例:

w1.setVisiable(False)  # 隐藏组件w1
hbox.setStretchFactor(w1, 0)  # 设置w1所在的盒子占比为0

多线程 QThread

在我使用 Qt for Python 编程的过程中需要用到多线程,在 Python 中是使用 threading 库,使用起来还是很简单的,但最初自以为 Qt 也一样,但发现其实不想,没有作用甚至会报错,简单说就是与 Qt 相关的代码要使用多线程都应该使用 Qt 提供的多线程类 QThread 。

class MyThread(QThread):
    def __init__(self):
        self.running = False

    def run(self):
        self.running = True
        while self.running:
            # do something

    def stop(self):
        self.running = False

框架如上,首先自定义一个线程类并继承自 QThread ,然后我定义了一个属性 running 用于控制线程的终止,run() 方法是重写的 QThread 中的方法,实际线程中的逻辑应该放到 run() 方法中去执行 。启动线程需要先实例化一个线程对象,然后调用 QThread 的 start() 接口函数即可启动线程。示例:

thread = MyThread()  # 实例化线程对象
thread.start()  # 启动线程
# do something
thread.stop()  # 终止线程

窗口大小自适应

虽然当前的电脑都在往着屏幕更大、分辨率更高的方向发展,但一款软件开发出来也难免可能会在一些老旧低分辨率的电脑上使用,所以如见如果可以自适应不同大小的屏幕,用户体验会更好。

以下代码独特之处在于对于小屏幕,窗口将默认最大化显示,且窗口大小不可调整。这样设置的目的是为了在小屏幕上尽可能完整的显示软件界面,否则软件界面超出了屏幕的可视范围或者被任务栏遮挡,用户体验就会很差~

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        # 此处省略一万字...
        screen = QDesktopWidget().screenGeometry(self)  # 获取整个屏幕的大小
        available = QDesktopWidget().availableGeometry(self)  # 除去任务栏的区域大小
        title_height = self.style().pixelMetric(QStyle.PM_TitleBarHeight)  # 窗口顶栏高度
        # 如果屏幕太小则默认窗口最大化且不可改变大小
        if screen.width() < 1280 or screen.height() < 768:
            self.setWindowState(Qt.WindowMaximized)  # 窗口最大化显示
            self.setFixedSize(available.width(), available.height() - title_height)  # 固定窗口大小
        # 否则设置一个最小宽高,窗口大小可调整
        else:
            self.setMinimumSize(QSize(1000, 800))  # 最小宽高
        self.show()  # 显示窗口

Qt 其实是有让窗口全屏的函数的,例如上述代码中的 show() 函数是显示窗口,如果设置了窗口大小则按设置的大小显示,事实上 Qt 提供了以下一些显示窗口的函数:

def show()  # 显示方式取决于WindowState等其它一些因素
def showFullScreen()  # 全屏显示,会遮挡任务栏
def showMaximized()  # 最大化显示,但直接使用用户仍然可以将窗口调小
def showMinimized()  # 最小化显示
def showNormal()  # 正常显示

自定义信号

Qt for Python 中自定义信号首先需要注意一点,PyQt5 和 PySide2 中定义信号的类名不一样!

在 PySide2 中应该使用 Signal() ,而在 PyQt5 中应使用 pyqtSignal() ,用法上其实没什么区别。

以下示例代码采用 PySide2 方式:

class MainWindow(QMainWindow):
    # 自定义信号
    signal1 = Signal()  # 无参的信号
    signal2 = Signal(int)  # 传递一个int型参数的信号
    signal3 = Signal(int, str)  # 传递一个int型参数和一个str型字符串的信号
    
    def __init__(self):
        super().__init__()
        # 绑定信号与槽
        self.signal1.connect(self.slot1)
        self.signal2.connect(self.slot2)
        self.signal3.connect(self.slot3)

    # 定义槽函数
    def slot1(self):
        # do something
        pass

    def slot2(self, num):
        # do something
        pass

    def slot3(self, num, string):
        # do something
        pass
    
    def test(self):
        # 触发信号
        self.signal1.emit()
        self.signal2.emit(123)
        self.signal3.emit(123, '123')

使用信号与槽还应特别注意它们的作用域以及生命周期。一般需要将信号定义为 Qt 对象的属性,槽函数定义为 Qt 对象的方法,也就是说信号与槽的生命周期将跟随对象的生命周期(可能是必须这样做,暂时没找到相关资料)~

QSpinBox 限制用户输入

QSpinBox 有好几个分类,这里以一般的 QSpinBox 为例,首先简要列举几个常用的接口函数:

def setPrefix(prefix)  # 设置前缀,例如¥
def setRange(min, max)  # 设置范围
def setSingleStep(val)  # 设置单步不长
def setSuffix(suffix)  # 设置后缀,例如m
def value()  # 获取当前的值

def setValue(val)  # 槽函数,设置值为val

def valueChanged(arg__1)  # 信号,值改变触发

使用上述的接口函数已经满足了大部分的需求,但假如我想创建一个 QSpinBox ,范围为 0 ~ 10 ,步长为 2 ,也就是 0 ~ 10 内的偶数,很容易,只需要:

self.spin = QSpinBox()
self.spin.setRange(0, 10)
self.spin.setSingleStep(2)

实际上,用户点击增减的按钮确实是没问题的,但如果用户手动输入,仍然可输入 0 ~ 10 范围内的奇数,就很尴尬~

不过稍微处理一下也是可以解决的,办法就是利用信号与槽,当值改变为奇数的时候,就自动将其修改为临近的值。

# 绑定信号与槽
self.spin.valueChanged.connect(self.standardization)

# 定义槽函数
def standardization(self):
    val = self.spin.value()
    if val % 2 == 1:
        self.spin.setValue(val - 1)  # 如果是奇数就-1

使用 paintEvent 绘制

我目前所了解到的要在 Qt 界面上动态绘制图案,就应该重写 paintEvent() 函数,在其内部编写具体的绘制逻辑,实际上 paintEvent() 是一个槽函数,触发的方式包括窗口大小、位置的改变,以及主动的调用 update()repaint() 接口。

  • repaint() 会在调用的时候立即刷新界面,及时性比较好,但考虑界面并没有什么变化却又一直在刷新的情况,就会产生很大的消耗,严重的情况下会导致界面卡死;
  • update() 是官方也推荐使用的刷新方法,区别是 update() 不会立即刷新界面,而是等到界面真正有变动的时候才刷新一次,可能实时性略差一些,但也满足绝大部分的需求了。
def paintEvent(self, event):
    qp = QPainter()
    qp.begin(self)
    self.draw(qp)  # 绘制的具体逻辑
    qp.end()

处理关闭事件

程序关闭前一般需要做一些处理,比如未结束的线程应当先结束,或者说需要保存一些参数,这些工作就可以放到 closeEvent() 参函数中进行处理~

def closeEvent(self, event):
    if self.thread.is_running():
        self.thread.stop()  # 停止未结束的线程
    self.save_data()  # 保存一些数据
    # 自定义的用于控制程序是否可关闭的bool量
    if self.can_close:
        event.accept()  # 关闭程序
    else:
        event.ignore()  # 拒绝关闭程序

使用自定义字体

注意字体文件的路径就可以了!

QFontDatabase.addApplicationFont('font/SourceCodePro-Regular.ttf')  # 自定义字体
self.log_widget.setStyleSheet('font: 14px Source Code Pro')  # 设置某组件要使用的字体

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!