Introduction
Having recently spent sometime working with WTL, I relise their is a distinct lack of
documentation. In my my opinion WTL is the best solution so far to wrapping up Win32, and the
more documentation we get on the web, the better it'll be for all of us.
This article tackles the approach used to create custom drawn controls using WTL.
I've choosen to use a listView to link up with the article
Neat Stuff to do with List Controls using CustomDraw,
which demonstrates the same concept using MFC. I choose to keep this simple and would refer you to that document for more
examples on what you can do to improve the look and feel of the list view.
MyListControl
The WTL makes creating custom drawn controls very easy. The trick is to know which bits of code you need in
your project to make it happen. We begin by defining our own class for a CListViewCtrl
, so we can keep
all the custom drawing in one place.
class MyListView : public CWindowImpl<MyListView, CListViewCtrl>,
public CCustomDraw<MyListView>
{
public:
BEGIN_MSG_MAP(MyListView)
CHAIN_MSG_MAP(CCustomDraw<MyListView>)
END_MSG_MAP()
}
We have to inherit from CWindowImpl
, otherwise we're not going to get any window messages. The most
important thing to note here is we also inherit from CCustomDraw
. This along with CHAIN_MSG_MAP(CCustomDraw<MyListView>)
macro gives us the ability to neatly over ride the process of drawing the CListViewContol.
The CHAIN_MSG_MAP()
macro routes the message to the default message map in the base class.
If you take a look inside at the code inside AtlCtrls.h at the class CCustomCtrl<T>
you'll see it defines
the marco NOTIFY_CODE_HANDLER(NM_CUSTOMDRAW, OnCustomDraw)
which handles the custom draw notification. It splits
the message into it component part and calls a function for each. These are the functions you can overload to deal with the drawing
of your control.
DWORD OnPrePaint(int , LPNMCUSTOMDRAW );
DWORD OnPostPaint(int , LPNMCUSTOMDRAW );
DWORD OnPreErase(int , LPNMCUSTOMDRAW );
DWORD OnPostErase(int , LPNMCUSTOMDRAW );
DWORD OnItemPrePaint(int , LPNMCUSTOMDRAW );
DWORD OnItemPostPaint(int , LPNMCUSTOMDRAW );
DWORD OnItemPreErase(int , LPNMCUSTOMDRAW );
DWORD OnItemPostErase(int , LPNMCUSTOMDRAW );
We'll override a couple of these functions and change the background fill color every second entry to give a stripped
look to the listView, making it easier to read accross the rows.
So in our MyListViewCtrl
we define the two overloads
class MyListView : public CWindowImpl<MyListView, CListViewCtrl>,
public CCustomDraw<MyListView>
{
public:
BEGIN_MSG_MAP(MyListView)
CHAIN_MSG_MAP(CCustomDraw<MyListView>)
END_MSG_MAP()
DWORD OnPrePaint(int , LPNMCUSTOMDRAW )
{
return CDRF_NOTIFYITEMDRAW;
}
DWORD OnItemPrePaint(int , LPNMCUSTOMDRAW lpNMCustomDraw)
{
NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>( lpNMCustomDraw );
COLORREF crText;
if ( (pLVCD->nmcd.dwItemSpec % 2) == 0 )
crText = RGB(200,200,255);
else
crText = RGB(255,255,255);
pLVCD->clrTextBk = crText;
return CDRF_DODEFAULT;
}
};
Creating the Control
Most examples you see in MSDN and in other articles assume the control is created as a resource and
linked into you code. To be different I'm going to create this list box directly onto the main window.
Below is all the code we're going to add the wizard generated CMainFrame
to create our custom drawn control.
Look out for the CHAIN_MSG_MAP_MEMBER(m_listView)
macro. Miss it out and your class won't get the
NM_CUSTOMDRAW
message. This macro routes the message to the default message map in the data member, which is what we want to do.
As a side note, we could have handled the NM_CUSTOMDRAW
message at this level. Checked to see what the control id was and process
the painting as appropriate. This maybe useful in some cases, but its much neater to do what we have done. Keeping our code
clean and clear.
class CMainFrame : public CFrameWindowImpl<CMainFrame>,
public CUpdateUI<CMainFrame>,
public CMessageFilter, public CIdleHandler
{
public:
BEGIN_MSG_MAP(CMainFrame)
CHAIN_MSG_MAP_MEMBER(m_listView)
END_MSG_MAP()
LRESULT OnCreate(UINT , WPARAM ,
LPARAM , BOOL& )
{
RECT r = {0,0,182,80};
m_listView.Create(m_hWnd,r,CListViewCtrl::GetWndClassName(),
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS |
WS_CLIPCHILDREN | LVS_REPORT, WS_EX_CLIENTEDGE);
m_listView.AddColumn(_T("Symbol"), 0);
m_listView.AddColumn(_T("Company "), 1);
m_listView.AddColumn(_T("Price "), 2);
m_listView.AddItem(0,0,"VOD");
m_listView.AddItem(0,1,"Vodaphone Airtouch");
m_listView.AddItem(0,2,"252.25");
m_listView.AddItem(1,0,"LAT");
m_listView.AddItem(1,1,"Lattice Group");
m_listView.AddItem(1,2,"149.9");
m_listView.AddItem(2,0,"CLA");
m_listView.AddItem(2,1,"Claims Direct");
m_listView.AddItem(2,2,"132.50");
return 0;
}
public :
MyListView m_listView;
};
In the OnCreate()
we just call create on the listView data member and fill in some demo data. Mission complete ...