見出し画像

Retro-gaming and so on

PythonでMVCによるGUI2例

さて、ここでGUIツールキットであるwxPythonを使った簡単なGUIアプリ(っつーかゲームだが)の実装例を紹介した。そこで紹介した「GUIアプリの基本的な構築法」をMVC(Model-View-Controller)と呼んだ。
MVCはCLIでのインタプリタの原理、REPL(Read-Eval-Print Loop)さえ分かっていればその構造のままGUIへと持ってこれるアプリ構築法だ。恐らくこれが一番簡単な「アプリのGUI化」だろう。ただし、より構造が簡単なCLIのアプリ自体が書けないとお話にならない。まぁ、当たり前の話だな。
実は件の例だとCLI版はそれなりにお題としては難しく、「いきなりGUI版を作ろう」とすればそこそこ頓挫しやすいブツだった。いや、MVCだろうとなかろうと、GUI版をいきなり作ろうとすればいずれにせよ頓挫しやすい。GUIは「計算自体とは本質的には関係がないコード」が多すぎて、作成者自身がロジックを見失いやすいわけだ。
世の中には「簡単にGUIが構築出来ます!」と言うような紹介をしてるサイトやら書籍なんかが多いけど、実際はんなに簡単なモンじゃない。やっぱり敷居が高いんだよ。
CLIアプリが簡単に書けないようなヤツは、少なくともUNIX生まれの言語処理系を相手にしてる以上、GUIには手を出さない方が賢明だとは思う。
ここではCLIアプリをベースとしたGUIアプリの例を二例程挙げようと思う。

一例目は良くある「GUIはこうやって簡単に作る事が出来る」と言う例なんかで出される典型的なブツを扱う。
問題は、次のようなカンジだ。

初期値を0とし、Add Moneyボタンをクリックすると10を足し、Remove Moneyボタンをクリックすると10を引いた数を表示するプログラムを書け。

ロジック的には非常に簡単なプログラムなんだけど、「ボタンをクリックして表示せよ」つー事がお題をクッソめんどいプログラムにする。その割にはぶっちゃけ、クソの役にも立たないプログラムだ(笑)。
このように「GUIを簡単に書く事が出来る」と主張するプログラム例は、大体クソの役にも立たないし、立たない割にはコード量「だけは」多い。
一方、CLIでこれを書くことは簡単だろう。標準入力でAdd Moneyを受け取れば10加算し、Remove Moneyを受け取れば10減算する。REPLを使う程でもねぇプログラムだが、ぶっちゃけ「この程度のCLIのプログラムも書けない」と言うのなら貴方はプログラミングに向いていない。
このプログラムのCLI版は例えばこのようになるだろう。特に難しくはないが、ここでeval(interp関数)の構成法に一つ新しいテクニックを導入する。
「環境」(Envクラス)にpackageと言うインスタンス変数が導入され、そこにこのプログラムで行う「evalの動作」(つまり「足したり」「引いたり」する関数)がローカル関数として定義され、なおかつ辞書型/ハッシュテーブルの値として詰め込まれているのが分かるだろうか。
この形式のプログラミング法をデータ駆動/主導型プログラミングと呼び、その応用だ(※1)。
evalは一般に、複雑な条件分岐の塊なんだけど、この形式だとeval本体は至極簡単なモノとなる。正直言うと、この程度のプログラムだとあまり旨味が感じられないが、evalがデカくなりそうだとこのスタイルは結構効いてくるし、「この程度の簡単なプログラムだからこそ」意味は明快にはなるだろう。
evalはここでは次のようになっている。

# Eval
def interp(x, env):
 return Env(env.package, env.package[x](env))

ここでは、単純に言うと、入力(x)から渡された文字列をキーとして、packageと名付けられた辞書型から値(関数)を探してきて、それを渡された環境(env)に適用して計算結果としてる。
結果、eval自体は辞書型への橋渡しとしてだけ存在する事となり、小さくて済むわけだ。

「あれ、でもevalが小さくなっても今度はpackageがデカくなるんじゃない?」

と不安に思う人もいるだろう。正解だ。その通りだ。
確かに、evalが持ってた複雑さをinstall_packageが肩代わりしただけだ、と言っちゃえばそれだけだ。
しかし、そのメリットはデカいんだよ。何故なら、install_packageに何か追加する場合でも、形式(引数と返り値)は合わさなアカンが単独の小さい関数自体を追加するこたぁ簡単だ。プログラミング上、条件分岐を弄くる方がめんどくさいんだわ。デカいevalのような複雑な条件分岐の塊の途中を弄くる、とか考えただけでアタマが痛くなってくんだろ?
また、この例だとevalの「中身」はinstall_package上、全部ローカル関数として書いたが、別に外に出してフツーの独立した関数としてもいいし、install_package自体を「キーと関数を追加出来る」関数として拡張しても良い。
いずれにせよ、こっちの形式の方が拡張性、っつーかメインテナンス性は高くなるわけだな。
Pythonプログラミングの上達の目安、っつーかコツは第一にリスト内包表記を使いこなせる事。第二は、これは別にPythonに限った話じゃないが、辞書型/ハッシュテーブル/連想配列等を徹底的に使いこなせる事だ。そして辞書型の値に関数を設定するようなテクニックは、まずC言語脳は教えてくれない。何故なら、C言語脳は「Pythonに於いて関数はファーストクラスオブジェクトだ」と言う事を諳んじても、その意味を分かってないからで、どうしても「C言語で考えてPythonを使う」事から離れられない(※2)。
いずれにせよ、「繰り返し」の次にめんどくさいのは「条件分岐」だ。辞書型を活用すれば条件分岐を排除して「自動で」適したモノを引っ張ってこれるんで、このテのデータ駆動/主導型プログラミングの方が精神衛生上良かったりする。

さて、GUIデザインは例えば次のようにする。


味も素っ気も無いがこの程度でいいだろう。wxGladeで生成されるxmlファイルはこんなカンジで、自動生成されるwxPythonでのガワはこんなカンジになる。
ウィジェットのヒエラルキーはこんなカンジだ。



ここで取り上げるポイントは次の3点だ。
まず1点目。今回は初めてTextCtrlと言うウィジェットを使っている。
これは本来は、「GUI上でテキストを入力する為」のウィジェットだが、今回は敢えて出力用にしていて、入力を受け付けないようにしている。
入力をさせない為には、TextCtrlウィジェットのプロパティ設定部分のCommonでDisabledにチェックを入れる。



2つ目。wxPythonでのGUIのレイアウトの弄り方だけど、各ウィジェットの位置やサイズを数値で直接弄る前に、まずはプロパティのLayoutタブ内の設定項目を活用しよう。



ここで大まかな位置関係やウィジェットの大きさを設定出来る。まずここで大雑把なポジショニングを決めるべきだ。
最後は、Codeタブ内への記述に付いて。
Extra (import) code for this widgetに記述した内容は生成ファイルの先頭の方に書いたコードを付け足してくれる。
一方、Code to be inserted before はウィジェット内での自動生成コードの先頭に、そしてCode to be inserted after はウィジェット内での自動生成コードの後方辺りに書いたコードを挿入する。



これらを上手く使ってウィジェット自体へコードを追加してみよう。

あとは前回やった通り、Viewに多少コードを追加すれば無事GUI版になる。
ここにコードを置いておく。修正/追加箇所にはコメントを追加してある。

さて、何度も言うが、CLI版は簡単に書けるプログラムでもGUI版はコードがクソ長くなる。その「クソ長い部分」をwxGladeで自動生成させて手間を減らそう、ってのが戦略なわけだが。
次の例はもうちょっと役に立ちそうなプログラム(※3)で、なおかつGUIで「良くある」インターフェースでの実装例を見てみよう。これがある意味一番ややこしい例となる。
お題は次のようなモノだ。GUI版のデザインは後回しとして、CDの情報を管理する簡単なデータベースを作る。項目は「タイトル」「アーティスト」「レーティング」「リップ」とする。レーティングは自分のそのCDの評価で、リップはそのCDがリッピング済みかどうか、と言う事とする。まぁ、実際この辺はどうでもいいんだけど(笑)。
そしてその「CDのデータベース」をファイルに書き出したり、読み込んだり出来るモノ、としよう。
このデータベースの単純な仕組みは、

  1. 4つのスロット(タイトル、アーティスト名、レーティング、リップ)がある構造体(Pythonでは実際はクラスだが)Contentを定義する。
  2. 1のContentを要素としたリストがデータベースとなる。
これだけだ。
なお、Pythonの標準ライブラリの一つであるpickleを使うと、ここで見た「Contentクラスのインスタンスを詰め込んだリスト」を直接バイナリにしてファイルの読み書きが出来る。つまり、作ったデータベースの保存や読み込みが簡単に出来るようになるわけだ。
CLIでの動作を見てみよう。



例えばinsert_valueと言うコマンドでその引数にデータを与えると、データベース本体にデータを取り込む事が出来てるのが分かるだろう。
また、selectと言うコマンドは「データベースの中身を表示する」事も分かる(※4)。
そう、そしてこのプログラムは事実上、小規模の、Pythonとは全く別の言語インタプリタになっている。

ゲームの類では触らなかったし、LispとS式を使えば殆ど万能だったんで無視してたが、通常、言語インタプリタでの読み込み部(read)の重要な役目は、標準入力を受け取り、それを「字句解析」(トーカナイズ)して「構文解析」したデータをevalへと手渡す(※5)。
当然、このプログラムはそこまで複雑な事はしてない、甚だ不完全な事をやってるが、ある程度動作すればエエや、ってぇんで割り切ってる。
例えばこのプログラムの入力の想定は次のような形式の文字列になる、と仮定してる。

'insert_value(千のナイフ, 坂本龍一, 5, Yes)'

これをreadでは次のような文字列のリストに変換したいわけだ。

['insert_value', '千のナイフ', '坂本龍一', '5', 'Yes']

つまり、'('や', 'や'、'や')'を区切り文字として分割すればいい。
これを行うのがPythonの性器正規表現モジュールのreだ。

>>> re.split('\(|,|、|\)', 'insert_value(千のナイフ, 坂本龍一, 5, Yes)')
['insert_value', '千のナイフ', ' 坂本龍一', ' 5', ' Yes', '']

re.splitの第一引数で文字列として「何を区切り文字とするか」を指定する。
これで上手い具合に「文字列をトークンに分割したリスト」が入手出来るが、同時にゴミ、つまり空文字列''が入ってるし、またトークンに空白が含まれる。それらをリスト内包表記でフィルタリングするとこうなる。

>>> [i.strip() for i in re.split('\(|,|、|\)', 'insert_value(千のナイフ, 坂本龍一, 5, Yes)') if i != '']
['insert_value', '千のナイフ', '坂本龍一', '5', 'Yes']

なお通常、プログラミング言語実装の場合、ここから構文木を作る構文解析に入るが、このプログラムの場合はそれは必要ない。readが返す「トークンのリスト」の第0要素が「コマンド名」で、それ以外は「引数」だと決め打ちしてるから、だ。

evalは先の例に従って、データ駆動/主導型プログラミングによって作る。
今回のevalはこうだ。

# Eval
def interp(x, env):
 return env.package[x[0]](x[1:], env)

xはreadから受け取ったトークンのリストで、第0要素が「コマンド名」だった。
と言う事は、データ駆動/主導型プログラミングの仕組みから言うと、「そこ」が辞書型である変数packageに与えるキーだと言う事になる。packageはキーに従って関数を返す。結果、その関数を「与えられたトークンのリスト」の第一要素以降のリストとenvに適用すればいい。
なお、このテの辞書型/ハッシュテーブル/連想配列を駆使した「関数の書き方」は一見見づらいかもしんない。がロジックはハッキリしてるだろう。あとは「慣れ」の問題だ。
言っちゃえば、Lisp以降のモダンなプログラミング言語ならフツーのプログラミング手法なんだけど、生憎C言語脳はそれを知らないんで教えてくんない
繰り返すが辞書型/ハッシュテーブル/連想配列を備えた言語で関数がファーストクラスオブジェクトならこういう手法は普遍的な、言わば極フツーの方法論なんだ。
せっかくPythonのようなモダンな言語を扱うんだったら、こういう手法に明るくなるべきだと思う。
「C言語のように」プログラムをPythonで書いてもしょーがない。この2つの言語は全く設計方針が違うんだ。

install_packageには他に、新規ファイル作成(new_file)、ファイルを開く(open_file)、保存(save_file)、ファイルに名前をつけて保存(save_file_as)、データを削除(delete)、終了(quit)等のコマンドを追加しておく。
この辺は解説しないが、特にファイル操作なんかはある意味Pythonでの基本的なプログラムの書き方から逸脱はしてないだろう。全関数の引数と返り値の形式を(アプリを終了するquit以外は)揃える事だけ、は気をつけよう。

CLI版アプリを作り終えたらGUI版に取り掛かる。ここではなるたけ「良くある」GUIアプリでのギミックを導入しよう。
まず、プログラムを立ち上げた際に出てくるウィンドウ(フレーム)は次のようにデザインする。

メニューバーがあって、ボタンがあって、まるでエクセルのような格子がある、と。「良くあるパターン」だ。
ウィジェットの階層構造は次のようになっている。



このガワで新顔のウィジェットには次のようなモノがある。

  • MenuBar: いわゆる「プルダウンメニュー」を作る部品
  • ListCtrl: エクセルのようにデータを格子上に表示する部品
  • Dialog: いわゆる「ポップアップ」にあたる部品
取り敢えず新しく登場したウィジェットの使い方は後回しとして、生成されたxmlファイルPythonファイルをザーッと見て欲しい。大まかにwxGladeを利用して記述したコードを説明していく。
まずはAddCDボタンをクリックしてAddCDDialogがポップアップする仕組みを書く(※6)。


ここで書くイベントハンドラは次のようなモノだ。

lambda event: AddCDDialog(self).ShowModal()

AddCDボタンはViewの一部なので、selfにはViewの情報が渡される。そして、ポップアップを生成するメソッドがShowModalだ。
ShowModalは良くある、ポップアップが生じてる際には本体(View)の操作が出来ない、と言う機能を含んだメソッドとなる。
さて、そのポップアップするAddCDDialogは次のようなデザインだ。


TextCtrlを4つ揃えていて、タイトル、アーティスト、レーティング、リップの情報を纏めて、OKボタンがクリックされた際にそれらをControllerへと送る。
その為のイベントハンドラはこう書く。

まだAddCDDialogに属するOnAddCDメソッドは書いてないが、これでいい。単純にはここでメッセージ送信するだけで良さそうだが、一般にはOKボタンを押せばポップアップは閉じる。つまり2つする事があるんで敢えてメソッドを設計する。これは後に、自動生成されたwxglade_out.pyをコピペしたファイルで追加する項目となる。

def OnAddCD(self):
 pub.sendMessage('Controller.read',
          x = ['OnAddCD', 
            self.text_ctrl_1.GetLineText(0),
            self.text_ctrl_2.GetLineText(0),
            self.text_ctrl_3.GetLineText(0),
            self.text_ctrl_4.GetLineText(0)],
          env = self.env)
 self.Destroy()

GetLineTextはTextCtrlに書き込まれた文字列を取ってくるメソッドだ。それを利用して、'OnAddCDD'と言う「情報」とTextCtrl4つ分の情報をリストで纏め、それをxとし、ViewからAddCDDialogに渡されてきたself情報の中のenvを合わせてControllerにメッセージ送信する。
メッセージ送信したらポップアップを閉じる。このポップアップを閉じるメソッドがDestroyだ。
従って、Cancelボタンのイベントハンドラは単にこのDestroyを使えば簡単に書ける事、となる。


次に、メニューバーで開くファイルダイアログを考える。
メニューバーには次の5つのコマンドを仕込もうとしてる。



この内、保存とQuitは別にポップアップを立ち上げる必要はないが、【新規作成】、【開く...】、【名前を付けて保存...】の3つはファイルダイアログを立ち上げないといけない。
そのため、wxGladeで3つそれぞれ専用のダイアログを作ってるが、実はこのままじゃ役に立たない
ちょっと仕組みを説明するが、フツーのDialogの場合、wxWidgetsで「デザイン」出来るようになっているが、一方、FileDialogはwxWidgetsから見ると「特殊なダイアログ」になっている。
と言うのも、FileDialogはwxWidgetsより下のレイヤー、つまり、OSなりOSが使ってるデスクトップ環境のデフォルトの「ファイル専用のダイアログ」を直接呼び出すようになっている。
つまり、WindowsならWindowsが用意してるファイルダイアログ、LinuxならLinuxが用意してる(GNOMEとかKDEの)ファイルダイアログを直接呼び出す。

Linux(Ubuntu)で用意されているファイルダイアログの例。このファイルダイアログ自体はwxWidgetsで用意されてるモノではなく、それより下のレイヤーであるデスクトップ環境が用意しているモノだ。
WindowsではWindowsが用意しているファイルダイアログを呼び出す。
そのお陰で、OSのルックアンドフィールと違和感が無くなるわけだ。

wxGladeではこのFileDialogを直接扱う事が出来ないんで、Dialogとして作成してる。従って、後でコピーしたファイルで弄くる対象にしないとならない。
とは言っても、wx.Dialogと書かれてる部分をwx.FileDialogへと置き換えるだけ、で済む。
また、デフォルトだと、各ダイアログではこのような記述が見られる。

kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_DIALOG_STYLE

ここのwx.DEFAULT_DIALOG_STYLEと言うのがwxWidgetsでのダイアログのデフォルト形式を決めてるんだが、ここを【新規作成】と【名前を付けて保存...】ではwx.FD_SAVE、【開く...】ではwx.FD_OPENを採用する。
この2つは殆ど同じなんだけど、ファイル名を指定する「テキスト入力欄」があるかどうか、ってのが違いだ。
wxGladeの時点でやれる事は、あとはそれぞれのDialogにExtra (import) Code for this widgetに何を書くか、が1つ目。

NewFile:
def OnNewFile(self):
 with NewFile(self) as fileDialog:
  if fileDialog.ShowModal() == wx.ID_CANCEL:
   return
  pub.sendMessage("Controller.read",
           x = ["OnNewFile", fileDialog.GetPath()],
             env = self.env)
OpenFile:
def OnOpen(self):
 with OpenFile(self) as fileDialog:
  if fileDialog.ShowModal() == wx.ID_CANCEL:
   return
  pub.sendMessage("Controller.read",
           x = ["OnOpen", fileDialog.GetPath()],
           env = self.env)
SaveFileAs:
def OnSaveFileAs(self):
 with SaveFileAs(self) as fileDialog:
  if fileDialog.ShowModal() == wx.ID_CANCEL:
   return
  pub.sendMessage("Controller.read",
           x = ["OnSaveFileAs", fileDialog.GetPath()],
           env = self.env)

これらはメソッドではない。関数だ。
FileDialogはDialogと違ってボタンが独立していない。従ってOKボタンやCancellボタンがクリックされた、と言う事柄自体がFileDialogの返り値になっている。よって、その返り値が何なのか、と言うのを判定する必要が出てくるわけだ。
なお、この3つの関数は殆ど形式は同じで、GetPathメソッドでFileDialogがどのディレクトリ/フォルダ上でポップアップしたのか調べるようになっている。
あとは、Code to be inserted beforeの項目に必ず次のコードを書く。

self.env = args[0].env

これは、各FileDialogが呼び出された際に、初期化時点で大元のViewの情報がargs変数に手渡されるから、だ。args変数は一要素のタプルになっていて、Viewのオブジェクト情報が入ってる。
従って、args変数の第一要素はenvと言うインスタンス変数を持っていて(何故ならそれをViewで設定し、後で見るが、プルダウンメニューのイベントハンドラを経由してselfとして手渡されるから、だ)、それによりControllerに投げるべき環境情報を得る事が出来る。

さて、ここでプルダウンメニューを実装する。
基本的にプルダウンメニュー自体は結果ボタンとそんなに変わらない。先程書いたイベントハンドラ(関数)でファイルダイアログをポップアップさせるだけ、かあるいは(【保存】や【Quit】では)単純にControllerにメッセージ送信するだけ、だ。
wxGladeによるプルダウンメニューの設定画面は次のようになってる。



プルダウンメニューは階層構造になっていて、Addボタンでメニューを追加出来る。
<ボタンと>ボタンはメニュー内容の階層をズラすようになっている。Levelが0のブツはプルダウンメニューの(【ファイル】とか【編集】等の)名前、Levelが1のブツは各「メニュー」に含まれるコマンドの名前となる。
【新規作成】、【開く】、【名前を付けて保存】では先程書いたイベントハンドラをラムダ式で包んで設定する。
【保存】と【Quit】に関しては単にControllerへメッセージ送信すればいい。

【保存】:
lambda event: pub.sendMessage('Controller.read', x = ['OnSaveFile'], env = self.env)
【Quit】:
lambda event: pub.sendMessage('Controller.read', x = ['OnQuit'], env = self.env)

さて、残りは新登場のListCtrlに付いて書いていこう。
このアプリの場合、データベースに登録されたデータを「表示」するのにListCtrlを使う。
例えば次のように表示されるわけだ。


データが一行毎にListCtrlに収められている。
単純に言うと、CLIのContentクラスの内容をListCtrlと対応させて表示させればいいだけ、なんだけど、実の事を言うとListCtrlの扱いがメンド臭い(苦笑)。
wxPythonはPythonで書かれてるが、これはwxWidgetsのラッパーだ。そしてwxWidgets自体はC++で書かれてる。
そしてListCtrlは(Pythonのせいじゃなくって)、下部でC++で書かれたプログラムが動いてる為、「メモリの領域確保」と、それに対するデータの追加、って結構ややこしい事やってんだな。そしてそれが「表面の」wxPythonの書式にも影響を与えている。
ListCtrlはInsertItemで行を確保(この時点ではアイテム一つ・・・Contentクラスのインスタンス変数一個しか持てない)し、SetItemで一行(っつーかそれこそ一つのリスト)にアイテムを追加していく、と言う形式になっている。LispのリストやPythonのリストの延長線上で考えると「クッソめんどい」んだ(笑)。C++のせいだ(笑)。
また、InsertItemで追加したアイテムは自然と列の0番目になり、SetItemでの追加で列の1番、2番、・・・と埋まっていく。
いずれにせよ、ListCtrlがデータ表示に必要な為、wxGladeが自動生成したwxglade_out.pyをコピーしたファイルのViewの項目に、次のメソッドを追加する。これがModelから送られてきたenv変数からデータを抜き出し、書き換えにより表示するメソッドとなる。

def SetProperties(self, env):
 self.env = env
 self.list_ctrl_1.
DeleteAllItems() # ここで一旦表示を全消去する
 [[self.list_ctrl_1.InsertItem(sys.maxsize, obj) if col == 0
  else self.list_ctrl_1.SetItem(index, col, obj) for col, obj
  in enumerate([item.title, item.artist, item.rating, item.ripped])]
  for index, item in enumerate(self.env.ls)]

このメソッドだとMVCのイベントループの周回で、前回表示したヤツを一旦全消去して、self.envからContentのリストを引っ張ってきて改めて表示してる。
多分「無駄だ」と思う人もいるだろうが、結果その方がラクなんだ。
と言うのも、ListCtrlは破壊的変更前提のデータ型で、一旦表示させるといつまでもそこに残ろうとする。結果、Modelが持ってるCLI版のinterp関数の動作と整合性が取れない。intertpは非破壊的に動いてるのに、ListCtrl側で勝手にデータを追加しようとしたり、保持しようとしたり、あるいは削除しようとしたりしたら困るわけだ(そういう機能が実はListCtrlには備わってる)。
出力は出力だけで余計な事をして欲しくない。結果、一旦表示を全消去して改めてデータを読み込んで表示するほうがプログラム上は簡単なんだ。
データの表示ではリスト内包表記とenumerateが活躍する。
外側のリスト内包表記では行と行番号indexを関連させ、内側では列と列番号を関連させる。
列番号が0の時はInsertItemを利用して先頭のobjを一個だけ使って行の領域を確保、他の数の場合は列番号に従ってSetItemを利用して残りのobjをハメていく。
それだけ、だ。

さて、ここでもう一つメンドイ事がある。「データを消去したい」時はどうすんのか?
良くある実装だと、データを選んで「右クリック」して、例えば「消去」を選ぶとデータを削除出来る、とか。そういうギミックが良くあんだろ?
ところがこの実装がメンドい(苦笑)。そもそもそのギミックをwxGladeが用意してないんだ。
よってその機能を実装するには手書きせんとあかん。使うウィジェットはwx.Menuと言う。
これもwxGladeが自動生成したwxglade_out.pyをコピーしたファイルを若干改造する前提になるが、その前に「マウスの右クリック」にイベントハンドラを仕込もう。



マウスの右クリックを感知するイベントはEVT_LIST_ITEM_RIGHT_CLICKだ。ここにrclickと言うメソッド(イベントハンドラ)を仕組む。これはまだ書いてないんで、あとでwxGladeが自動生成したwxglade_out.pyをコピーしたファイルのViewの項目に追加するメソッドだ。
「右クリック」 -> 「メニュー項目選択」 -> 「実行」と言う流れでは、上のようにまずは「マウスの右クリック」と言うイベントが(上の例では)rclickと言うイベントハンドラを呼び出し、rclickと言うイベントハンドラが右クリックによるメニューを生成し、ポップアップを生成する。そこから「メニュー項目選択」と言う別メソッドを呼び出し、そしてControllerへとメッセージ送信する。
この多段構えの為に、実は「右クリック」はヒッジョーに実装がメンドイ(※7)。
右クリックのイベントハンドラrclickと、メニュー項目選択用メソッド、menu_selectは次のように書く。

def rclick(self, event):
 menu_file = wx.Menu()         # メニューを定義
 item = wx.MenuItem(menu_file, 1, '削除') # メニューの選択肢を定義
 menu_file.Append(item)        # 選択肢をメニューに仕込む
 menu_file.Bind(wx.EVT_MENU, self.menu_select) # メニューのイベントハンドラとしてmenu_selectを使用
 wx.Window.PopupMenu(self, menu_file, event.GetPoint()) # ポップアップを生成

def menu_select(self, event):
 id = event.GetId() # 選択肢番号を取得
 if id == 1:
  pub.sendMessage('Controller.read',
           x = ['Delete',
             str(self.list_ctrl_1.GetFocusedItem())],
           env = self.env)

メニューをポップアップとして生成するにはwx.Window.PopupMenuを用いる。ListCtrlの上から(0から数えて)何番目の行が選択されてるのか、と言う情報はGetPointで得られる。
今回は右クリックメニューの選択肢が一個しかないが、複数あった場合に「何番目の選択肢か」知るのがGetIdだ。
GetFocusedItemで「ListCtrlの何番目の情報を選んでるのか」分かる。Controllerにはこれを含めた情報をメッセージ送信する。

これで右クリックから「メニューのポップアップ」で、コマンドを選択/実行出来る



あとは、ViewのExtra (import) code for this widget で必要なライブラリ(CLI版のsimple_databaseファイルを含む)のimportを指定したり、ControllerとModelの定義を書き込めばイイ。

from pubsub import pub
from simple_database import Env, install_package, interp

class Controller(object):
 def __init__(self):
  pub.subscribe(self.read, "Controller")
  self.table = {"OnNewFile": "new_file",
        "OnOpen": "open_file",
        "OnSaveFile": "save_file",
        "OnSaveFileAs": "save_file_as",
        "OnQuit": "quit",
        "OnAddCD": "insert_value",
        "Delete": "delete"}
 def read(self, x, env):
  pub.sendMessage("Model.eval",
           x = [self.table[x[0]]] + x[1:],
           env = env)

class Model(object):
 def __init__(self):
  pub.subscribe(self.eval, "Model")
 def eval(self, x, env):
  pub.sendMessage("View.SetProperties", env = interp(x, env))

この辺はぶっちゃけワンパターンだ。いや、ワンパターンなのはイイことなんだけどな。何故ならフォーマットが決まってると言うのは何事にも替えがたい
ただ、ここのController部分では、初期化時点で送られてきたメッセージの「コマンド」をCLI版の「コマンド」に変換する為の変換テーブルを辞書型で作っている。これを利用して、改めてModelにラップされたinterp関数へと必要とするキーワード引数xをリストとして組み立てて送り込む。
以上だ。最後に完成形のソースコードをここに添付しておく。「改変場所」にはコメントを付加してるので、どこをどう改造/追加していってるのか見ていって欲しい。

※1: 本質的にはここで紹介した「オブジェクト指向でのメソッドの本来の意味」、つまり総称関数とネタ的には全く同じモノだ。
つまり、ここでのテクニックはeval総称関数的なブツを適用する事で条件分岐を消してしまう、と言う事だ。

※2: 正確に言うと、C言語でも似たような事は出来るし、C言語のエキスパートならこういうプログラミング法を知ってるし行っている。
ただし、素のC言語はPythonで言う「辞書型」を持ってないし(つまり、最悪自作せんとアカン)、また「関数がファーストクラスオブジェクトではない」C言語だと、関数ポインタと呼ばれる黒魔術を駆使しないと同等の事が出来ない。
つまり、このテのプログラミング法を行う事は、C言語だと「億劫」になるわけだ。
なお、そもそもC言語脳は「ローカル関数」と言う概念を知らない。Pascalユーザーは知ってるのにC言語脳は知らない、と言うのは、単にC言語にはローカル関数がないから、だ。反面、Pascalにはローカル関数がある。

※3: 役に立たない(笑)。このテのプログラムを作るくらいだったらSQLiteを使った方が実用的にはマシだ。
あくまでプログラミングの練習だ、と捉えて欲しい。

※4: なお、ここでのinsert_valueselect等のコマンド名は、基本的にすべてSQLのパロディ。

※5: 字句解析、と言うのは一番単純には、受け取った文字列をスペースやカッコ等の区切り文字で単語毎にバラバラにする事を指す。また、構文解析、とは字句解析で分解したデータ毎に「意味」をタグ付けして木構造に変換する事だ。
Pythonインタプリタでもユーザーから入力を受け付けた際、読み込み部でこのような事を行っている。

※6: マトモなMVCならAddCDボタンをクリックするとそれをControllerへメッセージ送信し、ControllerがModelにメッセージ送信して、ModelがViewに「ダイアログを立ち上げろ」とメッセージ送信する、と言う形式になるだろう。
しかしここでは、wxGladeを使って大幅にGUIデザインの為のコードを書くのを手抜きしてる、と言う事と、ViewはView内で完結してた方がシンプルになる、と言う判断にしてる。

※7: それだけじゃなく、そもそもwxPython公式サイトでも例示が少なく、しかもそこのドキュメントは事実上wxWidgets公式サイトのブツの丸写しだ。
正直言うと、ここのドキュメントは決して出来が良いモノではなく、Scheme系サイトのブツくらいの酷さで、やっぱLinux系のオープンソースのドキュメントはクソだ、と実感する。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

最近の「プログラミング」カテゴリーもっと見る

最近の記事
バックナンバー
人気記事