03. 窗口设计

一、PySide6窗口运行原理

  窗口是图形用户界面(GUI)程序开发的基础,我们平常所见的各种图形界面都是在窗口中放置不同的控件、菜单和工具条,实现不同的动作和目的。图形界面程序开发就是在窗口上放置不同类型的控件、菜单和工具条按钮,并为各个控件、菜单和工具条按钮编写代码使其 “活跃” 起来。

  PySide6 的 QtWidgets 模块集中了可视化编程的各种窗口和控件,这些窗口和控件一般都是直接或间接从 QWidget 类继承来的。QWidget 是从 QObjectQPaintDevice 类继承而来的,QObject 类主要 实现信号和槽的功能QPaintDevice 类主要 实现控件绘制的功能

  QWidget 类通常用作独立显示的窗口,这时窗口上部有标题栏,QWidget 类也可以当作普通的容器控件使用,在一个窗口或其他容器中添加 QWidget,再在 QWidget 中添加其它控件。当一个控件有父窗口时,不显示该控件的标题栏;当控件没有父窗口时,会显示标题栏。常用于独立窗口的类还有 QMainWindowQDialog,它们都是从 QWidget 类继承而来的。

继承于QWidget的类

  在创建 QWidget 窗口对象之前,需要先介绍一个 QApplication 类。QApplication 类管理可视化 QWidget 窗口,对 QWidget 窗口的运行进行初始化参数设置,并负责 QWidget 窗口的退出收尾工作,因此 在创建 QWidget 窗口对象之前,必须先创建一个 QApplication 类的实例,为后续的窗口运行做好准备。

  如果不是基于 QWidget 窗口的程序,可以使用 QGuiApplication 类进行初始化,有些程序通过命令行参数执行任务而不是通过 GUI,这时可以使用 QCoreApplication 类进行初始化,以避免初始化占用不必要的资源。

QApplication类的继承关系

  QApplication 类是从 QGuiApplication 类继承来的,QGuiApplication 类为 QWidget 窗口 提供会话管理功能,用户退出时可以友好地终止程序,如果终止不了还可以取消对应的进程,可以保存程序的所有状态用于将来的会话。

  QGuiApplication 类继承自 QCoreApplication 类,QCoreApplication 类的一个核心功能是 提供事件循环(event loop)。这些事件可以来自操作系统,如鼠标、定时器(timer)、网络,以及其他原因产生的事件都可以被收发。通过调用 exec() 函数 进入事件循环,遇到 quit() 函数 退出事件循环,退出时发送 aboutToQuit() 信号,类似于 Python 的 sys 模块的 exit() 方法。

  当某个控件 发出信号 时,sendEvent() 函数 立即处理事件postEvent() 函数 把事件放入事件队列以等待后续处理,处于队列中的事件可以通过 removePostedEvent() 方法 删除,也可通过 sendPostedEvent() 方法 立即处理事件

  由于 QApplication 类进行可视化界面的初始化工作,因此在任何可视化对象创建之前必须先创建 QApplication 对象,而且还可以通过命令行参数设置一些内部状态。QApplication 类的主要功能有

  • 处理命令行参数设置程序的内部初始状态
  • 处理事件从窗口接收事件,并通过 sendEvent()postEvent() 发送给需要的窗口。
  • 获取指定位置处的窗口(widgetAt())、顶层窗口列表(topLevelWidgets()),处理窗口关闭(closeAllWindows())等事件。
  • 使用桌面对象信息进行初始化,这些设置如调色板(palette)、字体(font)、双击间隔(doubleClickInterval),并跟踪这些对象的变化。
  • 定义这个可视化界面的外观,外观由 QStyle 对象包装,运行时通过 setStyle() 函数进行设置。
  • 提供一些非常方便的类,例如屏幕信息类(desktop)和剪切板类(clipboard)。
  • 管理鼠标(setOverrideCursor())。

二、QWidget窗口的创建

  PySide6 的窗口类主要有三种,分别为 QWidgetQMainWindowQDialog,其中 QMainWindowQDialogQWidget 类继承而来。要创建和显示窗口,需要用这 3 个类中的任意一个类实例化对象,并让窗口对象显示并运行起来。

  我们可以在终端中使用 pip 安装 PySide6 模块。默认是从国外的主站上下载,因此,我们可能会遇到网络不好的情况导致下载失败。我们可以在 pip 指令后通过 -i 指定国内镜像源下载

pip install pyside6 -i https://mirrors.aliyun.com/pypi/simple

  国内常用的 pip 下载源列表:

import sys

from PySide6.QtWidgets import QApplication, QWidget

if __name__ == "__main__":
    app = QApplication(sys.argv)                                                # 1.创建一个QApplication类的实例

    window = QWidget()                                                          # 2.创建一个窗口
    window.resize(300, 150)                                                     # 3.设置窗口的尺寸
    window.move(300, 300)                                                       # 4.移动窗口
    window.setWindowTitle("基于PySide6的桌面应用程序")                            # 5.设置窗口标题

    window.show()                                                               # 6.显示窗口
  
    n = app.exec()                                                              # 7.执行exec()方法,进入事件循环,若遇到窗口退出命名,返回整数n
    sys.exit(n)                                                                 # 8.进入程序的主循环并通过exit()函数确保主循环安全结束

  窗口类在 PySide6QtWidgets 模块中,使用窗口类之前,需要用 from PySide6.QtWidgets import QWidget, QMainWindow, QDialog 语句把它们导入进来。

  在 PySide6 中窗口类主要有三种,分别是 QWidgetQMainWindowQDialog,其中 QMainWindowQDialogQWidget 类继承而来。要创建和显示窗口,需要使用这 3 个类中的任意一个类实例化对象,并让窗口对象显示并运行出来。

  第 1 步是创建 QApplication 类的实例对象 app,为 窗口的创建进行初始化,其中 sys.argv字符串列表记录启动程序时的程序文件名和运行参数sys.argv 的第 1 个元素的值是 程序文件名及路径,也可以不输入参数。sys.argv 创建 QApplication 实例对象 app。QApplication 可以接受的两个参数是 -nograb-dograb-nograb 告诉 Python 禁止获取鼠标和键盘事件,-dograb 则忽略 -nograb 选项功能,而不管 -nograb 参数是否存在于命令行参数中。一个程序中只能创建一个 QApplication 实例,并且要在创建窗口前创建。

  第 2 步是用不带参数的 QWidget创建 QWidget 窗口实例对象 window,该窗口是独立窗口,有标题栏。

  第 3 步是窗口实例对象 window 调用 resize(width:int, height:int) 方法 设置窗口的尺寸

  第 4 步是窗口实例对象 window 调用 move(x:int, y:int ) 方法 将窗口移动到屏幕的指定位置

  第 5 步是窗口实例对象 window 调用 setWindowTitle(title:str) 方法 设置窗口的标题

  第 6 步是窗口实例对象 window 调用 show() 方法 显示窗口,这时窗口是可见的。

  第 7 步是执行 QApplication 实例对象 app 的 exec() 方法,开始窗口的事件循环,从而保证窗口一直处于显示状态。如果窗口上有其他控件,并为控件的消息编写了处理程序,则可以完成相应的动作。如果用户 单击窗口右上角的关闭窗口按钮正常退出界面,或者因 程序崩溃而非正常终止窗口的运行,都将 引发关闭窗口(closeAllWindows())事件,这时 app 的方法 exec()会返回一个整数,如果这个整数是 0 表示 正常退出,如果 非 0 表示 非正常退出。请注意,当执行到 app 的 exec() 方法时,会停止后续语句的执行,直到所有可视化窗体都关闭(退出)后才执行后续的语句。

  第 8 步是调用系统模块的 exit() 方法,通知 Python 解释器程序已经结束,如果是 sys.exit(0) 状态,则 Python 认为是 正常退出;如果不是 sys.exit(0) 状态,则 Python 认为是 非正常退出。无论什么情况,sys.exit() 都会抛出一个异常 SystemExit,这时可以使用 try…except 语句捕获这个异常,并执行 except 中的语句。

三、PySide6可视化编程架构

  之前的代码将创建窗口和创建窗口控件的代码与控件的事件代码放到同一段程序中,如果程序非常复杂,控件很多,事件也很多,势必造成代码混杂,程序可读性差,编程效率也不高。为此可以把创建窗口控件的代码放到一个函数或类中,创建窗口的代码放到主程序中,从而使程序的可读性得到提高,也提高了编程效率。

3.1、界面用函数来定义

  下面的代码将创建窗口控件的代码放到函数 setupUi() 中。setupUi() 函数的形参是窗口,在主程序中调用 setupUi() 函数,并把窗口实例作为实参传递给 setupUi() 函数,在 setupUi() 函数中往窗口上创建控件。

import sys

from PySide6.QtWidgets import QApplication, QWidget

def setupUi(window:QWidget):
    window.resize(300, 150)                                                     # 1.设置窗口的尺寸
    window.move(300, 300)                                                       # 2.移动窗口
    window.setWindowTitle("基于PySide6的桌面应用程序")                            # 3.设置窗口标题

if __name__ == "__main__":
    app = QApplication(sys.argv)                                                # 1.创建一个QApplication类的实例
    window = QWidget()                                                          # 2.创建一个窗口
    setupUi(window)                                                             # 3.将窗口作为实参传递给setupUi()函数
    window.show()                                                               # 4.显示窗口
    sys.exit(app.exec())                                                        # 5.进入程序的主循环并通过exit()函数确保主循环安全结束

3.2、界面用类来定义

  下面的程序将创建窗口控件的代码定义到 MyUi 类的 setupUi() 函数中,各个控件是 MyUi 类中的属性,在主程序中用 MyUi 类实例化对象 ui,这样在主程序中就可以用 ui 引用窗口上的任何控件,在主程序中通过 ui 就可以修改控件的参数。

import sys

from PySide6.QtWidgets import QApplication, QWidget

class MyUi:
    def setupUi(self, window:QWidget):
        window.resize(300, 150)                                                 # 1.设置窗口的尺寸
        window.move(300, 300)                                                   # 2.移动窗口
        window.setWindowTitle("基于PySide6的桌面应用程序")                        # 3.设置窗口标题

if __name__ == "__main__":
    app = QApplication(sys.argv)                                                # 1.创建一个QApplication类的实例
    window = QWidget()                                                          # 2.创建一个窗口
    ui = MyUi()                                                                 # 3.用MyUi类创建实例ui
    ui.setupUi(window)                                                          # 4.窗口作为实参传入
    window.show()                                                               # 5.显示窗口
    sys.exit(app.exec())                                                        # 6.进入程序的主循环并通过exit()函数确保主循环安全结束

3.3、界面用模块来定义

  如果一个界面非常复杂,创建界面控件的代码也就会很多,我们可以使用包和模块来管理,即在程序中创建界面控件的类 MyUi 可以单独存放到一个文件中,在使用的时候用 import 语句把 MyUi 类导入进来,实现控件代码与窗口代码的分离。

from PySide6.QtWidgets import QWidget

class MyUi:
    def setupUi(self, window:QWidget):
        window.resize(300, 150)                                                 # 1.设置窗口的尺寸
        window.move(300, 300)                                                   # 2.移动窗口
        window.setWindowTitle("基于PySide6的桌面应用程序")                        # 3.设置窗口标题

  新建一个 py 文件,在这个 py 文件中用 from myUi import MyUi 语句把 MyUi 类导入进来,在主程序中用 ui = MyUi() 语句创建 MyUi 类的实例对象 ui,然后就可以用 ui 引用 myUi.py 文件中定义的控件。

import sys

from PySide6.QtWidgets import QApplication, QWidget

from ui import MyUi

if __name__ == "__main__":
    app = QApplication(sys.argv)                                                # 1.创建一个QApplication类的实例
    window = QWidget()                                                          # 2.创建一个窗口
    ui = MyUi()                                                                 # 3.用MyUi类创建实例ui
    ui.setupUi(window)                                                          # 4.把窗口作为实参传入
    window.show()                                                               # 5.显示窗口
    sys.exit(app.exec())                                                        # 6.进入程序的主循环并通过exit()函数确保主循环安全结束

3.4、界面与逻辑的分离

  上面的代码中,可以把创建窗口和对控件操作的代码单独放到一个函数或类中,含有控件的代码称为界面代码实现控件动作的代码称为逻辑或业务代码

  下面的代码创建 widget() 函数,在函数中用 widget = QWidget(parent) 语句创建 QWidget 类的实例对象 widget,这时首先执行的是 QWidget 类的初始化函数 __init__(),经过初始化后的对象成为真正窗口,注意 widget() 函数的返回值是窗口实例对象 widget。在主程序中调用 widget() 函数,得到返回值,然后显示窗口并进入消息循环。

import sys

from typing import Optional

from PySide6.QtWidgets import QApplication, QWidget

from ui import MyUi

def widget(parent:Optional[QWidget]=None):
    widget = QWidget(parent)                                                    # 1.创建QWidget类的对象,调用QWidget类的__init__()方法
  
    ui = MyUi()                                                                 # 2.实例化myUi.py文件中的MyUi类
    ui.setupUi(widget)                                                          # 3.以widget为实参传递给形参window
    return widget                                                               # 4.函数的返回值就是窗口实例对象

if __name__ == "__main__":
    app = QApplication(sys.argv)                                                # 1.创建一个QApplication类的实例
    window = widget()                                                           # 2.创建一个窗口
    window.show()                                                               # 3.显示窗口
    sys.exit(app.exec())                                                        # 4.进入程序的主循环并通过exit()函数确保主循环安全结束

  上面代码中,只是用一个函数将界面与逻辑或业务分离,如果要对界面进行多种操作或运算,显然只用一个函数来定义是不够的。由于在类中可以定义多个函数,因而如果用类来代替上述函数,就可以极大地提高编程效率。为此可以把创建窗口和对控件操作的代码放到一个类中。

  下面的代码中,我们采用多继承的方式定义了一个类 MyWidget,称这个类为窗体的业务逻辑类,它的父类是 QWidget 和 MyUi。在这个类的构造方法中,首先调用 super() 方法获取父类,并执行父类的构造方法。在多继承时,使用 super() 得到的第一个基类,在这里就是 QWidget。所以,执行这条语句后,self 就是一个 QWidget 对象。

  因为 MyWidget 的基类包括 MyUi 类,所以可以调用 MyUi 类的 setupUi() 函数。同时,经过前面调用父类的构造方法,self 是一个 QWidget 对象,可以作为参数传递给 setupUi() 函数,正好作为各组件的窗体容器。

import sys

from typing import Optional

from PySide6.QtWidgets import QApplication, QWidget

from ui import MyUi

class MyWidget(QWidget, MyUi):
    def __init__(self, parent:Optional[QWidget]=None):
        super().__init__(parent)                                                # 1.调用父类的__init__()方法
        self.setupUi(self)                                                      # 2.以self为实参传递给形参window

if __name__ == "__main__":
    app = QApplication(sys.argv)                                                # 1.创建一个QApplication类的实例
    window = MyWidget()                                                         # 2.创建一个窗口
    window.show()                                                               # 3.显示窗口
    sys.exit(app.exec())                                                        # 4.进入程序的主循环并通过exit()函数确保主循环安全结束

  通过这样的多继承,MyUi 类中定义的窗体上的所有界面组件对象就变成了新定义的类 MyWidget 的公共属性,外界可以直接访问这些界面组件。这种方式有点是访问方便,缺点是过于开放,不符合面向对象严格封装的设计思想。并且在界面上的组件和 MyWidget 类中新定义的属性混合在一起,不便于区分。

  针对多继承存在的一些问题,这里我们改用单继承的方式。新定于的窗体业务逻辑类 MyWidget 只有一个基类 QWidget,在 MyWidget 的构造方法中,首先调用父类(也就是 QWidget)的构造方法,这样 self 就是一个 QWidget 对象。

  然后,我们显示地创建了一个 MyUi 类的私有属性 self.__ui。该私有属性包含了可视化设计 UI 窗体上的所有组件。所以,只有通过 self.__ui 才可以访问窗体上的组件,包含调用其创建界面组件的 setupUi() 函数。

  由于 self.__ui 是 MyWidget 类的私有属性,因此在应用程序中创建的 MyWidget 类的对象不能直接访问 window.__ui 属性,也就无法直接访问窗体上的界面组件。

import sys

from typing import Optional

from PySide6.QtWidgets import QApplication, QWidget

from ui import MyUi

class MyWidget(QWidget):
    def __init__(self, parent:Optional[QWidget]=None):
        super().__init__(parent)                                                # 1.调用父类Qwidget类的__init__()方法
        self.__ui = MyUi()                                                      # 2.实例化myUi.py文件中的MyUi类
        self.__ui.setupUi(self)                                                 # 3.以self为实参传递给形参window

if __name__ == "__main__":
    app = QApplication(sys.argv)                                                # 1.创建一个QApplication类的实例
    window = MyWidget()                                                         # 2.创建一个窗口
    window.show()                                                               # 3.显示窗口
    sys.exit(app.exec())                                                        # 4.进入程序的主循环并通过exit()函数确保主循环安全结束
posted @ 2024-12-11 18:48  星光映梦  阅读(548)  评论(0)    收藏  举报