2018/03/01

wxPython & XRC で Custom Frame を作る方法2種+α

2018/03/04 追記
よく調べてみたところ、より使い勝手のよい方法が公式に存在したので書き直しました。
[wxPython & XRC で Custom Frame を作る方法3種 | 穀風](https://kokufu.blogspot.com/2018/03/wxpython-xrc-custom-frame-3.html)
@wxPython 4.0.1 (Phoenix) UI が複雑になってくると、Custom Frame や Custom Panel を作ってコンポーネント化したくなります。 wxPython では `wx.Frame` や `wx.Panel` を継承した class を作ることでこれを実現できますが、どうもコードベースで実装している例が多いようです。 しかし、View は分離しておきたいもの。特に複雑なUIの場合はなおさらです。 そこで、[XRC](https://wiki.wxpython.org/XRCTutorial) を使って Custom Frame (Panel) を作る方法を調べてみました。 今回は、例として、Button が一つ中央にある Custom Frame を作ってみます例が全く複雑なUIじゃない!
基本的にはXRCを使った Custom Frame の作り方は以下の2通りあるようです。 ただ、基本の使い方だと少々使いづらいので、私は少し使い方を変更して使っています。最後にそちらも紹介します。 - subclass を使う - XmlResourceHandler を使う ### subclass を使う方法 まずは XRC でレイアウトを作成しますUIは複雑じゃないけど、XMLは既に複雑。エディタを使わなければ意味無いですね。。 この時、トップレベルの wxFrame に `subclass` という属性を設け、そこに Custom Frame 名を入れます。
```xml `highlight: 3; title: "MyFrame.xrc"; <?xml version="1.0" ?> <resource> <object class="wxFrame" name="top" subclass="main.MyFrame"> <object class="wxBoxSizer"> <orient>wxVERTICAL</orient> <object class="sizeritem"> <object class="wxBoxSizer"> <object class="sizeritem"> <object class="wxButton" name="button"> <label>Click me!</label> </object> <flag>wxALIGN_CENTRE_VERTICAL</flag> </object> <orient>wxHORIZONTAL</orient> </object> <option>1</option> <flag>wxALIGN_CENTRE_HORIZONTAL</flag> </object> </object> </object> </resource> ``` Python コードでは `subclass` に設定したクラスを作成します。 このクラスは XRC で指定したクラスの子クラスにします。 上記の例だと `wx.Frame` です。 XRC によって生成されたリソースへのアクセスは Window 生成完了後でなければなりません。 そのため、`EVT_WINDOW_CREATE` を登録します。 ```python `highlight: [5, 10]; title: "main.py"; import wx import wx.xrc as xrc class MyFrame(wx.Frame): def __init__(self, *args, **kw): super().__init__(*args, **kw) # window create イベント登録 self.Bind(wx.EVT_WINDOW_CREATE, self.on_create) def on_create(self, event): # window create 後でないとリソースへのアクセスはできない self.button = xrc.XRCCTRL(self, 'button') self.button.Bind(wx.EVT_BUTTON, self.on_click) def on_click(self, event): print("clicked") def main(): app = wx.App(False) # 以下の処理を wx.App を継承したクラスに書いてあるものが多いが、見やすさ重視で res = xrc.XmlResource('MyFrame.xrc') frame = res.LoadFrame(None, "top") frame.Show() app.MainLoop() if __name__ == '__main__': main() ``` この方法、Python と XML に相互依存があって、私はあまり好きになれません。 ### XmlResourceHandler を使う方法 `XmlResourceHandler` を使用する方法の場合、`subclass` を指定する必要はありません。
```xml `highlight: 3; title: "MyFrame.xrc"; <?xml version="1.0" ?> <resource> <object class="wxFrame" name="top"> <object class="wxBoxSizer"> <orient>wxVERTICAL</orient> <object class="sizeritem"> <object class="wxBoxSizer"> <object class="sizeritem"> <object class="wxButton" name="button"> <label>Click me!</label> </object> <flag>wxALIGN_CENTRE_VERTICAL</flag> </object> <orient>wxHORIZONTAL</orient> </object> <option>1</option> <flag>wxALIGN_CENTRE_HORIZONTAL</flag> </object> </object> </object> </resource> ``` `MyFrame` クラスは先のものと全く変わりません。 そのかわり、`xrc.XmlResourceHandler` を継承したクラスを作成し、 `InsertHandler` で登録します。 この、`xrc.XmlResourceHandler` は XML から node をインスタンス化する方法を提供するものです。 `CanHandle` で特定の node を指定し、`DoCreateResource` でインスタンス化します。 なお、複数の `xrc.XmlResourceHandler` を登録することが出来ますが、場合によっては順番が重要なので注意が必要です`InsertHandler` は先頭に挿入します。 ```python `highlight: [12, 39]; title: "main.py"; import wx import wx.xrc as xrc class MyFrameXmlHandler(xrc.XmlResourceHandler): def CanHandle(self, node): # DoCreateResource を呼ぶ条件を指定 # 今回の場合、wxFrame だけで大丈夫だが、現実的には名前のチェックも入れたほうが良い return self.IsOfClass(node, "wxFrame") and node.GetAttribute('name') == 'top' def DoCreateResource(self): frame = MyFrame(None) # XML から子クラスを生成してぶらさげる self.CreateChildren(frame) return frame class MyFrame(wx.Frame): def __init__(self, *args, **kw): super().__init__(*args, **kw) # window create されてからリソースアクセスするためにイベント登録 self.Bind(wx.EVT_WINDOW_CREATE, self._on_create) def _on_create(self, event): self.button = xrc.XRCCTRL(self, 'button') self.button.Bind(wx.EVT_BUTTON, self._on_click) def _on_click(self, event): print("clicked") def main(): app = wx.App(False) res = xrc.XmlResource('MyFrame.xrc') # ここで InsertHandler を呼ぶのがポイント res.InsertHandler(MyFrameXmlHandler()) frame = res.LoadFrame(None, "top") frame.Show() app.MainLoop() if __name__ == '__main__': main() ``` 相互依存は無くなりましたが、XMLリソースの指定や `xrc.XmlResourceHandler` の設定が `MyFrame` の外で行われているのはやはりイマイチです。 ### 我流に修正 個人的には、以下のようにインスタンスを生成して `Show` するだけで使用できるべきだと思います。 ```python app = wx.App(False) frame = MyFrame(None) frame.Show() app.MainLoop() ``` というわけで、理想の形になるよう修正してみました。 ポイントは `MyFrameXmlHandler` を `__init__` の中で実装していることです。 これにより、自身を `xrc.XmlResourceHandler` 中で扱えるので、XLM の指定からインスタンス生成までを `MyFrame` クラス内に閉じる事が出来ます。 また、副次的効果として、リソースへのアクセスが `__init__` 内で行えるようになりました。 ```python `highlight: [9, 16, 17]; title: "main.py"; import wx import wx.xrc as xrc class MyFrame(wx.Frame): def __init__(self, *args, **kw): super().__init__(*args, **kw) this_frame = self class MyFrameXmlHandler(xrc.XmlResourceHandler): def CanHandle(self, node): return self.IsOfClass(node, "wxFrame") and node.GetAttribute('name') == 'top' def DoCreateResource(self): self.CreateChildren(this_frame) return this_frame res = xrc.XmlResource('MyFrame.xrc') res.InsertHandler(MyFrameXmlHandler()) # 以下を呼ぶことで XML からインスタンス化される res.LoadFrame(None, "top") # __init__ 内でリソースアクセス出来る self.button = xrc.XRCCTRL(self, 'button') self.button.Bind(wx.EVT_BUTTON, self._on_click) def _on_click(self, event): print("clicked") def main(): app = wx.App(False) frame = MyFrame(None) frame.Show() app.MainLoop() if __name__ == '__main__': main() ```

0 件のコメント: