Houdiniで始めるGUIツール作成【PySide2】

<b>Houdini Advent Calendar 2020 8日目の記事です。</b>

突然ですが、近年パイプラインが熱い！....と思っています。

私の身の回りに限った話ですが、TA育成ブームやHoudiniとUE4の蜜月が話題となっている今、
パイプライン周りに一枚噛めるスキルがあると飯の種になると思います。
そして、スキルのアピールに一役買ってくれるのがGUIツールです。

私自身も、ここ2,3年「GUIツールが作りたいなー。プログラマに依頼するのも気が引けるし、自分で作れるようになりたいなー。誰か教えてくれないかなー」と耳元で囁かれる機会が増え、せっかくなら記事にしようと思った次第です。


当記事が皆さんのGUIツール制作に踏み出す、第一歩目の手助けとなることを願っています。


####対象者
Pythonの勉強を始めGUI付きのツールに興味を持つも、
PySide2が難しそうで、いまいち手が伸びていないアナタ。


####目標
サンプルコード（後述）を改造して、Houdini上で簡単なGUIツールが作れるようになる。
未知の領域（PySide2）への苦手意識をなくす。


##1. ダイアログの作成

簡素なダイアログを表示するだけのコードです。
実際にご自身の環境で試してみてください。


```python
from PySide2 import QtWidgets, QtCore

# ダイアログの作成
dialog = QtWidgets.QWidget()

# 作ったダイアログとHoudiniウィンドウの間に親子関係をつくる。
dialog.setParent(hou.qt.mainWindow(), QtCore.Qt.Window)

# ダイアログの表示
dialog.show()
```

<img width="500" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/f3266d0a-a9da-07b3-2abb-cfc8954df29a.png">


####解説 

PySide2ではUIを構成するパーツのことをWidgetと呼びます。
QWidgetはシンプルなWidgetで、様々なWidgetのベースになっています。
今回のコードはQWidgetを作り、そのまま表示させるものです。
<br>

<b>▼ from PySide2 import QtWidgets, QtCore</b>
　PySide2には様々なモジュールが存在しており、QtWidgetsやQtCoreもその1つです。
　どのモジュールに何が入っているのかは、使っていくうちに覚えていきます。
　私がツール制作で主に使うのは <b>QtWidgets, QtCore, QtGui</b> の3つです。
　
　興味のある方は[ドキュメントページ](https://doc.qt.io/qtforpython/modules.html)を見てください。
　


<b>▼ 親子関係</b>
　ウィンドウ間に主従関係を作ります。
　Houdiniウィンドウを親、作ったダイアログを子に設定することで、
　<b>ダイアログが勝手に閉じたり、Houdiniウィンドウの後ろに隠れたりしなくなります。</b>
　また、<b>見た目もHoudiniのスタイルに合わせたものに変化します。</b>
　


<b>▼ dialog.setParent(親ウィジェット, ウィンドウのタイプ)</b>
　ダイアログの親を設定します。
　

<b>▼ hou.qt.mainWindow()</b>
　Houdiniウィンドウを返します（Houdini上でのみ使用可能です）
　https://www.sidefx.com/docs/houdini/hom/hou/qt/mainWindow.html
　

<b>▼ QtCore.Qt.Window</b>
　ウィンドウのタイプです。
　その他のタイプは[こちら](https://doc.qt.io/qtforpython/PySide2/QtCore/Qt.html?highlight=windowflags)
　

なんとなく把握出来たら、次に進んでみましょう。
　
##2. ボタンの配置

ダイアログが表示出来たので、
次は、作ったダイアログにボタンを配置してみます。

```python

from PySide2 import QtWidgets, QtCore

# ボタンが押されたときに実行される関数
def p_something():
    print "ボタンが押されました"

dialog = QtWidgets.QWidget()
dialog.setParent(hou.qt.mainWindow(), QtCore.Qt.Window)

# ボタンの作成
button = QtWidgets.QPushButton("Click")

# ボタンがクリックされたときに実行したい関数を登録する
button.clicked.connect(p_something)

# 縦並びレイアウトの作成
v_layout = QtWidgets.QVBoxLayout()
# レイアウトにボタンを追加する
v_layout.addWidget(button)

# ダイアログにレイアウトを設定する
dialog.setLayout(v_layout)

dialog.show()
```
<img width="600" alt="button.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/ef5cbd0e-18f9-776b-2773-add913c235b9.gif">

####解説 
今回のコードでは前回のコードに加えて、
ボタン・レイアウトの追加と関数の登録を行っています。
　

<b>▼ QPushButton</b>
　・ボタン形式のWidgetです。[QWidgetから派生して作られています。](https://doc.qt.io/qtforpython/PySide2/QtWidgets/QPushButton.html)
　・引数に文字列を渡すことでボタンに名前をつけることも出来ます。
　

<b>▼ QVBoxLayout</b>
　・複数のWidgetをいい感じにレイアウトしてくれます。
　・<b>QVBoxLayoutは縦方向</b>に、<b>QHBoxLayoutは横方向</b>にレイアウトするようになっています。
　・addWidget(任意のWidget)でQVBoxLayout内にWidgetをどんどん配置することが出来ます。
　・使用する際は、setLayout()でQVBoxLayoutをWidgetに登録する必要があります。
　
　
<b>▼ button.clicked.connect(p_something)</b>
　PySide2には<b>シグナル</b>と<b>スロット</b>と呼ばれる仕組みがあります。

　<b>▽ シグナル</b>
　　Widgetに起こったイベントに対して、通知を飛ばすための機能です。
　　<b>どんなイベントに対して、通知を飛ばすのかはWidget毎に変わります。</b>
　　[ドキュメントページ](https://doc.qt.io/qtforpython/PySide2/QtWidgets/QAbstractButton.html)のSignals欄を見ると対応しているイベント内容がわかります。

　<b>▽ スロット</b>
　　通知が飛んだときに、任意のアクションを実行するための機能です。
　　partialを使うことで実行する関数に引数を渡すことも可能です。（後述）

　今回はボタンがクリックされたというシグナルに対して、
　p_somethingを実行するようにスロットを設定しています。

　シグナルとスロットの設定は
　<b>任意のWidget.シグナル.connect(任意のアクション)</b>
　で行います。
 

##3. テキストエディタの追加

今度はテキストエディタを追加し、文字入力を出来るようにします。

```python
from PySide2 import QtWidgets, QtCore
from functools import partial

def p_something(t_editor):
    print "ボタンが押されました"
    print t_editor.text().encode("utf_8")

    
dialog = QtWidgets.QWidget()
dialog.setParent(hou.qt.mainWindow(), QtCore.Qt.Window)

button = QtWidgets.QPushButton("Click")
# テキストエディタの作成
text_editor = QtWidgets.QLineEdit()
# ボタンを押したときに関数の引数としてtext_editorを渡す
button.clicked.connect(partial(p_something, text_editor))

v_layout = QtWidgets.QVBoxLayout()
# 縦並びレイアウトにテキストエディタを追加する
v_layout.addWidget(text_editor)
v_layout.addWidget(button)

dialog.setLayout(v_layout)
dialog.show()

```

<img width="600" alt="line_edit.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/0ff4eb89-2f89-deec-0da9-138ffded0af5.gif">

####解説 
前回までに作ったダイアログにテキストエディタを追加し、
partialを使いスロットに設定した関数に引数を渡せるようにしました。
　

<b>▼ QLineEdit</b>
　・テキストエディタ形式のWidgetです。[QWidgetから派生して作られています。](https://doc.qt.io/qtforpython/PySide2/QtWidgets/QLineEdit.html)
　
<b>▼ button.clicked.connect(partial(p_something, text_editor))</b>
　・スロットの設定を行う際に、<b>partial(関数, 引数)</b>と書くことで、
　　関数に引数を渡すことが出来ます。
　・partialを使うにはfunctoolsモジュールをインポートする必要があります。
　
<b>▼ t_editor.text().encode("utf_8")</b>
　・text()を使うとテキストエディタに入力された文字列を取り出せます。
　・QLineEditが他にどんなメソッドを持っているか知りたい場合は[こちら](https://doc.qt.io/qtforpython/PySide2/QtWidgets/QLineEdit.html)を見て下さい。

　※ .encode("utf_8")はテキストエディタに日本語を入力したときに、
　　 エラーになるのを防ぐ目的で記述しています。
　

##4. チェックボックスの作成

新しくダイアログを作り、チェックボックスを追加します。

```python
from PySide2 import QtWidgets, QtCore
from functools import partial

    
# チェックボックスがON, OFFされたときに実行される
def p_state(index):
    state = ""
    if index == 0:
        state = "OFF"
    if index == 1:
        state = "部分的にON"
    if index == 2:
        state = "ON"
        
    print "チェックボックスの状態は", state, "です"


dialog = QtWidgets.QWidget()
dialog.setParent(hou.qt.mainWindow(), QtCore.Qt.Window)

# グループボックスの作成
group_box = QtWidgets.QGroupBox("Check Box Group")

# チェックボックスの作成
check_box = QtWidgets.QCheckBox("Check01")
# チェックボックスの状態が変化したときにp_state関数を実行する
check_box.stateChanged.connect(p_state)

# グループボックスの中にチェックボックスを配置する用のレイアウト
v_layout = QtWidgets.QVBoxLayout()
v_layout.addWidget(check_box)
group_box.setLayout(v_layout)

# ダイアログの中にグループボックスを配置する用のレイアウト
v_layout2 = QtWidgets.QVBoxLayout()
v_layout2.addWidget(group_box)
dialog.setLayout(v_layout2)

dialog.show()
```

<img width="600" alt="check_box.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/d7130be2-d069-0d4c-8a5f-c573e7f6f054.gif">



####解説 
・ダイアログにチェックボックスを追加しました。
・チェックボックスはグループボックス内に配置されています。
・グループボックスはダイアログに配置されています。
・スロットに設定した関数にチェックボックスの状態が引数として渡されるようにしました。
　

<b>▼ QCheckBox</b>
　・チェックボックス形式のWidgetです。[QWidgetから派生して作られています。](https://doc.qt.io/qtforpython/PySide2/QtWidgets/QCheckBox.html)
　・スロットの設定には「チェックボックスの状態変化を感知する」シグナルを使用しています。
　　check_box.<b>stateChanged</b>.connect(p_state)
　
 
<b>▼ QGroupBox</b>
　・複数のWidgetをグループとしてまとめるのに便利なWidgetです。
　　[QWidgetから派生して作られています。](https://doc.qt.io/qtforpython/PySide2/QtWidgets/QGroupBox.html)
　

<b>▼ def p_state(index):</b>
　・シグナルの種類によっては<b>partialによる引数渡しがなくても、関数に引数が渡されます。</b>
　・stateChangedは、チェック状態を示す0, 1, 2の数字が引数として渡されます。
　　<b>どんな引数が渡されるのかは各シグナルのParametersの部分を見るとわかります。</b>
　

##5. スピンボックスの作成

スピンボックスを作成し、値の変化をラベルに表示します。

```python

from PySide2 import QtWidgets, QtCore
from functools import partial


# スピンボックスの数字が変化したときに実行される
def int_value(value):
    print "p_value:スピンボックスの値は", value, "です 型は", type(value), "です"


# スピンボックスの数字が変化したときに実行される
def unicode_value(q_label, value):
    print "c_value:スピンボックスの値は", value, "です 型は", type(value), "です"
    # ラベルの文字を変更する
    q_label.setText(value)

    
dialog = QtWidgets.QWidget()
dialog.setParent(hou.qt.mainWindow(), QtCore.Qt.Window)

# ラベルの作成
label = QtWidgets.QLabel()

# スピンボックスの作成
spin_box = QtWidgets.QSpinBox()

# シグナルに複数の型がある場合は[型]を使って、任意の方を指定する
spin_box.valueChanged[int].connect(int_value)
# シグナルに複数の型がある場合は[型]を使って、任意の方を指定する
spin_box.valueChanged[unicode].connect(partial(unicode_value, label))

# 横並びレイアウトの作成
h_layout = QtWidgets.QHBoxLayout()
h_layout.addWidget(label)
h_layout.addWidget(spin_box)

dialog.setLayout(h_layout)
dialog.show()

```

<img width="600" alt="spin_box.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/538979f8-6ccd-f786-a06c-d8e20fa49a8a.gif">

####解説 
・ラベルとスピンボックスを作成しました。
・同じシグナルでも、異なる型の引数が渡されるパターンがあることを示しました。
・関数に渡った引数の値が文字の時、値をラベルに設定しました。
　

<b>▼ QLabel</b>
　・文字や画像を表示出来るWidgetです。[QWidgetから派生して作られています。](https://doc.qt.io/qtforpython/PySide2/QtWidgets/QLabel.html)
　・文字を設定するときはsetText()を使用します。
　

<b>▼ QSpinBox</b>
　・数字を入力できるWidgetです。[QWidgetから派生して作られています。](https://doc.qt.io/qtforpython/PySide2/QtWidgets/QSpinBox.html)
　
 
<b>▼ spin_box.valueChanged[int].connect(int_value)</b>
<b>▼ spin_box.valueChanged[unicode].connect(partial(unicode_value, label))</b>
　・スロットの設定には「値の変化を感知する」シグナルを使用しました（valueChanged）
　・「valueChanged」シグナルはスピンボックスの値を<b> 数字 or テキスト </b>で引数に渡します。
　　今回のように異なるパターンで引数に値が渡されるような場合は、
　　<b>シグナル[タイプ]</b>と書くことで引数の型を指定することが出来ます。
　
##6. コンボボックスの作成

コンボボックスを作り、ドロップダウンリストを表示します。

```python
from PySide2 import QtWidgets, QtCore
from functools import partial


def reset_index(c_box):
    # 表示項目を一番上に戻す
    c_box.setCurrentIndex(0)

dialog = QtWidgets.QWidget()
dialog.setParent(hou.qt.mainWindow(), QtCore.Qt.Window)

# コンボボックスの作成
combo_box = QtWidgets.QComboBox()

# コンボボックスに項目を追加
combo_box.addItem("Apple")
combo_box.addItem("Banana")
combo_box.addItem("Coconut")
combo_box.addItem("Durian")

# 表示項目を一番上に戻すボタンを作る
button = QtWidgets.QPushButton("reset")
button.clicked.connect(partial(reset_index, combo_box))

# レイアウトを縦詰めにするスペーサーを作る。
v_spacer = QtWidgets.QSpacerItem(0,0,vData=QtWidgets.QSizePolicy.Expanding)

v_layout = QtWidgets.QVBoxLayout()
v_layout.addWidget(combo_box)
v_layout.addWidget(button)
# スペーサーをレイアウトに追加する。
v_layout.addItem(v_spacer)

# ダイアログのサイズを変更
dialog.resize(400, 80)
dialog.setLayout(v_layout)

dialog.show()
```


<img width="600" alt="combobox.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526231/8016a4f1-0a2b-7a36-9edd-93fe0439f7af.gif">



####解説 
・コンボボックスを作り、4つの項目を用意しました。
・リセットボタンを作り、ボタンを押すことで表示項目が一番上に戻るようにしました。
・Widgetを上に詰めて並べるスペーサーを追加しました。
・ダイアログのサイズを横400px, 縦80pxに設定しました。
　

<b>▼ QComboBox</b>
　・コンボボックス形式のWidgetです。[QWidgetから派生して作られています。](https://doc.qt.io/qtforpython/PySide2/QtWidgets/QComboBox.html)
　・項目を追加するときは addItem("項目名") を使います。　
　・表示する項目は setCurrentIndex(番号) で指定できます。
　

<b>▼ QSpacerItem</b>
　・Widgetを詰めて表示するための つっかえ棒 です。
　　ウィンドウのサイズを変えるような時に威力を発揮します。
　・QSpacerItem(横サイズ,　縦サイズ, 横のサイズポリシー, 縦のサイズポリシー)
　　で予め大きさを指定して作ります。
　　サイズポリシーは、スペーサーの長さの固定や伸縮可能な状態に設定するときに使います。
　　今回のコードでは横方向に伸縮可能な状態にしています。
　　詳しい情報を知りたい方は[こちら](https://doc.qt.io/qtforpython/PySide2/QtWidgets/QSizePolicy.html#PySide2.QtWidgets.PySide2.QtWidgets.QSizePolicy.Policy)を見て下さい。
　
 
##おわり

如何でしたでしょうか。
ほぼ全部PySide2の話になってしまいましたが、逆に言えばHoudini特有の知識が
無くてもHoudiniで動くGUIツールを作れると分かったのではないかと思います。

また、今回紹介したのはPySide2のほんの一部分です。もっと面白い使い方や、
実用的なコード書き方など、まだまだ奥が深いので、是非色々試してみて下さい。

ここまで読んでいただきありがとうございました！！