PyQt5 系统化学习: 创建简单窗口

3 基本功能

参考自: pyqt5/firstprograms

在 PyQt5 教程的这一部分中,我们学习一些基本功能。这些示例显示了一个提示窗口(tooltip)和一个图标,关闭了一个窗口,显示了一个消息框,并在桌面上将窗口居中。

3.1 创建一个简单的窗口

每个 PyQt5 应用都必须创建一个应用对象 QApplication

app = QApplication(sys.argv)

其中 sys.argv 是一组命令行参数的列表。Python 可以在 shell 里运行,这个参数提供对脚本控制的功能。这是一种通过参数来选择启动脚本的方式。如果不需要传入参数,可以使用 [] 代替 sys.argv

QApplication 管理 GUI 程序的控制流和主要设置。它包含由窗口系统和其他来源处理过和发送过的主事件循环。它也处理应用程序的初始化和收尾工作,并提供对话管理。它还可以对系统和应用的大部分设置项进行设置。对于用 Qt 写的任何一个 GUI 应用,不管这个应用有没有窗口或多少个窗口,有且只有一个 QApplication 对象。而对于用 Qt 写的非 GUI 应用,则有且只有一个 QCoreApplication 对象,并且这个应用不依赖 QtGui 库。

为了让 GUI 启动,需要使用 app.exec_() 启动事件循环(启动应用,直至用户关闭它)。app.exec_() 的作用是运行主循环,必须调用此函数才能开始事件处理,调用该方法进入程序的主循环直到调用 exit() 结束。主事件循环从窗口系统接收事件,并将其分派给应用程序小部件。如果没有该方法,那么在运行的时候还没有进入程序的主循环就直接结束了,所以运行的时候窗口会闪退。

app.exec_() 在退出时会返回状态代码(如果程序运行成功,则返回 0,否则为非 0)。也可以用 sys.exit(app.exec_())sys.exit(n) 的作用是退出应用程序并返回 n 到父进程)。我们进入了应用的主循环中,事件处理器这个时候开始工作。主循环从窗口上接收事件,并把事件传入到派发到应用控件里。当调用 exit() 方法或直接销毁主控件时,主循环就会结束。sys.exit() 方法能确保主循环安全退出。外部环境能通知主控件怎么结束。

这样便可以写出最小化的 PyQt5 代码:

import sys
from PyQt5.QtWidgets import QApplication
# You need one (and only one) QApplication instance per application.
# Pass in sys.argv to allow command line arguments for your app.
# If you know you won't use command line arguments QApplication([]) is fine.
app = QApplication(sys.argv)
# Start the event loop.
sys.exit(app.exec_())
# Your application won't reach here until you exit and the event
# loop has stopped.

此时运行,什么也没有出现?这是因为,我们没有创建一个可视化的窗口。

QWidget 控件(所有的窗口和控件都直接或者间接继承自 QWidget)是一个用户界面的基础控件,它提供了基本的应用构造器。默认情况下,构造器是没有父级的,没有父级的构造器被称为窗口(window)。

这样,我们可以创建一个简单的窗口:

import sys
from PyQt5.QtWidgets import QApplication, QWidget
app = QApplication(sys.argv)
window = QWidget()
window.show() # IMPORTANT!!!!! Windows are hidden by default.
# Start the event loop.
app.exec_()

展示如下:

PyQt5 创建的窗口默认是隐藏的,需要调用 show() 显示。

QMainWindowQWidgetQDialog 三个类都是用来创建窗口的,可以直接使用也可以继承后来使用。QMainWindow 窗口可以包含菜单栏、工具栏、状态栏和标题栏等,是最常见的窗口形式,是 GUI 程序的主窗口。QDialog 是对话框的基类,对话框主要用来执行短期任务和与用户进行互动任务,有模态和非模态两种形式。QWidget 可以用作嵌入其他窗口。

本节仅仅介绍 QMainWindowQWidget

3.2 自定义窗口

窗口是整个程序的整体界面,有边框、标题栏、菜单栏、工具栏、关闭按钮、最小化按钮、最大化按钮等。控件是指按钮、复选框、文本框、表格、进度条等这些组成 GUI 的基本元素。一个程序可以有多个窗口,一个窗口可以有多个控件。

3.2.1 窗口坐标系统

PyQt5 使用统一的坐标系统来定义窗口控件的位置和大小。以屏幕(或窗口)的左上角为坐标原点 (0,0),从左到右为 x 轴的正方向,从上到下为 y 轴的正方向。一般称窗口的原点与坐标轴围成的区域为客户区(Client Area)。

QWidget 的成员函数可分为三类:

QWidget 直接提供的成员函数:x()y() 获得窗口左上角坐标,width()height() 获得客户区的宽度和高度。 QWidgetgeometry() 提供的成员函数:x()y() 获得客户区左上角的坐标,width()height() 获得客户区的宽度和高度。 QWidgetframeGeometry() 提供的成员函数:x()y() 获得窗口左上角,width()height() 获得客户区、标题栏和边框的整个窗口的的宽度和高度。

Qt 提供 Window and Dialog Widgets 详细说明了坐标系统,下图是其截图:

3.2.2 常用的几何结构

QWidget 包含两种常见结构:不包含边框的结构与包含边框的结构。

一般情况下,不包含边框的部分是客户区,即用户操作的界面,可以添加子控件。在 Qt 中使用 QRect 类保存了它的位置和大小。您可以对其进行修改:

  • 改变客户区的大小:QWidget.resize(width, height)。可以通过鼠标的来改变尺寸。
  • 获得客户区的大小,宽度、高度分别使用:QWidget.size()QWidget.width()QWidget.height()
  • 设定不可使用鼠标修改的宽度或者高度:QWidget.setFixedWidth(int width)QWidget.setFixedHeight(int height)
  • 同时修改客户区的大小和位置,可以使用 QWidget.setGeometry(int x, int y, int width, int height)
  • 包含边框的结构是窗口在屏幕上显示的整个区域。它包含如下函数:

  • 获得窗口的大小和位置:QWidget.frameGeometry()
  • 设置窗口的位置:QWidget.move(int x, int y)
  • 获得窗口左上角坐标:QWidget.pos()

    下面是一个简单的例子:

    from PyQt5.QtWidgets import QApplication, QWidget
    if __name__ == '__main__':
        app = QApplication([])
        w = QWidget()
        #不同操作系统可能对窗口最小宽度有规定,若设置宽度小于规定值,则会以规定值进行显示
        w.resize(250, 150)
        # 以 QWidget 左上角为(0, 0)点
        w.move(300, 300)
        w.setWindowTitle('PyQt5 坐标系统例子') # 修改窗口的标题
        w.show()
        print('控件的左上角坐标,宽度,高度,以及尺寸分别为', (w.x(), w.y()), w.width(), w.height(), w.size())
        print('客户区的左上角坐标,宽度,高度,以及尺寸分别为', (w.geometry().x(), w.geometry().y()), 
              w.geometry().width(), w.geometry().height(), w.geometry().size())
        print('窗口的左上角坐标,宽度,高度分别为', w.pos(), w.frameGeometry().width(), 
              w.frameGeometry().height())
        app.exec_()
    

    展示效果如下:

    其中 QWidget.setWindowTitle 用于修改窗口的标题。

    3.2.3 设置窗口的图标

    窗口图标通常是显示在窗口的左上角,标题栏的最左边。QWidget.setWindowIcon(QIcon('cartoon1.ico') 提供了设置图标的方法。在某些环境下,图标显示不出来。如果你遇到了这个问题,看我在Stackoverfolw的回答

    import sys
    from PyQt5.QtGui import QIcon  
    from PyQt5.QtWidgets  import QWidget, QApplication
    class Icon(QWidget):  
        def __init__(self,  parent = None):  
            super(Icon,self).__init__(parent)    
            self.initUI()
        def initUI(self):
            self.setGeometry(500, 500, 450, 150)  
            self.setWindowTitle('演示程序图标例子')  
            self.setWindowIcon(QIcon('7092.gif'))  
    if __name__ == '__main__':   
        app = QApplication(sys.argv)
        icon = Icon()  
        icon.show()
        sys.exit(app.exec_())
    
    import sys
    from PyQt5.QtWidgets import QWidget, QToolTip, QApplication
    from PyQt5.QtGui import QFont
    class Winform(QWidget):
        def __init__(self):
            super().__init__()
            self.initUI()
        def initUI(self):
            QToolTip.setFont(QFont('SansSerif', 10))
            self.setToolTip('这是一个<b>气泡提示</b>')
            self.setGeometry(200, 300, 400, 400)
            self.setWindowTitle('气泡提示')           
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        win = Winform()
        win.show()
        sys.exit(app.exec_())
    

    3.4 创建主窗口类

    QWidget 可以作为嵌套型的窗口存在,结构简单,而 QMainWindow 是一个程序框架,有自己的布局,可以在布局中添加控件,如将工具栏添加到布局管理器中。它常常作为 GUI 的主窗口。

    主窗口在 GUI 程序是唯一的,是所有窗口中的最顶层窗口,其他窗口都是它的直接或间接子窗口。PyQt5 中,主窗口中会有一个控件(QWidget)占位符来占着中心窗口,可以使用 setCentralWidget() 来设置中心窗口。QMainWindow 继承自 QWidget 类,拥有 QWidget 所有的派生方法和属性。QMainWindow 类中比较重要的方法,如下表:

    import sys
    from PyQt5.QtWidgets import QMainWindow, QApplication
    class MainWidget(QMainWindow):
        def __init__(self, parent=None):
            super().__init__(parent)
            # 设置主窗体标签
            self.setWindowTitle("QMainWindow 例子")         
            self.resize(400, 200) 
            self.status = self.statusBar() 
            # 5 秒后状态栏消失
            self.status.showMessage("这是状态栏提示", 5000)
    if __name__ == "__main__": 
        app = QApplication(sys.argv)
        main = MainWidget()
        main.show()
        sys.exit(app.exec_())
    

    3.4.2 菜单栏

    菜单栏是一组命令的集合:

    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):
            # 创建一个图标、一个 exit 的标签和一个快捷键组合,都执行了一个动作
            exitAct = QAction(QIcon('7092.gif'), '&Exit', self)
            exitAct.setShortcut('Ctrl+Q')
            # 创建了一个状态栏,当鼠标悬停在菜单栏的时候,能显示当前状态
            exitAct.setStatusTip('Exit application')
            # 当执行这个指定的动作时,就触发了一个事件。
            ## 这个事件跟 QApplication 的 quit() 行为相关联,所以这个动作就能终止这个应用。
            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()
    def main():
        app = QApplication(sys.argv)
        ex = Example()
        sys.exit(app.exec_())
    if __name__ == '__main__':
        main()
    

    我们创建了只有一个命令的菜单栏,这个命令就是终止应用。同时也创建了一个状态栏。而且还能使用快捷键 Ctrl+Q 退出应用。QAction 是菜单栏、工具栏或者快捷键的动作的组合。

    3.4.3 子菜单

    子菜单是嵌套在菜单里面的二级或者三级等的菜单。

    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') # 使用 QMenu 创建一个新菜单 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() def main(): app = QApplication(sys.argv) ex = Example() sys.exit(app.exec_()) if __name__ == '__main__': main() 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_())

    本例创建了一个行为菜单。这个行为/动作能切换状态栏显示或者隐藏。用 checkable 选项创建一个能选中的菜单。

    3.4.5 context 菜单

    context menu 也叫弹出框,是在某些场合下显示的一组命令。例如,Opera 浏览器里,网页上的右键菜单里会有刷新,返回或者查看页面源代码。如果在工具栏上右键,会得到一个不同的用来管理工具栏的菜单。

    为了使 context menu 生效,需要实现 contextMenuEvent() 方法。

    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")
            openAct = cmenu.addAction("Open")
            quitAct = cmenu.addAction("Quit")
            # 使用 exec_() 方法显示菜单。从鼠标右键事件对象中获得当前坐标。
            # mapToGlobal() 方法把当前组件的相对坐标转换为窗口(window)的绝对坐标。
            action = cmenu.exec_(self.mapToGlobal(event.pos()))
            if action == quitAct:
                qApp.quit()
    def main():
        app = QApplication(sys.argv)
        ex = Example()
        sys.exit(app.exec_())
    if __name__ == '__main__':
        main()
    

    菜单栏包含了所有的命令,工具栏就是常用的命令的集合。

    import sys
    from PyQt5.QtWidgets import QMainWindow, QAction, qApp, QApplication
    from PyQt5.QtGui import QIcon
    from PyQt5.QtCore import QFileInfo
    class Example(QMainWindow):
        def __init__(self):
            super().__init__()
            self.initUI()
        def initUI(self):
            #root = QFileInfo(__file__).absolutePath() # 避免图标不显示,而使用绝对路径
            exitAct = QAction(QIcon('paper.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()
    def main():
        app = QApplication(sys.argv)
        ex = Example()
        sys.exit(app.exec_())
    if __name__ == '__main__':
        main()
    

    3.5 主窗口居中显示

    QMainWindow 利用 QDesktopWidget 类实现窗口居中显示,如下示例:

    from PyQt5.QtWidgets import QDesktopWidget, QApplication, QMainWindow
    import sys
    class Winform(QMainWindow):
        def __init__(self, parent=None):
            super().__init__(parent)
            self.setWindowTitle('主窗口放在屏幕中间例子')
            self.resize(300, 200)
            self.center()
        def center(self):
            # 获取屏幕信息
            screen = QDesktopWidget().screenGeometry()
            size = self.geometry()
            x = int((screen.width() - size.width()) / 2)
            y = int((screen.height() - size.height()) / 2)
            self.move(x, y)
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        win = Winform()
        win.show()
        sys.exit(app.exec_())
    

    也可以直接使用 QDesktopWidget().availableGeometry().center() 设置窗口居中:

    import sys
    from PyQt5.QtWidgets import QWidget, QDesktopWidget, QApplication
    class CenterWin(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)
            # 然后通过 move 函数把主窗口的左上角移动到其框架的左上角,这样就把窗口居中了
            self.move(qr.topLeft())
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        ex = CenterWin()
        sys.exit(app.exec_())
    

    3.6 关闭窗口

    关闭一个窗口最直观的方式就是点击标题栏的那个叉,接下来,我们展示的是如何用程序关闭一个窗口。这里我们将接触到一点 signalsslots 的知识。本例使用的是 QPushButton 组件类。

    QPushButton(string text, QWidget parent = None)
    

    text 参数是想要显示的按钮名称,parent 参数是放在按钮上的组件,在我们的例子里,这个参数是 QWidget。应用中的组件都是一层一层的,在这个层里,大部分的组件都有自己的父级,没有父级的组件,是顶级的窗口(即主窗口)。

    下面创建一个点击之后就退出窗口的按钮:

    import sys
    from PyQt5.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()
    

    事件传递系统在 PyQt5 内建的 signal 和 slot 机制里面。点击按钮之后,信号会被捕捉并给出既定的反应。QCoreApplication 包含了事件的主循环,它能添加和删除所有的事件,instance() 创建了一个它的实例。QCoreApplication 是在 QApplication 里创建的。点击事件和能终止进程并退出应用的 quit 函数绑定在了一起。在发送者和接受者之间建立了通讯,发送者就是按钮,接受者就是应用对象。

    3.7 消息盒子

    默认情况下,我们点击标题栏的 X 按钮,QWidget 就会关闭。但是有时候,我们修改默认行为。比如,如果我们打开的是一个文本编辑器,并且做了一些修改,我们就会想在关闭按钮的时候让用户进一步确认操作。

    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)
            # 这里判断返回值,如果点击的是Yes按钮,我们就关闭组件和应用,否者就忽略关闭事件。
            if reply == QMessageBox.Yes:
                event.accept()
            else:
                event.ignore()
    def main():
        app = QApplication(sys.argv)
        ex = Example()
        sys.exit(app.exec_())