2018/03/04

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

@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 の作り方は私が調べた限りだと以下の3通りあるようです2。 後述しますが、後半2種は使い勝手が悪いので、基本的には Two Stage Creation を使うと良いでしょう。

  • Two Stage Creation を使う
  • subclass を使う
  • XmlResourceHandler を使う

今回使用する XRC を以下に示します3。 3種でほぼ同じですが、subclass を使う場合だけ少し変更します。

gui.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>

Two Stage Creation を使う方法

ここでは wxPython 4.0.1 (Phoenix) での方法を書きます。3以前に関しては以下を参照してください。

TwoStageCreation - wxPyWiki

Two Stage Creation とは、Window インスタンスの作成時に parent が決まっていない場合、後でUIを生成する方法です。wxWidget ではコンストラクタのオーバーロードによって実現しています。

wxPython 3 系以前では特殊な関数を用意することで同じことを実現していましたが、wxPython 4 (Phoenix) 以降ではより本家に近い実装になったとのこと。

参考

Python コードでは親クラス(wx.Frame クラス)を継承したクラスを作成します。この時、親のコンストラクタを呼ぶ際に、parent を指定しないのがポイントです。 これにより、後の LoadFrame() 時までUI の生成を遅らせる事が出来ます。

gui.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
# -*- 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")

以下のように使用します。

main.py
1
2
3
4
5
6
7
8
9
10
11
12
#!/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 属性にした方が良いのではないか?と思いました4

しかし、これは複数のオブジェクトを1モジュール内に記述することを想定して、このようになっているのです。 具体的な例は以下を参照してください。

wxPython & XRC でパネルを動的追加する方法 with Two Stage Creation | 穀風

subclass を使う方法

この方法では XRC に1点変更を加えます。 以下のように wxFrame の subclass 属性に Custom Frame 名が入ります。

gui.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="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 を登録します。

gui.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# -*- 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")

使用方法は以下です。 クラスの外からリソースファイルを指定しなければなりません。

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

gui.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
# -*- 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")

使用方法は以下です。 この方法もクラスの外からリソースファイルを指定しなければなりません。

main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/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 後でないとリソースアクセス出来ない使い勝手の悪さもあります。

  1. 例が全く複雑なUIじゃない! 
  2. とりあえず今のところ 
  3. UIは複雑じゃないけど、XMLは既に複雑。エディタを使わなければ意味無いですね。 
  4. 1クラス、1ファイルと考えてしまうのは他の言語の悪い影響 
  5. InsertHandler は先頭に挿入します 
?

0 件のコメント: