Introduction
Currently, wx only provides (as Robin aptly described), 'app-modal' modality; a modal dialog can only block the entire application. In many applications with frames that are very independent, its sometimes desirable for a dialog to pop up a question that blocks those frames-- but leaves the rest of the application puttering along happily. Thus, the 'parent-modal' frame.
Process Overview
In a normal modal dialog, the execution of the parent code is halted at the 'ShowModal' method, but in this mechanism, callbacks are used to return the result of what the parent-modal frame has accomplished.
The basic strategy is:
The Show method on the parent-modal frame takes two arguments, one a callback, and the other a cancelCallback.
- When shown, the frame disables the parent, centers itself over the parent, and uses the wx.FRAME_FLOAT_ON_PARENT style to ensure it remains 'on top'.
- If the user clicks OK, the frame calls the callback provided earlier and passes it the value it retrieved.
- If the user clicks Cancel, the frame calls the cancelCallback provided earlier -- if one was provided at all, of course.
- In either case, the parent is enabled, and the frame is then destroyed.
Code Sample
1 import wx
2 class ParentModalDialog(wx.Frame):
3 def __init__(self, parent, title="My Dialog"):
4 wx.Frame.__init__(self, parent, -1, title=title, size=(200,100), style=wx.FRAME_FLOAT_ON_PARENT | wx.CAPTION | wx.FRAME_TOOL_WINDOW)
5 panel = wx.Panel(self, -1)
6
7 self.callback = None
8 self.cancelCallback = None
9
10 mainsizer = wx.BoxSizer(wx.VERTICAL)
11
12 ctrlsizer = wx.BoxSizer(wx.HORIZONTAL)
13 ctrlsizer.Add(wx.StaticText(panel, -1, "Foo:"), wx.ALL, 5)
14 self.textctrl = wx.TextCtrl(panel, -1)
15 ctrlsizer.Add(self.textctrl, 0, wx.ALL, 5)
16
17 mainsizer.Add(ctrlsizer, 1, wx.EXPAND)
18
19 buttonsizer = wx.BoxSizer(wx.HORIZONTAL)
20 okbutton = wx.Button(panel, wx.ID_OK, "OK")
21 cancelbutton = wx.Button(panel, wx.ID_CANCEL, "Cancel")
22 buttonsizer.Add(okbutton, 0, wx.ALIGN_CENTER | wx.ALL, 5)
23 buttonsizer.Add(cancelbutton, 0, wx.ALIGN_CENTER | wx.ALL, 5)
24
25 mainsizer.Add(buttonsizer)
26
27 panel.SetSizer(mainsizer)
28 panel.Layout()
29 mainsizer.Fit(self)
30 okbutton.SetDefault()
31 okbutton.Bind(wx.EVT_BUTTON, self.OnOK)
32 cancelbutton.Bind(wx.EVT_BUTTON, self.OnCancel)
33
34 def Show(self, callback=None, cancelCallback=None):
35 self.callback = callback
36 self.cancelCallback = cancelCallback
37
38 self.CenterOnParent()
39 self.GetParent().Enable(False)
40 wx.Frame.Show(self)
41 self.Raise()
42 def OnOK(self, event):
43 try:
44 if self.callback:
45 self.callback(self.textctrl.GetValue())
46 finally:
47 self.GetParent().Enable(True)
48
49 self.Destroy()
50
51 def OnCancel(self, event):
52 try:
53 if self.cancelCallback:
54 self.cancelCallback()
55 finally:
56 self.GetParent().Enable(True)
57
58 self.Destroy()
59 class TestFrame(wx.Frame):
60 def __init__(self, parent, **kwargs):
61 wx.Frame.__init__(self, parent, -1, size=(400,300), **kwargs)
62 panel = wx.Panel(self, -1)
63 sizer = wx.BoxSizer(wx.VERTICAL)
64
65 self.textctrl = wx.TextCtrl(panel, -1)
66 choice = wx.Choice(panel, -1, choices=['Choice-1', 'Choice-2', 'Choice-3'], size=(200,20))
67
68 sizer.Add(self.textctrl, 0, wx.EXPAND)
69 sizer.Add(choice)
70 btn = wx.Button(panel, -1, "Block Me")
71 sizer.Add(btn)
72
73 panel.SetSizer(sizer)
74 panel.Layout()
75
76 btn.Bind(wx.EVT_BUTTON, self.OnBlock)
77
78 def OnBlock(self, event):
79 dlg = ParentModalDialog(self)
80 dlg.Show(callback=self._ReceiveAnswer, cancelCallback=self._ReceiveCancellation)
81
82 def _ReceiveAnswer(self, value):
83 self.textctrl.SetValue(value)
84
85 def _ReceiveCancellation(self):
86 print "Dialog was cancelled! Oh well!"
87 class TestApp(wx.App):
88 def OnInit(self):
89 wx.InitAllImageHandlers()
90 frame1 = TestFrame(None, title="Testing Frame #1", pos=(20,20))
91 frame2 = TestFrame(None, title="Testing Frame #2", pos=(200,200))
92 frame1.Show(True)
93 frame2.Show(True)
94
95 self.SetTopWindow(frame1)
96 return True
97 def main(*args, **kwargs):
98 app = TestApp(0)
99 app.MainLoop()
100 if __name__ == "__main__":
101 main()
Comments
The sample above is runnable, to get a feel for how it works; you'll notice that even if you click 'Block me' on one frame, you can still click over and operate on the other.
... oh, and this was my first wxPyWiki contribution, so excuse any fudges -- StephenHansen 2007-12-04 08:10:55
I thought I would try using a python generator to keep from separating the launch of the dialog to handling the result. I'm not sure that this is any better but the logic for handling the dialog is now in two methods. Here's the code above as modified. -- MarkErbaugh
... Personally? I can't understand what it's really doing anymore, and you've now tightly coupled the parent-modal frame with the parent itself, instead of being able to use any number of 'parent-modal' dialogs that just work on top of the parent frame. In my original design, all the parent had to know was what the dialog would return as a result. It's not really modular anymore, nor able to be used with any arbitrary frame-- it has to be used with a frame now specially created for it. To each his own -- StephenHansen 2007-12-04 18:44:15 (... I'm editing back in my vertical whitespace in the above recipe, as the previous edit made it go away and it's so hard to read without it)
1 import wx
2 class ParentModalDialog(wx.Frame):
3 def __init__(self, parent, title="My Dialog"):
4 wx.Frame.__init__(self, parent, -1, title=title, size=(200,100), style=wx.FRAME_FLOAT_ON_PARENT | wx.CAPTION | wx.FRAME_TOOL_WINDOW)
5 panel = wx.Panel(self, -1)
6 self.callback = None
7 mainsizer = wx.BoxSizer(wx.VERTICAL)
8 ctrlsizer = wx.BoxSizer(wx.HORIZONTAL)
9 ctrlsizer.Add(wx.StaticText(panel, -1, "Foo:"), wx.ALL, 5)
10 self.textctrl = wx.TextCtrl(panel, -1)
11 ctrlsizer.Add(self.textctrl, 0, wx.ALL, 5)
12 mainsizer.Add(ctrlsizer, 1, wx.EXPAND)
13 buttonsizer = wx.BoxSizer(wx.HORIZONTAL)
14 okbutton = wx.Button(panel, wx.ID_OK, "OK")
15 cancelbutton = wx.Button(panel, wx.ID_CANCEL, "Cancel")
16 buttonsizer.Add(okbutton, 0, wx.ALIGN_CENTER | wx.ALL, 5)
17 buttonsizer.Add(cancelbutton, 0, wx.ALIGN_CENTER | wx.ALL, 5)
18 mainsizer.Add(buttonsizer)
19 panel.SetSizer(mainsizer)
20 panel.Layout()
21 mainsizer.Fit(self)
22 okbutton.SetDefault()
23 okbutton.Bind(wx.EVT_BUTTON, self.OnBtn)
24 cancelbutton.Bind(wx.EVT_BUTTON, self.OnBtn)
25 def OnBtn(self, event):
26 self.result = event.EventObject.GetId()
27 if self.callback <> None:
28 self.callback()
29 class TestFrame(wx.Frame):
30 def __init__(self, parent, **kwargs):
31 wx.Frame.__init__(self, parent, -1, size=(400,300), **kwargs)
32 panel = wx.Panel(self, -1)
33 sizer = wx.BoxSizer(wx.VERTICAL)
34 self.textctrl = wx.TextCtrl(panel, -1)
35 choice = wx.Choice(panel, -1, choices=['Choice-1', 'Choice-2', 'Choice-3'], size=(200,20))
36 sizer.Add(self.textctrl, 0, wx.EXPAND)
37 sizer.Add(choice)
38 btn = wx.Button(panel, -1, "Block Me")
39 sizer.Add(btn)
40 panel.SetSizer(sizer)
41 panel.Layout()
42 btn.Bind(wx.EVT_BUTTON, self.OnBlock)
43 def ParentModal(self):
44 self.Enable(False)
45 dlg = ParentModalDialog(self)
46 dlg.callback = self.x.next
47 dlg.CenterOnParent()
48 dlg.Show()
49 dlg.Raise()
50 yield None
51 if dlg.result == wx.ID_OK:
52 self.textctrl.SetValue(dlg.textctrl.GetValue())
53 else:
54 print "Dialog was cancelled! Oh well!"
55 dlg.Destroy()
56 self.Enable(True)
57 yield None
58 def OnBlock(self, event):
59 self.x = self.ParentModal()
60 self.x.next()
61 class TestApp(wx.App):
62 def OnInit(self):
63 wx.InitAllImageHandlers()
64 frame1 = TestFrame(None, title="Testing Frame #1", pos=(20,20))
65 frame2 = TestFrame(None, title="Testing Frame #2", pos=(200,200))
66 frame1.Show(True)
67 frame2.Show(True)
68 self.SetTopWindow(frame1)
69 return True
70 def main(*args, **kwargs):
71 app = TestApp(0)
72 app.MainLoop()
73 if __name__ == "__main__":
74 main()