Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

COddButton

0.00/5 (No votes)
26 Oct 2002 4  
How to make owner-draw buttons handle default state

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 problem: default buttons

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:

Focused button doesn't have the default state!

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?):

Two default buttons!

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:

  1. The user presses Tab or clicks on a control or performs some action that causes a change in the input focus
  2. The system sends WM_GETDLGCODE to the current focused control
  3. 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)
  4. The system then sends WM_GETDLGCODE to the control receiving the focus
  5. 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)
  6. The system sends WM_KILLFOCUS to the current focused control
  7. The system sends WM_SETFOCUS to the control receiving the focus
  8. If any previous BM_SETSTYLE message involved a repaint (lParam != 0), WM_PAINT messages are posted by the system
  9. 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 ) 
    {
        //deflate rectangleCRect

        oRect(lpDrawItemStruct->rcItem);
        oRect.DeflateRect(1, 1);
        lpDrawItemStruct->rcItem = oRect;
    }
    
    /*your drawing code goes here*/
    
    if( bDefault ) 
    {
        //inflate rectangleCRect

        oRect(lpDrawItemStruct->rcItem);
        oRect.InflateRect(1, 1);
        lpDrawItemStruct->rcItem = oRect;
    
        //draw the default frame

        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 ASSERTs, 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.

The Demo App's window

As you can see, the test zone part of demo has three buttons:

  1. Fixed owner-draw (derived from COddButton)
  2. Simple owner-draw (derived from CButton)
  3. 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:

  1. It doesn't work if the code is executed from within the button click handler
  2. 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here