Python(PySide + PyQtGraph)による「おれおれグラフ生成アプリケーション」その2

その2 - PySideによるGUIプログラミング

#PySideのGUI作成方法
PySideのGUI作成方法には2種類の方法があります。「①直打ちする方法」と、「②QtDesignerを利用する方法」です。前者はプログラムコード内に直接記述し、後者はツールを使います。個人的に①の方がGUI作成にあたってのPySideの理解が進むと思うので、今回はこちらを採用します。

PySideではざっくり言うと、下のようにレイアウトとウィジェット(GUI部品)をそれぞれ入れ子構造にしていくことでGUIを作成します。
レイアウトに関して主なものを挙げると、孫レイアウトのように要素を水平方向に並べるもの、子レイアウトのように要素を鉛直方向に並べるもの、グリッド上(格子状)に要素を並べるものがあります。

```
┏ウィンドウ━━━━━━━━━━━━━━━━━━━┓
┃　　　　　　　　　　　　　　　　　　　　　　　　┃
┃┏親レイアウト━━━━━━━━━━━━━━━━┓┃
┃┃┏子レイアウト━━━━━━━━━━━┓┏━┓┃┃
┃┃┃┏孫レイアウト━━━━━━━━━┓┃┃　┃┃┃
┃┃┃┃┏━━━━━━┳━━━━━━┓┃┃┃ウ┃┃┃
┃┃┃┃┃ウィジェット┃ウィジェット┃┃┃┃ィ┃┃┃
┃┃┃┃┗━━━━━━┻━━━━━━┛┃┃┃ジ┃┃┃
┃┃┃┗━━━━━━━━━━━━━━━┛┃┃ェ┃┃┃
┃┃┃┏━━━━━━━━━━━━━━━┓┃┃ッ┃┃┃
┃┃┃┃　　　　 ウィジェット 　　　　┃┃┃ト┃┃┃
┃┃┃┗━━━━━━━━━━━━━━━┛┃┃　┃┃┃
┃┃┗━━━━━━━━━━━━━━━━━┛┗━┛┃┃
┃┗━━━━━━━━━━━━━━━━━━━━━━┛┃
┗━━━━━━━━━━━━━━━━━━━━━━━━┛
```

今回GUIを作成する中では、水平レイアウトと鉛直レイアウトおよび少し特殊なレイアウトを使っていきます。また、グリッドレイアウト(のようなもの)はPyQtGraphを扱う中で使うことになります。

なお、実際にGUIをプログラム内で記述していく際には、中身から作成していくのが基本です。つまり、

1. ウィジェットの生成と初期設定
2. レイアウトの生成
3. 内側から順にウィジェット・レイアウトを上階層のレイアウトに格納
4. 親レイアウトをウィンドウのメインレイアウトとして設定

という流れを組んだコードを書いていくことになります。

#PySideによるGUIプログラミング

さっそくGUIを作成していきましょう。
以下、上から順にコードを記載し説明していきます。前回の[4]\_\_init\_\_()の中に順次書き足していってください。関数全文は一番最後に載せておきます。

```py3
# [4]
    # MainWindowクラスの初期化(GUIの生成、シグナルスロット接続)
    def __init__(self, parent = None):
        super(MainWindow, self).__init__(parent)
        # [4-1] ウィンドウのタイトルを設定する
        self.setWindowTitle('おれおれグラフ生成アプリケーション')
        
```

GUIプログラミングにおいては、アプリケーション名を決めるという作業は非常に神聖な儀式です。**[4-1]**のようにsetWindowTitle()関数を用いれば、自分のアプリケーションに命を吹き込むことが出来ます。
この時点で起動すると中身は相変わらずまっさらですが、タイトルバーとタスクバーに自分の命名した名前が表示されるようになりました。味気なさが少しなくなったように感じませんか？……そうですか。

![ウィンドウタイトルの表示](https://qiita-image-store.s3.amazonaws.com/0/55744/dd6ee330-4d5f-2852-3a70-c27abfac8e99.png)


ちなみに、self.setWindowIcon()関数でアイコンを設定することも可能です。が、用意するのが面倒なのでここでは割愛します。

***
```py3
        # [4-2] 係数を設定する
        self.keisu1 = 5.
        self.keisu2 = 10.
        
        # [4-3] GUI部品を作成する
        self.keisu1Label = QLabel('係数1')
        self.keisu1Edit = QLineEdit(str(self.keisu1))
        self.keisu2Label = QLabel('係数2')
        self.keisu2Edit = QLineEdit(str(self.keisu2))
        self.folderPathLabel = QLabel("フォルダのパス:")
        self.folderPathEdit = QLineEdit(".\\")
        self.fileNameListWidget = QListWidget()
        self.button = QPushButton('画像出力')
        
        # [4-4] GUI部品を設定する
        self.fileNameListWidget.setSelectionMode(QAbstractItemView.ExtendedSelection)
        
```

続いて**[4-2]**でデータを整形する際に使うfloat型の係数を2つ用意します。
**[4-3]**で今回のアプリケーションに必要なGUI部品であるQLabel(ラベル)、QLineEdit(一行テキストボックス)、QListWidget(リストウィジェット)、QPushButton(ボタン)の生成しています。QLabel、QLineEdit、QPushButtonは引数に文字列を置くことで、生成と同時にウィジェット内部にその文字列を表示させることが出来ます。
folderPathEditは、後ほどファイル一覧を表示するために、ディレクトリーを入力するテキストボックスとして生成しています。ここではとりあえず、このプログラムファイル(graphApp.py)があるフォルダを相対パスで指定することとします。実験結果のデータファイルのあるフォルダが決まっているときはそこを指定しておくと楽ですし、設定ファイルを作れば前回開いていたディレクトリを引き継ぐことも可能です。
**[4-4]**では、QListWidgetであるfileNameListWidgetが中身を複数選択できるように設定し直しています。この辺り、各GUI部品の様々な設定項目や機能については[PySideのリファレンス](http://pyside.github.io/docs/pyside/)をご参照ください。
GUI部品を生成したところでプログラムを起動してみても、ウィンドウの中身はまっさらなままです。レイアウトの生成・設定をまだ行っていないからです。

***
```py3
        # [4-5] PyQtGraphのグラフ描写領域を作成する
        self.lw = pg.LayoutWidget()
        self.lw.setMinimumSize(800, 600)
        self.lw.setMaximumSize(800, 600)
        
```

**[4-5]**にてPyQtGraphのウィジェットであるLayoutWidgetを生成しています。前回述べたように、PyQtGraphはPySide(Qt)を派生させたグラフ描写に特化したGUIライブラリーです。LayoutWidgetも実は継承元はQWidgetというウィジェットなので、PySideのレイアウトに設置することが出来ます。詳細は次々回に説明することとします。
ウィジェットを増やしただけなので、相変わらず起動してもウィンドウに変化はありません。次にレイアウトを作成していきましょう。

***
```py3
        # [4-6] レイアウトを作成しウィジェットを追加する
        keisuLayout = QFormLayout()
        keisuLayout.addRow(self.keisu1Label, self.keisu1Edit)
        keisuLayout.addRow(self.keisu2Label, self.keisu2Edit)
        folderLayout = QFormLayout()
        folderLayout.addRow(self.folderPathLabel, self.folderPathEdit)
        
        leftLayout = QVBoxLayout()
        leftLayout.addLayout(keisuLayout)
        leftLayout.addLayout(folderLayout)
        leftLayout.addWidget(self.fileNameListWidget)
        leftLayout.addWidget(self.button)
        
        layout = QHBoxLayout()
        layout.addLayout(leftLayout)
        layout.addWidget(self.lw)
        
```

今回のアプリケーションに使用するレイアウトは、**[4-6]**の通りQFormLayout、QVBoxLayout、QHBoxLayoutの3つです。
QFormLayoutはフォーム画面にありがちな左にラベル、右にテキストボックスやラジオボタンという構図を簡単に作るためのレイアウトです。addRow()関数を用いて、ラベルと各種ウィジェットを1セットとしてレイアウト内に追加します。
対してQVBoxLayout、QHBoxLayoutは鉛直(Vertical)、水平(Horizontal)方向に要素を並べられるレイアウトです。addWidget()関数の他、addLayout()関数を用いることで、入れ子構造を持ったより複雑なGUIを作成することが出来ます。
なお、ここでプログラムを起動すると……相変わらず何も変りません。最後に「親レイアウト(layout)をウィンドウのメインレイアウトに設定」する必要があります。

***
```py3
        # [4-7] ウィンドウのレイアウトを設定する
        self.setLayout(layout)
        
```

**[4-7]**のsetLayout()関数により、このアプリケーションのウィンドウのメインレイアウトにlayoutを設定します。これでようやくウィンドウの中に下のようなGUIを表示できるようになります。

![その2成果](https://qiita-image-store.s3.amazonaws.com/0/55744/2c97c605-6345-5746-ebce-33fe6fb0c2e6.png)


どうでしょうか。これでこのまとめで開発していくグラフ生成アプリケーションの基本形が完成しました。

1. ウィジェットの生成と初期設定
2. レイアウトの生成
3. 内側から順にウィジェット・レイアウトを上階層のレイアウトに格納
4. 親レイアウトをウィンドウのメインレイアウトとして設定

の手順を踏むことで、意外なほど簡単にそれっぽいGUIを持ったアプリケーションを作れてしまったのではないでしょうか。各レイアウトはaddする順番で上から(左から)要素が並んでいきます。順番を変えたり、他のGUI部品を追加したりしながら、ぜひ自分なりの「おれおれ」GUIを見つけてください。

と、GUIが完成したところではありますが、もう少しだけ続きます。

***
```py3
        # [4-8] シグナルとスロットを接続する
        self.folderPathEdit.textEdited.connect(lambda: self.updateFileNameListWidget(self.fileNameListWidget, self.folderPathEdit.text()))
        self.fileNameListWidget.itemSelectionChanged.connect(lambda: self.createGraph(self.folderPathEdit.text(), self.fileNameListWidget.selectedItems()[0].text()))
        
        # [4-9] fileNameListWidgetを更新する
        self.updateFileNameListWidget(self.fileNameListWidget, self.folderPathEdit.text())
```

シグナルスロットの話は次節で述べますので**[4-8]**の話は一旦置いておきます。
最後に、そのままだと空なfileNameListWidgetに**[4-9]**updateFileNameListWidget()関数を使って、起動と同時に規定のフォルダのファイル一覧を表示することとします。ただ、updateFileNameListWidget()関数の処理部は未実装ですので、現時点では起動と同時に`updateFileNameListWidget`とコマンドプロンプト(ターミナル/端末)に出力されるだけです。

#シグナルとスロット

PySideにおける初期化関数[4]\_\_init\_\_()は、GUIの作成と共に、**[4-8]**シグナルとスロットの接続を大きな役割としています。

手続き型のCUIプログラムのような、上から順に「名前と身長と体重を入力するとBMIを出力する」といった処理を行えばいいものとは異なり、GUIアプリケーションでは任意のタイミング、任意の順に操作されるボタンやテキストボックスに対応した処理を行う必要があります。
この所謂イベント駆動型なプログラミングを行うために、PySide(Qt)では上記のシグナルとスロットという仕組みが用意されています。

**[4-8]**に話を戻して即して説明を行います。

```py3
        # [4-8] シグナルとスロットを接続する
        self.folderPathEdit.textEdited.connect(lambda: self.updateFileNameListWidget(self.fileNameListWidget, self.folderPathEdit.text()))
        self.fileNameListWidget.itemSelectionChanged.connect(lambda: self.createGraph(self.folderPathEdit.text(), self.fileNameListWidget.selectedItems()[0].text()))
        
```

上のコードではシグナルスロットの仕組みを利用し、以下の二つの動作を実現しています。

1. テキストボックスが編集される → リストウィジェットのファイル一覧を更新する
2. リストウィジェット内のアイテムが選択される → グラフ描写する

「シグナル・スロット・接続」と、2行のコードのうち「上のコード」と、「1. の文」はそれぞれ、

*シグナル* : `self.folderPathEdit.textEdited` : テキストボックスが編集される
*接続* : `connect` : →
*スロット* : `self.updateFileNameListWidget()`: リストウィジェットのファイル一覧を更新する

が対応しています。
コードの意味としては、テキストボックスが編集された(textEdited)というシグナルと、リストウィジェットを更新する(updateFileNameListWidget())関数をスロットにして、両者を結びつけて(connect()関数)いるわけです。

シグナルスロット接続は、例えるならばリモコンとテレビでしょうか。
ボタンが押されるとリモコンから発信される固有パターンの信号と、それに対応したテレビの機能(チャンネル変えたり音量上げたり)を、あらかじめ結びつけているといったところです。

実際にシグナルスロット接続が出来ているか確認するためには、このアプリケーションを起動して、GUI上のfolderPathEditの中身を編集すればわかります。おそらく、入力したり消去したりする毎に`updateFileNameListWidget`とコマンドプロンプト(ターミナル/端末)に出力されるはずです。

シグナルにはこの他、ボタンがクリックされた(QButton.clicked)や、文章が選択された(QPlainTextEdit.selectionChanged)など、ウィジェット毎に様々なものが用意されていますし、自分で作成することも出来ます。
なお、`lambda:`とあるのはラムダ式というものを使ってるということなのですが、とりあえず覚えとくととても幸せになれます(ラムダ式を使うとスロットの関数に対して好き勝手に引数を与えられるようになるのです)。

#次回予告

以上がPySideによるGUIプログラミングの話でした。
次回は、今回処理部が未実装だったGUI上のリストウィジェットにファイル一覧を表示する関数を実装し、更にファイルを読み込んでグラフ描写に使うデータを生成するところまでを行います。まぁ正直、人によっては解説必要な話でもないかと思うので、そのような場合は飛ばしてくださっても構いません。

その1 - 導入とプログラム全体像
*その2 - PySideによるGUIプログラミング* … 今回
**その3 - ファイル一覧の表示とデータリスト一式の生成** … 次回
その4 - PyQtGraphによるグラフ生成1（基本編）
その5 - PyQtGraphによるグラフ生成2（多軸グラフ編）
その6 - PyQtGraphによるグラフ生成3（体裁を整える編）
以降未定……

#QtDesignerを利用したGUI作成について
最後に『PySideのGUI作成方法』で挙げたGUI作成方法「②QtDesignerを利用する方法」について、簡単に説明だけしておきます。
QtDesignerはPySideインストール時に同時にダウンロードさるGUIをGUI上で作成できるQtのツールです。「(Pythonインストール先)\Lib\site-packages\PySide」内に「designer.exe」等の名前で入っているはずなので起動して触ってみてください。今回、グラフ生成アプリケーションを作るために使ったGUI部品の他にも、非常にバラエティに富んだGUI部品が用意されていることがわかると思います。QtDesignerはD&Dでレイアウトやウィジェットを配置することができるので、簡単にGUIを作れて楽しいです。
実際にはQtDesignerが生成する.uiファイルを、プログラム本体内でQtUiTools.QUiLoaderで読み込んで使うといった流れになります。

#[4]\_\_init\_\_()関数 全文

```py3
# [4]
    # MainWindowクラスの初期化(GUIの生成、シグナルスロット接続)
    def __init__(self, parent = None):
        super(MainWindow, self).__init__(parent)
        # [4-1] ウィンドウのタイトルを設定する
        self.setWindowTitle('おれおれグラフ生成アプリケーション')
        
        # [4-2] 係数を設定する
        self.keisu1 = 5.
        self.keisu2 = 10.
        
        # [4-3] GUI部品を作成する
        self.keisu1Label = QLabel('係数1')
        self.keisu1Edit = QLineEdit(str(self.keisu1))
        self.keisu2Label = QLabel('係数2')
        self.keisu2Edit = QLineEdit(str(self.keisu2))
        self.folderPathLabel = QLabel("フォルダのパス:")
        self.folderPathEdit = QLineEdit(".\\")
        self.fileNameListWidget = QListWidget()
        self.button = QPushButton('画像出力')
        
        # [4-4] GUI部品を設定する
        self.fileNameListWidget.setSelectionMode(QAbstractItemView.ExtendedSelection)
        
        # [4-5] PyQtGraphのグラフ描写領域を作成する
        self.lw = pg.LayoutWidget()
        self.lw.setMinimumSize(800, 600)
        self.lw.setMaximumSize(800, 600)
        
        # [4-6] レイアウトを作成しウィジェットを追加する
        keisuLayout = QFormLayout()
        keisuLayout.addRow(self.keisu1Label, self.keisu1Edit)
        keisuLayout.addRow(self.keisu2Label, self.keisu2Edit)
        folderLayout = QFormLayout()
        folderLayout.addRow(self.folderPathLabel, self.folderPathEdit)
        
        leftLayout = QVBoxLayout()
        leftLayout.addLayout(keisuLayout)
        leftLayout.addLayout(folderLayout)
        leftLayout.addWidget(self.fileNameListWidget)
        leftLayout.addWidget(self.button)
        
        layout = QHBoxLayout()
        layout.addLayout(leftLayout)
        layout.addWidget(self.lw)
        
        # [4-7] ウィンドウのレイアウトを設定する
        self.setLayout(layout)
        
        # [4-8] シグナルとスロットを接続する
        self.folderPathEdit.textEdited.connect(lambda: self.updateFileNameListWidget(self.fileNameListWidget, self.folderPathEdit.text()))
        self.fileNameListWidget.itemSelectionChanged.connect(lambda: self.createGraph(self.folderPathEdit.text(), self.fileNameListWidget.selectedItems()[0].text()))
        
        # [4-9] fileNameListWidgetを更新する
        self.updateFileNameListWidget(self.fileNameListWidget, self.folderPathEdit.text())