Sam Peterson
PLEASE EDIT MY HOMEPAGE!! Please correct anything that's wrong. I want the Truth, the Whole Truth, and Nothing But the Truth! It's my hope that by documenting my experience with wxPython on the wiki I can help out other novice users similar to myself.
Thank you Robin for answering many of my questions! What a community; the head developers take time out of their busy schedules to help the initiates such as myself . I'm flattered.
Email: <peabodyenator AT SPAMFREE gmail DOT com>
I've always been a hobbiest programmer and I'm finally pursuing my Computer Science B.S. at the University of California, Davis. I'm currently familiarizing myself with wxPython because I'm a weird breed of Linux user who loves guis (even though I do most things from the shell).
I use Emacs. I'll be posting elisp stuff I make for wxPython if I get around to writing any elisp stuff :).
Whys, What's, When's, Where's, and How's
I'm a terrible GUI programmer who wants to become a better GUI programmer . In learning wxPython I've come across things that weren't obvious to me. I document them here. Please, if the experienced gurus could answer my questions and correct my misconceptions I would be eternally grateful.
Why do I need to use a wx.Frame?
One of the first example programs I tried only contained a wx.Dialog. The program didn't seem to exit properly. I found out from further experimenting tha wx.Frames didn't have this problem. I still don't know why. My guess is that wx.Dialog doesn't contain the proper plumbing in its Destroy() to close the program.
Robin: Dialog-only apps are legal and supported. I expect that you were doing something else that caused the app to think that there were still top-level windows in existence. There are also ways to force the exit if needed.
1 import wx
2
3 class Dialog(wx.Dialog):
4 def __init__(self, *args, **kw):
5 wx.Dialog.__init__(self, *args, **kw)
6 wx.Button(self, wx.ID_OK, pos=(20,20))
7 wx.Button(self, wx.ID_CANCEL, pos=(20, 60))
8
9 app = wx.App()
10 dlg = Dialog(None, title="Dialog-only")
11 dlg.ShowModal()
12 dlg.Destroy()
13 app.MainLoop()
Sam: Well what do you know? Guess it had been a while since I'd tried. I'm not sure where the code was I was using before, I'll try and find it. Knowing myself, I probably did something stupid and weird . Here's another question though, since both wx.Frame and wx.Dialog are containers, what are the pros and cons of each? Just from looking at it, the main difference seems to be that using a dialog will mean that your program will block in ShowModal. Dialogs also don't seem to allow themselves to be resized.
Why and When do I call SetAutoLayout?
When first learning wxPython I tried copying and pasting the examples from the Demo and working with them. I found SetAutoLayout a confusing function. Do I call it on a Frame? On a panel? On a panel embedded in a frame? What does it do exactly?
Answer: SetAutoLayout turns on the auto-resizing plumbing of panels that works with sizers. Apparently I don't even need to call it. Supposedly since wxPython 2.4, SetSizer automatically calls SetAutoLayout. Regardless, don't call it on frames Xp.
Robin: It is mainly a historical artifact. Before sizers there were layout constraints and so when sizers were introduced the auto layout flag was added so one or the other of them could be active or not. Since then it was decided that you would never do a SetSizer and not want to do a SetAutoLayout at the same time so it was refactored to do it for you. You can still turn off the auto layout flag if desired, but I don't think anybody ever does.
Where do sizer's go?
Just what can have a sizer?
Answer: Panels mostly. Not frames. What else?
Robin: Any container window, including frames, can have a sizer. The purpose of the sizer is to assist windows in sizing and positioning their child windows, so anything that can have child windows can have a sizer. In theory that means any class derived from wx.Window, but there really isn't any practical application of putting child windows on a wx.Button, so you would typically only do it for containers. BTW, a wx.Window can have a sizer too but it doesn't check the auto layout flag by default, so to get sizers to work with a wx.Window you need to catch its EVT_SIZE event and call self.Layout from the handler.
Sam: This is very helpful. Easy to read and well put.
Why would I call Fit?
In many of the code examples I saw the Fit member function of sizers being called. I sort of assumed these always had to be called.
Answer: Fit is meant to resize a sizer's parent window to its contents. It only needs to be called if you need to do that.
What the heck is the difference between wx.EXPAND and wx.GROW?
I've seen both in example code. I pretty much know what wx.EXPAND does. What does wx.GROW do?
Robin: Nothing. They are aliases for each other.
Where is the documentation on filters for the wx.FileDialog?
I'd like to figure out how to have more than one globbing file pattern like *.jpg,*.gif,*.png.
Robin: The sample in the C++ docs for wxFileDialog shows two patterns. One for *.bmp and *.gif, and another for *.png.
Sam: Aha! Thanks for the C++ docs tip. Using a semi-colon to seperate the patterns lets you use more than one glob on the same pattern. Thanks.
How many sizers are there?
BoxSizer? GridSizer? FlexGridSizer? RowColSizer? GridBagSizer? Are there more? When and where should I use 'em?
Robin: You missed StaticBoxSizer. RowColSizer has pretty much been obsoleted by GridBagSizer. There can be other sizers as well, as they can be implemented in Python by deriving from wx.PySizer. (In fact GridSizer and FlexGridSizer existed in Python before they did in C++.) When to use each kind depends on what the needs of the layout are.
When do I need to call Layout?
The Layout function on Sizers seems to need to be called manually at times. In what situations do I need to do this? I'm having problems where embedding a panel within a panel seems to sometimes require that I call the Layout method on the child manually. I thought I shouldn't have to do this. Am I doing something wrong? Nevermind, solved my problem, left out a SetSizer line while I was copying and pasting text.
Robin: There are times when it is needed. Keep in mind that Layout is normally called from the EVT_SIZE handler of the window that the sizer belongs to, so if something is done that needs a new layout but does not result in the window changing size, then you'll need to call Layout manually. For example, if the font of a wx.StaticText is changed then a new layout may need to be done to accomodate the new label size, but since the parent Panel has not changed size the relayout will not automatically be done.
Mac OS X issues
I run Fedora Core 4 on an Athlon as my primary machine, but I also have an iBook running OS X 10.3.9 with Python 2.4 and wxPython 2.6.0.0. I've noticed some issues:
GridBagSizers don't work. Resizing their parent windows caused them to continually grow on my version. I had to rewrite something because of that.
- wx.Gauge seems to have one vertical size. Super-skinny. I can't get it to be bigger than that.
- Everything needs more borders or the layout is too clumped together.
- Menus don't seem to work...?
Robin: 2.6.0.0 is pretty old. You should upgrade.
If it still happens in the new version please send a note about it and a small sample that shows the problem to the wxPython-users [MailingLists mail list].
- If the native widget doesn't support changing size then it won't change size.
- See the Apple human Interface Guide. wxMac tries to follow the standards set there wherever possible. Mac people are usually pretty picky and if an app doesn't follow the HIG they tend to either complain a lot or laugh it out of town.
- In what way? What kind of menus? What doesn't work? Again a note and a sample app sent to wxPython-users will help to either explain how to properly do what you want to do or will serve as a test-case for somebody to fix a bug.
Sam: Will do as soon as I get the chance.
My simple programs
I'll probably make this a subpage eventually since it crowds my homepage. I don't intend to make the Wiki my own personal sourceforge either. I hope that someone finds the code useful for learning wxPython or, even better, someone looks at this code, pukes, and tells me how to write better code.
Warning: Hopefully my programs aren't too offensive. One of them (currently only one program here anyway , more to come...) does involve downloading the Model of the Day off playboy.com. I was bored. You've been warned.
Everything's LGPL version 2 unless stated otherwise. If it can be that is. I realize TheRules mean I relinquish copyright of this page to the wxPython folks. There seems to be questions regarding that, but anyway I don't care too much about all the legal nonsense.
1 #!/usr/bin/env python
2 """
3 Model of the day app.
4
5 Heard of message of the day? Well this is model of the day.
6 Playboy.com stores about a months worth of images on their web site
7 that can be accessed simply by changing the date on the url. This app
8 takes the user's input, or randomly selects a date and tries to
9 download the image.
10
11 This is my first significant wxPython program and it's merely used as
12 a test bed for me to get used to wxPython."""
13
14 __author__ = "Sam Peterson <peabodyenator@gmail.com>"
15 __version__ = 0.01
16
17 import wx, time, random, urllib2, cStringIO, os
18
19 class ImagePanel(wx.Panel):
20 def __init__(self, parent, data, image, *args, **kwargs):
21 wx.Panel.__init__(self, parent, *args, **kwargs)
22
23 self.filter = "Image Files (*.jpg,*.gif,*.png)|*.jpg"
24 try: self.dirname = self.GetParent().GetParent().dirname
25 except AttributeError: self.dirname = os.path.expanduser('~')
26 self.data = data
27 self.sizer = wx.BoxSizer(wx.VERTICAL)
28 self.SetSizer(self.sizer)
29 self.SetAutoLayout(True)
30 self.bmp = image.ConvertToBitmap()
31 self.sbmp = wx.StaticBitmap(self, bitmap=self.bmp)
32 self.sizer.Add(self.sbmp)
33
34 # button sizer
35 button_sizer = wx.BoxSizer(wx.HORIZONTAL)
36 self.sizer.Add(button_sizer, flag=wx.ALL, border=2)
37
38 # buttons
39 self.ok_btn = wx.Button(self, label="&Save")
40 button_sizer.Add(self.ok_btn, flag=wx.ALL, border=2)
41 self.close_btn = wx.Button(self, label="&Close")
42 button_sizer.Add(self.close_btn, flag=wx.ALL, border=2)
43
44 # events
45 self.close_btn.Bind(wx.EVT_BUTTON, self.OnClickClose)
46 self.ok_btn.Bind(wx.EVT_BUTTON, self.OnClickSave)
47 self.sizer.Fit(self.GetParent())
48
49 def OnClickSave(self, event):
50 dlg = wx.FileDialog(self.GetParent(), defaultDir=self.dirname,
51 wildcard=self.filter, style=wx.SAVE)
52 ok = False
53 if dlg.ShowModal() == wx.ID_OK:
54 ok = True
55 try: self.GetParent().GetParent().dirname = dlg.GetDirectory()
56 except: pass
57 for each_file in dlg.GetPaths():
58 if os.path.splitext(each_file)[1].upper() != ".JPG":
59 each_file = each_file + ".jpg"
60 output = file(each_file, "wb")
61 output.write(self.data)
62 output.close()
63
64 dlg.Destroy()
65
66 if ok: self.GetParent().Close()
67
68 def OnClickClose(self, event):
69 self.GetParent().Close()
70
71 class ProgressPanel(wx.Panel):
72 def __init__(self, *args, **kwargs):
73 wx.Panel.__init__(self, *args, **kwargs)
74
75 # variables
76 self.downloading = False
77 self.amount = 0
78 self.max_range = 100
79 self.sizer = wx.BoxSizer(wx.VERTICAL)
80
81 # text sizer
82 text_sizer = wx.BoxSizer(wx.HORIZONTAL)
83
84 # text controls
85 self.day_lbl = wx.StaticText(self, label="Day:")
86 text_sizer.Add(self.day_lbl)
87
88 self.day_txt = wx.TextCtrl(self, value="%s" % time.strftime("%d"))
89 text_sizer.Add(self.day_txt)
90
91 text_sizer.Add((-1,-1),1)
92
93 self.month_lbl = wx.StaticText(self, label="Month:")
94 text_sizer.Add(self.month_lbl, flag=wx.ALIGN_RIGHT)
95
96 self.month_txt = wx.TextCtrl(self, value="%s" % time.strftime("%m"))
97 text_sizer.Add(self.month_txt)
98
99 self.sizer.Add(text_sizer, 1, wx.EXPAND | wx.RIGHT)
100
101 # progress bar
102 self.progress = wx.Gauge(self, range=100)
103 self.sizer.Add(self.progress, flag=wx.EXPAND | wx.ALL)
104
105 # label sizer
106 label_sizer = wx.BoxSizer(wx.HORIZONTAL)
107 self.sizer.Add(label_sizer)
108
109 # download label
110 self.download_label = wx.StaticText(self, label="Downloading:")
111 label_sizer.Add(self.download_label, flag=wx.EXPAND)
112
113 # url area
114 self.url_label = wx.StaticText(self, label="None")
115 label_sizer.Add(self.url_label, flag= wx.EXPAND)
116
117 # button sizer
118 button_sizer = wx.BoxSizer(wx.HORIZONTAL)
119 self.sizer.Add(button_sizer)
120
121 # view image button
122 self.view_image_btn = wx.Button(self, label="View &Image")
123 #self.view_image_btn.Disable()
124 button_sizer.Add(self.view_image_btn, flag=wx.ALL, border=2)
125
126 # stop button
127 self.stop_btn = wx.Button(self, label="&Stop")
128 self.stop_btn.Disable()
129 button_sizer.Add(self.stop_btn, flag=wx.ALL, border = 2)
130
131 # random button
132 self.x_btn = wx.Button(self, label="&Random Porn!")
133 button_sizer.Add(self.x_btn, border=2, flag=wx.EXPAND | wx.ALL)
134
135 # size label
136 self.size_lbl = wx.StaticText(self, label="Size = XXXXXXXXXXXX")
137 button_sizer.Add(self.size_lbl, flag=wx.EXPAND | wx.ALL, border=2)
138
139 # set events
140 self.stop_btn.Bind(wx.EVT_BUTTON, self.OnClickStop)
141 self.x_btn.Bind(wx.EVT_BUTTON, self.OnClickX)
142 self.view_image_btn.Bind(wx.EVT_BUTTON, self.OnClickViewImage)
143 self.month_txt.Bind(wx.EVT_CHAR, self.OnClickViewImage)
144 self.day_txt.Bind(wx.EVT_CHAR, self.OnClickViewImage)
145
146 self.SetSizer(self.sizer)
147 self.SetAutoLayout(True)
148
149 # set growable row and column
150 #self.sizer.AddGrowableRow(0)
151 #self.sizer.AddGrowableCol(2)
152
153 def OnClickViewImage(self,event):
154 event_type = event.GetEventType()
155 if event_type == wx.wxEVT_CHAR:
156 if event.KeyCode() == wx.WXK_RETURN:
157 self._view_image()
158 else:
159 self._view_image()
160 event.Skip()
161
162 def _view_image(self):
163 day = int(self.day_txt.GetValue())
164 month = int(self.month_txt.GetValue())
165 self.BeginDownload(day, month)
166
167
168 def OnClickStop(self, event):
169 if self.stop_btn.GetLabel() == "Stop":
170 self.downloading = False
171 self.stop_btn.SetLabel("&Resume")
172 else:
173 self.downloading = True
174 self.Slurp()
175 self.stop_btn.SetLabel("&Stop")
176
177 def Slurp(self, *arg, **kwargs):
178 """Slurp up the image in the background via Future Calls"""
179 try:
180 if self.downloading:
181 data = self.socket.read(4096)
182 amount_read = len(data)
183 if data:
184 self.data.write(data)
185 self.amount += amount_read
186 self.progress.SetValue(int((float(self.amount) / self.content_length) * 100))
187 self.future_call.Restart(10)
188 else:
189 self.data.seek(0)
190 self._clean()
191 image = wx.ImageFromStream(self.data)
192 self.data.seek(0)
193 size = (image.GetWidth(), image.GetHeight() + 80)
194 frame = wx.Frame(self, title="Yowza!", size=size)
195 img_panel = ImagePanel(frame, self.data.read(), image)
196 frame.Show(True)
197 except IOError, e:
198 self._clean()
199 self.Complain(e)
200
201 def _clean(self):
202 self.progress.SetValue(0)
203 self.downloading = False
204 self.amount = 0
205 self.size_lbl.SetLabel("Size = 0")
206 self.url_label.SetLabel("None")
207 self.stop_btn.Disable()
208
209 def Complain(self, message):
210 dlg = wx.MessageDialog(self, style=wx.OK,
211 message="I can'a do it captin'!\n%s" % str(message),
212 caption="Trouble!")
213 dlg.ShowModal()
214 dlg.Destroy()
215
216
217 def BeginDownload(self, day=None, month=1):
218 """Starts a not-really-but-sort-of asynchronous network download"""
219 if day == None:
220 day = random.randint(1,30)
221 month = random.randint(1,int(time.strftime("%m")))
222 if self.downloading: return
223 else:
224 self.stop_btn.Enable()
225 date = time.strftime("%y%m%d", (int(time.strftime("%y")),month,day,1,1,1,1,1,1))
226 url = 'http://playboy.com/sex/day/%s/imx/motd.jpg' % date
227 self.url_label.SetLabel(url)
228 try: self.socket = urllib2.urlopen(url)
229 except IOError, e:
230 self._clean()
231 self.Complain(e)
232 return
233 self.downloading = True
234 info = self.socket.info()
235 self.content_length = int(info['content-length'])
236 self.size_lbl.SetLabel("Size = %d" % self.content_length)
237 self.data = cStringIO.StringIO()
238 self.amount = 0
239 self.future_call = wx.FutureCall(20, self.Slurp, "test")
240
241 def OnClickX(self, event):
242 """Starts a not-really-but-sort-of asynchronous network download"""
243 if self.downloading: return
244 else:
245 self.BeginDownload()
246 def UpdateProgress(self, amount):
247 self.amount += amount
248 self.progress.SetValue(self.amount % self.max_range)
249 def ClearProgress(self):
250 self.progress.SetValue(0)
251
252 def test_dialog():
253 app = wx.PySimpleApp()
254 frame = wx.Dialog(None)
255 #panel = ProgressPanel(frame)
256 frame.ShowModal()
257 app.MainLoop()
258 app.Exit()
259
260
261 def main():
262 app = wx.PySimpleApp()
263 frame = wx.Frame(None, title='Test suite')
264 panel = ProgressPanel(frame)
265 panel.sizer.Fit(frame)
266 frame.SetMinSize(frame.GetSize())
267 frame.Show(True)
268 app.MainLoop()
269 app.Exit()
270
271 if __name__ == '__main__':
272 main()
273 #test_dialog()