Currently viewing: GipsySoft » Front Page» Articles

Buddy Button

Skip right to the end if you just want the source code and the demo

Let's take a look at what we'd like to achieve. Below is a screenshot of what we want.

You've probably seen something like the above in applications before but wondered how to achieve it. In this article I'll discuss how it's achieved and at the end I'll we'll have a single function that can be used in your applications.

Goals

Let's first look at my goals for this article.

Must be easy to use
The API for using Buddy Buttons inside edit controls must be simple and easy to use. We don't want to clutter our projects with a ton of stuff that is unrelated to the task in hand.
Must be flexible
Maybe you have an owner drawn button you want in the edit, maybe the button displays a menu or a file browser dialog.
We won't use fixed pieces of text, fixed measurements or anything like that.
Code must be simple
Because I did this purely for an article I wanted the code to be clean and simple. A personal goal really.

[TOP]

Overview

Here is a list of the fundamental steps we need to go through, there is no magic.

  1. Create a dialog and add the edit control and button to the dialog
  2. During the WM_INITDIALOG (OnInitDialog in MFC) processing subclass the edit control.
  3. In the subclassed edit control alter the client size of the edit control to allow enough space for the button.
  4. In the subclassed edit control process the WM_NCCALCSIZE message to allow the button to get mouse messages.
  5. In the subclassed edit control process the WM_SIZE messages to alter the position of the button.
  6. In the subclassed edit control process the WM_NCHITTEST message to allow the button to get mouse messages.
  7. In the subclassed edit control process the WM_NCDESTROY message to free any allocated resources and to clean attachments to the window.
  8. Use BuddyButton in a dialog

I think you'll be surprised at just how simple the above is...Let's take one step at a time.

[TOP]

Create the dialog

In our example we're going to place two pushbuttons and a check box inside three different edit controls. Below is a scree shot showing how the resources for our Buddy Button demo look without calling the Buddy Button function, and next to it you can see what it looks like with the Buddy Button.

Note: It's important to get the z-order (tab order) of the buttons just right. The left aligned button really needs to be before the edit control, right aligned Buddy Buttons need to be after the edit control. This means your customers will tab around the dialog and everything will feel natural to them.

[TOP]

Subclass the control

Subclassing in Windows API is fairly straightforward. We set the window procedure of the HWND we are interested in, and we optionally store the old one someplace in case we need it - we which invariably do.

Here's the entire function for adding a Buddy Button to an edit control:

BOOL EnableBuddyButton( HWND hwndEdit, HWND hwndButton, UINT uStyle )
{
  //
  //  Quick check to amke sure our parameters are good
  if( uStyle == BBS_LEFT || uStyle == BBS_RIGHT )
  {
    if( IsWindow( hwndEdit ) && IsWindow( hwndButton ) )
    {
      //
      //  Subclass the edit control so we can catch some handy messages
      FARPROC lpfnWndProc = (FARPROC)SetWindowLong( hwndEdit
          , GWL_WNDPROC, (LONG) SubClassedProc );
      ASSERT( lpfnWndProc != NULL );
      VERIFY( ::SetProp( hwndEdit, g_szOldProc
          , reinterpret_cast( lpfnWndProc ) ) );

      //
      //  Create our data object.
      //	We later give this to our subclassed edit control so we can 
      CData *pData = new CData;
      pData->m_uStyle = uStyle;

      CRect rcButton;
      ::GetWindowRect( hwndButton, rcButton );

      pData->m_uButtonWidth = rcButton.Width();
      pData->m_hwndButton = hwndButton;

      VERIFY( ::SetProp( hwndEdit, g_szData, reinterpret_cast( pData ) ) );

      //
      //  Doing this forces our edit window to
      //  pay attention to our change in it's client size
      SetWindowPos( hwndEdit, NULL, 0, 0, 0, 0
          , SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED );

      return TRUE;
    }
    else
    {
      //
      //  Set the last error to be something meaningful
      SetLastErrorEx( ERROR_INVALID_WINDOW_HANDLE, SLE_ERROR );
    }
  }
  else
  {
    //
    //  Set the last error to be something meaningful
    SetLastErrorEx( ERROR_INVALID_DATA, SLE_ERROR );
  }
  return FALSE;
}

There are some interesting points. Firstly, we check the parameters and if they are no good for us we set the last error and return FALSE. We set the last error so that we might give users of the function some clue as to what went wrong. We could have added an extra check to make sure the hwndEdit is indeed and edit control but in this instance I left it out to keep the code clean.

Next we do the subclass. Notice that the old window procedure is returned when we use SetWindowLong. We then take the old one and store it as a property of the window. Properties are very handy for this type of thing.

We allocate a simple object and assign some values for later. These are for use within our window subclass procedure, one of the most interesting is the width of the button. We also use window properties to store this object.

Lastly, we have to perform a little trick with the edit control using SetWindowPos. We need to coerce it to adjust it's frame. We'll see why soon...

[TOP]

Process WM_NCCALCSIZE

WM_NCCALCSIZE is sent to the window to calculate the size and position of the window's client area. This is perfect for us because we need to fool the edit control into thinking it has less client space than it really has. Below is the code.

//
//	Adjust the client rect accordingly
LRESULT lr = CallWindowProc( WndProc, hwnd, message, wParam, lParam );
LPNCCALCSIZE_PARAMS lpnccs = reinterpret_cast( lParam );
if( pData->m_uStyle == BBS_RIGHT )
{
	lpnccs->rgrc[ 0 ].right -= pData->m_uButtonWidth;
}
else
{
	lpnccs->rgrc[ 0 ].left += pData->m_uButtonWidth;
}

return lr;

Not a lot interesting going on in there, we just subtract our button width from whichever side our button will go. However, this only got sent because of our call to SetWindowPos in EnableBuddyButton.

[TOP]

Process WM_SIZE

WM_SIZE is sent to the window when it's size has changed. We use this as an opportunity to alter the position of our button.

CRect rc;
::GetClientRect( hwnd, rc );
if( pData->m_uStyle == BBS_RIGHT )
{
	rc.left = rc.right;
	rc.right = rc.left + pData->m_uButtonWidth;
}
else
{
	rc.right = rc.left;
	rc.left = rc.left - pData->m_uButtonWidth;
}

//
//	Change our co-ordinates to be client co-ordinates relative to our parent
::MapWindowPoints( hwnd, GetParent( hwnd ), (LPPOINT)&rc, 2 );

//
//	Move the button but don't adjust it's z-order
::SetWindowPos( pData->m_hwndButton, NULL, rc.left, rc.top, rc.Width(), rc.Height(), SWP_NOZORDER );

Basically take the client rectangle of the edit control and from it get the rectangle of our button. Because the button is not in our edit control, and is instead a child of our parent, we must adjust the co-ordinates to those of our parent using MapWindowPoints which will convert points in one window space into point of another window. We then set the size and position of the button being careful not to alter the Z-order of the button

[TOP]

Process WM_NCHITTEST

WM_NCHITTEST is sent to the window when the OS needs to know where in the window the mouse it. It uses this information to handle things like the minimise and maximise buttons as well as window sizing.

//
//	We need this so that mouse clicks get through to our button (or whatever) control
LRESULT lr = CallWindowProc( WndProc, hwnd, message, wParam, lParam );
if( lr == HTNOWHERE )
{
	lr = HTTRANSPARENT;
}
return lr;

I observed that once we had moved our button on top of our edit control it would sometimes not respond to mouse clicks. I assumed that it must be because the edit control is eating the mouse clicks inside it's non-client area.

To get around this I check to see what the edit control returns. If it says nothing then I assume the mouse must be on our button. I could do more rigorous checking against the button rectangle but for now I chose to keep things simple

[TOP]

Process WM_NCDESTROY

WM_NCDESTROY is pretty much the last thing sent to a window. It is the perfect place to unhook our window procedure from the edit control and free any resources we may have allocated.

//
//	Clean up our junk so we don't leak.
SetWindowLong( hwnd, GWL_WNDPROC, (LONG) WndProc );
RemoveProp( hwnd, g_szOldProc );
RemoveProp( hwnd, g_szData );

delete pData;

I think it's just too simple to deserve any comment.

[TOP]

Use BuddyButton in a dialog

It couldn't be simpler. Call EnableBuddyButton during WM_INITDIALOG (OnInitDialog in MFC) and pass it the HWND of your edit control and the button you'd like to use as it's buddy. Also, you'll need to specify whether you want the Buddy Button to be left or right aligned.

//
//	For simplicity get our two windows.
HWND hwndEdit = GetDlgItem( IDC_EDIT1 )->GetSafeHwnd();
HWND hwndButton = GetDlgItem( IDC_BUTTON1 )->GetSafeHwnd();

VERIFY( ::EnableBuddyButton( hwndEdit, hwndButton, BBS_RIGHT ) );

Notes

The supplied Buddy Button code is included in two source files called EnableBuddyButton.cpp and EnableBuddyButton.h. You should be able to drop these straight into your project without doing anything fancy.

Hacking around like is this fairly simple. There aren't any real guidelines other than try it and fix what fails. Sometimes unexpected messages come through the subclass procedure , and sometimes they come in an unpredictable order. This may at first seem frustrating but it goes with the territory.

I haven't tested it on WinXP or 9x, just Win2K I'm afraid.

The code builds with level 4 warnings and warnings as errors. There's nothing that should bother a unicode compile so it should build cleanly there too - but not tested.

I hope you find a use for Buddy Buttons in your applications and I hope you enjoy using the code as much as I did writing it ;-)

[TOP]

Download