[PySide] Maya、Windowsスタンドアロンの記述

少し記述に違いがあったり、モジュールとか環境の違いも出るので、メモが必要な感じ。

極端に古い環境を考えても仕方ないけど、そもそも自分も結構古い環境なので、現行との違いとか忘れてしまうのもある…

2014/2016/2018で考えてみたい。

PySide/PySide2

PySide1: 2014, 2016
PySide2: 2018(※2017から、PySideのバージョンが変わってる)

shiboken

2015/2016辺りでMixin記述が出来るようになったので、2016, 2018はshiboken不要。
2014の場合はshiboken入れるなり、MayaのWindowハンドルを取得するなりして、PySide側のWindowにParentの設定が必要。

QApplicationのinstance生成

これは、スタンドアロンとMayaの場合で実装が異なる。
スタンドアロンの場合は、よくあるexampleのようにインスタンスを生成すればいい。

Mayaの場合は、既にインスタンスが作られた状態から動かすので、必要に応じてinstanceメソッドでインスタンスを取得する。

でも既にイベントループとか動いているので、app.exec_()しなくてもウィンドウ作成&表示で動き始める。
必要なら呼べば大丈夫な感じ(そういえば、これはOKなのか?)ていうか、要らない。

[Maya] ノードの変更を検出してツールの値を更新

 掲題の通り、Maya向けにアトリビュート編集をするUIをPySideで作っている時に、MayaでもPySideのUIどちらからも値の変更ができるようにする処理を書いていて、何故かMayaがCrashする現象を起こしてしまって何故だーと思っていたら、単にシグナルのブロックが不完全だった…というお話。

 やってるつもりでもポカしている部分はあるもので、油断は禁物。という話になってしまった。

ノードの変更を検出

 これは、先に書いた記事(ノードの変更を検出する)を参照すれば基本的なことは分かると思う。
 指定のノードに変更が掛かると、コールバック関数が呼ばれる。
 コールバック関数内でなんの変更が掛かったか判定して、必要に応じて対応をする。単に値の変更を捕まえるなら、MNodeMessage.kAttributeSetをチェックすればいい。

 問題なのは、MayaとPySide2つの変更に対応する場合にどう書くか。

 基本的にコアな部分は、この2通り。

 ・Maya(getAttr)⇒PySide(set)
 ・PySide(get)⇒Maya(setAttr)

 イベントのトリガーは、基本2つ。
 (※Mayaのタイムライン移動にも対応とか諸々対応すると増えるが、とりあえず値変更に限定した場合)

 ・Maya : ノードの変更を検出
 ・PySide: widgetの変更イベント

 動作としては、以下の通り。

 ・Mayaトリガー
  ノードの変更コールバック処理で、
  Mayaの対象ノードをgetAttr、PySideのwidgetにsetする。

 ・PySideトリガー
  PySideのwidgetの値更新イベント処理で、
  PySideの値をget、Mayaの対象ノードにsetAttrする。

シグナルの抑止

 これだけで書くと、冒頭に書いたCrashの件のような問題が起こる。
 単純に、Mayaトリガーの変更を実装。PySideトリガーの変更を実装。
 …というように進めてみるとふと気づくのが、「変更イベントが循環する」という問題。

 PySideでGUIのプログラムで凝ったものを書き始めると途端に出てくる障壁のような問題なのだが、これを制御しようとし始めると「シグナル」の機構を理解しておく必要が出てくる。
 その機構の利用方法の1つが、blockSignalsというメソッドの機能。

 このメソッドを呼ぶと「シグナルの抑止動作」を制御してくれる。
 単に止めるのではなく、止めるか止めないかを設定できるので、必要に応じて一時的に止めてその隙に変更を掛ける。ということができるようになる。

 これを然るべき位置に設定しておけば、(無限ループに嵌って)Crashすることなく意図通り動いてくれるようになる。

循環しない形

 先ほどの処理の話で言うと、循環が起こると例えば以下のような状況が発生する。

 ・Mayaトリガー
  ⇒PySideのwidgetに値をset
  ・PySideトリガー
   ⇒Mayaのノードに値をsetAttr
   ・Mayaトリガー
    ⇒PySideのwidgetに値をset
    ・PySideトリガー
     ⇒Mayaのノードに値をsetAttr
     :
     :

 このように、シグナルを止めなければ循環してしまう。
 なので形としては、以下のようにしなければならない。

 ・Mayaトリガー
  ⇒flg=blockSignal(True)
   PySideのwidgetに値をset
   blockSignal(flg)

 ・PySideトリガー
  ⇒flg=blockSignal(True)
   Mayaのノードに値をsetAttr
   blockSignal(flg)

 PySideトリガーの場合、Maya側のトリガー(ノードの変更通知)をどこで抑止するかにもよるが、blockSignalsメソッドを利用するにはシグナルの機構を自前で用意する必要がある点に注意。
 Pythonは基本マルチスレッド動作ではないので、(並列処理が絡まなければ)単にフラグ管理でも動くだろうし、ちゃんとシグナルを使って記述するもよし、である。

 それからblockSignalsメソッドを使う場合は、flg=blockSignal(True) してから blockSignal(flg)で終わらせるのが基本的なお作法。
 これは、blockSignalsが“入れ子”になってしまう場合を想定した書き方になる。

 途中のメソッド内が、blockSignal(False) で終わらせるとおかしなことになってしまうので、注意したい。

QTreeViewの項目追加, 項目へのWidget追加

試したこと、めも。
viewにwidget反映させる部分とか良い方法ないんかな…あとsenderメソッドも、この場合に利かないのは何故なのか判らなくて、スッキリしない。

参考:
 https://gist.github.com/skriticos/5415869
 http://hareoff.blogspot.com/2013/03/pyqtqtreeviewwidget.html

self.sender() で発信元が見えない場合

 自分は、self.ui=loader(path)でui読み込んだ後、gui.ui.show() とかやっていたが、これではsender()で発信元を受け取れなかった。
 仕組み的に、ちゃんとQMainWindowにQDialogとか読み込んで配置するには、正しい場所にuiを配置しないとダメだった…ということです。

関連:シグナルの発信元を取得する

シグナルの発信元を取得する

覚書。

QListview等利用時のmodelの更新

覚書。

Qt.py 便利だ…

 前の記事(PySideのノード描画ライブラリ)で Python3&PySide2対応させたけれども、やっぱりPython&PySide環境でも利用するには…と思い始めてしまったので、とりあえず気になっていたQt.pyを試してみた。

 感想としては、「やっぱり便利だ…。」という感じ。
 実質PySide2に書き換えたコードのimport文の指し先を、PySide2からQtに変えただけで特に問題なく動いてしまった。
 PySideで作られたコードの場合は、PySide2相当の記述に変えないといけないのかもしれないけど、一旦PySide2で作ったらimport文以外変更なしでいけそうな気配。

 PySide2になってちょこちょこ引数のとり方も変わってるので、まったくPySide2対応していない場合にそれを今更直すのは面倒だけど、新しく書いたコードを古い環境に適用させないといけない可能性が有るなら便利かと思う。(古い環境にもQt.pyがあれば、コード変更要らずなわけで。)

参考

Qt.pyでPySideとPySide2を共通コードで使う

PySideでIDLEイベントを捕まえるには??

 そもそもの話 PySideにはIDLEイベントがなくて、どうしよう?ってなったんですが、ググったら頭いい人がいました。

 emit SIGNAL when GUI Thread is idle in Qt?

IDLEイベントというのは、Windowプログラムが「現在お手隙な状態です」っていうタイミングを示すイベントって意味です。
要するに、GUIがなにもしてないので、なんか仕事ありませんか?って言ってくれる感じです。

 
 元はC++のコードなんですが、ざっくりPySide向けに書き直してみたんですが、これでいいのか良くわかんないんです…
 なんせイベントを上げたら次のaboutToBlockが発生するので、この実装を使うと実際には1コア占有しちゃう。
 IDLEで発行したイベントはカウントしない様にして欲しかったりするんですけどね…

Qtでスレッドを使う前に知っておこう

スレッド関連を調べていて、掲題の記事を見つけたのでメモ。

Qtでスレッドを使う前に知っておこう

内容としては、How To Really, Truly Use QThreads; The Full Explanationというブログ記事で語られる方法が最近のスレッド利用時の基本的なお作法になってますよ。的な話。

C++記事なんで、Python(PySide)で書き下してみたのが、こちら。
※全部のシグナルがこれで正しい設定なのか若干わかってないので、参考程度に。

いっぱいスレッド回そうと思ったら、こんな感じ?
※同時に表示されるタイミングがあるので、セマフォを使って順に処理するようにした。あと、処理終了時にウィンドウを閉じる設定も追加。

裏でスレッドを回しつつPySideのGUIの動作を滑らかにしたい

冒頭が一番最後、結論的な話で始めるけれど…

 実際に実装を書いて実行してみると、やっぱり場合により動作がおかしくて、「アプリケーションが停止しました」ってアプリが落ちたりする。
 ログを取ってみると、どうやら裏のプロセス実行が早すぎて表のUI処理が終わらないうちに次々と裏から要求が来てしまう。
 その為に表示処理が多重に呼び出されて、呼び出された関数が多すぎてスタックを食いつぶしているっぽい…という結論になった。

 ここにはまだ書いてないけど、裏から送られる処理を1つずつ順に片付けて、表の情報反映に必要な関数実行が多重化しないように制限を加えたら、安定して動くようになった。

 プロセス処理は、いろいろPythonを使ってると意識しなくてよかったことを意識させられてしまう。
 Pythonではなかなかしんどい部類の処理かもしれない。

うまく行かず、考え直してみた

 いろいろやってみて(後述)、PC替えたら正常に動作しないことが判明。
 調べてみると、PySide(というかPyQt?)は別スレッドから動かすことを許していない模様。
 自分のPCは4スレッドしか回らないのだけど、PCのスペックが高くて10~20スレッドが平気で動くハイスペックなPCだと、「Warning: QPixmapがどうたら」のようなメッセージがstdoutかstderrに吐かれてしまった…無念。

 
 どうにも、他の方法を検討する必要がある。
 とりあえず、ディレクトリ取る程度だとさほど処理を食わないのでスレッドでも平気だが、その後にファイルを読み込んだり、更に内容を読み取るような場合は別プロセス起動を考えないと、処理スレッドに移る度に数秒停滞という事が起こってしまう。

 感覚的には、処理毎に10ms程度のオーダーで処理を完了するような形にまとめないと、GUIが思うように動かなくなると思う。

 別スレッドで処理するサンプルをメモっておく。

いろいろやった事柄(時系列的には、ここがはじまり。)

参考ページ1
参考ページ2

 最初は参考ページ1で頑張っていたけど、どうにもならなくて…
 いろいろ試した上に、結局Pythonの並列処理モジュールを使わないとダメだってこと(※)が参考ページ2をベースにいろいろ試してみておおまかに理解できたと思う。
 ※掲題のGUIをスムーズに動かすって事が、厳しいってこと。

なにが引っ掛かるのか

 端的に言えば、PythonのGILの仕組みのせいで(いくらスレッドを複数回したところで)実際にはそれは並列処理ではないので、バトンリレーの要領でタスク切替が行われなければ無駄にスレッドが空回りしてしまう。

 処理A –> 処理B –> (処理A) .. みたいな流れをしたいのに、
 実際には、処理A ——-(不要な間)————> 処理B —(不要な間)–> (処理A) —(不要な間)–> … のようなことが起こる。

 不要な間の時には、処理が無駄な空回りをしているか、やって欲しくない処理を延々続けていたりする。
 無駄な空回りをしていると当然その間ほかのスレッドの処理は停滞してしまうので、GUIに至っては応答なし状態が続き、最悪アプリケーションエラーで不意に落ちることも。。

Pythonの並列処理モジュール

 この辺のモジュールのお世話になった。
 threading — スレッドベースの並列処理(@Python Doc)
 queue — 同期キュークラス

 簡単に言うと、並列処理モジュールを仲介させることでスムーズにスレッド間の処理が切り替わってくれるようになるので、無駄にスレッドが空回りしてしまう状況を回避できる。(ノンプリエンティブ・マルチタスクのイメージ。なるべく短い時間で処理を切り上げ、ほかに処理を渡す…という書き方。高速に切り替わるほどに、滑らかに処理されるようになる。)

 処理A –> 処理B –> (処理A) .. みたいな流れを作り出せる。

 今回は、「処理A -(queue)-> 処理B -(queue)-> (処理A)」 .. みたいに、Queueでバトンを渡す感じに作った。
 でもlistWidgetの都合…なのか、登録中にやたらpaint処理が走るせいで、ウィンドウが白くなっていることが多くてあまりよろしくない感じ。
 描画タイミングとかを考えると、登録方法にも手を入れないとダメっぽい…

その後

 いろいろゴチャゴチャ書いてしまったけど、結局のところ今回の件の場合はセマフォで解決(※)という話だった。

 ※実際は解決どころか、ここからがドツボ本番で…PCを変えたらWarning連発で、記事冒頭の考え直しの項に進みます。
  ただ処理的にダメって話ではなくて、それじゃまだ対策が足りない…という話だったんですが。

 参考ページ3

QTableWidgetで複数セル消そうとしたら無限ループ

イベントハンドリングしている場合に、無限ループにはまったのでメモ。

Set data om multiple selected items in TableView

要は modelの更新がトリガーだから、model.blockSignals(True)して更新せよ。という話だった。
(更新終わったら、model.blockSignals(False)に。)

あと、再描画は自前になるので widgetのrepaint呼んどく。

PySideのウィンドウが閉じる際のイベントをキャッチできない場合

思い出したので、めも。

Widget’s “destroyed” signal is not fired (PyQT)

終了時に保存処理とか入れてるつもりなのに、なんか知らないうちにプログラム終わって保存できてねーっすケド?!みたいな状況があったりする場合の対処法。
「windowクラスの setAttributeメソッドで、QtCore.Qt.WA_DeleteOnCloseを設定しておきなさい。」という話。

python/pyside 開発でちょっとトレース実行したい..

久々に Python+PySide でツール開発。
しかし、いろいろ挙動を思い描けないというか、イメージが不鮮明すぎてトレース実行プリーズ!

という状況に直面したので、早速Goooooogle 先生に質問しましたら、無事にHit!
助かりました。

>【Python】いつまでprintデバッグで消耗してるの?(@らっちゃいブログ)