TI/Programowanie dla Fizyków Medycznych:Ramki

Z Brain-wiki

Ramki

W tym rozdziale opiszę obiekty klasy wx.Frame, czyli ramki. Są one podstawowym obiektem graficznym w wxPythonie, wszystko inne wyświetlane jest w jakiejś ramce lub obiekcie klasy wx.Dialog (o nich w innym rozdziale). Aby upodobnić nasze programy do innych do ramki można dodać menu narzędziowe (pasek z ikonkami odpowiadającymi pewnym czynnościom), pasek menu (na górze zawierające zazwyczaj menu Plik, Edycja itd) i pasek statusu (na dole zazwyczaj zawierający informacje o tym co jest aktualnie wykonywane w programie lub na co wskazuje myszka). Konstruktor ramki wygląda następująco:

wx.Frame(parent, id = -1, title = "", pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.DEFAULT_FRAME_STYLE, name = "frame")

Jeśli jako parent przekażemy None to ramka jest tworzona jako ramka najwyższego poziomu. Aplikacja wxPythona kończy swoje życie gdy zostaną zniszczone wszystkie ramki najwyższego poziomu. Parametr title jest wyświetlany w pasku na górze okna (pod warunkiem, że ten pasek jest wyświetlany, co zależy od zastosowanych stylów). Zauważmy, że ramka z domyślnymi stylami ma wiele elementów: przyciski do zamykania, minimalizacji i maksymalizacji, menu systemowe (osiągalne po kliknięciu prawym klawiszem na pasek z tytułem), pasek z tytułem, możliwe jest także zmienianie rozmiaru ramki - każdy z tych elementów może zostać usunięty przez zadanie parametru stylu w konstruktorze, domyślny styl jest sumą następujących stylów: wx.MINIMIZE_BOX, wx.MAXIMIZE_BOX, wx.CLOSE_BOX, wx.RESIZE_BORDER, wx.SYSTEM_MENU i wx.CAPTION. Pozostałe możliwe style to: wx.FRAME_NO_TASKBAR - zastosowanie powoduje, że na pasku programów nie jest wyświetlana ikonka związana z naszą aplikacją, wx.FRAME_SHAPED - pozwala zdefiniować okno o dowolnym kształcie, ale nie będziemy tego omawiać na zajęciach, wx.FRAME_TOOL_WINDOW - ma mniejszy pasek tytułowy, wx.ICONIZE (wx.MINIMIZE) - zastosowanie powoduje, że okno po stworzeniu zostanie zminimalizowane, wx.MAXIMIZE - po stworzeniu okno zostanie zmaksymalizowane, wx.FRAME_FLOAT_ON_PARENT - zapewni, że okno będzie można przesuwać tylko w obrębie rodzica, wx.STAY_ON_TOP - zapewnia, że okno będzie zawsze na wierzchu. Zmieniając style okna należy uważać aby nie stworzyć okna, którego nie da się zamknąć gdyż wtedy zakończenie aplikacji okaże się niemożliwe. Aby okno zostało wyświetlone należy wywołać na nim metodę Show(). Oto najprostszy przykład okna:

import wx

app = wx.App()
wx.Frame(None, title = 'Najprostsze okno').Show()
app.MainLoop()

Zauważ, że poniższe przykłady generują aplikacje, których nie da się zakończyć:

import wx

app = wx.App()
wx.Frame(None, title = 'Najprostsze okno', style = 0).Show()
app.MainLoop()
import wx

app = wx.App()
wx.Frame(None)
app.MainLoop()

Zazwyczaj nie stosuje się okien w ten sposób, zamiast tego tworzy się własną klasę dziedziczącą po wx.Frame i w niej opisuje się wygląd interfejsu i tworzy obsługę zdarzeń, oto prosty przykład okna zawierającego parę obiektów:

import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, title = 'okienko klasy MyFrame', size = (400, 400))
        self.InitUI()
        self.Show()
    def InitUI(self):
        panel = wx.Panel(self)
        wx.Button(panel, label = 'wx.Button', pos = (0, 0), size = (200, 200))
        wx.TextCtrl(panel, value = 'wx.TextCtrl', pos = (201, 201), size = (100, 100))
        wx.ComboBox(panel, value = 'wx.ComboBox', pos = (302, 302), size = (50, 50))
        wx.Slider(panel, pos = (353, 353), size = (25, 25))
        wx.StaticText(panel, label = 'wx.StaticText', pos = (379, 379), size = (13, 13))
        
app = wx.App()
MyFrame()
app.MainLoop()

Umieszczanie obiektów nie bezpośrednio w oknie ale w obiekcie klasy wx.Panel jest wygodne, gdyż dzięki temu mamy zapewnioną obsługę przechodzenia focusem między naszymi widgetami (tabem w jedną i shit + tab w drugą stronę), widać przy okazji, że wx.StaticText jest pomijany przy przechodzeniu focusem do kolejnego widgeta. Minusem tej aplikacji jest to, że mimo, że okienko pozwala na zmianę rozmiaru, ale widgety na nim nie reagują na zmiany rozmiaru okna. Zmianę rozmiarów i położeń widgetów należałoby opisać ręcznie i reagować na event EVT_SIZE - prezentuje to poniższy przykład:

# -*- coding: utf-8 -*-
import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, title = 'okienko klasy MyFrame', size = (400, 400))
        self.InitUI()
        self.Show()
    def InitUI(self):
        panel = wx.Panel(self)
        self.btn = wx.Button(panel, label = 'wx.Button')
        self.txt = wx.TextCtrl(panel, value = 'wx.TextCtrl')
        self.cmb = wx.ComboBox(panel, value = 'wx.ComboBox')
        self.sld = wx.Slider(panel)
        self.stt = wx.StaticText(panel, label = 'wx.StaticText')
        self.Bind(wx.EVT_SIZE, self.OnSize)
    def OnSize(self, evt):
        m, n = self.GetSize()
        m /= 32.
        n /= 32.
        self.btn.SetDimensions(0, 0, m * 16, n * 16)
        self.txt.SetDimensions(m * 16 + 1, n * 16 + 1, m * 8, n * 8)
        self.cmb.SetDimensions(m * 24 + 2, n * 24 + 2, m * 4, n * 4)
        self.sld.SetDimensions(m * 28 + 3, n * 28 + 3, m * 2, n * 2)
        self.stt.SetDimensions(m * 30 + 4, n * 30 + 4, m, n)
        evt.Skip()
app = wx.App()
MyFrame()
app.MainLoop()

Widać, że nie jest to zbyt wygodne, a w przypadku okien zawierających więcej widgetów poprawne obsłużenie wszystkich może być bardzo żmudne, dlatego wxPython dostarcza mechanizmy, które wykonują to za nas. Mowa o podklasach klasy wx.Sizer, obiekty te definiują rozplanowanie (layout) widgetów wewnątrz kontenera i same dbają o zmiany wielkości przy zmienianiu wielkości okienka. Dotąd aby wyświetlić widget w panelu wystarczyło podać panel jako rodzic przy tworzeniu obiektu, korzystając z sizerów konieczne będzie ręcznie dodaie obiektu do sizera, a następnie ustawienie sizera w panelu. Ilustruje to przykład dla BoxSizera (typy sizerów będą opisane poniżej):

# -*- coding: utf-8 -*-
import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, title = 'okienko klasy MyFrame', size = (400, 400))
        self.InitUI()
        self.Show()
    def InitUI(self):
        panel = wx.Panel(self)
        btn = wx.Button(panel, label = 'wx.Button')
        txt = wx.TextCtrl(panel, value = 'wx.TextCtrl')
        cmb = wx.ComboBox(panel, value = 'wx.ComboBox')
        sld = wx.Slider(panel)
        stt = wx.StaticText(panel, label = 'wx.StaticText')
        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(btn, proportion = 4, flag = wx.ALL, border = 10)
        self.sizer.Add(txt, proportion = 3, flag = wx.EXPAND|wx.ALL, border = 10)
        self.sizer.Add(cmb, proportion = 2, flag = wx.EXPAND|wx.ALL, border = 10)
        self.sizer.Add(sld, proportion = 1, flag = wx.EXPAND|wx.ALL, border = 10)
        self.sizer.Add(stt, proportion = 0, flag = wx.EXPAND|wx.ALL, border = 10)
        panel.SetSizer(self.sizer)
        self.Bind(wx.EVT_BUTTON, self.OnButton, btn)
    def OnButton(self, evt):
        if self.sizer.GetOrientation() == wx.HORIZONTAL:
            self.sizer.SetOrientation(wx.VERTICAL)
        else:
            self.sizer.SetOrientation(wx.HORIZONTAL)
        self.sizer.Layout()
app = wx.App()
MyFrame()
app.MainLoop()

Zaprezentowany wx.BoxSizer ustawia dodane do niego widgety jeden za drugim w pionie (gdy utworzony z parametrem wx.VERTICAL) albo w poziomie (gdy z wx.HORIZONTAL), jak widać dodawanie pojedynczego elementu jest realizowane metodą Add, która przyjmuje kolejno: dodawany obiekt, parametr mówiący jak ma się zmieniać wielkość obiektu w kierunku głównym sizera przy zmianie wielkości ramki (0 oznacza brak zmian rozmiaru, pozostałe liczby należy rozumieć relatywnie czyli na przykład sld będzie zwiększany 4 razy mniej niż btn), flagę mówiącą o ramce (wx.ALL - tworzy ramkę dookoła obiektu, inne możliwości to wx.BOTTOM, wx.TOP, wx.LEFT i wx.RIGHT) i o tym jaka ma być wielkość obiektu w kierunku który nie jest głównym kierunkiem sizera - podanie wx.EXPAND powoduje, że widget będzie zajmował całą dostępną przestrzeń. Przy pomocy metod SetOrientation(int) i GetOrientation() możemy manipulować kierunkiem głównym utworzonego sizera, po każdej zmianie sizera należy wywołać na nim metodę Layout. Pozostałe dostępne sizery to:

  • wx.GridSizer - umożliwia ustawianie widgetów w siatce o równych polach, każdy widget może zajmować dokładnie jedno pole
  • wx.FlexGridSizer - jak wyżej ale szerokość danej kolumny siatki jest dopasowywana do najszerszego elementu w danej kolumnie, podobnie wysokość wiersza jest dopasowywana do najwyższego widgeta w danym wierszu
  • wx.GridBagSizer - jak wyżej ale widgety mogą zajmować więcej niż jedną komórkę
  • wx.StaticBoxSizer

Poza metodą Add, są jeszcze dwie: mająca identyczną składnię: Prepend - dodająca obiekt na początek i biorąca na pierwszym miejscu dodatkowy parametr pos: Insert - wstawiająca obiekt na zadaną pozycję. Obiektem dodawanym w każdej z tych metod może być obiekt klasy wx.Window (typowy widget), inny sizer, albo pusta przestrzeń (jako obiekt klasy wx.Size). Po utworzeniu sizera i dodaniu go do panela można dopasować wielkość panela tak aby pomieścił wszystkie widgety przy pomocy metody sizer.Fit(panel). Przykład użycia wx.GridSizer:

# -*- coding: utf-8 -*-
import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, title = 'okienko klasy MyFrame', size = (400, 400))
        self.InitUI()
        self.Show()
    def InitUI(self):
        panel = wx.Panel(self)
        rows, cols = 7, 3
        sizer = wx.GridSizer(rows = rows, cols = 0, hgap = 2, vgap = 10)
        for i in xrange(rows * cols):
            sizer.Add(wx.Button(panel, label = 'i = ' + str(i)), flag = wx.EXPAND)
        panel.SetSizer(sizer)
app = wx.App()
MyFrame()
app.MainLoop()

Podanie parametru cols równego 0 powoduje, że liczba kolumn będzie policzona na podstawie liczby dodanych elementów. Poniżej przykład ilustrujący możliwe style:

# -*- coding: utf-8 -*-
import wx
import random

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, title = 'okienko klasy MyFrame', size = (400, 400))
        self.InitUI()
        self.Show()
    def InitUI(self):
        panel = wx.Panel(self)
        rows, cols = 7, 3
        align_dict = {wx.ALIGN_BOTTOM : 'bottom', wx.ALIGN_CENTER : 'center', wx.ALIGN_CENTER_HORIZONTAL : 'center_h', wx.ALIGN_CENTER_VERTICAL : 'center_v', wx.ALIGN_LEFT : 'left', wx.ALIGN_RIGHT : 'right', wx.ALIGN_TOP : 'top', wx.EXPAND : 'expand', wx.SHAPED : 'shaped'}
        align = [wx.ALIGN_BOTTOM, wx.ALIGN_CENTER, wx.ALIGN_CENTER_HORIZONTAL, wx.ALIGN_CENTER_VERTICAL, wx.ALIGN_LEFT, wx.ALIGN_RIGHT, wx.ALIGN_TOP, wx.EXPAND, wx.SHAPED]
        sizer = wx.GridSizer(rows = rows, cols = 0, hgap = 2, vgap = 10)
        for i in xrange(rows * cols):
            flag = random.choice(align)
            sizer.Add(wx.Button(panel, label = align_dict[flag]), flag = flag)
        panel.SetSizer(sizer)
app = wx.App()
MyFrame()
app.MainLoop()

wx.EXPAND zapewnia, że obiekt zajmuje całą dostępną przestrzeń (wymiennie można stosować wx.GROW), wx.SHAPED podobnie ale z warunkiem, że stosunek wymiarów obiektu jest zachowany, wx.CENTER ustawia środek widgeta na środku przeznaczonego dla niego miejsca. PRzy pomocy metod klasy wx.Widget SetMinSize(w, h), SetMaxSize(w, h) i SetSizeHints(minW, minH, maxW, maxH) można ustawiać minimalne i maksymalne wymiary każdego widgeta. Poniższy przykład pokazuje jak działa wx.FlexGridSizer:

# -*- coding: utf-8 -*-
import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, size = (900, 600))
        self.InitUI()
        self.Show()
    def InitUI(self):
        panel = wx.Panel(self)
        rows, cols = 4, 4
        sizer = wx.FlexGridSizer(rows = rows, cols = 0, hgap = 2, vgap = 10)
        for i in xrange(rows * cols):
            btn = wx.Button(panel, label = str(i))
            sizer.Add(btn, flag = wx.CENTER)
            btn.SetMinSize((15 * i, 15 * i))
        panel.SetSizer(sizer)
app = wx.App()
MyFrame()
app.MainLoop()

Jak widać determinuje on rozmiary poszczególnych pól na podstawie umieszczonych w nich obiektów i domyślnie nie zmienia wielkości pól. Można zdefiniować, która kolumna i wiersz będą zwiększały swoje rozmiary metodami AddGrowableCol i AddGrowableRow przyjmujących dwa parametry: numer kolumny/wiersza i proporcje:

# -*- coding: utf-8 -*-
import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, size = (900, 600))
        self.InitUI()
        self.Show()
    def InitUI(self):
        panel = wx.Panel(self)
        rows, cols = 3, 3
        sizer = wx.FlexGridSizer(rows = rows, cols = 0, hgap = 2, vgap = 10)
        for i in xrange(rows * cols):
            btn = wx.Button(panel, label = str(i))
            sizer.Add(btn, flag = wx.LEFT|wx.TOP)
            btn.SetMinSize((15 * i, 15 * i))
        sizer.AddGrowableCol(1, 1)
        sizer.AddGrowableRow(1, 1)
        sizer.AddGrowableCol(0, 2)
        sizer.AddGrowableRow(0, 2)
        panel.SetSizer(sizer)
app = wx.App()
MyFrame()
app.MainLoop()

Poniższy przykład prezentuje działanie klasy wx.GridBagSizer - jej główną zaletą jest to, że widgety mogą być rozciągnięte na wiele wierszy lub kolumn:

# -*- coding: utf-8 -*-
import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, size = (900, 400))
        self.InitUI()
        self.Show()
    def InitUI(self):
        panel = wx.Panel(self)
        rows, cols = 3, 3
        sizer = wx.GridBagSizer(hgap = 5, vgap = 5)
        sizer.Add(wx.Button(panel, label = 'pos(0, 0) span(1, 1)'), pos = (0, 0), span = (1, 1), flag = wx.EXPAND)
        sizer.Add(wx.Button(panel, label = 'pos(1, 0) span(1, 1)'), pos = (1, 0), span = (1, 1), flag = wx.EXPAND)
        sizer.Add(wx.Button(panel, label = 'pos(0, 1) span(1, 1)'), pos = (0, 1), span = (1, 1), flag = wx.EXPAND)
        sizer.Add(wx.Button(panel, label = 'pos(2, 0) span(1, 1)'), pos = (2, 0), span = (1, 1), flag = wx.EXPAND)
        sizer.Add(wx.Button(panel, label = 'pos(0, 2) span(1, 1)'), pos = (0, 2), span = (1, 1), flag = wx.EXPAND)
        sizer.Add(wx.Button(panel, label = 'pos(3, 0) span(1, 1)'), pos = (3, 0), span = (1, 1), flag = wx.EXPAND)
        sizer.Add(wx.Button(panel, label = 'pos(0, 3) span(1, 1)'), pos = (0, 3), span = (1, 1), flag = wx.EXPAND)
        sizer.Add(wx.Button(panel, label = 'pos(0, 4) span(1, 1)'), pos = (0, 4), span = (1, 1), flag = wx.EXPAND)
        sizer.Add(wx.Button(panel, label = 'pos(1, 1) span(2, 2)'), pos = (1, 1), span = (3, 2), flag = wx.EXPAND)
        sizer.Add(wx.Button(panel, label = 'pos(4, 0) span(1, 5)'), pos = (4, 0), span = (1, 5), flag = wx.EXPAND)
        sizer.Add(wx.Button(panel, label = 'pos(1, 3) span(2, 2)'), pos = (1, 3), span = (2, 2), flag = wx.EXPAND)
        panel.SetSizer(sizer)
app = wx.App()
MyFrame()
app.MainLoop()

wx.StaticBoxSizer dostarcza ramkę z tytułem dookoła obiektów warto podkreślić, że wymagane jest tu stworzenie obiektu klasy wx.StaticBox:

# -*- coding: utf-8 -*-
import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, size = (400, 400))
        self.InitUI()
        self.Show()
    def InitUI(self):
        panel = wx.Panel(self)
        rows, cols = 3, 3
        hBox = wx.BoxSizer(wx.HORIZONTAL)
        for i in ['A', 'B', 'C']:
            box = wx.StaticBox(panel, label = 'opis ' + i)
            sizer = wx.StaticBoxSizer(box, wx.VERTICAL)
            for j in [1, 2, 3]:
                sizer.Add(wx.Button(panel, label = i + str(j)))
            hBox.Add(sizer)
        panel.SetSizer(hBox)
app = wx.App()

Opiszę teraz jak do ramki dodać pasek statusu, jak kontrolować jego wygląd i zawartość. Klasa wx.Frame udostępnia metodę tworzącą statusbar i zwracającą go - CreateStatusBar(self, number, style, winid, name), gdzie number oznacza liczbę pól, które chcemy mieć w pasku statusu. Aby zmieniać zawartość pól na pasku można wykorzystać metodę SetStatusText(self, text, number) z klasy wx.StatusBar. Szerokość pól można zmieniać za pomocą metody SetStatusWidth przyjmującej listę liczb długości takiej ile jest pól w statusbarze zawierającą absolutne długości pól, jeśli podamy liczby ujemne to będą one oznaczały względny rozmiar pól, a wszystkie pola w sumie będą miały szerokość ramki. Dostępne są też metody GetStatusText. Ciekawe jest, że pola statusbara zachowują się jak stos udostępniając metody PopStatusText() i PushStatusText():

# -*- coding: utf-8 -*-
import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, size = (400, 400))
        self.InitUI()
        self.left = 0
        self.right = 0
        self.Show()
    def InitUI(self):
        self.CreateStatusBar(4).SetStatusWidths([-1, -2, -2, -1])
        panel = wx.Panel(self)
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        self.txt = wx.TextCtrl(panel)
        dodajButton = wx.Button(panel, label = 'Dodaj')
        cofnijButton = wx.Button(panel, label = 'Cofnij')
        sizer.Add(self.txt, proportion = 1)
        sizer.Add(dodajButton, proportion = 1)
        sizer.Add(cofnijButton, proportion = 1)
        panel.SetSizer(sizer)
        panel.Bind(wx.EVT_MOTION, self.OnMotion)
        panel.Bind(wx.EVT_LEFT_DOWN, self.OnLeft)
        panel.Bind(wx.EVT_RIGHT_DOWN, self.OnRight)
        self.Bind(wx.EVT_BUTTON, self.OnDodaj, dodajButton)
        self.Bind(wx.EVT_BUTTON, self.OnCofnij, cofnijButton)
    def OnDodaj(self, evt):
        self.GetStatusBar().PushStatusText(self.txt.GetValue(), 3)
    def OnCofnij(self, evt):
        self.GetStatusBar().PopStatusText(3)
    def OnLeft(self, evt):
        self.left += 1
        self.GetStatusBar().SetStatusText('Lewy klawisz : ' + str(self.left), 1)
    def OnRight(self, evt):
        self.right += 1
        self.GetStatusBar().SetStatusText('Prawy klawisz : ' + str(self.right), 2)        
    def OnMotion(self, evt):
        self.GetStatusBar().SetStatusText(str(evt.GetPositionTuple()))
app = wx.App()
MyFrame()
app.MainLoop()

Kolejnym ważnym elementem okna jest menu. Do tworzenia menu w wxPythonie używa się trzech klas: wx.MenuBar - reprezentuje pasek zawierający menu typu Plik, Edycja itp, wx.Menu - reprezentuje same menu czyli Plik, Edycja itp i w końcu wx.MenuItem reprezentuje poszczególne elementy menu na przykład Otwórz albo Nowy. Po kliknięciu obiekty klasy wx.MenuItem zgłaszają event EVT_MENU co pozwala obsługiwać ich kliknięcia. Przykład pokazuje, dwa sposoby dodawania skrótów klawiszowych (przez tablicę akceleracji i wpisanie w labelce menuitema), a także dwie metody dodawania menuitemów (najpierw tworzenie a później dodawanie lub dodawanie z tworzeniem w jednym kroku). Do dodawania menuitemów służą metody Append, Prepend i Insert, przykład pokazuje jak dodawać separatory. W przykładzie widać też, co się dzieje gdy wysyłamy do okna event EVT_CLOSE, taki event może zostać odrzucony (zawetowany), w metodzie OnClose można dopilnować sprzątania (zapisywania na dysk wyników pracy, oddawania zasobów takich jak połączenie z bazą danych albo plikami):

# -*- coding: utf-8 -*-
import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, size = (400, 400))
        self.InitUI()
        self.Show()
    def InitUI(self):
        menuBar = wx.MenuBar()
        fileMenu = wx.Menu()
        fileMenu.Append(102, u'&Otwórz', 'Tworzy nowy plik')
        fileMenu.AppendSeparator()
        fileMenu.Append(wx.ID_EXIT, '&Koniec', 'Zamyka program')
        fileMenu.Prepend(101, '&Nowy\tCtrl+N', 'Tworzy nowy plik')
        save = fileMenu.Insert(1, 103, '&Zapisz', 'Zapisuje na dysk')
        editMenu = wx.Menu()
        menuBar.Append(fileMenu, '&Plik')
        menuBar.Append(editMenu, '&Edycja')
        copyItem = wx.MenuItem(editMenu, 104, '&Kopiuj', 'Kopiuje')
        pasteItem = wx.MenuItem(editMenu, 105, '&Wklej', 'Wkleja')
        editMenu.AppendItem(copyItem)
        editMenu.AppendItem(pasteItem)
        self.SetMenuBar(menuBar)
        self.Bind(wx.EVT_MENU, self.OnMenu, id = 102, id2 = 105)
        self.Bind(wx.EVT_MENU, self.OnKoniec, id = wx.ID_EXIT)
        self.Bind(wx.EVT_CLOSE, self.OnClose)
        wx.Panel(self)
        acceltbl = wx.AcceleratorTable([(wx.ACCEL_CTRL, ord('S'), 103)])
        self.SetAcceleratorTable(acceltbl)
    def OnMenu(self, evt):
        wx.MessageBox(self.GetMenuBar().FindItemById(evt.GetId()).GetLabel())
    def OnKoniec(self, evt):
        self.Close()
    def OnClose(self, evt):
        if wx.MessageDialog(None, u'Czy chcesz zakończyć?', u'Zakończyć?', wx.YES_NO).ShowModal() == wx.ID_YES:
            self.Destroy()
        else:
            evt.Veto()
app = wx.App()
MyFrame()
app.MainLoop()

Warto podkreślić, że bez utworzenia pustego panelu powyższy przykład nie obsługiwałby skrótu klawiszowego Ctrl+S. Następny przykład pokaże jak dodawać podmenu i jakich typów mogą być menuitemy (znów każdy z nich możemy tworzyć na dwa sposoby):

# -*- coding: utf-8 -*-
import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, size = (400, 400))
        self.InitUI()
        self.Show()
    def InitUI(self):
        menuBar = wx.MenuBar()
        fileMenu = wx.Menu()
        fileMenu.AppendCheckItem(-1, 'Opcja 1')
        fileMenu.AppendItem(wx.MenuItem(fileMenu, -1, 'Opcja 2', kind = wx.ITEM_CHECK))
        fileMenu.AppendSeparator()
        fileMenu.AppendRadioItem(-1, 'Wersja 1')
        fileMenu.AppendItem(wx.MenuItem(fileMenu, -1, 'Wersja 2', kind = wx.ITEM_RADIO))
        fileMenu.AppendItem(wx.MenuItem(fileMenu, kind = wx.ITEM_SEPARATOR))
        submenu = wx.Menu()
        submenu.Append(-1, 'Podmenu 1')
        submenu.Append(-1, 'Podmenu 2')
        fileMenu.AppendMenu(-1, 'Podmenu', submenu)
        menuBar.Append(fileMenu, '&Plik')
        self.SetMenuBar(menuBar)
        
app = wx.App()
MyFrame()
app.MainLoop()

Warto dodać, że metody tworzące i dodające w jednym kroku menuitemy zwracają je. wxPython pozwala nam też tworzyć popupmenu czyli menu wyskakujące gdy klikniemy prawym klawiszem myszki na panel (wyświetlanie jest zaimplementowane jako obsługa eventu EVT_CONTEXT_MENU), oto przykład:

# -*- coding: utf-8 -*-
import wx

class MyFrame(wx.Frame):
    def __init__(self):
        super(MyFrame, self).__init__(None, size = (400, 400))
        self.InitUI()
        self.Show()
    def InitUI(self):
        self.popupmenu = wx.Menu()
        self.popupmenu.AppendCheckItem(-1, 'Opcja 1')
        self.popupmenu.AppendItem(wx.MenuItem(self.popupmenu, -1, 'Opcja 2', kind = wx.ITEM_CHECK))
        self.popupmenu.AppendSeparator()
        self.popupmenu.AppendRadioItem(-1, 'Wersja 1')
        self.popupmenu.AppendItem(wx.MenuItem(self.popupmenu, -1, 'Wersja 2', kind = wx.ITEM_RADIO))
        self.popupmenu.AppendItem(wx.MenuItem(self.popupmenu, kind = wx.ITEM_SEPARATOR))
        submenu = wx.Menu()
        submenu.Append(-1, 'Podmenu 1')
        submenu.Append(-1, 'Podmenu 2')
        self.popupmenu.AppendMenu(-1, 'Podmenu', submenu)
        self.panel = wx.Panel(self)
        self.panel.Bind(wx.EVT_CONTEXT_MENU, self.OnShowPopup)
    def OnShowPopup(self, evt):
        pos = evt.GetPosition()
        pos = self.panel.ScreenToClient(pos)
        self.panel.PopupMenu(self.popupmenu, pos)
        
app = wx.App()
MyFrame()
app.MainLoop()

"Programowanie dla Fizyków Medycznych"