Revert "GITBOOK-7: change request with no subject merged in GitBook"

This reverts commit 685b4f07dcf7faf59c8b902a04b4e0e0ed841d07.
This commit is contained in:
maicss 2023-11-05 21:17:16 +08:00
parent 2a12e7e798
commit c6c7938774
102 changed files with 10513 additions and 0 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -1,3 +1,31 @@
# Table of contents
* [欢迎](README.md)
* [pyqt6](pyqt6/README.md)
* [开始](pyqt6/index.md)
* [PyQt6 简介](pyqt6/introduction.md)
* [PyQt6 日期和时间](pyqt6/datetime.md)
* [PyQt6 的第一个程序](pyqt6/firstprograms.md)
* [PyQt6 的菜单和工具栏](pyqt6/menustoolbars.md)
* [PyQt6 的布局管理](pyqt6/layout.md)
* [PyQt6 事件和信号](pyqt6/eventssignals.md)
* [PyQt6 的对话框](pyqt6/dialogs.md)
* [PyQt6 组件(一)](pyqt6/widgets.md)
* [PyQt6 组件(二)](pyqt6/widgets2.md)
* [PyQt6 的拖拽操作](pyqt6/dragdrop.md)
* [PyQt6 的绘制](pyqt6/painting.md)
* [PyQt6 自定义部件](pyqt6/customwidgets.md)
* [俄罗斯方块](pyqt6/tetris.md)
* [pyqt5](pyqt5/README.md)
* [介绍](pyqt5/index.md)
* [Hello World](pyqt5/hello\_world.md)
* [菜单和工具栏](pyqt5/cai-dan-he-gong-ju-lan.md)
* [布局管理](pyqt5/bu-ju-guan-li.md)
* [事件和信号](pyqt5/shi-jian-he-xin-hao.md)
* [对话框](pyqt5/dui-hua-kuang.md)
* [控件(1)](pyqt5/kong-jian-1.md)
* [控件(2)](pyqt5/kong-jian-2.md)
* [拖拽](pyqt5/tuo-zhuai.md)
* [绘图](pyqt5/hui-tu.md)
* [自定义组件](pyqt5/zi-ding-yi-zu-jian.md)
* [俄罗斯方块游戏](pyqt5/e-luo-si-fang-kuai-you-xi.md)

View File

@ -0,0 +1,2 @@
# pyqt5

View File

@ -0,0 +1,367 @@
# 布局管理
在一个GUI程序里布局是一个很重要的方面。布局就是如何管理应用中的元素和窗口。有两种方式可以搞定绝对定位和PyQt5的layout类
## 绝对定位
每个程序都是以像素为单位区分元素的位置,衡量元素的大小。所以我们完全可以使用绝对定位搞定每个元素和窗口的位置。但是这也有局限性:
* 元素不会随着我们更改窗口的位置和大小而变化。
* 不能适用于不同的平台和不同分辨率的显示器
* 更改应用字体大小会破坏布局
* 如果我们决定重构这个应用,需要全部计算一下每个元素的位置和大小
下面这个就是绝对定位的应用
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This example shows three labels on a window
using absolute positioning.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QWidget, QLabel, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
lbl1 = QLabel('Zetcode', self)
lbl1.move(15, 10)
lbl2 = QLabel('tutorials', self)
lbl2.move(35, 40)
lbl3 = QLabel('for programmers', self)
lbl3.move(55, 70)
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Absolute')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
我们使用move\(\)方法定位了每一个元素使用x、y坐标。x、y坐标的原点是程序的左上角。
```text
lbl1 = QLabel('Zetcode', self)
lbl1.move(15, 10)
```
这个元素的左上角就在这个程序的左上角开始的\(15, 10\)的位置。
程序展示:
![Absolute positioning](images/3-absolute.png)
## 盒布局
使用盒布局能让程序具有更强的适应性。这个才是布局一个应用的更合适的方式。`QHBoxLayout``QVBoxLayout`是基本的布局类,分别是水平布局和垂直布局。
如果我们需要把两个按钮放在程序的右下角,创建这样的布局,我们只需要一个水平布局加一个垂直布局的盒子就可以了。再用弹性布局增加一点间隙。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we position two push
buttons in the bottom-right corner
of the window.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import (QWidget, QPushButton,
QHBoxLayout, QVBoxLayout, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
okButton = QPushButton("OK")
cancelButton = QPushButton("Cancel")
hbox = QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(okButton)
hbox.addWidget(cancelButton)
vbox = QVBoxLayout()
vbox.addStretch(1)
vbox.addLayout(hbox)
self.setLayout(vbox)
self.setGeometry(300, 300, 300, 150)
self.setWindowTitle('Buttons')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
上面的例子完成了在应用的右下角放了两个按钮的需求。当改变窗口大小的时候,它们能依然保持在相对的位置。我们同时使用了`QHBoxLayout``QVBoxLayout`
```text
okButton = QPushButton("OK")
cancelButton = QPushButton("Cancel")
```
这是创建了两个按钮。
```text
hbox = QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(okButton)
hbox.addWidget(cancelButton)
```
创建一个水平布局并增加弹性空间和两个按钮。stretch函数在两个按钮前面增加了一块弹性空间它会将按钮挤到窗口的右边。
```text
vbox = QVBoxLayout()
vbox.addStretch(1)
vbox.addLayout(hbox)
```
为了布局需要,我们把这个水平布局放到了一个垂直布局盒里面。弹性元素会把水平布局挤到窗口的下边。
```text
self.setLayout(vbox)
```
最后,我们就得到了我们想要的布局。
程序预览:
![buttons](images/3-buttons.png)
## 栅格布局
最常用的还是栅格布局了。这种布局是把窗口分为行和列。创建和使用栅格布局需要使用QGridLayout模块。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we create a skeleton
of a calculator using a QGridLayout.
author: Jan Bodnar
website: zetcode.com
last edited: January 2015
"""
import sys
from PyQt5.QtWidgets import (QWidget, QGridLayout,
QPushButton, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
grid = QGridLayout()
self.setLayout(grid)
names = ['Cls', 'Bck', '', 'Close',
'7', '8', '9', '/',
'4', '5', '6', '*',
'1', '2', '3', '-',
'0', '.', '=', '+']
positions = [(i,j) for i in range(5) for j in range(4)]
for position, name in zip(positions, names):
if name == '':
continue
button = QPushButton(name)
grid.addWidget(button, *position)
self.move(300, 150)
self.setWindowTitle('Calculator')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
这个例子里,我们创建了栅格化的按钮。
```text
grid = QGridLayout()
self.setLayout(grid)
```
创建一个QGridLayout实例并把它放到程序窗口里。
```text
names = ['Cls', 'Bck', '', 'Close',
'7', '8', '9', '/',
'4', '5', '6', '*',
'1', '2', '3', '-',
'0', '.', '=', '+']
```
这是我们将要使用的按钮的名称。
```text
positions = [(i,j) for i in range(5) for j in range(4)]
```
创建按钮位置列表。
```text
for position, name in zip(positions, names):
if name == '':
continue
button = QPushButton(name)
grid.addWidget(button, *position)
```
创建按钮,并使用`addWidget()`方法把按钮放到布局里面。
程序预览:
![Calculator skeleton](images/3-calculator.png)
## 制作提交反馈信息的布局
组件能跨列和跨行展示,这个例子里,我们就试试这个功能。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we create a more
complicated window layout using
the QGridLayout manager.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import (QWidget, QLabel, QLineEdit,
QTextEdit, QGridLayout, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
title = QLabel('Title')
author = QLabel('Author')
review = QLabel('Review')
titleEdit = QLineEdit()
authorEdit = QLineEdit()
reviewEdit = QTextEdit()
grid = QGridLayout()
grid.setSpacing(10)
grid.addWidget(title, 1, 0)
grid.addWidget(titleEdit, 1, 1)
grid.addWidget(author, 2, 0)
grid.addWidget(authorEdit, 2, 1)
grid.addWidget(review, 3, 0)
grid.addWidget(reviewEdit, 3, 1, 5, 1)
self.setLayout(grid)
self.setGeometry(300, 300, 350, 300)
self.setWindowTitle('Review')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
我们创建了一个有三个标签的窗口。两个行编辑和一个文版编辑,这是用`QGridLayout`模块搞定的。
```text
grid = QGridLayout()
grid.setSpacing(10)
```
创建标签之间的空间。
```text
grid.addWidget(reviewEdit, 3, 1, 5, 1)
```
我们可以指定组件的跨行和跨列的大小。这里我们指定这个元素跨5行显示。
程序预览:
![review example](images/3-review.png)

View File

@ -0,0 +1,539 @@
# 菜单和工具栏
这个章节,我们会创建状态栏、菜单和工具栏。菜单是一组位于菜单栏的命令。工具栏是应用的一些常用工具按钮。状态栏显示一些状态信息,通常在应用的底部。
## 主窗口
`QMainWindow`提供了主窗口的功能,使用它能创建一些简单的状态栏、工具栏和菜单栏。
主窗口是下面这些窗口的合称,所以教程在最下方。
## 状态栏
状态栏是用来显示应用的状态信息的组件。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This program creates a statusbar.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QMainWindow, QApplication
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.statusBar().showMessage('Ready')
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Statusbar')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
状态栏是由QMainWindow创建的。
```text
self.statusBar().showMessage('Ready')
```
调用`QtGui.QMainWindow`类的`statusBar()`方法,创建状态栏。第一次调用会创建一个状态栏,而再次调用会返回一个状态栏对象。`showMessage()`方法在状态栏上显示一条信息。
程序预览: ![status](images/2-status.png)
## 菜单栏
菜单栏是非常常用的。是一组命令的集合Mac OS下状态栏的显示不一样为得到最相似的外观我们可以增加一行语句`menubar.setNativeMenuBar(False)`\)。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This program creates a menubar. The
menubar has one menu with an exit action.
Author: Jan Bodnar
Website: zetcode.com
Last edited: January 2017
"""
import sys
from PyQt5.QtWidgets import QMainWindow, QAction, qApp, QApplication
from PyQt5.QtGui import QIcon
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
exitAct = QAction(QIcon('exit.png'), '&Exit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.setStatusTip('Exit application')
exitAct.triggered.connect(qApp.quit)
self.statusBar()
menubar = self.menuBar()
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(exitAct)
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Simple menu')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
在上面的示例中,我们创建了只有一个命令的菜单栏,这个命令就是终止应用。同时也创建了一个状态栏。而且还能使用快捷键`Ctrl+Q`退出应用。
```text
exitAct = QAction(QIcon('exit.png'), '&Exit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.setStatusTip('Exit application')
```
`QAction`是菜单栏、工具栏或者快捷键的动作的组合。上面三行中前两行创建了一个图标、一个exit的标签和一个快捷键组合都执行了一个动作第三行创建了一个状态栏当鼠标悬停在菜单栏的时候能显示当前状态。
```text
exitAct.triggered.connect(qApp.quit)
```
当执行这个指定的动作时,就触发了一个事件。这个事件跟`QApplication的quit()`行为相关联,所以这个动作就能终止这个应用。
```text
menubar = self.menuBar()
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(exitAct)
```
`menuBar()`创建菜单栏。这里创建了一个菜单栏,并用`addMenu()`在上面添加了一个file菜单`addAction()`关联了点击退出应用的事件。
程序预览: ![menu](images/2-menu.png)
## 子菜单
子菜单是嵌套在菜单里面的二级或者三级等的菜单。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This program creates a submenu.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QMainWindow, QAction, QMenu, QApplication
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
menubar = self.menuBar()
fileMenu = menubar.addMenu('File')
impMenu = QMenu('Import', self)
impAct = QAction('Import mail', self)
impMenu.addAction(impAct)
newAct = QAction('New', self)
fileMenu.addAction(newAct)
fileMenu.addMenu(impMenu)
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Submenu')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
这个例子里有两个子菜单一个在File菜单下面一个在File的Import下面。
```text
impMenu = QMenu('Import', self)
```
使用`QMenu`创建一个新菜单。
```text
impAct = QAction('Import mail', self)
impMenu.addAction(impAct)
```
使用`addAction()`添加一个动作。
程序预览: ![submenu](images/2-submenu.png)
## 勾选菜单
下面是一个勾选菜单的例子
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This program creates a checkable menu.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QMainWindow, QAction, QApplication
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.statusbar = self.statusBar()
self.statusbar.showMessage('Ready')
menubar = self.menuBar()
viewMenu = menubar.addMenu('View')
viewStatAct = QAction('View statusbar', self, checkable=True)
viewStatAct.setStatusTip('View statusbar')
viewStatAct.setChecked(True)
viewStatAct.triggered.connect(self.toggleMenu)
viewMenu.addAction(viewStatAct)
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Check menu')
self.show()
def toggleMenu(self, state):
if state:
self.statusbar.show()
else:
self.statusbar.hide()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
本例创建了一个行为菜单。这个行为/动作能切换状态栏显示或者隐藏。
```text
viewStatAct = QAction('View statusbar', self, checkable=True)
```
`checkable`选项创建一个能选中的菜单。
```text
viewStatAct.setChecked(True)
```
默认设置为选中状态。
```text
def toggleMenu(self, state):
if state:
self.statusbar.show()
else:
self.statusbar.hide()
```
依据选中状态切换状态栏的显示与否。 程序预览:
![checkmenu](images/2-checkmenu.png)
## 右键菜单
右键菜单也叫弹出框是在某些场合下显示的一组命令。例如Opera浏览器里网页上的右键菜单里会有刷新返回或者查看页面源代码。如果在工具栏上右键会得到一个不同的用来管理工具栏的菜单。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This program creates a context menu.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QMainWindow, qApp, QMenu, QApplication
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Context menu')
self.show()
def contextMenuEvent(self, event):
cmenu = QMenu(self)
newAct = cmenu.addAction("New")
opnAct = cmenu.addAction("Open")
quitAct = cmenu.addAction("Quit")
action = cmenu.exec_(self.mapToGlobal(event.pos()))
if action == quitAct:
qApp.quit()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
还是使用`contextMenuEvent()`方法实现这个菜单。
```text
action = cmenu.exec_(self.mapToGlobal(event.pos()))
```
使用`exec_()`方法显示菜单。从鼠标右键事件对象中获得当前坐标。`mapToGlobal()`方法把当前组件的相对坐标转换为窗口window的绝对坐标。
```text
if action == quitAct:
qApp.quit()
```
如果右键菜单里触发了事件,也就触发了退出事件,执行关闭菜单行为。
程序预览:
![contextmenu](images/2-contextmenu.png)
## 工具栏
菜单栏包含了所有的命令,工具栏就是常用的命令的集合。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This program creates a toolbar.
The toolbar has one action, which
terminates the application, if triggered.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QMainWindow, QAction, qApp, QApplication
from PyQt5.QtGui import QIcon
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
exitAct = QAction(QIcon('exit24.png'), 'Exit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.triggered.connect(qApp.quit)
self.toolbar = self.addToolBar('Exit')
self.toolbar.addAction(exitAct)
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Toolbar')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
上面的例子中,我们创建了一个工具栏。这个工具栏只有一个退出应用的动作。
```text
exitAct = QAction(QIcon('exit24.png'), 'Exit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.triggered.connect(qApp.quit)
```
和上面的菜单栏差不多,这里使用了一个行为对象,这个对象绑定了一个标签,一个图标和一个快捷键。这些行为被触发的时候,会调用`QtGui.QMainWindow`的quit方法退出应用。
```text
self.toolbar = self.addToolBar('Exit')
self.toolbar.addAction(exitAct)
```
`addToolBar()`创建工具栏,并用`addAction()`将动作对象添加到工具栏。
程序预览:
![toolbar](images/2-toolbar.png)
## 主窗口
主窗口就是上面三种栏目的总称,现在我们把上面的三种栏在一个应用里展示出来。
> 首先要自己弄个小图标命名为exit24.png
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This program creates a skeleton of
a classic GUI application with a menubar,
toolbar, statusbar, and a central widget.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QMainWindow, QTextEdit, QAction, QApplication
from PyQt5.QtGui import QIcon
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
textEdit = QTextEdit()
self.setCentralWidget(textEdit)
exitAct = QAction(QIcon('exit24.png'), 'Exit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.setStatusTip('Exit application')
exitAct.triggered.connect(self.close)
self.statusBar()
menubar = self.menuBar()
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(exitAct)
toolbar = self.addToolBar('Exit')
toolbar.addAction(exitAct)
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Main window')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
上面的代码创建了一个很经典的菜单框架,有右键菜单,工具栏和状态栏。
```text
textEdit = QTextEdit()
self.setCentralWidget(textEdit)
```
这里创建了一个文本编辑区域,并把它放在`QMainWindow`的中间区域。这个组件会占满所有剩余的区域。
程序预览:
![mainwindow](images/2-mainwindow.png)

View File

@ -0,0 +1,373 @@
# 对话框
对话框是一个现代GUI应用不可或缺的一部分。对话是两个人之间的交流对话框就是人与电脑之间的对话。对话框用来输入数据修改数据修改应用设置等等。
## 输入文字
`QInputDialog`提供了一个简单方便的对话框,可以输入字符串,数字或列表。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we receive data from
a QInputDialog dialog.
Aauthor: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QWidget, QPushButton, QLineEdit,
QInputDialog, QApplication)
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.btn = QPushButton('Dialog', self)
self.btn.move(20, 20)
self.btn.clicked.connect(self.showDialog)
self.le = QLineEdit(self)
self.le.move(130, 22)
self.setGeometry(300, 300, 290, 150)
self.setWindowTitle('Input dialog')
self.show()
def showDialog(self):
text, ok = QInputDialog.getText(self, 'Input Dialog',
'Enter your name:')
if ok:
self.le.setText(str(text))
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
这个示例有一个按钮和一个输入框,点击按钮显示对话框,输入的文本会显示在输入框里。
```text
text, ok = QInputDialog.getText(self, 'Input Dialog',
'Enter your name:')
```
这是显示一个输入框的代码。第一个参数是输入框的标题第二个参数是输入框的占位符。对话框返回输入内容和一个布尔值如果点击的是OK按钮布尔值就返回True。
```text
if ok:
self.le.setText(str(text))
```
把得到的字符串放到输入框里。
程序展示:
![input dialog](images/5-inputdialog.png)
## 选取颜色
QColorDialog提供颜色的选择。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we select a color value
from the QColorDialog and change the background
color of a QFrame widget.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QWidget, QPushButton, QFrame,
QColorDialog, QApplication)
from PyQt5.QtGui import QColor
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
col = QColor(0, 0, 0)
self.btn = QPushButton('Dialog', self)
self.btn.move(20, 20)
self.btn.clicked.connect(self.showDialog)
self.frm = QFrame(self)
self.frm.setStyleSheet("QWidget { background-color: %s }"
% col.name())
self.frm.setGeometry(130, 22, 100, 100)
self.setGeometry(300, 300, 250, 180)
self.setWindowTitle('Color dialog')
self.show()
def showDialog(self):
col = QColorDialog.getColor()
if col.isValid():
self.frm.setStyleSheet("QWidget { background-color: %s }"
% col.name())
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
例子里有一个按钮和一个`QFrame`,默认的背景颜色为黑色,我们可以使用`QColorDialog`改变背景颜色。
```text
col = QColor(0, 0, 0)
```
初始化`QtGui.QFrame`的背景颜色。
```text
col = QColorDialog.getColor()
```
弹出一个`QColorDialog`对话框。
```text
if col.isValid():
self.frm.setStyleSheet("QWidget { background-color: %s }"
% col.name())
```
我们可以预览颜色,如果点击取消按钮,没有颜色值返回,如果颜色是我们想要的,就从取色框里选择这个颜色。
程序展示:
![color dialog](images/5-colordialog.png)
## 选择字体
`QFontDialog`能做字体的选择。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we select a font name
and change the font of a label.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QPushButton,
QSizePolicy, QLabel, QFontDialog, QApplication)
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
vbox = QVBoxLayout()
btn = QPushButton('Dialog', self)
btn.setSizePolicy(QSizePolicy.Fixed,
QSizePolicy.Fixed)
btn.move(20, 20)
vbox.addWidget(btn)
btn.clicked.connect(self.showDialog)
self.lbl = QLabel('Knowledge only matters', self)
self.lbl.move(130, 20)
vbox.addWidget(self.lbl)
self.setLayout(vbox)
self.setGeometry(300, 300, 250, 180)
self.setWindowTitle('Font dialog')
self.show()
def showDialog(self):
font, ok = QFontDialog.getFont()
if ok:
self.lbl.setFont(font)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
我们创建了一个有一个按钮和一个标签的`QFontDialog`的对话框,我们可以使用这个功能修改字体样式。
```text
font, ok = QFontDialog.getFont()
```
弹出一个字体选择对话框。`getFont()`方法返回一个字体名称和状态信息。状态信息有OK和其他两种。
```text
if ok:
self.label.setFont(font)
```
如果点击OK标签的字体就会随之更改。
程序展示:
![font dialog](images/5-fontdialog.png)
## 选择文件
`QFileDialog`给用户提供文件或者文件夹选择的功能。能打开和保存文件。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we select a file with a
QFileDialog and display its contents
in a QTextEdit.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QMainWindow, QTextEdit,
QAction, QFileDialog, QApplication)
from PyQt5.QtGui import QIcon
import sys
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.textEdit = QTextEdit()
self.setCentralWidget(self.textEdit)
self.statusBar()
openFile = QAction(QIcon('open.png'), 'Open', self)
openFile.setShortcut('Ctrl+O')
openFile.setStatusTip('Open new File')
openFile.triggered.connect(self.showDialog)
menubar = self.menuBar()
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(openFile)
self.setGeometry(300, 300, 350, 300)
self.setWindowTitle('File dialog')
self.show()
def showDialog(self):
fname = QFileDialog.getOpenFileName(self, 'Open file', '/home')
if fname[0]:
f = open(fname[0], 'r')
with f:
data = f.read()
self.textEdit.setText(data)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
本例中有一个菜单栏,一个置中的文本编辑框,一个状态栏。点击菜单栏选项会弹出一个`QtGui.QFileDialog`对话框,在这个对话框里,你能选择文件,然后文件的内容就会显示在文本编辑框里。
```text
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
```
这里设置了一个文本编辑框,文本编辑框是基于`QMainWindow`组件的。
```text
fname = QFileDialog.getOpenFileName(self, 'Open file', '/home')
```
弹出`QFileDialog`窗口。`getOpenFileName()`方法的第一个参数是说明文字,第二个参数是默认打开的文件夹路径。默认情况下显示所有类型的文件。
```text
if fname[0]:
f = open(fname[0], 'r')
with f:
data = f.read()
self.textEdit.setText(data)
```
读取选中的文件并显示在文本编辑框内但是打开HTML文件时是渲染后的结果
程序展示:
![file Dialog](images/5-filedialog.png)

View File

@ -0,0 +1,876 @@
# 俄罗斯方块游戏
本章我们要制作一个俄罗斯方块游戏。
## Tetris
> 译注:称呼:方块是由四个小方格组成的
俄罗斯方块游戏是世界上最流行的游戏之一。是由一名叫Alexey Pajitnov的俄罗斯程序员在1985年制作的从那时起这个游戏就风靡了各个游戏平台。
俄罗斯方块归类为下落块迷宫游戏。游戏有7个基本形状S、Z、T、L、反向L、直线、方块每个形状都由4个方块组成方块最终都会落到屏幕底部。所以玩家通过控制形状的左右位置和旋转让每个形状都以合适的位置落下如果有一行全部被方块填充这行就会消失并且得分。游戏结束的条件是有形状接触到了屏幕顶部。
方块展示:
![tetrominoes](images/11-tetrominoes.png)
PyQt5是专门为创建图形界面产生的里面一些专门为制作游戏而开发的组件所以PyQt5是能制作小游戏的。
制作电脑游戏也是提高自己编程能力的一种很好的方式。
## 开发
没有图片,所以就自己用绘画画出来几个图形。每个游戏里都有数学模型的,这个也是。
开工之前:
* 用`QtCore.QBasicTimer()`创建一个游戏循环
* 模型是一直下落的
* 模型的运动是以小块为基础单位的,不是按像素
* 从数学意义上来说,模型就是就是一串数字而已
代码由四个类组成Tetris, Board, Tetrominoe和Shape。Tetris类创建游戏Board是游戏主要逻辑。Tetrominoe包含了所有的砖块Shape是所有砖块的代码。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This is a Tetris game clone.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import QMainWindow, QFrame, QDesktopWidget, QApplication
from PyQt5.QtCore import Qt, QBasicTimer, pyqtSignal
from PyQt5.QtGui import QPainter, QColor
import sys, random
class Tetris(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
'''initiates application UI'''
self.tboard = Board(self)
self.setCentralWidget(self.tboard)
self.statusbar = self.statusBar()
self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)
self.tboard.start()
self.resize(180, 380)
self.center()
self.setWindowTitle('Tetris')
self.show()
def center(self):
'''centers the window on the screen'''
screen = QDesktopWidget().screenGeometry()
size = self.geometry()
self.move((screen.width()-size.width())/2,
(screen.height()-size.height())/2)
class Board(QFrame):
msg2Statusbar = pyqtSignal(str)
BoardWidth = 10
BoardHeight = 22
Speed = 300
def __init__(self, parent):
super().__init__(parent)
self.initBoard()
def initBoard(self):
'''initiates board'''
self.timer = QBasicTimer()
self.isWaitingAfterLine = False
self.curX = 0
self.curY = 0
self.numLinesRemoved = 0
self.board = []
self.setFocusPolicy(Qt.StrongFocus)
self.isStarted = False
self.isPaused = False
self.clearBoard()
def shapeAt(self, x, y):
'''determines shape at the board position'''
return self.board[(y * Board.BoardWidth) + x]
def setShapeAt(self, x, y, shape):
'''sets a shape at the board'''
self.board[(y * Board.BoardWidth) + x] = shape
def squareWidth(self):
'''returns the width of one square'''
return self.contentsRect().width() // Board.BoardWidth
def squareHeight(self):
'''returns the height of one square'''
return self.contentsRect().height() // Board.BoardHeight
def start(self):
'''starts game'''
if self.isPaused:
return
self.isStarted = True
self.isWaitingAfterLine = False
self.numLinesRemoved = 0
self.clearBoard()
self.msg2Statusbar.emit(str(self.numLinesRemoved))
self.newPiece()
self.timer.start(Board.Speed, self)
def pause(self):
'''pauses game'''
if not self.isStarted:
return
self.isPaused = not self.isPaused
if self.isPaused:
self.timer.stop()
self.msg2Statusbar.emit("paused")
else:
self.timer.start(Board.Speed, self)
self.msg2Statusbar.emit(str(self.numLinesRemoved))
self.update()
def paintEvent(self, event):
'''paints all shapes of the game'''
painter = QPainter(self)
rect = self.contentsRect()
boardTop = rect.bottom() - Board.BoardHeight * self.squareHeight()
for i in range(Board.BoardHeight):
for j in range(Board.BoardWidth):
shape = self.shapeAt(j, Board.BoardHeight - i - 1)
if shape != Tetrominoe.NoShape:
self.drawSquare(painter,
rect.left() + j * self.squareWidth(),
boardTop + i * self.squareHeight(), shape)
if self.curPiece.shape() != Tetrominoe.NoShape:
for i in range(4):
x = self.curX + self.curPiece.x(i)
y = self.curY - self.curPiece.y(i)
self.drawSquare(painter, rect.left() + x * self.squareWidth(),
boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
self.curPiece.shape())
def keyPressEvent(self, event):
'''processes key press events'''
if not self.isStarted or self.curPiece.shape() == Tetrominoe.NoShape:
super(Board, self).keyPressEvent(event)
return
key = event.key()
if key == Qt.Key_P:
self.pause()
return
if self.isPaused:
return
elif key == Qt.Key_Left:
self.tryMove(self.curPiece, self.curX - 1, self.curY)
elif key == Qt.Key_Right:
self.tryMove(self.curPiece, self.curX + 1, self.curY)
elif key == Qt.Key_Down:
self.tryMove(self.curPiece.rotateRight(), self.curX, self.curY)
elif key == Qt.Key_Up:
self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY)
elif key == Qt.Key_Space:
self.dropDown()
elif key == Qt.Key_D:
self.oneLineDown()
else:
super(Board, self).keyPressEvent(event)
def timerEvent(self, event):
'''handles timer event'''
if event.timerId() == self.timer.timerId():
if self.isWaitingAfterLine:
self.isWaitingAfterLine = False
self.newPiece()
else:
self.oneLineDown()
else:
super(Board, self).timerEvent(event)
def clearBoard(self):
'''clears shapes from the board'''
for i in range(Board.BoardHeight * Board.BoardWidth):
self.board.append(Tetrominoe.NoShape)
def dropDown(self):
'''drops down a shape'''
newY = self.curY
while newY > 0:
if not self.tryMove(self.curPiece, self.curX, newY - 1):
break
newY -= 1
self.pieceDropped()
def oneLineDown(self):
'''goes one line down with a shape'''
if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
self.pieceDropped()
def pieceDropped(self):
'''after dropping shape, remove full lines and create new shape'''
for i in range(4):
x = self.curX + self.curPiece.x(i)
y = self.curY - self.curPiece.y(i)
self.setShapeAt(x, y, self.curPiece.shape())
self.removeFullLines()
if not self.isWaitingAfterLine:
self.newPiece()
def removeFullLines(self):
'''removes all full lines from the board'''
numFullLines = 0
rowsToRemove = []
for i in range(Board.BoardHeight):
n = 0
for j in range(Board.BoardWidth):
if not self.shapeAt(j, i) == Tetrominoe.NoShape:
n = n + 1
if n == 10:
rowsToRemove.append(i)
rowsToRemove.reverse()
for m in rowsToRemove:
for k in range(m, Board.BoardHeight):
for l in range(Board.BoardWidth):
self.setShapeAt(l, k, self.shapeAt(l, k + 1))
numFullLines = numFullLines + len(rowsToRemove)
if numFullLines > 0:
self.numLinesRemoved = self.numLinesRemoved + numFullLines
self.msg2Statusbar.emit(str(self.numLinesRemoved))
self.isWaitingAfterLine = True
self.curPiece.setShape(Tetrominoe.NoShape)
self.update()
def newPiece(self):
'''creates a new shape'''
self.curPiece = Shape()
self.curPiece.setRandomShape()
self.curX = Board.BoardWidth // 2 + 1
self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
if not self.tryMove(self.curPiece, self.curX, self.curY):
self.curPiece.setShape(Tetrominoe.NoShape)
self.timer.stop()
self.isStarted = False
self.msg2Statusbar.emit("Game over")
def tryMove(self, newPiece, newX, newY):
'''tries to move a shape'''
for i in range(4):
x = newX + newPiece.x(i)
y = newY - newPiece.y(i)
if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
return False
if self.shapeAt(x, y) != Tetrominoe.NoShape:
return False
self.curPiece = newPiece
self.curX = newX
self.curY = newY
self.update()
return True
def drawSquare(self, painter, x, y, shape):
'''draws a square of a shape'''
colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]
color = QColor(colorTable[shape])
painter.fillRect(x + 1, y + 1, self.squareWidth() - 2,
self.squareHeight() - 2, color)
painter.setPen(color.lighter())
painter.drawLine(x, y + self.squareHeight() - 1, x, y)
painter.drawLine(x, y, x + self.squareWidth() - 1, y)
painter.setPen(color.darker())
painter.drawLine(x + 1, y + self.squareHeight() - 1,
x + self.squareWidth() - 1, y + self.squareHeight() - 1)
painter.drawLine(x + self.squareWidth() - 1,
y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1)
class Tetrominoe(object):
NoShape = 0
ZShape = 1
SShape = 2
LineShape = 3
TShape = 4
SquareShape = 5
LShape = 6
MirroredLShape = 7
class Shape(object):
coordsTable = (
((0, 0), (0, 0), (0, 0), (0, 0)),
((0, -1), (0, 0), (-1, 0), (-1, 1)),
((0, -1), (0, 0), (1, 0), (1, 1)),
((0, -1), (0, 0), (0, 1), (0, 2)),
((-1, 0), (0, 0), (1, 0), (0, 1)),
((0, 0), (1, 0), (0, 1), (1, 1)),
((-1, -1), (0, -1), (0, 0), (0, 1)),
((1, -1), (0, -1), (0, 0), (0, 1))
)
def __init__(self):
self.coords = [[0,0] for i in range(4)]
self.pieceShape = Tetrominoe.NoShape
self.setShape(Tetrominoe.NoShape)
def shape(self):
'''returns shape'''
return self.pieceShape
def setShape(self, shape):
'''sets a shape'''
table = Shape.coordsTable[shape]
for i in range(4):
for j in range(2):
self.coords[i][j] = table[i][j]
self.pieceShape = shape
def setRandomShape(self):
'''chooses a random shape'''
self.setShape(random.randint(1, 7))
def x(self, index):
'''returns x coordinate'''
return self.coords[index][0]
def y(self, index):
'''returns y coordinate'''
return self.coords[index][1]
def setX(self, index, x):
'''sets x coordinate'''
self.coords[index][0] = x
def setY(self, index, y):
'''sets y coordinate'''
self.coords[index][1] = y
def minX(self):
'''returns min x value'''
m = self.coords[0][0]
for i in range(4):
m = min(m, self.coords[i][0])
return m
def maxX(self):
'''returns max x value'''
m = self.coords[0][0]
for i in range(4):
m = max(m, self.coords[i][0])
return m
def minY(self):
'''returns min y value'''
m = self.coords[0][1]
for i in range(4):
m = min(m, self.coords[i][1])
return m
def maxY(self):
'''returns max y value'''
m = self.coords[0][1]
for i in range(4):
m = max(m, self.coords[i][1])
return m
def rotateLeft(self):
'''rotates shape to the left'''
if self.pieceShape == Tetrominoe.SquareShape:
return self
result = Shape()
result.pieceShape = self.pieceShape
for i in range(4):
result.setX(i, self.y(i))
result.setY(i, -self.x(i))
return result
def rotateRight(self):
'''rotates shape to the right'''
if self.pieceShape == Tetrominoe.SquareShape:
return self
result = Shape()
result.pieceShape = self.pieceShape
for i in range(4):
result.setX(i, -self.y(i))
result.setY(i, self.x(i))
return result
if __name__ == '__main__':
app = QApplication([])
tetris = Tetris()
sys.exit(app.exec_())
```
游戏很简单所以也就很好理解。程序加载之后游戏也就直接开始了可以用P键暂停游戏空格键让方块直接落到最下面。游戏的速度是固定的并没有实现加速的功能。分数就是游戏中消除的行数。
```text
self.tboard = Board(self)
self.setCentralWidget(self.tboard)
```
创建了一个Board类的实例并设置为应用的中心组件。
```text
self.statusbar = self.statusBar()
self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)
```
创建一个`statusbar`来显示三种信息:消除的行数,游戏暂停状态或者游戏结束状态。`msg2Statusbar`是一个自定义的信号用在Board类交互`showMessage()`方法是一个内建的用来在statusbar上显示信息的方法。
```text
self.tboard.start()
```
初始化游戏:
```text
class Board(QFrame):
msg2Statusbar = pyqtSignal(str)
...
```
创建了一个自定义信号`msg2Statusbar`,当我们想往`statusbar`里显示信息的时候,发出这个信号就行了。
```text
BoardWidth = 10
BoardHeight = 22
Speed = 300
```
这些是`Board`类的变量。`BoardWidth``BoardHeight`分别是board的宽度和高度。`Speed`是游戏的速度每300ms出现一个新的方块。
```text
...
self.curX = 0
self.curY = 0
self.numLinesRemoved = 0
self.board = []
...
```
`initBoard()`里初始化了一些重要的变量。`self.board`定义了方块的形状和位置取值范围是0-7。
```text
def shapeAt(self, x, y):
return self.board[(y * Board.BoardWidth) + x]
```
`shapeAt()`决定了board里方块的的种类。
```text
def squareWidth(self):
return self.contentsRect().width() // Board.BoardWidth
```
board的大小可以动态的改变。所以方格的大小也应该随之变化。`squareWidth()`计算并返回每个块应该占用多少像素--也即`Board.BoardWidth`
```text
def pause(self):
'''pauses game'''
if not self.isStarted:
return
self.isPaused = not self.isPaused
if self.isPaused:
self.timer.stop()
self.msg2Statusbar.emit("paused")
else:
self.timer.start(Board.Speed, self)
self.msg2Statusbar.emit(str(self.numLinesRemoved))
self.update()
```
`pause()`方法用来暂停游戏,停止计时并在`statusbar`上显示一条信息。
```text
def paintEvent(self, event):
'''paints all shapes of the game'''
painter = QPainter(self)
rect = self.contentsRect()
...
```
渲染是在paintEvent\(\)方法里发生的`QPainter`负责PyQt5里所有低级绘画操作。
```text
for i in range(Board.BoardHeight):
for j in range(Board.BoardWidth):
shape = self.shapeAt(j, Board.BoardHeight - i - 1)
if shape != Tetrominoe.NoShape:
self.drawSquare(painter,
rect.left() + j * self.squareWidth(),
boardTop + i * self.squareHeight(), shape)
```
渲染游戏分为两步。第一步是先画出所有已经落在最下面的的图,这些保存在`self.board`里。可以使用`shapeAt()`查看这个这个变量。
```text
if self.curPiece.shape() != Tetrominoe.NoShape:
for i in range(4):
x = self.curX + self.curPiece.x(i)
y = self.curY - self.curPiece.y(i)
self.drawSquare(painter, rect.left() + x * self.squareWidth(),
boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
self.curPiece.shape())
```
第二步是画出更在下落的方块。
```text
elif key == Qt.Key_Right:
self.tryMove(self.curPiece, self.curX + 1, self.curY)
```
`keyPressEvent()`方法获得用户按下的按键。如果按下的是右方向键,就尝试把方块向右移动,说尝试是因为有可能到边界不能移动了。
```text
elif key == Qt.Key_Up:
self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY)
```
上方向键是把方块向左旋转一下
```text
elif key == Qt.Key_Space:
self.dropDown()
```
空格键会直接把方块放到底部
```text
elif key == Qt.Key_D:
self.oneLineDown()
```
D键是加速一次下落速度。
```text
def tryMove(self, newPiece, newX, newY):
for i in range(4):
x = newX + newPiece.x(i)
y = newY - newPiece.y(i)
if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
return False
if self.shapeAt(x, y) != Tetrominoe.NoShape:
return False
self.curPiece = newPiece
self.curX = newX
self.curY = newY
self.update()
return True
```
`tryMove()`是尝试移动方块的方法。如果方块已经到达board的边缘或者遇到了其他方块就返回False。否则就把方块下落到想要
```text
def timerEvent(self, event):
if event.timerId() == self.timer.timerId():
if self.isWaitingAfterLine:
self.isWaitingAfterLine = False
self.newPiece()
else:
self.oneLineDown()
else:
super(Board, self).timerEvent(event)
```
在计时器事件里要么是等一个方块下落完之后创建一个新的方块要么是让一个方块直接落到底move a falling piece one line down
```text
def clearBoard(self):
for i in range(Board.BoardHeight * Board.BoardWidth):
self.board.append(Tetrominoe.NoShape)
```
`clearBoard(`\)方法通过`Tetrominoe.NoShape`清空`broad`
```text
def removeFullLines(self):
numFullLines = 0
rowsToRemove = []
for i in range(Board.BoardHeight):
n = 0
for j in range(Board.BoardWidth):
if not self.shapeAt(j, i) == Tetrominoe.NoShape:
n = n + 1
if n == 10:
rowsToRemove.append(i)
rowsToRemove.reverse()
for m in rowsToRemove:
for k in range(m, Board.BoardHeight):
for l in range(Board.BoardWidth):
self.setShapeAt(l, k, self.shapeAt(l, k + 1))
numFullLines = numFullLines + len(rowsToRemove)
...
```
如果方块碰到了底部,就调用`removeFullLines()`方法,找到所有能消除的行消除它们。消除的具体动作就是把符合条件的行消除掉之后,再把它上面的行下降一行。注意移除满行的动作是倒着来的,因为我们是按照重力来表现游戏的,如果不这样就有可能出现有些方块浮在空中的现象。
```text
def newPiece(self):
self.curPiece = Shape()
self.curPiece.setRandomShape()
self.curX = Board.BoardWidth // 2 + 1
self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
if not self.tryMove(self.curPiece, self.curX, self.curY):
self.curPiece.setShape(Tetrominoe.NoShape)
self.timer.stop()
self.isStarted = False
self.msg2Statusbar.emit("Game over")
```
`newPiece()`方法是用来创建形状随机的方块。如果随机的方块不能正确的出现在预设的位置,游戏结束。
```text
class Tetrominoe(object):
NoShape = 0
ZShape = 1
SShape = 2
LineShape = 3
TShape = 4
SquareShape = 5
LShape = 6
MirroredLShape = 7
```
`Tetrominoe`类保存了所有方块的形状。我们还定义了一个`NoShape`的空形状。
Shape类保存类方块内部的信息。
```text
class Shape(object):
coordsTable = (
((0, 0), (0, 0), (0, 0), (0, 0)),
((0, -1), (0, 0), (-1, 0), (-1, 1)),
...
)
...
```
coordsTable元组保存了所有的方块形状的组成。是一个构成方块的坐标模版。
```text
self.coords = [[0,0] for i in range(4)]
```
上面创建了一个新的空坐标数组,这个数组将用来保存方块的坐标。
坐标系示意图:
![coordinates](images/11-coordinates.png)
上面的图片可以帮助我们更好的理解坐标值的意义。比如元组`(0, -1), (0, 0), (-1, 0), (-1, -1)`代表了一个Z形状的方块。这个图表就描绘了这个形状。
```text
def rotateLeft(self):
if self.pieceShape == Tetrominoe.SquareShape:
return self
result = Shape()
result.pieceShape = self.pieceShape
for i in range(4):
result.setX(i, self.y(i))
result.setY(i, -self.x(i))
return result
```
`rotateLeft()`方法向右旋转一个方块。正方形的方块就没必要旋转,就直接返回了。其他的是返回一个新的,能表示这个形状旋转了的坐标。
程序展示:
![Tetris](images/11-tetris.png)

View File

@ -0,0 +1,528 @@
# Hello World
## 本章学习Qt的基本功能
## 例1简单的窗口
这个简单的小例子展示的是一个小窗口。但是我们可以在这个小窗口上面做很多事情改变大小最大化最小化等这需要很多代码才能实现。这在很多应用中很常见没必要每次都要重写这部分代码Qt已经提供了这些功能。PyQt5是一个高级的工具集合相比使用低级的工具能省略上百行代码。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we create a simple
window in PyQt5.
author: Jan Bodnar
website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QApplication, QWidget
if __name__ == '__main__':
app = QApplication(sys.argv)
w = QWidget()
w.resize(250, 150)
w.move(300, 300)
w.setWindowTitle('Simple')
w.show()
sys.exit(app.exec_())
```
运行上面的代码,能展示出一个小窗口。
```text
import sys
from PyQt5.QtWidgets import QApplication, QWidget
```
这里引入了PyQt5.QtWidgets模块这个模块包含了基本的组件。
```text
app = QApplication(sys.argv)
```
每个PyQt5应用都必须创建一个应用对象。sys.argv是一组命令行参数的列表。Python可以在shell里运行这个参数提供对脚本控制的功能。
```text
w = QWidget()
```
QWidge控件是一个用户界面的基本控件它提供了基本的应用构造器。默认情况下构造器是没有父级的没有父级的构造器被称为窗口window
```text
w.resize(250, 150)
```
`resize()`方法能改变控件的大小这里的意思是窗口宽250px高150px。
```text
w.move(300, 300)
```
`move()`是修改控件位置的方法。它把控件放置到屏幕坐标的(300, 300)的位置。注:屏幕坐标系的原点是屏幕的左上角。
```text
w.setWindowTitle('Simple')
```
我们给这个窗口添加了一个标题,标题在标题栏展示(虽然这看起来是一句废话,但是后面还有各种栏,还是要注意一下,多了就蒙了)。
```text
w.show()
```
`show()`能让控件在桌面上显示出来。控件在内存里创建,之后才能在显示器上显示出来。
```text
sys.exit(app.exec_())
```
最后,我们进入了应用的主循环中,事件处理器这个时候开始工作。主循环从窗口上接收事件,并把事件派发到应用控件里。当调用`exit()`方法或直接销毁主控件时,主循环就会结束。`sys.exit()`方法能确保主循环安全退出。外部环境会收到主控件如何结束的信息。
`exec_()`之所以有个下划线,是因为`exec`是一个Python的关键字。
程序预览:
![simple](images/1-simple.png)
## 例2带窗口图标
窗口图标通常是显示在窗口的左上角标题栏的最左边。下面的例子就是怎么用PyQt5创建一个这样的窗口。
在某些环境下图标显示不出来。如果你遇到了这个问题看我在Stackoverfolw的[回答](https://stackoverflow.com/questions/44080247/pyqt5-does-now-show-icons/45439678#45439678)
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This example shows an icon
in the titlebar of the window.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtGui import QIcon
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 220)
self.setWindowTitle('Icon')
self.setWindowIcon(QIcon('web.png'))
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
前一个例子是使用的[过程式编程](https://www.wikiwand.com/zh/%E8%BF%87%E7%A8%8B%E5%BC%8F%E7%BC%96%E7%A8%8B)。Python还支持[面向对象](https://www.wikiwand.com/zh/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1)的编程:
```text
class Example(QWidget):
def __init__(self):
super().__init__()
...
```
面向对象编程最重要的三个部分是类\(class\)、数据和方法。我们创建了一个类的调用,这个类继承自`QWidget`。这就意味着,我们调用了两个构造器,一个是这个类本身的,一个是这个类继承的。`super()`构造器方法返回父级的对象。`__init__()`方法是构造器的一个方法。
```text
self.initUI()
```
使用`initUI()`方法创建一个GUI。
```text
# 自己准备一个web.png
self.setGeometry(300, 300, 300, 220)
self.setWindowTitle('Icon')
self.setWindowIcon(QIcon('web.png'))
```
上面的三个方法都继承自`QWidget`类。`setGeometry()`有两个作用把窗口放到屏幕上并且设置窗口大小。参数分别代表屏幕坐标的x、y和窗口大小的宽、高。也就是说这个方法是`resize()``move()`的合体。最后一个方法是添加了图标。先创建一个QIcon对象然后接受一个路径作为参数显示图标。
```text
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
应用和示例的对象创立,主循环开始。
程序预览:
![icon](images/1-icon.png)
## 例3提示框
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This example shows a tooltip on
a window and a button.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import (QWidget, QToolTip,
QPushButton, QApplication)
from PyQt5.QtGui import QFont
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
QToolTip.setFont(QFont('SansSerif', 10))
self.setToolTip('This is a <b>QWidget</b> widget')
btn = QPushButton('Button', self)
btn.setToolTip('This is a <b>QPushButton</b> widget')
btn.resize(btn.sizeHint())
btn.move(50, 50)
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Tooltips')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
在这个例子中,我们为应用创建了一个提示框。
```text
QToolTip.setFont(QFont('SansSerif', 10))
```
`setFont()`这个静态方法设置了提示框的字体我们使用了10px的SansSerif字体。
```text
self.setToolTip('This is a <b>QWidget</b> widget')
```
调用`setToolTip()`创建提示框可以使用富文本格式的内容。
```text
btn = QPushButton('Button', self)
btn.setToolTip('This is a <b>QPushButton</b> widget')
```
创建一个按钮,并且为按钮添加了一个提示框。
```text
btn.resize(btn.sizeHint())
btn.move(50, 50)
```
调整按钮大小,并让按钮在屏幕上显示出来,`sizeHint()`方法提供了一个默认的按钮大小。
程序预览:
![tooltip](images/1-tooltips.png)
## 例4关闭窗口
关闭一个窗口最直观的方式就是点击标题栏的那个叉这个例子里我们展示的是如何用程序关闭一个窗口。这里我们将接触到一点signals(信号)和slots(槽)的知识。
本例使用的是QPushButton组件类。
```text
QPushButton(string text, QWidget parent = None)
```
`text`参数是想要显示的按钮名称,`parent`参数是放在按钮上的组件,在我们的 例子里,这个参数是`QWidget`。应用中的组件都是一层一层(继承而来的?)的,在这个层里,大部分的组件都有自己的父级,没有父级的组件,是顶级的窗口。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This program creates a quit
button. When we press the button,
the application terminates.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QWidget, QPushButton, QApplication
from PyQt5.QtCore import QCoreApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
qbtn = QPushButton('Quit', self)
qbtn.clicked.connect(QCoreApplication.instance().quit)
qbtn.resize(qbtn.sizeHint())
qbtn.move(50, 50)
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Quit button')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
这里创建了一个点击之后就退出窗口的按钮。
```text
from PyQt5.QtCore import QCoreApplication
```
程序需要`QtCore`对象。
```text
qbtn = QPushButton('Quit', self)
```
创建一个继承自`QPushButton`的按钮。第一个参数是按钮的文本,第二个参数是按钮的父级组件,这个例子中,父级组件就是我们创建的继承自`QWidget``Example`类。
```text
qbtn.clicked.connect(QCoreApplication.instance().quit)
```
事件传递系统在PyQt5内建的signals和slots机制里面。点击按钮之后信号会被捕捉并给出既定的反应。`QCoreApplication`包含了事件的主循环,它能添加和删除所有的事件,`instance()`创建了一个它的实例。`QCoreApplication`是在`QApplication`里创建的。 点击事件和能终止进程并退出应用的quit函数绑定在了一起。在发送者和接受者之间建立了通讯发送者就是按钮接受者就是应用对象。
程序预览:
![quitbutton](images/1-quitbutton.png)
## 例5消息盒子
默认情况下我们点击标题栏的×按钮QWidget就会关闭。但是有时候我们修改默认行为。比如如果我们打开的是一个文本编辑器并且做了一些修改我们就会想在关闭按钮的时候让用户进一步确认操作。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This program shows a confirmation
message box when we click on the close
button of the application window.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QWidget, QMessageBox, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Message box')
self.show()
def closeEvent(self, event):
reply = QMessageBox.question(self, 'Message',
"Are you sure to quit?", QMessageBox.Yes |
QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
event.accept()
else:
event.ignore()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
如果关闭QWidget就会产生一个QCloseEvent并且把它传入到closeEvent函数的event参数中。改变控件的默认行为就是替换掉默认的事件处理。
```text
reply = QMessageBox.question(self, 'Message',
"Are you sure to quit?", QMessageBox.Yes |
QMessageBox.No, QMessageBox.No)
```
我们创建了一个消息框上面有俩按钮Yes和No.第一个字符串显示在消息框的标题栏,第二个字符串显示在对话框,第三个参数是消息框的俩按钮,最后一个参数是默认按钮,这个按钮是默认选中的。返回值在变量`reply`里。
```text
if reply == QMessageBox.Yes:
event.accept()
else:
event.ignore()
```
这里判断返回值如果点击的是Yes按钮我们就关闭组件和应用否者就忽略关闭事件。
程序预览:
![messagebox](images/1-messagebox.png)
## 例6窗口居中
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This program centers a window
on the screen.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.resize(250, 150)
self.center()
self.setWindowTitle('Center')
self.show()
def center(self):
qr = self.frameGeometry()
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
`QtGui.QDesktopWidget`提供了用户的桌面信息,包括屏幕的大小。
```text
self.center()
```
这个方法是调用我们下面写的,实现对话框居中的方法。
```text
qr = self.frameGeometry()
```
获得主窗口所在的框架。
```text
cp = QDesktopWidget().availableGeometry().center()
```
获取显示器的分辨率,然后得到屏幕中间点的位置。
```text
qr.moveCenter(cp)
```
然后把主窗口框架的中心点放置到屏幕的中心位置。
```text
self.move(qr.topLeft())
```
然后通过move函数把主窗口的左上角移动到其框架的左上角这样就把窗口居中了。
程序预览:
![center](images/1-center.png)

577
translated/pyqt5/hui-tu.md Normal file
View File

@ -0,0 +1,577 @@
# 绘图
## 绘图
PyQt5绘图系统能渲染矢量图像、位图图像和轮廓字体文本。一般会使用在修改或者提高现有组件的功能或者创建自己的组件。使用PyQt5的绘图API进行操作。
绘图由`paintEvent()`方法完成,绘图的代码要放在`QPainter`对象的`begin()``end()`方法之间。是低级接口。
### 文本涂鸦
我们从画一些Unicode文本开始。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we draw text in Russian Cylliric.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QColor, QFont
from PyQt5.QtCore import Qt
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.text = "Лев Николаевич Толстой\nАнна Каренина"
self.setGeometry(300, 300, 280, 170)
self.setWindowTitle('Drawing text')
self.show()
def paintEvent(self, event):
qp = QPainter()
qp.begin(self)
self.drawText(event, qp)
qp.end()
def drawText(self, event, qp):
qp.setPen(QColor(168, 34, 3))
qp.setFont(QFont('Decorative', 10))
qp.drawText(event.rect(), Qt.AlignCenter, self.text)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
写了一些文本上下居中对齐的俄罗斯Cylliric语言的文字。
```text
def paintEvent(self, event):
...
```
在绘画事件内完成绘画动作。
```text
qp = QPainter()
qp.begin(self)
self.drawText(event, qp)
qp.end()
```
`QPainter`是低级的绘画类。所有的绘画动作都在这个类的`begin()``end()`方法之间完成,绘画动作都封装在`drawText()`内部了。
```text
qp.setPen(QColor(168, 34, 3))
qp.setFont(QFont('Decorative', 10))
```
为文字绘画定义了笔和字体。
```text
qp.drawText(event.rect(), Qt.AlignCenter, self.text)
```
`drawText()`方法在窗口里绘制文本,`rect()`方法返回要更新的矩形区域。
程序展示:
![drawing text](images/9-drawtext.png)
### 点的绘画
点是最简单的绘画了。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In the example, we draw randomly 1000 red points
on the window.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter
from PyQt5.QtCore import Qt
import sys, random
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 190)
self.setWindowTitle('Points')
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawPoints(qp)
qp.end()
def drawPoints(self, qp):
qp.setPen(Qt.red)
size = self.size()
for i in range(1000):
x = random.randint(1, size.width()-1)
y = random.randint(1, size.height()-1)
qp.drawPoint(x, y)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
我们在窗口里随机的画出了1000个点。
```text
qp.setPen(Qt.red)
```
设置笔的颜色为红色,使用的是预定义好的颜色。
```text
size = self.size()
```
每次更改窗口大小,都会产生绘画事件,从`size()`方法里获得当前窗口的大小,然后把产生的点随机的分配到窗口的所有位置上。
```text
qp.drawPoint(x, y)
```
`drawPoint()`方法绘图。
程序展示:
![points](images/9-points.png)
## 颜色
颜色是一个物体显示的RGB的混合色。RBG值的范围是0~255。我们有很多方式去定义一个颜色最常见的方式就是RGB和16进制表示法也可以使用RGBA增加了一个透明度的选项透明度值的范围是0~10代表完全透明。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This example draws three rectangles in three
#different colours.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QColor, QBrush
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 350, 100)
self.setWindowTitle('Colours')
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawRectangles(qp)
qp.end()
def drawRectangles(self, qp):
col = QColor(0, 0, 0)
col.setNamedColor('#d4d4d4')
qp.setPen(col)
qp.setBrush(QColor(200, 0, 0))
qp.drawRect(10, 15, 90, 60)
qp.setBrush(QColor(255, 80, 0, 160))
qp.drawRect(130, 15, 90, 60)
qp.setBrush(QColor(25, 0, 90, 200))
qp.drawRect(250, 15, 90, 60)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
我们画出了三个颜色的矩形。
```text
color = QColor(0, 0, 0)
color.setNamedColor('#d4d4d4')
```
使用16进制的方式定义一个颜色。
```text
qp.setBrush(QColor(200, 0, 0))
qp.drawRect(10, 15, 90, 60)
```
定义了一个笔刷,并画出了一个矩形。笔刷是用来画一个物体的背景。`drawRect()`有四个参数分别是矩形的x、y、w、h。 然后用笔刷和矩形进行绘画。
程序展示:
![colours](images/9-colours.png)
### QPen
`QPen`是基本的绘画对象,能用来画直线、曲线、矩形框、椭圆、多边形和其他形状。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example we draw 6 lines using
different pen styles.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QPen
from PyQt5.QtCore import Qt
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 280, 270)
self.setWindowTitle('Pen styles')
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawLines(qp)
qp.end()
def drawLines(self, qp):
pen = QPen(Qt.black, 2, Qt.SolidLine)
qp.setPen(pen)
qp.drawLine(20, 40, 250, 40)
pen.setStyle(Qt.DashLine)
qp.setPen(pen)
qp.drawLine(20, 80, 250, 80)
pen.setStyle(Qt.DashDotLine)
qp.setPen(pen)
qp.drawLine(20, 120, 250, 120)
pen.setStyle(Qt.DotLine)
qp.setPen(pen)
qp.drawLine(20, 160, 250, 160)
pen.setStyle(Qt.DashDotDotLine)
qp.setPen(pen)
qp.drawLine(20, 200, 250, 200)
pen.setStyle(Qt.CustomDashLine)
pen.setDashPattern([1, 4, 5, 4])
qp.setPen(pen)
qp.drawLine(20, 240, 250, 240)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
在这个例子里我们用不同的笔画了6条直线。PyQt5有五个预定义的笔另外一个笔的样式使我们自定义的。
```text
pen = QPen(Qt.black, 2, Qt.SolidLine)
```
新建一个`QPen`对象设置颜色黑色宽2像素这样就能看出来各个笔样式的区别。`Qt.SolidLine`是预定义样式的一种。
```text
pen.setStyle(Qt.CustomDashLine)
pen.setDashPattern([1, 4, 5, 4])
qp.setPen(pen)
```
这里我们自定义了一个笔的样式。定义为`Qt.CustomDashLine`然后调用`setDashPattern()`方法。数字列表是线的样式要求必须是个数为奇数奇数位定义的是空格偶数位为线长数字越大空格或线长越大比如本例的就是1像素线4像素空格5像素线4像素空格。
程序展示:
![pen styles](images/9-penstyles.png)
### QBrush
`QBrush`也是图像的一个基本元素。是用来填充一些物体的背景图用的,比如矩形,椭圆,多边形等。有三种类型:预定义、渐变和纹理。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This example draws nine rectangles in different
brush styles.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QBrush
from PyQt5.QtCore import Qt
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 355, 280)
self.setWindowTitle('Brushes')
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawBrushes(qp)
qp.end()
def drawBrushes(self, qp):
brush = QBrush(Qt.SolidPattern)
qp.setBrush(brush)
qp.drawRect(10, 15, 90, 60)
brush.setStyle(Qt.Dense1Pattern)
qp.setBrush(brush)
qp.drawRect(130, 15, 90, 60)
brush.setStyle(Qt.Dense2Pattern)
qp.setBrush(brush)
qp.drawRect(250, 15, 90, 60)
brush.setStyle(Qt.DiagCrossPattern)
qp.setBrush(brush)
qp.drawRect(10, 105, 90, 60)
brush.setStyle(Qt.Dense5Pattern)
qp.setBrush(brush)
qp.drawRect(130, 105, 90, 60)
brush.setStyle(Qt.Dense6Pattern)
qp.setBrush(brush)
qp.drawRect(250, 105, 90, 60)
brush.setStyle(Qt.HorPattern)
qp.setBrush(brush)
qp.drawRect(10, 195, 90, 60)
brush.setStyle(Qt.VerPattern)
qp.setBrush(brush)
qp.drawRect(130, 195, 90, 60)
brush.setStyle(Qt.BDiagPattern)
qp.setBrush(brush)
qp.drawRect(250, 195, 90, 60)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
我们画了9个不同的矩形。
```text
brush = QBrush(Qt.SolidPattern)
qp.setBrush(brush)
qp.drawRect(10, 15, 90, 60)
```
创建了一个笔刷对象,添加笔刷样式,然后调用`drawRect()`方法画图。
程序展示:
![brushes](images/9-brushes.png)
### 贝塞尔曲线
噩梦可以使用PyQt5的`QPainterPath`创建贝塞尔曲线。绘画路径是由许多构建图形的对象,具体表现就是一些线的形状,比如矩形,椭圆,线和曲线。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This program draws a Bézier curve with
QPainterPath.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QPainterPath
from PyQt5.QtCore import Qt
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 380, 250)
self.setWindowTitle('Bézier curve')
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
qp.setRenderHint(QPainter.Antialiasing)
self.drawBezierCurve(qp)
qp.end()
def drawBezierCurve(self, qp):
path = QPainterPath()
path.moveTo(30, 30)
path.cubicTo(30, 30, 200, 350, 350, 30)
qp.drawPath(path)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
这个示例中,我们画出了一个贝塞尔曲线。
```python
path = QPainterPath()
path.moveTo(30, 30)
path.cubicTo(30, 30, 200, 350, 350, 30)
```
`QPainterPath`路径创建贝塞尔曲线。使用`cubicTo()`方法生成,分别需要三个点:起始点,控制点和终止点。
```python
qp.drawPath(path)
```
`drawPath()`绘制最后的图像。
程序展示:
![B&#xE9;zier curve](https://github.com/maicss/PyQt5-Chinese-tutorial/tree/856a93979da60f75493c28bae96380a6fe71e2ea/images/9-Bézier%20curve.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

51
translated/pyqt5/index.md Normal file
View File

@ -0,0 +1,51 @@
# 介绍
本教程的目的是带领你入门PyQt5。教程内所有代码都在Linux上测试通过。[PyQt4 教程](http://zetcode.com/gui/pyqt4/)是PyQt4的教程PyQt4是一个Python同时支持2和3版的Qt库。
## 关于 PyQt5
PyQt5 是Digia的一套Qt5应用框架与python的结合同时支持2.x和3.x。本教程使用的是3.x。Qt库由Riverbank Computing开发是最强大的GUI库之一 官方网站www.riverbankcomputing.co.uk/news。
PyQt5是由一系列Python模块组成。超过620个类6000函数和方法。能在诸如Unix、Windows和Mac OS等主流操作系统上运行。PyQt5有两种证书GPL和商业证书。
PyQt5类分为很多模块主要模块有
* QtCore 包含了核心的非GUI的功能。主要和时间、文件与文件夹、各种数据、流、URLs、mime类文件、进程与线程一起使用。
* QtGui 包含了窗口系统、事件处理、2D图像、基本绘画、字体和文字类。
* QtWidgets
* QtMultimedia
* QtBluetooth
* QtNetwork
* QtPositioning
* Enginio
* QtWebSockets
* QtWebKit
* QtWebKitWidgets
* QtXml
* QtSvg
* QtSql
* QtTest
QtWidgets类包含了一系列创建桌面应用的UI元素。 QtMultimedia包含了处理多媒体的内容和调用摄像头API的类。 QtBluetooth模块包含了查找和连接蓝牙的类。 QtNetwork包含了网络编程的类这些工具能让TCP/IP和UDP开发变得更加方便和可靠。 QtPositioning包含了定位的类可以使用卫星、WiFi甚至文本。 Engine包含了通过客户端进入和管理Qt Cloud的类。 QtWebSockets包含了WebSocket协议的类。 QtWebKit包含了一个基WebKit2的web浏览器。 QtWebKitWidgets包含了基于QtWidgets的WebKit1的类。 QtXml包含了处理xml的类提供了SAX和DOM API的工具。 QtSvg提供了显示SVG内容的类Scalable Vector Graphics \(SVG\)是一种是一种基于可扩展标记语言XML用于描述二维矢量图形的图形格式这句话来自于维基百科。 QtSql提供了处理数据库的工具。 QtTest提供了测试PyQt5应用的工具。
## PyQt4和PyQt5的区别
**PyQt5不兼容PyQt4**。PyQt5有一些巨大的改进。但是迁移并不是很难两者的区别如下
* 重新组合模块,一些模块已经被废弃\(QtScript\),有些被分为两个子模块\(QtGui, QtWebKit\)。
* 添加了新的模块比如QtBluetooth, QtPositioning和Enginio。
* 废弃了SINGAL\(\)和SLOT\(\)的调用方式使用了新的信号和xx处理方式。
* 不再支持被标记为废弃的或不建议使用的API。
## Python语言的介绍
> 这个部分建议看百科这里写的很简略。如果你还不太熟悉Python建议先去官网看看文档。
Python is a general-purpose, dynamic, object-oriented programming language. The design purpose of the Python language emphasizes programmer productivity and code readability. Python was initially developed by Guido van Rossum. It was first released in 1991. Python was inspired by ABC, Haskell, Java, Lisp, Icon, and Perl programming languages. Python is a high-level, general purpose, multiplatform, interpreted language. Python is a minimalistic language. One of its most visible features is that it does not use semicolons nor brackets. It uses indentation instead. There are two main branches of Python currently: Python 2.x and Python 3.x. Python 3.x breaks backward compatibility with previous releases of Python. It was created to correct some design flaws of the language and make the language more clean. The most recent version of Python 2.x is 2.7.9, and of Python 3.x is 3.4.2. Python is maintained by a large group of volunteers worldwide. Python is open source software. Python is an ideal start for those who want to learn programming.
Python programming language supports several programming styles. It does not force a programmer to a specific paradigm. Python supports object-oriented and procedural programming. There is also a limited support for functional programming.
Python语言的官方网站是python.org
PerlPython和Ruby都是使用最广泛的脚本语言它们有很多共同的特点也是相互的竞争对手。

View File

@ -0,0 +1,559 @@
# 控件\(1\)
控件就像是应用这座房子的一块块砖。PyQt5有很多的控件比如按钮单选框滑动条复选框等等。在本章我们将介绍一些很有用的控件`QCheckBox``ToggleButton``QSlider``QProgressBar``QCalendarWidget`
## QCheckBox
`QCheckBox` 组件有俩状态:开和关。通常跟标签一起使用,用在激活和关闭一些选项的场景。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, a QCheckBox widget
is used to toggle the title of a window.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import QWidget, QCheckBox, QApplication
from PyQt5.QtCore import Qt
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
cb = QCheckBox('Show title', self)
cb.move(20, 20)
cb.toggle()
cb.stateChanged.connect(self.changeTitle)
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('QCheckBox')
self.show()
def changeTitle(self, state):
if state == Qt.Checked:
self.setWindowTitle('QCheckBox')
else:
self.setWindowTitle(' ')
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
这个例子中,有一个能切换窗口标题的单选框。
```text
cb = QCheckBox('Show title', self)
```
这个是`QCheckBox`的构造器。
```text
cb.toggle()
```
要设置窗口标题,我们就要检查单选框的状态。默认情况下,窗口没有标题,单选框未选中。
```text
cb.stateChanged.connect(self.changeTitle)
```
`changeTitle()`方法和`stateChanged`信号关联起来。这样,`changeTitle()`就能切换窗口标题了。
```text
def changeTitle(self, state):
if state == Qt.Checked:
self.setWindowTitle('QCheckBox')
else:
self.setWindowTitle('')
```
控件的状态是由`changeTitle()`方法控制的,如果空间被选中,我们就给窗口添加一个标题,如果没被选中,就清空标题。
程序展示:
![QCheckBox](images/6-qcheckbox.png)
## 切换按钮
切换按钮就是`QPushButton`的一种特殊模式。 它只有两种状态:按下和未按下。我们在点击的时候切换两种状态,有很多场景会使用到这个功能。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we create three toggle buttons.
They will control the background color of a
QFrame.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QWidget, QPushButton,
QFrame, QApplication)
from PyQt5.QtGui import QColor
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.col = QColor(0, 0, 0)
redb = QPushButton('Red', self)
redb.setCheckable(True)
redb.move(10, 10)
redb.clicked[bool].connect(self.setColor)
greenb = QPushButton('Green', self)
greenb.setCheckable(True)
greenb.move(10, 60)
greenb.clicked[bool].connect(self.setColor)
blueb = QPushButton('Blue', self)
blueb.setCheckable(True)
blueb.move(10, 110)
blueb.clicked[bool].connect(self.setColor)
self.square = QFrame(self)
self.square.setGeometry(150, 20, 100, 100)
self.square.setStyleSheet("QWidget { background-color: %s }" %
self.col.name())
self.setGeometry(300, 300, 280, 170)
self.setWindowTitle('Toggle button')
self.show()
def setColor(self, pressed):
source = self.sender()
if pressed:
val = 255
else: val = 0
if source.text() == "Red":
self.col.setRed(val)
elif source.text() == "Green":
self.col.setGreen(val)
else:
self.col.setBlue(val)
self.square.setStyleSheet("QFrame { background-color: %s }" %
self.col.name())
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
我们创建了一个切换按钮和一个`QWidget`,并把`QWidget`的背景设置为黑色。点击不同的切换按钮,背景色会在红、绿、蓝之间切换(而且能看到颜色合成的效果,而不是单纯的颜色覆盖)。
```text
self.col = QColor(0, 0, 0)
```
设置颜色为黑色。
```text
redb = QPushButton('Red', self)
redb.setCheckable(True)
redb.move(10, 10)
```
创建一个`QPushButton`,然后调用它的`setCheckable()`的方法就把这个按钮编程了切换按钮。
```text
redb.clicked[bool].connect(self.setColor)
```
把点击信号和我们定义好的函数关联起来,这里是把点击事件转换成布尔值。
```text
source = self.sender()
```
获取被点击的按钮。
```text
if source.text() == "Red":
self.col.setRed(val)
```
如果是标签为“red”的按钮被点击就把颜色更改为预设好的对应颜色。
```text
self.square.setStyleSheet("QFrame { background-color: %s }" %
self.col.name())
```
使用样式表就是CSS的SS改变背景色
程序展示:
![toggle button](images/6-togglebutton.png)
## 滑块
`QSlider`是个有一个小滑块的组件这个小滑块能拖着前后滑动这个经常用于修改一些具有范围的数值比文本框或者点击增加减少的文本框spin box方便多了。
本例用一个滑块和一个标签展示。标签为一个图片,滑块控制标签(的值)。
> 先准备四个分别表示静音、小音量、中音量、大音量的图标文件名分别叫mute.png, min.png, med.png, max.png。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This example shows a QSlider widget.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QWidget, QSlider,
QLabel, QApplication)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
sld = QSlider(Qt.Horizontal, self)
sld.setFocusPolicy(Qt.NoFocus)
sld.setGeometry(30, 40, 100, 30)
sld.valueChanged[int].connect(self.changeValue)
self.label = QLabel(self)
self.label.setPixmap(QPixmap('mute.png'))
self.label.setGeometry(160, 40, 80, 30)
self.setGeometry(300, 300, 280, 170)
self.setWindowTitle('QSlider')
self.show()
def changeValue(self, value):
if value == 0:
self.label.setPixmap(QPixmap('mute.png'))
elif value > 0 and value <= 30:
self.label.setPixmap(QPixmap('min.png'))
elif value > 30 and value < 80:
self.label.setPixmap(QPixmap('med.png'))
else:
self.label.setPixmap(QPixmap('max.png'))
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
这里是模拟的音量控制器。拖动滑块,能改变标签位置的图片。
```text
sld = QSlider(Qt.Horizontal, self)
```
创建一个水平的`QSlider`
```text
self.label = QLabel(self)
self.label.setPixmap(QPixmap('mute.png'))
```
创建一个`QLabel`组件并给它设置一个静音图标。
```text
sld.valueChanged[int].connect(self.changeValue)
```
`valueChanged`信号跟`changeValue()`方法关联起来。
```text
if value == 0:
self.label.setPixmap(QPixmap('mute.png'))
...
```
根据音量值的大小更换标签位置的图片。这段代码是如果音量为0就把图片换成 mute.png。
程序展示:
![QSlider widget](images/6-qslider.png)
## 进度条
进度条是用来展示任务进度的(我也不想这样说话)。它的滚动能让用户了解到任务的进度。`QProgressBar`组件提供了水平和垂直两种进度条进度条可以设置最大值和最小值默认情况是0~99。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This example shows a QProgressBar widget.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QWidget, QProgressBar,
QPushButton, QApplication)
from PyQt5.QtCore import QBasicTimer
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.pbar = QProgressBar(self)
self.pbar.setGeometry(30, 40, 200, 25)
self.btn = QPushButton('Start', self)
self.btn.move(40, 80)
self.btn.clicked.connect(self.doAction)
self.timer = QBasicTimer()
self.step = 0
self.setGeometry(300, 300, 280, 170)
self.setWindowTitle('QProgressBar')
self.show()
def timerEvent(self, e):
if self.step >= 100:
self.timer.stop()
self.btn.setText('Finished')
return
self.step = self.step + 1
self.pbar.setValue(self.step)
def doAction(self):
if self.timer.isActive():
self.timer.stop()
self.btn.setText('Start')
else:
self.timer.start(100, self)
self.btn.setText('Stop')
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
我们创建了一个水平的进度条和一个按钮,这个按钮控制进度条的开始和停止。
```text
self.pbar = QProgressBar(self)
```
新建一个`QProgressBar`构造器。
```text
self.timer = QtCore.QBasicTimer()
```
用时间控制进度条。
```text
self.timer.start(100, self)
```
调用`start()`方法加载一个时间事件。这个方法有两个参数:过期时间和事件接收者。
```text
def timerEvent(self, e):
if self.step >= 100:
self.timer.stop()
self.btn.setText('Finished')
return
self.step = self.step + 1
self.pbar.setValue(self.step)
```
每个`QObject`和又它继承而来的对象都有一个`timerEvent()`事件处理函数。为了触发事件,我们重载了这个方法。
```text
def doAction(self):
if self.timer.isActive():
self.timer.stop()
self.btn.setText('Start')
else:
self.timer.start(100, self)
self.btn.setText('Stop')
```
里面的`doAction()`方法是用来控制开始和停止的。
程序展示:
![QProgressBar](images/6-qprogressbar.png)
## 日历
`QCalendarWidget`提供了基于月份的日历插件,十分简易而且直观。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This example shows a QCalendarWidget widget.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QWidget, QCalendarWidget,
QLabel, QApplication, QVBoxLayout)
from PyQt5.QtCore import QDate
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
vbox = QVBoxLayout(self)
cal = QCalendarWidget(self)
cal.setGridVisible(True)
cal.clicked[QDate].connect(self.showDate)
vbox.addWidget(cal)
self.lbl = QLabel(self)
date = cal.selectedDate()
self.lbl.setText(date.toString())
vbox.addWidget(self.lbl)
self.setLayout(vbox)
self.setGeometry(300, 300, 350, 300)
self.setWindowTitle('Calendar')
self.show()
def showDate(self, date):
self.lbl.setText(date.toString())
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
这个例子有日期组件和标签组件组成,标签显示被选中的日期。
```text
cal = QCalendarWidget(self)
```
创建一个`QCalendarWidget`
```text
cal.clicked[QDate].connect(self.showDate)
```
选择一个日期时,`QDate`的点击信号就触发了,把这个信号和我们自己定义的`showDate()`方法关联起来。
```text
def showDate(self, date):
self.lbl.setText(date.toString())
```
使用`selectedDate()`方法获取选中的日期,然后把日期对象转成字符串,在标签里面显示出来。
程序展示:
![calendar](images/6-calendar.png)

View File

@ -0,0 +1,363 @@
# 控件\(2\)
本章我们继续介绍PyQt5控件。这次的有`QPixmap``QLineEdit``QSplitter`,和`QComboBox`
## 图片
`QPixmap`是处理图片的组件。本例中,我们使用`QPixmap`在窗口里显示一张图片。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we dispay an image
on the window.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QWidget, QHBoxLayout,
QLabel, QApplication)
from PyQt5.QtGui import QPixmap
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
hbox = QHBoxLayout(self)
pixmap = QPixmap("redrock.png")
lbl = QLabel(self)
lbl.setPixmap(pixmap)
hbox.addWidget(lbl)
self.setLayout(hbox)
self.move(300, 200)
self.setWindowTitle('Red Rock')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
```text
pixmap = QPixmap("redrock.png")
```
创建一个`QPixmap`对象,接收一个文件作为参数。
```text
lbl = QLabel(self)
lbl.setPixmap(pixmap)
```
`QPixmap`实例放到`QLabel`组件里。
程序展示:
![pixmap](images/7-pixmap.png)
## 行编辑
`QLineEdit`组件提供了编辑文本的功能,自带了撤销、重做、剪切、粘贴、拖拽等功能。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This example shows text which
is entered in a QLineEdit
in a QLabel widget.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import (QWidget, QLabel,
QLineEdit, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.lbl = QLabel(self)
qle = QLineEdit(self)
qle.move(60, 100)
self.lbl.move(60, 40)
qle.textChanged[str].connect(self.onChanged)
self.setGeometry(300, 300, 280, 170)
self.setWindowTitle('QLineEdit')
self.show()
def onChanged(self, text):
self.lbl.setText(text)
self.lbl.adjustSize()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
例子中展示了一个编辑组件和一个标签,我们在输入框里键入的文本,会立即在标签里显示出来。
```text
qle = QLineEdit(self)
```
创建一个`QLineEdit`对象。
```text
qle.textChanged[str].connect(self.onChanged)
```
如果输入框的值有变化,就调用`onChanged()`方法。
```text
def onChanged(self, text):
self.lbl.setText(text)
self.lbl.adjustSize()
```
`onChanged()`方法内部,我们把文本框里的值赋值给了标签组件,然后调用`adjustSize()`方法让标签自适应文本内容。
程序展示:
![QLineEdit](images/7-qlineedit.png)
## QSplitter
`QSplitter`组件能让用户通过拖拽分割线的方式改变子窗口大小的组件。本例中我们展示用两个分割线隔开的三个`QFrame`组件。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This example shows
how to use QSplitter widget.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QWidget, QHBoxLayout, QFrame,
QSplitter, QStyleFactory, QApplication)
from PyQt5.QtCore import Qt
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
hbox = QHBoxLayout(self)
topleft = QFrame(self)
topleft.setFrameShape(QFrame.StyledPanel)
topright = QFrame(self)
topright.setFrameShape(QFrame.StyledPanel)
bottom = QFrame(self)
bottom.setFrameShape(QFrame.StyledPanel)
splitter1 = QSplitter(Qt.Horizontal)
splitter1.addWidget(topleft)
splitter1.addWidget(topright)
splitter2 = QSplitter(Qt.Vertical)
splitter2.addWidget(splitter1)
splitter2.addWidget(bottom)
hbox.addWidget(splitter2)
self.setLayout(hbox)
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('QSplitter')
self.show()
def onChanged(self, text):
self.lbl.setText(text)
self.lbl.adjustSize()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
三个窗口和两个分割线的布局创建完成了,但是要注意,有些主题下,分割线的显示效果不太好。
```text
topleft = QFrame(self)
topleft.setFrameShape(QFrame.StyledPanel)
```
为了更清楚的看到分割线,我们使用了设置好的子窗口样式。
```text
splitter1 = QSplitter(Qt.Horizontal)
splitter1.addWidget(topleft)
splitter1.addWidget(topright)
```
创建一个`QSplitter`组件,并在里面添加了两个框架。
```text
splitter2 = QSplitter(Qt.Vertical)
splitter2.addWidget(splitter1)
```
也可以在分割线里面再进行分割。
程序展示:
![QSplitter widget](images/7-qsplitter.png)
## 下拉选框
`QComboBox`组件能让用户在多个选择项中选择一个。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This example shows how to use
a QComboBox widget.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QWidget, QLabel,
QComboBox, QApplication)
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.lbl = QLabel("Ubuntu", self)
combo = QComboBox(self)
combo.addItem("Ubuntu")
combo.addItem("Mandriva")
combo.addItem("Fedora")
combo.addItem("Arch")
combo.addItem("Gentoo")
combo.move(50, 50)
self.lbl.move(50, 150)
combo.activated[str].connect(self.onActivated)
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('QComboBox')
self.show()
def onActivated(self, text):
self.lbl.setText(text)
self.lbl.adjustSize()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
本例包含了一个`QComboBox`和一个`QLabel`。下拉选择框有五个选项都是Linux的发行版名称标签内容为选定的发行版名称。
```text
combo = QComboBox(self)
combo.addItem("Ubuntu")
combo.addItem("Mandriva")
combo.addItem("Fedora")
combo.addItem("Arch")
combo.addItem("Gentoo")
```
创建一个`QComboBox`组件和五个选项。
```text
combo.activated[str].connect(self.onActivated)
```
在选中的条目上调用`onActivated()`方法。
```text
def onActivated(self, text):
self.lbl.setText(text)
self.lbl.adjustSize()
```
在方法内部,设置标签内容为选定的字符串,然后设置自适应文本大小。
程序展示:
![QComboBox](images/7-qcombobox.png)

View File

@ -0,0 +1,428 @@
# 事件和信号
## 事件
> signals and slots 被其他人翻译成信号和槽机制,\(⊙o⊙\)…我这里还是不翻译好了。
所有的应用都是事件驱动的。事件大部分都是由用户的行为产生的当然也有其他的事件产生方式比如网络的连接窗口管理器或者定时器等。调用应用的exec\_\(\)方法时,应用会进入主循环,主循环会监听和分发事件。
在事件模型中,有三个角色:
* 事件源
* 事件
* 事件目标
事件源就是发生了状态改变的对象。事件是这个对象状态改变的内容。事件目标是事件想作用的目标。事件源绑定事件处理函数,然后作用于事件目标身上。
PyQt5处理事件方面有个signal and slot机制。Signals and slots用于对象间的通讯。事件触发的时候发生一个signalslot是用来被Python调用的相当于一个句柄这个词也好恶心就是相当于事件的绑定函数slot只有在事件触发的时候才能调用。
## Signals & slots
下面是signal & slot的演示
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we connect a signal
of a QSlider to a slot of a QLCDNumber.
Author: Jan Bodnar
Website: zetcode.com
Last edited: January 2017
"""
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QWidget, QLCDNumber, QSlider,
QVBoxLayout, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
lcd = QLCDNumber(self)
sld = QSlider(Qt.Horizontal, self)
vbox = QVBoxLayout()
vbox.addWidget(lcd)
vbox.addWidget(sld)
self.setLayout(vbox)
sld.valueChanged.connect(lcd.display)
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Signal and slot')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
上面的例子中,显示了`QtGui.QLCDNumber``QtGui.QSlider`模块,我们能拖动滑块让数字跟着发生改变。
```text
sld.valueChanged.connect(lcd.display)
```
这里是把滑块的变化和数字的变化绑定在一起。
`sender`是信号的发送者,`receiver`是信号的接收者,`slot`是对这个信号应该做出的反应。
程序展示:
![signal &amp; slot](images/4-sigslot.png)
## 重构事件处理器
在PyQt5中事件处理器经常被重写也就是用自己的覆盖库自带的
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we reimplement an
event handler.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Event handler')
self.show()
def keyPressEvent(self, e):
if e.key() == Qt.Key_Escape:
self.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
这个例子中,我们替换了事件处理器函数`keyPressEvent()`
```text
def keyPressEvent(self, e):
if e.key() == Qt.Key_Escape:
self.close()
```
此时如果按下ESC键程序就会退出。
程序展示:
这个就一个框,啥也没,就不展示了。
## 事件对象
事件对象是用python来描述一系列的事件自身属性的对象。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we display the x and y
coordinates of a mouse pointer in a label widget.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QApplication, QGridLayout, QLabel
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
grid = QGridLayout()
grid.setSpacing(10)
x = 0
y = 0
self.text = "x: {0}, y: {1}".format(x, y)
self.label = QLabel(self.text, self)
grid.addWidget(self.label, 0, 0, Qt.AlignTop)
self.setMouseTracking(True)
self.setLayout(grid)
self.setGeometry(300, 300, 350, 200)
self.setWindowTitle('Event object')
self.show()
def mouseMoveEvent(self, e):
x = e.x()
y = e.y()
text = "x: {0}, y: {1}".format(x, y)
self.label.setText(text)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
这个示例中我们在一个组件里显示鼠标的X和Y坐标。
```text
self.text = "x: {0}, y: {1}".format(x, y)
self.label = QLabel(self.text, self)
```
X Y坐标显示在`QLabel`组件里
```text
self.setMouseTracking(True)
```
事件追踪默认没有开启,当开启后才会追踪鼠标的点击事件。
```text
def mouseMoveEvent(self, e):
x = e.x()
y = e.y()
text = "x: {0}, y: {1}".format(x, y)
self.label.setText(text)
```
`e`代表了事件对象。里面有我们触发事件(鼠标移动)的事件对象。`x()``y()`方法得到鼠标的x和y坐标点然后拼成字符串输出到`QLabel`组件里。
程序展示:
![event object](images/4-eventobject.png)
## 事件发送
有时候我们会想知道是哪个组件发出了一个信号PyQt5里的`sender()`方法能搞定这件事。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we determine the event sender
object.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtWidgets import QMainWindow, QPushButton, QApplication
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
btn1 = QPushButton("Button 1", self)
btn1.move(30, 50)
btn2 = QPushButton("Button 2", self)
btn2.move(150, 50)
btn1.clicked.connect(self.buttonClicked)
btn2.clicked.connect(self.buttonClicked)
self.statusBar()
self.setGeometry(300, 300, 290, 150)
self.setWindowTitle('Event sender')
self.show()
def buttonClicked(self):
sender = self.sender()
self.statusBar().showMessage(sender.text() + ' was pressed')
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
这个例子里有俩按钮,`buttonClicked()`方法决定了是哪个按钮能调用`sender()`方法。
```text
btn1.clicked.connect(self.buttonClicked)
btn2.clicked.connect(self.buttonClicked)
```
两个按钮都和同一个slot绑定。
```text
def buttonClicked(self):
sender = self.sender()
self.statusBar().showMessage(sender.text() + ' was pressed')
```
我们用调用`sender()`方法的方式决定了事件源。状态栏显示了被点击的按钮。
程序展示:
![event sender](images/4-eventsender.png)
## 信号发送
`QObject`实例能发送事件信号。下面的例子是发送自定义的信号。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we show how to
emit a custom signal.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
import sys
from PyQt5.QtCore import pyqtSignal, QObject
from PyQt5.QtWidgets import QMainWindow, QApplication
class Communicate(QObject):
closeApp = pyqtSignal()
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.c = Communicate()
self.c.closeApp.connect(self.close)
self.setGeometry(300, 300, 290, 150)
self.setWindowTitle('Emit signal')
self.show()
def mousePressEvent(self, event):
self.c.closeApp.emit()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
我们创建了一个叫closeApp的信号这个信号会在鼠标按下的时候触发事件与`QMainWindow`绑定。
```text
class Communicate(QObject):
closeApp = pyqtSignal()
```
`Communicate`类创建了一个`pyqtSignal()`属性的信号。
```text
self.c = Communicate()
self.c.closeApp.connect(self.close)
```
`closeApp`信号`QMainWindow``close()`方法绑定。
```text
def mousePressEvent(self, event):
self.c.closeApp.emit()
```
点击窗口的时候发送closeApp信号程序终止。
程序展示:
这个也是啥也没。

View File

@ -0,0 +1,283 @@
# 拖拽
在GUI里拖放是指用户点击一个虚拟的对象拖动然后放置到另外一个对象上面的动作。一般情况下需要调用很多动作和方法创建很多变量。
拖放能让用户很直观的操作很复杂的逻辑。
一般情况下我们可以拖放两种东西数据和图形界面。把一个图像从一个应用拖放到另外一个应用上的实质是操作二进制数据。把一个表格从Firefox上拖放到另外一个位置 的实质是操作一个图形组。
## 简单的拖放
本例使用了`QLineEdit``QPushButton`。把一个文本从编辑框里拖到按钮上,更新按钮上的标签(文字)。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
This is a simple drag and
drop example.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QPushButton, QWidget,
QLineEdit, QApplication)
import sys
class Button(QPushButton):
def __init__(self, title, parent):
super().__init__(title, parent)
self.setAcceptDrops(True)
def dragEnterEvent(self, e):
if e.mimeData().hasFormat('text/plain'):
e.accept()
else:
e.ignore()
def dropEvent(self, e):
self.setText(e.mimeData().text())
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
edit = QLineEdit('', self)
edit.setDragEnabled(True)
edit.move(30, 65)
button = Button("Button", self)
button.move(190, 65)
self.setWindowTitle('Simple drag and drop')
self.setGeometry(300, 300, 300, 150)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
ex.show()
app.exec_()
```
```text
class Button(QPushButton):
def __init__(self, title, parent):
super().__init__(title, parent)
self.setAcceptDrops(True)
```
为了完成预定目标,我们要重构一些方法。首先用`QPushButton`上构造一个按钮实例。
```text
self.setAcceptDrops(True)
```
激活组件的拖拽事件。
```text
def dragEnterEvent(self, e):
if e.mimeData().hasFormat('text/plain'):
e.accept()
else:
e.ignore()
```
首先,我们重构了`dragEnterEvent()`方法。设定好接受拖拽的数据类型plain text
```text
def dropEvent(self, e):
self.setText(e.mimeData().text())
```
然后重构`dropEvent()`方法,更改按钮接受鼠标的释放事件的默认行为。
```text
edit = QLineEdit('', self)
edit.setDragEnabled(True)
```
`QLineEdit`默认支持拖拽操作,所以我们只要调用`setDragEnabled()`方法使用就行了。
程序展示:
![drag &amp; drop](images/8-dragdrop.png)
## 拖放按钮组件
这个例子展示怎么拖放一个button组件。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this program, we can press on a button with a left mouse
click or drag and drop the button with the right mouse click.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import QPushButton, QWidget, QApplication
from PyQt5.QtCore import Qt, QMimeData
from PyQt5.QtGui import QDrag
import sys
class Button(QPushButton):
def __init__(self, title, parent):
super().__init__(title, parent)
def mouseMoveEvent(self, e):
if e.buttons() != Qt.RightButton:
return
mimeData = QMimeData()
drag = QDrag(self)
drag.setMimeData(mimeData)
drag.setHotSpot(e.pos() - self.rect().topLeft())
dropAction = drag.exec_(Qt.MoveAction)
def mousePressEvent(self, e):
super().mousePressEvent(e)
if e.button() == Qt.LeftButton:
print('press')
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setAcceptDrops(True)
self.button = Button('Button', self)
self.button.move(100, 65)
self.setWindowTitle('Click or Move')
self.setGeometry(300, 300, 280, 150)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
position = e.pos()
self.button.move(position)
e.setDropAction(Qt.MoveAction)
e.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
ex.show()
app.exec_()
```
上面的例子中,窗口上有一个`QPushButton`组件。左键点击按钮,控制台就会输出`press`。右键可以点击然后拖动按钮。
```text
class Button(QPushButton):
def __init__(self, title, parent):
super().__init__(title, parent)
```
`QPushButton`继承一个`Button`类,然后重构`QPushButton`的两个方法:`mouseMoveEvent()``mousePressEvent()`.`mouseMoveEvent()`是拖拽开始的事件。
```text
if e.buttons() != Qt.RightButton:
return
```
我们只劫持按钮的右键事件,左键的操作还是默认行为。
```text
mimeData = QMimeData()
drag = QDrag(self)
drag.setMimeData(mimeData)
drag.setHotSpot(e.pos() - self.rect().topLeft())
```
创建一个`QDrag`对象用来传输MIME-based数据。
```text
dropAction = drag.exec_(Qt.MoveAction)
```
拖放事件开始时,用到的处理函数式`start()`.
```text
def mousePressEvent(self, e):
QPushButton.mousePressEvent(self, e)
if e.button() == Qt.LeftButton:
print('press')
```
左键点击按钮会在控制台输出“press”。注意我们在父级上也调用了`mousePressEvent()`方法,不然的话,我们是看不到按钮按下的效果的。
```text
position = e.pos()
self.button.move(position)
```
`dropEvent()`方法里,我们定义了按钮按下后和释放后的行为,获得鼠标移动的位置,然后把按钮放到这个地方。
```text
e.setDropAction(Qt.MoveAction)
e.accept()
```
指定放下的动作类型为moveAction。
程序展示:
这个就一个按钮没啥可展示的弄GIF太麻烦了。

View File

@ -0,0 +1,224 @@
# 自定义组件
PyQt5有丰富的组件但是肯定满足不了所有开发者的所有需求PyQt5只提供了基本的组件像按钮文本滑块等。如果你还需要其他的模块应该尝试自己去自定义一些。
自定义组件使用绘画工具创建,有两个基本方式:根据已有的创建或改进;通过自己绘图创建。
## Burning widget
这个组件我们会在NeroK3B或者其他CD/DVD烧录软件中见到。
```python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
ZetCode PyQt5 tutorial
In this example, we create a custom widget.
Author: Jan Bodnar
Website: zetcode.com
Last edited: August 2017
"""
from PyQt5.QtWidgets import (QWidget, QSlider, QApplication,
QHBoxLayout, QVBoxLayout)
from PyQt5.QtCore import QObject, Qt, pyqtSignal
from PyQt5.QtGui import QPainter, QFont, QColor, QPen
import sys
class Communicate(QObject):
updateBW = pyqtSignal(int)
class BurningWidget(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setMinimumSize(1, 30)
self.value = 75
self.num = [75, 150, 225, 300, 375, 450, 525, 600, 675]
def setValue(self, value):
self.value = value
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawWidget(qp)
qp.end()
def drawWidget(self, qp):
MAX_CAPACITY = 700
OVER_CAPACITY = 750
font = QFont('Serif', 7, QFont.Light)
qp.setFont(font)
size = self.size()
w = size.width()
h = size.height()
step = int(round(w / 10))
till = int(((w / OVER_CAPACITY) * self.value))
full = int(((w / OVER_CAPACITY) * MAX_CAPACITY))
if self.value >= MAX_CAPACITY:
qp.setPen(QColor(255, 255, 255))
qp.setBrush(QColor(255, 255, 184))
qp.drawRect(0, 0, full, h)
qp.setPen(QColor(255, 175, 175))
qp.setBrush(QColor(255, 175, 175))
qp.drawRect(full, 0, till-full, h)
else:
qp.setPen(QColor(255, 255, 255))
qp.setBrush(QColor(255, 255, 184))
qp.drawRect(0, 0, till, h)
pen = QPen(QColor(20, 20, 20), 1,
Qt.SolidLine)
qp.setPen(pen)
qp.setBrush(Qt.NoBrush)
qp.drawRect(0, 0, w-1, h-1)
j = 0
for i in range(step, 10*step, step):
qp.drawLine(i, 0, i, 5)
metrics = qp.fontMetrics()
fw = metrics.width(str(self.num[j]))
qp.drawText(i-fw/2, h/2, str(self.num[j]))
j = j + 1
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
OVER_CAPACITY = 750
sld = QSlider(Qt.Horizontal, self)
sld.setFocusPolicy(Qt.NoFocus)
sld.setRange(1, OVER_CAPACITY)
sld.setValue(75)
sld.setGeometry(30, 40, 150, 30)
self.c = Communicate()
self.wid = BurningWidget()
self.c.updateBW[int].connect(self.wid.setValue)
sld.valueChanged[int].connect(self.changeValue)
hbox = QHBoxLayout()
hbox.addWidget(self.wid)
vbox = QVBoxLayout()
vbox.addStretch(1)
vbox.addLayout(hbox)
self.setLayout(vbox)
self.setGeometry(300, 300, 390, 210)
self.setWindowTitle('Burning widget')
self.show()
def changeValue(self, value):
self.c.updateBW.emit(value)
self.wid.repaint()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
```
本例中,我们使用了`QSlider`和一个自定义组件由进度条控制。显示的有物体也就是CD/DVD的总容量和剩余容量。进度条的范围是1~750。如果值达到了700OVER\_CAPACITY就显示为红色代表了烧毁了的意思。
烧录组件在窗口的底部,这个组件是用`QHBoxLayout``QVBoxLayout`组成的。
```python
class BurningWidget(QWidget):
def __init__(self):
super().__init__()
```
基于`QWidget`组件。
```text
self.setMinimumSize(1, 30)
```
修改组件进度条的高度,默认的有点小。
```text
font = QFont('Serif', 7, QFont.Light)
qp.setFont(font)
```
使用比默认更小一点的字体,这样更配。
```text
size = self.size()
w = size.width()
h = size.height()
step = int(round(w / 10.0))
till = int(((w / 750.0) * self.value))
full = int(((w / 750.0) * 700))
```
动态的渲染组件,随着窗口的大小而变化,这就是我们计算窗口大小的原因。最后一个参数决定了组件的最大范围,进度条的值是由窗口大小按比例计算出来的。最大值的地方填充的是红色。注意这里使用的是浮点数,能提高计算和渲染的精度。
绘画由三部分组成,黄色或红色区域和黄色矩形,然后是分割线,最后是添上代表容量的数字。
```text
metrics = qp.fontMetrics()
fw = metrics.width(str(self.num[j]))
qp.drawText(i-fw/2, h/2, str(self.num[j]))
```
这里使用字体去渲染文本。必须要知道文本的宽度,这样才能让文本的中间点正好落在竖线上。
```text
def changeValue(self, value):
self.c.updateBW.emit(value)
self.wid.repaint()
```
拖动滑块的时候,调用了`changeValue()`方法。这个方法内部我们自定义了一个可以传参的updateBW信号。参数就是滑块的当前位置。这个数值之后还用来于Burning组件然后重新渲染Burning组件。
![burning widget](images/10-burning.png)

View File

@ -0,0 +1,2 @@
# pyqt6

View File

@ -0,0 +1,224 @@
# PyQt6 自定义部件
*最后更新于 2021.05.17*
PyQt6 已经有丰富的部件,但是没有任何工具包能提供开发者开发应用中需要的全部部件。工具包通常只提供最常见的小部件,如按钮、文本小部件或滑块。如果需要满足特定需求的小部件,我们必须自己创建。
自定义小部件是使用工具包提供的绘图工具创建的。基本上有两种方式:程序员可以修改或增强现有的小部件,或者他可以从头开始创建自定义小部件。
## PyQt6 烧录部件
这个部件可以在 Nero、K3B 或其他的 CD/DVD 烧录软件里看到。
``` python
# file: burning_widget.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example, we create a custom widget.
Author: Jan Bodnar
Website: zetcode.com
"""
from PyQt6.QtWidgets import (QWidget, QSlider, QApplication,
QHBoxLayout, QVBoxLayout)
from PyQt6.QtCore import QObject, Qt, pyqtSignal
from PyQt6.QtGui import QPainter, QFont, QColor, QPen
import sys
class Communicate(QObject):
updateBW = pyqtSignal(int)
class BurningWidget(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setMinimumSize(1, 30)
self.value = 75
self.num = [75, 150, 225, 300, 375, 450, 525, 600, 675]
def setValue(self, value):
self.value = value
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawWidget(qp)
qp.end()
def drawWidget(self, qp):
MAX_CAPACITY = 700
OVER_CAPACITY = 750
font = QFont('Serif', 7, QFont.Weight.Light)
qp.setFont(font)
size = self.size()
w = size.width()
h = size.height()
step = int(round(w / 10))
till = int(((w / OVER_CAPACITY) * self.value))
full = int(((w / OVER_CAPACITY) * MAX_CAPACITY))
if self.value >= MAX_CAPACITY:
qp.setPen(QColor(255, 255, 255))
qp.setBrush(QColor(255, 255, 184))
qp.drawRect(0, 0, full, h)
qp.setPen(QColor(255, 175, 175))
qp.setBrush(QColor(255, 175, 175))
qp.drawRect(full, 0, till - full, h)
else:
qp.setPen(QColor(255, 255, 255))
qp.setBrush(QColor(255, 255, 184))
qp.drawRect(0, 0, till, h)
pen = QPen(QColor(20, 20, 20), 1,
Qt.PenStyle.SolidLine)
qp.setPen(pen)
qp.setBrush(Qt.BrushStyle.NoBrush)
qp.drawRect(0, 0, w - 1, h - 1)
j = 0
for i in range(step, 10 * step, step):
qp.drawLine(i, 0, i, 5)
metrics = qp.fontMetrics()
fw = metrics.horizontalAdvance(str(self.num[j]))
x, y = int(i - fw/2), int(h / 2)
qp.drawText(x, y, str(self.num[j]))
j = j + 1
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
OVER_CAPACITY = 750
sld = QSlider(Qt.Orientation.Horizontal, self)
sld.setFocusPolicy(Qt.FocusPolicy.NoFocus)
sld.setRange(1, OVER_CAPACITY)
sld.setValue(75)
sld.setGeometry(30, 40, 150, 30)
self.c = Communicate()
self.wid = BurningWidget()
self.c.updateBW[int].connect(self.wid.setValue)
sld.valueChanged[int].connect(self.changeValue)
hbox = QHBoxLayout()
hbox.addWidget(self.wid)
vbox = QVBoxLayout()
vbox.addStretch(1)
vbox.addLayout(hbox)
self.setLayout(vbox)
self.setGeometry(300, 300, 390, 210)
self.setWindowTitle('Burning widget')
self.show()
def changeValue(self, value):
self.c.updateBW.emit(value)
self.wid.repaint()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
这个示例中,有一个 `QSlider` 和一个自定义小部件——滑块控制自定义小部件。此小部件以图形方式显示介质的总容量和可用的可用空间。自定义小部件的最小值为 1最大值为 `OVER_CAPACITY`。 如果值达到 `MAX_CAPACITY` ,会变成红色,代表需要烧录的数据大于介质的容量。
烧录部件位于窗口底部。用 `QHBoxLayout``QVBoxLayout` 实现。
```python
class BurningWidget(QWidget):
def __init__(self):
super().__init__()
```
烧录组件基于 `QWidget`
```python
self.setMinimumSize(1, 30)
```
设置部件的高,默认的高度有点小。
```python
font = QFont('Serif', 7, QFont.Weight.Light)
qp.setFont(font)
```
这里使用了较小的字体大小,这样看起来更适合我们的需求。
```python
size = self.size()
w = size.width()
h = size.height()
step = int(round(w / 10))
till = int(((w / OVER_CAPACITY) * self.value))
full = int(((w / OVER_CAPACITY) * MAX_CAPACITY))
```
部件是动态渲染的。窗口越大,部件就越大,反之亦然。所以我们需要动态计算部件的大小必须计算在其上绘制自定义小部件的小部件的大小。参数 `till` 决定了部件的总大小,这个值来自于滑块部件,它是相对整个区域的一个比例。参数 `full` 是红色色块的起点。
绘图包括三个步骤,先绘制有黄色或红色和黄色的矩形,然后绘制垂直线,将小部件分成几个部分,最后画出表示介质容量的数字。
``` python
metrics = qp.fontMetrics()
fw = metrics.horizontalAdvance(str(self.num[j]))
x, y = int(i - fw/2), int(h / 2)
qp.drawText(x, y, str(self.num[j]))
```
我们使用字体材料来绘制文本,所以必须知道文本的宽度才能使其垂直居中。
``` python
def changeValue(self, value):
self.c.updateBW.emit(value)
self.wid.repaint()
```
移动滑块时,调用 `changeValue` 方法。在方法内部,触发一个带有参数的自定义 `updateBW` 信号,参数是滑块的当前值,这个值也要用于计算要绘制的 `Burning` 小部件的容量,这样,这个部件就绘制出来了。
![The burning widget](./images/burning.png)
图示: 烧录组件
本章的 PyQt6 教程里,我们创建了一个自定义部件。

View File

@ -0,0 +1,359 @@
# PyQt6 日期和时间
*最后更新于 2021.04.23*
## QDate, QTime, QDateTime
PyQt6 有 `QDate`, `QDateTime`, `QTime` 类处理日期和时间。`QDate` 是用于处理公历中的日期的类。它有获取、比较或操作日期的方法。`QTime` 类用于处理时间。它提供了比较时间、确定时间和其他各种时间操作方法。`QDateTime``QDate``QTime` 的组合。
## PyQt 当前日期和时间
PyQt6 有 `currentDate``currentTime``currentDateTime` 方法获取当前的日期或时间。
``` python
# file: current_date_time.py
#!/usr/bin/python
from PyQt6.QtCore import QDate, QTime, QDateTime, Qt
now = QDate.currentDate()
print(now.toString(Qt.DateFormat.ISODate))
print(now.toString(Qt.DateFormat.RFC2822Date))
datetime = QDateTime.currentDateTime()
print(datetime.toString())
time = QTime.currentTime()
print(time.toString(Qt.DateFormat.ISODate))
```
上面的代码打印出了当前日期,当前日期和时间,不同格式的时间。
``` python
now = QDate.currentDate()
```
`currentDate` 方法返回当前的日期。
``` python
print(now.toString(Qt.DateFormat.ISODate))
print(now.toString(Qt.DateFormat.RFC2822Date))
```
`toString` 传入不同的参数: `Qt.DateFormat.ISODate``Qt.DateFormat.RFC2822Date` 获取不同格式的日期。
```python
datetime = QDateTime.currentDateTime()
```
`currentDateTime` 方法返回当前的日期和时间。
``` python
time = QTime.currentTime()
```
`currentTime` 方法返回了当前时间。
``` sh
$ ./current_date_time.py
2021-04-23
23 Apr 2021
Fri Apr 23 11:41:39 2021
11:41:39
```
## PyQt6 UTC 时间
我们的星球是一个球体绕着它自己的轴旋转。地球向东旋转所以太阳在不同的时间在不同的地点升起。地球大约每24小时自转一次。因此世界被划分为24个时区。在每个时区都有不同的当地时间。当地时间通常会被夏时制进一步修改。
实际上也需要一个标准时间。一个标准时间有助于避免时区和夏令时的混淆。选择UTC(通用协调时间)作为主要的时间标准。UTC时间用于航空、天气预报、飞行计划、空中交通管制许可和地图。与当地时间不同UTC时间不随季节变化而变化。
``` python
# file: utc_local.py
#!/usr/bin/python
from PyQt6.QtCore import QDateTime, Qt
now = QDateTime.currentDateTime()
print('Local datetime: ', now.toString(Qt.DateFormat.ISODate))
print('Universal datetime: ', now.toUTC().toString(Qt.DateFormat.ISODate))
print(f'The offset from UTC is: {now.offsetFromUtc()} seconds')
本例获取了标准时间和本地时间。
print('Local datetime: ', now.toString(Qt.DateFormat.ISODate))
```
`currentDateTime` 方法返回了本地时间的当前时间。我们可以使用 `toLocalTime` 方法把标准时间转换成本地时间。
``` python
print('Universal datetime: ', now.toUTC().toString(Qt.DateFormat.ISODate))
```
我们使用 `toUTC` 方法从时间对象里获取了标准时间。
``` python
print(f'The offset from UTC is: {now.offsetFromUtc()} seconds')
```
`offsetFromUtc` 方法给出了本地时间与标准时间的差,以秒为单位。
``` sh
$ ./utc_local.py
Local datetime: 2021-04-23T11:44:15
Universal datetime: 2021-04-23T09:44:15Z
The offset from UTC is: 7200 seconds
```
## PyQt6 天数
`daysInMonth` 方法返回了指定月份的天数,`daysInYear` 方法返回了指定年份的天数。
``` python
# file: n_of_days.py
#!/usr/bin/python
from PyQt6.QtCore import QDate
now = QDate.currentDate()
d = QDate(1945, 5, 7)
print(f'Days in month: {d.daysInMonth()}')
print(f'Days in year: {d.daysInYear()}')
```
本例打印了指定年份和月份的天数。
``` sh
$ ./n_of_days.py
Days in month: 31
Days in year: 365
```
## PyQt6 天数差
`daysTo` 方法返回了一个日期到另外一个日期的差。
``` python
# file: xmas.py
#!/usr/bin/python
from PyQt6.QtCore import QDate, Qt
now = QDate.currentDate()
y = now.year()
print(f'today is {now.toString(Qt.DateFormat.ISODate)}')
xmas1 = QDate(y-1, 12, 25)
xmas2 = QDate(y, 12, 25)
dayspassed = xmas1.daysTo(now)
print(f'{dayspassed} days have passed since last XMas')
nofdays = now.daysTo(xmas2)
print(f'There are {nofdays} days until next XMas')
```
该示例计算出了从上一个圣诞节到下一个圣诞节的天数。
``` sh
$ ./xmas.py
today is 2021-04-23
119 days have passed since last XMas
There are 246 days until next XMas
```
## PyQt6 时间的计算
我们经常需要对天,秒或者年进行加减等计算。
``` python
# file: arithmetic.py
#!/usr/bin/python
from PyQt6.QtCore import QDateTime, Qt
now = QDateTime.currentDateTime()
print(f'Today: {now.toString(Qt.DateFormat.ISODate)}')
print(f'Adding 12 days: {now.addDays(12).toString(Qt.DateFormat.ISODate)}')
print(f'Subtracting 22 days: {now.addDays(-22).toString(Qt.DateFormat.ISODate)}')
print(f'Adding 50 seconds: {now.addSecs(50).toString(Qt.DateFormat.ISODate)}')
print(f'Adding 3 months: {now.addMonths(3).toString(Qt.DateFormat.ISODate)}')
print(f'Adding 12 years: {now.addYears(12).toString(Qt.DateFormat.ISODate)}')
```
该示例展示了对当前日期时间添加或减去天、秒、月或年。
``` sh
$ ./arithmetic.py
Today: 2021-04-23T12:06:10
Adding 12 days: 2021-05-05T12:06:10
Subtracting 22 days: 2021-04-01T12:06:10
Adding 50 seconds: 2021-04-23T12:07:00
Adding 3 months: 2021-07-23T12:06:10
Adding 12 years: 2033-04-23T12:06:10
```
## PyQt6 daylight saving time
夏令时 (DST) 是在夏季调快时间,使晚上变得更长。 初春时调前调一小时,秋季时调后调至标准时间。
``` python
# file: daylight_saving.py
#!/usr/bin/python
from PyQt6.QtCore import QDateTime, QTimeZone, Qt
now = QDateTime.currentDateTime()
print(f'Time zone: {now.timeZoneAbbreviation()}')
if now.isDaylightTime():
print('The current date falls into DST time')
else:
print('The current date does not fall into DST time')
```
该例判断一个时间是不是夏令时。
```python
print(f'Time zone: {now.timeZoneAbbreviation()}')
```
`timeZoneAbbreviation` 方法返回了时区的缩写。
``` python
if now.isDaylightTime():
...
```
`isDaylightTime` 判断日期是不是夏令时。
``` sh
$ ./daylight_saving.py
Time zone: CEST
The current date falls into DST time
```
当前日期属于夏令时时间,中欧城市布拉迪斯拉发在夏季执行。 中欧夏令时 (CEST) 比世界时间早 2 小时。 该时区是夏令时时区,用于欧洲和南极洲。
## PyQt6 unix 纪元
纪元是被选为特定纪元起源的时间瞬间。 例如,在西方基督教国家,时间纪元从耶稣诞生的第 0 天开始。 另一个例子是使用了十二年的法国共和历。 这个时代是共和时代的开始1792 年 9 月 22 日宣布第一共和国成立,君主制也被废除。
计算机也有它的时代。 最受欢迎的时代之一是 Unix 时代。 Unix 纪元是 1970 年 1 月 1 日 UTC 时间 00:00:00或 1970-01-01T00:00:00Z ISO 8601。 计算机中的日期和时间是根据自该计算机或平台定义的纪元以来经过的秒数或时钟滴答数确定的。
Unix 时间是自 Unix 纪元以来经过的秒数。
``` sh
$ date +%s
1619172620
```
Unix date 命令可用于获取 Unix 时间。 在这个特殊的时刻,自 Unix 时代以来已经过去了 1619172620 秒。
``` python
# file: unix_time.py
#!/usr/bin/python
from PyQt6.QtCore import QDateTime, Qt
now = QDateTime.currentDateTime()
unix_time = now.toSecsSinceEpoch()
print(unix_time)
d = QDateTime.fromSecsSinceEpoch(unix_time)
print(d.toString(Qt.DateFormat.ISODate))
```
该例展示了 Unix 时间,并把它转换成了 `QDateTime`
``` python
now = QDateTime.currentDateTime()
```
首先获取当前的日期和时间。
```python
unix_time = now.toSecsSinceEpoch()
```
`toSecsSinceEpoch` 返回了 Unix 时间。
```python
d = QDateTime.fromSecsSinceEpoch(unix_time)
```
使用 `fromSecsSinceEpoch` 方法把 Unix 时间转换成 `QDateTime`
``` sh
$ ./unix_time.py
1619172679
2021-04-23T12:11:19
```
## PyQt6 Julian day
儒略日是指自儒略时期开始以来的连续天数。 它主要由天文学家使用。 它不应与儒略历混淆。 它开始于公元前 4713 年。第 0 天是公元前 4713 年 1 月 1 日中午的那天。
儒略日数 (JDN) 是自此期间开始以来经过的天数。 任何时刻的儒略日期 (JD) 是前一个中午的儒略日数加上自该时刻起当天的分数。 Qt 不计算这个分数。)除了天文学,儒略日期经常被军事和大型机程序使用。
``` python
# file: julian_day.py
#!/usr/bin/python
from PyQt6.QtCore import QDate, Qt
now = QDate.currentDate()
print('Gregorian date for today:', now.toString(Qt.DateFormat.ISODate))
print('Julian day for today:', now.toJulianDay())
```
该例中,我们得到了今天在公历中的表达和在儒略日的表达。
``` python
print('Julian day for today:', now.toJulianDay())
```
`toJulianDay()` 返回了儒略日的日期。
``` sh
$ ./julian_day.py
Gregorian date for today: 2021-04-23
Julian day for today: 2459328
```
## 历史战役
使用儒略日可以进行跨越几个世纪的计算。
``` python
# file: battles.py
#!/usr/bin/python
from PyQt6.QtCore import QDate, Qt
borodino_battle = QDate(1812, 9, 7)
slavkov_battle = QDate(1805, 12, 2)
now = QDate.currentDate()
j_today = now.toJulianDay()
j_borodino = borodino_battle.toJulianDay()
j_slavkov = slavkov_battle.toJulianDay()
d1 = j_today - j_slavkov
d2 = j_today - j_borodino
print(f'Days since Slavkov battle: {d1}')
print(f'Days since Borodino battle: {d2}')
```
该示例计算自两个历史事件以来经过的天数。
``` python
borodino_battle = QDate(1812, 9, 7)
slavkov_battle = QDate(1805, 12, 2)
```
这是两个拿破仑战斗日期。
```python
j_today = now.toJulianDay()
j_borodino = borodino_battle.toJulianDay()
j_slavkov = slavkov_battle.toJulianDay()
```
这是今天以及斯拉夫科夫和博罗季诺战役的儒略日。
``` python
d1 = j_today - j_slavkov
d2 = j_today - j_borodino
```
这是两次大战的天数差。
``` sh
$ ./battles.py
Days since Slavkov battle: 78670
Days since Borodino battle: 76199
```
当我们运行这个脚本时,距离斯拉夫科夫战役已经过去了 78670 天,距离博罗季诺战役已经过去了 76199 天。
在 PyQt6 教程的这一部分中,我们使用了日期和时间。

364
translated/pyqt6/dialogs.md Normal file
View File

@ -0,0 +1,364 @@
# PyQt6 的对话框
*最后更新于 2021.04.30*
对话是两个或更多人之间的交谈。在计算机程序中,对话框是用于与应用程序“交谈”的窗口,用于诸如从用户那里获取数据或更改应用程序设置之类的事情。
## PyQt6 QInputDialog
`QInputDialog` 提供了一个简单方便的对话框来从用户那里获取输入。输入值可以是字符串、数字或列表中的项目。
``` python
# file: input_dialog.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example, we receive data from
a QInputDialog dialog.
Aauthor: Jan Bodnar
Website: zetcode.com
"""
from PyQt6.QtWidgets import (QWidget, QPushButton, QLineEdit,
QInputDialog, QApplication)
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.btn = QPushButton('Dialog', self)
self.btn.move(20, 20)
self.btn.clicked.connect(self.showDialog)
self.le = QLineEdit(self)
self.le.move(130, 22)
self.setGeometry(300, 300, 450, 350)
self.setWindowTitle('Input dialog')
self.show()
def showDialog(self):
text, ok = QInputDialog.getText(self, 'Input Dialog',
'Enter your name:')
if ok:
self.le.setText(str(text))
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
这个示例有一个按钮和行内编辑部件,按钮打开输入一个对话框,对话框里有一个文本输入框,用户输入的文本会显示在行内编辑部件里。
``` python
text, ok = QInputDialog.getText(self, 'Input Dialog',
'Enter your name:')
```
这行代码打开了输入对话框,第一个参数是对话框标题,第二个参数是对话框里的提示信息。对话框会返回输入的文本和一个布尔值。如果点击 OK 按钮,这个布尔值是 `true`
``` python
if ok:
self.le.setText(str(text))
```
使用 `setText()` 从对话框里获取输入的文本。
![Input dialog](./images/inputdialog.png)
图示:输入框
## PyQt6 QColorDialog
`QColorDialog` 是可以选择颜色对话框。
``` python
# file: color_dialog.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example, we select a color value
from the QColorDialog and change the background
color of a QFrame widget.
Author: Jan Bodnar
Website: zetcode.com
"""
from PyQt6.QtWidgets import (QWidget, QPushButton, QFrame,
QColorDialog, QApplication)
from PyQt6.QtGui import QColor
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
col = QColor(0, 0, 0)
self.btn = QPushButton('Dialog', self)
self.btn.move(20, 20)
self.btn.clicked.connect(self.showDialog)
self.frm = QFrame(self)
self.frm.setStyleSheet("QWidget { background-color: %s }"
% col.name())
self.frm.setGeometry(130, 22, 200, 200)
self.setGeometry(300, 300, 450, 350)
self.setWindowTitle('Color dialog')
self.show()
def showDialog(self):
col = QColorDialog.getColor()
if col.isValid():
self.frm.setStyleSheet("QWidget { background-color: %s }"
% col.name())
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
示例里有个按钮和一个 `QFrame`。部件的背景颜色是默认颜色,可以使用 `QColorDialog` 修改部件的背景颜色。
``` python
col = QColor(0, 0, 0)
```
这是 `QFrame` 的初始背景色。
``` python
col = QColorDialog.getColor()
```
这一行弹出 `QColorDialog`
``` python
if col.isValid():
self.frm.setStyleSheet("QWidget { background-color: %s }"
% col.name())
```
这里检查了颜色是不是有效的。如果点击取消按钮,没有返回可用的颜色值。如果返回的颜色是有效值,就使用样式表修改背景颜色。
## PyQt6 QFontDialog
`QFontDialog` 是选择字体的对话框。
``` python
# file: font_dialog.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example, we select a font name
and change the font of a label.
Author: Jan Bodnar
Website: zetcode.com
"""
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QPushButton,
QSizePolicy, QLabel, QFontDialog, QApplication)
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
vbox = QVBoxLayout()
btn = QPushButton('Dialog', self)
btn.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
btn.move(20, 20)
vbox.addWidget(btn)
btn.clicked.connect(self.showDialog)
self.lbl = QLabel('Knowledge only matters', self)
self.lbl.move(130, 20)
vbox.addWidget(self.lbl)
self.setLayout(vbox)
self.setGeometry(300, 300, 450, 350)
self.setWindowTitle('Font dialog')
self.show()
def showDialog(self):
font, ok = QFontDialog.getFont()
if ok:
self.lbl.setFont(font)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
本例中,有个有文本的按钮。使用 `QFontDialog` 可以修改按钮文本的字体。
``` python
font, ok = QFontDialog.getFont()
```
这里弹出了字体选择对话框。`getFont` 方法返回了选择的字体名称和 `ok` 参数,如果点击 Ok 按钮,`ok` 的值是 `True`,反则是 `False`
``` python
if ok:
self.label.setFont(font)
```
如果点击 Ok 按钮,`setFont` 方法会修改文本的字体。
## PyQt6 QFileDialog
`QFileDialog` 是选择文件或者文件夹的对话框,可以用作选择或者保存操作。
``` python
# file: file_dialog.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example, we select a file with a
QFileDialog and display its contents
in a QTextEdit.
Author: Jan Bodnar
Website: zetcode.com
"""
from PyQt6.QtWidgets import (QMainWindow, QTextEdit,
QFileDialog, QApplication)
from PyQt6.QtGui import QIcon, QAction
from pathlib import Path
import sys
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.textEdit = QTextEdit()
self.setCentralWidget(self.textEdit)
self.statusBar()
openFile = QAction(QIcon('open.png'), 'Open', self)
openFile.setShortcut('Ctrl+O')
openFile.setStatusTip('Open new File')
openFile.triggered.connect(self.showDialog)
menubar = self.menuBar()
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(openFile)
self.setGeometry(300, 300, 550, 450)
self.setWindowTitle('File dialog')
self.show()
def showDialog(self):
home_dir = str(Path.home())
fname = QFileDialog.getOpenFileName(self, 'Open file', home_dir)
if fname[0]:
f = open(fname[0], 'r')
with f:
data = f.read()
self.textEdit.setText(data)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
该示例有一个有居中显示的文本编辑部件的菜单栏和一个状态栏。菜单项显示了用于选择文件的 `QFileDialog`。文件的内容被加载到文本编辑小部件中。
``` python
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
```
示例是基于 `QMainWindow` 部件,是因为需要把文本编辑部件居中显示。
``` python
home_dir = str(Path.home())
fname = QFileDialog.getOpenFileName(self, 'Open file', home_dir)
```
这里弹出 `QFileDialog``getOpenFileName` 的第一个参数字符串是标题,第二个字符串指定对话框工作目录。我们使用 `path` 模块来确定用户的主目录。默认情况下,文件过滤器设置为所有文件 (*)。
``` python
if fname[0]:
f = open(fname[0], 'r')
with f:
data = f.read()
self.textEdit.setText(data)
```
读取选择文件并把内容放置岛文本编辑部件里。
本章教程,我们学习了对话框。

View File

@ -0,0 +1,286 @@
# PyQt6 中的拖拽操作
*最后更新于 2021.05.15*
本章教程,讲的是 PyQt6 中的拖拽操作。
计算机图形界面中,拖拽操作是点击一个对象不放,把它放在另外一个地方或者另外一个对象上面的操作。一般来说,这会触发很多类型的行为,或者在两个对象上建立多种关系。
在计算机图形用户界面中,拖放是(或支持)点击虚拟对象并将其拖到不同位置或另一个虚拟对象上的动作。 一般来说,它可以用来调用多种动作,或者在两个抽象对象之间创建各种类型的关联。
拖放是图形界面的一部分,使用户能够直观地做复杂的事情。
通常,我们可以拖放两个东西:数据或图形对象。将图像从一个应用程序拖到另一个应用程序,操作的是二进制数据。如果在 Firefox 中拖动一个选项卡并将其移动到另一个位置,操作的是一个图形组件。
## QDrag
`QDrag` 提供对基于 `MIME` 的拖放数据传输的支持。它处理拖放操作的大部分细节。传输的数据包含在 `QMimeData` 对象中
## Simple drag and drop example in PyQt6
In the first example, we have a `QLineEdit` and a `QPushButton`. We drag plain text from the line edit widget and drop it onto the button widget. The button's label will change.
示例中,有一个 `QLineEdit``QPushButton` 部件,我们将纯文本从行编辑小部件拖放到按钮小部件上,以改变按钮的标签。
``` python
# file: simple.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This is a simple drag and
drop example.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import (QPushButton, QWidget,
QLineEdit, QApplication)
class Button(QPushButton):
def __init__(self, title, parent):
super().__init__(title, parent)
self.setAcceptDrops(True)
def dragEnterEvent(self, e):
if e.mimeData().hasFormat('text/plain'):
e.accept()
else:
e.ignore()
def dropEvent(self, e):
self.setText(e.mimeData().text())
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
edit = QLineEdit('', self)
edit.setDragEnabled(True)
edit.move(30, 65)
button = Button("Button", self)
button.move(190, 65)
self.setWindowTitle('Simple drag and drop')
self.setGeometry(300, 300, 300, 150)
def main():
app = QApplication(sys.argv)
ex = Example()
ex.show()
app.exec()
if __name__ == '__main__':
main()
```
示例展示了简单的拖拽操作。
``` python
class Button(QPushButton):
def __init__(self, title, parent):
super().__init__(title, parent)
...
```
为了完成把文本拖到 `QPushButton` 部件上,我们必须实现某些方法才可以,所以这里创建了一个继承自 `QPushButton``Button` 类。
``` python
self.setAcceptDrops(True)
```
使用 `setAcceptDrops` 方法处理部件的释放事件。
```python
def dragEnterEvent(self, e):
if e.mimeData().hasFormat('text/plain'):
e.accept()
else:
e.ignore()
```
`dragEnterEvent` 方法,定义了我们接受的数据类型————纯文本。
```python
def dropEvent(self, e):
self.setText(e.mimeData().text())
```
`dropEvent` 方法,处理释放事件————修改按钮组件的文本。
```python
edit = QLineEdit('', self)
edit.setDragEnabled(True)
```
`QLineEdit` 部件支持拖放操作,这里只需要调用 `setDragEnabled` 方法激活它。
![Simple drag and drop](./images/dragdrop.png)
图片:简单的拖放操作
## 拖放按钮组件
接下来的示例演示了如何拖放按钮组件。
``` python
# file: drag_button.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
在这个程序里,有一个按钮,可以用鼠标左键点击,也可以鼠标右键拖拽
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtCore import Qt, QMimeData
from PyQt6.QtGui import QDrag
from PyQt6.QtWidgets import QPushButton, QWidget, QApplication
class Button(QPushButton):
def __init__(self, title, parent):
super().__init__(title, parent)
def mouseMoveEvent(self, e):
if e.buttons() != Qt.MouseButton.RightButton:
return
mimeData = QMimeData()
drag = QDrag(self)
drag.setMimeData(mimeData)
drag.setHotSpot(e.position().toPoint() - self.rect().topLeft())
dropAction = drag.exec(Qt.DropAction.MoveAction)
def mousePressEvent(self, e):
super().mousePressEvent(e)
if e.button() == Qt.MouseButton.LeftButton:
print('press')
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setAcceptDrops(True)
self.button = Button('Button', self)
self.button.move(100, 65)
self.setWindowTitle('Click or Move')
self.setGeometry(300, 300, 550, 450)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
position = e.position()
self.button.move(position.toPoint())
e.setDropAction(Qt.DropAction.MoveAction)
e.accept()
def main():
app = QApplication(sys.argv)
ex = Example()
ex.show()
app.exec()
if __name__ == '__main__':
main()
```
本例中,窗口里有个 `QPushButton`,鼠标左键点击它,会在控制台打印 'press'消息,鼠标右键可以点击拖拽它。
``` python
class Button(QPushButton):
def __init__(self, title, parent):
super().__init__(title, parent)
```
基于 `QPushButton` 创建了一个 `Button` 类,并实现了两个 `QPushButton` 方法:`mouseMoveEvent``mousePressEvent``mouseMoveEvent` 方法是处理拖放操作开始的地方。
``` python
if e.buttons() != Qt.MouseButton.RightButton:
return
```
定义鼠标右键为触发拖拽操作的按钮,鼠标左键只会触发点击事件。
``` python
drag = QDrag(self)
drag.setMimeData(mimeData)
drag.setHotSpot(e.position().toPoint() - self.rect().topLeft())
```
创建 `QDrag` 对象,以提供基于 MIME 数据类型的拖拽操作。
``` python
dropAction = drag.exec(Qt.DropAction.MoveAction)
```
`drag` 对象的 `exec` 方法执行拖拽操作。
``` python
def mousePressEvent(self, e):
super().mousePressEvent(e)
if e.button() == Qt.MouseButton.LeftButton:
print('press')
```
如果鼠标左键点击按钮,会在控制台打印 'press' 消息,注意,这里在父级上也调用了 `mousePressEvent` 方法,不然按钮按下的动作不会展现出来。
``` python
position = e.pos()
self.button.move(position)
```
`dropEvent` 方法处理鼠标释放按钮后的操作————把组件的位置修改为鼠标当前坐标。
``` python
e.setDropAction(Qt.MoveAction)
e.accept()
```
使用 `setDropAction` 指定拖放操作的类型————鼠标移动。
本章讲述了 PyQt6 中的拖拽操作。

View File

@ -0,0 +1,410 @@
# PyQt6 事件和信号
*最后更新于 2021.04.29*
这部分教程,我们探索 PyQt6 程序中的事件和信号。
## PyQt6 中的事件
GUI 应用程序是事件驱动的。事件主要由应用程序的用户触发,但也可以通过其他方式生成,例如 Internet 连接、窗口管理器或定时器。当我们调用应用程序的 `exec()` 方法时,应用程序进入主循环。 主循环获取事件并将它们发送到对象。
在事件模型里,有三个要素:
- 事件源 event source
- 事件对象 event object
- 事件目标 event target
事件源是状态改变的对象,它会产生事件。*event object*(事件)封装了事件源中的状态变化。 *event target* 是要被通知的对象。事件源对象将处理事件的任务委托给事件目标。
PyQt6 有独特的信号和插槽机制来处理事件,用于对象之间的通信,当特定事件发生时触发。插槽可以是任意可调用的 Python 脚本。当发出连接的信号时,调用插槽脚本
> 译注:可以理解成钩子 (hooks) 或回调函数(callback)。
## PyQt6 信号和插槽
下面的示例展示了 PyQt6 的信号和插槽。
``` python
# file: signals_slots.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
本例中,把 QSlider 触发的事件和 QLCDNumber 插槽绑定起来
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (QWidget, QLCDNumber, QSlider,
QVBoxLayout, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
lcd = QLCDNumber(self)
sld = QSlider(Qt.Orientation.Horizontal, self)
vbox = QVBoxLayout()
vbox.addWidget(lcd)
vbox.addWidget(sld)
self.setLayout(vbox)
sld.valueChanged.connect(lcd.display)
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Signal and slot')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
本例中,展示了 `QtGui.QLCDNumber``QtGui.QSlider`。我们可以通过拖动滑块改变显示器里的数字。
``` python
sld.valueChanged.connect(lcd.display)
```
把滑块的 `valueChanged` 事件和 显示器 `display` 插槽绑定到一起。
*sender* 是触发信号的对象, *receiver* 是接收信号的对象,*slot* 是对信号做出反应的方法。
![信号和插槽](./images/sigslot.png)
图示: 信号和插槽
## PyQt6 重新实现事件处理器
PyQt6里事件的处理器一般都会重新实现。
> 译注:所有的事件处理器都有默认的实现,也就是默认事件。默认事件可能有自己的逻辑,比如拖选,点击,有的可能只是一个空函数。空函数都需要重新覆盖原来的实现,达到事件处理的目的。有默认事件处理函数的,也有可能被覆盖实现,比如禁用自带的拖选,或者重写拖选的效果等。
``` python
# file: reimplement_handler.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
本例中,我们重新实现了一个事件处理器。
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QWidget, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Event handler')
self.show()
def keyPressEvent(self, e):
if e.key() == Qt.Key.Key_Escape.value:
self.close()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
本例中,我们重新实现了 `keyPressEvent` 的事件处理器
``` python
def keyPressEvent(self, e):
if e.key() == Qt.Key.Key_Escape.value:
self.close()
```
按下 Escape 按钮,应用会退出。
## PyQt6 事件对象
事件对象是一个 Python object包含了一系列描述这个事件的属性具体内容要看触发的事件。
``` python
# file: event_object.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
本例中,在标签组件里,展示了鼠标的坐标。
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QWidget, QApplication, QGridLayout, QLabel
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
grid = QGridLayout()
x = 0
y = 0
self.text = f'x: {x}, y: {y}'
self.label = QLabel(self.text, self)
grid.addWidget(self.label, 0, 0, Qt.AlignmentFlag.AlignTop)
self.setMouseTracking(True)
self.setLayout(grid)
self.setGeometry(300, 300, 450, 300)
self.setWindowTitle('Event object')
self.show()
def mouseMoveEvent(self, e):
x = int(e.position().x())
y = int(e.position().y())
text = f'x: {x}, y: {y}'
self.label.setText(text)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
本例中,在标签组件里,展示了鼠标的坐标。
``` python
self.setMouseTracking(True)
```
鼠标跟踪默认是关闭的,鼠标移动时,组件只能在鼠标按下的时候接收到事件。开启鼠标跟踪,只移动鼠标不按下鼠标按钮,也能接收到事件。
``` python
def mouseMoveEvent(self, e):
x = int(e.position().x())
y = int(e.position().y())
...
```
`e` 是事件对象,它包含了事件触发时候的数据。通过 `position().x()``e.position().y()` 方法,能获取到鼠标的坐标值。
``` python
self.text = f'x: {x}, y: {y}'
self.label = QLabel(self.text, self)
```
坐标值 x 和 y 显示在 `QLabel` 组件里。
![事件对象](./images/eventobject.png)
图示: 事件对象
# PyQt6 事件触发者
某些时候需要知道事件的触发者是谁PyQt6 有获取事件触发者的方法。
``` python
event_sender.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example, we determine the event sender
object.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QMainWindow, QPushButton, QApplication
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
btn1 = QPushButton("Button 1", self)
btn1.move(30, 50)
btn2 = QPushButton("Button 2", self)
btn2.move(150, 50)
btn1.clicked.connect(self.buttonClicked)
btn2.clicked.connect(self.buttonClicked)
self.statusBar()
self.setGeometry(300, 300, 450, 350)
self.setWindowTitle('Event sender')
self.show()
def buttonClicked(self):
sender = self.sender()
msg = f'{sender.text()} was pressed'
self.statusBar().showMessage(msg)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
本例中有两个按钮。 `buttonClicked` 调用触发者方法确定了是哪个按钮触发的事件。
``` python
btn1.clicked.connect(self.buttonClicked)
btn2.clicked.connect(self.buttonClicked)
```
两个按钮绑定了同一个插槽。
``` python
def buttonClicked(self):
sender = self.sender()
msg = f'{sender.text()} was pressed'
self.statusBar().showMessage(msg)
```
在应用的状态栏里,显示了是哪个按钮被按下。
![事件触发者](./images/eventsender.png)
图示: 事件触发者
## PyQt6 触发信号
`QObject` 可以主动触发信号。下面的示例显示了如果触发自定义信号。
``` python
# file: custom_signal.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example, we show how to
emit a custom signal.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtCore import pyqtSignal, QObject
from PyQt6.QtWidgets import QMainWindow, QApplication
class Communicate(QObject):
closeApp = pyqtSignal()
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.c = Communicate()
self.c.closeApp.connect(self.close)
self.setGeometry(300, 300, 450, 350)
self.setWindowTitle('Emit signal')
self.show()
def mousePressEvent(self, e):
self.c.closeApp.emit()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
创建了一个叫 `closeApp` 的信号,在鼠标按下的时候触发,和关闭插槽 `QMainWindow` 绑定。
``` python
class Communicate(QObject):
closeApp = pyqtSignal()
```
外部 Communicate 类的属性 `pyqtSignal` 创建信号。
``` python
self.c = Communicate()
self.c.closeApp.connect(self.close)
```
自定义信号 `closeApp` 绑定到 `QMainWindow` 的关闭插槽上。
``` python
def mousePressEvent(self, event):
self.c.closeApp.emit()
```
在窗口上点击鼠标按钮的时候,触发 `closeApp` 信号,程序终止。
本章教程,我们讲述了信号和插槽。

View File

@ -0,0 +1,400 @@
# PyQt6 的第一个程序
*最后更新于 2021.04.23*
在 PyQt6 教程的这一部分中,我们将学习一些基本功能。这些示例显示工具提示和图标、关闭窗口、显示消息框以及在桌面上居中显示窗口。
## PyQt6 简单示例
这是一个显示小窗口的简单示例。但我们可以用这个窗口做很多事情。我们可以调整它的大小、最大化或最小化它。 实现这些功能通常需要写大量的代码但这些功能很常见所以这部分功能已经封装好了我们直接使用即可。PyQt6 是一个高级工具包,如果使用低级的工具包,下面的代码示例很可能需要数百行才可以实现。
``` python
# file: simple.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example, we create a simple
window in PyQt6.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QApplication, QWidget
def main():
app = QApplication(sys.argv)
w = QWidget()
w.resize(250, 200)
w.move(300, 300)
w.setWindowTitle('Simple')
w.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
上面的代码会再屏幕上显示一个小窗口。
``` python
import sys
from PyQt6.QtWidgets import QApplication, QWidget
```
这里引入了必要的包,基础小组件位于 `PyQt6.QtWidgets` 模块。
``` python
app = QApplication(sys.argv)
```
每个 PyQt6 应用程序都必须创建一个应用程序对象。`sys.argv` 参数是来自命令行的参数列表。Python 脚本可以从 shell 运行,这是应用启动的一种方式。
``` python
w = QWidget()
```
QWidget 小部件是 PyQt6 中所有用户界面对象的基类。我们为 QWidget 提供了默认构造函数。默认构造函数没有父级。没有父级的小部件称为窗口。
``` python
w.resize(250, 150)
```
`resize` 方法改变了小部件的尺寸现在它250像素宽150像素高。
``` python
w.move(300, 300)
```
`move` 方法把小部件移动到屏幕的指定坐标(300, 300)。
``` python
w.setWindowTitle('Simple')
```
使用 `setWindowTitle` 给窗口设置标题,标题显示在标题栏。
``` python
w.show()
```
`show` 方法是在屏幕上显示小部件的方法。显示一个部件的步骤是首先在内存里创建,然后在屏幕上显示。
``` python
sys.exit(app.exec())
```
最后,我们进入应用程序的主循环。事件处理从这里开始。主循环从窗口系统接收事件并将它们分派给应用程序小部件。 如果我们调用 exit 方法或主小部件被销毁,则主循环结束。`sys.exit` 方法确保一个干净的退出。环境将被告知应用程序如何结束。
![Simple](./images/simple.png)
图示: Simple
## PyQt6 tooltip
我们可以为程序创建一个气泡提示。
``` python
# file: tooltip.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This example shows a tooltip on
a window and a button.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import (QWidget, QToolTip,
QPushButton, QApplication)
from PyQt6.QtGui import QFont
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
QToolTip.setFont(QFont('SansSerif', 10))
self.setToolTip('This is a <b>QWidget</b> widget')
btn = QPushButton('Button', self)
btn.setToolTip('This is a <b>QPushButton</b> widget')
btn.resize(btn.sizeHint())
btn.move(50, 50)
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Tooltips')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
该示例中,我们给两个小部件创建了一个气泡提示框。
``` python
QToolTip.setFont(QFont('SansSerif', 10))
```
这个静态方法给气泡提示框设置了字体这里使用了10pt 的 SansSerif 字体。
``` python
self.setToolTip('This is a <b>QWidget</b> widget')
```
调用 `setTooltip` 方法创建气泡提示框,可以使用富文本内容。
``` python
btn = QPushButton('Button', self)
btn.setToolTip('This is a <b>QPushButton</b> widget')
```
在气泡提示框上添加了一个按钮部件。
``` python
btn.resize(btn.sizeHint())
btn.move(50, 50)
```
`sizeHint` 方法是给按钮一个系统建议的尺寸,然后使用 `move` 方法移动这个按钮的位置。
![Tooltips](./images/tooltips.png)
Figure: Tooltips
## PyQt6 退出按钮
关闭窗口的明显方法是单击标题栏上的 x 标记。在下一个示例中,我们将展示如何以编程方式关闭窗口。 我们将简要介绍信号和插槽。
本例中使用 `QPushButton` 部件完成这个功能。
``` python
QPushButton(string text, QWidget parent = None)
```
参数 `text` 是将显示在按钮上的文本。`parent` 是我们放置按钮的小部件。在我们的例子中,它将是一个`QWidget`。应用程序的小部件形成层次结构,在这个层次结构中,大多数小部件都有父级。没有父级的小部件的父级是顶级窗口。
```python
# file: quit_button.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This program creates a quit
button. When we press the button,
the application terminates.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QWidget, QPushButton, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
qbtn = QPushButton('Quit', self)
qbtn.clicked.connect(QApplication.instance().quit)
qbtn.resize(qbtn.sizeHint())
qbtn.move(50, 50)
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Quit button')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
本例创建了一个退出按钮,点击此按钮退出程序。
``` python
qbtn = QPushButton('Quit', self)
```
我们创建了一个按钮,它是 `QPushButton` 类的一个实例。构造函数的第一个参数是按钮的标签。 第二个参数是父级小部件。父小部件是 `Example` 小部件,它继承自 `QWidget`
``` python
qbtn.clicked.connect(QApplication.instance().quit)
```
PyQt6 的事件处理系统是由信号和插槽机制构成的,点击按钮(事件),会发出点击信号。事件处理插槽可以是 Qt 自带的插槽,也可以是普通 Python 函数
使用 `QApplication.instance` 获取的 `QCoreApplication` 对象包含主事件循环————它处理和分派所有事件。 单击的信号连接到终止应用程序的退出方法。 通信是在两个对象之间完成的:发送者和接收者。 发送者是按钮,接收者是应用程序对象。
![Quit button](./images/quitbutton.png)
图示:退出按钮
## PyQt6 弹窗
默认情况下,如果我们点击标题栏上的 x 按钮,`QWidget` 会被关闭。有时我们想修改这个默认行为。 例如,如果在编辑器中打开了一个文件,修改了部分内容,我们需要显示一个消息框来确认退出程序的操作。
``` python
# file: messagebox.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This program shows a confirmation
message box when we click on the close
button of the application window.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QWidget, QMessageBox, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 350, 200)
self.setWindowTitle('Message box')
self.show()
def closeEvent(self, event):
reply = QMessageBox.question(self, 'Message',
"Are you sure to quit?", QMessageBox.StandardButton.Yes |
QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
event.accept()
else:
event.ignore()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
关闭 `QWidget` 操作会产生 `QCloseEvent` 事件。重新实现 `closeEvent` 事件处理,替换部件的默认行为。
``` python
reply = QMessageBox.question(self, 'Message',
"Are you sure to quit?", QMessageBox.Yes |
QMessageBox.No, QMessageBox.No)
```
这里创建了一个带有两个按钮的消息框:是和否。第一个参数是标题栏,第二个参数是对话框显示的消息文本,第三个参数是对话框中的按钮组合,最后一个参数是默认选中的按钮。返回值存储在变量 `reply` 中。
``` python
if reply == QtGui.QMessageBox.Yes:
event.accept()
else:
event.ignore()
```
对返回值进行判断,如果单击 Yes 按钮,执行小部件关闭和终止应用程序,否则我们忽略关闭事件。
![Message box](./images/messagebox.png)
图示:对话框
## PyQt6 窗口居中
下面的脚本会在屏幕上显示一个居中的窗口。
``` python
# file: center.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This program centers a window
on the screen.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QWidget, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.resize(350, 250)
self.center()
self.setWindowTitle('Center')
self.show()
def center(self):
qr = self.frameGeometry()
cp = self.screen().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
`QScreen` 类可以查询屏幕属性。
``` python
self.center()
```
使用自定义 `center` 方法居中显示窗口。
``` python
qr = self.frameGeometry()
```
这样就可以得到一个矩形的窗口,这里可以放置所有类型的窗口。
``` python
cp = self.screen().availableGeometry().center()
```
从屏幕属性里计算出分辨率,然后计算出中心点位置。
``` python
qr.moveCenter(cp)
```
我们已经知道矩形窗口的宽高,只需要把矩形窗口的中心点放置到屏幕窗口的中心点即可。这不会修改矩形窗口的大小。
``` python
self.move(qr.topLeft())
```
把应用窗口的左上方点坐标移动到矩形窗口的左上方,这样就可以居中显示了。
本章教程,我们创建了一个简单的 PyQt6 应用。

14
translated/pyqt6/index.md Normal file
View File

@ -0,0 +1,14 @@
# Python PyQt6
*最后更新于 2021.05.18*
这是一个 PyQt6 教程。本教程是个初、中级教程,学完本教程后,您可以编写一个非常不错的 PyQt6 应用。代码示例在作者的 Github 仓库 [PyQt6-Tutorial-Examples](https://github.com/janbodnar/PyQt6-Tutorial-Examples)。
一本涵盖 PyQt5 库的高级特性的电子书: [Advanced PyQt5 e-book](https://zetcode.com/ebooks/advancedpyqt5/).
## 相关教程
[PyQt5 tutorial](https://zetcode.com/gui/pyqt5/) 是介绍了上个版本的 PyQt 。
[wxPython tutorial](https://zetcode.com/wxpython/) 、 [Python Gtk tutorial](https://zetcode.com/python/gtk/) 和
[Tkinter tutorial](https://zetcode.com/tkinter/) 是关于其他比较流行的 Python GUI 教程。

View File

@ -0,0 +1,63 @@
# PyQt6 简介
*最后更新于 2021.04.22*
本教程是 PyQt6 的入门教程。本教程的目的是让您开始使用 PyQt6 库。
## 关于 PyQt6
PyQt6 Digia 公司的 Qt 程序的 Python 中间件。Qt库是最强大的GUI库之一。PyQt6的官网[www.riverbankcomputing.co.uk/news](https://www.riverbankcomputing.co.uk/news)。PyQt6是由Riverbank Computing公司开发的。~~Digia和RiverBank的关系不太清楚大家多提意见~~
PyQt6 是基于 Python 的一系列模块。它是一个多平台的工具包可以在包括Unix、Windows和Mac OS在内的大部分主要操作系统上运行。PyQt6 有两个许可证,开发人员可以在 GPL 和商业许可之间进行选择。
## 安装 PyQt6
```sh
$ pip install PyQt6
```
我们可以使用 `pip` 工具安装 PyQt6。
## PyQt6 模块
PyQt6 类是由一系列模块组成的,包括如下的模块:
- QtCore
- QtGui
- QtWidgets
- QtDBus
- QtNetwork
- QtHelp
- QtXml
- QtSvg
- QtSql
- QtTest
`QtCore` 模块是非 GUI 的核心库。这个模块用来处理时间、文件、目录、各种类型的数据、流stream、URLsmime 类型、线程和进程。 `QtGui` 有窗口系统集成、事件处理、2D图形基本图像、字体、文本的类。 `QtWidgets` 有创建经典风格的用户界面的类。
`QtDBus` 是使用 D-Bus 处理 IPC 通讯的类。`QtNetwork` 是网络变成类,这些类使网络编程变得更容易,可移植性也更好,方便了 TCP/IP 和 UDP 服务端和客户端编程。 `QtHelp` 包含了创建、查看和搜索文档的类。
`QtXml` 包含了处理 XML 文件的类,实现了 SAX 和 DOM API。`QtSvg` 提供了显示 SVG 的类,可缩放矢量图形(SVG)是一种描述二维图像和图像应用的 XML 语言。`QtSql` 模块提供了数据库的类,`QtTest` 提供了可以对 PyQt6 应用进行单元测试的工具。
## Python
Python 是一种通用的、动态的、面向对象的编程语言。Python语言的设计目的强调程序员的生产力和代码的可读性。它于1991年首次发行。Python 的灵感来自于 ABC、Haskell、Java、Lisp、Icon 和 Perl 编程语言。Python 是一种高级的、通用的、多平台的解释性语言,是一种极简语言,由世界各地的一大群志愿者维护。
官方网站是 https://python.org
## PyQt6 version
`QT_VERSION_STR` 可以显示 Qt 的版本信息,`PYQT_VERSION_STR` 可以显示 PyQt 的版本信息
``` python
# file: version.py
#!/usr/bin/python
from PyQt6.QtCore import QT_VERSION_STR
from PyQt6.QtCore import PYQT_VERSION_STR
print(QT_VERSION_STR)
print(PYQT_VERSION_STR)
```
运行这个脚本可以显示 QT 和 PyQt 的版本。
``` sh
$ ./version.py
6.0.2
6.0.3
```
这个章节介绍的是 PyQt 的工具类库。

360
translated/pyqt6/layout.md Normal file
View File

@ -0,0 +1,360 @@
# PyQt6 的布局管理
*最近更新于 2021.04.27*
布局管理是我们在应用程序窗口中放置小部件的方式。我们可以使用绝对定位或布局类来放置小部件。使用布局管理器管理布局是组织小部件的首选方法。
## 绝对定位
以像素为单位指定每个小部件的位置和大小。在使用绝对定位时,我们必须了解以下局限性:
-如果我们调整窗口大小,窗口小部件的大小和位置不会改变
-应用程序在不同的平台上看起来可能不同,改变应用程序的字体可能会破坏布局
-如果要改变布局,我们必须完全重做我们的布局,这很繁琐耗时
下面的示例以绝对坐标来定位小部件。
``` python
# file: absolute.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This example shows three labels on a window
using absolute positioning.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QWidget, QLabel, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
lbl1 = QLabel('ZetCode', self)
lbl1.move(15, 10)
lbl2 = QLabel('tutorials', self)
lbl2.move(35, 40)
lbl3 = QLabel('for programmers', self)
lbl3.move(55, 70)
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Absolute')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
我们使用 `move` 方法来定位小部件。在本例中也就是标签。我们通过提供x和y坐标来定位。坐标系的起始点在左上角x值从左到右递增。y值从上到下递增。
``` python
lbl1 = QLabel('ZetCode', self)
lbl1.move(15, 10)
The label widget is positioned at x=15 and y=10.
```
![Absolute positioning](./images/absolute.png)
图示:绝对位置
## PyQt6 QHBoxLayout
`QHBoxLayout``QVBoxLayout` 是基本的布局类,用于水平和垂直地排列小部件。
假设我们想在右下角放置两个按钮。为了创建这样的布局,我们使用一个水平框和一个垂直框。为了创造必要的空间,我们添加了一个“拉伸因子”。
``` python
# file: box_layout.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example, we position two push
buttons in the bottom-right corner
of the window.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import (QWidget, QPushButton,
QHBoxLayout, QVBoxLayout, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
okButton = QPushButton("OK")
cancelButton = QPushButton("Cancel")
hbox = QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(okButton)
hbox.addWidget(cancelButton)
vbox = QVBoxLayout()
vbox.addStretch(1)
vbox.addLayout(hbox)
self.setLayout(vbox)
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Buttons')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
窗口的右下角有两个按钮。当我们调整应用程序窗口的大小时,它们仍然在那里。这里使用了 `HBoxLayout``QVBoxLayout`
``` python
okButton = QPushButton("OK")
cancelButton = QPushButton("Cancel")
```
这里创建两个按钮。
``` python
hbox = QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(okButton)
hbox.addWidget(cancelButton)
```
创建一个水平框布局,并添加一个拉伸因子和两个按钮。拉伸在两个按钮之前增加了一个可拉伸的空间,这将把他们推到窗口的右边。
``` python
vbox = QVBoxLayout()
vbox.addStretch(1)
vbox.addLayout(hbox)
```
水平布局被放入垂直布局中。垂直框中的拉伸因子将把带有按钮的水平框推到窗口的底部。
``` python
self.setLayout(vbox)
```
最后,把布局放到窗口中里。
![按钮](./images/buttons.png)
图示:按钮
## PyQt6 QGridLayout
`QGridLayout` 是最常用的布局类,它能把空间分为多行多列。
``` python
# file: calculator.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example, we create a skeleton
of a calculator using QGridLayout.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import (QWidget, QGridLayout,
QPushButton, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
grid = QGridLayout()
self.setLayout(grid)
names = ['Cls', 'Bck', '', 'Close',
'7', '8', '9', '/',
'4', '5', '6', '*',
'1', '2', '3', '-',
'0', '.', '=', '+']
positions = [(i, j) for i in range(5) for j in range(4)]
for position, name in zip(positions, names):
if name == '':
continue
button = QPushButton(name)
grid.addWidget(button, *position)
self.move(300, 150)
self.setWindowTitle('Calculator')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
这里创建了一组按钮。
``` python
grid = QGridLayout()
self.setLayout(grid)
```
`QGridLayout` 实例创建了并把布局设置到窗口中。
``` python
names = ['Cls', 'Bck', '', 'Close',
'7', '8', '9', '/',
'4', '5', '6', '*',
'1', '2', '3', '-',
'0', '.', '=', '+']
```
这些是随后要用到的按钮上的标签。
``` python
positions = [(i,j) for i in range(5) for j in range(4)]
```
创建了给格栅用到的位置。
``` python
for position, name in zip(positions, names):
if name == '':
continue
button = QPushButton(name)
grid.addWidget(button, *position)
```
创建了按钮,并用 `addWidget` 方法添加到布局里。
![计算器骨架](./images/calculator.png)
图示:计算器骨架
## 示例:回复
组件可以跨越多个行和列,下面的示例来演示这个。
``` python
# file: review.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example, we create a bit
more complicated window layout using
the QGridLayout manager.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import (QWidget, QLabel, QLineEdit,
QTextEdit, QGridLayout, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
title = QLabel('Title')
author = QLabel('Author')
review = QLabel('Review')
titleEdit = QLineEdit()
authorEdit = QLineEdit()
reviewEdit = QTextEdit()
grid = QGridLayout()
grid.setSpacing(10)
grid.addWidget(title, 1, 0)
grid.addWidget(titleEdit, 1, 1)
grid.addWidget(author, 2, 0)
grid.addWidget(authorEdit, 2, 1)
grid.addWidget(review, 3, 0)
grid.addWidget(reviewEdit, 3, 1, 5, 1)
self.setLayout(grid)
self.setGeometry(300, 300, 350, 300)
self.setWindowTitle('Review')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
窗口里有三个标签,两个行编辑器和一个文本编辑组件,布局使用了 `QGridLayout`
``` python
grid = QGridLayout()
grid.setSpacing(10)
```
创建一个格栅布局,并设置了组件间的分割空间。
``` python
grid.addWidget(reviewEdit, 3, 1, 5, 1)
```
如果需要往格栅里添加组件,指定组件的跨行和跨列的个数。本例,`reviewEdit` 组件占用了5行。
![Review example](./images/review.png)
图示:评论框的例子
本例展示了 PyQt6 的布局管理。

View File

@ -0,0 +1,523 @@
# PyQt6 的菜单和工具栏
*最后更新于 20201.04.24*
在这部分教程中,我们创建了一个状态栏、菜单栏和工具栏。菜单是位于菜单栏中的一组命令。工具栏有一些按钮和应用程序中的一些常用命令。状态栏显示状态信息,通常位于应用程序窗口的底部。
## PyQt6 QMainWindow
`QMainWindow` 类提供了主程序窗口。在这里可以创建一个具有状态栏、工具栏和菜单栏的经典应用程序框架。
## PyQt6 状态栏
状态栏是显示状态信息的小部件。
``` python
# file: statusbar.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This program creates a statusbar.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QMainWindow, QApplication
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.statusBar().showMessage('Ready')
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Statusbar')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
使用 `QMainWindow` 创建状态栏。
``` python
self.statusBar().showMessage('Ready')
```
使用 `QtGui.QMainWindow` 方法创建状态栏该方法的创建了一个状态栏并返回statusbar对象再调用 `showMessage` 方法在状态栏上显示一条消息。
## PyQt6 简单菜单
菜单栏在GUI应用程序中很常见它是位于各种菜单中的一组命令。(Mac OS 对菜单栏的处理是不同的,要得到类似的结果,我们可以添加下面这行: `menubar.setNativeMenuBar(False)`
``` python
# file: simple_menu.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This program creates a menubar. The
menubar has one menu with an exit action.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QMainWindow, QApplication
from PyQt6.QtGui import QIcon, QAction
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
exitAct = QAction(QIcon('exit.png'), '&Exit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.setStatusTip('Exit application')
exitAct.triggered.connect(QApplication.instance().quit)
self.statusBar()
menubar = self.menuBar()
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(exitAct)
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Simple menu')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
上门的示例中,创建了有一个菜单的菜单栏。这个菜单命令是终止应用,也绑定了快捷键 `Ctrl+Q`。示例中也创建了一个状态栏。
``` python
exitAct = QAction(QIcon('exit.png'), '&Exit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.setStatusTip('Exit application')
```
`QAction` 是行为抽象类,包括菜单栏,工具栏,或自定义键盘快捷方式。在上面的三行中,创建了一个带有特定图标和 'Exit' 标签的行为。此外,还为该行为定义了一个快捷方式。第三行创建一个状态提示,当我们将鼠标指针悬停在菜单项上时,状态栏中就会显示这个提示。
``` python
exitAct.triggered.connect(QApplication.instance().quit)
```
当选择指定的行为时,触发了一个信号,这个信号连接了 `QApplication` 组件的退出操作,这会终止这个应用程序。
``` python
menubar = self.menuBar()
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(exitAct)
```
`menuBar` 方法创建了一个菜单栏,然后使用 `addMenu` 创建一个文件菜单,使用 `addAction` 创建一个行为。
## PyQt6 子菜单
子菜单是位于菜单里的菜单。
``` python
# file: submenu.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This program creates a submenu.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QMainWindow, QMenu, QApplication
from PyQt6.QtGui import QAction
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
menubar = self.menuBar()
fileMenu = menubar.addMenu('File')
impMenu = QMenu('Import', self)
impAct = QAction('Import mail', self)
impMenu.addAction(impAct)
newAct = QAction('New', self)
fileMenu.addAction(newAct)
fileMenu.addMenu(impMenu)
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Submenu')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
本例中,有两个菜单项,一个在 File 菜单里,一个在 File 的 Import 子菜单里。
``` python
impMenu = QMenu('Import', self)
```
使用 `QMenu` 创建一个新菜单。
``` python
impAct = QAction('Import mail', self)
impMenu.addAction(impAct)
```
使用`addAction` 给子菜单添加行为.
![Submenu](./images/submenu.png)
图示:子菜单
## PyQt6 勾选菜单
下面的示例中,创建了一个可以勾选的菜单。
``` python
# file: check_menu.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This program creates a checkable menu.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QMainWindow, QApplication
from PyQt6.QtGui import QAction
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.statusbar = self.statusBar()
self.statusbar.showMessage('Ready')
menubar = self.menuBar()
viewMenu = menubar.addMenu('View')
viewStatAct = QAction('View statusbar', self, checkable=True)
viewStatAct.setStatusTip('View statusbar')
viewStatAct.setChecked(True)
viewStatAct.triggered.connect(self.toggleMenu)
viewMenu.addAction(viewStatAct)
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Check menu')
self.show()
def toggleMenu(self, state):
if state:
self.statusbar.show()
else:
self.statusbar.hide()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
创建只有一个行为的 View 菜单。这个行为用来展现或者隐藏状态栏,如果状态栏可见,菜单是勾选的状态。
``` python
viewStatAct = QAction('View statusbar', self, checkable=True)
```
使用 `checkable` 参数创建一个可以勾选的菜单。
``` python
viewStatAct.setChecked(True)
```
因为状态栏默认可见,所以使用 `setChecked` 方法勾选菜单。
``` python
def toggleMenu(self, state):
if state:
self.statusbar.show()
else:
self.statusbar.hide()
```
根据行为的状态,设置状态栏的状态。
![Check menu](./images/checkmenu.png)
图书:勾选菜单
## PyQt6 上下文菜单
上下文菜单,也称为弹出菜单,是在某些上下文下出现的命令列表。例如,在 Opera 浏览器中,在网页上按下鼠标右键,我们会得到一个上下文菜单,这个菜单上,可以重新加载页面、返回或查看页面源代码。如果右击工具栏,会得到另一个用于管理工具栏的上下文菜单。
``` python
# file: context_menu.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This program creates a context menu.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QMainWindow, QMenu, QApplication
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Context menu')
self.show()
def contextMenuEvent(self, event):
cmenu = QMenu(self)
newAct = cmenu.addAction("New")
openAct = cmenu.addAction("Open")
quitAct = cmenu.addAction("Quit")
action = cmenu.exec(self.mapToGlobal(event.pos()))
if action == quitAct:
QApplication.instance().quit()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
重新实现 `contextMenuEvent` 方法,调出一个上下文菜单。
``` python
action = cmenu.exec(self.mapToGlobal(event.pos()))
```
使用 `exec` 方法调出上下文菜单,通过鼠标事件对象获得鼠标坐标点,再调用 `mapToGlobal` 方法把组件的坐标设置成全局的屏幕坐标。
``` python
if action == quitAct:
QApplication.instance().quit()
```
如果上下文菜单触发的动作是退出动作,就终止程序。
## PyQt6 工具栏
菜单包含了一个应用程序里所有需要使用到的命令,工具栏则是放置常用命令的地方。
``` python
# file: toolbar.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This program creates a toolbar.
The toolbar has one action, which
terminates the application, if triggered.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QMainWindow, QApplication
from PyQt6.QtGui import QIcon, QAction
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
exitAct = QAction(QIcon('exit24.png'), 'Exit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.triggered.connect(QApplication.instance().quit)
self.toolbar = self.addToolBar('Exit')
self.toolbar.addAction(exitAct)
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Toolbar')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
上面的示例中创建了一个简单的状态栏,只有一个行为,关闭应用。
``` python
exitAct = QAction(QIcon('exit24.png'), 'Exit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.triggered.connect(QApplication.instance().quit)
```
和上面菜单栏示例相似,创建一个行为对象,对象有标签,图标和快捷键。然后把 `QApplication` 的退出方法和行为发出的信号绑定。
``` python
self.toolbar = self.addToolBar('Exit')
self.toolbar.addAction(exitAction)
```
使用 `addToolBar` 方法创建工具栏,然后使用 `addAction` 方法添加行为。
![Toolbar](./images/toolbar.png)
图示:工具栏
## PyQt6 主窗口
这是本章最后一个示例,这里创建一个菜单栏,一个工具栏和一个状态栏,并增加一个中心布局组件。
``` python
# file: main_window.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This program creates a skeleton of
a classic GUI application with a menubar,
toolbar, statusbar, and a central widget.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QMainWindow, QTextEdit, QApplication
from PyQt6.QtGui import QIcon, QAction
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
textEdit = QTextEdit()
self.setCentralWidget(textEdit)
exitAct = QAction(QIcon('exit24.png'), 'Exit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.setStatusTip('Exit application')
exitAct.triggered.connect(self.close)
self.statusBar()
menubar = self.menuBar()
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(exitAct)
toolbar = self.addToolBar('Exit')
toolbar.addAction(exitAct)
self.setGeometry(300, 300, 350, 250)
self.setWindowTitle('Main window')
self.show()
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
示例展示了一个经典的 GUI 应用的布局,有一个菜单栏,一个工具栏和一个状态栏。
``` python
textEdit = QTextEdit()
self.setCentralWidget(textEdit)
```
这里创建了一个文本编辑器组件,并把它设置到 `QMainWindow` 的中央。居中布局组件撑满了所有空白的部分。
![Main window](./images/mainwindow.png)
图示:主窗口
本部分 PyQt6 教程,我们展示了菜单,工具栏,状态栏和主程序窗口。

View File

@ -0,0 +1,580 @@
# PyQt6 的绘制
*最后更新于 2021.03.15*
PyQt6绘画系统能够呈现矢量图形、图像和基于字体的文本轮廓。想要更改或增强现有的小部件或者从头创建自定义小部件时需要 PyQt6 工具包提供的绘图 API 进行绘制。
## QPainter
QPainter 在小部件和其他可绘制单元上执行底层绘制。从简单的线条到复杂的形状,它可以画任何东西。
## paintEvent 方法
绘制时由 paintEvent 方法完成的。绘制代码位于 QPainter 对象的开始和结束方法之间。它在小部件和其他绘制单元上执行底层绘制。
## PyQt6 绘制文本
从绘制一些 Unicode 文本开始。
``` python
# file: draw_text.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example, we draw text in Russian Cylliric.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtWidgets import QWidget, QApplication
from PyQt6.QtGui import QPainter, QColor, QFont
from PyQt6.QtCore import Qt
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.text = "Лев Николаевич Толстой\nАнна Каренина"
self.setGeometry(300, 300, 350, 300)
self.setWindowTitle('Drawing text')
self.show()
def paintEvent(self, event):
qp = QPainter()
qp.begin(self)
self.drawText(event, qp)
qp.end()
def drawText(self, event, qp):
qp.setPen(QColor(168, 34, 3))
qp.setFont(QFont('Decorative', 10))
qp.drawText(event.rect(), Qt.AlignmentFlag.AlignCenter, self.text)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
本例中,绘制了一些西里尔字母,并水平和垂直对齐了文本。
``` python
def paintEvent(self, event):
...
```
使用 paintEvent 完成绘画。
``` python
qp = QPainter()
qp.begin(self)
self.drawText(event, qp)
qp.end()
```
`QPainter` 类负责所有的底层绘制。所有的绘制都在开始和结束方法之间。实际的绘制被委托给 drawText 方法。
``` python
qp.setPen(QColor(168, 34, 3))
qp.setFont(QFont('Decorative', 10))
```
这里定义了绘制文本的笔触和字体。
``` python
qp.drawText(event.rect(), Qt.AlignmentFlag.AlignCenter, self.text)
```
drawText 方法在窗口上绘制文本。paintEvent 的rect方法返回需要更新的矩形。用 `Qt.AlignmentFlag.AlignCenter` 在两个维度上对齐文本。
![Drawing text](./images/drawingtext.png)
图示:绘制文本
## PyQt6 绘制点
点是绘制里最简单的图形对象。
``` python
# file: draw_points.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In the example, we draw randomly 1000 red points
on the window.
Author: Jan Bodnar
Website: zetcode.com
"""
from PyQt6.QtWidgets import QWidget, QApplication
from PyQt6.QtGui import QPainter
from PyQt6.QtCore import Qt
import sys, random
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setMinimumSize(50, 50)
self.setGeometry(300, 300, 350, 300)
self.setWindowTitle('Points')
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawPoints(qp)
qp.end()
def drawPoints(self, qp):
qp.setPen(Qt.GlobalColor.red)
size = self.size()
for i in range(1000):
x = random.randint(1, size.width() - 1)
y = random.randint(1, size.height() - 1)
qp.drawPoint(x, y)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
上例中在窗口里绘制了1000个随机的红点。
``` python
qp.setPen(Qt.GlobalColor.red)
```
用预定义 `Qt.GlobalColor.red` 常量常量把笔触设置为红色。
``` python
size = self.size()
```
每次改变窗口大小,都会产生一个绘制事件。获得当前窗口大小,根据这个大小把点分布到窗口上的各个位置。
``` python
qp.drawPoint(x, y)
```
使用 `drawPoint` 方法绘制点。
![Points](./images/points.png)
图示:点
## PyQt6 颜色
颜色是表示红色、绿色和蓝色 (RGB) 强度值组合的对象。有效的 RGB 值的范围是0到255。可以用不同的方法定义一种颜色。最常见的是RGB十进制值或十六进制值。还可以使用 RGBA 值,它代表红色、绿色、蓝色和 Alpha 通道添加了透明度信息。Alpha 值为255定义完全不透明0表示完全透明也就是颜色不可见。
``` python
# file: colours.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This example draws three rectangles in three
different colours.
Author: Jan Bodnar
Website: zetcode.com
"""
from PyQt6.QtWidgets import QWidget, QApplication
from PyQt6.QtGui import QPainter, QColor
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 350, 100)
self.setWindowTitle('Colours')
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawRectangles(qp)
qp.end()
def drawRectangles(self, qp):
col = QColor(0, 0, 0)
col.setNamedColor('#d4d4d4')
qp.setPen(col)
qp.setBrush(QColor(200, 0, 0))
qp.drawRect(10, 15, 90, 60)
qp.setBrush(QColor(255, 80, 0, 160))
qp.drawRect(130, 15, 90, 60)
qp.setBrush(QColor(25, 0, 90, 200))
qp.drawRect(250, 15, 90, 60)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
上例中,绘制了三个不同颜色的矩形。
``` python
color = QColor(0, 0, 0)
color.setNamedColor('#d4d4d4')
```
使用16进制定义颜色。
``` python
qp.setBrush(QColor(200, 0, 0))
qp.drawRect(10, 15, 90, 60)
```
这里定义一个笔刷并绘制一个矩形。画笔是一种基本的图形对象用于绘制形状的背景。drawRect 方法接受四个参数前两个是轴上的x和y值第三和第四个参数是矩形的宽度和高度使用选择的笔触和笔刷绘制矩形。
![Colours](./images/colours.png)
图示:颜色
## PyQt6 QPen
`QPen` 是一个基本图形对象,可以绘制线条,曲线和矩形,椭圆,多边形等形状的轮廓。
``` python
# file: pens.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
In this example we draw 6 lines using
different pen styles.
Author: Jan Bodnar
Website: zetcode.com
"""
from PyQt6.QtWidgets import QWidget, QApplication
from PyQt6.QtGui import QPainter, QPen
from PyQt6.QtCore import Qt
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 280, 270)
self.setWindowTitle('Pen styles')
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawLines(qp)
qp.end()
def drawLines(self, qp):
pen = QPen(Qt.GlobalColor.black, 2, Qt.PenStyle.SolidLine)
qp.setPen(pen)
qp.drawLine(20, 40, 250, 40)
pen.setStyle(Qt.PenStyle.DashLine)
qp.setPen(pen)
qp.drawLine(20, 80, 250, 80)
pen.setStyle(Qt.PenStyle.DashDotLine)
qp.setPen(pen)
qp.drawLine(20, 120, 250, 120)
pen.setStyle(Qt.PenStyle.DotLine)
qp.setPen(pen)
qp.drawLine(20, 160, 250, 160)
pen.setStyle(Qt.PenStyle.DashDotDotLine)
qp.setPen(pen)
qp.drawLine(20, 200, 250, 200)
pen.setStyle(Qt.PenStyle.CustomDashLine)
pen.setDashPattern([1, 4, 5, 4])
qp.setPen(pen)
qp.drawLine(20, 240, 250, 240)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
示例中我们画了6条线。线条是用六种不同的笔触风格样式的。有五种预定义的笔触。我们也可以创建自定义笔触样式。最后一条线是使用自定义笔触风格样式的。
``` python
pen = QPen(Qt.GlobalColor.black, 2, Qt.PenStyle.SolidLine)
```
这里创建了一个 `QPen` 对象颜色是黑色宽度2像素这样就能区别不同的笔触。`Qt.SolidLine` 是一个预定义的笔触。
``` python
pen.setStyle(Qt.PenStyle.CustomDashLine)
pen.setDashPattern([1, 4, 5, 4])
qp.setPen(pen)
```
这里我们自定义了一个笔触。样式设置为 `Qt.PenStyle。CustomDashLine`,用 `setDashPattern` 方法设置具体样式参数一定是偶数个奇数定义破折号偶数定义空格。数字越大空格或破折号就越大。这里设置的是1px横线4px空格5px横线4px空格等等。
![Pen styles](./images/penstyles.png)
图示:笔触样式
## PyQt6 QBrush
`QBrush` 是一个基本图形对象。它用于绘制矩形、椭圆等形状的背景。笔刷有三种类型:预定义的笔刷、渐变或纹理模式。
``` python
# file: brushes.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This example draws nine rectangles in different
brush styles.
Author: Jan Bodnar
Website: zetcode.com
"""
from PyQt6.QtWidgets import QWidget, QApplication
from PyQt6.QtGui import QPainter, QBrush
from PyQt6.QtCore import Qt
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 355, 280)
self.setWindowTitle('Brushes')
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawBrushes(qp)
qp.end()
def drawBrushes(self, qp):
brush = QBrush(Qt.BrushStyle.SolidPattern)
qp.setBrush(brush)
qp.drawRect(10, 15, 90, 60)
brush.setStyle(Qt.BrushStyle.Dense1Pattern)
qp.setBrush(brush)
qp.drawRect(130, 15, 90, 60)
brush.setStyle(Qt.BrushStyle.Dense2Pattern)
qp.setBrush(brush)
qp.drawRect(250, 15, 90, 60)
brush.setStyle(Qt.BrushStyle.DiagCrossPattern)
qp.setBrush(brush)
qp.drawRect(10, 105, 90, 60)
brush.setStyle(Qt.BrushStyle.Dense5Pattern)
qp.setBrush(brush)
qp.drawRect(130, 105, 90, 60)
brush.setStyle(Qt.BrushStyle.Dense6Pattern)
qp.setBrush(brush)
qp.drawRect(250, 105, 90, 60)
brush.setStyle(Qt.BrushStyle.HorPattern)
qp.setBrush(brush)
qp.drawRect(10, 195, 90, 60)
brush.setStyle(Qt.BrushStyle.VerPattern)
qp.setBrush(brush)
qp.drawRect(130, 195, 90, 60)
brush.setStyle(Qt.BrushStyle.BDiagPattern)
qp.setBrush(brush)
qp.drawRect(250, 195, 90, 60)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
示例中绘制了9个不同的矩形。
``` python
brush = QBrush(Qt.BrushStyle.SolidPattern)
qp.setBrush(brush)
qp.drawRect(10, 15, 90, 60)
```
这里定义了一个笔刷对象,调用 drawRect 方法绘制矩形。
![Brushes](./images/brushes.png)
图示:笔刷
## 贝塞尔曲线
贝塞尔曲线是三次方曲线。PyQt6 中的贝塞尔曲线可以用 `QPainterPath` 创建。画线路径是由许多图形构建块(如矩形、椭圆、直线和曲线)组成的对象。
``` python
# file: bezier_curve.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This program draws a Bézier curve with
QPainterPath.
Author: Jan Bodnar
Website: zetcode.com
"""
import sys
from PyQt6.QtGui import QPainter, QPainterPath
from PyQt6.QtWidgets import QWidget, QApplication
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 380, 250)
self.setWindowTitle('Bézier curve')
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
qp.setRenderHint(QPainter.RenderHint.Antialiasing)
self.drawBezierCurve(qp)
qp.end()
def drawBezierCurve(self, qp):
path = QPainterPath()
path.moveTo(30, 30)
path.cubicTo(30, 30, 200, 350, 350, 30)
qp.drawPath(path)
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
This example draws a Bézier curve.
```python
path = QPainterPath()
path.moveTo(30, 30)
path.cubicTo(30, 30, 200, 350, 350, 30)
```
使用 `QPainterPath` 创建贝塞尔曲线路径。使用 `cubicTo` 方法绘制曲线,该方法需要三个点:起始点,控制点,结束点。
``` python
qp.drawPath(path)
```
使用 drawPath 方法绘制最终的路径。
![Bézier curve](./images/beziercurve.png)
图示:贝塞尔曲线
本章讲解了基本的绘画。

859
translated/pyqt6/tetris.md Normal file
View File

@ -0,0 +1,859 @@
# 俄罗斯方块
*更新于 2021.05.05*
本章实现一个俄罗斯方块游戏。
## 简介
俄罗斯方块游戏是有史以来最受欢迎的电脑游戏之一。最初的游戏是由俄罗斯程序员 Alexey Pajitnov 在1985年设计并编写的。从那时起《俄罗斯方块》便以多种形式出现在几乎所有平台上。
俄罗斯方块被称为掉落方块拼图游戏。在这款游戏中我们有7种不同的形状叫做砖块tetrminoesS形、Z形、T形、L形、线形、反向L形和方形。每个形状都是由四个正方形组成的。这些形状从顶部掉落。《俄罗斯方块》游戏的目标是移动和旋转形状尽可能的拼到一起如果拼成一行这一行会消失这样就能得分直到方块堆叠到顶部游戏结束。
![Tetrominoes](./images/tetrominoes.png)
图示Tetrominoes
PyQt6 目标是创建应用程序有些其他的库的目标是创造电脑游戏。尽管如此PyQt6 和其他库也可以用于创建简单的游戏。
制作一个电脑游戏是提高编程技能的好方法。
## 开发
因为没有游戏砖块的图片,所以这里使用 PyQt6 编程工具包中的绘图 API 来绘制砖块。每一款电脑游戏的背后都有一个数学模型,俄罗斯方块也是如此。
一些思路:
- 使用 `QtCore.QBasicTimer` 创建游戏循环
- 画出砖块
- 砖块整体旋转或移动(不是分开操作)
- 数学意义上,游戏面板是个简单的数字列表
代码包含四个类:`Tetris``Board``Tetrominoe``Shape``Tetris` 类设置了游戏。`Board` 是编写游戏逻辑的地方。`Tetrominoe` 类包含所有俄罗斯方块的名称,`Shape` 类包含俄罗斯方块的代码。
``` python
# files: tetris.py
#!/usr/bin/python
"""
ZetCode PyQt6 tutorial
This is a Tetris game clone.
Author: Jan Bodnar
Website: zetcode.com
"""
import random
import sys
from PyQt6.QtCore import Qt, QBasicTimer, pyqtSignal
from PyQt6.QtGui import QPainter, QColor
from PyQt6.QtWidgets import QMainWindow, QFrame, QApplication
class Tetris(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
"""initiates application UI"""
self.tboard = Board(self)
self.setCentralWidget(self.tboard)
self.statusbar = self.statusBar()
self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)
self.tboard.start()
self.resize(180, 380)
self.center()
self.setWindowTitle('Tetris')
self.show()
def center(self):
"""centers the window on the screen"""
qr = self.frameGeometry()
cp = self.screen().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
class Board(QFrame):
msg2Statusbar = pyqtSignal(str)
BoardWidth = 10
BoardHeight = 22
Speed = 300
def __init__(self, parent):
super().__init__(parent)
self.initBoard()
def initBoard(self):
"""initiates board"""
self.timer = QBasicTimer()
self.isWaitingAfterLine = False
self.curX = 0
self.curY = 0
self.numLinesRemoved = 0
self.board = []
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
self.isStarted = False
self.isPaused = False
self.clearBoard()
def shapeAt(self, x, y):
"""determines shape at the board position"""
return self.board[(y * Board.BoardWidth) + x]
def setShapeAt(self, x, y, shape):
"""sets a shape at the board"""
self.board[(y * Board.BoardWidth) + x] = shape
def squareWidth(self):
"""returns the width of one square"""
return self.contentsRect().width() // Board.BoardWidth
def squareHeight(self):
"""returns the height of one square"""
return self.contentsRect().height() // Board.BoardHeight
def start(self):
"""starts game"""
if self.isPaused:
return
self.isStarted = True
self.isWaitingAfterLine = False
self.numLinesRemoved = 0
self.clearBoard()
self.msg2Statusbar.emit(str(self.numLinesRemoved))
self.newPiece()
self.timer.start(Board.Speed, self)
def pause(self):
"""pauses game"""
if not self.isStarted:
return
self.isPaused = not self.isPaused
if self.isPaused:
self.timer.stop()
self.msg2Statusbar.emit("paused")
else:
self.timer.start(Board.Speed, self)
self.msg2Statusbar.emit(str(self.numLinesRemoved))
self.update()
def paintEvent(self, event):
"""paints all shapes of the game"""
painter = QPainter(self)
rect = self.contentsRect()
boardTop = rect.bottom() - Board.BoardHeight * self.squareHeight()
for i in range(Board.BoardHeight):
for j in range(Board.BoardWidth):
shape = self.shapeAt(j, Board.BoardHeight - i - 1)
if shape != Tetrominoe.NoShape:
self.drawSquare(painter,
rect.left() + j * self.squareWidth(),
boardTop + i * self.squareHeight(), shape)
if self.curPiece.shape() != Tetrominoe.NoShape:
for i in range(4):
x = self.curX + self.curPiece.x(i)
y = self.curY - self.curPiece.y(i)
self.drawSquare(painter, rect.left() + x * self.squareWidth(),
boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
self.curPiece.shape())
def keyPressEvent(self, event):
"""processes key press events"""
if not self.isStarted or self.curPiece.shape() == Tetrominoe.NoShape:
super(Board, self).keyPressEvent(event)
return
key = event.key()
if key == Qt.Key.Key_P:
self.pause()
return
if self.isPaused:
return
elif key == Qt.Key.Key_Left.value:
self.tryMove(self.curPiece, self.curX - 1, self.curY)
elif key == Qt.Key.Key_Right.value:
self.tryMove(self.curPiece, self.curX + 1, self.curY)
elif key == Qt.Key.Key_Down.value:
self.tryMove(self.curPiece.rotateRight(), self.curX, self.curY)
elif key == Qt.Key.Key_Up.value:
self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY)
elif key == Qt.Key.Key_Space.value:
self.dropDown()
elif key == Qt.Key.Key_D.value:
self.oneLineDown()
else:
super(Board, self).keyPressEvent(event)
def timerEvent(self, event):
"""handles timer event"""
if event.timerId() == self.timer.timerId():
if self.isWaitingAfterLine:
self.isWaitingAfterLine = False
self.newPiece()
else:
self.oneLineDown()
else:
super(Board, self).timerEvent(event)
def clearBoard(self):
"""clears shapes from the board"""
for i in range(Board.BoardHeight * Board.BoardWidth):
self.board.append(Tetrominoe.NoShape)
def dropDown(self):
"""drops down a shape"""
newY = self.curY
while newY > 0:
if not self.tryMove(self.curPiece, self.curX, newY - 1):
break
newY -= 1
self.pieceDropped()
def oneLineDown(self):
"""goes one line down with a shape"""
if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
self.pieceDropped()
def pieceDropped(self):
"""after dropping shape, remove full lines and create new shape"""
for i in range(4):
x = self.curX + self.curPiece.x(i)
y = self.curY - self.curPiece.y(i)
self.setShapeAt(x, y, self.curPiece.shape())
self.removeFullLines()
if not self.isWaitingAfterLine:
self.newPiece()
def removeFullLines(self):
"""removes all full lines from the board"""
numFullLines = 0
rowsToRemove = []
for i in range(Board.BoardHeight):
n = 0
for j in range(Board.BoardWidth):
if not self.shapeAt(j, i) == Tetrominoe.NoShape:
n = n + 1
if n == 10:
rowsToRemove.append(i)
rowsToRemove.reverse()
for m in rowsToRemove:
for k in range(m, Board.BoardHeight):
for l in range(Board.BoardWidth):
self.setShapeAt(l, k, self.shapeAt(l, k + 1))
numFullLines = numFullLines + len(rowsToRemove)
if numFullLines > 0:
self.numLinesRemoved = self.numLinesRemoved + numFullLines
self.msg2Statusbar.emit(str(self.numLinesRemoved))
self.isWaitingAfterLine = True
self.curPiece.setShape(Tetrominoe.NoShape)
self.update()
def newPiece(self):
"""creates a new shape"""
self.curPiece = Shape()
self.curPiece.setRandomShape()
self.curX = Board.BoardWidth // 2 + 1
self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
if not self.tryMove(self.curPiece, self.curX, self.curY):
self.curPiece.setShape(Tetrominoe.NoShape)
self.timer.stop()
self.isStarted = False
self.msg2Statusbar.emit("Game over")
def tryMove(self, newPiece, newX, newY):
"""tries to move a shape"""
for i in range(4):
x = newX + newPiece.x(i)
y = newY - newPiece.y(i)
if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
return False
if self.shapeAt(x, y) != Tetrominoe.NoShape:
return False
self.curPiece = newPiece
self.curX = newX
self.curY = newY
self.update()
return True
def drawSquare(self, painter, x, y, shape):
"""draws a square of a shape"""
colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]
color = QColor(colorTable[shape])
painter.fillRect(x + 1, y + 1, self.squareWidth() - 2,
self.squareHeight() - 2, color)
painter.setPen(color.lighter())
painter.drawLine(x, y + self.squareHeight() - 1, x, y)
painter.drawLine(x, y, x + self.squareWidth() - 1, y)
painter.setPen(color.darker())
painter.drawLine(x + 1, y + self.squareHeight() - 1,
x + self.squareWidth() - 1, y + self.squareHeight() - 1)
painter.drawLine(x + self.squareWidth() - 1,
y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1)
class Tetrominoe:
NoShape = 0
ZShape = 1
SShape = 2
LineShape = 3
TShape = 4
SquareShape = 5
LShape = 6
MirroredLShape = 7
class Shape:
coordsTable = (
((0, 0), (0, 0), (0, 0), (0, 0)),
((0, -1), (0, 0), (-1, 0), (-1, 1)),
((0, -1), (0, 0), (1, 0), (1, 1)),
((0, -1), (0, 0), (0, 1), (0, 2)),
((-1, 0), (0, 0), (1, 0), (0, 1)),
((0, 0), (1, 0), (0, 1), (1, 1)),
((-1, -1), (0, -1), (0, 0), (0, 1)),
((1, -1), (0, -1), (0, 0), (0, 1))
)
def __init__(self):
self.coords = [[0, 0] for i in range(4)]
self.pieceShape = Tetrominoe.NoShape
self.setShape(Tetrominoe.NoShape)
def shape(self):
"""returns shape"""
return self.pieceShape
def setShape(self, shape):
"""sets a shape"""
table = Shape.coordsTable[shape]
for i in range(4):
for j in range(2):
self.coords[i][j] = table[i][j]
self.pieceShape = shape
def setRandomShape(self):
"""chooses a random shape"""
self.setShape(random.randint(1, 7))
def x(self, index):
"""returns x coordinate"""
return self.coords[index][0]
def y(self, index):
"""returns y coordinate"""
return self.coords[index][1]
def setX(self, index, x):
"""sets x coordinate"""
self.coords[index][0] = x
def setY(self, index, y):
"""sets y coordinate"""
self.coords[index][1] = y
def minX(self):
"""returns min x value"""
m = self.coords[0][0]
for i in range(4):
m = min(m, self.coords[i][0])
return m
def maxX(self):
"""returns max x value"""
m = self.coords[0][0]
for i in range(4):
m = max(m, self.coords[i][0])
return m
def minY(self):
"""returns min y value"""
m = self.coords[0][1]
for i in range(4):
m = min(m, self.coords[i][1])
return m
def maxY(self):
"""returns max y value"""
m = self.coords[0][1]
for i in range(4):
m = max(m, self.coords[i][1])
return m
def rotateLeft(self):
"""rotates shape to the left"""
if self.pieceShape == Tetrominoe.SquareShape:
return self
result = Shape()
result.pieceShape = self.pieceShape
for i in range(4):
result.setX(i, self.y(i))
result.setY(i, -self.x(i))
return result
def rotateRight(self):
"""rotates shape to the right"""
if self.pieceShape == Tetrominoe.SquareShape:
return self
result = Shape()
result.pieceShape = self.pieceShape
for i in range(4):
result.setX(i, -self.y(i))
result.setY(i, self.x(i))
return result
def main():
app = QApplication([])
tetris = Tetris()
sys.exit(app.exec())
if __name__ == '__main__':
main()
```
游戏被简化了一点,这样更容易理解。游戏启动后立即开始。我们可以按 `P` 键暂停游戏。按空格键,方块会立即掉落到底部。游戏以固定的速度运行,没有任加速度。分数是消除的行数。
```python
self.tboard = Board(self)
self.setCentralWidget(self.tboard)
```
实例化 Board 类,并将其设置为应用程序的中心部件。
```python
self.statusbar = self.statusBar()
self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)
```
这里创建一个状态栏,用于显示消息。我们将显示三种可能的消息:已经删除的行数,暂停消息,或游戏结束消息。`msg2Statusbar` 是在 Board 类中实现的自定义信号。`showMessage` 是一个内置方法,用于在状态栏上显示消息。
``` python
self.tboard.start()
```
初始化游戏。
```python
class Board(QFrame):
msg2Statusbar = pyqtSignal(str)
...
```
`pyqtSignal` 创建了一个自定义信号。想在状态栏上展示信息或者分数,触发 `msg2Statusbar` 信号即可。
``` python
BoardWidth = 10
BoardHeight = 22
Speed = 300
```
这些是 `Board` 的参数。`BoardWidth``BoardHeight` 定义画板的宽高。 `Speed` 是游戏的速度每300毫秒游戏循环进行一次。
``` python
...
self.curX = 0
self.curY = 0
self.numLinesRemoved = 0
self.board = []
...
```
`initBoard` 方法中,我们初始化一些变量。`self.board` 是一个从0到7的数字列表代表了砖块的各种形状和位置信息。
``` python
def shapeAt(self, x, y):
"""determines shape at the board position"""
return self.board[(y * Board.BoardWidth) + x]
```
`shapeAt` 方法决定了形状的位置。
```python
def squareWidth(self):
"""returns the width of one square"""
return self.contentsRect().width() // Board.BoardWidth
```
画板可以动态调整大小。`squareWidth` 计算并返回单个形状的像素宽度。`Board.BoardWidth` 是画板的大小。
```python
def pause(self):
"""pauses game"""
if not self.isStarted:
return
self.isPaused = not self.isPaused
if self.isPaused:
self.timer.stop()
self.msg2Statusbar.emit("paused")
else:
self.timer.start(Board.Speed, self)
self.msg2Statusbar.emit(str(self.numLinesRemoved))
self.update()
```
`pause` 方法暂停游戏,停止计时并在状态栏上显示一个信息。
```python
def paintEvent(self, event):
"""paints all shapes of the game"""
painter = QPainter(self)
rect = self.contentsRect()
...
```
`paintEvent` 方法内绘制游戏。`QPainter` 是 PyQt6 里执行底层绘制的方法。
```python
for i in range(Board.BoardHeight):
for j in range(Board.BoardWidth):
shape = self.shapeAt(j, Board.BoardHeight - i - 1)
if shape != Tetrominoe.NoShape:
self.drawSquare(painter,
rect.left() + j * self.squareWidth(),
boardTop + i * self.squareHeight(), shape)
```
游戏绘制分为两个步骤。在第一步中,我们画出所有的形状,或者已经落在画板底部的形状。所有的方格都记录在 `self.board` 的变量列表。可以使用 `shapeAt` 方法访问该变量。
```python
if self.curPiece.shape() != Tetrominoe.NoShape:
for i in range(4):
x = self.curX + self.curPiece.x(i)
y = self.curY - self.curPiece.y(i)
self.drawSquare(painter, rect.left() + x * self.squareWidth(),
boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
self.curPiece.shape())
```
第二步画出正在下落的砖块。
```python
elif key == Qt.Key.Key_Right.value:
self.tryMove(self.curPiece, self.curX + 1, self.curY)
```
`keyPressEvent` 方法中,我们检查按下的键。如果按下右箭头键,将尝试部件向右移动。说“尝试”是因为它可能无法移动。
```python
elif key == Qt.Key.Key_Up.value:
self.tryMove(self.curPiece.rotateLeft(), self.curX, self.curY)
```
`Up` 键向左旋转砖块。
```python
elif key == Qt.Key.Key_Space.value:
self.dropDown()
```
`Space` 键会让砖块直接落到底部。
```python
elif key == Qt.Key.Key_D.value:
self.oneLineDown()
```
按下D键则会让砖块加速下落一会。
```python
def timerEvent(self, event):
"""handles timer event"""
if event.timerId() == self.timer.timerId():
if self.isWaitingAfterLine:
self.isWaitingAfterLine = False
self.newPiece()
else:
self.oneLineDown()
else:
super(Board, self).timerEvent(event)
```
在计时器事件中,要么在前一个砖块到底部之后创建一个新的,要么将一个砖块向下移动一行。
```python
def clearBoard(self):
"""clears shapes from the board"""
for i in range(Board.BoardHeight * Board.BoardWidth):
self.board.append(Tetrominoe.NoShape)
```
`clearBoard` 方法用设置 `Tetrominoe.NoShape` 的方式做到清空画板上的全部砖块。
```python
def removeFullLines(self):
"""removes all full lines from the board"""
numFullLines = 0
rowsToRemove = []
for i in range(Board.BoardHeight):
n = 0
for j in range(Board.BoardWidth):
if not self.shapeAt(j, i) == Tetrominoe.NoShape:
n = n + 1
if n == 10:
rowsToRemove.append(i)
rowsToRemove.reverse()
for m in rowsToRemove:
for k in range(m, Board.BoardHeight):
for l in range(Board.BoardWidth):
self.setShapeAt(l, k, self.shapeAt(l, k + 1))
numFullLines = numFullLines + len(rowsToRemove)
...
```
如果砖块落在底部,就调用 `removeFullLines` 方法找出所有完整的一行并删除。将所有行移动到当前整行的位置上做到删除的效果。注意我们颠倒了要删除的行的顺序不然会出现BUG在我们的例子中我们使用了 `naive gravity`,这意味着不是整行的砖块可能漂浮在空白的间隙之上。
```python
def newPiece(self):
"""creates a new shape"""
self.curPiece = Shape()
self.curPiece.setRandomShape()
self.curX = Board.BoardWidth // 2 + 1
self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
if not self.tryMove(self.curPiece, self.curX, self.curY):
self.curPiece.setShape(Tetrominoe.NoShape)
self.timer.stop()
self.isStarted = False
self.msg2Statusbar.emit("Game over")
```
`newPiece` 方法创建随机的砖块,如果创建的砖块不能到达初始位置,游戏结束。
``` python
def tryMove(self, newPiece, newX, newY):
"""tries to move a shape"""
for i in range(4):
x = newX + newPiece.x(i)
y = newY - newPiece.y(i)
if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
return False
if self.shapeAt(x, y) != Tetrominoe.NoShape:
return False
self.curPiece = newPiece
self.curX = newX
self.curY = newY
self.update()
return True
```
`tryMove` 方法尝试移动一个砖块,如果砖块在画板或者另外一个砖块的边缘,返回 `False`。否则就执行移动。
``` python
class Tetrominoe:
NoShape = 0
ZShape = 1
SShape = 2
LineShape = 3
TShape = 4
SquareShape = 5
LShape = 6
MirroredLShape = 7
```
`Tetrominoe` 类包含了所有形状的名称,这里也有一个叫 `NoShape` 的空形状。
`Shape` 类保存了砖块的信息。
``` python
class Shape(object):
coordsTable = (
((0, 0), (0, 0), (0, 0), (0, 0)),
((0, -1), (0, 0), (-1, 0), (-1, 1)),
...
)
...
```
`coordsTable` 元组里包括了所有砖块的组合坐标,可以从这个模板里拼出需要的砖块。
```python
self.coords = [[0,0] for i in range(4)]
```
初始化时,创建一个空的坐标列表,用来存储砖块的坐标。
![Coordinates](./images/coordinates.png)
图示: 坐标
上面的图能帮助我们更好的理解坐标值的意义。比如,`(0, -1), (0, 0), (-1, 0), (-1, -1)
` 代表了Z字形的砖块图示里就是这个形状。
``` python
def rotateLeft(self):
"""rotates shape to the left"""
if self.pieceShape == Tetrominoe.SquareShape:
return self
result = Shape()
result.pieceShape = self.pieceShape
for i in range(4):
result.setX(i, self.y(i))
result.setY(i, -self.x(i))
return result
```
`rotateLeft` 方法向左旋转砖块。方形没必要旋转,所以就直接返回当前的对象。当砖块发生旋转时,会产生一个新的对象代表这个旋转后的砖块。
![Tetris](./images/tetris.png)
图示: 俄罗斯方块

Some files were not shown because too many files have changed in this diff Show More