Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

Irregular shaped buttons – owner drawn buttons made easy

4.93/5 (44 votes)
21 Sep 2005CPOL4 min read 1   3.8K  
Freehand draw - make a button with irregular shape. A step by step beginner's guide.

Sample Image - irregular_buttons.jpg

Introduction

Button is the only control where we have to struggle or have to take more effort to change its shape and properties. But, it is not that much difficult to change its properties as it seems. The only tough thing is, we need to know how to draw well. A good artist can make a good-looking button. If you know well to play with “Device Caps” you can do more with button designs, that's all. This article is just about the basics of making your own owner drawn irregular shape buttons.

What to do is simple

Here is what you have to do, if you want to develop your own button control. Subclass CButton (CMyButton in my sample), i.e.:

  1. Go to your workspace, right click on your project name, click on New Class.

    New Class

  2. Type in your new class name, select its base class as CButton.

    New Class

  3. Right click on your class name in the workspace, click on Add Virtual Function.

    Add Virtual Function

  4. Add DrawItem(), PreSubclassWindow() virtual functions to your class.

    Add Virtual Function

  5. Modify the style of your button to owner drawn (no problem if already set). In PreSubclassWindow() function, add the following code:
    ModifyStyle(0,BS_OWNERDRAW);
  6. DrawItem() virtual function comes along with a parameter lpDrawItemStruct of type LPDRAWITEMSTRUCT. DrawItem() is where your button has been drawn. Refer MSDN for full details of the structure LPDRAWITEMSTRUCT. Here I am explaining the members needed just to make a simple button.

LPDRAWITEMSTRUCT structure

lpDrawItemStruct ->itemAction:

Defines the drawing action required. This will be one or more of the following bits:

  • ODA_DRAWENTIRE - This bit is set when the entire control needs to be drawn.
  • ODA_FOCUS - This bit is set when the control gains or loses input focus. The itemState member should be checked to determine whether the control has focus.
  • ODA_SELECT - This bit is set when only the selection status has changed. The itemState member should be checked to determine the new selection state.
lpDrawItemStruct ->itemState:

Specifies the visual state of the item after the current drawing action takes place. That is, if a menu item is to be dimmed, the state flag ODS_GRAYED will be set. The state flags are as follows:

  • ODS_CHECKED - This bit is set if the menu item is to be checked. This bit is used only in a menu.
  • ODS_DISABLED - This bit is set if the item is to be drawn as disabled.
  • ODS_FOCUS - This bit is set if the item has input focus.
  • ODS_GRAYED - This bit is set if the item is to be dimmed. This bit is used only in a menu.
  • ODS_SELECTED - This bit is set if the item’s status is selected.
  • ODS_COMBOBOXEDIT - The drawing takes place in the selection field (edit control) of an ownerdrawn combo box.
  • ODS_DEFAULT - The item is the default item.
LpDrawItemSturct->hDC

Identifies a device context. This device context must be used when performing drawing operations on the control.

DrawItem Virtual Function

Here is where your drawing is being done. The following is the code snap of the sample:

void CMyButton::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) 
{
 SetWindowRgn(rgn,TRUE);
 // Construct your buttons region. This wont reflect in your view. 
 // But you can sense it by clicking the region area.

 CDC* pDC=CDC::FromHandle(lpDrawItemStruct->hDC);
 // Get dc for the button

 switch(lpDrawItemStruct->itemAction) 
 {
    case ODA_SELECT:
    {
    }      // no break; for this case
    case ODA_DRAWENTIRE:
    {
       if(lpDrawItemStruct->itemState & ODS_SELECTED) 
       {
          pDC->FillRgn(CRgn::FromHandle(trgn), 
          CBrush::FromHandle((HBRUSH)GetStockObject(GRAY_BRUSH)));
       }   
       // Draw button down state
       else
       {
         pDC->FillRgn(CRgn::FromHandle(trgn), 
                      CBrush::FromHandle((HBRUSH)GetStockObject(BLACK_BRUSH)));
         HRGN r= ::CreateRectRgn(0,0,0,0);
         CombineRgn(r,trgn,0,RGN_COPY);
         OffsetRgn(r,2,2);
         pDC->FillRgn(CRgn::FromHandle(r), 
                      CBrush::FromHandle((HBRUSH)GetStockObject(GRAY_BRUSH)));
       }
       break;
    }
    case ODA_FOCUS: 
    {
        pDC->FillRgn(CRgn::FromHandle(trgn),
                     CBrush::FromHandle((HBRUSH)GetStockObject(LTGRAY_BRUSH))); 

    }
    break;
  }
  // Draw button caption.
}

About the Sample

The sample application demonstrates owner drawn irregular shape buttons. You can draw your own button using your mouse, on the dialog. The button will be created in the drawn shape on left button up. Nothing special with this concept. Just the drawn coordinates are collected as path using BeginPath and EndPath of the CDC. Path is then converted into region. Using that region irregular buttons are created and drawn (FillRgn).

To do so, a DC of the dialog is created as a member function (usually no one will do) CDC *m_pdc;. This DC is used to create a path while you draw in the dialog. Here is the drawing and path making code:

void CButtonSubDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
#ifdef _SAMPLE1
 m_pdc->BeginPath();
 m_pdc->MoveTo(point);
 m_spoint=point;
 SetCapture();
#endif
 CDialog::OnLButtonDown(nFlags, point);
}
void CButtonSubDlg::OnMouseMove(UINT nFlags, CPoint point) 
{
 // Show mouse position
 CString mouse; mouse.Format("Irregular Buttons - X:%d Y:%d",point.x,point.y);
 SetWindowText(mouse);
 if(nFlags==MK_LBUTTON)
 {
#ifdef _SAMPLE1
  CClientDC dc(this);
  m_pdc->LineTo(point);
  dc.MoveTo(m_spoint);
  dc.LineTo(point);
  m_spoint=point; 
#endif
 }
 CDialog::OnMouseMove(nFlags, point);
}
void CButtonSubDlg::OnLButtonUp(UINT nFlags, CPoint point)
{
#ifdef _SAMPLE1
 m_pdc->EndPath();
 cRgn.DeleteObject();
 cRgn.CreateFromPath(m_pdc);

 HRGN rrgn;
 rrgn=::CreateRectRgn(0,0,0,0);
 int res=CombineRgn(rrgn,cRgn.operator HRGN(),0,RGN_COPY);
 
 if(NULLREGION ==res)
  MessageBox("Null Region::Cannot create region for button");
 else if(ERROR==res)
  MessageBox("Error::Cannot create region");
 else
 {
  CRect rect;
  GetClientRect(rect);
  m_but= new CMyButton();
  m_but->SetRgn(CRgn::FromHandle(rrgn));
  m_but->Create("",WS_CHILD|WS_VISIBLE| 
         WS_TABSTOP|BS_PUSHBUTTON,
         rect,this,1000+idcount++);
 }
 ReleaseCapture();
#endif
 CDialog::OnLButtonUp(nFlags, point);
}

Note: BeginPath() is called in OnLButtonDown() and EndPath() is called in OnLButtonUp(). This is to grab the path from the drawn freehand drawing.

Following is the code to sense the clicked dynamically created buttons:

BOOL CButtonSubDlg::OnCommand(WPARAM wParam, LPARAM lParam)
{
 if(HIWORD(wParam)==BN_CLICKED)
 {
  for(int i=0;i<idcount;i++)
  {
   if(LOWORD(wParam)==1000+i)
   {
    CString smsg;smsg.Format("Button %d Clicked",i+1);
    MessageBox(smsg);
    break;
   }
  }
 }
 return CDialog::OnCommand(wParam, lParam);
}

Finally delete all dynamically created buttons and the device cap (used to generate the path):

void CButtonSubDlg::OnDestroy()
{
 CDialog::OnDestroy();
 for(int i=0;i<idcount;i++)
 {
  m_but=(CMyButton *)GetDlgItem(1000+i);
  if(m_but)
   delete m_but;
 }
#ifdef _SAMPLE1
 if(m_pdc)
  delete m_pdc;
#endif
}

The region generated has been set to the CMyButton class using the following function:

void CMyButton::SetRgn(CRgn *region)
{
 rgn=::CreateRectRgn(0,0,0,0);
 CombineRgn(rgn,region->operator HRGN(),NULL,RGN_COPY);
 trgn=::CreateRectRgn(0,0,0,0);
 CombineRgn(trgn,region->operator HRGN(),NULL,RGN_COPY);
}

About Region

Region is where this demo application stands at all. The freehand drawing drawn by you will be generated into a path and the path is converted into region, finally a button is created using that region.

SetWindowRgn() function will set the shape for a window as by the given region. In the case of an owner-drawn button SetWindowRgn() is used to set the region of the button. But, this is not enough to visually set an irregular shaped button. For visual effects, you need to draw the button as per the region.

CombineRgn in this demo is used to copy a region into variables. RGN_COPY option lets the CombineRgn function to copy a region into another.

Demo Parts

This demo project is of two parts, _SAMPLE1, _SAMPLE2. _SAMPLE1 is the project part defined above. _SAMPLE2 is the code sample from MSDN to change a button's text color. For the _SAMPLE2 project, go to project settings, and change _SAMPLE1 to _SAMPLE2 in the preprocessor definition.

Conclusion

None of the articles can satisfy one's expectations. But, each article should be a seed for your technical growth. Thus, I believe that this would be a seed. Thank you all.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)