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.:
- Go to your workspace, right click on your project name, click on New Class.
- Type in your new class name, select its base class as
CButton
.
- Right click on your class name in the workspace, click on Add Virtual Function.
- Add
DrawItem()
, PreSubclassWindow()
virtual functions to your class.
- 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);
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);
CDC* pDC=CDC::FromHandle(lpDrawItemStruct->hDC);
switch(lpDrawItemStruct->itemAction)
{
case ODA_SELECT:
{
}
case ODA_DRAWENTIRE:
{
if(lpDrawItemStruct->itemState & ODS_SELECTED)
{
pDC->FillRgn(CRgn::FromHandle(trgn),
CBrush::FromHandle((HBRUSH)GetStockObject(GRAY_BRUSH)));
}
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;
}
}
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)
{
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.