Introduction
The owner-draw buttons are generally speaking cool. However, that coolness comes at a price: the button no longer handles its default state as a normal button does. This article presents an almost undocumented technique to assist owner-draw buttons in behaving just like standard buttons. It also provides a new base class to derive your owner-draw buttons from, to easily enable this capability.
Here is a short summary of the article, so you can jump to the interesting parts:
The owner-draw buttons are different. They usually look pretty and they are the tops of the art masterpieces in GUI design. They don't, however, behave properly when it comes to act as a default button. It can manifest in many different ways, depending on the particular implementation.
For example take a look at this picture and try to say what would happen if Enter were pressed:
As you can see, probably the most dangerous "feature" of the owner-draw buttons is that when they are focused they don't become a default buttons. Very often it causes the premature "clicking OK". While user expects that, if using a Tab key to navigate, the focused button would process the input of the Enter key, the dialog's default - often an OK button - is pressed instead. The situation gets even more complicated if different buttons are mixed together because some of them will work and other won't.
OK, lets take another picture (any guess?):
That illustrates the second problem: more than one button can indicate a default state creating confusion as to which button will process the request. This often happens if the default button is set programmatically.
Interestingly enough, the bitmap or icon button is working correctly. That is the button with the BS_BITMAP
or BS_ICON
style set. But not the CBitmapButton provided by MFC.
All in all, user just can't tell what will happen if he presses the Enter key!
To understand why the problem appears let's summarize the system's behavior when handling the default state for a standard push button first:
- The user presses Tab or clicks on a control or performs some action that causes a change in the input focus
- The system sends
WM_GETDLGCODE
to the current focused control
- If it claims to be the default button (returning
DLGC_DEFPUSHBUTTON
) the system sends it a BM_SETSTYLE
to remove its default state (wParam == BS_PUSHBUTTON
)
- The system then sends
WM_GETDLGCODE
to the control receiving the focus
- If it claims to be a non-default button (returning
DLGC_UNDEFPUSHBUTTON
) the system sends it a BM_SETSTYLE
to set its default state (wParam == BS_DEFPUSHBUTTON
)
- The system sends
WM_KILLFOCUS
to the current focused control
- The system sends
WM_SETFOCUS
to the control receiving the focus
- If any previous
BM_SETSTYLE
message involved a repaint (lParam != 0
), WM_PAINT
messages are posted by the system
- Messages are not necessarily sent in this order and
WM_GETDLGCODE
can be sent multiple times.
Knowing this, what needs to be done to have a well-behaved owner-drawn button? As explained below, exactly what the system is supposed to do with standard buttons, except that it can store the default state flag in the button's style, while we have to provide our own space.
The problem is that owner-draw buttons return DLGC_BUTTON
for WM_GETDLGCODE
messages, telling the system that they don't want to interfere with the default state. On the other hand, if you return DLGC_DEFPUSHBUTTON
or DLGC_UNDEFPUSHBUTTON
in addition to DLGC_BUTTON
, you will receive BM_SETSTYLE
messages and lose the owner-draw style.
The BS_OWNERDRAW
style is lost because it is mutually exclusive with BS_PUSHBUTTON
, BS_DEFPUSHBUTTON
and all the other styles specifying different types of button controls (radio, group boxes, etc.), so it seems that the owner-draw style can't be compatible with the default state.
What's more, to correctly reply to WM_GETDLGCODE
messages you have to know if your button is the default one. A standard button performs this task by looking at the current window's style, but an owner-draw button can't use the same method.
You may think to send a DM_GETDEFID
message to the parent (which is supposed to be a dialog) and then compare the result with the button's ID, but this doesn't work. The dialog's default button is not the same as the current default button, which can change along with the focus.
So, all in all, you have to keep the default state in an internal variable, because there's no room for it in the window's style. You should use that variable to know how to reply to WM_GETDLGCODE
messages and when you have to draw the default state border around the button. When you receive BM_SETSTYLE
messages, you update the variable and invalidate the control if needed.
The solution: an Owner-Draw Default Button
Here it comes COddButton
, a new base class for owner-draw buttons that provides basic support for default state handling. It should have been simple from the start, and we've made it simple in the end. This button is the most peculiar owner-draw button you've ever seen because... it has no drawing code! It should be used as a base class for owner-draw buttons in place of CButton
.
What it does have is the code to support other owner-draw buttons. All the ugly details are hidden in the class internals so the button derived from COddButton
is gaining the default state properly on it's own. All is left to do in derived class is to indicate the default state when appropriate. That can be done with the simple call of COddButton::IsDefault()
to determine the default state and typically drawing the black frame around the button when it becomes default.
COddButton::IsDefault
BOOL IsDefault()
- This function should be used to know when your button has the default state, so you can redraw your control accordingly. The return value is
TRUE
for the default button, FALSE
otherwise.
Most of the time COddButton::IsDefault()
will be enough to make things working, there are however supplemental methods to allow more flexibility and control as described below.
COddButton::EnableDefault
BOOL EnableDefault(BOOL bEnable)
- This function can be used to determine if the control supports/wants the default state. If the parameter value is
TRUE
the control is enabled to receive the default state, FALSE
disables the processing and button will behave like a non-fixed owner-draw.
COddButton::GetControlType
UINT GetControlType()
- This function should be used instead of
GetStyle()
or GetButtonStyle()
to know which type of control to draw in your DrawItem()
override. The class stores the initial style of the button before setting owner-draw style in PreSubclassWindow()
.
- The return value is one of the Button Styles specifying the various types of controls, except for the value
BS_DEFPUSHBUTTON
(which is mapped to BS_PUSHBUTTON
) and obviously BS_OWNERDRAW
.
Using the class
Using the class is very straightforward. Simply use COddButton
instead of CButton
as a base class for your owner-draw button.
#include "OddButton.h"
class CMyOwnerDrawButton : public COddButton
{
}
In the resource editor you should not set the "Owner draw" check box, the style will be set properly at run time. The control's type should not change after creation, while other style bits can change.
The next thing to do is to handle the drawing of the default state when appropriate. That is simply done in the CButton::DrawItem()
override using the COddButton::IsDefault()
call to figure out the state of the default flag and painting a thin black line as an indicator. For example, if the button were rectangular then it would be something like that:
void CMyOwnerDrawButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
const BOOL bDefault = IsDefault();
if( bDefault )
{
oRect(lpDrawItemStruct->rcItem);
oRect.DeflateRect(1, 1);
lpDrawItemStruct->rcItem = oRect;
}
if( bDefault )
{
oRect(lpDrawItemStruct->rcItem);
oRect.InflateRect(1, 1);
lpDrawItemStruct->rcItem = oRect;
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
CPen *pOldPen = (CPen*)pDC->SelectStockObject(BLACK_PEN);
CBrush *pOldBrush= (CBrush*)pDC->SelectStockObject(NULL_BRUSH);
pDC->Rectangle(lpDrawItemStruct->rcItem);
pDC->SelectObject( pOldPen );
pDC->SelectObject( pOldBrush );
}
}
In addition all critical settings are verified with ASSERT
s, so at least in the Debug version you will know if something goes wrong.
The story: How it all began
(Chapter by Paolo Messina)
I use WinCVS quite often to maintain important projects and all my CodeProject articles, and I find the CvsIn add-in very useful for this purpose, especially the Wizard dialog (also because it's resizable).
Clicking around all those nice buttons, like every good programmer does, and playing with all the windows that obscured my little desktop, I discovered that some buttons weren't redrawing correctly. At a closer look, in fact, there were two default buttons. "It's not possible!" was my first thought, and to prove that I moved the dialog partially off screen and then back in, to see which button had still the default border around it.
I was right, only one button can have the default state at the same time, so the next question that came up to my mind was: "What happened?".
"They're owner-draw buttons, all this kind of buttons have the same problems with the default state". This was Jerzy Kaczorowski's answer (the creator of CvsIn), and it's probably the same answer that many of you have heard or would have given. But before giving up and accepting the idea of multiple default-looking buttons, I had to try.
I had the CvsIn's sources, holiday's time, and a little bit of overrated self-esteem. Not to mention Jerzy's encouraging words, my VC++ compiler and the indispensable Spy++.
MSDN was not of much help, it only vaguely explains how standard buttons receive the default state and the messages involved in the process, but this was enough to start using Spy++, to filter out unneeded messages.
Spying windows
With Spy++ I could notice that owner-draw buttons were replying differently from standard buttons to the WM_GETDLGCODE
message, so I thought that maybe returning the same code could solve the problem. Then I built up a quick MFC dialog based project and a CButton
-derived class to do experiments with owner-draw.
The first attempt was to reply to WM_GETDLGCODE
messages so as to mimic a standard button, which returns DLGC_BUTTON
combined with either DLGC_DEFPUSHBUTTON
or DLGC_UNDEFPUSHBUTTON
, whether or not it has the default state.
Unfortunately, I had the bad idea of drawing my owner-draw button like a standard button, with no special distinctive sign, and the button now had a beautiful black border whenever it should. "That's too easy!" was my exclamation, and a hint of a smile came up on my face. But I couldn't be more wrong than this.
In fact, as MSDN briefly says for the DM_SETDEFID
message, there's another message being sent to a button when the system changes the default button and that is BM_SETSTYLE
. The system changed the style of my owner-draw button, reverting it to a standard push button, and since they had similar aspect I was mistaken.
So how could I tell the system I wanted default state for my button without losing owner-draw style? The answer is easy, as you may argue, but not too easy, hence we have this article...
The demo: It works!
After all the talking, time to see the OddButton in action. We include the sample application that demonstrates the usage of COddButton
as well as the problem with the owner-draw buttons itself.
As you can see, the test zone part of demo has three buttons:
-
Fixed owner-draw (derived from COddButton
)
-
Simple owner-draw (derived from CButton
)
-
Standard button
There is an edit box that allows you to simulate a real-life situation when user is entering the data and presses enter to process the input. Initially the default button is set to the fixed owner-draw button, but you can change that by using the Tab key and controls in the default button part. If any button is pressed a message box will popup to tell you what happened. Try different combinations of focus and default and see if it always follows your expectations and if it draws correctly.
Apart from the basic testing area there is also a miniature of Spy that will show you a detailed log of messages sent and received in a dialog so you can analyze what is going on. The dialog is resizable so you can extend it to make more room for the messages details.
Surprise ending: Is that the end of troubles?
Well, almost... ;)
It's the best that can be done from the owner-draw button's perspective. However, the more we've drilled the problem, the more odd things happened.
It turned out that there are some issues with a normal(!) buttons when setting a default button for the dialog using the CDialog::SetDefID
method. As described on MSDN when describing the DM_SETDEFID
message (which is what the CDialog::SetDefID
is using to do it's job) there is a possibility that more than one button will indicate the default state after sending the message. There is also a brief suggestion that in such case application should send the BM_SETSTYLE
on it's own to make sure that indication is accurate.
At that point we were considering making a whole new article on that subject alone. But further search revealed the Microsoft Knowledge Base Article - Q67655 (HOWTO: Change or Set the Default Push Button in a Dialog Box) containing a pseudo code demonstrating the above-mentioned technique. Presented solution has its drawbacks thought:
- It doesn't work if the code is executed from within the button click handler
- It destroys owner-draw buttons and turns them into the normal buttons
We have found the way to overcome (or rather work-around) these problems and since the code has become somehow complex we have wrapped it up and provided as a static method of COddButton
class:
COddButton::SetDefID
static void SetDefID(CDialog* pDialog, const UINT nID)
- This function should be used to set the new default button for your dialog instead of
CDialog::SetDefID
.
The License: Open Source (of course)
The COddButton
and the demo application are distributed under the terms of Artistic License.
This project is developed with CVS and its repository is hosted at Source Forge.
To obtain the latest development code (which may or may not compile) using CVS type this line in the command prompt (press Enter for password):
cvs -d:pserver:anonymous@cvs.oddbutton.sourceforge.net:/cvsroot/oddbutton
login
The next step is to checkout the module. To get the demo source use module OddButtonDemoSrc - simply type this line (it will put the code into the directory ButtonsDemo):
cvs -z3
-d:pserver:anonymous@cvs.oddbutton.sourceforge.net:/cvsroot/oddbutton
co OddButtonDemoSrc
To get the OddButton files only use the module OddButtonSrc, type the following line (files will be in directory OddButtonSrc):
cvs -z3
-d:pserver:anonymous@cvs.oddbutton.sourceforge.net:/cvsroot/oddbutton
co OddButtonSrc
That's it!
History
- 27 Oct 2001 - setting default button for the dialog added, updated source and demo.
- 28 Aug 2001 - updated source and demo.
- 5 Sep 2001 - updated source and demo.