Flat-look Buttons 

Flat look (cool) toolbar buttons have been well covered. But what about including the same sort of button in a Dialog (or form or dialog bar)? This is not supported by the new common controls, so you have to do it yourself. 

Here is a class that does flat-look buttons for you. Just like a toolbar, the buttons get borders when the mouse moves over them. The code uses DrawState to draw the buttons. 

This class can be (I'll send further articles when I get some time :-) enhanced to include check-style, optional standard looks, icons as well as text, change cursor as mouse moves over, color change (for links), 3D text, size text to fit etc etc. But this is a good starting point :-) 

class CMyFlatButton : public CButton {

     DECLARE_DYNAMIC(CMyFlatButton)

private:

     bool m_bRaised;

     bool m_bDrawBackground;

public:

     CMyFlatButton()

          : CButton()

          , m_bRaised(false)  // internal only

          , m_bDrawBackground(true)      // internal only

     {}

     // drawing

protected:

     virtual void DrawText(CDC* pDC, const CRect& rect, LPCTSTR text, UINT state=0);

     virtual void DrawBorders(CDC* pDC, CRect& rect, UINT state=0);

     virtual void Draw(CDC* pDC, const CRect& rect, UINT state=0);

protected:

     //{{AFX_MSG(CMyFlatButton)

     afx_msg BOOL OnEraseBkgnd(CDC* pDC);

     afx_msg void OnMouseMove(UINT nFlags, CPoint point);

     afx_msg LONG OnMouseLeave(WPARAM, LPARAM);

     //}}AFX_MSG

     //{{AFX_VIRTUAL(CMyFlatButton)

     virtual void PreSubclassWindow();

     virtual void DrawItem(LPDRAWITEMSTRUCT lpDIS);

     //}}AFX_VIRTUAL

     DECLARE_MESSAGE_MAP();

};

///////////////////////////////////////////////////////////////////////////

//

BEGIN_MESSAGE_MAP(CMyFlatButton, CButton)

//{{AFX_MSG_MAP(CMyFlatButton)

ON_WM_ERASEBKGND()

ON_WM_MOUSEMOVE()

ON_MESSAGE(WM_MOUSELEAVE,OnMouseLeave)

//}}AFX_MSG_MAP

END_MESSAGE_MAP()

IMPLEMENT_DYNAMIC(CMyFlatButton,CButton)

// this routine uses DrawState to draw the text approriately.

// it automatically centres the text within the button border

void CMyFlatButton::DrawText(CDC* pDC, const CRect& rect, LPCTSTR text, UINT state) {

     // we want transparent text

     int nBkModeOld = pDC->SetBkMode(TRANSPARENT);

     // setup text color

     COLORREF textcol = ::GetSysColor(COLOR_BTNTEXT);

     COLORREF oldTextColor = pDC->SetTextColor(textcol);

     // centre the text

     CSize textSizeClient = pDC->GetTextExtent(text,strlen(text));

     int x = rect.left+(rect.Width()-textSizeClient.cx)/2;

     int y = rect.top+(rect.Height()-textSizeClient.cy)/2;

     // draw the text

     pDC->DrawState(CPoint(x,y), rect.Size(), text,

          (state & ODS_DISABLED?DSS_DISABLED:DSS_NORMAL), true, 0, (HBRUSH)NULL

          );

     // restore dc

     pDC->SetTextColor(oldTextColor);

     pDC->SetBkMode(nBkModeOld);

}

// this routine (optionally) draws the borders on the button

// flat buttons use only single-pixel borders, so we cannot use

// DrawFrameControl.

void CMyFlatButton::DrawBorders(CDC* pDC, CRect& rect,UINT state) {

     if (state & ODS_SELECTED) {

          pDC->Draw3dRect(rect,::GetSysColor(COLOR_3DSHADOW),

                               ::GetSysColor(COLOR_3DHILIGHT));

          rect.DeflateRect(1,1);

     } else if (! m_bRaised && ! (state & ODS_FOCUS)) {

          // no border - flat look

          rect.DeflateRect(1,1);

     } else {

          pDC->Draw3dRect(rect,::GetSysColor(COLOR_3DHILIGHT),

                               ::GetSysColor(COLOR_3DSHADOW));

          rect.DeflateRect(1,1);

     }

     rect.DeflateRect(1,1);

     if (state & ODS_SELECTED) {

          rect.OffsetRect(1,1);    // offset image when pressed

     }

}

// Here we draw the appropriate parts of the button

void CMyFlatButton::Draw(CDC* pDC, const CRect& inrect, UINT state) {

     CRect rect = inrect;

     DrawBorders(pDC,rect,state);

     CString text;

     GetWindowText(text);

     if (! text.IsEmpty()) {

          DrawText(pDC,rect,text,state);

     }

}

// Overrides

void CMyFlatButton::PreSubclassWindow() {

     SetButtonStyle(GetButtonStyle() | BS_OWNERDRAW | BS_NOTIFY);

}

// we use m_bDrawBackground flag to allow us to

// repaint behind the button before drawing on top

void CMyFlatButton::DrawItem(LPDRAWITEMSTRUCT lpDIS) {

     if (m_bDrawBackground) {

          m_bDrawBackground = false;

          // if not redrawing the whole thing,

          CWnd* pParent = GetParent();

          CRect rect; GetWindowRect(rect);

          pParent->ScreenToClient(rect);

          pParent->InvalidateRect(rect);

          pParent->UpdateWindow();

     } else {

          m_bDrawBackground = true;

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

          ASSERT_VALID(pDC);

          CRect rectClient = lpDIS->rcItem;

          Draw(pDC,rectClient,lpDIS->itemState);

     }

}

// Messages

BOOL CMyFlatButton::OnEraseBkgnd(CDC*) {

     return true;    // we don't do any erasing when owner drawn

}

// the m_bRaised flag inidicates whether or not to

// draw the borders.  If the mouse is over the button

// the borders are drawn.  We use TrackMouseEvent to

// detect when the mouse leaves the button so we can

// turn off the flag

void CMyFlatButton::OnMouseMove(UINT, CPoint) {

     if (! m_bRaised) {

          // draw with button borders

          m_bRaised = true;

          Invalidate();

          // remember to remove button borders when we leave

          TRACKMOUSEEVENT trackmouseevent;

          trackmouseevent.cbSize = sizeof(trackmouseevent);

          trackmouseevent.dwFlags = TME_LEAVE;

          trackmouseevent.hwndTrack = GetSafeHwnd();

          trackmouseevent.dwHoverTime = HOVER_DEFAULT;

          _TrackMouseEvent(&trackmouseevent);

     }

}

LONG CMyFlatButton::OnMouseLeave(WPARAM, LPARAM) {

     // remove button borders

     m_bRaised = false;

     Invalidate();

     return 0;

}

Last updated: 13 May 1998