PyQt5教程-Martin Qt的basic features


PyQt5是一个python库,用来调用Qt。Qt本身是一个C++的GUI framework。使用PyQt5,可以使得我们在不怎么损失C++的速度优势的情况下,利用Python创建一个gui应用。

基础

创建一个application

# py example1.py
import sys

from PyQt5.QtWidgets import QApplication

# 每个app都需要且只需要一个QApplication实例,可以将命令行参数传入其中。
# 如果确定不需要命令行参数,则传入[]即可
app = QApplication(sys.argv)
# 开启主事件循环
app.exec_()

# application不会运行到这个地方,直到你退出,并且主事件循环结束

以上是一个最简单的PyQt5应用,当然,运行它不会出现任何东西(不止如此,还无法正常关闭它),但它实际上确实work了,通过查看任务管理器可以知道。

每个application都需要且只需要一个QApplication示例,其管理着event loop。

event loop是GUI界面运行的标准模式。每次我们和图形界面的交互(按下按钮、点击鼠标)都会产生一个event对象,放入event queue中进行等待。event loop是一个无限循环的loop,其在每次迭代的时候都会进行检查,并找到在event queue等待的evnet,并将event和控制权移交给其对应的event handler,当event handler处理完事件后,将控制权返回给event loop,然后再去寻找下一个等待中的event。每个application中只会有一个event loop。

时刻注意查看任务管理器,很可能有许多程序我们没有正常关闭。

创建一个窗口

# py example2.py
import sys

from PyQt5.QtWidgets import QApplication, QWidget


app = QApplication(sys.argv)

window = QWidget()
window.show()  # important! 窗口默认是隐藏的

# sys.exit(app.exec_())
app.exec_()

运行结果:


进入event loop后,Ctrl-C不会使其退出

如果怕无法有效的退出,可以使用下面的语句:

# py example2-2.py
import sys

from PyQt5.QtWidgets import QApplication, QWidget


try:
    app = QApplication(sys.argv)

    window = QWidget()
    window.show()  # important! 窗口默认是隐藏的

    sys.exit(app.exec_())

finally:
    pass

实际上,我们在PyQt5中看到的所有组件,都是一个QWidget,如果其没有parent,则其成为一个main window。但我们有一个更加好的创建主窗口的方式,即一个子类QMainWidget,其可以轻松的添加toolbars、statusbars和dockable等组件,所以我们还是用QMainWidget吧:

# py example2-3.py
import sys

from PyQt5.QtWidgets import QApplication, QMainWindow

app = QApplication(sys.argv)

window = QMainWindow()
window.show()

app.exec_()


这个相比上面的那个比较小。。。

实际上我们可以创建多个QMainWidget实例,程序会在最后一个实例关闭后退出。

添加一个组件

这里我们只添加一个widget QLabel。如果我们想要添加更多的组件的话,需要使用Qt layouts,但只添加一个暂时不需要。

# py example3.py
import sys

from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow
from PyQt5.QtCore import Qt


# 这里使用面向对象的方法
class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        label = QLabel("This is a PyQt5 window!")

        # Qt namespace中有大量的特征用于自定义widget的行为,类似常量
        label.setAlignment(Qt.AlignCenter)

        # 将widget放入window的中央,默认widget将填满整个window
        self.setCentralWidget(label)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()


可能是因为字体的问题,还有下面的错误信息:

qt.qpa.fonts: Unable to open default EUDC font: "C:\\Users\\hasee\\AppData\\LocalLow\\SogouPY\\EUDC\\SGPYEUDC_2.TTE"
qt.qpa.fonts: Unable to enumerate family ' "Droid Sans Mono Dotted for Powerline" '
qt.qpa.fonts: Unable to enumerate family ' "Droid Sans Mono Slashed for Powerline" '
qt.qpa.fonts: Unable to enumerate family ' "Roboto Mono Medium for Powerline" '
qt.qpa.fonts: Unable to enumerate family ' "Ubuntu Mono derivative Powerline" '

Signals, Slots and Events

signal的必要性

就像前面所讲的,整个application是事件驱动的。event有多种类型,比如mouse events或者keyboard events。

点击一个widget会触发QMouseEvent,并送入widget中的.mousePressEvent event handler(这实际上是这个widget的方法),这个handler会处理这个event并得到information(什么触发了这个事件、在哪个位置触发的)。

所以,如果我们想要对某个操作进行自定义,就可以通过继承widget并重写其对应的event handler方法即可。比如我们可以用一下代码来更改Qbutton.KeyPressEvent handler:

class CustomButton(Qbutton):

    def keyPressEvent(self, e):
        super(CustomButton, self).keyPressEvent(e)
        ...

以上的方法是好的,但如果我们现在需要捕获作用在20个buttons上的一个event,则我们需要重新继承20个buttons。。。显然,我们不会这个做。

什么是signals和slots

Qt提供了一个更加整洁的方式,即Signals

  • Signals类似event,当application发生变化时其会发送一个signal
    • 比如所有会引起event的行为
    • 或者是box中的文本发生了变化
    • signal会携带改变的数据一起发送过去
  • 在Qt的术语中,接受Signals的方法称为Slots,Qt类中提供了一些标准的Slots,但我们可以使用python func来自己定义slots

Signals实验

Qt5已经有[Python的文档]

常看文档后,发现QMainWidget有一个windowTitleChanged signal,我们现在使用它来进行下述实验:

# py example4.py
import sys

from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow
from PyQt5.QtCore import Qt


# 这里使用面向对象的方法
class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        # 使用connect方法会将signal连接到对应的slot(func)上去,一旦signal
        #   触发(window title改变),则会使用这些funcs
        # 我们可以一次连接多个这样的func,当signal触发时,这些func都会运行
        self.windowTitleChanged.connect(self.onWindowTitleChange)
        self.windowTitleChanged.connect(lambda x: self.my_custom_fn())
        self.windowTitleChanged.connect(lambda x: self.my_custom_fn(x))
        self.windowTitleChanged.connect(lambda x: self.my_custom_fn(x, 25))

        # 在这里设置title的时候会触发上面的func一次
        self.setWindowTitle("My Awesome App")

        label = QLabel("This is a PyQt5 window!")

        # Qt namespace中有大量的特征用于自定义widget的行为,类似常量
        label.setAlignment(Qt.AlignCenter)

        # 将widget放入window的中央,默认widget将填满整个window
        self.setCentralWidget(label)

    def onWindowTitleChange(self, x):
        # 这个x是signal触发是排出的内容,具体要查看文档
        print(x)

    def my_custom_fn(self, a="Hello", b=5):
        print(a, b)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

许多signal会被传入已存在的、标准的一些方法中,自然我们可以通过更改这些方法完成我们的目的。同样我们也可以像上面描述的一样,使用connect来连接一个新的slot,这样不会影响原来的slot,他们都会被触发。

当我们不得不使用event时

所以,因为signals的存在,大多数时候我们不会用到events。但当signal无法解决我们的问题时,我们就必须和event打交道了。

下面是对右击窗口区域的行为的一次更改:

# py example5.py
import sys

from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow
from PyQt5.QtCore import Qt


# 这里使用面向对象的方法
class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        label = QLabel("This is a PyQt5 window!")

        # Qt namespace中有大量的特征用于自定义widget的行为,类似常量
        label.setAlignment(Qt.AlignCenter)

        # 将widget放入window的中央,默认widget将填满整个window
        self.setCentralWidget(label)

    def contextMenuEvent(self, event):
        # 右键点击会触发一个event,如果此发生在MainWindow中,则其会被送入
        #   此方法内,这里我们让它在console中打印一些东西
        print("Context menu event!")


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

结果:


如果我们希望保留其原来的功能,可以使用下面的写法:

def contextMenuEvent(self, event):
    print("Context menu event!")
    super(MainWindow, self).contextMenuEvent(event)

其实就是调用了父类的方法。

在一些比较复杂的application上,widget接受了event后,还会将此event发送到其父widget上,如果我们不希望此event发送,可以在widget的handler上添加下面的操作:

class CustomButton(Qbutton):
    def event(self, e):
        e.accept()

相似的,如果希望它发送,则使用

class CustomButton(Qbutton):
    def event(self, e):
        e.ignore()

这个命名也有点太别扭了。

基础的组件

Toolbars

整个代码如下:

# py example11.py
import sys

import PyQt5.QtWidgets as qtw
import PyQt5.QtCore as qtc
import PyQt5.QtGui as qtg


# 这里使用面向对象的方法
class MainWindow(qtw.QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        label = qtw.QLabel("This is a PyQt5 window!")

        # Qt namespace中有大量的特征用于自定义widget的行为,类似常量
        label.setAlignment(qtc.Qt.AlignCenter)

        # 将widget放入window的中央,默认widget将填满整个window
        self.setCentralWidget(label)

        # 创建一个toolbar,并将toolbar加入到main window中
        toolbar = qtw.QToolBar("My Main toolbar")
        # 告诉toolbar,我们使用的icon 图片的大小,这样icon可以填充整个按钮
        toolbar.setIconSize(qtc.QSize(16, 16))
        self.addToolBar(toolbar)

        # 创建一个按钮(这里使用的是action,这是一个更加通用的抽象的按钮类)
        #   action相对于button的好处在于,button只是一个按钮,所以其唯一接受的交互形式是
        #   press。但action可以作为任何形式的交互出现,即其既可以是按钮,又可以出现在菜单栏,
        #   也可以通过键盘快捷键触发。这在toolbar和menubar的实现中是非常必要的,因为一般来说
        #   有很多内容在toolbar和menu中是共享的,如果没有action,我们需要为menu中和toolbar
        #   中都有的一样的功能实现两次。。
        # action接受的是一个QIcon对象作为其icon、string作为其介绍,还有最后的参数是其parent,
        #   这里其signal会传递非其父元素,所以这里设定为main window
        button_action = qtw.QAction(
            qtg.QIcon("bug.png"), "Your button", self
        )
        # hover到action上,会在status上打印下面的信息
        button_action.setStatusTip("This is you button")
        # 为action的triggeredsignal绑定slot
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        # 该设置使action 按钮有按下的效果
        button_action.setCheckable(True)
        # 将action加入到toolbar中
        toolbar.addAction(button_action)

        # 添加分割线
        toolbar.addSeparator()

        # 添加第二个button
        button_action2 = qtw.QAction(
            qtg.QIcon("bug.png"), "You button2", self
        )
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        # toolbar也可以加入其他的widgets
        toolbar.addWidget(qtw.QLabel("Hello"))
        toolbar.addWidget(qtw.QCheckBox())

        self.setStatusBar(qtw.QStatusBar(self))

    def onMyToolBarButtonClick(self, s):
        # 这里可以知道,传入的信息是个boolean
        print("click", s)


app = qtw.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()


主要需要注意的内容有:

  1. 虽然Qt的toolbars支持icons、text或者其他的Qt标准widgets,但最好的实现方式是使用QAction,原因如上所述。简单来说,QAction可以看做一个综合版的交互单元,可以放在任何组件内,提高了复用性。

  2. 使用mainwin.setStatusBar来创建状态栏,需要接受一个QStatusBar实例。

  3. 使用setCheckable来控制按下的效果。

    实际上有一个signal.toggled来传递这个按下的消息,但这个消息的功能太单一了,所以用的不多。

    按钮可以下载fugue icon set中的图片来进行美化。

  4. 添加icon,需要使用下列3个步骤:

    • 使用Action.setIconSize方法来接受一个QSize对象,指定icon大小
    • 使用QIcon类来接受图片创建一个Icon对象
    • 将Icon对象送入Action的第一个参数

    因为用的是相对路径来添加图片,所以运行时路径必须在文件所在的路径

    文字和Icon的显示方式,我们可以通过.setToolButtonStyle方法来调整,其值可以使用以下的flag:

  5. 最后可以添加多个QAction对象,甚至是其他的widgets。

menu就是下图所示的gui元素:


代码如下:

# py example12.py
import sys

import PyQt5.QtWidgets as qtw
import PyQt5.QtCore as qtc
import PyQt5.QtGui as qtg


# 这里使用面向对象的方法
class MainWindow(qtw.QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        label = qtw.QLabel("This is a PyQt5 window!")

        # Qt namespace中有大量的特征用于自定义widget的行为,类似常量
        label.setAlignment(qtc.Qt.AlignCenter)

        # 将widget放入window的中央,默认widget将填满整个window
        self.setCentralWidget(label)

        toolbar = qtw.QToolBar("My Main toolbar")
        toolbar.setIconSize(qtc.QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = qtw.QAction(
            qtg.QIcon("bug.png"), "Your button", self
        )
        button_action.setStatusTip("This is you button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        # 为了此action添加快捷键
        button_action.setShortcut(qtg.QKeySequence("Ctrl+p"))
        toolbar.addAction(button_action)

        # 添加分割线
        toolbar.addSeparator()

        # 添加第二个button
        button_action2 = qtw.QAction(
            qtg.QIcon("bug.png"), "You button2", self
        )
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)

        # toolbar也可以加入其他的widgets
        toolbar.addWidget(qtw.QLabel("Hello"))
        toolbar.addWidget(qtw.QCheckBox())

        # 本来QMainWindow就已经有menu bar了,只是menu bar里是空的
        menu = self.menuBar()

        self.setStatusBar(qtw.QStatusBar(self))

        # 添加一个menu
        file_menu = menu.addMenu("&File")  # &使得此Menu可以有alt来控制
        file_menu.addAction(button_action)  # 为file menu添加一个按钮
        file_menu.addSeparator()  # 添加一个分割线
        file_menu.addAction(button_action2)  # 添加第二个按钮

        # 可以我menu继续添加子menu
        file_submenu = file_menu.addMenu("Submenu")
        file_submenu.addAction(button_action2)

    def onMyToolBarButtonClick(self, s):
        print("click", s)


app = qtw.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
  1. QMainWindow中本身自带了menuBar,所以只需要使用self.menuBar得到即可。

  2. 使用addMenu方法来添加menu,里面输入menu的名称即可

    不要把menu理解成一个按钮,其更是一个容器,一个下拉菜单,里面储存这一个一个的acton

  3. menu可以嵌套,只需要使用mean的addMenu方法即可。

  4. 使用setShortCut方法添加快捷键,需要传入QKeySequence对象。

widgets

widgets就是UI的基本组件,是可以和用户产生交互的元素。我们看到的各种toolbar、button等,都是widgets。

Qt有大量的widgets,而且还支持自定义。以下代码将一些常用的widgets放置在一个QWidget基类中,并放置在main windows的中央。

这里QWidget被用作一个容器

# py example13.py
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

import sys


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        layout = QVBoxLayout()
        widgets = [
            QCheckBox,
            QComboBox,
            QDateTimeEdit,
            QDial,
            QDoubleSpinBox,
            QFontComboBox,
            QLCDNumber,
            QLabel,
            QLineEdit,
            QProgressBar,
            QPushButton,
            QRadioButton,
            QSlider,
            QSpinBox,
            QTimeEdit
        ]

        for w in widgets:
            layout.addWidget(w())

        widget = QWidget()
        widget.setLayout(layout)

        self.setCentralWidget(widget)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()


常用的widgets

1. QLabel

这是最简单的widget,不存在交互,只是显示一行字而已。以下代码展示了如何改变其中的文字内容和文字样式:

# py example14.py
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

import sys


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        widget1 = QLabel("Hello")  # 可以直接传入文字
        widget1.setText("Hello world")  # 通过此方法改变文字的内容
        # 以下是改变文字样式的方法
        font = widget1.font()
        font.setPointSize(30)
        widget1.setFont(font)
        # 改变文字的对齐位置
        widget1.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)

        widget2 = QLabel("")
        widget2.setPixmap(QPixmap("space.jpg"))
        widget2.setScaledContents(True)

        layout = QVBoxLayout()
        layout.addWidget(widget1)
        layout.addWidget(widget2)

        widget = QWidget()
        widget.setLayout(layout)

        self.setCentralWidget(widget)

app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()


这里有几点需要注意:

  1. 改变文字样式的方式,最好是先拿到其中的font对象,将其更改后,再把其送入widget中,这样能够保证一些默认样式不会丢失。

  2. Qt中的flag可以被用于设置widgets的属性,这里使用了关于文字对齐的部分:

    水平对齐的flags:


    垂直对齐的flags:


    一个特殊的flags:


    我们可以使用|来组合一个水平和一个垂直flags来得到4个角落,至于为什么使用|而不是&,这涉及到bitmasks的问题,反正先记住吧。所有Qt的flags的合并操作都是这样的

  3. 可以使用setPixMap(QPixmpa(a.png))来设置图片。使用.setScaledContents(True)来将图片整个布满按钮。

2. QCheckBox

代码如下:

# py example15.py
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

import sys


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        widget1 = QCheckBox()
        # 这个使用boolean来设置是否选中
        # widget1.setCheckted(True)
        # 这个使用flag来设置状态,有3种
        widget1.setCheckState(Qt.Checked)  # 设置状态是checked
        # 可以使用此方法来选择第三种状态
        # widget1.setTristate(True)

        # 将state改变的flag连接到slot,传递给slot的实际上就是flag
        widget1.stateChanged.connect(self.show_state)

        self.setCentralWidget(widget1)

    def show_state(self, s):
        # 打印一下flag
        print(s == Qt.Checked)
        print(s)

app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

结果:


注意:

  1. 有两种方式来改变其checkbox的状态:

    • 使用setCheckState,需要传入flags


    • 或者使用方法setChecked(True)setTristate(True),这两个不需要flags,直接设置boolean就可以

  2. 其有一个signal:stateChanged,其传入slot的参数就是上面的flags:

    这些flags实际上就是0, 1, 2,但有了flag,我们没有必要去进行记忆。

3. QComboBox

即下拉列表,代码如下:

# py example16.py
import sys

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        widget = QComboBox()
        # 增加可选元素
        widget.addItems(["one", "two", "Three"])

        # 这个signal默认传给slot的是index
        widget.currentIndexChanged.connect(self.index_changed)
        # 可以通过此操作,使signal传递给slot的是上面的text
        widget.currentIndexChanged[str].connect(self.text_changed)
        # 此操作使得combo box可以输入文本
        widget.setEditable(True)
        # 这些输入的文本怎么处理
        # 当我们输入文本并按enter后,下面的设置会使这个文本选项称为新的Items
        #   并插入到Item列表的最后
        widget.setInsertPolicy(QComboBox.InsertAtBottom)
        # Items最多有5个
        widget.setMaxCount(5)

        self.setCentralWidget(widget)

    def index_changed(self, i):
        print(i)

    def text_changed(self, s):
        print(s)

app = QApplication(sys.argv)
win = MainWindow()
win.show()

app.exec_()

结果:


注意:

  1. 设置插入模式,可以有以下的flags(不再Qt中,而是在QComboBox本身中)


  2. 一个应用就是使用combobox来选择字体,但注意,Qt中有一个QFontComboBox直接实现了这个功能。

4. QListWidget

可以认为是始终展开的QComboBox,和QComboBox基本一致,区别在于可用的signals。代码:

# py example17.py
import sys

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        widget = QListWidget()
        # 增加可选元素
        widget.addItems(["one", "two", "Three"])

        widget.currentItemChanged.connect(self.index_changed)
        widget.currentTextChanged.connect(self.text_changed)

        self.setCentralWidget(widget)

    def index_changed(self, i):
        # 这里传入的不是index,而是一个QListItem对象
        print(i.text())

    def text_changed(self, s):
        print(s)

app = QApplication(sys.argv)
win = MainWindow()
win.show()

app.exec_()

结果:


注意:

  1. 其将QComboBox的一个signal拆成了两个。
  2. 无法更改文本。

5. QLineEdit

单行文本输入框,代码如下:

# py example18.py
import sys

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        widget = QLineEdit()
        # 设置可以输入的最大字符数量
        widget.setMaxLength(10)
        # 设置预先填入的提示字符
        widget.setPlaceholderText("Enter your text")

        # 此signal在按下enter时触发
        widget.returnPressed.connect(self.return_pressed)
        # 此signal在选中的文本发生变化的时候触发
        widget.selectionChanged.connect(self.selection_changed)
        # 此signal在text发生变化时触发,并返回改变的文本
        widget.textChanged.connect(self.text_changed)
        # 此signal在进行编辑的时候触发,基本上和textChanged同时触发
        widget.textEdited.connect(self.text_edited)

        self.setCentralWidget(widget)

    def return_pressed(self):
        print("Return preseed!")
        self.centralWidget().setText("BOOM!")

    def selection_changed(self):
        print("Selection changed")
        print(self.centralWidget().selectedText())

    def text_changed(self, s):
        print("Text changed...")
        print(s)

    def text_edited(self, s):
        print("Text edited...")
        print(s)


app = QApplication(sys.argv)
win = MainWindow()
win.show()

app.exec_()

结果:


注意:

  1. 可以使用self.CentralWidget()得到位于main window中央的widget,这里就是QLineEdit
  2. 使用widget.setTextwidget.selectionText设置输入框中的文本和返回选中的文本。

布局

在Qt中有4种布局方式:


当然,Qt Designer也是可以的,这是更加方便的布局方式。

1. QVBoxLayout

就是将widgets从上到下堆叠起来


代码:

# py example19.py
import sys

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class Color(QWidget):
    """
    一个自定义的类,用于展示布局
    """
    def __init__(self, color, *args, **kwargs):
        super(Color, self).__init__(*args, **kwargs)
        # 开启后,其自动使用window color来填充背景
        self.setAutoFillBackground(True)
        # 得到调色板,默认是global desktop palette。调色板中记录了
        #   widgets的各种元素的颜色,可以看做是widgets颜色设置的一组
        #   集合
        palette = self.palette()
        # 将调色板中窗口颜色改变,需要使用QColor类
        palette.setColor(QPalette.Window, QColor(color))
        # 将这组设置更新到当前的widgets中
        self.setPalette(palette)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        layout = QVBoxLayout()
        layout.addWidget(Color("red"))
        layout.addWidget(Color("green"))
        layout.addWidget(Color("blue"))

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)


app = QApplication(sys.argv)
win = MainWindow()
win.show()

app.exec_()

结果:


2. QHBoxLayout

就是将widgets从左到右堆叠起来


代码:

# py example20.py
import sys

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class Color(QWidget):
    """
    一个自定义的类,用于展示布局
    """
    def __init__(self, color, *args, **kwargs):
        super(Color, self).__init__(*args, **kwargs)
        # 开启后,其自动使用window color来填充背景
        self.setAutoFillBackground(True)
        # 得到调色板,默认是global desktop palette。调色板中记录了
        #   widgets的各种元素的颜色,可以看做是widgets颜色设置的一组
        #   集合
        palette = self.palette()
        # 将调色板中窗口颜色改变,需要使用QColor类
        palette.setColor(QPalette.Window, QColor(color))
        # 将这组设置更新到当前的widgets中
        self.setPalette(palette)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        layout = QHBoxLayout()
        layout.addWidget(Color("red"))
        layout.addWidget(Color("green"))
        layout.addWidget(Color("blue"))

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)


app = QApplication(sys.argv)
win = MainWindow()
win.show()

app.exec_()

结果:


3. Nesting layouts

任何一个widget都有.addLayout方法,当使用这个方法,其传入的layout中储存的多个widgets就会以当前widget为容器在其中进行排列。每个widget都可以成为这样一个容器,layout本身也不例外。只是layout除了可以addLayout外,还可以直接addWidget,而其他的widgets只能addLayout

代码:

# py example21.py
import sys

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class Color(QWidget):
    """
    一个自定义的类,用于展示布局
    """
    def __init__(self, color, *args, **kwargs):
        super(Color, self).__init__(*args, **kwargs)
        # 开启后,其自动使用window color来填充背景
        self.setAutoFillBackground(True)
        # 得到调色板,默认是global desktop palette。调色板中记录了
        #   widgets的各种元素的颜色,可以看做是widgets颜色设置的一组
        #   集合
        palette = self.palette()
        # 将调色板中窗口颜色改变,需要使用QColor类
        palette.setColor(QPalette.Window, QColor(color))
        # 将这组设置更新到当前的widgets中
        self.setPalette(palette)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        layout1 = QHBoxLayout()
        layout2 = QVBoxLayout()
        layout3 = QVBoxLayout()

        layout2.addWidget(Color("red"))
        layout2.addWidget(Color("yellow"))
        layout2.addWidget(Color("purple"))

        layout1.addLayout(layout2)
        layout1.addWidget(Color("green"))

        layout3.addWidget(Color("red"))
        layout3.addWidget(Color("purple"))

        layout1.addLayout(layout3)

        layout1.setContentsMargins(0, 0, 0, 0)
        layout1.setSpacing(20)

        widget = QWidget()
        widget.setLayout(layout1)
        self.setCentralWidget(widget)


app = QApplication(sys.argv)
win = MainWindow()
win.show()

app.exec_()

结果:


另外,我们可以通过layout.setContentsMargins来控制layout的外边距的宽度,layout.setSpacing来控制里面元素的间隔。

所以我们在代码中加入:

layout1.setContentsMargins(0, 0, 0, 0)
layout1.setSpacing(20)

得到以下结果:


4. QGridLayout

模式如下:


里面的元素没有必要全都给满,所以下面的模式也是可以实现的:


代码:

# py example22.py
import sys

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class Color(QWidget):
    """
    一个自定义的类,用于展示布局
    """
    def __init__(self, color, *args, **kwargs):
        super(Color, self).__init__(*args, **kwargs)
        # 开启后,其自动使用window color来填充背景
        self.setAutoFillBackground(True)
        # 得到调色板,默认是global desktop palette。调色板中记录了
        #   widgets的各种元素的颜色,可以看做是widgets颜色设置的一组
        #   集合
        palette = self.palette()
        # 将调色板中窗口颜色改变,需要使用QColor类
        palette.setColor(QPalette.Window, QColor(color))
        # 将这组设置更新到当前的widgets中
        self.setPalette(palette)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        layout = QGridLayout()

        layout.addWidget(Color("red"), 0, 0)
        layout.addWidget(Color("green"), 1, 0)
        layout.addWidget(Color("blue"), 1, 1)
        layout.addWidget(Color("purple"), 2, 1)

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)


app = QApplication(sys.argv)
win = MainWindow()
win.show()

app.exec_()

结果:


5. QStackedLayout

这个布局是用来实现widgets的堆叠的。在实现graphics和类似书签式的东西时非常有用。


代码:

# py example23.py
import sys

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class Color(QWidget):
    """
    一个自定义的类,用于展示布局
    """
    def __init__(self, color, *args, **kwargs):
        super(Color, self).__init__(*args, **kwargs)
        # 开启后,其自动使用window color来填充背景
        self.setAutoFillBackground(True)
        # 得到调色板,默认是global desktop palette。调色板中记录了
        #   widgets的各种元素的颜色,可以看做是widgets颜色设置的一组
        #   集合
        palette = self.palette()
        # 将调色板中窗口颜色改变,需要使用QColor类
        palette.setColor(QPalette.Window, QColor(color))
        # 将这组设置更新到当前的widgets中
        self.setPalette(palette)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        layout = QStackedLayout()

        layout.addWidget(Color("red"))
        layout.addWidget(Color("green"))
        layout.addWidget(Color("blue"))
        layout.addWidget(Color("yellow"))

        layout.setCurrentIndex(3)  # 让yellow显示在上面

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)


app = QApplication(sys.argv)
win = MainWindow()
win.show()

app.exec_()

结果:


注意,有一个QStackedWidget能够实现相同的功能,其一般作为放在Main Windows中央的widget。

但上面的结果我们只能看到最上面的widget,所以,我们可以另外设置一些button来实现widgets的切换:

# py example24.py
import sys

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class Color(QWidget):
    """
    一个自定义的类,用于展示布局
    """
    def __init__(self, color, *args, **kwargs):
        super(Color, self).__init__(*args, **kwargs)
        # 开启后,其自动使用window color来填充背景
        self.setAutoFillBackground(True)
        # 得到调色板,默认是global desktop palette。调色板中记录了
        #   widgets的各种元素的颜色,可以看做是widgets颜色设置的一组
        #   集合
        palette = self.palette()
        # 将调色板中窗口颜色改变,需要使用QColor类
        palette.setColor(QPalette.Window, QColor(color))
        # 将这组设置更新到当前的widgets中
        self.setPalette(palette)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        pagelayout = QVBoxLayout()
        button_layout = QHBoxLayout()
        layout = QStackedLayout()

        pagelayout.addLayout(button_layout)
        pagelayout.addLayout(layout)

        cs = ["red", "green", "blue", "yellow"]
        for n, color in enumerate(cs):
            btn = QPushButton(color)
            btn.pressed.connect(lambda n=n: layout.setCurrentIndex(n))
            # 这里使用了python的一个技巧。这里将index设为lambda参数的默认值,则其会被缓存,
            #   不然,所有的这四个lambda里的n都只会是黄色
            button_layout.addWidget(btn)
            layout.addWidget(Color(color))

        widget = QWidget()
        widget.setLayout(pagelayout)
        self.setCentralWidget(widget)


app = QApplication(sys.argv)
win = MainWindow()
win.show()

app.exec_()

结果:


As a matter of fact,Qt内部实现了一个类似功能的组件,可以直接使用:

# py example25.py
import sys

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *


class Color(QWidget):
    """
    一个自定义的类,用于展示布局
    """
    def __init__(self, color, *args, **kwargs):
        super(Color, self).__init__(*args, **kwargs)
        # 开启后,其自动使用window color来填充背景
        self.setAutoFillBackground(True)
        # 得到调色板,默认是global desktop palette。调色板中记录了
        #   widgets的各种元素的颜色,可以看做是widgets颜色设置的一组
        #   集合
        palette = self.palette()
        # 将调色板中窗口颜色改变,需要使用QColor类
        palette.setColor(QPalette.Window, QColor(color))
        # 将这组设置更新到当前的widgets中
        self.setPalette(palette)


class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")

        tabs = QTabWidget()
        tabs.setDocumentMode(True)
        tabs.setTabPosition(QTabWidget.East)  # 控制标签所在的位置,右侧
        tabs.setMovable(True)

        cs = ["red", "green", "blue", "yellow"]
        for n, color in enumerate(cs):
            tabs.addTab(Color(color), color)

        self.setCentralWidget(tabs)


app = QApplication(sys.argv)
win = MainWindow()
win.show()

app.exec_()

结果:


对话框

用于和用户交流,比如打开保存文件、设置或者其他不会在主窗口实现的内容。Qt实现了一系列常用的对话框类型。

作为对话框的基本class是QDialog,其接受的参数是其父元素(其依附的窗口):

我们可以有以下代码:

# py example26.py
import sys

import PyQt5.QtWidgets as qtw
import PyQt5.QtCore as qtc
import PyQt5.QtGui as qtg


class CustomDialog(qtw.QDialog):

    def __init__(self, *args, **kwargs):
        super(CustomDialog, self).__init__(*args, **kwargs)

        self.setWindowTitle("HELLO!")

        QBtn = qtw.QDialogButtonBox.Ok | qtw.QDialogButtonBox.Cancel

        self.buttonBox = qtw.QDialogButtonBox(QBtn)
        self.buttonBox.accepted.connect(self.accept)
        self.buttonBox.rejected.connect(self.reject)

        self.layout = qtw.QVBoxLayout()
        self.layout.addWidget(self.buttonBox)
        self.setLayout(self.layout)


# 这里使用面向对象的方法
class MainWindow(qtw.QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.setWindowTitle("My Awesome App")
        label = qtw.QLabel("This is a PyQt5 window!")
        label.setAlignment(qtc.Qt.AlignCenter)
        self.setCentralWidget(label)
        toolbar = qtw.QToolBar("My Main toolbar")
        toolbar.setIconSize(qtc.QSize(16, 16))
        self.addToolBar(toolbar)

        button_action = qtw.QAction(
            qtg.QIcon("bug.png"), "Your button", self
        )
        button_action.setStatusTip("This is you button")
        button_action.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        button_action.setShortcut(qtg.QKeySequence("Ctrl+p"))
        toolbar.addAction(button_action)
        toolbar.addSeparator()
        button_action2 = qtw.QAction(
            qtg.QIcon("bug.png"), "You button2", self
        )
        button_action2.triggered.connect(self.onMyToolBarButtonClick)
        button_action.setCheckable(True)
        toolbar.addAction(button_action)
        toolbar.addWidget(qtw.QLabel("Hello"))
        toolbar.addWidget(qtw.QCheckBox())
        menu = self.menuBar()
        self.setStatusBar(qtw.QStatusBar(self))
        file_menu = menu.addMenu("&File")  # &使得此Menu可以有alt来控制
        file_menu.addAction(button_action)  # 为file menu添加一个按钮
        file_menu.addSeparator()  # 添加一个分割线
        file_menu.addAction(button_action2)  # 添加第二个按钮
        file_submenu = file_menu.addMenu("Submenu")
        file_submenu.addAction(button_action2)

    def onMyToolBarButtonClick(self, s):
        print("click", s)

        # 主要改动在这里
        dlg = CustomDialog(self)
        if dlg.exec_():
            print("Success!")
        else:
            print("Cancel!")


app = qtw.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

结果:


注意:

  1. QDialog的启动形式和我们的application很像,因为它毕竟是一个新的窗口,也需要一个event loop。

  2. 开启QDialog的event loop后,其会阻塞主窗口的event loop,这个问题需要使用多线程来解决。

  3. 这里创建dialog按钮的方式有点异类,这里使用Qt准备好的一些flags来创建的,这些flags有以下:



    这种做法的好处在于可以将多个button的行为捏合到一个button对象上。之后再去将其不同的signals映射到不同的slots即可。


文章作者: Luyiyun
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Luyiyun !
评论
评论
  目录