Virtual Developer Workshop: Containerized Development with Docker
The Red X, ControlBox, or whatever you call it, can be pesky. The ControlBox closes a window in a WinForms application. That's what it's for. If that form is the last open form or the main form, your application closes. If you handle the FormClosing event, you can Cancel the close operation. But, what happens if you write something like a wizard and the current form is not the main form? The current form closes, but now the user is left staring at a blank screen.
In a wizard scenario, what the user probably intends is that they don't want to complete the wizard; they want to bail. Well, you probably close or hide the form when you navigate between wizard forms and you definitely close the form when the user clicks the ControlBox. The former means navigate to the next or previous form; clicking the ControlBox (usually) means quit altogether.
The scenario is that you want to create a wizard. If the user clicks next or previous, the current form closes and navigation happens. If the user clicks a Cancel button, the wizard stops. If the user presses the Escape keym the wizard shuts down. Finally, if the user presses the ControlBox, the wizard again shuts down. Here is a summary:
- Any escape means stop the wizard.
- Any Cancel button press stops the wizard.
- Clicking the ControlBox means stop the wizard (only works by default on the main form).
- Pressing Alt+F4 is equivalent to the ControlBox close behavior.
Making Escape Mean Cancel
By default, pressing escape doesn't do anything on a Windows form. However, if you add a button to a form, you can indicate that the behavior button represents the Form's cancel behavior. Set the Form.CancelButton (in the Properties window) to your cancel button. Now, pressing Escape will run your Cancel behavior.
For your wizard, the Cancel button as indicated in the previous section means to stop the wizard. You can affect this by writing Application.Exit() in the button's click event handler. Coding the Cancel button event and associating that button with the Form.CancelButton property takes care of Items 1 and 2 in the previous section.
Making the ControlBox Mean Exit
The WndProc method is the message pump handler for WinForms applications. You can override this method in your forms, and if used sparingly it can afford you a bit of extra coolness.
When you click the minimize, maximize, and ControlBox buttons, Windows sends a WM_SYSCOMMAND message. If you click the ControlBox (see Figure 1), the message is WM_SYSCOMMAND and the WParam is SC_CLOSE. Use the FormClosing event to determine when your form is closing and check for WM_SYSCOMMAND and SC_CLOSE in the WndProc to determine whether a form is closing because the ControlBox was clicked.
Figure 1: The ControlBox button.
Listing 1 contains a do-nothing form that represents the main form. Click Next to go to the second form in the wizard. Click Cancel (or the ControlBox) to quit. Listing 2 contains a form that represents a subsequent wizard step. Click Back to return to Form1. Click Cancel or the ControlBox to shut down the wizard.
Listing 1: A Basic do-nothing form that is a stub for the main form in a wizard.
Public Class Form1 Private Sub Button2_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button2.Click Close() End Sub Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click Hide() Dim form As Form2 = New Form2 form.Show() End Sub End Class
Notice that in Listing 1 Form1 is only hidden. If you close it, you close the wizard.
Listing 2: A Basic do-nothing form that shows you how to differentiate between a ControlBox close and any other close in your wizard.
Public Class Form2 Private Sub Button2_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button2.Click Application.Exit() End Sub Protected Overrides Sub WndProc(ByRef m _ As System.Windows.Forms.Message) MyBase.WndProc(m) If (m.Msg = WM_SYSCOMMAND And _ m.WParam.ToInt32() = SC_CLOSE) Then Application.Exit() End If End Sub Private Const WM_SYSCOMMAND As Integer = &H112 Private Const SC_CLOSE As Integer = &HF060 Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click Close() If (My.Application.ApplicationContext.MainForm _ Is Nothing = False) Then My.Application.ApplicationContext.MainForm.Show() End If End Sub End Class
Button1_Click returns to the MainForm via the ApplicationContext and My feature. Button2_Click is the actual Cancel button, and WndProc actually receives all messages and is a convenient place to check for WM_SYSCOMMAND and SC_CLOSE. The constant values are defined by the Windows API. (Any time you need such values, you usually can find their integer value by searching by name.)
WndProc is called a lot. If you put too much code in there, your application will suffer performance issues. However, in your case you are inserting about four bytes of code and shutting down if the if..conditional check succeeds.
I wrote this article because I was tinkering with a utility wizard and stumbled on the problem. I diligently searched the web for a clean solution and really didn't like what was out there. When I wrote my wizard, every form registered with a controller. All of the navigation was in the controller, including Next, Back, and Cancel. This approach facilitates centralized navigation. Further, every form except the main form is closed when not in use. I don't like Hide and Seek forms. I quickly realized that turning the ControlBox off was the easiest way to get out of the dilemma, but then the user lost the convenience of the ControlBox but could still close with Alt+F4, resulting in the original problem happening a different way. As a result, it seemed necessary to figure out how to handle a mid-stream wizard form close.
After running a few wizards in Visual Studio, it was apparent that Microsoft leaves the ControlBox in place and supports the expected close behavior. Because I couldn't find a specific event, the WndProc naturally came to mind. The WndProc after all is the universal message/event handler. After that, it was a matter of writing the Msg and WParam and LParam from WndProc to the Debug Output window, clicking the ControlBox, and looking for my event. Finding the event name, I looked it (WM_SYSCOMMAND) up on the web and homed in on what I needed.
About the Author
Paul Kimmel is the VB Today columnist for www.codeguru.com and has written several books on object-oriented programming and .NET. Check out his upcoming book LINQ Unleashed for C#, now available on Amazon.com and in fine bookstores everywhere. You may contact him for technology questions at firstname.lastname@example.org.
Copyright © 2008 by Paul T. Kimmel. All Rights Reserved.