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.Framewx.Panel を継承した class を作ることでこれを実現できますが、どうもコードベースで実装している例が多いようです。

しかし、View は分離しておきたいもの。特に複雑なUIの場合はなおさらです。 そこで、XRC を使って Custom Frame (Panel) を作る方法を調べてみました。

今回は、例として、Button が一つ中央にある Custom Frame を作ってみます1

基本的にはXRCを使った Custom Frame の作り方は以下の2通りあるようです。 ただ、基本の使い方だと少々使いづらいので、私は少し使い方を変更して使っています。最後にそちらも紹介します。

  • subclass を使う
  • XmlResourceHandler を使う

subclass を使う方法

まずは XRC でレイアウトを作成します2。 この時、トップレベルの wxFrame に subclass という属性を設け、そこに Custom Frame 名を入れます。

MyFrame.xrc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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 を登録します。

main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 を指定する必要はありません。

MyFrame.xrc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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 を登録することが出来ますが、場合によっては順番が重要なので注意が必要です3

main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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 するだけで使用できるべきだと思います。

1
2
3
4
app = wx.App(False)
frame = MyFrame(None)
frame.Show()
app.MainLoop()

というわけで、理想の形になるよう修正してみました。 ポイントは MyFrameXmlHandler__init__ の中で実装していることです。 これにより、自身を xrc.XmlResourceHandler 中で扱えるので、XLM の指定からインスタンス生成までを MyFrame クラス内に閉じる事が出来ます。

また、副次的効果として、リソースへのアクセスが __init__ 内で行えるようになりました。

main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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()
  1. 例が全く複雑なUIじゃない! 
  2. UIは複雑じゃないけど、XMLは既に複雑。エディタを使わなければ意味無いですね。 
  3. InsertHandler は先頭に挿入します 
?

0 件のコメント: