Date: | August 2, 2004 / year-entry #296 |
Tags: | code |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20040802-00/?p=38283 |
Comments: | 22 |
Summary: | Setting focus in a dialog box is more than just calling SetFocus. A dialog box maintains the concept of a "default button" (which is always a pushbutton). The default button is typically drawn with a distinctive look (a heavy outline or a different color) and indicates what action the dialog box will take when you... |
Setting focus in a dialog box is more than just calling SetFocus. A dialog box maintains the concept of a "default button" (which is always a pushbutton). The default button is typically drawn with a distinctive look (a heavy outline or a different color) and indicates what action the dialog box will take when you hit Enter. Note that this is not the same as the control that has the focus. For example, open the Run dialog from the Start menu. Observe that the OK button is the default button; it has a different look from the other buttons. But focus is on the edit control. Your typing goes to the edit control, until you hit Enter; the Enter activates the default button, which is OK. As you tab through the dialog, observe what happens to the default button. When the dialog box moves focus to a pushbutton, that pushbutton becomes the new default button. But when the dialog box moves focus to something that isn't a pushbutton at all, the OK button resumes its position as the default button. The dialog manager remebers which control was the default button when the dialog was initially created, and when it moves focus to something that isn't a button, it restores that original button as the default button. You can ask a dialog box what the default button is by sending it the DM_GETDEFID message; similarly, you can change it with the DM_SETDEFID message. (Notice that the return value of the DM_GETDEFID message packs the control ID in the low word and flags in the high word. Another place where expanding dialog control IDs to 32-bit values doesn't buy you anything.) As the remarks to the DM_SETDEFID function note, messing directly with the default ID carelessly can lead to odd cases like a dialog box with two default buttons. Fortunately, you rarely need to change the default ID for a dialog. A bigger problem is using SetFocus to shove focus around a dialog. If you do this, you are going directly to the window manager, bypassing the dialog manager. This means that you can create "impossible" situations like having focus on a pushbutton without that button being the default! To avoid this problem, don't use SetFocus to change focus on a dialog. Instead, use the WM_NEXTDLGCTL message. void SetDialogFocus(HWND hdlg, HWND hwndControl) { SendMessage(hdlg, WM_NEXTDLGCTL, (WPARAM)hwndControl, TRUE); } As the remarks for the WM_NEXTDLGCTL message observe, the DefDlgProc function handles the WM_NEXTDLGCTL message by updating all the internal dialog manager bookkeeping, deciding which button should be default, all that good stuff. Now you can update dialog boxes like the professionals, avoiding oddities like having no default button, or worse, multiple default buttons! |
Comments (22)
Comments are closed. |
I was sure I knew everything about dialogs. Now I learn that not only was I wrong but I’m not even a professional ! Please don’t tell the guy who signs my pay check at the end of each month ;-)
Also, MFC seems to make the same mistake. Even though there is a CWnd::GotoDlgCtrl(), MFC doesn’t uses it much itself (e.g. see CDataVaildation::Fail())
Hi,
maybe this is off-topic, but I think I found a bug in the dialog manager.
I created a standard dialog in Visual C++ 6 and tested it there. (I didn’t wrote any code).
I could produre this behaviour:
Abbrechen (Cancel) is the default push button, but OK has the focus:
http://matthias-hoffrichter.privat.t-online.de/dialog1.jpg
And if you now activate the OK button. You have 2 default push buttons.
http://matthias-hoffrichter.privat.t-online.de/dialog2.jpg
I hope you can reprodure this:
Press the tab-key and hold it down. The focus rotates between the 2 buttons. Now release the tab-key and press a button.
Maybe this explanation is not correct because this happens all very fast. ;)
Play around with tab and click the button.
Ah, one correction: I think it requires a double click on the button.
…set focus in a dialog box. I always thought MacOS Carbon was messy….
Okay, when I subclass the buttons, catch WM_LBUTTONDBLCLK and just return 0 this behaviour doesn’t occur.
I uploaded a small example if you want to test it.
http://matthias-hoffrichter.privat.t-online.de/Dialog.zip
Does anybody understand whats going on?
By the "rules" of dialogs, if a button has focus then it must be the default button. However, when your dialog is initializing the first control to get focus is a button. The dialog manager doesn’t seem to have noticed that it has to change the default button in this case, which might well be causing the rest of the confusion.
Sadly, I’m not using Windows right now so I can’t play with your code.
Sometimes it sure would be better to have no default button. An example is the Device Manager snap-in of MMS. If the user has ever used the tab key to move from one control to another, then the user probably expects the enter key to do invoke the active control rather than invoke the OK button. There used to be some kinds of dialogs (not in MMS) where, for example, if a combo box had focus then the enter key would make the list drop down, the arrow keys would move the selection to a lower or higher list entry, and then the enter key would act on the selected entry. In Device Manager, sometimes the user has to press the enter key, sometimes the user has to just press arrow keys (and not see the entire list but memorize all the entries as they get displayed one at a time), sometimes the user has to press the space key, etc. If the user guesses wrong at any time and hits the enter key when something else was necessary, then the OK button automatically takes over, commits some undesired change, closes the dialog, and makes the user hunt for all the operations in sequence before finding the same point in the dialog again.
Well, if the user is trying to debug something other than the mouse then the problem isn’t usually that bad. But nonetheless it is bad when it doesn’t have to be. If the user has tabbed off the default button, then the new default should either be the focused control (if it is meaningful to be a default) or should be none at all, i.e. a no-op, instead of ambushing and reverting to a button that the user has tabbed off from.
Norman> Not really an answer to your concerns but you can hit F4 or Alt+DownArrow to open a combo box (assuming it’s not a funky custom control that is almost, but not quite, entirely the same as the built-in combo box).
There’s lots of cases where this "default" behaviour does not work, even in MS apps. Take the VSS Check Out dialog in Visual Studio 2003 that I was using just before reading this article:
The dialog has three buttons along the bottom – Check Out (heavy default border) Cancel and Help but the focus is in the Comments edit box.
Hitting return here by the rules, I would expect the default button to be pushed but in fact it just puts a <cr> in the edit box….
Very interesting article, certainly didn’t know I’ve been doing this wrong all these years. I wonder why SetFocus() can’t just do the right thing? At least the help page could remark on the correct method to call if setting focus to a button.
Ant: I don’t see what’s wrong. Focus can be on a non-button initially. And multiline edit controls use the WM_GETDLGCODE message (discussed earlier http://weblogs.asp.net/oldnewthing/archive/2003/11/26/55872.aspx ) to indicate that they want the Enter key to go to them and not to the dialog. This is typically what people expect when typing into a text box.
Dim St Thomas: I guess I don’t see what the problem is. Certainly you can’t send wizard messages when not in a wizard! In the same way you certainly can’t send listbox messages to a button control.
CPropertySheet is an MFC thing and I know nothing about MFC. What does SetWizardMode correspond to in Win32?
Steve: SetFocus can’t do "the right thing" because SetFocus is the low level function. The dialog manager is built on top of the window manager, not vice versa.
If I’m a user entering text in a multi-line edit control in a dialog, and I see that there is a button with a thick border, I am not going to press Enter to get a newline, and risk prematurely dismissing the dialog. Even though the edit control may be overriding the default Enter key behavior while it has focus, there is no visual indication of that, so there’s no way for me to know without actually trying it — and possibly suffering unwanted consequences.
Faced with such a situation, I’d just compose the text in Notepad — newlines at all — and then paste it into the dialog.
Some dialogs work around this problem by using Ctrl+Enter instead of Enter for entering newlines in multi-line edit controls. That works, but it requires an unsightly line of static text explaining how to enter a newline. It’s also error-prone, since it’s not necessarily easy for a user to override his or her automatic Enter keypress to get a newline.
Suggestion: When a multi-line edit control uses WM_GETDLGCODE to trap the Enter key for its own purposes, it might be a good idea to temporarily remove the "default button" status from the default button, so as not to needlessly confuse the user.
That’s an interesting suggestion. Too bad it can’t be done given the current design. (Look at the design of the WM_GETDLGCODE message.)
In one application I worked on we had PropertyPages that were used in both wizard and non-wizard mode PropertySheets. To allow for the wizard mode case we would call CPropertySheet::SetWizardButtons from OnSetActive as it tells you to do in the doc. If this would be the first page of a wizard we would use:
CPropertySheet* psheet = (CPropertySheet*) GetParent();
psheet->SetWizardButtons(PSWIZB_NEXT);
The problem occurs when you use such a page in a non-wizard mode sheet. As soon as the page is selected, the above code is called and it sets the Cancel button as the default button for the sheet.
To get round this we had to set a flag to tell the page when it was in non-wizard mode so it would not call SetWizardButtons. A CPropertySheet::GetWizardMode would have come in handy here. The CPropertySheet::SetWizardMode doc says it "sets the PSF_WIZARD flag", but PSF_WIZARD doesn’t seem to be defined anywhere.
Raymond, you say "The default button is typically drawn with a distinctive look (a heavy outline or a different color) and indicates what action the dialog box will take when you hit Enter." and that’s what I’ve always taken will happen. But when the edit box has the focus, it also accepts the enter despite the visual appearance of the default button suggesting otherwise. This is why I think it’s wrong. Action does not match visual cue. If the edit box too away the default outline from the button as other buttons do, all would be well.
Right, and my point is that if you look at the WM_GETDLGCODE message you can see why the dialog manager cannot do that. The dialog manager is not psychic.
Raymond wrote "Dim St Thomas: I guess I don’t see what the problem is. Certainly you can’t send wizard messages when not in a wizard! In the same way you certainly can’t send listbox messages to a button control."
I would compare it with sending a BM_SETCHECK message to a push button. I think most people would not expect this to have any effect. The "problem" seems to be that PSWIZB_NEXT is the same as IDCANCEL and the PropertySheet does not check if it is in wizard mode when handling the PSM_SETWIZARDBUTTONS message, though you could argue that this is the responsibility of the programmer.
Raymond wrote "CPropertySheet is an MFC thing and I know nothing about MFC. What does SetWizardMode correspond to in Win32?"
Looking at the MFC source, SetWizardMode adds PSH_WIZARD (not PSF_WIZARD as stated in the SetWizardMode doc) to the PROPSHEETHEADER flags. I also found an undocumented IsWizard function which could be useful.
Okay… I’m trying to do a simple thing here, change focus on a dialog to a given button. I’m using
SendMessage(hwnd, WM_NEXTDLGCTL, (WPARAM)hB[dwDefault], TRUE);
to set the default button (hB is an array of button HWNDs). Furthermore, I create the default button with the BS_DEFPUSHBUTTON style. I’ve also tried using the DM_SETDEFID message to set the default button. But in each case, the first button created is always the default. I really don’t know what I’m doing wrong. If anyone has a pointer or even wants to e-mail me, see code, etc., I’d really appreciate it.
Seth (at mccarus dot com).
Hi, I tried to set focus to combobox edit field in OnInitDialog(){…}.
SendMessage(hdlg, WM_NEXTDLGCTL, …) didn’t work, but PostMessage(…) :-))
I’ve tested it under XP and VC 7.1
Read the documentation on WM_INITDIALOG for an explanation.
I actually have a question. I am new to VC++. I have a dialog which has many edit text boxes on it. I want my program to do some calculation after the user enter something into the text box and click else where on the dialog (not a control on the dialog though, just the grey background of the dialog). So I use the LostFocus method, but it works only after I click on the other text box or the other control, not after I click on the dialog. I just want my program to lost the focus when I click on the dialog (not on another control). Is there anyway to do this?
Walk the template items and do what they say.