PythonのクラスをGUIで扱いやすいようラップ
既存のクラスのGUIを書こうとすると既存コードを弄る必要がある件
GUIプログラムを書く際に、ユーザの入力に対応して内部の状態を変更して、GUIを手動で更新みたいのは面倒すぎるので、内部の状態を更新するとUIは自動で更新される仕組みにしたい、と思うわけですが、それをやろうとすると結局既存のコードに手をいれる必要があって。めんどくさいなーーと思ったりします。
悪いパターン
obj.handleEvent() if val == 1 and obj.isActive(): btn1.enabled = True btn2.enabled = False else: btn1.enabled = False btn2.enabled = True
良いパターン
obj.handleEvent() # UIは自動で更新
既存クラスのメソッド呼び出しをフックする
そこで簡単な解決方法として、メソッド呼び出しをラッパー経由で行い、呼び出しをフックしてコールバックする仕組みを考えました。GUI向けに書かれてないクラスに対してイベントハンドラを注入する感じ。
具体的には、__getattr__で属性値アクセスをフックして、インスタンスメソッドなら、インスタンス等に対する参照と、コールバック呼び出しとメソッド呼び出しを含むクロージャを返します。
この仕組みを使えば、既存のコードに変更を加える事なく、新しくラップクラスを個別に定義することもなく、綺麗にGUIに取り込めそう。
もともとはC#とIronPythonをうまく連携させられないか考えて書いたクラスでしたが他のGUIプラットフォームでも使えそうな感じ。
# coding: utf-8 # modelproxy.py import inspect class ModelProxy: def __init__(self, object): self.__dict__["_object"] = object self.__dict__["_handlers"] = {} def addHandler(self, methodname, callback): if methodname in self._handlers: self._handlers[methodname].append(callback) else: self._handlers[methodname] = [callback] def update(self, methodname): for callback in self._handlers.get(methodname, []): callback(self._object) def __getattr__(self, name): obj = self.__dict__["_object"] attr = getattr(obj, name, None) if inspect.ismethod(attr): fun = attr def gun(*args, **kwargs): ret = fun(*args, **kwargs) self.update(name) return ret return gun else: return attr
サンプルとして、wxPythonとTkinterで、fooモジュールにあるBinaryStateクラスの状態に応じて、複数のGUIコントロールを自動で更新するコードを書いてみました。
foo.py
class BinaryState: def __init__(self): self.state = 0 def flip(self): self.state = 1 - self.state def getState(self): return self.state
Tkinterの場合
tktest.py
# coding: utf-8 # see: http://www.pythonware.com/library/tkinter/introduction/ from Tkinter import * import modelproxy from foo import BinaryState class App: def __init__(self, master): frame = Frame(master) frame.pack() self.btn = Button(frame, text='change state') self.btn.bind('<Button-1>', lambda e: self.bs.flip()) #### self.btn.pack() self.btn2 = Button(frame, text='hoge') self.btn2.pack() self.svr = StringVar() self.lbl = Label(frame, textvariable=self.svr) self.lbl.pack() # create model proxy self.bs = modelproxy.ModelProxy(BinaryState()) # set handler self.bs.addHandler('flip', self.updateLabel) #### self.bs.addHandler('flip', self.updateVisible) #### self.bs.update('flip') def updateLabel(self, e): self.svr.set(['off', 'on'][self.bs.getState()]) def updateVisible(self, e): self.btn2.config(state = [DISABLED, NORMAL][self.bs.getState()]) def main(): root = Tk() app = App(root) root.mainloop() if __name__ == '__main__': main()
wxPythonの場合
wxtest.py
# coding: utf-8 import wx import modelproxy from foo import BinaryState class Frame(wx.Frame): def __init__(self): wx.Frame.__init__(self, None, -1, 'wx', size=(200, 150)) self.pnl = wx.Panel(self) self.btn = wx.Button(self.pnl, -1, 'change state') self.btn.Bind(wx.EVT_BUTTON, lambda e: self.bs.flip()) #### self.btn2 = wx.Button(self.pnl, -1, 'hoge', pos=(0, 30)) self.lbl = wx.StaticText(self.pnl, -1, '', pos=(20, 70)) self.bs = modelproxy.ModelProxy(BinaryState()) self.bs.addHandler('flip', self.updateLabel) #### self.bs.addHandler('flip', self.updateVisible) #### self.bs.update('flip') def updateLabel(self, e): self.lbl.SetLabel(['off', 'on'][self.bs.getState()]) def updateVisible(self, e): if self.btn2.Enabled: self.btn2.Disable() else: self.btn2.Enable() def main(): app = wx.PySimpleApp() Frame().Show() app.MainLoop() if __name__ == '__main__': main()
これを使えば、実行中のPythonプログラムの状態を動的に表示していくプログラムを簡単に書いたりできるだろうなーとか妄想。