9

I have a wx.Dialog subclass that needs to perform a couple of cleanup operations when the user clicks the OK button. The wx.Dialog documentation says that clicking OK or Cancel should emit an EVT_CLOSE event:

EVT_CLOSE: The dialog is being closed by the user or programmatically (see Window.Close ). The user may generate this event clicking the close button (typically the ‘X’ on the top-right of the title bar) if it’s present (see the CLOSE_BOX style) or by clicking a button with the ID_CANCEL or ID_OK ids.

I’m using WX 2.9.5.0 (via wxPython), however, and when I click OK or Cancel in this test application the OnClose method is not called. OnClose is called when I click the system’s close button (I’m using OS X). Am I implementing this event-handling wrong or does wx.Dialog really not conform to its documentation? And in the latter case, what’s the best way to intercept a click on the OK button?

from __future__ import print_function
import wx

class TestDialog(wx.Dialog):
    def __init__(self, parent):
        wx.Dialog.__init__(self, parent, title='Test Dialog')

        sizer = wx.BoxSizer(wx.VERTICAL)

        message = wx.StaticText(self, wx.NewId(), 'This is some dummy text')
        sizer.Add(message)

        ok_button = wx.Button(self, wx.ID_OK, 'OK')
        cancel_button = wx.Button(self, wx.ID_CANCEL, 'Cancel')

        btn_sizer = self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL)
        btn_sizer.Add(cancel_button)
        btn_sizer.Add(ok_button)
        sizer.Add(btn_sizer)

        self.SetSizer(sizer)

        self.Bind(wx.EVT_CLOSE, self.OnClose)

    def OnClose(self, event):
        print('In OnClose')
        event.Skip()

if __name__ == '__main__':
    app = wx.App(False)

    dialog = TestDialog(None)
    result = dialog.ShowModal()
    print('Result: {}'.format(result))
bdesham
  • 15,430
  • 13
  • 79
  • 123

4 Answers4

4

When you click the Ok or Cancel buttons on a modal dialog the dialog is not closed with Close, instead it is ended with EndModal so the EVT_CLOSE event is not sent. Code that needs to run when a modal dialog is completed normally is usually put after the call to ShowModal. I think in this case that the documentation is incorrect.

OTOH, if the dialog is shown modeless (with Show instead of ShowModal) then they should be closed with Close and you will get the EVT_CLOSE event.

Mike Driscoll
  • 32,629
  • 8
  • 45
  • 88
RobinDunn
  • 6,116
  • 1
  • 15
  • 14
  • 1
    “Code… is usually put after the call to ShowModal.” In my case, the code that needs to be run is the responsibility of the Dialog subclass, not of the code that calls `ShowModal`. But I don’t want to use `Show` because I want to prevent interaction with the rest of my application. Any ideas? – bdesham Jan 27 '14 at 21:54
  • I ended up wrapping my Dialog subclass in another class that does the actual ShowModal call, performs the extra logic, and returns whatever value ShowModal returned. It’s not super clean but it works. – bdesham Jan 28 '14 at 18:04
1

This is because Destroy() would be called instead of Close(). EVT_CLOSE would not be raised without Close(). You can try following code.

import wx
class TestDialog(wx.Dialog):
    def __init__(self, parent):
        wx.Dialog.__init__(self, parent, title='Test Dialog')

        sizer = wx.BoxSizer(wx.VERTICAL)

        message = wx.StaticText(self, wx.NewId(), 'This is some dummy text')
        sizer.Add(message)

        ok_button = wx.Button(self, wx.ID_OK, 'OK')
        cancel_button = wx.Button(self, wx.ID_CANCEL, 'Cancel')

        btn_sizer = self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL)
        btn_sizer.Add(cancel_button)
        btn_sizer.Add(ok_button)
        sizer.Add(btn_sizer)

        self.SetSizer(sizer)
        self.Bind(wx.EVT_CLOSE, self.OnClose)
        self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)

    def OnClose(self, event):
        print 'In OnClose'
        event.Skip()

    def OnDestroy(self, event):
        print 'In OnDestroy'
        event.Skip()

if __name__ == '__main__':
    app = wx.App(False)
    frame = wx.Frame(None)
    frame.Show()
    dialog = TestDialog(frame)
    result = dialog.ShowModal()
    dialog.Close(True)
    print 'Result: {}'.format(result)
    app.MainLoop()
Mike Driscoll
  • 32,629
  • 8
  • 45
  • 88
Jerry_Y
  • 1,724
  • 12
  • 9
  • 2
    The problem with waiting until the EVT_WINDOW_DESTROY is that by that time the dialog is in the process of being destroyed, and so it is not safe to do very much with it. With a EVT_CLOSE or in the code after ShowModal the dialog will still exist and can be programatically interacted with. (Or in the case of EVT_CLOSE, it's even possible to prevent the closing.) – RobinDunn Jan 27 '14 at 20:57
1

I just had this problem and based on the answer here I came up with the solution below. This was tested using wxPython 4.1.0 and python 3.8.5.

In essence, all wx.EVT_BUTTON events in the wx.Dialog window are bound to a method in the wx.Dialog window. In the method, the ID of the clicked button is compared to the IDs used to create the buttons in the wx.Dialog and actions are taken accordingly.

In the example below, clicking the OK button generates a random number. The wx.Dialog only closes if the random number is > 0.5.

import random
import wx

class Example(wx.Frame):
    """Main window """
    def __init__(self, parent):
        super().__init__(
            parent, 
            title = 'Intercept wx.Dialog button events',
            size  = (260, 180),
        )
        self.btn = wx.Button(self, label = 'Show Dialog Window')
        self.Sizer = wx.BoxSizer()
        self.Sizer.Add(self.btn, 0, wx.ALIGN_CENTER|wx.ALL, 5)
        self.SetSizer(self.Sizer)
        self.btn.Bind(wx.EVT_BUTTON, self.OnDialog)
        self.Centre()
        self.Show()

    def OnDialog(self, event):
        with MyDialog(self) as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                print("Dialog closed with OK - Message from Main Window")
                print("")
            else:
                print("Dialog closed with Cancel - Message from Main Window")
                print("")

class MyDialog(wx.Dialog):
    def __init__(self, parent):
        super().__init__(
            parent, 
            title = 'My Dialog',
            size  = (220, 90),
        )
        self.btnSizer = self.CreateStdDialogButtonSizer(
            wx.OK|wx.CANCEL
        )
        self.Sizer = wx.BoxSizer()
        self.Sizer.Add(self.btnSizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
        self.SetSizer(self.Sizer)
        self.CentreOnParent()
        # HERE IS THE TRICK, Get all Button events
        self.Bind(wx.EVT_BUTTON, self.OnButton)

    def OnButton(self, event):
        """Process the OK button click"""
        clicked_button_id = event.GetEventObject().GetId()
        if clicked_button_id == wx.ID_OK:
            print("Processing OK button click")
            if (n := random.random()) > 0.5:
                #--> Everything fine, window will close
                print(n, "> 0.5. Ok, closing window")
                pass
            else:
                #--> Error, window will remain open
                print(n, "< 0.5. Not Ok, window remains open")
                return False 
        else:
            print("Processing Cancel button click")
        # Skip event to make sure default behavior is executed
        event.Skip()

if __name__ == '__main__':
    app = wx.App()
    Example(None)
    app.MainLoop()
Mr_and_Mrs_D
  • 32,208
  • 39
  • 178
  • 361
kbr85
  • 1,416
  • 12
  • 27
0

For anybody looking for the answer to a similar question... you could just capture the OK button click event and only call event.Skip() when you want the form to close. If you don't call it, Destroy() won't be called. I don't know how to write this in wxPython but in C++ it would be:

void MyForm::OnOKButtonClick(wxCommandEvent& event)
{
    // check that the inputs are valid
    if (!DoChecks())
    {
        // show error message; 
        // by not calling event.skip() here we force the window to remain open
        wxMessageBox("Uh oh", "Invalid Input", wxOK | wxICON_ERROR);
    }
    else
    {
        event.Skip();
    }
}