MFC, Round Windows, and Highlight Buttons

Environment: VC6, VC7, Windows 9x/NT/2000/XP

If you’ve tried to create programs based on the standard gray-rectangles Windows interface, this article for you. It describes creating windows with original shapes, backgrounds, and buttons. The VC++ v6.0 demo project is attached.

Starting the Project

The first thing you must do is create a bitmap with images that you need. You could use any painter program, as Adobe Photoshop, Corel Photo-Paint, and so forth. You must design the background image of your window and all buttons’ images in different conditions. The standard Windows button conditions are normal, pressed, disabled, and default. To use the highlight condition, you need to create some additional code. Also, you need to include your background bmp file into your project resources.

Creating the Project

In most cases, a non-standard design is used for dialog windows. This project example is based on a Dialog MFC Project. I created the Dialog Project, opened the recourse editor, and removed the title and borders of dialog (by using the Properties menu). Working with such a window is easier. If you have a title, you must calculate its size because the title size is dependent on your Windows settings. If you do not have a title and borders, all of the window’s rectangle is the client area. By using the WM_PAINT message, we can draw all over the window’s rectangle.

This demo dialog includes only one button, “OK.” To repaint the button from your code, you need switch off the “Default Button” parameter and switch on the “Owner Draw” parameter. You could do it by using the resource editor.

Designing the Window Region

The visible edge of a window could be any shape. The shape is determined by its region. The CRgn class supports rectangles, round rectangles, elliptical, and polygon regions. More than that, you could combine regions. For example, when I was creating the Color Chains game (http://www.brigsoft.com/colorch), I created a window that looks like three rounded rectangles. In the BSAlarm project (http://www.brigsoft.com/bsalarm), I used a combination of round rectangle regions and an elliptical region.

I do not change the window region during program execution, so I attach the region to the window before it is shown. The best place to do it is in the OnCreate() message handler for a normal window or OnInitDialog() for a dialog window. Use the wizard and the WM_CREATE (WM_INITDIALOG) messages to insert these functions into your project. Here is OnInitDialog() from the demo project:

BOOL CRoundWindowDlg::OnInitDialog()
  {
    CDialog::OnInitDialog();

    VERIFY( SetWindowPos( NULL, 0, 0, m_nW, m_nH, SWP_NOMOVE
                          | SWP_NOOWNERZORDER ) );
    VERIFY( m_WinRgn.CreateEllipticRgn( 0, 0, m_nW, m_nH ) );
    VERIFY( SetWindowRgn(m_WinRgn , TRUE ) );

    m_ExitBtn.Move();

    return TRUE;
  }

I use SetWindowPos to set the window size according to the background image. It is much easier than making the dialog window size using the resource editor. Pay attention to m_WinRgn. The region is a member of the CRoundWindowDlg class, so it is created before the window is created and removes itself after the window is destroyed. It is important. Using a local variable for the window region could cause an error.

Drawing the Background

You can draw any image into a dialog window as into a plain window by using the WM_PAINT message or the OnPaint() MCF handler. Draw the background before drawing other images. The drawing is trivial. Here is a function from the demo project:

void CRoundWindowDlg::OnPaint()
  {
    CPaintDC dc(this); // device context for painting

    CBitmap BkBmp, *pOldBmp;
    BkBmp.LoadBitmap(IDB_BKBITMAP);
    CDC BmpDc;
    VERIFY( BmpDc.CreateCompatibleDC(&dc) );
    pOldBmp = (CBitmap *)BmpDc.SelectObject(&BkBmp);
    dc.BitBlt(0,0,m_nW,m_nH,&BmpDc,0,0,SRCCOPY);
    BmpDc.SelectObject(pOldBmp);
  }

It is only for drawing the background.

Cool Buttons

The highlight button status is not standard for Windows. So we must catch the WM_MOUSEMOVE message and highlight it in our code. To do so, I created the CColorBtn class that’s based on CButton. Using the wizard, I created a button member in the demo project and then changed CBurtton to CColorBtn in the dialog window class declaration. The CColorBtn class uses the same bitmap recourse as the background window. Button conditions images are placed below the dialog window background image. The drawing process is trivial for “Owner Draw” buttons:

void CColorBtn::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
  {

    CDC *pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
    CDC BmpDC;

    VERIFY( BmpDC.CreateCompatibleDC(pDC) );
    CBitmap tmpBmp;
    tmpBmp.LoadBitmap(IDB_BKBITMAP);
    CBitmap *pOldBmp = (CBitmap *)BmpDC.SelectObject(&tmpBmp);

    switch(m_nStatus){

  case Normal:
    pDC->BitBlt(lpDrawItemStruct->rcItem.left,
    lpDrawItemStruct->rcItem.top,
    lpDrawItemStruct->rcItem.right - lpDrawItemStruct->rcItem.left,
    lpDrawItemStruct->rcItem.bottom -lpDrawItemStruct->rcItem.top,
    &BmpDC, m_nBmpX, m_nBmpY, SRCCOPY);
    break;

  case Light:
    pDC->BitBlt(lpDrawItemStruct->rcItem.left,
    lpDrawItemStruct->rcItem.top,
    lpDrawItemStruct->rcItem.right - lpDrawItemStruct->rcItem.left,
    lpDrawItemStruct->rcItem.bottom -lpDrawItemStruct->rcItem.top,
    &BmpDC, m_nBmpX+m_nW, m_nBmpY, SRCCOPY);
    break;

  case Pressed:
    pDC->BitBlt(lpDrawItemStruct->rcItem.left,
    lpDrawItemStruct->rcItem.top,
    lpDrawItemStruct->rcItem.right - lpDrawItemStruct->rcItem.left,
    lpDrawItemStruct->rcItem.bottom -lpDrawItemStruct->rcItem.top,
    &BmpDC, m_nBmpX+m_nW*2, m_nBmpY, SRCCOPY);
    break;
  }

  BmpDC.SelectObject(pOldBmp);

}

The value of the m_nStatus member is assigned in the OnLButtonDown(), OnLButtonUp(), and OnMouseMove() functions. OnLButtonDown() and OnLButtonUp() are clear. I had already implemented OnMouseMove(), so the following code was used:

void CColorBtn::OnMouseMove(UINT nFlags, CPoint point)
  {
    if(GetCapture()!=this){
      SetCapture();
    }

    CRect WinRect(0,0,m_nW, m_nH);

    if(WinRect.PtInRect(point)){
      m_nStatus = Light;
    }
    else{

    m_nStatus = Normal;
    if(GetCapture()==this){
    bool bRet = ReleaseCapture();
      if(!bRet){
        ASSERT(0);
      }
    }
  }

    CButton::OnMouseMove(nFlags, point);
    Invalidate(FALSE);
    UpdateWindow();
}

I had captured the mouse to determine the moment when the mouse pointer left the button shape. Other ways of doing this are using TRACKMOUSEEVENT or checking the mouse pointer from the OnTime() or OnIdle() functions.

Button coordinates are assigned in the class constructor. The Move() function places the button window to its coordinates.

Radio Buttons and Check Boxes

Radio buttons and check boxes do not have the “Owner Draw” parameter. In this demo project, I have not used these buttons, but I have used them before. As with any window, these buttons have a window procedure. If we catch this procedure, we can change any window’s functionality. We need catch the drawing. To do so, use these steps:

  1. By using the GetDlgItem function, we could receive the handle of the button window (SDK) or pointer to CWnd (MFC).
  2. By using the GetWindowLong function, we can change the window proc function to our code.
  3. Then we draw what we need while processing the WM_ENABLE, BM_SETSTATE, BM_SETCHECK, and WM_PAINT messages. Do not ask me why it is so. All questions should be sent to Microsoft. I was tracing all button messages to define when drawing took place.

Links

I used the technique that was described in this article in the following projects:

© Alex Rest, 2002

Downloads

Download demo project – 38 Kb

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read