下面的笔记是24年甚至23年的了……?这里又要捡起 PyQt 了,把这篇写到一半的笔记发出来。
学习 PyQt 有两个作用:
- Krita 插件编程需要 PyQt
- 能够用来编写更多类型的小工具来帮助生产生活实践,当前很多时候都是现写 nodejs 代码,限制多,重用也不方便
so,here we are。参考资料是 。
环境搭建
| pip install pyqt5 pip install pyqt5-tools
|
执行 pyqt5-tools designer
启动 Qt Designer 用来可视化编辑 QML。
Hello World
qt 项目至少需要一个 QWidget
,不然一个窗口都没有。最后一个 QWidget 关闭时,应用关闭。
Hello World 示例如下,避免使用from .. import *
的语法来污染作用域:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import sys from PyQt5.QtWidgets import QApplication, QWidget, QLabel
app = QApplication(sys.argv)
window = QWidget() window.show()
app.exec()
print("application terminated")
|
QMainWindow
QMainWindow 提供了一些和窗口相关的功能,比如设置窗口标题等。下面通过继承来使用 QMainWindow,这是一种抽象,去自包含一些东西。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import sys from PyQt5.QtCore import QSize, Qt from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("My App") self.setFixedSize(QSize(400, 300)) button = QPushButton("Press Me!") self.setCentralWidget(button)
app = QApplication(sys.argv) window = MainWindow() window.show() app.exec()
|
QWidget
总是单个元素,如果需要组织多个元素,需要使用 layout
。
事件循环
TODO
signal & slot
事件循环是类似浏览器 js 那样的机制,用户缩放窗口,点击按钮,按下按键时,都会触发事件,执行相应 js,在 js 中称为事件/事件监听器,在 qt 中称为信号 signal /槽 slot。
触发事件时,事件会丢到事件队列中,一个事件处理线程会按序处理队列中的事件;事件的处理总是单线程的。QApplication
持有事件队列。
下面的例子创建一个包含选中状态的 QPushButton,并绑定了两个 slot,其中第二个 slot 能够获取当前选中状态,这证明 signal 能够携带信息。
需要注意,这里的 QPushButton 的行为类似不受管组件,有自己的状态(isChecked),每次点击时,它会先修改自己的状态,再执行 slot。
可以定义变量在 checked 状态变化时将它保存起来,供外部使用,这是一种单向绑定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import sys from PyQt5.QtCore import QSize, Qt from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton class MainWindow(QMainWindow): def __init__(self): super().__init__() button = QPushButton("Press Me!") button.setCheckable(True) button.clicked.connect(self.on_click) button.clicked.connect(self.another_on_click) self.setCentralWidget(button) def on_click(self): print("btn clicked") def another_on_click(self, checked: bool): print(f"btn {checked=}") app = QApplication(sys.argv) window = MainWindow() window.show() app.exec()
|
下面是另一个例子,其使用 pressed 和 released signal 来实时改变按钮中的文字,clicked signal 在 released 后发送:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import sys from PyQt5.QtCore import QSize, Qt, pyqtSignal from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton class MainWindow(QMainWindow): def __init__(self): super().__init__() button = QPushButton("Press Me!") self.button = button button.pressed.connect(self.on_press) button.released.connect(self.on_release) self.setCentralWidget(button) self.count = 1 def on_press(self): self.button.setText(f'Pressing...{self.count=}') self.count += 1 def on_release(self): self.button.setText("Press Me!") app = QApplication(sys.argv) window = MainWindow() window.show() app.exec()
|
很多时候能直接通过组件之间的方法来连接两个组件,比如下面的示例,它引入了 QVBoxLayout,顾名思义,一个 vertical 的布局,以在主窗口中放置多个组件。
| class MainWindow(QMainWindow): def __init__(self): super().__init__() self.label = QLabel() self.input = QLineEdit() self.input.textChanged.connect(self.label.setText) layout = QVBoxLayout() layout.addWidget(self.input) layout.addWidget(self.label) container = QWidget() container.setLayout(layout) self.setCentralWidget(container)
|
signal 和 slot 的文档在文档中可以查看,比如这里是 QLabel 的文档 https://doc.qt.io/qt-5/qlabel.html#public-slots[QLabel]。
event & eventHandler
qt 中也有 event 的概念,它和 signal 类似,但是是更加底层的,主要是来自操作系统的信息,比如鼠标/键盘抬起按下,滚轮,触屏,数位笔绘制…
不得不怀疑 signal 是对 event 的抽象。在 js 里,signal 和 event 是混在一起的,但这里区分了开来,用户能自己定义 signal,但不能自己定义 event
每个 widget 都可以去处理事件,通过继承父类并实现相应的方法,下面重写了主窗口和两个组件的鼠标按下事件,并展示了冒泡机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| class MainWindow(QMainWindow): def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None: super().mousePressEvent(ev) print(f'QMainWindow clicked, {ev.x()=}, {ev.y()=}') def __init__(self): class MyQLabel(QLabel): def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None: super().mousePressEvent(ev) ev.accept() print(f'QLabel clicked, {ev.x()=}, {ev.y()=}') class MyQLineEdit(QLineEdit): def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None: super().mousePressEvent(ev) ev.ignore() print(f'QLineEdit clicked, {ev.x()=}, {ev.y()=}') super().__init__() self.label = MyQLabel() self.input = MyQLineEdit() self.input.textChanged.connect(self.label.setText) layout = QVBoxLayout() layout.addWidget(self.input) layout.addWidget(self.label) container = QWidget() container.setLayout(layout) self.setCentralWidget(container)
|
事件能够冒泡,比如这里如果不继承 QLabel,在 MainWindow 里重载 mouseMoveEvent,则鼠标在 label 处移动会触发 MainWindow 的事件。
QEventLoop 异步转同步
QEventLoop
很适用于“异步转同步”,它看上去会阻塞整个线程,有点吓人,但其实并非如此——在执行QEventLoop.exec
时,会启动一个新的事件循环,去替代原事件循环,直到自己exit;而现象上,就是当前“程序流”而非线程阻塞在这里,线程自己仍旧能处理其它事件,实现了一种类似于协程的效果。
这种替代非常奇特——它是自旋——就像(父循环的)递归调用。QThread.currentThread().loopLevel()
可以获取当前循环层级来证明这一点。实际上,打印当前调用栈,能够看到子循环的exec在父循环的exec上面,而循环层级过深(100层左右)时程序会直接退出,猜测是因为栈溢出。
下面是QEventLoop的一种常见的使用模式:
| loop = QEventLoop() def cb(): loop.exit() some_async_fn(cb) loop.exec()
|
事实上可以直接利用QEventLoop实现Promise,但这……感觉会引起滥用。
Widget |
What it does |
QLabel |
label |
QPushButton |
按钮 |
QRadioButton |
单选框集合 |
QComboBox |
下拉单选框 |
QCheckbox |
复选框 |
QSlider |
滑条 |
QLineEdit |
文字编辑 |
QDateEdit |
日期编辑框 |
QDateTimeEdit |
日期-时间编辑框 |
QTimeEdit |
时间编辑框 |
QSpinBox |
整数编辑框 |
QDoubleSpinbox |
浮点数编辑框 |
QDial |
旋钮 |
QLCDNumber |
LCD 显示数字 |
QProgressBar |
进度条 |
QFontComboBox |
字体选择 |
标签 QLabel
QLabel 可以用来显示文字和图片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| class MainWindow(QMainWindow): def __init__(self): super().__init__() layout = QVBoxLayout() example0 = QLabel("Hello我是谁world") example1 = QLabel("多就是好,大就是强!") font = example1.font() font.setPointSize(30) font.setBold(True) font.setItalic(True) example1.setFont(font)
example2 = QLabel("Position Zero") example2.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter)
example3 = QLabel("Position Zero") example3.setAlignment(Qt.AlignCenter)
example4 = QLabel() example4.setPixmap(QPixmap("81280af55a7a908e2f8c34835da9fc18.jpg")) example4.setScaledContents(True)
layout.addWidget(example0) layout.addWidget(example1) layout.addWidget(example2) layout.addWidget(example3) layout.addWidget(example4) widget = QWidget() widget.setLayout(layout) self.setCentralWidget(widget)
|
复选框 QCheckBox
复选框,Qt的复选框自带label,且允许一种中间状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class MainWindow(QMainWindow): def __init__(self): super().__init__() layout = QVBoxLayout()
def print_current_state(state: int): print('current state:', state, type(state))
example0 = QCheckBox("Remember Me") example0.setCheckState(Qt.Checked) example0.stateChanged.connect(print_current_state)
example1 = QCheckBox("Remember Me") example1.setTristate(True) example1.stateChanged.connect(print_current_state)
layout.addWidget(example0) layout.addWidget(example1) widget = QWidget() widget.setLayout(layout) self.setCentralWidget(widget)
|
下拉单选框 QComboBox
下拉单选框,Qt允许编辑下拉框。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class MainWindow(QMainWindow): def __init__(self): super().__init__() layout = QVBoxLayout()
def index_change(i: int): print('index changed: ', i) def text_change(text: str): print('text changed: ', text)
example0 = QComboBox() example0.addItems(["One", "Two", "Three", 'One']) example0.currentIndexChanged.connect(index_change) example0.currentTextChanged.connect(text_change)
example1 = QComboBox() example1.addItems(["One", "Two", "Three", 'One']) example1.setEditable(True) example1.setInsertPolicy(QComboBox.NoInsert) example1.currentIndexChanged.connect(index_change) example1.currentTextChanged.connect(text_change)
layout.addWidget(example0) layout.addWidget(example1) widget = QWidget() widget.setLayout(layout) self.setCentralWidget(widget)
|
html中没有类似的组件,这玩意长的就像下拉单选框把框的内容直接放外面一样。
TODO
QTextEdit
TODO
QSpinBox and QDoubleSpinBox
TODO
QSlider
Layout
TODO
TODO
运行时操作
TODO