レイアウト管理

一般的なアプリケーションは様々なウィジェットで構成されています。 これらのウィジェットはコンテナウィジェットの中に配置されます。 プログラマはアプリケーションの配置を管理しなければならず、簡単な作業とは言えません。 wxPythonでは2つの選択肢があります。

Table of Contents

絶対位置を指定する

プログラマがウィジェットのサイズと位置取りをピクセル単位で指定します。 絶対位置を指定するときは、いくつかのことを頭に入れておく必要があります。

  • ウィンドウをリサイズしても、ウィジェットのサイズと位置は変化しない。
  • プラットフォームによってアプリケーションの見た目が変になります。
  • アプリケーションのフォントを変えると、レイアウトが損なわれることがあります。
  • レイアウトを変えようと思ったら、もう一度最初からレイアウトを作りなおさねばなりません。これはひどく退屈で、時間ばかりかかります。

絶対位置を指定できる状況を考えてみましょう。 例としてこのチュートリアルを取り上げます。 私はサンプルを難しくしたくないので、絶対座標指定を使うことがあります。 しかし、実際のプログラム業界では、プログラマはSizerを使うことが多いです。

今回扱う例は簡単なテキストエディタの雛形です。 ウィンドウサイズを変更しても、wx.TextCtrlのサイズは思ったようには変わりません。

./img/absolute1.png ./img/absolute2.png

#! /usr/bin/env python

# absolute.py

import wx

class Absolute(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(250, 180))
        panel = wx.Panel(self, -1)

        menubar = wx.MenuBar()
        file = wx.Menu()
        edit = wx.Menu()
        help = wx.Menu()

        menubar.Append(file, '&File')
        menubar.Append(edit, '&Edit')
        menubar.Append(help, '&Help')
        self.SetMenuBar(menubar)

        wx.TextCtrl(panel, -1, pos=(-1, -1), size=(250, 150))

        self.Centre()
        self.Show(True)

app = wx.App()
Absolute(None, -1, 'absolute.py')
app.MainLoop()
wx.TextCtrl(panel, -1, pos=(-1, -1), size=(250, 150))

wx.TextCtrlのコンストラクタで絶対座標指定を行いました。 今回はこのウィジェットにデフォルトの位置を与えています。 また幅は250pxで高さは150pxです。

Sizerを使う

絶対位置指定のところで言及した問題全てに、Sizerを使って対処できます。 以下のSizerを選ぶことができます。

  • wx.BoxSizer
  • wx.StaticBoxSizer
  • wx.GridSizer
  • wx.FlexGridSizer
  • wx.GridBagSizer

./img/absolute1.png ./img/sizer.png

#! /usr/bin/env python

# sizer.py

import wx

class Sizer(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(250, 180))

        menubar = wx.MenuBar()
        file = wx.Menu()
        edit = wx.Menu()
        help = wx.Menu()

        menubar.Append(file, '&File')
        menubar.Append(edit, '&Edit')
        menubar.Append(help, '&Help')
        self.SetMenuBar(menubar)

        wx.TextCtrl(self, -1)

        self.Centre()
        self.Show(True)

app = wx.App()
Sizer(None, -1, 'sizer.py')
app.MainLoop()

「Sizerなんてどこにも無いじゃないか! 」と言いたげですね。 そう、このコード例は少しばかり小細工を効かせてあります。 実は、wx.TextCtrlをwx.Frameウィジェットの内側に配置したのです。 wx.Frameウィジェットには特別にSizerが組み込まれています。 wx.Frameの中にはウィジェットを1つしか配置できません。 子ウィジェットは、枠・メニュー・ツールバーやステータスバーには与えられていない、全ての隙間を埋めることになります。

wx.BoxSizer

wx.BoxSizerによって、様々なウィジェットを行または列に配置できるようになります。 別のSizerを既にあるSizerに入れることが可能となります。 こうすることで、とても複雑なレイアウトを作ることが出来るようになります。

box = wx.BoxSizer(integer orient)
box.Add(wx.Window window, integer proportion=0, integer flag = 0, integer border = 0)

wx.BoxSizer の向きは wx.VERTICAL または wx.HORIZONTAL のどちらかです。 ウィジェットを wx.BoxSizer に追加するには Add() メソッド経由で行います。 このことを理解するために、このメソッドの引数を見ていきましょう。

引数proportionは、定義された方向に変わる割合を定義します。 proportionがそれぞれ0,1,2のボタンについて考えてみましょう。 ボタンは水平なwx.BoxSizerに追加されます。 プロポーションが0のボタンは一切変化しません。 プロポーションが2のボタンは、プロポーションが1のボタンに比べて2倍以上変化します。

引数 flag の値によって、 wx.BoxSizer 内でのウィジェットの振る舞いを制御できます。 ウィジェット間の境界線を制御できます。 ウィジェットの間に空白をピクセル単位で追加できます。 枠線を適用するために、枠線が使われる端を定義する必要があります。 |(パイプ)演算子でフラグを組み合わせることができます。 例えば、 wx.LEFT | wx.BOTTOM といった具合です。 以下の中からフラグを選べます。

  • wx.LEFT
  • wx.RIGHT
  • wx.BOTTOM
  • wx.TOP
  • wx.ALL

./img/border.png

#! /usr/bin/env python

# border.py

import wx

class Border(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(250,200))

        panel = wx.Panel(self, -1)
        panel.SetBackgroundColour('#4f5049')
        vbox = wx.BoxSizer(wx.VERTICAL)
        
        midPan = wx.Panel(panel, -1)
        midPan.SetBackgroundColour('#ededed')

        vbox.Add(midPan, 1, wx.EXPAND | wx.ALL, 20)
        panel.SetSizer(vbox)
        self.Centre()
        self.Show(True)

app = wx.App()
Border(None, -1, 'border.py')
app.MainLoop()

border.pyファイルでは、midPanパネルの周りに20pxの枠線を設置しました。 wx.ALL はパネルの上下左右全ての辺に枠線を適用します。

wx.EXPAND フラグを使うと、ウィジェットは自身に割り当てられた余白全てを使うようになります。 最後にウィジェットのアラインメントを定義します。 これは下記のフラグを使って行います。

  • wx.ALIGN_LEFT
  • wx.ALIGN_RIGHT
  • wx.ALIGN_TOP
  • wx.ALIGN_BOTTOM
  • wx.ALIGN_CENTER_VERTICAL
  • wx.ALIGN_CENTER_HORIZONTAL
  • wx.ALIGN_CENTER

Go To Class

以下の例で、いくつかの重要な概念を紹介します。

./img/gotoclass.png

#! /usr/bin/env python

# gotoclass.py

import wx

class GoToClass(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(390, 350))
        panel = wx.Panel(self, -1)

        font = wx.SystemSettings_GetFont(wx.SYS_SYSTEM_FONT)
        font.SetPointSize(9)

        vbox = wx.BoxSizer(wx.VERTICAL)

        hbox1 = wx.BoxSizer(wx.HORIZONTAL)
        st1 = wx.StaticText(panel, -1, 'Class Name')
        st1.SetFont(font)
        hbox1.Add(st1, 0, wx.RIGHT, 8)
        tc = wx.TextCtrl(panel, -1)
        hbox1.Add(tc, 1)
        vbox.Add(hbox1, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 10)

        vbox.Add((-1, 10))

        hbox2 = wx.BoxSizer(wx.HORIZONTAL)
        st2 = wx.StaticText(panel, -1, 'Matching Classes')
        st2.SetFont(font)
        hbox2.Add(st2, 0)
        vbox.Add(hbox2, 0, wx.LEFT | wx.TOP, 10)
        
        vbox.Add((-1, 10))

        hbox3 = wx.BoxSizer(wx.HORIZONTAL)
        tc2 = wx.TextCtrl(panel, -1, style=wx.TE_MULTILINE)
        hbox3.Add(tc2, 1, wx.EXPAND)
        vbox.Add(hbox3, 1, wx.LEFT | wx.RIGHT | wx.EXPAND, 10)

        vbox.Add((-1, 25))

        hbox4 = wx.BoxSizer(wx.HORIZONTAL)
        cb1 = wx.CheckBox(panel, -1, 'Case Sensitive')
        cb1.SetFont(font)
        hbox4.Add(cb1)
        cb2 = wx.CheckBox(panel, -1, 'Nested Classes')
        cb2.SetFont(font)
        hbox4.Add(cb2, 0, wx.LEFT, 10)
        cb3 = wx.CheckBox(panel, -1, 'Non-Project classes')
        cb3.SetFont(font)
        hbox4.Add(cb3, 0, wx.LEFT, 10)
        vbox.Add(hbox4, 0, wx.LEFT, 10)

        vbox.Add((-1, 25))

        hbox5 = wx.BoxSizer(wx.HORIZONTAL)
        btn1 = wx.Button(panel, -1, 'Ok', size=(70, 30))
        hbox5.Add(btn1, 0)
        btn2 = wx.Button(panel, -1, 'Close', size=(70, 30))
        hbox5.Add(btn2, 0, wx.LEFT | wx.BOTTOM, 5)
        vbox.Add(hbox5, 0, wx.ALIGN_RIGHT | wx.RIGHT, 10)
        
        panel.SetSizer(vbox)
        self.Centre()
        self.Show(True)

app = wx.App()
GoToClass(None, -1, 'gotoclass.py')
app.MainLoop()

レイアウトは簡単です。 垂直のSizerを作成し、5つの水平Sizerをその中に入れます。

font = wx.SystemSettings_GetFont(wx.SYS_SYSTEM_FONT)
font.SetPointSize(9)

標準のシステムフォントの大きさは10pxです。 私の環境ではこの手のウィンドウに対しては大きすぎるので、9pxに変更しました。

vbox.Add(hbox3, 1, wx.LEFT | wx.RIGHT | wx.EXPAND, 10)

vbox.Add((-1, 25))

flag 変数と border 変数を組み合わせることで、ウィジェット同士の距離を調整する方法は先程学びました。 しかし、現実的には制約があります。 Add() メソッドでは、与えられた辺に対して1つの枠線だけを設定することができます。 今回の例では、10pxの枠線を右左に与えました。 しかし、25pxの余白を下に与えることはできないのです。 この時できるのは左右と同じく10pxにするか、 wx.Bottom を外したければ0pxにするかのどちらかです。 ですから、異なった値を設定する必要があるときは、余分に空白を追加できます。 Add() メソッドで、ウィジェットとスペースを追加しましょう。

vbox.Add(hbox5, 0, wx.ALIGN_RIGHT | wx.RIGHT, 10)

2つのボタンをウィンドウの右側に配置しました。 これを実現するためには、3つの重要なことを知っておかねばなりません。 比率(proportion)アラインメントフラグwx.EXPAND フラグです。 proportion 変数の値は必ず0でなければいけません。 ボタンはウィンドウサイズが変更されても大きさを変更すべきではないからです。

また、wx.EXPANDフラグを設定してはいけません。 ボタンは与えられた領域からはみ出るべきではないからです。

そして最後に、 wx.ALIGN_RIGHT フラグを設定しなければなりません。 水平なSizerはウィンドウの左端から右端に広がります。 wx.ALIGN_RIGHT フラグを設定していると、望んだとおりに、ボタンは右端に配置されます。

検索と置換ダイアログ

続いては複雑なサンプルです。 検索と置換ダイアログを作ります。 この手のダイアログは、EclipseなどのIDEで見かけます。

./img/find_replace.png

#! /usr/bin/env python

# findreplace.py

import wx

class FindReplace(wx.Dialog):
    def __init__(self, parent, id, title):
        wx.Dialog.__init__(self, parent, id, title, size=(255, 365))

        vbox_top = wx.BoxSizer(wx.VERTICAL)
        panel = wx.Panel(self, -1)

        vbox = wx.BoxSizer(wx.VERTICAL)

        #panel1
        panel1 = wx.Panel(panel, -1)
        grid1 = wx.GridSizer(2, 2)
        grid1.Add(wx.StaticText(panel1, -1, 'Find: ', (5, 5)), 0, wx.ALIGN_CENTER_VERTICAL)
        grid1.Add(wx.ComboBox(panel1, -1, size=(120, -1)))
        grid1.Add(wx.StaticText(panel1, -1, 'Replace with: ', (5, 5)), 0, wx.ALIGN_CENTER_VERTICAL)
        grid1.Add(wx.ComboBox(panel1, -1, size=(120, -1)))
        
        panel1.SetSizer(grid1)
        vbox.Add(panel1, 0, wx.BOTTOM | wx.TOP, 9)

        # panel2
        panel2 = wx.Panel(panel, -1)
        hbox2 = wx.BoxSizer(wx.HORIZONTAL)

        sizer21 = wx.StaticBoxSizer(wx.StaticBox(panel2, -1, 'Direction'), orient=wx.VERTICAL)
        sizer21.Add(wx.RadioButton(panel2, -1, 'Forward', style=wx.RB_GROUP))
        sizer21.Add(wx.RadioButton(panel2, -1, 'Backward'))
        hbox2.Add(sizer21, 1, wx.RIGHT, 5)

        sizer22 = wx.StaticBoxSizer(wx.StaticBox(panel2, -1, 'Scope'), orient=wx.VERTICAL)
        sizer22.Add(wx.RadioButton(panel2, -1, 'All', style=wx.RB_GROUP))
        sizer22.Add(wx.RadioButton(panel2, -1, 'Selected Lines'))
        hbox2.Add(sizer22, 1)
        
        panel2.SetSizer(hbox2)
        vbox.Add(panel2, 0, wx.BOTTOM, 9)

        # panel3
        panel3 = wx.Panel(panel, -1)
        sizer3 = wx.StaticBoxSizer(wx.StaticBox(panel3, -1, 'Options'), orient=wx.VERTICAL)
        vbox3 = wx.BoxSizer(wx.VERTICAL)
        grid = wx.GridSizer(3, 2, 0, 5)
        grid.Add(wx.CheckBox(panel3, -1, 'Case Sensitive'))
        grid.Add(wx.CheckBox(panel3, -1, 'Wrap Search'))
        grid.Add(wx.CheckBox(panel3, -1, 'Whole Word'))
        vbox3.Add(grid)
        vbox3.Add(wx.CheckBox(panel3, -1, 'Regular expressions'))
        sizer3.Add(vbox3, 0, wx.TOP, 4)

        panel3.SetSizer(sizer3)
        vbox.Add(panel3, 0, wx.BOTTOM, 15)
        
        # panel4
        panel4 = wx.Panel(panel, -1)
        sizer4 = wx.GridSizer(2, 2, 2, 2)
        sizer4.Add(wx.Button(panel4, -1, 'Find', size=(120, -1)))
        sizer4.Add(wx.Button(panel4, -1, 'Replace/Find', size=(120, -1)))
        sizer4.Add(wx.Button(panel4, -1, 'Replace', size=(120, -1)))
        sizer4.Add(wx.Button(panel4, -1, 'Replace All', size=(120, -1)))

        panel4.SetSizer(sizer4)
        vbox.Add(panel4, 0, wx.BOTTOM, 9)

        # panel5
        panel5 = wx.Panel(panel, -1)
        sizer5 = wx.BoxSizer(wx.HORIZONTAL)
        sizer5.Add((191, -1), 1, wx.EXPAND | wx.ALIGN_RIGHT)
        sizer5.Add(wx.Button(panel5, -1, 'Close', size=(50, -1)))

        panel5.SetSizer(sizer5)
        vbox.Add(panel5, 1, wx.BOTTOM, 9)
        
        vbox_top.Add(vbox, 1, wx.LEFT, 5)
        panel.SetSizer(vbox_top)
        
        self.Centre()
        self.ShowModal()
        self.Destroy()

app = wx.App()
FindReplace(None, -1, 'findreplace.py')
app.MainLoop()

※ Windowsユーザーの方は、self.SetClientSize(panel.GetBestSize())の行をShowModal()メソッドの前に置いてください。

レイアウトを記述していく前に、目標を達成する方法について考えておかねばなりません。 複雑なダイアログやウィンドウをシンプルにスケッチしておくことは非常に有効です。 ダイアログのスクリーンショットを見るときは、 そのダイアログが5つの部品に分解可能だということを、はっきりと理解しましょう。 Closeボタンは分割パネルを持っています。 それぞれの部品が独立したwx.Panelです。 合わせると6つのパネルがあることになります。 最初のパネルは親パネルで、私たちが確認できる5つのパネルを保持しています。

5つのパネルは全て一列に並んでいます。 親パネルは垂直なwx.BoxSizerになります。 wx.BoxSizerから離れて、wx.GridSizerを使います。 wx.GridSizerは次の章で説明します。 wx.GridSizerの使い方はとても簡単ですから、そう多くを説明することはありません。

sizer4 = wx.GridSizer(2, 2, 2, 2)
sizer4.Add(wx.Button(panel4, -1, 'Find', size=(120, -1)))
sizer4.Add(wx.Button(panel4, -1, 'Replace/Find', size=(120, -1)))
sizer4.Add(wx.Button(panel4, -1, 'Replace', size=(120, -1)))
sizer4.Add(wx.Button(panel4, -1, 'Replace All', size=(120, -1)))

wx.GridSizerのサンプルは有用性が高いです。 同じ大きさの4つのボタンを特定のパネルに収めたいわけですが、こういうときこそwx.GridSizerの出番です。 なぜなら、wx.GridSizerはあらゆるウィジェットをマス目の中に配置するからです。 これらのマス目は全て同じ幅と高さです。

wx.GridSizer

wx.GridSizerを使うことで、ウィジェットを2次元の表に配置できます。 表に含まれる、それぞれのマス目は全て同じ大きさです。

wx.GridSizer(int rows=1, int cols=0, int vgap=0, int hgap=0)

コンストラクタで表の行と列の数を設定できます。 セル同士の縦横の隙間も設定できます。

今回のサンプルでは、電卓の雛形を作っていきます。 wx.GridSizerの使用例としてこれ以上のものはありません。

./img/gridsizer.png

#! /usr/bin/env python

# gridsizer.py

import wx

class GridSizer(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(300, 250))

        menubar = wx.MenuBar()
        file = wx.Menu()
        file.Append(1, '&Quit', 'Exit Calculator')
        menubar.Append(file, '&File')
        self.SetMenuBar(menubar)

        self.Bind(wx.EVT_MENU, self.OnClose, id=1)

        sizer = wx.BoxSizer(wx.VERTICAL)
        self.display = wx.TextCtrl(self, -1, '', style=wx.TE_RIGHT)
        sizer.Add(self.display, 0, wx.EXPAND | wx.TOP | wx.BOTTOM, 4)
        gs = wx.GridSizer(4, 4, 3, 3)

        gs.AddMany( [(wx.Button(self, -1, 'Cls'), 0, wx.EXPAND),
                     (wx.Button(self, -1, 'Bck'), 0, wx.EXPAND),
                     (wx.StaticText(self, -1, ''), 0, wx.EXPAND),
                     (wx.Button(self, -1, 'Close'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '7'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '8'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '9'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '/'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '4'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '5'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '6'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '*'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '1'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '2'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '3'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '-'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '0'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '.'), 0, wx.EXPAND),
                     (wx.Button(self, -1, '='), 0, wx.EXPAND),
                     (wx.Button(self, -1, '+'), 0, wx.EXPAND) ])

        sizer.Add(gs, 1, wx.EXPAND)
        self.SetSizer(sizer)
        self.Centre()
        self.Show(True)

    def OnClose(self, event):
        self.Close()

app = wx.App()
GridSizer(None, -1, 'gridsizer.py')
app.MainLoop()

BckボタンとCloseボタンの間にどうやって空白を入れたかに着目してください。 単に空白のwx.StaticTextを配置しているだけです。 こうした小技は非常によく使われます。

サンプルコードで、 AddMany() メソッドを使っています。 複数のウィジェットを一度に追加するのに便利です。

AddMany(list items)

ウィジェットは追加された順番で、ウィジェットの内側に配置されていきます。 1行目が最初に埋まり、次に2行目が埋まる…といった具合です。

wx.FlexGridSizer

このSizerはwx.GridSizerと似ていて、ウィジェットを2次元の表に配置します。 さらに柔軟性も追加されています。 wx.GridSizerのセルは同じ大きさでした。 wx.FlexGridSizerの全てのセルは、同一列では同じ幅を持ち、同一行では同じ高さを持ちます。 しかし、全ての行と列が必ずしも同じ幅と高さである必要はないのです。

wx.FlexGridSizer(int rows=1, int cols=0, int vgap=0, int hgap=0)

rowscols でSizerの行と列の数を設定します。 vgaphgap でウィジェット間の縦横の空白を追加します。

開発者は、データ入力と変更のために、何度もダイアログを改良する必要があります。 そういう作業に wx.FlexGridSizer は最適だと思われます。 開発者は、このSizerを使って、簡単にダイアログウィンドウを作ることができます。 wx.GridSizer を使っても同じことができますが、見た目によろしくないでしょう。 なぜなら、それぞれのセルが同じ大きさになるという制約があるからです。

#! /usr/bin/env python

# flexgridsizer.py

import wx

class FlexGridSizer(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(290, 250))

        panel = wx.Panel(self, -1)
        
        hbox  = wx.BoxSizer(wx.HORIZONTAL)
        
        fgs   = wx.FlexGridSizer(3, 2, 9, 25)

        title  = wx.StaticText(panel, -1, 'Title')
        author = wx.StaticText(panel, -1, 'Author')
        review = wx.StaticText(panel, -1, 'Review')

        tc1 = wx.TextCtrl(panel, -1)
        tc2 = wx.TextCtrl(panel, -1)
        tc3 = wx.TextCtrl(panel, -1, style=wx.TE_MULTILINE)

        fgs.AddMany([(title),  (tc1, 1, wx.EXPAND),
                     (author), (tc2, 1, wx.EXPAND),
                     (review), (tc3, 1, wx.EXPAND)])

        fgs.AddGrowableRow(2, 1)
        fgs.AddGrowableCol(1, 1)

        hbox.Add(fgs, 1, wx.ALL | wx.EXPAND, 15)
        panel.SetSizer(hbox)

        self.Centre()
        self.Show(True)

app = wx.App()
FlexGridSizer(None, -1, 'flexgridsizer.py')
app.MainLoop()

hbox = wx.BoxSizer(wx.HORIZONTAL)
...
hbox.Add(fgs, 1, wx.ALL | wx.EXPAND, 15)

ウィジェットのテーブル周りに15pxほど空白を入れるために、水平のボックスSizerを作ります。

fgs.AddMany([(title), (tc1, 1, wx.EXPAND),
             (author), (tc2, 1, wx.EXPAND),
             (review, 1, wx.EXPAND), (tc3, 1, wx.EXPAND)])

AddMany() メソッドでSizerにウィジェットを追加していきます。 wx.FlexGridSizerwx.GridSizer はこのメソッドを共有しています。

fgs.AddGrowableRow(2, 1)
fgs.AddGrowableCol(1, 1)

3行目と2列目をサイズ変更可能にしました。 こうすることで、ウィンドウがリサイズされた時、テキストコントロールがのび縮みします。 1〜2番目のテキストコントロールは水平方向に伸びます、3番目のテキストコントロールは両方向に伸びます。 このままでは使いづらいので、 wx.EXPAND フラグでウィジェットを伸縮可能にするのを忘れずに。

./img/flexgridsizer.png

wx.GridBagSizer

The most complicated sizer in wxPython. Many programmer find it difficult to use. This kind of sizer is not typical only for wxPython. We can find it in other toolkits as well. There is no magic in using this sizer. Even though it is more complicated, it is certainly not rocket science. All we have to do is to create several layouts with it. Find all the quirks. Play with it a bit. There are more difficult things in programming. Believe me.

wx.GridBagSizer はwxPythonの中で最も複雑なSizerです。 多くのプログラマが使いづらいと感じることでしょう。 この種のSizerはwxPythonに限らず、他のツールキットでもよく見かけます。 このSizerを楽に使いこなせるような方法はありません。 ただし、複雑ではありますが、難しすぎてわからないということもないのです。 とにかく wx.GridBagSizer を使っていろいろレイアウトを作ってみましょう。 特徴をつかみましょう。 すこし遊んでみましょう。 プログラムにはこれよりもっと難しいことがあります。 私を信じてください。

This sizer enables explicit positioning of items. Items can also optionally span more than one row and/or column. wx.GridBagSizer has a simple constructor.

このSizerを使うと、アイテムをきっちりと配置することができます。 アイテムは選択次第で1行または1列以上を占めることもできます。 wx.GridBagSizer は単純なコンストラクタを持っています。

wx.GridBagSizer(integer vgap, integer hgap)

全ての子要素で使用される垂直水平の間隔を、ピクセル単位で定義します。 Add() メソッドによりグリッドにアイテムを追加します。

Add(self, item, tuple pos, tuple span=wx.DefaultSpan, integer flag=0,
    integer border=0, userData=None)

Item is a widget that you insert into the grid. pos specifies the position in the virtual grid. The topleft cell has pos of (0, 0). span is an optional spanning of the widget. e.g. span of (3, 2) spans a widget across 3 rows and 2 columns. flag and border were discussed earlier by wx.BoxSizer. The items in the grid can change their size or keep the default size, when the window is resized. If you want your items to grow and shrink, you can use these two methods.

item とは、グリッドに挿入するウィジェットのことです。 pos で仮想的なグリッドの中における位置を設定します。 左上のセルが(0, 0)という座標を保持しています。 span は任意のウィジェット幅です。 例えば、(3, 2) という値はウィジェットを3行2列に渡って結合します。 flagborder については wx.BoxSizer の章で説明しました。 ウィンドウの大きさが変わったときに、グリッドの中のアイテムがサイズを変えるか否か設定できます。 もし、アイテムをのび縮みさせたければ、これら2つのメソッドを使うとよいでしょう。

AddGrowableRow(integer row)
AddGrowableCol(integer col)

名前変更ダイアログ

./img/rename.png

The first example is intentionally a very simple one. So that it could be easily understood. There is no need to be afraid of wx.GridBagSizer. Once you understand it's logic, it is quite simple to use it. In our example, we will create a rename dialog. It will have one wx.StaticText, one wx.TextCtrl and two wx.Button-s.

最初のサンプルは意図的に簡単なものにしてあるので、理解しやすいでしょう。 wx.GridBagSizerを恐れることはありません。 一度理屈を分かってしまえば、使うのはとても容易いのです。 このサンプルでは、名前変更ダイアログを作ります。 このダイアログは、wx.StaticTextを1つ、wx.TextCtrlを1つ、そしてwx.Buttonを2つ使用しています。

#! /usr/bin/env python

# rename.py

import wx

class Rename(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(320, 130))

        panel = wx.Panel(self, -1)
        sizer = wx.GridBagSizer(4, 4)

        text = wx.StaticText(panel, -1, 'Rename To')
        sizer.Add(text, (0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=5)

        tc = wx.TextCtrl(panel, -1)
        sizer.Add(tc, (1, 0), (1, 5), wx.EXPAND | wx.LEFT | wx.RIGHT, 5)

        buttonOk    = wx.Button(panel, -1, 'Ok', size=(90, 28))
        buttonClose = wx.Button(panel, -1, 'Close', size=(90, 28))
        sizer.Add(buttonOk, (3, 3))
        sizer.Add(buttonClose, (3, 4), flag=wx.RIGHT | wx.BOTTOM, border=5)

        sizer.AddGrowableCol(1)
        sizer.AddGrowableRow(2)
        panel.SetSizerAndFit(sizer)
        self.Centre()
        self.Show(True)

app = wx.App()
Rename(None, -1, 'rename.py')
app.MainLoop()

We must look at the dialog window as a one big grid table.

ダイアログウィンドウを一つの大きなマス目の表として見なければなりません。

text = wx.StaticText(panel, -1, 'Rename To')
sizer.Add(text, (0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=5)

The text 'Rename to' goes to the left upper corner. So we specify the (0, 0) position. Plus we add some space to the bottom, left and bottom.

'Rename to' というテキストは左上隅に行きます。 (0, 0) という場所を設定したのです。 さらに、上・左・下に余白をいくらか追加します。

tc = wx.TextCtrl(panel, -1)
sizer.Add(tc, (1, 0), (1, 5), wx.EXPAND | wx.LEFT | wx.RIGHT, 5)

The wx.TextCtrl goes to the beginning of the second row (1, 0). Remember, that we count from zero. It expands 1 row and 5 columns. (1, 5). Plus we put 5 pixels of space to the left and to the right of the widget.

wx.TextCtrl は2行目の (1, 0) に配置されます。 行や列の添字は0から始まっていることに気をつけてください。 wx.TextCtrl は (1, 5) まで伸びます。 さらに、左右に5ピクセルの余白を配置しています。

sizer.Add(buttonOk, (3, 3))
sizer.Add(buttonClose, (3, 4), flag=wx.RIGHT | wx.BOTTOM, border=5)

We put two buttons into the fourth row. The third row is left empty, so that we have some space between the wx.TextCtrl and the buttons. We put the ok button into the fourth column and the close button into the fifth one. Notice that once we apply some space to one widget, it is applied to the whole row. That's why I did not specify bottom space for the ok button. A careful reader might notice, that we did not specify any space between the two buttons. e.g. we did not put any space to the right of the ok button, or to the right of the close button. In the constructor of the wx.GridBagSizer, we put some space between all widgets. So there is some space already.

4行目には2つのボタンを配置しました。 3列目は左側が空なので、 wx.TextCtrl とボタンとの間にいくらか隙間を入れておきます。 Okボタンを4列目に、Closeボタンを5列目に配置します。 あるウィジェットに一度でも余白を適用すると、列全体に適用されることに注意しましょう。 Okボタンに下の空白を設定していないのはそのためです。 注意深い読者の方ならお気づきでしょうが、2つのボタンの間には空白を設定していません。 どういうことかというと、Okボタンの右側と、Closeボタンの右側に空白を入れていないのです。 実は、 wx.GridBagSizer のコンストラクタで、全てのウィジェット同士の隙間を設定しているのです。 ですから、設定せずとも空白は既に存在するのです。

リソースを開く

The next example will be a bit more complicated. We will create an Open Resource window. This example will show a layout of a very handy dialog which you can find in Eclipse IDE.

次のサンプルはもうちょっとだけ複雑になります。 リソースを開くためのウィンドウを作成します。 このサンプルは、Eclipseの統合開発環境にあるような、非常に便利なダイアログの配置を表示します。

./img/openresource.png

#! /usr/bin/env python

# openresource.py

import wx

class OpenResource(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(400, 500))

        panel = wx.Panel(self, -1)
        sizer = wx.GridBagSizer(4, 4)

        text1 = wx.StaticText(panel, -1, 'Select a resource to open')
        sizer.Add(text1, (0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=5)

        tc = wx.TextCtrl(panel, -1)
        sizer.Add(tc, (1, 0), (1, 3), wx.EXPAND | wx.LEFT | wx.RIGHT, 5)

        text2 = wx.StaticText(panel, -1, 'Matching resources')
        sizer.Add(text2, (2, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=5)

        list1 = wx.ListBox(panel, -1, style=wx.LB_ALWAYS_SB)
        sizer.Add(list1, (3, 0), (5, 3), wx.EXPAND | wx.LEFT | wx.RIGHT, 5)

        text3 = wx.StaticText(panel, -1, 'In Folders')
        sizer.Add(text3, (8, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=5)

        list2 = wx.ListBox(panel, -1, style=wx.LB_ALWAYS_SB) 
        sizer.Add(list2, (9, 0), (3, 3), wx.EXPAND | wx.LEFT | wx.RIGHT, 5)

        cb = wx.CheckBox(panel, -1, 'Show derived resources')
        sizer.Add(cb, (12, 0), flag=wx.LEFT | wx.RIGHT, border=5)

        buttonOk     = wx.Button(panel, -1, 'OK', size=(90, 28))
        buttonCansel = wx.Button(panel, -1, 'Cancel', size=(90, 28))
        sizer.Add(buttonOk, (14, 1))
        sizer.Add(buttonCansel, (14, 2), flag=wx.RIGHT | wx.BOTTOM, border=5)

        help = wx.BitmapButton(panel, -1, wx.Bitmap('icons/help.png'), style=wx.NO_BORDER)
        sizer.Add(help, (14, 0), flag=wx.LEFT, border=5)

        sizer.AddGrowableCol(0)
        sizer.AddGrowableRow(3)
        sizer.AddGrowableRow(9)
        sizer.SetEmptyCellSize((5, 5))
        panel.SetSizer(sizer)

        self.Centre()
        self.Show(True)

app = wx.App()
OpenResource(None, -1, 'openresource.py')
app.MainLoop()
sizer.AddGrowableRow(3)
sizer.AddGrowableRow(9)

We want to have both wx.ListBox-es growable. So we make the first row of each wx.ListBox growable.

両方の wx.ListBox を伸びるようにしたいので、 1行目にある wx.ListBox をどちらも伸びるように設定しました。

新しいクラスを作る

newclass.py example is a type of a window, that I found in JDeveloper. It is a dialog window for creating a new class in Java.

newclass.py サンプルは、JDeveloperにあるJavaの新しいクラスを作るためのダイアログウィンドウです。

./img/newclass.png

#! /usr/bin/env python

# newclass.py

import wx

class NewClass(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title)

        panel = wx.Panel(self, -1)
        sizer = wx.GridBagSizer(0, 0)

        text1 = wx.StaticText(panel, -1, 'Java Class')
        sizer.Add(text1, (0, 0), flag=wx.TOP | wx.LEFT | wx.BOTTOM, border=15)

        icon = wx.StaticBitmap(panel, -1, wx.Bitmap('icons/exec.png'))
        sizer.Add(icon, (0, 4), flag=wx.LEFT, border=45)

        line = wx.StaticLine(panel, -1)
        sizer.Add(line, (1, 0), (1, 5), wx.TOP | wx.EXPAND, -15)

        text2 = wx.StaticText(panel, -1, 'Name')
        sizer.Add(text2, (2, 0), flag=wx.LEFT, border=10)

        tc1 = wx.TextCtrl(panel, -1, size=(-1, 30))
        sizer.Add(tc1, (2, 1), (1, 3), wx.TOP | wx.EXPAND, -5)

        text3 = wx.StaticText(panel, -1, 'Package')
        sizer.Add(text3, (3, 0), flag=wx.LEFT | wx.TOP, border=10)

        tc2 = wx.TextCtrl(panel, -1)
        sizer.Add(tc2, (3, 1), (1, 3), wx.TOP | wx.EXPAND, 5)

        button1 = wx.Button(panel, -1, 'Browse...', size=(-1, 30))
        sizer.Add(button1, (3, 4), (1, 1), wx.TOP | wx.LEFT | wx.RIGHT, 5)

        text4 = wx.StaticText(panel, -1, 'Extends')
        sizer.Add(text4, (4, 0), flag=wx.TOP | wx.LEFT | wx.RIGHT, border=10)

        combo = wx.ComboBox(panel, -1)
        sizer.Add(combo, (4, 1), (1, 3), wx.TOP | wx.EXPAND, 5)

        button2 = wx.Button(panel, -1, 'Browse...', size=(-1, 30))
        sizer.Add(button2, (4, 4), (1, 1), wx.TOP | wx.LEFT | wx.RIGHT, 5)

        sb = wx.StaticBox(panel, -1, 'Optional Attributes')
        boxsizer = wx.StaticBoxSizer(sb, wx.VERTICAL)
        boxsizer.Add(wx.CheckBox(panel, -1, 'Public'), 0, wx.LEFT | wx.TOP, 5)
        boxsizer.Add(wx.CheckBox(panel, -1, 'Generate Default Constructor'), 0, wx.LEFT, 5)
        boxsizer.Add(wx.CheckBox(panel, -1, 'Generate Main Method'), 0, wx.LEFT | wx.BOTTOM, 5)
        sizer.Add(boxsizer, (5, 0), (1, 5), wx.EXPAND | wx.TOP | wx.LEFT | wx.RIGHT, 10)
        button3 = wx.Button(panel, -1, 'Help', size=(-1, 30))
        sizer.Add(button3, (7, 0), (1, 1), wx.LEFT, 10)

        button4 = wx.Button(panel, -1, 'Ok', size=(-1, 30))
        sizer.Add(button4, (7, 3), (1, 1), wx.LEFT, 10)

        button5 = wx.Button(panel, -1, 'Cancel', size=(-1, 30))
        sizer.Add(button5, (7, 4), (1, 1), wx.LEFT | wx.BOTTOM | wx.RIGHT, 10)

        sizer.AddGrowableCol(2)
        
        panel.SetSizer(sizer)
        sizer.Fit(self)

        self.Centre()
        self.Show(True)

app = wx.App()
NewClass(None, -1, 'newclass.py')
app.MainLoop()
line = wx.StaticLine(panel, -1 )
sizer.Add(line, (1, 0), (1, 5), wx.TOP | wx.EXPAND, -15)

Notice, that we have used negative number for setting the top border. We could use wx.BOTTOM with border 15. We would get the same result.

上の枠線を設定するのに、負の数字を使っていることに注目してください。 枠線の幅が15pxの wx.BOTTOM と同じ結果を得ることができます。

icon = wx.StaticBitmap(panel, -1, wx.Bitmap('icons/exec.png'))
sizer.Add(icon, (0, 4), flag=wx.LEFT,  border=45)

We put an wx.StaticBitmap into the first row of the grid. We place it on the right side of the row. By using images we can make our applications look better.

グリッドの1行目の右側に wx.StaticBitmap を配置します。 画像を使用することで、アプリケーションの見た目が良くなります。

sizer.Fit(self)

We did not set the size of the window explicitly. If we call Fit() method, the size of the window will exactly cover all widgets available. Try to comment this line and see what happens.

ウィンドウの大きさをはっきりと設定していません。 Fit() メソッドを呼ぶと、ウィンドウの大きさがぴったりと広がり、全てのウィジェットが使えるようになります。 Fit() の部分をコメントアウトして、どうなるか試してみてください。

The original page is here.

Date: 2010-11-09 22:19:53 JST

HTML generated by org-mode 6.36c in emacs 23