PyQt5桌面应用开发(10):界面布局基本支持

PyQt5桌面应用系列

布局

布局是设计报表和交互中的重要工作。但是布局的内涵有两个层面上的:总体布局和界面布局。

从报表设计的角度出发,布局应该把最重要的信息显示在最显眼的位置,把次要的信息放在次显眼的位置,把不重要的信息放在不显眼的位置。报表设计始终还是要围绕报表的目的(也就是用户利用数据、信息进行决策的目的)来进行的。围绕一个决策来布局信息的分类、汇总、分析、比较、评价、预测等功能,这些功能的实现都是为了帮助用户做出决策。

从交互设计的角度出发,应该把核心、重要的功能放在最显眼的位置,把次要的功能放在次显眼的位置,把不重要的功能放在不显眼的位置,这与报表设计相同;此外,还应该把相互关联的操作放在一起,把相互独立的功能分开。

这个在实际的设计中,可以通过分组、分页、分栏、分区等方式来实现报表信息和交互的总体布局。

而在较低的一个层次上,则是一个具体的界面上的控件如何定位、缩放、对齐,控件上相关的控件之间如何相互协调。这个层次上的布局,可以通过控件的定位位置、几何尺寸来描述。最基础的布局就是控件的位置和尺寸,这也叫绝对位置布局。在这个基础上,还能实现相对位置布局,通过以某个控件的位置和尺寸为基准,把其它控件的位置和尺寸转换相对坐标系来描述。这都是直接和更底层的布局算法,当然所有的更抽象的布局都会归结成相对布局算法和绝对布局算法。

  1. 抽象布局
  2. 相对布局
  3. 绝对布局

而抽象布局是在人类认知的概念中对一组控件的排列进行抽象的描述,比如一列、一行、一个网格、一个表格、一个树形结构,等等。抽象的层次较高的界面布局,就更加便于人类使用,为思考布局提供了工具,但是也限制了布局的灵活性;较低层次的布局,需要更多的精力来思考,但是也提供了更强大的功能和灵活性。

大部分的UI开发工具包,都提供了一定层次的抽象布局工具。例如JavaFX、Qt、Web前端工具中都有类似的概念和工具支持。Qt5提供的布局工具并不丰富,但是也足够形成复杂的界面。

利器

Qt5提供布局的方式是每一个QWidget都有一个QLayout对象,通过setLayout方法来设置。QWidget的子节点,通过加入布局类中来获得自动计算相对位置、大小和绝对位置大小的能力。Qt5中最常用的布局类有四个:QHBoxLayout、QVBoxLayout、QGridLayout、QFormLayout。

在这里插入图片描述

其中QHBoxLayout和QVBoxLayout是QBoxLayout的子类,这三个类描述的就是一行或者一列控件。当控件排成一列时,其x方向的位置和尺寸都保持一致,y方向的位置成线性增长,尺寸通过按比例伸缩、固定最小尺寸、固定最大尺寸、扩张、最小化等概念来计算。最后这个部分通常称为sizePolicy,也就是伸缩策略。同样,当控件拍成一行时,其y方向的位置和尺寸都保持一致,x方向的位置成线性增长,尺寸通过按比例伸缩、固定最小尺寸、固定最大尺寸、扩张、最小化等概念来计算。

QGridLayout是QLayout的子类,它描述的是一个网格布局,也就是一个二维的布局。它的每一个子控件都有一个行号和列号,通过这两个号码来描述控件的位置。每一个行和列都有一个伸缩策略,通过这个策略来计算控件的尺寸。QGridLayout的伸缩策略是通过行和列的伸缩策略来计算的,而不是通过每一个控件的伸缩策略来计算的。

QFormLayout是QLayout的子类,它描述的是一个表格布局,也就是一个二维的布局。它的每一个子控件都有一个行号和列号,通过这两个号码来描述控件的位置。每一个行和列都有一个伸缩策略,通过这个策略来计算控件的尺寸。QFormLayout的伸缩策略是通过行和列的伸缩策略来计算的,而不是通过每一个控件的伸缩策略来计算的。

最后还有一个我不知道有什么用的QStackedLayout,这个布局类描述的是一个堆栈布局,也就是一堆控件,只有一个控件是可见的,其它的控件都是不可见的。这个布局类还没有提供显式的切换方案,必须自己调用QStackedLayout的setCurrentIndex方法来切换。通常会把这个方法作为一个槽函数绑定到某个信号上。

游戏

这里我们设计一个展示这几中布局的小程序。

  • 报表:每种布局的展示;
  • 报表:QStackLayout自动(定时)切换控件。
  • 交互:切换布局;

在这里插入图片描述

程序源代码如下。

import random
import sys
from functools import partial
from typing import Union

from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QFormLayout, \
    QStackedLayout, QComboBox, QLabel, QMainWindow, QDockWidget

# module shared variables
colors = ['yellow', 'red', 'cyan', 'green', 'white',
          'gray', 'orange', 'darkgray', 'transparent']
name_layouts = {'vbox': QVBoxLayout,
                'hbox': QHBoxLayout,
                'grid': QGridLayout,
                'form': QFormLayout,
                'stacked': QStackedLayout}

timer: Union[QTimer, None] = None


# module shared functions
def show_layout(parent: QMainWindow, layout_short_name: str):
    random.shuffle(colors)

    widgets = [QLabel(c, win) for c in colors]

    for i, (c, w) in enumerate(zip(colors, widgets)):
        w.setStyleSheet(f"background-color: {c}")
        w.setAlignment(Qt.AlignCenter)
        w.setFont(QFont("SimHei", 24))

    widget = QWidget(parent)

    if not (layout_short_name in name_layouts):
        raise ValueError(
            f"{layout_short_name} not in {list(name_layouts.keys())}")
    layout = name_layouts[layout_short_name]()

    for i, w in enumerate(widgets):
        if layout_short_name == 'grid':
            layout.addWidget(w, i // 3, i % 3)
        elif layout_short_name == 'form':
            layout.addRow(w.text() + ":", w)
        else:
            layout.addWidget(w)

    global timer
    if timer is not None:
        timer.stop()
        timer = None

    if layout_short_name == 'stacked':
        timer = QTimer(parent)

        def change_layout():
            try:
                layout.setCurrentIndex((layout.currentIndex() + 1) % layout.count())
            except Exception as e:
                pass

        timer.timeout.connect(change_layout)
        timer.start(1000)

    widget.setLayout(layout)
    parent.setCentralWidget(widget)


class QCycleComboBox(QComboBox):
    """
    A combobox that cycles through its items when the mouse wheel is used, or the up/down keys are pressed.
    """

    def __init__(self, parent: QWidget = None) -> None:
        super(QCycleComboBox, self).__init__(parent)

    def wheelEvent(self, event):
        if event.angleDelta().y() > 0:
            self.setCurrentIndex((self.currentIndex() - 1) % self.count())
        else:
            self.setCurrentIndex((self.currentIndex() + 1) % self.count())

    def keyPressEvent(self, e) -> None:
        if e.key() == Qt.Key_Up:
            self.setCurrentIndex((self.currentIndex() - 1) % self.count())
        if e.key() == Qt.Key_Down:
            self.setCurrentIndex((self.currentIndex() + 1) % self.count())
        else:
            return super(QCycleComboBox, self).keyPressEvent(e)


if __name__ == '__main__':
    app = QApplication([])
    win = QMainWindow()
    
    choice = QCycleComboBox(win)
    choice.addItems(name_layouts.keys())
    dock = QDockWidget('Layouts selection', win)
    dock.setWidget(choice)
    dock.setFeatures(QDockWidget.NoDockWidgetFeatures)
    win.addDockWidget(Qt.TopDockWidgetArea, dock)
    choice.currentTextChanged.connect(partial(show_layout, win))
    show_layout(win, 'vbox')

    win.setWindowTitle("Layouts")
    win.setGeometry(100, 100, 800, 600)
    win.show()

    sys.exit(app.exec_())

这个程序的结构和逻辑非常简单,不再赘述。

总结

  1. 布局分为两种层次,高层次布局和界面布局;
  2. 界面布局分为:概念布局、相对布局和绝对布局;
  3. Qt5的常用布局类有:QVBoxLayout、QHBoxLayout、QGridLayout、QFormLayout和QStackedLayout。
posted @ 2023-05-07 22:39  大福是小强  阅读(60)  评论(0编辑  收藏  举报  来源