さて、次のようなwxPythonを使った記事があります。
ModelViewController
これはwx.lib.pubsubと呼ばれるライブラリを使ってMVCパターンでGUIアプリを作る例なんですが、かなり錯綜してるように見えますね。
基本的には
これは以前、Pythonでのイスカンダルのトーフ屋ゲームの実行形式作った時にも利用したユーティリティでコマンドライン形式のアプリケーション実行形式を作る場合には結構簡単に使えるんですが、ところがQtなんかのGUIモノをコンパイルする場合ハマったので、一応ここに解説しておきます。
まずは何はともあれ、どーゆーわけか
と言うものがPCにインストールされてないといけません。しかもSP1とか2010とかじゃダメで、上のブツじゃないとダメなんです。こいつに含まれてるmsvcp90.dllってブツが実行形式作成に必須なモノなんです。
なお、上のヤツは再配布可能なdll(ダイナミックリンクライブラリ)が含まれてるんですが、利用規約にはフォルダMicrosoft.VC90.CRT(msvcp90.dllが存在するVC以下のフォルダ)に含まれてる「3つのdll」は全て単独で配布するのは禁止されていて、同じフォルダにあるMANIFEST ファイルを含め、全部纏めて配布せなアカン、って決められてる模様です。
ああ、クソメンドくせえなぁ、Microsoftさんよ(苦笑)。
そこで、py2exeチュートリアルに従って、まずは次のようなsetup.pyを作ります。
上のようなsetup.pyを作ると、自動生成される実行形式をぶち込んだフォルダ(distフォルダ)にdllが入ってるフォルダをコピペしてくれます。これでMicrosoftの規約は守れるようになるわけですね。
そしてその後、
ModelViewController
これはwx.lib.pubsubと呼ばれるライブラリを使ってMVCパターンでGUIアプリを作る例なんですが、かなり錯綜してるように見えますね。
基本的には
- ModelがsendMessageメソッドを用いてインスタンス変数を(相手が誰か分からないにせよ)メッセージとして送信する。
- ControllerがView(あるいはフレーム)のインスタンスを保持してる。
- Controllerがsubscribeメソッドを用いてメッセージを受信する。
- 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化するにはどうすれば良いかを考えてました。なかなかこれが苦戦してたんですが、ある程度の方針が分かりました。それは次のようなモノです。
- ViewにはControllerのインスタンスを埋め込む。
- ControllerにはModelのインスタンスを埋め込む。
- Modelは返り値を返す(returnする)。
- ViewはControllerのインスタンス.メソッドを引数を付けて呼び出して、その返り値を利用して何かしら表示する。
- GUIフレームワークのイベントループにはViewのインスタンスを渡す。
- 環境情報等は極論Viewが保持する(もちろん環境クラスを作っても良いが、そのインスタンスはViewで管理する)。
の6つですね。これなら比較的REPLの構造を崩さないで済みます。
ただ、Viewに環境等の情報を埋め込む限り、破壊的操作は避けられないと言うデメリットが生じますが、これは現行のGUIフレームワークを用いる以上、しょうがないでしょう。
では、実際に前出のwxPythonを用いたMVCの例に従って、GUIで(大した事はないですが)アプリケーションを組んでいきましょう。
CLIのアプリケーションを作る
まずは、上記のアプリと同じように動くCLIのアプリケーションをREPLモデルで組み上げます。まあ、アプリケーションって程大したプログラムでもないんですが、一方、MVCにどうやってコンバートするのか、と言うのを見るには単純な方が良いんで、まあいいでしょう。
次のコードがCLI版になります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
import sys | |
class Read: | |
def __init__(self): | |
pass | |
def addMoney(self): | |
return 10 | |
def removeMoney(self): | |
return -10 | |
def parse(self, y): | |
return self.parseAux(raw_input(), y[1]) | |
def parseAux(self, x, env): | |
if x == u'Add Money': | |
return self.addMoney(), env | |
elif x == u'Remove Money': | |
return self.removeMoney(), env | |
else: | |
return [], env | |
class Eval: | |
def __init__(self): | |
pass | |
def interp(self, y): | |
x, env = y[0], y[1] | |
try: | |
return x + env, x + env | |
except TypeError: | |
sys.exit() | |
class Print: | |
def __init__(self): | |
pass | |
def display(self, y): | |
x, env = y[0], y[1] | |
print x | |
return x, env | |
class REPL: | |
def __init__(self): | |
pass | |
def do(self, env = (False, 0)): | |
r = Read() | |
e = Eval() | |
p = Print() | |
while env: | |
env = p.display(e.interp(r.parse(env))) | |
if __name__ == "__main__": | |
repl = REPL() | |
repl.do() |
ホント大した事がないんですが、それでも重要な事があります。それはこれです。
です。これ、どういう理由なんだか知らないんですが、プライベートメソッドにしちゃうと、Pythonだと子クラスに継承出来ないんですね。色々Pythonに関して調べてみたんですが、どういう理由でこうなってるのか分かりません。
まあそれさえ気を付ければ良い、って事ですね。
ではこれをひな形にして、MVCによるGUI版を作っていきます。
Windowsの場合、PySideをインストールすると、C:\Python27\Lib\site-packages\PySide\にdesigner.exe、つまりQt Designerと言うGUI Builderが入ってますんで、これ使ってサクサクとまずはFrameを作っていきます。
Qt Desingerを起動すると次のようなポップアップが現れます。
Dialog without Buttonsを選んで[Create]ボタンを押します。
左側にあるWidget Boxから、Label、LCDNumber、Button二つをドラックアンドドロップで持ってきて、適当に次のようにDialog上に配置します。
んで、レイアウト調整なんですが、上のツールバーにこの手のボタンが並んでるんですが。
例えばボタン二個をCtrl押しながらマウスで選択して、一番左のボタン(Lay Out Horizontally)とか押すと、ボタン二個がキチンと並んで、かつその状態(ボタン二個が入った大枠)を引き伸ばしたりして大きさ調整出来るんですね。
同様に、Label、LCDNumber、そしてボタンの大枠を全部選択してLay Out in a Gridボタンを押すと次のようになって、また大枠を引き伸ばせます。
Formプルダウンメニューから[Preview...]を選べば、今の状態が実際にどんなもんなのか見る事が出来ます。
ちとLabelが大きすぎるので、Labelを選択して、右側のPropertyからSize Policyを探しだして、Vertical PolicyをFixedにします。
うん、イイ感じになってきました。
LabelがTextLabelと言う名称で左寄せなんで、これをMy Moneyと改名して中央寄せにしたい。その場合は、さっきのように、Labelを選んで、右側のPropetyのTextでTextLabel -> My Moneyと変更、AlignmentのHorizontalをAlignCenterにします。
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のコマンドラインは使いづらいんですよねぇ。パス記述するのがおうおうにして厄介です。
まあ、しょうがないんですが、基本的には完全パス与えて実行した方が良さそうです。例えばこの場合ですと、
でしょうか。オプション引数 -oを忘れないようにしましょう。また、こう言うのがホント気になるんですが、変換後のファイル、元ファイル、の順序です(普通の発想なら元ファイル -> 変換後のファイル、になるんじゃねえの?とか思うんですが、しばしばこう言う「語順」を見かける)。
さて、無事に変換が済めば、PySide用に変換されたpyファイルが出来てる筈です。
ってなわけでこいつを利用してMVCモデルによるGUIアプリケーションを作っていきます。
曰く、
とか言いやがって(笑)。んなもん知るか、っての(笑)。
調べてみたら、Pythonの新スタイルクラス、ってのは2.2以降に出てきた、とか書かれてるんですが、Pythonに触れ出したのは2.4以降なので、全く何のこっちゃ、です(笑)。
まあ、いずれにせよ、Qt自体が多重継承推奨してない模様なんで従いますか。この場合の「従う」ってのは、結果REPLのPrintは捨てる、って事です(爆)。
ポイントは、最初書いた通りControllerのインスタンスを保持してる事、それと、REPLモデルの場合の引数の一部にあたる環境 env をインスタンス変数として持っている事です。
単純な流れとしては、
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から、Label、LCDNumber、Button二つをドラックアンドドロップで持ってきて、適当に次のようにDialog上に配置します。
んで、レイアウト調整なんですが、上のツールバーにこの手のボタンが並んでるんですが。
例えばボタン二個をCtrl押しながらマウスで選択して、一番左のボタン(Lay Out Horizontally)とか押すと、ボタン二個がキチンと並んで、かつその状態(ボタン二個が入った大枠)を引き伸ばしたりして大きさ調整出来るんですね。
同様に、Label、LCDNumber、そしてボタンの大枠を全部選択してLay Out in a Gridボタンを押すと次のようになって、また大枠を引き伸ばせます。
Formプルダウンメニューから[Preview...]を選べば、今の状態が実際にどんなもんなのか見る事が出来ます。
ちとLabelが大きすぎるので、Labelを選択して、右側のPropertyからSize Policyを探しだして、Vertical PolicyをFixedにします。
うん、イイ感じになってきました。
LabelがTextLabelと言う名称で左寄せなんで、これをMy Moneyと改名して中央寄せにしたい。その場合は、さっきのように、Labelを選んで、右側のPropetyのTextでTextLabel -> My Moneyと変更、AlignmentのHorizontalをAlignCenterにします。
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ファイルとしてデザインが保存されます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<ui version="4.0"> | |
<class>dialog</class> | |
<widget class="QDialog" name="dialog"> | |
<property name="geometry"> | |
<rect> | |
<x>0</x> | |
<y>0</y> | |
<width>400</width> | |
<height>300</height> | |
</rect> | |
</property> | |
<property name="windowTitle"> | |
<string>Main View</string> | |
</property> | |
<widget class="QWidget" name=""> | |
<property name="geometry"> | |
<rect> | |
<x>10</x> | |
<y>20</y> | |
<width>381</width> | |
<height>271</height> | |
</rect> | |
</property> | |
<layout class="QGridLayout" name="gridLayout"> | |
<item row="0" column="0"> | |
<widget class="QLabel" name="label"> | |
<property name="sizePolicy"> | |
<sizepolicy hsizetype="Preferred" vsizetype="Fixed"> | |
<horstretch>0</horstretch> | |
<verstretch>0</verstretch> | |
</sizepolicy> | |
</property> | |
<property name="text"> | |
<string>My Money</string> | |
</property> | |
<property name="alignment"> | |
<set>Qt::AlignCenter</set> | |
</property> | |
</widget> | |
</item> | |
<item row="1" column="0"> | |
<widget class="QLCDNumber" name="lcdNumber"/> | |
</item> | |
<item row="2" column="0"> | |
<layout class="QHBoxLayout" name="horizontalLayout"> | |
<item> | |
<widget class="QPushButton" name="pushButton"> | |
<property name="text"> | |
<string>Add Money</string> | |
</property> | |
</widget> | |
</item> | |
<item> | |
<widget class="QPushButton" name="pushButton_2"> | |
<property name="text"> | |
<string>Remove Money</string> | |
</property> | |
</widget> | |
</item> | |
</layout> | |
</item> | |
</layout> | |
</widget> | |
</widget> | |
<resources/> | |
<connections> | |
<connection> | |
<sender>pushButton</sender> | |
<signal>clicked()</signal> | |
<receiver>dialog</receiver> | |
<slot>buttonClicked()</slot> | |
<hints> | |
<hint type="sourcelabel"> | |
<x>123</x> | |
<y>230</y> | |
</hint> | |
<hint type="destinationlabel"> | |
<x>192</x> | |
<y>262</y> | |
</hint> | |
</hints> | |
</connection> | |
<connection> | |
<sender>pushButton_2</sender> | |
<signal>clicked()</signal> | |
<receiver>dialog</receiver> | |
<slot>buttonClicked()</slot> | |
<hints> | |
<hint type="sourcelabel"> | |
<x>290</x> | |
<y>227</y> | |
</hint> | |
<hint type="destinationlabel"> | |
<x>353</x> | |
<y>261</y> | |
</hint> | |
</hints> | |
</connection> | |
</connections> | |
<slots> | |
<slot>buttonClicked()</slot> | |
</slots> | |
</ui> |
んで、まずはこいつを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ファイルが出来てる筈です。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
# Form implementation generated from reading ui file 'c:\Users\cametan\Documents\Python\project\MVCtest\ui_mvctest.ui' | |
# | |
# Created: Mon Jan 27 07:30:54 2014 | |
# by: pyside-uic 0.2.15 running on PySide 1.2.1 | |
# | |
# WARNING! All changes made in this file will be lost! | |
from PySide import QtCore, QtGui | |
class Ui_dialog(object): | |
def setupUi(self, dialog): | |
dialog.setObjectName("dialog") | |
dialog.resize(400, 300) | |
self.widget = QtGui.QWidget(dialog) | |
self.widget.setGeometry(QtCore.QRect(10, 20, 381, 271)) | |
self.widget.setObjectName("widget") | |
self.gridLayout = QtGui.QGridLayout(self.widget) | |
self.gridLayout.setContentsMargins(0, 0, 0, 0) | |
self.gridLayout.setObjectName("gridLayout") | |
self.label = QtGui.QLabel(self.widget) | |
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) | |
sizePolicy.setHorizontalStretch(0) | |
sizePolicy.setVerticalStretch(0) | |
sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) | |
self.label.setSizePolicy(sizePolicy) | |
self.label.setAlignment(QtCore.Qt.AlignCenter) | |
self.label.setObjectName("label") | |
self.gridLayout.addWidget(self.label, 0, 0, 1, 1) | |
self.lcdNumber = QtGui.QLCDNumber(self.widget) | |
self.lcdNumber.setObjectName("lcdNumber") | |
self.gridLayout.addWidget(self.lcdNumber, 1, 0, 1, 1) | |
self.horizontalLayout = QtGui.QHBoxLayout() | |
self.horizontalLayout.setObjectName("horizontalLayout") | |
self.pushButton = QtGui.QPushButton(self.widget) | |
self.pushButton.setObjectName("pushButton") | |
self.horizontalLayout.addWidget(self.pushButton) | |
self.pushButton_2 = QtGui.QPushButton(self.widget) | |
self.pushButton_2.setObjectName("pushButton_2") | |
self.horizontalLayout.addWidget(self.pushButton_2) | |
self.gridLayout.addLayout(self.horizontalLayout, 2, 0, 1, 1) | |
self.retranslateUi(dialog) | |
QtCore.QObject.connect(self.pushButton, QtCore.SIGNAL("clicked()"), dialog.buttonClicked) | |
QtCore.QObject.connect(self.pushButton_2, QtCore.SIGNAL("clicked()"), dialog.buttonClicked) | |
QtCore.QMetaObject.connectSlotsByName(dialog) | |
def retranslateUi(self, dialog): | |
dialog.setWindowTitle(QtGui.QApplication.translate("dialog", "Main View", None, QtGui.QApplication.UnicodeUTF8)) | |
self.label.setText(QtGui.QApplication.translate("dialog", "My Money", None, QtGui.QApplication.UnicodeUTF8)) | |
self.pushButton.setText(QtGui.QApplication.translate("dialog", "Add Money", None, QtGui.QApplication.UnicodeUTF8)) | |
self.pushButton_2.setText(QtGui.QApplication.translate("dialog", "Remove Money", None, QtGui.QApplication.UnicodeUTF8)) |
ってなわけでこいつを利用してMVCモデルによるGUIアプリケーションを作っていきます。
残念なお知らせ
当初の予定では、CLIのアプリをREPLモデルとして作る -> 各クラスとGUI部品を多重継承で受け取ったMVC部品を作ってく、って予定だったんですが、なんと、PySideが多重継承させてくれないんですよ(苦笑)。何でやねん、とか怒ってたんですが(笑)。曰く、
新スタイルクラスじゃないと多重継承出来ません
とか言いやがって(笑)。んなもん知るか、っての(笑)。
調べてみたら、Pythonの新スタイルクラス、ってのは2.2以降に出てきた、とか書かれてるんですが、Pythonに触れ出したのは2.4以降なので、全く何のこっちゃ、です(笑)。
まあ、いずれにせよ、Qt自体が多重継承推奨してない模様なんで従いますか。この場合の「従う」ってのは、結果REPLのPrintは捨てる、って事です(爆)。
View
と言うわけで、まずはViewを作っていきます。Viewのコードは以下の通り。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
import sys, repl | |
from PySide import QtCore, QtGui | |
from ui_mvctest import Ui_dialog | |
from repl import Read, Eval | |
class View(QtGui.QWidget): | |
def __init__(self, parent = None): | |
# GUI部品の初期化 | |
super(View, self).__init__(parent) | |
self.ui = Ui_dialog() | |
self.ui.setupUi(self) | |
self.initUI() | |
# 環境保持 | |
self.env = 0 | |
self.c = Controller() # Controller のインスタンスを持つ | |
def initUI(self): | |
self.show() # 本当はここに置かなくても構わない | |
def buttonClicked(self): # Button が click() された時に結ばれてるスロット | |
sender = self.sender() # これがシグナル | |
x, env = self.c.parse(sender.text(), self.env) # シグナルの中身のテキストと、環境 env を Controller の parse メソッドに渡す | |
self.ui.lcdNumber.display(x) # LCDNumber は display メソッドで表示する | |
self.env = env # インスタンス変数 env を書き換えておく |
ポイントは、最初書いた通りControllerのインスタンスを保持してる事、それと、REPLモデルの場合の引数の一部にあたる環境 env をインスタンス変数として持っている事です。
単純な流れとしては、
- ボタンがクリックされると buttonClicked() メソッドが呼び出され、その中でControllerのインスタンスのparse()メソッドが呼び出され(結果Model()のインスタンスが返す)返り値をxとenvに代入する。
- 返り値xはLCDディスプレイの表示に使われる。
- 返り値envはインスタンス変数envに代入される。
です。
今回はシンプルだったんですが、基本的にはボタン押されたと同時にController(翻ってはModel)からの返り値利用して表示までこぎつけるようにした方が良いでしょうね。ここで色々分けてしまうと(つまり、別の何かに手渡す感じにする)、GUIのコード的には相当ワヤクチャになってしまうでしょう。
Controller
Controllerのコードは以下の通りです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Controller(Read): | |
def __init__(self): | |
Read.__init__(self) # Read クラスを継承して初期化する | |
self.m = Model() # Model クラスのインスタンスを保持する | |
def parse(self, x, env): # Read の parse をオーバーライドする | |
if x == u'Add Money': | |
return self.m.interp(self.addMoney(), env) # Model のインスタンスメソッド interp を呼び出す | |
else: | |
return self.m.interp(self.removeMoney(), env) |
ControllerもModelのインスタンスを持っています。ここではparseメソッドをオーバーライドしてますが、特徴としては、ReadはreturnしてたトコをModelのインスタンスからinterpメソッドを呼び出して引数を与えてるトコですね。まあ、見た通りReadのコードより(継承も相まって)相当単純化されています。
まあ、今回はシンプルなんですが、こう言う感じで、例えばViewが複数の情報を送るような場合、View側から適切なControllerインスタンスのメソッドを呼び出し、Controller側でも入力に応じたメソッドを用意しておく、と言うようなカタチにすれば複雑なプログラムにも対処出来るんじゃないでしょうか。かつ、ここではやっぱ「計算自体は」行いません。あくまでModelに対するプロトコル形式を選択出来るカタチに注力すべきだと思います。
これも継承の為相当シンプルになっています。まあ、このアプリケーションのケースだと、元々Eval自体もシンプルなんですが、GUIの場合余計な入力は最初っから入ってこない前提なんで(特にボタンとかはそう)殆ど書くのはハナクソですね。
もう一回書いておきますが、
つまり、Model自体は表面的には見えないで隠されてるカンジになりますね。
ってなわけで REPL -> MVC のコンバートは完了です。以下にMVCの全コードを置いておきます。
まあ、今回はシンプルなんですが、こう言う感じで、例えばViewが複数の情報を送るような場合、View側から適切なControllerインスタンスのメソッドを呼び出し、Controller側でも入力に応じたメソッドを用意しておく、と言うようなカタチにすれば複雑なプログラムにも対処出来るんじゃないでしょうか。かつ、ここではやっぱ「計算自体は」行いません。あくまでModelに対するプロトコル形式を選択出来るカタチに注力すべきだと思います。
Model
最後はModelです。Modelのコードは以下の通りです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Model(Eval): | |
def __init__(self): | |
Eval.__init__(self) # Eval を継承して初期化する | |
def interp(self, x, env): # Read の interp をオーバーライドする | |
num = x + env | |
return num, num # 普通に return して構わない(これは View に返り値として利用される) |
これも継承の為相当シンプルになっています。まあ、このアプリケーションのケースだと、元々Eval自体もシンプルなんですが、GUIの場合余計な入力は最初っから入ってこない前提なんで(特にボタンとかはそう)殆ど書くのはハナクソですね。
もう一回書いておきますが、
View が Controller のインスタンスからメソッドを呼び出し -> Controller は Model インスタンスからメソッドを呼び出し -> Model インスタンスが計算結果を返す -> View が結果を知るって流れなんですが、恐らくこのように見えるでしょう。
View が Controller のインスタンスからメソッドを呼び出す -> すぐ返り値が返ってくる
つまり、Model自体は表面的には見えないで隠されてるカンジになりますね。
Main
ZetCodeのPySideチュートリアルに依ると、main関数を作って、こう書くのが「お約束」の模様です(笑)。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def main(): | |
app = QtGui.QApplication(sys.argv) # アプリケーションオブジェクト作成 | |
v = View() # View のインスタンス作成 | |
sys.exit(app.exec_()) # app.exec_() がイベントループを司ってるらしい(良く知らん・笑) | |
if __name__ == '__main__': # お馴染み! | |
main() |
ってなわけで REPL -> MVC のコンバートは完了です。以下にMVCの全コードを置いておきます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
import sys, repl | |
from PySide import QtCore, QtGui | |
from ui_mvctest import Ui_dialog | |
from repl import Read, Eval | |
class View(QtGui.QWidget): | |
def __init__(self, parent = None): | |
super(View, self).__init__(parent) | |
self.ui = Ui_dialog() | |
self.ui.setupUi(self) | |
self.initUI() | |
self.env = 0 | |
self.c = Controller() | |
def initUI(self): | |
self.show() | |
def buttonClicked(self): | |
sender = self.sender() | |
x, env = self.c.parse(sender.text(), self.env) | |
self.ui.lcdNumber.display(x) | |
self.env = env | |
class Controller(Read): | |
def __init__(self): | |
Read.__init__(self) | |
self.m = Model() | |
def parse(self, x, env): | |
if x == u'Add Money': | |
return self.m.interp(self.addMoney(), env) | |
else: | |
return self.m.interp(self.removeMoney(), env) | |
class Model(Eval): | |
def __init__(self): | |
Eval.__init__(self) | |
def interp(self, x, env): | |
num = x + env | |
return num, num | |
def main(): | |
app = QtGui.QApplication(sys.argv) | |
v = View() | |
sys.exit(app.exec_()) | |
if __name__ == '__main__': | |
main() |
ついでだから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を作ります。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from distutils.core import setup | |
from glob import glob | |
import py2exe, sys | |
# 目的の dll があるパスを明示する | |
sys.path.append("C:\\Program Files (x86)\\Microsoft Visual Studio 9.0\\VC\\redist\\x86\\Microsoft.VC90.CRT") | |
# じゃないと、ここでパスを明示しても py2exe が dll を拾ってきてくれない | |
data_files = [('Microsoft.VC90.CRT', \ | |
glob(r'C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\redist\x86\Microsoft.VC90.CRT\*.*'))] | |
setup( | |
windows = [{'script':'c:\プロジェクトフォルダ\mvc_test.py'}], | |
data_files = data_files | |
) |
上のようなsetup.pyを作ると、自動生成される実行形式をぶち込んだフォルダ(distフォルダ)にdllが入ってるフォルダをコピペしてくれます。これでMicrosoftの規約は守れるようになるわけですね。
そしてその後、
- DOS窓でプロジェクトフォルダに移動する
- 「python プロジェクトフォルダ/setup.py py2exe」 をDOS窓で走らせる
とすればプロジェクトフォルダ内に新しくbuildとdistと二つのフォルダが作られます。distフォルダにめでたくWindows実行形式が生まれていますね。
まあ、こんなカタチでやっとこさ、GUIアプリの実行形式を作るまで辿り着きました。お疲れ様です。
0 件のコメント:
コメントを投稿