Qt for Pythonでメニューからレイアウトの切り替え

[「Qt for Pythonで終了するだけのアプリケーションを作成」](https://qiita.com/kubotama/items/f83e569b361d4601b0e2)で作成したアプリケーションにメニューを追加して、ラベル([QLabel](https://doc.qt.io/qtforpython/PySide2/QtWidgets/QLabel.html))の配置と、レイアウトの変更をできるようにしました。


# ソースコード

以下はサンプルのプログラムです。最初に4つのラベルを縦に並べて、メニューからの選択で横並びと縦並びに変更します。横並びに表示されているときは「横並び」項目が、縦並びに表示されているときは「縦並び」項目を無効にして選択できなくします。

```python
from PySide2.QtWidgets import QWidget
from PySide2.QtWidgets import QApplication
from PySide2.QtWidgets import QMainWindow
from PySide2.QtWidgets import QMenu
from PySide2.QtWidgets import QAction
from PySide2.QtWidgets import QLabel
from PySide2.QtWidgets import QVBoxLayout
from PySide2.QtWidgets import QHBoxLayout
from PySide2.QtWidgets import QBoxLayout
from PySide2.QtGui import QKeySequence
from PySide2.QtCore import Qt

class MainWindow(QMainWindow):
    def __init__(self, app: QApplication):
        super(MainWindow, self).__init__()
        self.app = app
        self.setWindowTitle("サンプルプログラム")
        self.resize(400,200)

    def main(self) -> None:
        file_item_list = {
            'exit': ('終了(&x)', self.exit, QKeySequence('Ctrl+Q')),
        }
        file_menu, self.file_action_list = self.create_menu('ファイル(&f)', file_item_list)
        self.menuBar().addMenu(file_menu)

        layout_item_list = {
            'vertical': ('縦並び(&v)', self.vertical, QKeySequence('Shift+Ctrl+V')),
            'horizontal': ('横並び(&h)', self.horizontal, QKeySequence('Shift+Ctrl+H')),
        }
        layout_menu, self.layout_action_list = self.create_menu('アクション(&a)', layout_item_list)
        self.menuBar().addMenu(layout_menu)

        self.lable_list = []
        for i in range(1, 5):
            label = QLabel(str(i))
            label.setAlignment(Qt.AlignCenter)
            self.lable_list.append(label)

        self.vertical()

        self.show()
        self.app.exec_()

    def exit(self) -> None:
        self.close()

    def set_layout(self, layout: QBoxLayout) -> None:
        widget = QWidget()
        for label in self.lable_list:
            layout.addWidget(label)
        widget.setLayout(layout)
        self.setCentralWidget(widget)

    def horizontal(self) -> None:
        self.layout_action_list['horizontal'].setDisabled(True)
        self.layout_action_list['vertical'].setEnabled(True)
        self.set_layout(QHBoxLayout())

    def vertical(self) -> None:
        self.layout_action_list['vertical'].setDisabled(True)
        self.layout_action_list['horizontal'].setEnabled(True)
        self.set_layout(QVBoxLayout())

    def create_menu(self, menu_name: str, item_list: dict) -> (QMenu, dict):
        action_list = {}
        menu = QMenu(menu_name)
        for key, (item_name, function, shortcut) in item_list.items():
            action = QAction(item_name, self)
            action.triggered.connect(function)
            if not shortcut is None:
                action.setShortcut(shortcut)
            menu.addAction(action)
            action_list[key] = action
        return menu, action_list

def main() -> None:
    app = QApplication()
    window = MainWindow(app)
    window.main()

if __name__ == '__main__':
    main()
```

少し長くなりましたが、追加したのは

- ラベルを作成してレイアウトで配置
- 「アクション」メニューと呼び出されるメソッド

です。簡単に説明します。

## MainWindowクラスの初期化

```python
    def __init__(self, app: QApplication):
        super(MainWindow, self).__init__()
        self.app = app
        self.setWindowTitle("サンプルプログラム")
        self.resize(400,200)
```
QMainWindowクラスのサブクラスとしてMainWindowクラスを作成します。main関数ではMainWindowクラスのインスタンスを作成して、mainメソッドを呼び出します。ここではMainWindowクラスの初期化としてタイトルやサイズを設定します。

## メニューの作成

```python
        file_item_list = {
            'exit': ('終了(&x)', self.exit, QKeySequence('Ctrl+Q')),
        }
        file_menu, self.file_action_list = self.create_menu('ファイル(&f)', file_item_list)
        self.menuBar().addMenu(file_menu)

        layout_item_list = {
            'vertical': ('縦並び(&v)', self.vertical, QKeySequence('Shift+Ctrl+V')),
            'horizontal': ('横並び(&h)', self.horizontal, QKeySequence('Shift+Ctrl+H')),
        }
        layout_menu, self.layout_action_list = self.create_menu('アクション(&a)', layout_item_list)
        self.menuBar().addMenu(layout_menu)
```
「終了」項目のある「ファイル」メニュー、「縦並び」「横並び」項目のある「アクション」メニューを作成しています。項目が選択されたときに呼び出されるメソッド、ショートカットキーもあわせて設定します。

## ラベルの作成

```python
        self.lable_list = []
        for i in range(1, 5):
            label = QLabel(str(i))
            label.setAlignment(Qt.AlignCenter)
            self.lable_list.append(label)
```

ラベルを作成して配列に代入しています。ラベルのテキストは1から4まで、縦と横ともにセンタリングします。

## レイアウトの設定

```python
    def set_layout(self, layout: QBoxLayout) -> None:
        widget = QWidget()
        for label in self.lable_list:
            layout.addWidget(label)
        widget.setLayout(layout)
        self.setCentralWidget(widget)

    def horizontal(self) -> None:
        self.layout_action_list['horizontal'].setDisabled(True)
        self.layout_action_list['vertical'].setEnabled(True)
        self.set_layout(QHBoxLayout())

    def vertical(self) -> None:
        self.layout_action_list['vertical'].setDisabled(True)
        self.layout_action_list['horizontal'].setEnabled(True)
        self.set_layout(QVBoxLayout())
```

horizontal, verticalメソッドは、選択された項目を無効にして、もう一方の項目を有効にします。それから、それぞれQHBoxLayout, QVBoxLayoutのインスタンスを引数にしてset_layoutメソッドを呼び出します。set_layoutメソッドでは渡されたインスタンスに、あらかじめ作成して配列に保存されているラベルをレイアウトします。

# レイアウトの上書きは×

上のプログラムではset_layoutが呼び出されるたびにQWidgetを作成して、作成したQWidgetにレイアウトを設定してcentral areaに割り当てています。これを事前に作成してcentral areaに割り当てたQWidgetにレイアウトを設定しても、表示には反映されませんでした。もしかしたら、QWidgetからなんらかのメソッドを呼び出せば反映するのかもしれませんが、見つかりませんでした。

# レイアウトの再利用も×

上のプログラムではset_layoutを呼び出すたびに、QHBoxLayout, QVBoxLayoutのインスタンスを作成しています。これも、事前にインスタンスを作成しておいて、レイアウトを切り替えるたびに再利用しようとすると、エラーが発生します。

具体的には、最初に縦並びにしてから、いったん横並びに変えた後に、もう一度縦並びにするときに、前に作成しておいたレイアウトのインスタンスを利用すると以下のエラーが発生しました。

```console
Traceback (most recent call last):
  File "main.py", line 67, in vertical
    self.set_layout(self.vbox_layout)
  File "main.py", line 52, in set_layout
    layout.addWidget(label)
RuntimeError: Internal C++ object (PySide2.QtWidgets.QVBoxLayout) already delete
```

どうやら一度割り当てられたレイアウトのインスタンスが、別のレイアウトのインスタンスに置き換えられるときに自動的に削除されているようです。このため、レイアウトを切り替えるたびにインスタンスを作成しています。

間違いや補足などがあれば、コメントをいただけると有難いです。