2014年1月27日月曜日

REPL VS. MVC

さて、次のようなwxPythonを使った記事があります。

ModelViewController

これはwx.lib.pubsubと呼ばれるライブラリを使ってMVCパターンでGUIアプリを作る例なんですが、かなり錯綜してるように見えますね。
基本的には


  1. ModelがsendMessageメソッドを用いてインスタンス変数を(相手が誰か分からないにせよ)メッセージとして送信する。
  2. ControllerがView(あるいはフレーム)のインスタンスを保持してる。
  3. Controllerがsubscribeメソッドを用いてメッセージを受信する。
  4. ControllerがViewをチェンジする。
となってて、Controllerの仕事が多すぎますし、全てがControllerに依存しています。
これが「望むべくMVCの書き方」なのか、って言われると、理論的には「?」なんじゃないんでしょうか。ちょっと釈然としませんね。
ところがこれが、クラス同士の通信、って概念で考えると、結構既存のGUIフレームワークとMVCの相性って良くねぇんじゃねえの、とか色々試行錯誤した結果思いました。REPLだとreturnで結んだRead、Eval、Print同士が仲良く、ある種イベントループを作り上げるんですが、現状のGUIフレームワークだと、恐らくそのルーツはVisual Basicらしいんですが、要するにフレームに「各種メソッドが個別にぶら下がってる」モデルがベースな為、実はREPL構造とは相性が悪い。悪いくせにMVCなんてやると結構大変ですね。
本当はMVCパターンを狙うならMVCに適したGUIフレームワークが必要なのかもしれません。ハッキリ言えばREPLを写像する、ってアイディアから言うとGUIフレームワークが用意してるイベントループが邪魔なんですよね。REPLだと自分でイベントループを簡単に書けちゃうし、またクラス同士の独立性も高まります。ひょっとしたら低レベルツールを使えば何とかなるのかもしれませんが、いずれにせよ、恐らく、現代の多くのGUIフレームワークは元々「個別のイベントとそれが関与してるメソッド」を「ぶら下げる」為に本体自体がイベントループを用意せざるを得なかったんでしょう。

ここんとこやってたのはREPLのMVCへの写像がテーマです。つまり、キチンとしたREPLを持つCLIのアプリケーションを書いて、そのCLIアプリケーションを最小限の努力でGUI化するにはどうすれば良いかを考えてました。なかなかこれが苦戦してたんですが、ある程度の方針が分かりました。それは次のようなモノです。

  1. ViewにはControllerのインスタンスを埋め込む。
  2. ControllerにはModelのインスタンスを埋め込む。
  3. Modelは返り値を返す(returnする)。
  4. ViewはControllerのインスタンス.メソッドを引数を付けて呼び出して、その返り値を利用して何かしら表示する。
  5. GUIフレームワークのイベントループにはViewのインスタンスを渡す。
  6. 環境情報等は極論Viewが保持する(もちろん環境クラスを作っても良いが、そのインスタンスはViewで管理する)。
の6つですね。これなら比較的REPLの構造を崩さないで済みます。
ただ、Viewに環境等の情報を埋め込む限り、破壊的操作は避けられないと言うデメリットが生じますが、これは現行のGUIフレームワークを用いる以上、しょうがないでしょう。
では、実際に前出のwxPythonを用いたMVCの例に従って、GUIで(大した事はないですが)アプリケーションを組んでいきましょう。

CLIのアプリケーションを作る


まずは、上記のアプリと同じように動くCLIのアプリケーションをREPLモデルで組み上げます。まあ、アプリケーションって程大したプログラムでもないんですが、一方、MVCにどうやってコンバートするのか、と言うのを見るには単純な方が良いんで、まあいいでしょう。
次のコードがCLI版になります。





ホント大した事がないんですが、それでも重要な事があります。それはこれです。

Pythonの場合、継承目的で作成してメソッドを全部子クラスに継承させたい場合、プライベートメソッド(アンダーバー二つ__で始まるアレ)は作らない

です。これ、どういう理由なんだか知らないんですが、プライベートメソッドにしちゃうと、Pythonだと子クラスに継承出来ないんですね。色々Pythonに関して調べてみたんですが、どういう理由でこうなってるのか分かりません。

注: Twitterで教えてもらったんですが、通常、OOPな言語の場合(例えばC#)、「他から呼び出されたくなくても、子クラスに継承したい場合のメソッドではプライベートじゃなくってProtectedメソッドにする模様です。 

まあそれさえ気を付ければ良い、って事ですね。
ではこれをひな形にして、MVCによるGUI版を作っていきます。

Qt Designer

原版はwxWidgetで作られていたんですが、今回はPySideと言うQtのPythonバインディングを用います。Qt知らない人はあんまいないと思うんですが、一応説明すると、Googleのアプリケーションなんかでも用いられているC++で書かれたマルチプラットフォームのGUIフレームワークで、他にはLinuxのKDEを作ってるツールって事で有名ですね。wxWidgetなんかに比べても高機能だと言われています。
Windowsの場合、PySideをインストールすると、C:\Python27\Lib\site-packages\PySide\にdesigner.exe、つまりQt Designerと言うGUI Builderが入ってますんで、これ使ってサクサクとまずはFrameを作っていきます。


Qt Desingerを起動すると次のようなポップアップが現れます。


Dialog without Buttonsを選んで[Create]ボタンを押します。


左側にあるWidget Boxから、LabelLCDNumberButton二つをドラックアンドドロップで持ってきて、適当に次のようにDialog上に配置します。


んで、レイアウト調整なんですが、上のツールバーにこの手のボタンが並んでるんですが。


例えばボタン二個をCtrl押しながらマウスで選択して、一番左のボタン(Lay Out Horizontally)とか押すと、ボタン二個がキチンと並んで、かつその状態(ボタン二個が入った大枠)を引き伸ばしたりして大きさ調整出来るんですね。


同様に、Label、LCDNumber、そしてボタンの大枠を全部選択してLay Out in a Gridボタンを押すと次のようになって、また大枠を引き伸ばせます。



Formプルダウンメニューから[Preview...]を選べば、今の状態が実際にどんなもんなのか見る事が出来ます。


ちとLabelが大きすぎるので、Labelを選択して、右側のPropertyからSize Policyを探しだして、Vertical PolicyFixedにします。



うん、イイ感じになってきました。
LabelがTextLabelと言う名称で左寄せなんで、これをMy Moneyと改名して中央寄せにしたい。その場合は、さっきのように、Labelを選んで、右側のPropetyTextでTextLabel -> My Moneyと変更、AlignmentHorizontalAlignCenterにします。


DialogのWindowTitleをMain Viewに変更したいので、右上のObject Inspectorからdialogを選択、またもやPropertyからWindowTitleを探しだしてMain Viewに変更します。


ボタンの名前も変えましょう。これも例によってPropetyを使います。TextをそれぞれAdd Money、Remove Moneyに変更します。


ここまで来ればあと一歩、です。Qtにはシグナルとスロットって機構がありまして、要するにボタンが押された時適切なメッセージを出して任意のメソッドに繋ぐ(スロット)わけですが、そのひな形もQt Designer上で作ります。
[Edit]プルダウンメニューから[Edit Signals/Slots]を選択してQt Designerのモードをシグナル/スロット編集モードにします。


さて、そうすると任意のボタンをとあるスロットに繋ぐわけですが、このシグナル/スロットモードでは、GUIでそれを抽象的に扱う事が出来ます。例えばAdd Moneyボタンをドラッグしようとするとヘンな矢印が出てくるんですね。これが「特定のメソッドへの接続」を表します。


マウスを離すとConfigure Connectionと言うポップアップが現れます。


ちょっと見れば分かるんですが、左側が「ボタンの動作」、右側が「ボタンの動作に従った望まれるメソッド」が並んでて、例えば左にあるclicked()ってメソッドは「ボタンがクリックされたら」ですね。つまり、「何かが成されたら(右側にある)何かをしろ」と言う関係になっています。
右側にはQtで想定されてるデフォルトの動作が並んでるんですが、ここではbuttonClicked()と言うメソッドを(まだ書いてないけど)新たに追加して、そいつと左側のclicked()を結びましょう。
右側の[Edit...]ボタンをクリックします。


上がスロット、下がシグナルになっています。今回はシグナルは使わないんで、上のスロットの+ボタンを押してbuttonClicked()と言うメソッドを追加します。


[OK]ボタンを押してConfigure Connectionに戻ります。左側からClick()を選択、右からbuttonClicked()を選択、[OK]ボタンを押します。



同様にして、Remove Moneyボタンも同じbuttonClicked()メソッドに繋いでしまいます。


これで準備はO.K.です。まずはui_mvctest.uiとでも名づけて、GUIデザインを保存しましょう。
Qt Designerではxmlファイルとしてデザインが保存されます。



んで、まずはこいつをPythonで解釈出来るpyファイルに変換せなアカンのですね。んで、その為のツールが、例えばWindowsならC:\Python27\Scripts\にpyside-uic.exeと言う名前でインストールされてる筈なんですが、どーゆーわけかコイツはコマンドラインのアプリケーションとなっています(苦笑)。何故にQt Designerから直接呼び出して変換出来ないのかサッパリ分かりません(苦笑)。
(wxGladeならGUIでPythonコードに変換してくれます!)
Linuxならこの手のツールがあっても、コマンドラインが使いやすいんでイイんですが、ことさらWindowsのコマンドラインは使いづらいんですよねぇ。パス記述するのがおうおうにして厄介です。
まあ、しょうがないんですが、基本的には完全パス与えて実行した方が良さそうです。例えばこの場合ですと、

C:\Python27\Scripts\pyside-uic.exe -o プロジェクトフォルダ\ui_mvctest.py プロジェクトフォルダ\ui_mvctest.ui

でしょうか。オプション引数 -oを忘れないようにしましょう。また、こう言うのがホント気になるんですが、変換後のファイル、元ファイル、の順序です(普通の発想なら元ファイル -> 変換後のファイル、になるんじゃねえの?とか思うんですが、しばしばこう言う「語順」を見かける)。
さて、無事に変換が済めば、PySide用に変換されたpyファイルが出来てる筈です。



ってなわけでこいつを利用してMVCモデルによるGUIアプリケーションを作っていきます。

残念なお知らせ

当初の予定では、CLIのアプリをREPLモデルとして作る -> 各クラスとGUI部品を多重継承で受け取ったMVC部品を作ってく、って予定だったんですが、なんと、PySideが多重継承させてくれないんですよ(苦笑)。何でやねん、とか怒ってたんですが(笑)。
曰く、

新スタイルクラスじゃないと多重継承出来ません

とか言いやがって(笑)。んなもん知るか、っての(笑)。
調べてみたら、Pythonの新スタイルクラス、ってのは2.2以降に出てきた、とか書かれてるんですが、Pythonに触れ出したのは2.4以降なので、全く何のこっちゃ、です(笑)。
まあ、いずれにせよ、Qt自体が多重継承推奨してない模様なんで従いますか。この場合の「従う」ってのは、結果REPLのPrintは捨てる、って事です(爆)。

View

と言うわけで、まずはViewを作っていきます。Viewのコードは以下の通り。



ポイントは、最初書いた通りControllerのインスタンスを保持してる事、それと、REPLモデルの場合の引数の一部にあたる環境 env をインスタンス変数として持っている事です。
単純な流れとしては、


  1. ボタンがクリックされると buttonClicked() メソッドが呼び出され、その中でControllerのインスタンスのparse()メソッドが呼び出され(結果Model()のインスタンスが返す)返り値をxとenvに代入する。
  2. 返り値xはLCDディスプレイの表示に使われる。
  3. 返り値envはインスタンス変数envに代入される。
です。
今回はシンプルだったんですが、基本的にはボタン押されたと同時にController(翻ってはModel)からの返り値利用して表示までこぎつけるようにした方が良いでしょうね。ここで色々分けてしまうと(つまり、別の何かに手渡す感じにする)、GUIのコード的には相当ワヤクチャになってしまうでしょう。

Controller

Controllerのコードは以下の通りです。




ControllerもModelのインスタンスを持っています。ここではparseメソッドをオーバーライドしてますが、特徴としては、ReadはreturnしてたトコをModelのインスタンスからinterpメソッドを呼び出して引数を与えてるトコですね。まあ、見た通りReadのコードより(継承も相まって)相当単純化されています。
まあ、今回はシンプルなんですが、こう言う感じで、例えばViewが複数の情報を送るような場合、View側から適切なControllerインスタンスのメソッドを呼び出し、Controller側でも入力に応じたメソッドを用意しておく、と言うようなカタチにすれば複雑なプログラムにも対処出来るんじゃないでしょうか。かつ、ここではやっぱ「計算自体は」行いません。あくまでModelに対するプロトコル形式を選択出来るカタチに注力すべきだと思います。

Model

最後はModelです。Modelのコードは以下の通りです。




これも継承の為相当シンプルになっています。まあ、このアプリケーションのケースだと、元々Eval自体もシンプルなんですが、GUIの場合余計な入力は最初っから入ってこない前提なんで(特にボタンとかはそう)殆ど書くのはハナクソですね。
もう一回書いておきますが、

View が Controller のインスタンスからメソッドを呼び出し -> Controller は Model インスタンスからメソッドを呼び出し -> Model インスタンスが計算結果を返す -> View が結果を知る
って流れなんですが、恐らくこのように見えるでしょう。

View が Controller のインスタンスからメソッドを呼び出す -> すぐ返り値が返ってくる

つまり、Model自体は表面的には見えないで隠されてるカンジになりますね。

Main

ZetCodeのPySideチュートリアルに依ると、main関数を作って、こう書くのが「お約束」の模様です(笑)。





ってなわけで REPL -> MVC のコンバートは完了です。以下にMVCの全コードを置いておきます。



ついでだからWindowsの実行形式も作ってしまおう

せっかくGUIのアプリケーションが作れたんだからWindowsの実行形式にコンパイル出来ればサイコーだったりしますよね。その為にはpy2exeと言うユーティリティを使います。
これは以前、Pythonでのイスカンダルのトーフ屋ゲームの実行形式作った時にも利用したユーティリティでコマンドライン形式のアプリケーション実行形式を作る場合には結構簡単に使えるんですが、ところがQtなんかのGUIモノをコンパイルする場合ハマったので、一応ここに解説しておきます。
まずは何はともあれ、どーゆーわけか


Microsoft Visual C++ 2008 Redistributable Package (x86)


と言うものがPCにインストールされてないといけません。しかもSP1とか2010とかじゃダメで、上のブツじゃないとダメなんです。こいつに含まれてるmsvcp90.dllってブツが実行形式作成に必須なモノなんです。
なお、上のヤツは再配布可能なdll(ダイナミックリンクライブラリ)が含まれてるんですが、利用規約にはフォルダMicrosoft.VC90.CRT(msvcp90.dllが存在するVC以下のフォルダ)に含まれてる「3つのdll」は全て単独で配布するのは禁止されていて、同じフォルダにあるMANIFEST ファイルを含め、全部纏めて配布せなアカン、って決められてる模様です。
ああ、クソメンドくせえなぁ、Microsoftさんよ(苦笑)。
そこで、py2exeチュートリアルに従って、まずは次のようなsetup.pyを作ります。



上のようなsetup.pyを作ると、自動生成される実行形式をぶち込んだフォルダ(distフォルダ)にdllが入ってるフォルダをコピペしてくれます。これでMicrosoftの規約は守れるようになるわけですね。
そしてその後、


  1. DOS窓でプロジェクトフォルダに移動する
  2. 「python プロジェクトフォルダ/setup.py py2exe」 をDOS窓で走らせる 
とすればプロジェクトフォルダ内に新しくbuildとdistと二つのフォルダが作られます。distフォルダにめでたくWindows実行形式が生まれていますね。

まあ、こんなカタチでやっとこさ、GUIアプリの実行形式を作るまで辿り着きました。お疲れ様です。


0 件のコメント:

コメントを投稿