Maya PySide2 / PySide チュートリアル 初学編 ⑥ – レイアウト管理ってなんだろう

python,qt,PySide,PySide2,Tutorial

このチュートリアルはレイアウトについて学んでいきます

PySideのようなGUIプログラミングで必ずとやらなければいけないことはレイアウト管理です
レイアウト管理とは、ウィンドウ上にウィジェットをどのように配置するかということです
管理方法は2種類あり、一つは絶対配置、もう一つはレイアウトクラスの2種類です

絶対配置とは各ウィジェットの位置とサイズをピクセル単位で指定します。絶対位置指定を使用する場合、いくつかの点を理解しておく必要があります

  • プラットフォームによっては,アプリケーションの見え方が変わる
  • ウィンドウのサイズを変更しても、ウィジェットのサイズと位置は変わらない
  • アプリケーションのフォントを変更すると、レイアウトが崩れることがある
  • レイアウトを変更しようとすると、レイアウトを全面的にやり直さなければならず、手間と時間がかかってしまう
  • 2Kや4Kなどの異なるモニターや解像度が異なる場合、レイアウトが崩れることがある※1

※1
Windowsの場合: 設定>ディスプレ>拡大縮小とレイアウトを100%以外にしている
Mayaの場合: WIndows>Setting/Preferences>Preferences - Categories>Interface - Onterface Scaling - Use Custom scalingを100%以外にしている

import sys

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *


class Example(QMainWindow):
    def __init__(self, parent=None, *args, **kwargs):
        super(Example, self).__init__(parent, *args, **kwargs)
        self.initUI()

    def initUI(self):
        self.setGeometry(500, 300, 400, 270)
        self.setWindowTitle("Absolute Layout")

        label1 = QLabel("Maya PySide2", self)
        label1.move(50, 40)

        label2 = QLabel("Tutorials", self)
        label2.move(150, 90)


def main():
    app = QApplication.instance()
    ex = Example()
    ex.show()
    sys.exit()
    app.exec_()


main()

20210824_01
今回の例では、move()メソッドを呼び出してウィジェットを配置しました
これまでのチュートリアルでもmove()メソッドを使用してきましたがxとyの座標を指定して、ラベルを配置しています
座標系の始点は左上の隅で、xの値は左から右に向かって大きくなります。そしてyの値は上から下に向かって大きくなります

ボックスレイアウト

レイアウトクラスを使ったレイアウト管理はより柔軟で使いやすいウィジェットです
基本的なレイアウトクラスは、QHBoxLayoutQVBoxLayoutです
この2種類のウィジェットを水平方向と垂直方向に並べてくれます
QBoxLayoutというボックスレイアウトもありますがこれはQHBoxLayoutQVBoxLayoutの親にあたるクラスになります

20210824_02

例えば、Mayaのオプション画面でよくある3つのボタンが下に配置したWindowを作りたいとします
このようなレイアウトを作成するには、水平と垂直のボックスを1つずつ用意し、必要なスペースを確保するために、ストレッチファクターを追加すると簡単に作れます

import sys

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *


class Example(QMainWindow):
    def __init__(self, parent=None, *args, **kwargs):
        super(Example, self).__init__(parent, *args, **kwargs)
        self.initUI()

    def initUI(self):
        self.setGeometry(500, 300, 400, 270)
        self.setWindowTitle("Box Layout")

        createButton = QPushButton("Create")
        applayButton = QPushButton("Apply")
        closeButton = QPushButton("Close")

        hBoxLayout = QHBoxLayout()
        hBoxLayout.addWidget(createButton)
        hBoxLayout.addWidget(applayButton)
        hBoxLayout.addWidget(closeButton)

        vBoxLayout = QVBoxLayout()
        vBoxLayout.addStretch(1)
        vBoxLayout.addLayout(hBoxLayout)

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


def main():
    app = QApplication.instance()
    ex = Example()
    ex.show()
    sys.exit()
    app.exec_()


main()

20210824_03

createButton = QPushButton("Create")
applayButton = QPushButton("Apply")
closeButton = QPushButton("Close")

ここではQPushButtonを三つ作っています

hBoxLayout = QHBoxLayout()
hBoxLayout.addWidget(createButton)
hBoxLayout.addWidget(applayButton)
hBoxLayout.addWidget(closeButton)

横長のボックスレイアウトを作成し、先ほど作成したボタンを追加しています

vBoxLayout = QVBoxLayout()
vBoxLayout.addStretch(1)
vBoxLayout.addLayout(hBoxLayout)

レイアウトを再現するために、水平方向のレイアウトを垂直方向のレイアウトに入れています
縦長のボックスのストレッチファクターは、ボタンのある横長のボックスをウィンドウの下部に押しやっています

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

先ほど設定したレイアウト(ウィジェット)をQMainWindowの描画するために新しいQWidgetにレイアウトを設定し、そのQWidgetをQMainWindowのCentralWidgetとして設定します

もしこの設定をしていない場合、設定したレイアウトが表示されなかったり、表示はされるが左上にウィジェットが表示されているだけという状態になります
これはQMainWindowの仕様上そうなってしまうのでQMainWindowにレイアウトを設定してウィジェットを表示する場合は気を付けましょう

グリッドレイアウト

グリッドレイアウトは空間を行と列に分割し、レイアウトすることができます
グリッドレイアウトを作成するには、QGridLayoutを使用します
20210824_04
グリッドレイアウトはMayaのTransform Attributesみたいなのを再現するときに便利です

import sys

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *


class Example(QMainWindow):
    def __init__(self, parent=None, *args, **kwargs):
        super(Example, self).__init__(parent, *args, **kwargs)
        self.initUI()

    def initUI(self):
        self.setGeometry(500, 300, 400, 270)
        self.setWindowTitle("Grid Layout")

        translateLabel = QLabel("Translate")
        rotateLabel = QLabel("Rotate")
        scaleLabel = QLabel("Scale")

        gridLayout = QGridLayout()

        gridLayout.addWidget(translateLabel, 0, 0)
        gridLayout.addWidget(QLineEdit("0.1"), 0, 1)
        gridLayout.addWidget(QLineEdit("0.2"), 0, 2)
        gridLayout.addWidget(QLineEdit("0.3"), 0, 3)

        gridLayout.addWidget(rotateLabel, 1, 0)
        gridLayout.addWidget(QLineEdit("1.1"), 1, 1)
        gridLayout.addWidget(QLineEdit("1.2"), 1, 2)
        gridLayout.addWidget(QLineEdit("1.3"), 1, 3)

        gridLayout.addWidget(scaleLabel, 2, 0)
        gridLayout.addWidget(QLineEdit("2.1"), 2, 1)
        gridLayout.addWidget(QLineEdit("2.2"), 2, 2)
        gridLayout.addWidget(QLineEdit("2.3"), 2, 3)

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


def main():
    app = QApplication.instance()
    ex = Example()
    ex.show()
    sys.exit()
    app.exec_()


main()

20210824_05

translateLabel = QLabel("Translate")
rotateLabel = QLabel("Rotate")
scaleLabel = QLabel("Scale")

gridLayout = QGridLayout()

QLabelとQGridLayoutを作成しています

# 0行目
gridLayout.addWidget(translateLabel, 0, 0)
gridLayout.addWidget(QLineEdit("0.1"), 0, 1)
gridLayout.addWidget(QLineEdit("0.2"), 0, 2)
gridLayout.addWidget(QLineEdit("0.3"), 0, 3)

# 1行目
gridLayout.addWidget(rotateLabel, 1, 0)
gridLayout.addWidget(QLineEdit("1.1"), 1, 1)
gridLayout.addWidget(QLineEdit("1.2"), 1, 2)
gridLayout.addWidget(QLineEdit("1.3"), 1, 3)

# 2行目
gridLayout.addWidget(scaleLabel, 2, 0)
gridLayout.addWidget(QLineEdit("2.1"), 2, 1)
gridLayout.addWidget(QLineEdit("2.2"), 2, 2)
gridLayout.addWidget(QLineEdit("2.3"), 2, 3)

addWidget()メソッドでウィジェットを追加できるのですがQBoxLayout系のレイアウトクラスと違い、引数に行数・列数を指定する必要があります※2

※2
QGridLayoutの場合は行数と列数の指定はQBoxLayout任意でstretchの指定ができます
QGridLayout::addWidget(QWidget widget, int row, int column, Qt::Alignment alignment = Qt::Alignment())
QBoxLayout::addWidget(QWidget
widget, int stretch = 0, Qt::Alignment alignment = Qt::Alignment())

サンプルでは一つ一つ位置を指定していましたがfor文などを使えばコードをすっきりとさせることができます

import sys

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *


class Example(QMainWindow):
    def __init__(self, parent=None, *args, **kwargs):
        super(Example, self).__init__(parent, *args, **kwargs)
        self.initUI()

    def initUI(self):
        self.setGeometry(500, 300, 400, 270)
        self.setWindowTitle("Grid Layout")

        labels_text = ["Translate", "Rotate", "Scale"]
        labels = [QLabel(text) for text in labels_text]

        gridLayout = QGridLayout()
        for count, label in enumerate(labels):
            gridLayout.addWidget(labels[count], count, 0)
            for column in range(1, 4):
                gridLayout.addWidget(QLineEdit("%s.%s" % (count, column)), count, column)

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


def main():
    app = QApplication.instance()
    ex = Example()
    ex.show()
    sys.exit()
    app.exec_()


main()

上にコードではストレッチがかかってしまうためストレッチを効かしたくない場合はsetSizeConstraintQLayout.SetMinAndMaxSizeQLayout.SetFixedSizeを設定するとストレッチを止めることができます
20210825_01

Maya PySide2 / PySide チュートリアルのこのパートではレイアウトを扱いました