简体   繁体   中英

The fundementals of wxPython

I'm trying to understand wxPython, but most of the documentation out there just presents programs in a monkey-see-monkey-do way without explaining the fundamentals of the library.

Consider this snippet of code:

import wx

class MyFrame(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, (-1, -1), wx.Size(250, 50))
        panel = wx.Panel(self, -1)
        box = wx.BoxSizer(wx.HORIZONTAL)
        box.Add(wx.Button(panel, -1, 'Button1'), 1 )
        box.Add(wx.Button(panel, -1, 'Button2'), 1 )
        box.Add(wx.Button(panel, -1, 'Button3'), 1 )
        panel.SetSizer(box)
        self.Centre()

class MyApp(wx.App):
     def OnInit(self):
         frame = MyFrame(None, -1, 'wxboxsizer.py')
         frame.Show(True)
         return True

app = MyApp(0)
app.MainLoop()

I see three containers here - A frame, a panel, and a box.

And then there are three buttons.

  1. Can someone explain which container goes inside which one?
  2. Does the panel go into the frame? If so, where is it added to the frame?
  3. What about the box? Does it go into the panel, or does the panel go into it?
  4. Were do the buttons go? Is it into the box?
  5. Why is panel.SetSizer() used in one place and box.Add() used in another place?

Let us slowly develop a wxPython application to see how it works.

This is the least amount of code required to make a wxPython application. It contains a wx.Frame (you may understand this as a window). There is nothing in the window. app.MainLoop() is the loop that captures any events such as mouse-clicks, closing or minimizing windows.

import wx

app = wx.App()

frame = wx.Frame(None, -1, 'A Frame')
frame.Show()

app.MainLoop()

A window on its own is not very interesting but is still fairly powerful. We can add things like menu items and titles and even select styles such as No Maximize button. Let's do all that.

import wx

class Frame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(Frame, self).__init__(*args, **kwargs)

        menubar = wx.MenuBar() # Create a menubar
        fileMenu = wx.Menu() # Create the file menu
        fitem = fileMenu.Append(wx.ID_EXIT, 'Quit', 'Quits application') # Add a quit line
        menubar.Append(fileMenu, '&File') # Add the File menu to the Menubar
        self.SetMenuBar(menubar) # Set the menubar as THE menu bar

        self.Bind(wx.EVT_MENU, self.OnQuit, fitem) # Bind the quit line

        self.Show() # Show the frame

    def OnQuit(self, e):
        self.Close()

app = wx.App()
Frame(None, -1, 'A Frame', style=wx.RESIZE_BORDER 
    | wx.SYSTEM_MENU | wx.CAPTION |  wx.CLOSE_BOX) # Some styles
app.MainLoop()

You'll notice things got a little more complicated fairly quickly. It's actually to help us stay organized. We moved our frame into its own class and we defined some characteristics. We asked for a menu, bound the menu item to the OnQuit() method which close the application. This is all at the most basic layer.

Let's add a panel. A panel is like a chalkboard. It sits on top of a wx.Frame (like a Chalkboard sits against a wall). Once we have a panel, we can start adding sizers and widgets.

import wx

class Frame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(Frame, self).__init__(*args, **kwargs)

        menubar = wx.MenuBar()
        fileMenu = wx.Menu()
        fitem = fileMenu.Append(wx.ID_EXIT, 'Quit', 'Quits application')
        menubar.Append(fileMenu, '&File')
        self.SetMenuBar(menubar)

        self.Bind(wx.EVT_MENU, self.OnQuit, fitem)

        panel = wx.Panel(self, -1) # Added a panel!

        self.Show()

    def OnQuit(self, e):
        self.Close()

app = wx.App()
Frame(None, -1, 'A Frame', style=wx.RESIZE_BORDER 
    | wx.SYSTEM_MENU | wx.CAPTION |  wx.CLOSE_BOX)
app.MainLoop()

You'll notice a slight difference depending on your platform. The window is now filled in, with a lighter colour. That's our panel. Now let's add some buttons.

import wx

class Frame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(Frame, self).__init__(*args, **kwargs)

        menubar = wx.MenuBar()
        fileMenu = wx.Menu()
        fitem = fileMenu.Append(wx.ID_EXIT, 'Quit', 'Quits application')
        menubar.Append(fileMenu, '&File')
        self.SetMenuBar(menubar)

        self.Bind(wx.EVT_MENU, self.OnQuit, fitem)

        panel = wx.Panel(self, -1)
        btn = wx.Button(panel, label='I am a closing button.') # Add a button
        btn.Bind(wx.EVT_BUTTON, self.OnQuit) # Bind the first button to quit
        btn2 = wx.Button(panel, label='I am a do nothing button.') # Add a second

        self.Show()

    def OnQuit(self, e):
        self.Close()

app = wx.App()
Frame(None, -1, 'A Frame', style=wx.RESIZE_BORDER 
    | wx.SYSTEM_MENU | wx.CAPTION |  wx.CLOSE_BOX)
app.MainLoop()

Now we have buttons that sit on the panel. But they look awful. They're stuck one on top of another. We could manually position them using the pos=(x,y) attribute but that's very tedious. Let's call in our friend the boxsizer.

import wx

class Frame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(Frame, self).__init__(*args, **kwargs)

        menubar = wx.MenuBar()
        fileMenu = wx.Menu()
        fitem = fileMenu.Append(wx.ID_EXIT, 'Quit', 'Quits application')
        menubar.Append(fileMenu, '&File')
        self.SetMenuBar(menubar)

        self.Bind(wx.EVT_MENU, self.OnQuit, fitem)

        panel = wx.Panel(self, -1)
        btn = wx.Button(panel, label='I am a closing button.')
        btn.Bind(wx.EVT_BUTTON, self.OnQuit)
        btn2 = wx.Button(panel, label='I am a do nothing button.')

        vbox = wx.BoxSizer(wx.VERTICAL) # Create a vertical boxsizer
        vbox.Add(btn) # Add button 1 to the sizer
        vbox.Add(btn2) # Add button 2 to the sizer

        panel.SetSizer(vbox) # Tell the panel to use this sizer as its sizer. 

        self.Show()

    def OnQuit(self, e):
        self.Close()

app = wx.App()
Frame(None, -1, 'A Frame', style=wx.RESIZE_BORDER 
    | wx.SYSTEM_MENU | wx.CAPTION |  wx.CLOSE_BOX)
app.MainLoop()

That's already much better! Let's see what a horizontal sizer looks like.

import wx

class Frame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(Frame, self).__init__(*args, **kwargs)

        menubar = wx.MenuBar()
        fileMenu = wx.Menu()
        fitem = fileMenu.Append(wx.ID_EXIT, 'Quit', 'Quits application')
        menubar.Append(fileMenu, '&File')
        self.SetMenuBar(menubar)

        self.Bind(wx.EVT_MENU, self.OnQuit, fitem)

        panel = wx.Panel(self, -1)
        btn = wx.Button(panel, label='I am a closing button.')
        btn.Bind(wx.EVT_BUTTON, self.OnQuit)
        btn2 = wx.Button(panel, label='I am a do nothing button.')

        vbox = wx.BoxSizer(wx.VERTICAL) # A vertical sizer
        hbox = wx.BoxSizer(wx.HORIZONTAL) # And a horizontal one?? but why?
        hbox.Add(btn) # Let's add the buttons first
        hbox.Add(btn2)

        panel.SetSizer(hbox) # see why we need to tell the panel which sizer to use? We might have two!

        self.Show()

    def OnQuit(self, e):
        self.Close()

app = wx.App()
Frame(None, -1, 'A Frame', style=wx.RESIZE_BORDER 
    | wx.SYSTEM_MENU | wx.CAPTION |  wx.CLOSE_BOX)
app.MainLoop()

Interesting! Notice how I kept our vbox? Let's try combining the two. We'll need lots more buttons and maybe some TextCtrls too.

import wx

class Frame(wx.Frame):
    def __init__(self, *args, **kwargs):
        super(Frame, self).__init__(*args, **kwargs)

        menubar = wx.MenuBar()
        fileMenu = wx.Menu()
        fitem = fileMenu.Append(wx.ID_EXIT, 'Quit', 'Quits application')
        menubar.Append(fileMenu, '&File')
        self.SetMenuBar(menubar)

        self.Bind(wx.EVT_MENU, self.OnQuit, fitem)

        panel = wx.Panel(self, -1)
        btn = wx.Button(panel, label='I am a closing button.')
        btn.Bind(wx.EVT_BUTTON, self.OnQuit)
        btn2 = wx.Button(panel, label='I am a do nothing button.')
        txt1 = wx.TextCtrl(panel, size=(140,-1))
        txt2 = wx.TextCtrl(panel, size=(140,-1))
        txt3 = wx.TextCtrl(panel, size=(140,-1))
        btn3 = wx.Button(panel, label='I am a happy button.')
        btn4 = wx.Button(panel, label='I am a bacon button.')
        btn5 = wx.Button(panel, label='I am a python button.')


        # So many sizers! 
        vbox = wx.BoxSizer(wx.VERTICAL) 
        hbox1 = wx.BoxSizer(wx.HORIZONTAL)
        hbox2 = wx.BoxSizer(wx.HORIZONTAL)
        hbox3 = wx.BoxSizer(wx.HORIZONTAL)
        hbox4 = wx.BoxSizer(wx.HORIZONTAL)
        hbox1.Add(btn)
        hbox1.Add(btn2)
        hbox1.Add(txt1)
        hbox2.Add(txt2)
        hbox2.Add(txt3)
        hbox2.Add(btn3)
        hbox3.Add(btn4)
        hbox4.Add(btn5)
        vbox.Add(hbox1)
        vbox.Add(hbox2)
        vbox.Add(hbox3)
        vbox.Add(hbox4)

        panel.SetSizer(vbox)

        self.Show()

    def OnQuit(self, e):
        self.Close()

app = wx.App()
Frame(None, -1, 'A Frame', style=wx.RESIZE_BORDER 
    | wx.SYSTEM_MENU | wx.CAPTION |  wx.CLOSE_BOX)
app.MainLoop()

We've added buttons and text widgets to horizontal sizers first, and then to vertical sizers second. We've got sizers in sizers, which is a very common way of doing things in wxPython.

__________________________
|__hbox1_|_______________|  \
|_hbox2____|______|____|_|   \___VBOX
|___hbox3______|_________|   /
|_______|__hbox4_|_______|  /

Sort of like that. In each hbox we have a number of widgets. Up to you how many you want. Hope this helps.

wxPython is complex, but so are other GUI toolkits. Let's break it down a bit. To answer the first question, the usual layout of the wxPython GUI is one of the following:

frame -> panel -> sizer -> widgets
frame -> sizer -> panel -> sizer -> widgets

I usually go with the first one. If the layout is complex, I may nest sizers, so I end up with something like this:

frame -> panel -> sizer -> sizer1 -> widgets
                        -> sizer2 -> widgets

2) The first panel should always be added to the frame as its sole widget:

wx.Frame.__init__(self, None, title="Test")
panel = wx.Panel(self)

3) The boxSizer usually goes into the panel. I usually have a top level boxSizer that I give to the panel and then create nested sizers inside of it.

4) The buttons and other widgets go in BOTH the panel and the sizer! You set the button's parent to the panel, then to layout the widgets inside the panel, you put them in the panel's sizer object. If you were to set the button's parent as the frame, you would have a mess.

5) SetSizer is used to tell wxPython which widget the sizer belongs to. In your code, you give the panel the box sizer instance. The Add() method of the sizer object is used to add widgets (and sizers) to the sizer itself.

I hope that answers all your questions. You might also find the following article useful as it links to the majority of the documentation I use for wx: http://www.blog.pythonlibrary.org/2010/12/05/wxpython-documentation/

I would strongly recommend getting hold of a copy of wxPython in Action by Noel Rappin and Robin Dunn. Since Robin is one of the main authors/architects of wxPython you will get very good insights and clear explanations.

NB Before anybody asks I have absolutely no commercial connection with the book, authors or publishers.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM