見出し画像

Retro-gaming and so on

Pythonで作るハングマン GUI編

前回は端末(DOS窓等)で遊べるCLI版ハングマンを作ってみた。
今回はそれをGUI版にする。
とは言っても、もう「ゲームロジック」を実装する必要はない。前回キチンとゲームロジックを評価部(Eval)としてinterp関数を実装した。
単純に言うと、GUIのガワを作ってinterp関数とやり取りする仕組みさえ作ればGUI版ハングマンが作れる、って事になる。言い換えるとCLI版に於ける読み込み部(Read)と印字部(Print)を捨てて、その辺をGUIに置き換えればいい、って事だ。

前回指針とした、「全てのCLIアプリの制作指針の基礎」をREPL(Read-Eval-Print Loop)と呼んだ。
一方、「全てのGUIアプリの制作指針の基礎」をMVC(Model-View-Controller)と呼ぶ(※1)。
単純に言うと、

  • Model: REPLのEvalに当たる
  • View: REPLのPrintに当たる
  • Controller: REPLのReadに当たる
と言う対応があって、ぶっちゃけ内情はREPLと全く同じだ。

前回のREPLでは
while True:
 env = display(interp(input(), env))
としてルーピングしてたが、全く同じ構造だ、と分かるだろう。
しかしながら、通常、GUIでのM->V->C->...と言うルーピングをイベントループと呼ぶ(※2)。
敢えてREPLとMVCの「プログラミング上の違い」を挙げるとすると、REPLのRead、Eval、Printは全て値を返す、つまりreturnしてた。
一方、MVCは、元々このモデルを開発したSmalltalkの流儀に従ってModelもViewもControllerもクラスで、returnする代わりにメッセージ送信を行う(※3)。
Modelと言うオブジェクト、Viewと言うオブジェクト、Controllerと言うオブジェクトは全てメッセージを送信する。大事な事なんで2回言った。そしてこれがGUIライブラリによっては厄介なトコなんだ(※4)。

と言うわけで今回の準備。
まずGUI版ハングマンを作るために画像を用意しよう。前回やった通り失敗最大回数が10回、っつーことは首吊りが完成するまでの10枚の画像が必要だ。絵心がある人はGimp辺りを使って描けばいいだろう。サイズは200x200としておき、また画像フォーマットをpngとしておこう(この辺は極論好きにしていい)。そしてそれらをワーキングディレクトリ上にimgと名付けたフォルダを作ってぶち込んでおく。
次にメッセージ送信の為のライブラリ(PyPubSub)を導入する。もう一つ、今回はPython備え付けのGUIライブラリであるtkinterではなく、もっとキレイなwxPythonを使用する。
いや、本当は、久しぶりにtkinterを使おうか、と思ったんだけど、こっちの環境だけ、なのか画像を上手く読み込めない。Pillowを使ってもダメだった。ってんで諦めたわけだ。
加えるとtkinterのベースになってるTcl/tkは超簡単にGUIのガワを作れるツールではあるけど汚い。一方、wxPythonのベースになっているwxWidgetsはルックアンドフィールが良い。PythonのIDLEの「見た目」とRacketのIDEの「見た目」の違いはそれを表してるんだ。IDLEはTcl/tkで書かれててRacketのIDEはwxWidgetsで作られている。


PythonのIDLE(左)とRacketのIDE(右)。PythonのIDLEは非常に便利ではある。が、昔よりマシにはなったけど汚い。と言うより、Tcl/tkの性質上、WindowsからもLinuxからもMacからも浮いてる(これはTcl/tkが元々、UNIXの低レベルなGUIの礎、XWindowやそのガワであるMotifをベースにしてる為)。
一方、RacketのIDEはwxWidgetsを使用してて、使ってるOSのルックアンドフィールから違和感が無いようなGUIを生成する。

wxWidgetsはC++で書かれていて、Python専用のGUIライブラリではない。tcl/tkも色んな言語からバインディングが提供されているが(Python組み込みのtkinterもその一つ)、wxWidgetsもそれに劣らないくらいだろう。C++、Perl、Lisp、JavaScriptなんかでもバインディングが提供されているので、他の言語に移動した際にも使える可能性が高い。
また、数多くのオープンソースのGUIアプリもwxWidgetsによって作られている。
まずはこれら2つのライブラリをpipなんかを使ってインストールしておこう。

pip install pypubsub wxpython

と端末で打てばインストール出来る筈だ。

ここでちと、PyPubSubの使い方を見てみよう。とは言っても、基本的な機能はメッセージ送信(sendMessage)とメッセージ受信側の設定(subscribe)の2つしかない。そして基本的にはマニュアルに書かれている例が全てだ。



subscribeはこの例では関数listener1と宛名(rootTopic)を結びつける。それが設定で、sendMessageは「宛名」に向けてメッセージ(この例だとarg1=123arg2=anObj)を送る。そうすれば「宛名」と結び付けられた関数が実行されるわけだ。


これが基本的な使い方、だ。
ただ、送り先が階層構造を持ってた場合・・・つまりクラスがあって、それにメソッドが属してて、メソッドに送りたい場合、宛名を宛名.メソッドとしなければならない。アパートに住んでる住人に手紙を送る際には「アパートの誰それ」が宛名になるのと同じだ。





これだけ、だ。
あと、sendMessageで送るメッセージは必ずキーワード引数になるんだけど、そのキーワードには送信先の関数/メソッドの仮引数名を使う、って事だけ押さえておこう。

次にもう一つ武器をインストールする。
前回、GUIは定型的なコードばっか書く割にはクソ長くてつまらん、と書いた。
一方、Visual BasicやC#、あるいはDelphiなんかのWindowsでのプログラミング言語処理系だとそれこそGUIでGUIを作れる、と言う話を書いた。
そのテのGUIでGUIを作るツールを古くはRAD(Rapid Application Development)、最近ではGUIビルダと呼ぶ。
問題はこのテのGUI ビルダと言うのがUNIX/Linux生まれのプログラミング開発環境に無いのか、っつー話だ。実はあるんだ(笑)。
しかしながら、UNIX/Linux系言語だと、Windowsみたいに言語と開発環境が統合されてないケースが多い、ってのが一つ。そういう意味ではVB/C#、あるいはDelphiより使い勝手は落ちるわけだ。
もう一つの理由ってのが、UNIX/Linux系のプログラマは「何が何でもテキストでプログラミングしたい」ってヘンなこだわりを持ってるヤツがやたら多いんだよ(笑)。大学の先生とか(笑)。だから、GUIビルダーとか「Windows臭いプログラミング手法」を不必要に嫌ってる人が多い。結果そのテの情報が広まらない、ってぇのがあるんだ。マジで「意識高い」系の行動はイミフだったりする(苦笑)。
ここではクソみたいなこだわりは捨て、wxWidgets用のGUIビルダであるwxGladeを導入する(※5)。っつーか俺は特にこだわりはねぇけどな(笑)。


wxGlade。左上部に「貼り付ける為の」パーツ(ウィジェット)が揃っている。
左下部はウィジェットのプロパティ(例えば画面サイズとか)の入力・表示領域、そして右半分はGUIのツリー構造を表示する領域になっている。

wxGladeの使い方については、古い記事だけどここに使い方が書かれているんで、ザーッと読んできて欲しい。見た目は違ってきてるが、基本的な使い方は全く同じだ。
wxWidgetsの基本は大体次のようになる。

  • ウィンドウの基底はFrameになる。
  • Frameの上に何かを直接は置けない。Widgetは必ずBoxSizer/GridSizerと言うパーツの上に置く。
  • Sizerが形成する一つの領域には(何であれ)一つのWidgetしか置けない。
ある意味、wxWidgetsの特徴はsizerが握っている、と言えるだろう。
通常、GUIビルダの場合、適当な位置にマウスで「部品」(ウィジェット)を置いてドラッグして位置調整をしたりするが、原則wxWidgetsはそういう操作はしない。
そうじゃなくって、Sizerが形作る「領域」にウィジェットをはめ込んだあとは勝手に適した状態に調整してくれる、ってのがwxWidgetsの特徴となる。
言わば、ある意味融通は利かないんだけど、その代わり細かい調整をしなくてもwxWidgets任せで何とかなる、と言うわけだ。
例えば大雑把に、ハングマンを次のようなGUIデザインにする事としよう。

そうすると、基底をFrameとして、まず上下二分割にしたBoxSizerを置いて、上半分に左右二分割にしたBoxSizerを置き、更に右半分にアルファベットを格納出来るだけのGridSizerを置く、と考える・・・。
また、下半分は上半分とカラーリングを変えたいんで、まずは色設定したPanelを置いて、更にその上には、文字、リセットボタン、終了ボタンを置きたいんで左中右と三分割したBoxSizerを置いて・・・と考える。
結果としてSizerの配置は次のようになるだろう。



この状態でツリー構造は(長くなるからGridSizerは閉じてるが)次のようになる。


次に左上側に画像を置きたいのでsizer_2のSLOT 0にはStatic Bitmapを置き、下段には左から文字情報、ボタン、ボタン、と置きたいので、sizer_3のSLOT 0にはStatic Text、残りにはButtonを置く、と。



あとはGridSizerの部分にボタンを当てはめていくわけだが・・・。うん、GUIでの操作って文章で説明するのはメンドくせぇな(笑・※6)。いずれにせよ、ガワの完成形はこんなカンジになる。



アルファベットのボタンがありすぎるんでGridSizerの内訳は省略するが、ツリー構造はこんなカンジだ。



この状態で保存すると、デフォルトではwxglade.wxgと言うファイル(実態はxmlファイル)が生成される。取り敢えずそれをここに挙げておこう。手元のwxGladeで読み込めば再現される筈だ。
さて、言わずとも分かるだろうが、wxGladeで作ってる部位は基本的にはMVCのViewに当たる(※7)。そこで元々frame(MyFrame)と言った名称をView(View)に変えたわけだが。
ここからは暫くこのテのプロパティ(Property)、つまりボタンのサイズとか、上で見たような「名称変更」等、どこをどう設定していくのか、箇条書きにしていく。
なお、PropertyはwxGladeの左下のエリアで弄くれる。


Application:
  • Application: 必ずしも必要ではないが、NameとClassをHangmanとする。Keep user codeにチェックを入れる。
View:
  • Common: NameとClassをViewとする。
  • Widgets: Titleを「ハングマン」にする。ウィンドウを固定サイズにしたいのでStyleの項目のwxMAXIMIZE_BOXとwxRESIZE_BORDERのチェックを外す。
Panel_1:
  • Common: Backgroundを#3299ccに。
Label_1:
  • Common: Sizeを200, 100にする。
基本的にサイズ調整やらカラーリング、名称設定等をPropertyでは行う。
残りはまずはボタン類なんだけど、

ボタンA〜Z(GridSizerに乗ってるヤツら):
  • Common: NameをA〜Zとする。sizeを50, 28とする。
  • Widget: LabelをA〜Zとする。
ボタン「リセット」&「終了」(sizer_3に乗ってるヤツら):
  • Common: Nameを「Reset」と「Quit」とする。Disabledにチェックを入れる。
  • Layout: AllignmentのwxALIGN_CENTRAL_VERTICALにチェックを入れる。
  • Widget: Labelを「リセット」及び「終了」とする。
ここでDisabledと言うチェック項目は、ボタンをinactive、つまりクリック出来ない状態にする事だ。
「リセット」ボタンと「終了」ボタンは、ゲームに勝つにせよ負けるにせよ一旦終わった時に限ってactiveにしたい。また、ハングマンのゲーム途中で「既に入力したアルファベット」のボタンはinactiveにするギミックが欲しいので後でその機能を付けよう。
なお、GridSizeは7x4で28個スロットがあるが、アルファベットは26文字だ。2つ余る。その「余りを埋めるパーツ」をSpacerと言う。WidgetのStatic項目の右端にあるんで確認しておくこと。

さて、これで全ウィジェットの名前やサイズ、位置調整等の設定が終わった(と思う・・・抜けが無けりゃ・笑)。
あとは、追加コードとボタンにまつわるイベントハンドラを設定する。
まずはGUIコードに対する単純な追加コードから。ViewのPropertiesのCodeタブを開く。



ここのExtra (import) code for this widgetに追加コードを記述する。具体的には必要とするライブラリや、Viewで表示する為のメッセージ、そしてView以外のModel、Controllerと言ったクラスを書いてしまおう。
こんなコードを書き込む。

from pubsub import pub
from hangman import dictionary, count_max, Env, initialize, interp

msg = {"lossmsg": 'あなたの負けです! 正解: \n\t{}',
   "playmsg": '当てる言葉 : \n\t {}',
   "successmsg": '正解!あなたの勝ちです。\nおめでとう。'}

class Controller(object):
 def __init__(self):
  pub.subscribe(self.read, "Controller")
 def read(self, x, env):
  pub.sendMessage("Model.eval", x='y' if x == 'リセット' else 'n'
          if x == '終了' else x, 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))

まず、メッセージをやり取りするpypubsubモジュールをimportする。これが無きゃ話にならない。
そして当然、CLI版ハングマンのファイルから英語辞書であるdictionary、許容誤答回数であるcount_max、環境設定のEnv、環境を初期化するinitialize、そしてゲームエンジンのinterpimportする。言っちゃえば表示部関連以外は軒並み「持ってきた」と言っていい。

そして今回のGUI版ハングマン用の文字表示用データを作る。シンプルにそれらは3種類だ・・・文字で全部説明する必要がなく、写真を使ったり何なりするんで、この辺はCLI版よかシンプルになる傾向がある。

Controllerは先にも書いたが、REPLでのRead代わりで入力にまつわる制御をするクラスだ。後で見るが、リセットボタンと終了ボタンは「リセット」と言う文字列と「終了」と言う文字列をメッセージとして送り込んでくるが、interp関数はゲーム終了時に"y"か"n"かでゲーム継続かゲーム終了かを判定してた。従って、ここでメッセージの内容を変換してModelに送ろうとしてるわけだな。ゲーム中ならViewが送ってきたクリックされたボタンが何か、と言うのをそのままModelへと送る。ついでにViewからやってきた環境envを送るのも忘れずに。

ModelはREPLで言うEvalにあたるクラスだ。と言うか、ここではEvalであるinterp関数をクラスでラップしてる、って言って良い。Controllerから送られてきた「メッセージ」(つまり何のボタンが押されたのか、と環境情報の2つ)はそのままinterp関数へevalメソッドを経由して手渡され、そこでゲーム進行に関する計算を行い、環境情報として返す。それをViewへと「メッセージ送信」する。
一見ややこしく見えるかもしれないけど、コード自体は簡単だし、CLI版を書くより(と言うより利用してる以上)「やること自体は」ここでは簡単だ。

ここで出来る残りは、ボタンをクリックする事に関する「動作定義」を書くことで、こういう関数をイベントハンドラと呼んだりする(※8)。いや、単純に言うと、ボタンがクリックされた事が検知されたらControllerへと「xxが押されたよ!」と言うメッセージを環境と合わせて送ればいい。
wxGladeではそのテのイベントハンドラの設定は、ボタン毎のPropertiesのEventsタブ内で設定出来る。


例えばボタンAに対してのイベントハンドラは、

lambda event: pub.sendMessage("Controller.read", x=self.A.GetLabel(), env=self.env)

だし、ボタンBに対してのイベントハンドラは、

lambda event: pub.sendMessage("Controller.read", x=self.B.GetLabel(), env=self.env)

だ。・・・・・・分かるね?コピペと修正だ(爆
なお、後で見てみれば分かるけど、self.Aself.B、・・・ってのは全部ボタンの変数名だ。そしてメッセージとして送られてるのはボタンの「Label名」って事になる・・・。単純に言うと"A"とか"B"って文字列だな。
また、これも後で分かるけど、実は今んトコself.envって言う変数もどこにも定義されてない。それはPythonファイルをGenerateしてから修正して付け足す。
いずれにせよ、ここで一旦Saveしよう。
そしてwxGladeの【File】から辿って【Generate Code】を選ぶ。



そうすれば、デフォルトだとwxglade_out.pyと言うPythonプログラムが生成される。今からそいつを編集するんだけど、直接編集するんじゃなくってコピーしたファイル(例えばファイル名をwxhangman.pyとかにして)を編集しよう。
いずれにしても、生成されたファイルはこんなカンジだ。如何にも「機械で自動生成しました!」ってカンジだろ(笑)。
では、wxhangman.pyを修正していく。先程、ボタン関係のイベントハンドラで使ったself.envと言う変数がまだ存在しない、って言った。よってそれを付け足す。

class View(wx.Frame):
 def __init__(self, *args, **kwds):
  self.env = initialize() # 環境を初期化
  # begin wxGlade: View.__init__
  ......

こうやって「自動生成されたコード」の範囲外で書いていく。
次に、現時点ではまだViewがメッセージを受け取る仕組みがない。従ってViewのケツ辺りにそのコードを追加する。

  ......
  # end wxGlade
  pub.subscribe(self.SetProperties, "View") # Viewがメッセージを受け取る仕組み

Modelからの送信先は"View.SetProperties"だったんでこうしてる。つまり、追加すべきメソッドはSetPropertiesと言う名前になる。

  def SetProperties(self, env):
   self.env = env

ここでModelから渡されたenvself.envに代入する。・・・残念!関数型プログラミングがここで崩れた(笑)!
まぁ、しょーがねぇよな。
さて、環境が更新された後、何をせんとアカンのか?
そうだね、画像の更新と表示文字の更新だ。
まずは画像の更新なんだけど・・・こう書きたいわけだよな。



ここではimgフォルダ内にあるハングマン用の画像の名称をgallows1.png、gallows2.png、...って事にしてる。CLI版だと文字列テンプレートとカウンタが連動してたが、ここでは画像番号とカウンタを連動させようとしてる。SetBitmapはwxPythonで画像をすげ替えるコマンドだ。
ところがこれは上手く動かない。と言うのもself.bitmap_1って変数が定義されてないからだ。
実はViewの__init__で定義されてるのはself.bitmap_1じゃなくって単なるbitmap_1だ。そして、メソッドSetPropertiesからは単なるbitmap_1にはアクセス出来ない。スコープ外、だからだよ。
よって、__init__内でのbitmap_1を全部self.bitmap_1に書き換えなければならない。じゃないと画像が固定されたままで大変楽しくなくなるわけだ。

  # この辺selfを付け足して修正
  self.bitmap_1 = wx.StaticBitmap(self, wx.ID_ANY,
wx.Bitmap(f"img/gallows{self.env.count}.png", wx.BITMAP_TYPE_ANY))
  sizer_2.Add(self.bitmap_1, 0, 0, 0)

この辺、自力で探さなくてもいいだろう。IDLEの置換機能(【Edit】-> 【Replace】)で何とかなる範疇だと思う。
そして表示文字列に関しても同様の問題がある。
ゲーム中、あるいはゲーム終了時の条件を考慮してこう書きたい(SetLabelは文字列を置き換えるwxPythonでの命令だ)。



しかしやっぱりself.label_1っちゅう変数は存在しない。
従って、__init__内のlabel_1self.label_1へと置換する。

  # self を付けて label_1 を self.label_1 へと修正
  self.label_1 = wx.StaticText(self.panel_1, wx.ID_ANY, msg['playmsg'].format(self.env.Word))
  self.label_1.SetMinSize((200, 100))
  sizer_3.Add(self.label_1, 0, 0, 0)

さて、先程、self.Aself.B、・・・と言うのは全部「ボタンの変数名」だと言う話をした。そこで次のようなリストをメソッド内に作ってみる。

  buttons = [self.A, self.B, self.C, self.D, self.E, self.F, self.G,
       self.H, self.I, self.J, self.K, self.L, self.M, self.N,
       self.O, self.P, self.Q, self.R, self.S, self.T, self.U,
       self.V, self.W, self.X, self.Y, self.Z]

一回使われたアルファベットを示すボタンはinactive、つまり押せない状態にしたい。要は、ボタンのラベル情報が環境のインスタンス変数、Guessedに含まれているかどうか調べればいい。
そのギミックはこう書ける。

  [i.Enable(i.GetLabel().lower() not in self.env.Guessed)
   for i in buttons]

ボタンがactiveな状態はボタン.Enable(True)、ボタンがinactiveな状態はボタン.Enable(False)で表現出来る。従って、ボタンのラベルを小文字化したブツがGuessedに含まれてるかどうか調べるだけでボタンを簡単にactive/inactive化する事が出来る。あとは一気にリスト内包表記で処理すればいい。
リセットボタン/終了ボタンに付いても同様だ。buttonリストを定義しなおして、現在の状態がゲーム終了状態を満たしてるかどうか調べるだけでボタンをactive/inactive化する事が出来る。

  buttons = [self.Reset, self.Quit]
  [i.Enable(self.env.count == count_max or
      self.env.theWord == self.env.Word) for i in buttons]

これでSetPropertiesメソッドを書き終えた。ゲーム進行によっての環境変化、つまり現時点でのself.envの状態を見ながらGUIに於ける「表示」を変更している。まさしくREPLでのPrintの役割を担っている。

残りは、だ。ちと自動生成されたwxhangman.pyのケツの部分を見てみよう。

class Hangman(wx.App):
 def OnInit(self):
  self.View = View(None, wx.ID_ANY, "")
  self.SetTopWindow(self.View)
  self.View.Show()
  return True

# end of class Hangman

if __name__ == "__main__":
 Hangman = Hangman(0)
 Hangman.MainLoop()

さてさて、一体これは何をやってんのか。
平たく言うとこれは全部、CLI版の次の部分の「代用」なんだ。

if __name__ == '__main__':
 env = display(initialize())
 while True:
  env = display(interp(input(), env))

そう、両者ともループしてる。前者は最初に書いたGUI用のイベントループを行うコードだ。
ただ、前者が厳しく見えるのは、関数型言語的な観点から見ると「わざわざループ用のクラスを作って」、それをインスタンス化した上に「ループ用メソッドを呼び出してる」ように見えるから、だ。うん、ぶっちゃけその通りなんだわ(笑)。
クラスHangmanはイベントループ(MainLoop)なんかのGUIの基礎機能を詰め込んだwx.Appクラスを継承してる。そしてViewクラスを使ってwx.AppのOnInitメソッドをオーバーライドする。ここはあくまで、wx.Appを継承したHangmanクラスを初期化してるんじゃなくって、Viewなんかの「プログラム上の部品」を初期化して、ぶん回す相手として指定してるわけだ。
っつー事は、だぜ?いずれにせよ、ここが「ループさせる対象を指定する場所」だと言う事になる。現時点、ControllerクラスもModelクラスも使われていない。よって「ループさせる対象を指定する場所」な以上、ここがControllerクラスのインスタンスとModelクラスのインスタンスをぶち込む場所だ、ってこった(※9)。

class Hangman(wx.App):
 def OnInit(self):
  self.View = View(None, wx.ID_ANY, "")
  self.Controller = Controller() # Controller クラスのインスタンス
  self.Model = Model()    # Model クラスのインスタンス
  self.SetTopWindow(self.View)
  self.View.Show()
  return True

これで終了、だ。
CLIなREPLを書き慣れた人だとREPLみたいにModel、View、Controllerが「組み合ってない」辺りに不安を覚えるかもしんない。実際、これらのインスタンスは「独立してるように」見えて互いに全く関係ないように思われる。
ただし、そこがメッセージ通信なんだ。今までCLIのREPLではお互い引数と返り値を用いて直接やり取りしてたが、メッセージ通信なら独立して存在してるオブジェクト同士の間で「メッセージが飛ばせる」。そこがこのMVCと言うモデルの実装上のキモ、と言えばキモになるわけだ。互いに直接接して無くても「電波のように」メッセージを飛ばし合うわけだ(まぁ、実際は、「飛ばし合う」と言うよか指向性通信だけどな)。
いずれにせよ、これで終了。あとはGUI版ハングマンで遊ぶだけ、だ。



wxhangman.pyと言うソースコードはここに置いておこう。また、パッケージ一式はここに置いておく。Python、wxPython、PyPubSubがインストールされていれば、ダウンロードして適当な場所で解凍しても遊べる筈だ。

以上。

※1: 基礎と言う割には「最近流行ってる」、オブジェクト指向関連での「デザインパターン」だと思われている。
事実は違って、元々最初のGUIを装備したプログラミング言語環境、Smalltalk(1980年登場)の開発に使われたモデルだ。
ちなみに、Appleの故・スティーヴ・ジョブスがXeroxのパロアルト研究所で見かけてMacintoshの開発動機になったのが、このSmalltalkだ(ジョブスはSmalltalkをOSだと勘違いした)。

※2: REPLもMVCも基本的には同じ仕組みだし、ループもイベントループも元々のアイディアが同じなのに、何で違う名前を付けるの?と不思議に思うかもしんない。
こういう「理論と用語の整理」と言うのがコンピュータサイエンスではほぼ行われないし、基本的にコンピュータ系の用語はバズワードを元に誕生する、と言うひっじょーにしょーもない背景がある(苦笑)。
つまり、理学と違って「何らかの定義をキチンと決めて」用語設定をするのではなく、「何となくカッコいいから」と言う理由で単語が生まれ、それがそのまま普遍化して使われるわけだ。

※3: しかし、メッセージ送信、と言う行為が原理的にはreturnと変わらない、と看破したのが、何度も言うがSchemeとその作成者、ガイ・スティールとジェリー・サスマンの2人だ。
理論的背景は例によって「SchemeとActor理論」を参照の事。アクターを「オブジェクト」と読み替えればイイ。

※4: 理論バリバリのハッカーが作ったGUIツールなんかでは、Smalltalkよろしくメッセージ送信を扱うsendが実装されてたりする。

ちなみに、何故にMVCが「長い間、基礎なのにも関わらず忘れ去られていたのか」と言うと、それこそVisual BasicやTurbo PascalなんかのGUIビルダを含めた開発環境登場の弊害だろう。
このテの「GUIでGUIを作る」モデルの場合、極論、イベントループもクソも関係なく、GUIのガワに「単独で存在するスクリプト群」がぶら下がってるような格好になる。結果、「全体で何かを統一的に構成する」ようなプログラムにはならない。
言い換えるとGUIビルダ自体が「構造化スパゲティコード」を書くようなツールになっていて、またそのスタイルを推奨し、拡散されるようになっていったわけだ。
結果、このように「素人でも簡単にGUIアプリが作れる」環境を整えた弊害により「簡単にスパゲティコードを量産出来る」状態になり、全体を俯瞰的に捉えるパースペクティヴを喪ってきた。
また、正直言うと、現状あるLinux系のGUIツールキットの多くはWindows謹製のGUIビルダの影響を多大に受けてて、「スパゲティコードを書く為の」ツールとしてデザインされている。それが殆どのGUIツールキットでMVCを素直に構成出来ない理由だ。

なお、本当の意味でGUIをキチンと理解したい場合、Smalltalkの理解は欠かせないんじゃないかな、とは思う。僕もよう知らんが(笑)、やはり原点確認は欠かせないだろうと「わたしのゴーストが囁くのよ」(謎
現代では、Smalltalk方言Squeakが簡単にSmalltalkを試せる環境となる。

※5: このwxGladeの存在が、今回wxPython使用に舵を切ったもう一つの理由だ。
tkinterにもGUIビルダが無いわけではないが、tcl/tkはwxWidgetsに比べると記述が簡単なせいで、tkinter用のGUIビルダの登場自体がwxのそれより遅い。かつ、新しいせいもあって、かなりバギーだ。
一方、wxGladeの方が歴史があり、結果安定してるGUIビルダを提供してるのはwxの方なんだ。

※6: Linuxを使い出した時、どの解説サイトを見てもコマンドラインでの説明しか書いてなかったんで辟易したモンだが、実は「端末にコマンドを打て」と言ってるのではなくて「コピペしろ」って意味なのが分かった時が衝撃だった。
そう、一見ユーザーとしてはGUIは便利なんだが、「GUIを文章で説明する」ってのは至極厄介なんだ。
CLIだったら「ブログに書かれているコマンドラインを」コピーして端末にペーストすれば「同じ動作になる」と言うのが保証されるわけで、それが分かった時「なるほどな」と思ったわけだ。

※7: 厳密に言うと、例えばボタンを「表示」するのはViewの役目だが、じゃあ、「入力を受け取った」と「知る」のはViewの役目じゃなくってControllerであるべきじゃないか?と言うような話がある。
この辺が実は混乱の元で、また巷の「MVCの紹介コード」がやたら「神経質に分けよう」として明快さを欠いて複雑化する原因になってると思う。
ここではあくまでREPLからの延長線上のアプローチ、と言う立場を貫き、このモデルでいい、としておく。

※8: つまり、「ボタンをクリックした」事で起動するイベントハンドラは原理的には一種の「高階関数へ渡す引数としての関数」だと捉える事が出来る。なお、そのテの引数側になる関数をコールバック関数と呼ぶ事がある。

※9: 実はこの部分はこう書いてもいい。

class Hangman(wx.App):
 def OnInit(self):
  self.View = View(None, wx.ID_ANY, "")
  self.SetTopWindow(self.View)
  self.View.Show()
  return True

# end of class Hangman

if __name__ == "__main__":
 Hangman = Hangman(0)
 c = Controller() # Controller のインスタンス
 m = Model()  # Model のインスタンス
 Hangman.MainLoop()

REPLに慣れているとReadに当たるControllerのインスタンスとEvalに当たるModelのインスタンスがイベントループに含まれてない事に違和感を感じるだろう。
一方、それらは互いに独立してるオブジェクトで、メッセージを送ったり受けたりするだけ、ってのが一つ。また、View以外は本体に「破壊的変更をするべき」データを保持していない。要は更新する必要がない。
結果、この2つはプログラム的には「データを加工してメッセージとして送信」するだけのフィルタのような存在なんで、ルーピング内部に存在しなくても構わないわけだ。
本文中ではREPLの構造との類似性を強調してイベントループ内に仕込んだが、それは実はプログラムとして細かく見ると、必然性が必ずしもあったわけじゃない。
  • Xでシェアする
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

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

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