21
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

HoudiniAdvent Calendar 2018

Day 23

Houdini+ローカルDB〜Qt+SQLiteを使った簡易システム構築〜

Last updated at Posted at 2018-12-22

本記事はHoudini Advent Calendar 2018 の23日目の記事です。

Houdiniでデータベース

なぜHoudiniでデータベース?

こんにちは、堀川です。
最近データ・ドリブンなデザイン手法にとっても興味を持っています。機械学習もはやっていますしね。そしてHoudiniは、データを視覚的に処理するハブとしても使えるなと最近思っているところです。

データを扱う際、安直な考えですがHoudiniで作ったデータをまとめておいたり、外部のデータをまとめておいたりしてHoudiniですぐ使えるようにすることができるような自分で自由に拡張できるデータベースが欲しいなと思いました。そんなこともあり、Houdiniでデータ構造をしっかり定義付けすることができるSQLベースのデータベースを扱えるようにしてみようと考えたわけです。

どのデータベースを使う?

主観ですが、データベースにはその特徴という面から考えたときに大きく二種類に分けられるように思えます。ひとつは、サーバー上にアプリケーションとして立ち上げておくデータベースで、もう一つはローカルにファイルとして保存しておくデータベースです。

前者の有名なところだと、SQLベースのMySQLとかPostgreSQLなどがあり、データ構造の拡張性も高くインターネットを介してアクセスしたりすることができ複数人でのシェアが前提となっています。最近はドキュメントベースのNoSQLなども人気になってきて、Firebaseなどのクラウドベースのデータベースもリアルタイム性があって便利なものが多いです。

一方ローカルのファイルに保存する形のデータベースの場合は、一番簡易なものだとCSVとか、SQLベースのものだとSQLiteなどがあり、拡張性がMySQLなどに比べると劣っていたり、リモートでの複数人とのシェアは不得意といったデメリットがあったりしますが、なんといってもとても手軽に扱えるという点ではささっとデータベースを使いたい場合には便利です。

拡張性、共有性を考えれば前者のDBの方が何かと便利ですが、今回はまずはデータベースを扱う事始めとして、ローカルに保存する形式のデータベースを扱いたいと思います。特に、モバイルアプリなどのローカルDBとしてもよく使われるSQLiteをHoudiniで扱えるようにしてみたいと思います。

データベースを使って何を作る?

個人的にまずはデータベースを扱えるようにするテンプレートがあるといいなと思ったので、次のような要件を自分で設けて、それを満たすようなものを作り、今後データベースをカスタムで作りたいときのテンプレートとなるようなものにしようと思いました。

  • Houdiniのデータをデータベースに保存できるようにする。
  • データベースにSQLiteを使う。
  • HoudiniのShelfからデータベースにアクセスできるようにする。
  • HoudiniのPythonを利用する際、デフォルトのライブラリでなんとかする。
  • データベースを視覚的に扱えるようにPySide2でCRUDを作る。

何かの参考になるかもしれないので、結果から書くのではなくで、これを作るにあたっての自分の試行錯誤をたどるような形で書いていきたいと思います。どういうステップで進んでいったかという情報は、読む人にとっては価値があるものかも?と思ったので。

Houdiniのどんなデータを保存する?

テンプレートを作るにあたって、じゃあどんなHoudiniのデータを保存できるようにしようかとまずは考えました。点群+Attribute?デザインに使うパラメータ一覧?メッシュ自体?

どんなデータでも入れようと思えば入れられるので、迷うところですが、今回はHoudiniのPythonモジュールであるhouの持つ関数で、hou.Node.asCode()というものを利用することで生成されるPythonコードを格納することにしました。

このasCode()を使うことによって、指定したノードネットワークのノードの構成(繋がれ方やパラメータ設定など)をまるまるPythonのコードとして保存してしまい、後からそのネットワークを再現できるようにすることができます。すごい関数です。

Houdiniを使う人なら経験あると思いますが、ノードの構成なんてしょっちゅうアップデートして変わります。この移り変わるノードネットワークの状態を、このasCode()を利用して履歴としてデータベースに保存できるようにしようというのが今回のサンプルの目的になります。

GIT使えば?というツッコミはごもっとも。テンプレとしてサンプルとして作るものなので、そこは目をつむってもらえれば嬉しいです。

tool2.gif

対象読者

  • Houdiniを使ったデータ・ドリブンなデザインを将来やりたい人
  • Houdiniでデータベース(SQLite)を使いたい人
  • データベースの使い道に興味ある人
  • HoudiniのPythonに慣れたい人
  • PySide2でGUIを作ってみたい人
  • GUIとデータベースを連携してCRUD(Create Read Update Delete)を作ってみたい人

SQLiteデータベースを作る

何はともあれ、HoudiniでSQLiteのデータベースを作るところからはじめないといけません。まずはHoudiniのPythonを使って、どうSQLiteを作れるかどうかを調べました。

準備(動作環境)

その前にこの試みは、次のような環境で行っています。ただ、特に外部のツールに依存しているということはないので、比較的どのOSでも、どのバージョンのHoudiniでも(たぶん、、!)使えるのではと思っています。少なくとも同じHoudiniのバージョンのApprenticeでは動きます。

  • macOS Mojave 10.14.1
  • Houdini Indie 17.0.352
  • DB Browser for SQLite -> Houdiniで作ったSQLiteのデータベースを確認するためのデータベース閲覧・編集ソフト

sqlite3モジュール

Pythonに標準で入っているモジュールに、ちょうどSQLiteを操作するものがありました。それがsqlite3です。SQLクエリは自分で書かなければいけない形にはなりますが、比較的簡単に扱えそうだったので、これを使うことにしました。これを使って、まずはデータベースを作り、データを格納できるところまでを試します。

Shelfにスクリプトを登録する

最初に、いつでも書いたスクリプトを起動できるようにHoudiniのShelfにPythonスクリプトを格納します。Houdiniを開いて、まずはスクリプトを格納する新しいシェルフを作ります。シェルフ画面右上の+ボタンから*New Shelf...*を選択して新しく作ります。

Screen Shot 2018-12-22 at 15.44.21.png

ツールバーのファイル名とシェルフ自体の名前に好きな名前を設定します。ファイル名はdefault.shelfではなくて別の名前にしておくと、他の人にシェルフのデータをシェアしやすいかもしれません。

Screen Shot 2018-12-22 at 15.53.12.png

新しいシェルフができたら、そこに新しいスクリプトを追加します。シェルフの空欄のところで右クリックをして、New Tool...を選び、

Screen Shot 2018-12-22 at 15.55.11.png

名前は例えばSaveStateToDBとでもして、データベースにデータを格納スクリプトとして作ります。

Screen Shot 2018-12-22 at 16.01.13.png

sqlite3を使ったデータベース作成のためのスクリプト記述

さっそくPythonを使ってデータベース作成のためのスクリプトを記述をしていくのですが、まずは空のデータベースを作るところから試しはじめました。

次のような形がまずはデータベース作成の基本となります。

import sqlite3 # sqlite3モジュールのインポート
import hou # Houdiniのモジュールのインポート

# database.dbという名前のSQLiteのデータベースファイルが
# HIPファイルと同じ階層に来るようにファイルパスを作る
dbname = hou.houdiniPath()[0] + "/database.db" 

# sqlite3を使ってデータベースを作り、historyというテーブルを
# まだデータベースにない場合に限り新規で作成する
conn = sqlite3.connect(dbname)
c = conn.cursor()
sql_create_table = ''' CREATE TABLE IF NOT EXISTS history (
                id integer PRIMARY KEY,
                name text,
                state_data text,
                created text
            ); '''
c.execute(sql_create_table)
conn.close()

conn = sqlite3.connect(dbname)を呼ぶと、dbnameのパスにSQLiteのデータベースを自動で作成します。もしすでにそのパスにデータベースが存在していればそのデータベースを読み込みます。

その上で、データを格納するためのテーブルを次のように作ります。sqlite3では、SQL文をstringで記述して、それをsqlite3のもつ関数でデータベースにクエリとして飛ばす形式になっています。

c = conn.cursor() # データベースにアクセス
sql_create_table = ''' CREATE TABLE IF NOT EXISTS history (
                id integer PRIMARY KEY,
                name text,
                state_data text,
                created text
            ); ''' # テーブル作成のクエリを作る
c.execute(sql_create_table) # データベースにクエリを飛ばす

conn.close() # データベースを閉じる

今回はノードの構成を履歴として保存することができるようにしたかったので、次のようなデータ構造を考えました。

  • id: データのユニークID(他のデータと被らないユニークな整数)。
  • name: データの名前。コメント的なもの。
  • state_data: ノードの構成情報。hou.Nodeの関数にある、ノードの構成をPythonの生成コードとして保存することができるasCode()の結果を入れることにします。
  • created: データ作成時の日付。

記述スクリプトをツール編集画面のAcceptボタンを押して保存します。

Screen Shot 2018-12-22 at 16.24.31.png

作ったデータベースの確認

早速シェルフ上に作ったスクリプトのツールアイコンをクリックして起動してみます。その前に、開いているHoudiniのファイルをどこかに保存しておく必要があります。

、、、押しました、押しても見た目何も変わっていないように見えます。背後で何かが起こっているんです。HIPファイルが保存されているディレクトリを見に行ってみてください。

Screen Shot 2018-12-22 at 16.30.33.png

スクリプト内で指定したデータベースの名前(database.db)というファイルが作られているのが確認できます。これがSQLiteのデータベースです。

データベース内に作ったテーブルの確認

とりあえず何か作られたのは確認できました。ではこの中身はどうやって確認するかですが、二種類方法があります。一つはテキストエディタで開く、もう一つは専用のSQLiteの閲覧ソフトで中を見るです。テキストエディタで見ても何が起こっているのか把握しずらいので、後者をおすすめします。

個人的に使っているのはDB Browser for SQLiteというもので、オープンソースで無料のSQLite閲覧ツールです。簡単に扱えるのでおすすめです。

このツールを使ってdatabase.dbを開いてみると次のようになっています。

Screen Shot 2018-12-22 at 16.35.38.png

historyというテーブルが存在しているのが確認できました。まずはよかった。

ただ、Browse Dataというタブをクリックすると、当然ながら何もまだデータを入れていないので空っぽです。次はデータを格納するコードを書く必要がありそうです。

Screen Shot 2018-12-22 at 16.36.40.png

データベースにデータを格納する

では実際にデータベースにデータを格納したいと思います。

ノードネットワークを作る

保存の検証をするために、適当なノードネットワークを作ります。例えばこんな感じに。

Screen Shot 2018-12-22 at 22.46.43.png

格納したいデータを準備する

次に格納したいデータをまず準備しましょう。

先ほどつくったhistoryというテーブルで設定したデータ構造は、

  • id
  • name
  • state_data
  • created

という4つの要素からなるものとなります。なので、それぞれに適切なアイテムを用意してあげて、それをひとまとめにして一つのhistoryのデータとしてデータベースに保存する必要があります。

この中のidは、ユニークな整数で、Autoincrementという性質を持っていて、データを追加するたびに自動で1ずつ繰り上がっていくので、特に自分では指定しません。

nameには、データを保存する際の目印となるような名前なりコメントを書く欄とします。ここには、後ほどGUIを用意して、自分でコメントを記述できるようにしたいと思いますが、テスト段階ではダミーなテキストを入れて保存されるか確かめます。

state_dateには現在の全ノードネットワークを再構成するためのPythonコードを保存します。これはhou.Node.asCode()を利用します。

createdには、データを追加した日時を入れます。いつ保存したのか後から見えるようにするためです。

それらを踏まえて、次のようにデータを準備しましょう。

import datetime





### 名前を作る
name = "test"

### ノードネットワーク再構成用のPythonコードを取得する
node = hou.node("/") # 一番上の階層のノードネットワークのパス
code = node.asCode(False, True, False, True, True, True) # ノードネットワークを再構成するためのPythonコードを取得する

### 現在の日時を取得する
created = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") 

データを格納するSQL文を記述する

次に、historyテーブルの中にデータを格納するSQL文を記述します。idは指定せず、name、state_data、createdの三つのアイテムを1セットのデータとしてデータベースに保存します。




sql_insert = ''' INSERT INTO history (name, state_data, created) 
                VALUES (
                    ?,
                    ?,
                    ?
                ); ''' #データ挿入のクエリ文を作る
c.execute(sql_insert, [name, code, created]) # データベースにクエリを送る
conn.commit() # データベースの変更分を保存する
    
conn.close() # データベースの接続を切る

前のコードと組み合わせると次のような形になります。

import sqlite3 # sqlite3モジュールのインポート
import hou # Houdiniのモジュールのインポート
import datetime

# database.dbという名前のSQLiteのデータベースファイルが
# HIPファイルと同じ階層に来るようにファイルパスを作る
dbname = hou.houdiniPath()[0] + "/database.db" 

# sqlite3を使ってデータベースを作り、historyというテーブルを
# まだデータベースにない場合に限り新規で作成する
conn = sqlite3.connect(dbname)
c = conn.cursor()
sql_create_table = ''' CREATE TABLE IF NOT EXISTS history (
                id integer PRIMARY KEY,
                name text,
                state_data text,
                created text
            ); '''
c.execute(sql_create_table)

### 名前を作る
name = "test"

### ノードネットワーク再構成用のPythonコードを取得する
node = hou.node("/") # 一番上の階層のノードネットワークのパス
code = node.asCode(False, True, False, True, True, True) # ノードネットワークを再構成するためのPythonコードを取得する

### 現在の日時を取得する
created = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") 

sql_insert = ''' INSERT INTO history (name, state_data, created) 
                VALUES (
                    ?,
                    ?,
                    ?
                ); ''' #データ挿入のクエリ文を作る
c.execute(sql_insert, [name, code, created]) # データベースにクエリを送る
conn.commit() # データベースの変更分を保存する
    
conn.close() # データベースの接続を切る

この状態で、Acceptボタンを押して、スクリプトを更新します。その上でシェルフのボタンを押して、DB Browser for SQLiteで再度データベースの中身を確認してみると、、、

Screen Shot 2018-12-22 at 20.43.24.png

name欄にtestと入ったデータが一個作られています。成功です。

データベースにあるデータを取り出す

次に、データベースに保存してあるすべてのデータを取得して、そこに保存されているノードネットワーク再構成用のPythonコードを利用して、実際にネットワークを再構成してみましょう。

データをPythonで確認する

コードをPythonで書いてみましょう。まずはデータを追加するShelfのツールとは別個に、もう一個別のツールを作っておきましょう。名前はLoadSaveStateとでもしておきます。

Screen Shot 2018-12-22 at 20.56.01.png

そして次のようなコードを記述します。データベースのテーブルは、存在していないことも想定して、存在していない場合は先ほど作ったような新しくテーブル作るクエリを先にデータベースに投げます。

import sqlite3
import hou

dbname = hou.houdiniPath()[0] + "/database.db"

conn = sqlite3.connect(dbname)
c = conn.cursor()
sql_create_table = ''' CREATE TABLE IF NOT EXISTS history (
                id integer PRIMARY KEY,
                name text,
                state_data text,
                created text
            ); '''
c.execute(sql_create_table)


sql_get_rows = ''' SELECT id, name, state_data, created FROM history ORDER BY created ''' # historyテーブルからデータをすべて取得、かつcreatedの順に並び替える

c.execute(sql_get_rows)  # データベースにクエリを送る
rows = c.fetchall() # すべてのデータをリストとして取得する

print(len(rows)) # リストの大きさ(データベースのデータの数)をプリントする
if len(rows) > 0:
    print(rows[0]) # 一個めのデータの内容をプリントする

とりあえずまずは、データベースにあるすべてのデータの数をプリントして確かめます。プリントしたログは、WindowメニューのPython Shellから確認できます。

Screen Shot 2018-12-22 at 21.24.11.png

シェルに追加したスクリプトのアイコンをクリックするとPython Shellにデータベースにある一個めのデータの内容がプリントされます。スクリーンショットにあるように、asCode()関数で生成されたPythonコードが画面を埋め尽くしています。

Screen Shot 2018-12-22 at 21.35.35.png

データベースのデータからネットワークを再構成する

次に、データベースに格納されているPythonのコードを使って、ノードネットワークを再構築してみましょう。Pythonのexec関数を使って格納されているPythonコードを実行します。次のようにPythonのスクリプトをアップデートします。




rows = c.fetchall() # すべてのデータをリストとして取得する

print(len(rows)) # リストの大きさ(データベースのデータの数)をプリントする
if len(rows) > 0:
    print(rows[0]) # 一個めのデータの内容をプリントする
    
    pythoncode = rows[0][2] # 一個目のデータの、2個めのアイテムがPythonコードとなっている
    exec(pythoncode)

全体としては次のような形になっています

import sqlite3
import hou

dbname = hou.houdiniPath()[0] + "/database.db"

conn = sqlite3.connect(dbname)
c = conn.cursor()
sql_create_table = ''' CREATE TABLE IF NOT EXISTS history (
                id integer PRIMARY KEY,
                name text,
                state_data text,
                created text
            ); '''
c.execute(sql_create_table)


sql_get_rows = ''' SELECT id, name, state_data, created FROM history ORDER BY created '''

c.execute(sql_get_rows)
rows = c.fetchall()

print(len(rows)) # リストの大きさ(データベースのデータの数)をプリントする
if len(rows) > 0:
    print(rows[0]) # 一個めのデータの内容をプリントする
    
    pythoncode = rows[0][2] # 一個目のデータの、2個めのアイテムがPythonコードとなっている
    exec(pythoncode)

このようにスクリプトをアップデートした状態でShellに登録されたスクリプトを起動してみると、データベースに登録された一個目のデータを使って画面に保存したノードネットワークが再構築されます。

geo1というノードの上に、geo2というノードが新たに生成されます。このgeo2のノードネットワークの中身を見てみると、geo1の中身と同じになっているはずです。

Screen Shot 2018-12-22 at 22.52.45.png Screen Shot 2018-12-22 at 22.57.11.png

ということで、データベースにデータを格納→データベースからデータの取得かつ利用までできました。

データベースにあるデータを削除する

もう一個、データベースにある指定のデータを削除する機能も作っておきましょう。保存するだけだとすぐ膨れ上がってしまうので。

データベースからデータを削除するSQLを記述する

データベースから指定したidのデータを削除するためのSQL文の書き方はつぎのような形になります。




id = 1 # idを指定する
sql_remove_item = ''' DELETE FROM history WHERE id = ?; ''' # データベースから指定したidのデータを削除するためのクエリ

c.execute(sql_remove_item, [id]) # idが1のデータをデータベースから削除する
conn.commit()
conn.close()

全体としては次のような形で記述しています。

import sqlite3
import hou

dbname = hou.houdiniPath()[0] + "/database.db"

conn = sqlite3.connect(dbname)
c = conn.cursor()

sql_create_table = ''' CREATE TABLE IF NOT EXISTS history (
                id integer PRIMARY KEY,
                name text,
                state_data text,
                created text
            ); '''
c.execute(sql_create_table)

"""
sql_get_rows = ''' SELECT id, name, state_data, created FROM history ORDER BY created '''

c.execute(sql_get_rows)
rows = c.fetchall()

print(len(rows)) # リストの大きさ(データベースのデータの数)をプリントする
if len(rows) > 0:
    print(rows[0][2]) # 一個めのデータの内容をプリントする
    
    pythoncode = rows[0][2] # 一個目のデータの、2個めのアイテムがPythonコードとなっている
    exec(pythoncode)
"""
    
id = 1 # idを指定する
sql_remove_item = ''' DELETE FROM history WHERE id = ?; ''' # データベースから指定したidのデータを削除するためのクエリ

c.execute(sql_remove_item, [id]) # idが1のデータをデータベースから削除する
conn.commit()
conn.close()

この状態でアップデートし、Shellに登録されたスクリプトを起動してみると、idが1のデータが削除されます。

Screen Shot 2018-12-22 at 23.32.25.png

ここまでで、ローカルに保存されたデータベースの一通りの操作がHoudiniのPython経由で行うことができました。

ただ、なにぶんこのままだと非常に使いにくいことこの上ないものです。そこで、データベースの操作をもう少しユーザフレンドリーにするために、PySide2モジュールを利用してGUIによるCRUD(Create Read Update Delete)を作ってみます。Updateに関してはこの例に関しては必要ないので作らないことにします。

データ保存のためのGUI作り

データ保存用GUIに必要な要素を確認

データ保存の際のフローとしてやりたいことは、次のとおりです。

  1. Shellの保存用のスクリプトアイコンをクリックする。
  2. GUIウィンドウが開く。
  3. テキストフィールドに保存するデータの名前を入力する。
  4. 保存ボタンを押すとウィンドウは閉じて、現在のノードネットワークの構成が入力した名前とともにデータベースに保存される。

特に難しいことはなさそうです、たぶん。

GUIのテンプレを利用する

PySide2を使ったGUIのサンプルがSideFXのドキュメントのページにあるのでそれを改変することでGUIのテンプレートを作っていきたいと思います。何しろ初めてなので、、

テンプレの内容は次の通りです。

from PySide2 import QtCore
from PySide2 import QtWidgets

class FontDemo(QtWidgets.QWidget):
    def __init__(self, parent=None):
        QtWidgets.QWidget.__init__(self, parent)

        hbox = QtWidgets.QHBoxLayout()

        self.setGeometry(500, 300, 250, 110)
        self.setWindowTitle('Font Demo')

        button = QtWidgets.QPushButton('Change Font', self)
        button.setFocusPolicy(QtCore.Qt.NoFocus)
        button.move(20, 20)

        hbox.addWidget(button)

        self.connect(button, QtCore.SIGNAL('clicked()'), self.showDialog)

        self.label = QtWidgets.QLabel('This is some sample text', self)
        self.label.move(130, 20)

        hbox.addWidget(self.label, 1)
        self.setLayout(hbox)

    def showDialog(self):
        font, ok = QtWidgets.QFontDialog.getFont()
        if ok:
            self.label.setFont(font)

dialog = FontDemo()
dialog.show()

これを起動すると画像のようなウィンドウが表示されます。

Screen Shot 2018-12-23 at 0.14.48.png

大味ですが、これから作るGUIもこれくらい大味でいきましょう。細かく作るのは面倒臭いものですし。

利用するUIのウィジェットを確認

今回利用したいUIは、テキストが入力できるテキストフィールドと、保存のためのボタンです。ボタンはすでに上のテンプレの中で使われているので、そちらを流用するとして、テキストフィールドはないので、どうやって作るか調べる必要があります。

なにはともあれ、今回GUIのために使うPySide2モジュールのドキュメントを確認するところからです。ここからいけます。

この中で欲しいのはQt Widgetsの情報であると目星をつけます。上のサンプルコードを見る限り、ボタン(QPushButton)がQtWidgetsの要素であるから、きっとそこにGUI関連のものがあると踏んだからです。

この中から、今度はテキストフィールドとして使えるウィジェットを探さないといけません。結構量が多いです。一覧を見ているだけでは分からないので、googleで"PySide2 text field"などと検索して、該当するウィジェットを割り出します。結果、QTextEditが使えそうだと目星をつけました。

これで準備はOKです。

GUIをコードで記述

PySide2のサンプルコードを改変して、次のように編集しました。

### PySide2モジュールをインポートする
from PySide2 import QtCore 
from PySide2 import QtWidgets

### ダイアログのクラスを作る
class SaveState(QtWidgets.QWidget):
    def __init__(self, parent=None):
        QtWidgets.QWidget.__init__(self, parent)

        vbox = QtWidgets.QVBoxLayout() # 縦に並べるレイアウト方式を使う

        self.setGeometry(500, 300, 300, 100) # ウィンドウのサイズの指定
        self.setWindowTitle('Save State') # ウィンドウのタイトルの指定
        
        self.label = QtWidgets.QLabel('Comment', self) 
        vbox.addWidget(self.label, 1)

        textedit = QtWidgets.QTextEdit() # テキストフィールドを作る
        vbox.addWidget(textedit) # レイアウトにテキストフィールドを追加する
        self.msgtextedit = textedit # ダイアログクラスのmsgtextedit変数にテキストフィールドを格納する

        button = QtWidgets.QPushButton('Save State', self) # 保存ボタンを作成する
        button.setFocusPolicy(QtCore.Qt.NoFocus)
        vbox.addWidget(button) # ボタンをレイアウトに追加する
        
        self.connect(button, QtCore.SIGNAL('clicked()'), self.save) # ボタンクリック時のイベントの処理を指定する
        
        self.setLayout(vbox) 

    def save(self):
        self.close() # ウィンドウを閉じる
        
dialog = SaveState() # ダイアログのクラスのインスタンスを作る
dialog.show() # ダイアログを表示する

このスクリプトを起動すると結果このようなダイアログが表示されます。

Screen Shot 2018-12-23 at 0.28.34.png

このスクリプトの中で、いくつかサンプルコードにはない点があります。

self.msgtextedit = textedit # ダイアログクラスのmsgtextedit変数にテキストフィールドを格納する

これは、後でボタンを押した時にデータベースにデータを保存する際、テキストフィールドに書かれたテキストを参照するためにクラスのインスタンス(self)の変数に格納したわけです。

データベースへの保存とGUIのコードを組み合わせる

ここまでできたところで、データベースの保存のコードと、GUIのコードを組み合わせます。

一番最初に作ったSaveStateToDBというシェルのスクリプトを次のように更新します。

import sqlite3 # sqlite3モジュールのインポート
import hou # Houdiniのモジュールのインポート
import datetime
from PySide2 import QtCore
from PySide2 import QtWidgets

node = hou.node("/")
# database.dbという名前のSQLiteのデータベースファイルが
# HIPファイルと同じ階層に来るようにファイルパスを作る
dbname = hou.houdiniPath()[0] + "/database.db" 

### ダイアログのクラスを作る
class SaveState(QtWidgets.QWidget):
    def __init__(self, parent=None):
        QtWidgets.QWidget.__init__(self, parent)

        vbox = QtWidgets.QVBoxLayout() # 縦に並べるレイアウト方式を使う

        self.setGeometry(500, 300, 300, 100) # ウィンドウのサイズの指定
        self.setWindowTitle('Save State') # ウィンドウのタイトルの指定
        
        self.label = QtWidgets.QLabel('Comment', self) 
        vbox.addWidget(self.label, 1)

        textedit = QtWidgets.QTextEdit() # テキストフィールドを作る
        vbox.addWidget(textedit) # レイアウトにテキストフィールドを追加する
        self.msgtextedit = textedit # ダイアログクラスのmsgtextedit変数にテキストフィールドを格納する

        button = QtWidgets.QPushButton('Save State', self) # 保存ボタンを作成する
        button.setFocusPolicy(QtCore.Qt.NoFocus)
        vbox.addWidget(button) # ボタンをレイアウトに追加する
        
        self.connect(button, QtCore.SIGNAL('clicked()'), self.save) # ボタンクリック時のイベントの処理を指定する
        
        self.setLayout(vbox) 
    
    
    def save(self):
        # sqlite3を使ってデータベースを作り、historyというテーブルを
        # まだデータベースにない場合に限り新規で作成する
        conn = sqlite3.connect(dbname)
        c = conn.cursor()
        sql_create_table = ''' CREATE TABLE IF NOT EXISTS history (
                        id integer PRIMARY KEY,
                        name text,
                        state_data text,
                        created text
                    ); '''
        c.execute(sql_create_table)
        
        ### 名前を作る
        name = self.msgtextedit.toPlainText()
        
        ### ノードネットワーク再構成用のPythonコードを取得する
        node = hou.node("/") # 一番上の階層のノードネットワークのパス
        code = node.asCode(False, True, False, True, True, True) # ノードネットワークを再構成するためのPythonコードを取得する
        
        ### 現在の日時を取得する
        created = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") 
        
        sql_insert = ''' INSERT INTO history (name, state_data, created) 
                VALUES (
                    ?,
                    ?,
                    ?
                ); ''' #データ挿入のクエリ文を作る
        c.execute(sql_insert, [name, code, created]) # データベースにクエリを送る
        conn.commit() # データベースの変更分を保存する
        
        conn.close() # データベースの接続を切る
        self.close()

        
dialog = SaveState()
dialog.show()

これで、指定したコメントで現状のノードネットワークの構成をデータベースにGUI経由で保存できるようになりました。

Screen Shot 2018-12-23 at 0.56.47.png Screen Shot 2018-12-23 at 0.58.07.png

データ読み込みのためのGUI作り

データ読み込み用GUIに必要な要素確認

データ読み込みの際のフローとしてやりたいことは、次のとおりです。

  1. Shellの読み込み用のスクリプトアイコンをクリックする。
  2. GUIウィンドウが開く。
  3. 保存されいるデータがすべて、テーブルビューで表示される
  4. 一覧からロードしたいデータを、データごとに用意されたボタンで選べられる
  5. 一覧から削除したいデータを、データごとに用意されたボタンで削除できる

保存よりは多少難しそうな気配がします。

利用するUIのウィジェットを確認

例によって、利用するウィジェットの確認をここから行います。

結論からいうと、QTableWidgetというウィジェットを使うことにします。

QTableWidgetのページから使い方のサンプルを確認しておきます。

Screen Shot 2018-12-23 at 1.09.46.png

GUIをコードで記述

では、データ読み取り用のGUIをPythonのコードで記述してみます。まだ視覚的確認のみで、データベースとのつなぎ合わせはしません。

import sqlite3
import hou
from PySide2 import QtCore
from PySide2 import QtWidgets
from functools import partial

# データベースから取得したと仮定するダミーデータを作る
rows = [
            [1, "ABC", "AAAAAAAAA", "2018-12-22"],
            [2, "DEF", "BBBBBBBBB", "2018-12-23"],
            [3, "GHI", "CCCCCCCCC", "2018-12-24"]
        ]

# 読み取りのダイアログのクラス
class LoadState(QtWidgets.QWidget):
    def __init__(self, datas, parent=None):
        QtWidgets.QWidget.__init__(self, parent)

        vbox = QtWidgets.QVBoxLayout()
        
        self.setGeometry(500, 300, 600, 300)
        self.setWindowTitle('Load State')
        
        tableWidget = QtWidgets.QTableWidget()
        tableWidget.setRowCount(len(datas)) # データベースにあるデータの数だけ行を作る
        tableWidget.setColumnCount(5) # 列を五つ作る
        tableWidget.setHorizontalHeaderLabels(["id", "commnet", "created", "", ""])
        
        for i in range(len(datas)):
            ### データベースの格納されたPythonコード以外をテーブルのセルに挿入する
            tableWidget.setItem(i, 0, QtWidgets.QTableWidgetItem(str(datas[i][0])))
            tableWidget.setItem(i, 1, QtWidgets.QTableWidgetItem(str(datas[i][1])))
            tableWidget.setItem(i, 2, QtWidgets.QTableWidgetItem(str(datas[i][3])))
            
            buttonLoad = QtWidgets.QPushButton("Load State") # 読み取り用のボタンを作る
            buttonLoad.setFocusPolicy(QtCore.Qt.NoFocus)
            buttonLoad.clicked.connect(partial(self.loadData, datas[i][2])) # ボタンクリック時の関数にデータベースに格納されたPythonのコードをパスする
            tableWidget.setCellWidget(i, 3, buttonLoad) # 読み取り用のボタンを4つめの列に挿入する
            
            buttonDelete = QtWidgets.QPushButton("Delete"); # データ削除用のボタンを作る
            buttonDelete.setFocusPolicy(QtCore.Qt.NoFocus)
            buttonDelete.clicked.connect(partial(self.deleteData, datas[i][0])) # 削除ボタンクリック時の関数にデータのidをパスする
            tableWidget.setCellWidget(i, 4, buttonDelete) # 五つめの列に削除ボタンを挿入する
        
        
        self.table = tableWidget # ダイアログのクラスの変数tableにテーブルのウィジェットを格納する
        
        ### テーブルのヘッダーのサイズを自動調整する
        header = tableWidget.horizontalHeader()
        header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
        header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
        header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
        header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
        header.setSectionResizeMode(4, QtWidgets.QHeaderView.ResizeToContents)
        
        vbox.addWidget(tableWidget) # テーブルのウィジェットをレイアウトに追加する
        
        self.setLayout(vbox)
    
    ### データ読み取りのボタンを押した際に呼ばれる関数
    def loadData(self, data):
        self.close()
    
    ### データ削除ボタンを押した際に呼ばれる関数
    def deleteData(self, id):
        ### idにマッチするデータを探す
        results = self.table.model().match(
            self.table.model().index(0, 0),
            QtCore.Qt.DisplayRole,
            id,
            -1,
            QtCore.Qt.MatchContains
        )
        ### foundRowに削除する対象の行番号を格納する
        foundRow = -1
        for result in results:
            foundRow = result.row()
        
        self.table.removeRow(foundRow) # 指定の行番号のテーブルの行を削除する


dialog = LoadState(rows)
dialog.show()

結果走らせると、次のようなダイアログウィンドウが表示されます。

Screen Shot 2018-12-23 at 1.50.14.png

データベースにあるデータが縦方向にずらっと並ぶイメージです。横にあるLoad Stateボタンを押すことで指定したデータのロード、つまりそのデータに格納されているPythonコードをつかってノードネットワークの再構成を行うようにするイメージです。現状は、押してもウィンドウが閉じるだけです。

最後に、Deleteボタンを押すと、そのデータがデータベースから、かつテーブルビューから削除されるイメージです。現状はテーブルビューからのみ行が削除されるようになっています。

Screen Shot 2018-12-23 at 1.52.55.png

データベースからの読み取り+削除とGUIのコードを組み合わせる

それでは大詰めです。データベースからの読み取りと削除の機能を、GUIのコードと組み合わせて、GUIで読み取りと削除が行えるようにします。

import sqlite3
import hou
from PySide2 import QtCore
from PySide2 import QtWidgets
from functools import partial

node = hou.node("/")
dbname = hou.houdiniPath()[0] + "/database.db"

conn = sqlite3.connect(dbname)
c = conn.cursor()
sql_create_table = ''' CREATE TABLE IF NOT EXISTS history (
                id integer PRIMARY KEY,
                name text,
                state_data text,
                created text
            ); '''
c.execute(sql_create_table)

conn.commit()

sql_get_rows = ''' SELECT id, name, state_data, created FROM history ORDER BY created '''

c.execute(sql_get_rows)
rows = c.fetchall()

conn.close()


# 読み取りのダイアログのクラス
class LoadState(QtWidgets.QWidget):
    def __init__(self, datas, parent=None):
        QtWidgets.QWidget.__init__(self, parent)

        vbox = QtWidgets.QVBoxLayout()
        
        self.setGeometry(500, 300, 600, 300)
        self.setWindowTitle('Load State')
        
        tableWidget = QtWidgets.QTableWidget()
        tableWidget.setRowCount(len(datas)) # データベースにあるデータの数だけ行を作る
        tableWidget.setColumnCount(5) # 列を五つ作る
        tableWidget.setHorizontalHeaderLabels(["id", "commnet", "created", "", ""])
        
        for i in range(len(datas)):
            ### データベースの格納されたPythonコード以外をテーブルのセルに挿入する
            tableWidget.setItem(i, 0, QtWidgets.QTableWidgetItem(str(datas[i][0])))
            tableWidget.setItem(i, 1, QtWidgets.QTableWidgetItem(str(datas[i][1])))
            tableWidget.setItem(i, 2, QtWidgets.QTableWidgetItem(str(datas[i][3])))
            
            buttonLoad = QtWidgets.QPushButton("Load State") # 読み取り用のボタンを作る
            buttonLoad.setFocusPolicy(QtCore.Qt.NoFocus)
            buttonLoad.clicked.connect(partial(self.loadData, datas[i][2])) # ボタンクリック時の関数にデータベースに格納されたPythonのコードをパスする
            tableWidget.setCellWidget(i, 3, buttonLoad) # 読み取り用のボタンを4つめの列に挿入する
            
            buttonDelete = QtWidgets.QPushButton("Delete"); # データ削除用のボタンを作る
            buttonDelete.setFocusPolicy(QtCore.Qt.NoFocus)
            buttonDelete.clicked.connect(partial(self.deleteData, datas[i][0])) # 削除ボタンクリック時の関数にデータのidをパスする
            tableWidget.setCellWidget(i, 4, buttonDelete) # 五つめの列に削除ボタンを挿入する
        
        
        self.table = tableWidget # ダイアログのクラスの変数tableにテーブルのウィジェットを格納する
        
        ### テーブルのヘッダーのサイズを自動調整する
        header = tableWidget.horizontalHeader()
        header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
        header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
        header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
        header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
        header.setSectionResizeMode(4, QtWidgets.QHeaderView.ResizeToContents)
        
        vbox.addWidget(tableWidget) # テーブルのウィジェットをレイアウトに追加する
        
        self.setLayout(vbox)
    
    ### データ読み取りのボタンを押した際に呼ばれる関数
    def loadData(self, data):
        exec(data) # データベースに格納されているPythonコードを実行して、ノードネットワークを再構成する
        self.close()
    
    ### データ削除ボタンを押した際に呼ばれる関数
    def deleteData(self, id):
        ### idにマッチするデータを探す
        results = self.table.model().match(
            self.table.model().index(0, 0),
            QtCore.Qt.DisplayRole,
            id,
            -1,
            QtCore.Qt.MatchContains
        )
        ### foundRowに削除する対象の行番号を格納する
        foundRow = -1
        for result in results:
            foundRow = result.row()
        
        self.table.removeRow(foundRow) # 指定の行番号のテーブルの行を削除する
        
        conn = sqlite3.connect(dbname)
        c = conn.cursor()
        sql_remove_item = ''' DELETE FROM history WHERE id = ?; ''' # 指定のidのデータをデータベースから削除するクエリを作る
                          
        c.execute(sql_remove_item, [id]) # データベースからデータを削除する
        conn.commit() # データベースを更新する
        conn.close()


dialog = LoadState(rows)
dialog.show()

これを走らせると、データベースに保存されたデータの一覧が表示され、指定したデータの読み取りや削除ができるようになっています。

Screen Shot 2018-12-23 at 2.02.53.png Screen Shot 2018-12-23 at 2.03.57.png

これで完成です!
無駄に長くなりました、、、!

余談

ノードネットワークをasCode()で保存する際のTips

hou.Node.asCode()関数を使う際、一部保存されないネットワークがあります。それがロックされているノードネットワークです。

大抵のノードネットワークは大丈夫なのですが、よく使うノードのなかに、デフォルトでロックされているものがあります。それがSolver系のノードです。

Screen Shot 2018-12-23 at 2.11.12.png

こういった鍵アイコンが見えているようなノードに関して、中のネットワークも保存したい場合は、ロックを解除してあげる必要があります。ロック解除の仕方としては、例えばSolverの場合、右クリックしてAllow Editing of Contentsを選ぶことでロックを解除することができます。

Screen Shot 2018-12-23 at 2.11.01.png

データダウンロード

ここで作ったサンプルのShelfツールのデータは、次のURLからダウンロードできます。toolbarフォルダに入れて使います。

https://github.com/jhorikawa/HoudiniSnippets/tree/master/0010%20SQLite%20Access

Macの場合、toolbarフォルダは次のパスにあります
~/Library/Preferences/houdini/17.0/toolbar/

おわりに

今回はローカルデータベースへのアクセスでしたが、同じような考え方でリモートのデータベースとの接続もできるはずです。個人的には用途の幅は非常に大きいと考えています。SQL系のデータベースは様々なフィルタリングができるので、細かい検索が可能です。大量なデータであればあるほど、その真価が発揮されるのじゃないかと思います(でも大量にデータがある場合は、今回のようにすべてのデータを取得して一覧などは重すぎるのでしない方がいいと思います)

ということで、ここまで読んでくれた方ありがとうございました!
なにか得るものがあればと信じています。

Houdini Advent Calendar 24日は @yuya_torii さんによる記事になります。

21
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?