Question

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))
Was it helpful?

Solution

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.

OTHER TIPS

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()

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()

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();
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top