Creating Windows XP Style ActiveX Button

Environment: VC6 SP4, NT4 SP3

Creating an ActiveX control is very simple if you use MFC wizard. Here I explain how to create a simple ActiveX button control in VC++ that looks like a Windows XP buttons.

Create a new project by selecting the MFC ActiveX control wizard. Give it the name "XpButtonEx." There are two steps in the ActiveX control wizard. In the first dialog box select one control, no runtime licensing, source-file comments, and no Help files. In the second dialog box select Activates When Visible, Available in "Insert Object" Dialog, and has an "About Box." Select "BUTTON" in the combo-box where the wizard asks: "Which window class, if any, should this control subclass?" Click Finish. AppWizard will create some 19 files. There are 3 classes—CxpButtonExApp, CxpButtonExCtrl, and CxpButtonExPropPage.

Now open the class wizard. Make sure that the selected class name is "CXpButtonExCtrl." Add the message map entries for WM_CREATE, WM_LBUTTONDOWN, WM_LBUTTONUP, and WM_MOUSEMOVE. Right-click "CXpButtonExCtrl" in the Class view tab and add the virtual function PreSubclassWindow. Add the line:

ModifyStyle(0, BS_OWNERDRAW|BS_NOTIFY)

after

COleControl::PreSubclassWindow()
in the PreSubclassWindow function. Now open XpButtonEx.h and add the following member variables and functions.

public:
CPen *pBoundryPen;
CPen *pInsideBoundryPenLeft;
CPen *pInsideBoundryPenTop;
CPen *pInsideBoundryPenRight;
CPen *pInsideBoundryPenBottom;
CPen *pOldPen;
CBrush *pFillActive;
CBrush *pFillInactive;
CBrush *pOldBrush;
BOOL m_bOverControl;
void DoGradientFill(CDC *pDC, CRect rect);
void DrawInsideBorder(CDC *pDC, CRect rect);

Open XpButtonEx.cpp. In the constructor function CXpButtonExCtrl(), add the following:

  m_bOverControl = FALSE;
  pBoundryPen = new CPen(PS_INSIDEFRAME|PS_SOLID,1,RGB(0,0,0));
  pInsideBoundryPenLeft = new CPen(PS_INSIDEFRAME|
      PS_SOLID,3,RGB(250,196,88));
  pInsideBoundryPenRight = new CPen(PS_INSIDEFRAME|
      PS_SOLID,3,RGB(251,202,106));
  pInsideBoundryPenTop = new CPen(PS_INSIDEFRAME|
      PS_SOLID,2,RGB(252,210,121));
  pInsideBoundryPenBottom = new CPen(PS_INSIDEFRAME|
      PS_SOLID,2,RGB(229,151,0));
  pFillActive = new CBrush(RGB(222,223,236));
  pFillInactive = new CBrush(RGB(222,223,236));

And in the destructor ~CXpButtonExCtrl(), delete the objects:

pBoundryPen->DeleteObject();
  pFillActive->DeleteObject();
  pFillInactive->DeleteObject();
  pOldPen->DeleteObject();
  pOldBrush->DeleteObject();
  pInsideBoundryPenLeft->DeleteObject();
  pInsideBoundryPenRight->DeleteObject();
  pInsideBoundryPenBottom->DeleteObject();
  pInsideBoundryPenTop->DeleteObject();

Add the following functions in XpButtonCtl.cpp:

void CXpButtonExCtrl::DoGradientFill(CDC *pDC, CRect rect)
{
    CBrush* pBrush[64];
    for (int i=0; i<64; i++)
      pBrush[i] = new CBrush(RGB(253-(i/2),
                             253-(i/3), 
                             253-(i/4)));
    int nWidth = (rect.right) - (rect.left);
    int nHeight = (rect.bottom) - (rect.top);
    CRect rct;

    for (i=rect.top; i < nHeight+2; i++)
  {
        rct.SetRect (rect.left, i, nWidth+2, i + 1);
        pDC->FillRect (&rct, pBrush[(i * 63) / nHeight]);
    }

    for (i=0; i<64; i++)
        delete pBrush[i];

}
void CXpButtonExCtrl::DrawInsideBorder(CDC *pDC,CRect rect)
{
  pOldPen = pDC->SelectObject(pInsideBoundryPenLeft);
  pDC->MoveTo(rect.left,rect.bottom-3);
  pDC->LineTo(rect.left,rect.top+2);
  pDC->SelectObject(pInsideBoundryPenRight);
  pDC->MoveTo(rect.right-1,rect.bottom-3);
  pDC->LineTo(rect.right-1,rect.top+2);
  pDC->SelectObject(pInsideBoundryPenTop);
  pDC->MoveTo(rect.left+2,rect.top);
  pDC->LineTo(rect.right-2,rect.top);
  pDC->SelectObject(pInsideBoundryPenBottom);
  pDC->MoveTo(rect.left+2,rect.bottom);
  pDC->LineTo(rect.right-2,rect.bottom);
  pDC->SelectObject(pOldPen);
}

Now go to the OnOcmCommand() and add the following switch statement before return 0.

...
switch(wNotifyCode)
{
  case BN_CLICKED:  // The click event should be fired
                    // when the button is clicked.
          FireClick();
   break;
}

We are using the Boolean variable m_bOverControll for tracking the mouse position. The button will receive WM_MOUSEMOVE when the mouse is over the button. Change the code of OnMouseMove to the following:

void CXpButtonExCtrl::OnMouseMove(UINT nFlags, CPoint point)
{
  // TODO: Add your message handler code
  // here and/or call default.

  COleControl::OnMouseMove(nFlags, point);
  if(!m_bOverControl)
  {
   m_bOverControl = TRUE;
   Invalidate(FALSE);
   TRACKMOUSEEVENT tm;
   tm.cbSize = sizeof(tm);
   tm.dwFlags = TME_LEAVE;
   tm.hwndTrack = this->m_hWnd;
   ::_TrackMouseEvent(&tm);
  }
}

Now, for detecting when the mouse leaves the button, we should manually add the following message handlers. Add

LRESULT OnMouseLeave(WPARAM, LPARAM);

in XpButtonExCtl.h and add

ON_MESSAGE(WM_MOUSELEAVE, OnMouseLeave)

in XpButtonExCtl.cpp. Add the function

LRESULT CXpButtonExCtrl::OnMouseLeave(WPARAM, LPARAM)
{
   m_bOverControl = FALSE;
  Invalidate(FALSE);
  return 0;
}

in XpButtonExCtl.cpp. Now, to draw the button, we should manually add the handler for the OCM_DRAWITEM message. Add LRESULT OnOcmDrawItem(WPARAM wParam, LPARAM lParam); in XpButtonExCtl.h and add ON_MESSAGE(OCM_DRAWITEM, OnOcmDrawItem) in XpButtonExCtl.cpp. Add the function OnOcmDrawItem in XpButtonEx.cpp.

LRESULT CXpButtonExCtrl::OnOcmDrawItem(WPARAM wParam,
                                       LPARAM lParam)
{
  UINT nIDCtl = (UINT) wParam;
  LPDRAWITEMSTRUCT lpDrawItemStruct = 
                            (LPDRAWITEMSTRUCT) lParam;
  CDC* pDC   = CDC::FromHandle(lpDrawItemStruct->hDC);
  CRect rect = lpDrawItemStruct->rcItem;
  UINT state = lpDrawItemStruct->itemState;
 
    // draw the control edges
  CPoint pt;
  pt.x = 10;
  pt.y = 10;
  
    pOldPen = pDC->SelectObject(pBoundryPen);
    if (state & ODS_SELECTED)
       pDC->RoundRect(rect,pt);
    else
       pDC->RoundRect(rect,pt);
    pDC->SelectObject(pOldPen);
    // Deflate the drawing rect by the size of the button's edges
    rect.DeflateRect( CSize(GetSystemMetrics(SM_CXEDGE),
                      GetSystemMetrics(SM_CYEDGE)));
    // Fill the interior color if necessary

   if (m_bOverControl)
   {
      pOldBrush = pDC->SelectObject(pFillActive);
      DoGradientFill(pDC,rect);
      DrawInsideBorder(pDC,rect);
   }
   else
   {
      pOldBrush = pDC->SelectObject(pFillInactive);
      DoGradientFill(pDC,rect);
   }
   pDC->SelectObject(pOldBrush);
    
      // Draw the text
    if (!m_title.IsEmpty())
    {
        CSize Extent = pDC->GetTextExtent(m_title/*strText*/);
        CPoint pt( rect.CenterPoint().x - Extent.cx/2, 
        rect.CenterPoint().y - Extent.cy/2 );

        if (state & ODS_SELECTED) 
            pt.Offset(1,1);

        int nMode = pDC->SetBkMode(TRANSPARENT);
        CFont *pOldFont = SelectStockFont( pDC );

        if (state & ODS_DISABLED)
            pDC->DrawState( pt,
                            Extent,
                            m_title,
                            DSS_DISABLED,
                            TRUE,
                            0, 
                            (HBRUSH)NULL);
        else
            pDC->TextOut(pt.x, pt.y, m_title);
pDC->SelectObject(pOldFont);
        pDC->SetBkMode(nMode);
    }
  return 0;
}

Now we should add two properties to our ActiveX Control. One is Tile and the other is Font. Open Class Wizard and select the Automation tab. Make sure that the selected class name is CxpButtonEx. Click on the "Add Property" button. In the Add Property dialog box, give it an external name such as "title" and select the type as Cstring. Accept the default names for the variable and function. Click OK. Close the class wizard by clicking OK. Add the following line in the DoPropExchange function in XpButtonExCtl.cpp after the TODO: comment:

PX_String( pPX, _T("title"), m_title, _T("Caption"));

Now go to the Resourses view tab and open the dialog template IDD_PROPPAGE_XPBUTTONEX. Remove the "TODO:..." statement. Add one edit box, IDC_TITLE. Now open the Class wizard and select the Member Variables tab. Select the class CxpButtonExPropPage. Select IDC_TITLE and click Add Variable. In the Add Member variable dialog box, give Member Variable a name such as m_title, Category - Value, Variable Type - CString, and give the Optional Property a name such as "title." Click OK. Close the class wizard by clicking OK.

Now we can add the stock property Font. Open Class Wizard and select the Automation tab. Make sure that the selected class name is CxpButtonEx. Click on the "Add Property" button. In the Add Property dialog box, select the external name as Font in the combo box. Click OK. Close the class wizard by clicking OK. Now we can set up a property page for the font. This is really simple because we can use a prewritten property page. Open XpButtonExCtl.cpp and find the following code.

BEGIN_PROPPAGEIDS(CDierollCtrl, 1)
  PROPPAGEID(CDierollPropPage::guid)
END_PROPPAGEIDS(CDierollCtrl)

In this, change the count to 2, and add another PROPPAGEID. The new code will be like this:

BEGIN_PROPPAGEIDS(CDierollCtrl, 2)
  PROPPAGEID(CDierollPropPage::guid)
  PROPPAGEID(CLSID_CFontPropPage)
END_PROPPAGEIDS(CDierollCtrl)

Now our control is ready. Test it in an ActiveX test container.

Downloads

Download demo project - 96.9 Kb
Download source - 5.48 Kb