2018/03/04

wxPython & XRC で Custom Frame を作る方法3種

@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 の作り方は私が調べた限りだと以下の3通りあるようですとりあえず今のところ。 後述しますが、後半2種は使い勝手が悪いので、基本的には Two Stage Creation を使うと良いでしょう。 - Two Stage Creation を使う - subclass を使う - XmlResourceHandler を使う 今回使用する XRC を以下に示しますUIは複雑じゃないけど、XMLは既に複雑。エディタを使わなければ意味無いですね。。 3種でほぼ同じですが、subclass を使う場合だけ少し変更します。
```xml `title: "gui.xrc"; highlight: [3, 9]; <?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> ``` ### Two Stage Creation を使う方法 > ここでは wxPython 4.0.1 (Phoenix) での方法を書きます。3以前に関しては以下を参照してください。 > > [TwoStageCreation - wxPyWiki](https://wiki.wxpython.org/TwoStageCreation) Two Stage Creation とは、Window インスタンスの作成時に parent が決まっていない場合、後でUIを生成する方法です。[wxWidget](https://www.wxwidgets.org/) ではコンストラクタのオーバーロードによって実現しています。 wxPython 3 系以前では特殊な関数を用意することで同じことを実現していましたが、wxPython 4 (Phoenix) 以降ではより本家に近い実装になったとのこと。 > 参考 > > - [wxPython Project Phoenix Migration Guide — wxPython Phoenix 4.0.1 documentation](https://docs.wxpython.org/MigrationGuide.html#phase-create) > - [XRC and two stage creation in Phoenix - Google グループ](https://groups.google.com/forum/#!topic/wxpython-users/KNCAAYJKpQU) > - [Phoenix compatibility in pywxrc by acolomb · Pull Request #726 · wxWidgets/Phoenix](https://github.com/wxWidgets/Phoenix/pull/726/files) Python コードでは親クラス(wx.Frame クラス)を継承したクラスを作成します。この時、親のコンストラクタを呼ぶ際に、`parent` を指定しないのがポイントです。 これにより、後の `LoadFrame()` 時までUI の生成を遅らせる事が出来ます。 ```python `title: "gui.py"; highlight: [34, 37]; # -*- coding: UTF-8 -*- import wx import wx.xrc as xrc __res = None def get_resources(): global __res if __res is None: __init_resources() return __res def __init_resources(): global __res __res = xrc.XmlResource() __res.Load("gui.xrc") class MyFrame(wx.Frame): def pre_create(self): """ クラスの初期化時に呼ばれる関数 window の生成前に呼ぶべきカスタムセットアップを記述してください。 SetWindowStyle() や SetExtraStyle() 等。 """ pass def __init__(self, parent): # 引数なしのコンストラクタを呼ぶ wx.Frame.__init__(self) self.pre_create() # ここで self.Create() が呼ばれる get_resources().LoadFrame(self, parent, "top") ``` 以下のように使用します。 ```python `title: "main.py"; #!/usr/bin/env python3 # -*- coding: UTF-8 -*- import wx from gui import MyFrame if __name__ == "__main__": app = wx.App(False) frame = MyFrame(None) frame.Show() app.MainLoop() ``` このサンプルコードを最初に見た時、`__res` 等は `MyFrame` の static 属性にした方が良いのではないか?と思いました1クラス、1ファイルと考えてしまうのは他の言語の悪い影響。 しかし、これは複数のオブジェクトを1モジュール内に記述することを想定して、このようになっているのです。 具体的な例は以下を参照してください。 > [wxPython & XRC でパネルを動的追加する方法 with Two Stage Creation | 穀風](https://kokufu.blogspot.com/2018/03/wxpython-with-two-stage-creation.html) ### subclass を使う方法 この方法では XRC に1点変更を加えます。 以下のように wxFrame の `subclass` 属性に Custom Frame 名が入ります。 ```xml `highlight: 3; title: "gui.xrc"; <?xml version="1.0" ?> <resource> <object class="wxFrame" name="top" subclass="gui.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 によって生成されたリソースへのアクセスは Window 生成完了後でなければなりません。 そのため、`EVT_WINDOW_CREATE` を登録します。 ```python `highlight: [7, 12]; title: "gui.py"; # -*- coding: UTF-8 -*- import wx import wx.xrc as xrc class MyFrame(wx.Frame): def __init__(self, *args, **kw): wx.Frame.__init__(self, *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") ``` 使用方法は以下です。 クラスの外からリソースファイルを指定しなければなりません。 ```python `title: "main.py"; #!/usr/bin/env python3 # -*- coding: UTF-8 -*- import wx import wx.xrc as xrc if __name__ == "__main__": app = wx.App(False) # 以下の処理を wx.App を継承したクラスに書いてあるものが多いが、見やすさ重視で res = xrc.XmlResource("gui.xrc") frame = res.LoadFrame(None, "top") frame.Show() app.MainLoop() ``` この方法、Python と XML に相互依存があるという問題があります。 また、`EVT_WINDOW_CREATE` 後でないとリソースアクセスできないのも少々使い勝手が悪いです。 ### XmlResourceHandler を使う方法 この方法では XRC を変更する必要はありません(`subclass` を指定する必要はありません)。 `MyFrame` クラスは `subclass` を使用するものと全く同じです。 そのかわり、`xrc.XmlResourceHandler` を継承したクラスを作成し、 `InsertHandler` で登録します。 この、`xrc.XmlResourceHandler` は XML から node をインスタンス化する方法を提供するものです。 `CanHandle` で特定の node を指定し、`DoCreateResource` でインスタンス化します。 なお、複数の `xrc.XmlResourceHandler` を登録することが出来ますが、場合によっては順番が重要なので注意が必要です`InsertHandler` は先頭に挿入します。 ```python `highlight: [14, 41]; title: "gui.py"; # -*- coding: UTF-8 -*- 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): wx.Frame.__init__(self, *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") ``` 使用方法は以下です。 この方法もクラスの外からリソースファイルを指定しなければなりません。 ```python `title: "main.py"; #!/usr/bin/env python3 # -*- coding: UTF-8 -*- import wx import wx.xrc as xrc if __name__ == "__main__": app = wx.App(False) res = xrc.XmlResource("gui.xrc") # ここで InsertHandler を呼ぶのがポイント res.InsertHandler(MyFrameXmlHandler()) frame = res.LoadFrame(None, "top") frame.Show() app.MainLoop() ``` 相互依存は無くなりましたが、XMLリソースの指定や `xrc.XmlResourceHandler` の設定を `MyFrame` の外で行わないといけないのは、やはりイマイチです。 また、`EVT_WINDOW_CREATE` 後でないとリソースアクセス出来ない使い勝手の悪さもあります。

0 件のコメント: