How to Skin CListCtrl, Including Scrollbars and Column Headers

Environment: VC6 SP5, Windows 2000 SP3, Windows 9x

Introduction

I have been programming in MFC for about five years and it has always been a problem for me to find help in the area of advanced user interfaces. I am developing version 3.0 of a very complex DJ audio application and I needed to have a slick-looking interface. Skinning the application itself is no big deal, but I needed to have lists in my application that did everything a CListCtrl could do, but also take on a custom look and feel.

Now, I had a few choices.

  1. I could buy a third-party list control that had all the functionality of a CListCtrl and allowed me to skin it, but I couldn't even find one anywhere for any price.
  2. I could use SkinMagic, ActiveSkin, or DirectSkin, but those products are slow, expensive, and don't skin CListCtrl controls without flickering or other annoying bugs.
  3. I could develop my own list control from scratch and try to add in the dragging and dropping of column headers, virtual list support, multiple columns, highlighting, drag and drop, resizing of columns, sorting, and so on, but this would take forever
  4. I could just try to skin the existing CListCtrl.

Obviously, I chose the latter because time is of the essence.

If I had had this source code before I undertook this task myself, it would have saved me many hours of work even with the not so clean code. So, I hope this helps a few of you out there.

It is very hard to explain how to do this because there are so many different elements working together, so you are probably better off just checking out the demo project. However this article should give a good idea of how I did this.

The code in this article was developed on Windows 2000 SP3 using Microsoft Visual C++ 6.0 with common controls DLL file version 5.81.4916.400 and product version 5.50.4916.400. This code was also tested on Windows 98 Second Edition.

Chronological Order of Efforts

First, I had to find a way to customize the existing column headers or make my own instead of settling for the typical grey ones. So, I derived a class from CHeaderCtrl and did an override for the OnPaint function and used bitmaps in place of the ugly grey headers and subclassed the CHeaderCtrl in my CSkinListCtrl class. This worked while retaining all the functionality of a CHeaderCtrl!

Next, I had to find a way to customize the existing scrollbars or else make my own. So, I tried to subclass the CScrollbar class; whenever I tried to use the GetScollbarCtrl() function from the CListCtrl, it returned null. Obviously, the scrollbars are not real. Unfortunately, this means I had to hide the existing scrollbars and create my own (a lot more work than just skinning the existing ones).

I began to try to hide the scrollbars of the CListCtrl and then somehow create my own. I found a solution for hiding the scrollbars in a CListCtrl on the CodeGuru message boards from Filbert Fox. This worked great, so my next task was to create my own scrollbars.

I chose to derive a class from CStatic and create the scrollbar from scratch using bitmaps. It took a while and a lot of tweaking, but I got the custom scrollbar created and working including the wheel mouse, arrow keys, and pageup/pagedown keys.

Now, I can't tell you how happy I was when I got this working! I hope to add some cool additions to this source code, which would be fairly easy to add. They would be rollover images for the up/down arrows, thumb control, and column header controls.

How to Use the Source Code in Your Own Projects

To use this source code for you own CListCtrls, all you have to do is copy the files (CSkinListCtrl.h, CSkinListCtrl.cpp, CSkinHeaderCtrl.h, CSkinHeaderCtrl.cpp, CSkinHorizontalScrollbar.h, CSkinHorizontalScrollbar.cpp, CSkinverticalScrollbar.h, and CSkinverticalScrollbar.cpp) into your project and add the files to your project (Project, Add to Project, files...). Now, go into each of the cpp files you just copied and change the #include "SkinList.h" to #include "<yourapp>.h"

Next, you must have some graphics you would like to use for your scrollbars and headerctrl. (Look at my graphics in the res folder to see how I cut them up to make them work properly.) Import those bmp graphics into your resource tab and give them all meaningful names. Then, you will have to go through the source code in the CSkinverticalScrollbar, CSkinHorizontalScrollbar, and CSkinHeaderCtrl classes and change the code to make it work with your bitmaps. There are hardcoded numbers in these classes used to position the bitmaps properly. For example, my left arrow is 26 pixels wide so my thumb control is positioned 25 pixels from the left. You will see the correlation between the numbers and the size of the graphics when you look at the source code. It will take a bit of playing around to get it working with your graphics, especially if your design is a lot different, but it should be a lot easier than having to write all this code from scratch.

Now once you have done all that, all you have to do is create your CListCtrl controls on your dialog in the resource editor within Visual Studio. When you create a member variable for your CListCtrl, just make sure to select CSkinListCtrl as the Control class instead of CListCtrl.

Note: If you don't see CSkinListCtrl as a choice for your control class when you create the member variable in the class wizard, you must delete the .clw file in the folder of your project. The next time you open the class wizard (Ctrl+W), it will rebuild the .clw file using the classes you added and you will then see the CSkinListCtrl as a choice for a control class.



Click here for a larger image.

Now, for everything to work, you must add the line m_SkinList.Init();. This is very important because this Init function is what creates the scrollbars and positions them on the CListCtrl. You must call this in your OnInitDialog function before you try to use the list, of course.


BOOL CSkinListDlg::OnInitDialog()
{
  ...

  //Important! You must call this first
  m_SkinList.Init();

  m_SkinList.SetBkColor(RGB(76,85,118));
  m_SkinList.SetTextColor(RGB(222,222,222));

  LOGFONT lf;
  memset(&lf, 0, sizeof(LOGFONT));
  lf.lfHeight = 12;
  strcpy(lf.lfFaceName, "Verdana");
  font.CreateFontIndirect(&lf);
  m_SkinList.SetFont(&font, TRUE);

  m_SkinList.InsertColumn(0, "BLANK",  LVCFMT_LEFT, 0);
  m_SkinList.InsertColumn(1, "SONG",   LVCFMT_LEFT, 100);
  m_SkinList.InsertColumn(2, "ARTIST", LVCFMT_LEFT, 100);
  m_SkinList.InsertColumn(3, "GENRE",  LVCFMT_LEFT, 100);

  m_SkinList.SetRedraw(FALSE);
  CString cszItem;
  for(int i=0; i<1000; i++)
  {
    cszItem.Format("%d - %s", i,
                   "Matthew Good - Near Fantastica");

    m_SkinList.InsertItem(i, cszItem);
    m_SkinList.SetItemText(i, 1, cszItem);
    m_SkinList.SetItemText(i, 2, "Matthew Good");
    m_SkinList.SetItemText(i, 3, "Rock");
  }

  m_SkinList.SetRedraw(TRUE);

  ListView_SetExtendedListViewStyle(m_SkinList.m_hWnd,
                                    LVS_EX_FULLROWSELECT  |
                                    LVS_EX_HEADERDRAGDROP);

  ...

}

How I Customized the CHeaderCtrl

Here we will set up the CSkinHeaderCtrl class that we made. We have to skin the header control using our own graphics.

//Add this line of code in the CSkinHeaderCtrl.h
public:
virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);

Now, override the OnPaint event in the CSkinHeaderCtrl class and write code to skin the column headers with our own graphics. If you use your own graphics and they are different sizes, you will have to modify the code in the OnPaint handler to draw your bitmaps correctly.

//add an include for the CMemDC class
#include "memdc.h"

...

//I added the following function. (This function does not do
//anything, but we need it so that we can get the properties of
//each column header in the OnPaint function.) If we don't have
//this function, our code in the OnPaint handler will crash.
void CSkinHeaderCtrl::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{

}

...

//First I did an overrode the OnPaint function
void CSkinHeaderCtrl::OnPaint() 
{
  CPaintDC dc(this); // device context for painting

  CRect rect, rectItem, clientRect;
  GetClientRect(&rect);
  GetClientRect(&clientRect);
  CMemDC memDC(&dc, rect);
  CDC bitmapDC;
  bitmapDC.CreateCompatibleDC(&dc);

  memDC.FillSolidRect(&rect, RGB(76,85,118));

  CBitmap bitmapSpan;
  bitmapSpan.LoadBitmap(IDB_COLUMNHEADER_SPAN);
  CBitmap* pOldBitmapSpan = bitmapDC.SelectObject(&bitmapSpan);

  for(int v=0; v<rect.Width(); v++)
  {
    memDC.BitBlt((rect.left+2)+(v*1), 0, 1, 12, &bitmapDC,0,0,
                  SRCCOPY);
  }

  bitmapDC.SelectObject(pOldBitmapSpan);
  bitmapSpan.DeleteObject();

  int nItems = GetItemCount();

  CBitmap bitmap;
  CBitmap bitmap2;
  CBitmap bitmap3;

  bitmap.LoadBitmap(IDB_COLUMNHEADER_START);
  bitmap2.LoadBitmap(IDB_COLUMNHEADER_SPAN);
  bitmap3.LoadBitmap(IDB_COLUMNHEADER_END);

  for(int i = 0; i <nItems; i++)
  {

    TCHAR buf1[256];
    HD_ITEM hditem1;

    hditem1.mask = HDI_TEXT | HDI_FORMAT | HDI_ORDER;
    hditem1.pszText = buf1;
    hditem1.cchTextMax = 255;
    GetItem( i, &hditem1 );

    GetItemRect(i, &rect);

    CBitmap* pOldBitmap = NULL;

    //make sure we draw the start piece
    //on the first item so it has a left border

    //For the following items we will just use the right border
    //of the previous items as the left border
    if(hditem1.iOrder==0)
    {
      pOldBitmap = bitmapDC.SelectObject(&bitmap);
      memDC.BitBlt(rect.left,rect.top,2,12,&bitmapDC,0,0,SRCCOPY);
    }
    else
    {
      memDC.BitBlt(rect.left-1,rect.top,2,12,&bitmapDC,0,0,
                   SRCCOPY);
      pOldBitmap = bitmapDC.SelectObject(&bitmap2);
      memDC.BitBlt(rect.left+1,rect.top,1,12,&bitmapDC,0,0,
                   SRCCOPY);
    }

    bitmapDC.SelectObject(pOldBitmap);

    //span the bitmap for the width of the column header item
    int nWidth = rect.Width() - 4;

    CBitmap* pOldBitmap2 = bitmapDC.SelectObject(&bitmap2);
    for(int v=0; v<nWidth; v++)
    {
      memDC.BitBlt((rect.left+2)+(v*1), 0, 1, 12, &bitmapDC,0,0,
                    SRCCOPY);
    }

    bitmapDC.SelectObject(pOldBitmap2);


    //draw the end piece of the column header
    CBitmap* pOldBitmap3 = bitmapDC.SelectObject(&bitmap3);
    memDC.BitBlt((rect.right-2), 0, 2, 12, &bitmapDC,0,0,SRCCOPY);
    bitmapDC.SelectObject(pOldBitmap3);

    //Get all the info for the current item so we can draw
    //the text to it in the desired font and style
    DRAWITEMSTRUCT DrawItemStruct;
    GetItemRect(i, &rectItem);


    DrawItemStruct.CtlType    = 100;
    DrawItemStruct.hDC        = dc.GetSafeHdc();
    DrawItemStruct.itemAction = ODA_DRAWENTIRE; 
    DrawItemStruct.hwndItem   = GetSafeHwnd(); 
    DrawItemStruct.rcItem     = rectItem;
    DrawItemStruct.itemID     = i;
    DrawItem(&DrawItemStruct);

    UINT uFormat = DT_SINGLELINE | DT_NOPREFIX | DT_TOP |
                   DT_CENTER | DT_END_ELLIPSIS ;


    CFont font;
    LOGFONT lf;
    memset(&lf, 0, sizeof(LOGFONT));
    lf.lfHeight = 8;
    strcpy(lf.lfFaceName, "Sevenet 7");
    font.CreateFontIndirect(&lf);
    CFont* def_font = memDC.SelectObject(&font);

    memDC.SetBkMode(TRANSPARENT);
    rectItem.DeflateRect(2,2,2,2);

    TCHAR buf[256];
    HD_ITEM hditem;

    hditem.mask = HDI_TEXT | HDI_FORMAT | HDI_ORDER;
    hditem.pszText = buf;
    hditem.cchTextMax = 255;
    GetItem( DrawItemStruct.itemID, &hditem );

    memDC.DrawText(buf, &rectItem, uFormat);
    memDC.SelectObject(def_font);
    font.DeleteObject();
  }
}

That pretty much does it for the CSkinHeaderCtrl. It was relatively easy to custom draw the CHeaderCtrl. It suprised me how easy it was because I saw so many posts on the message boards that asked how to do this and none had any replies. To include your own graphics, you will obviously have to modify this code to get it work properly for your design, but that is fairly straightforward now that you have the framework code right here.

How I Created the CSkinverticalScrollbar and CSkinHorizontalScrollbar Controls

Creating the vertical and horizontal scrollbar controls and making them work in conjunction with the CListCtrl was obviously the most daunting task. Basically, what I did was create a scrollbar control out of bitmaps using a CStatic and the base class. I added code to allow for the movement of the thumb control using the drag and drop and also code to handle clicks on the arrow buttons and channel area. I also added code to udpate the thumb position based on the ScrollPos of the list. Doing this allowed me to keep all the original functionality of a CListCtrl in terms of the wheel mouse, pgup/pgdown, arrow, home, and end keys. The horizontal and vertical scrollbar controls are basically the same, so I am just going to show you the code for the CSkinverticalScrollbar control for simplicity.

I won't post all the code here because there is too much, but I will try to explain what I did to make the scrollbars work with the CListCtrl, showing you the most important peices of the code.

First, I overrode the OnPaint handler so that I could draw the scrollbar using the graphics I wanted to use.

//CSkinverticalScrollbar.h file

...

public:
  CListCtrl* pList;
  void LimitThumbPosition();
  void Draw();
  void UpdateThumbPosition();
  bool bMouseDownArrowUp, bMouseDownArrowDown;
  bool bDragging;
  bool bMouseDown;

  int nThumbTop;
  double dbThumbInterval;

  void ScrollDown();
  void ScrollUp();
  void PageUp();
  void PageDown();
  
  ...

//CSkinverticalScrollbar.cpp file

...

void CSkinverticalScrollbar::OnPaint()
{
  CPaintDC dc(this); 

  Draw();
}

void CSkinverticalScrollbar::Draw()
{

  CClientDC dc(this);
  CRect clientRect;
  GetClientRect(&clientRect);
  CMemDC memDC(&dc, &clientRect);
  memDC.FillSolidRect(&clientRect,  RGB(74,82,107));
  CDC bitmapDC;
  bitmapDC.CreateCompatibleDC(&dc);

  CBitmap bitmap;
  bitmap.LoadBitmap(IDB_vertical_SCROLLBAR_TOP);
  CBitmap* pOldBitmap = bitmapDC.SelectObject(&bitmap);
  memDC.BitBlt(clientRect.left,clientRect.top,12,11,&bitmapDC,0,0,
               SRCCOPY);
  bitmapDC.SelectObject(pOldBitmap);
  bitmap.DeleteObject();
  pOldBitmap = NULL;

  bitmap.LoadBitmap(IDB_vertical_SCROLLBAR_UPARROW);
  pOldBitmap = bitmapDC.SelectObject(&bitmap);
  memDC.BitBlt(clientRect.left,clientRect.top+11,12,26,&bitmapDC,
               0,0,SRCCOPY);
  bitmapDC.SelectObject(pOldBitmap);
  bitmap.DeleteObject();
  pOldBitmap = NULL;

  //draw the background (span)
  bitmap.LoadBitmap(IDB_vertical_SCROLLBAR_SPAN);
  pOldBitmap = bitmapDC.SelectObject(&bitmap);
  int nHeight = clientRect.Height() - 37;

  for(int i=0; i<nHeight; i++)
  {
    memDC.BitBlt(clientRect.left,(clientRect.top+37)+(i),12,1,
                 &bitmapDC,0,0,SRCCOPY);
  }

  bitmapDC.SelectObject(pOldBitmap);
  bitmap.DeleteObject();
  pOldBitmap = NULL;

  //draw the down arrow of the scrollbar
  bitmap.LoadBitmap(IDB_vertical_SCROLLBAR_DOWNARROW);
  pOldBitmap = bitmapDC.SelectObject(&bitmap);
  memDC.BitBlt(clientRect.left,nHeight,12,26,&bitmapDC,0,0,
               SRCCOPY);
  bitmapDC.SelectObject(pOldBitmap);
  bitmap.DeleteObject();
    pOldBitmap = NULL;

  //draw the down arrow of the scrollbar
  bitmap.LoadBitmap(IDB_vertical_SCROLLBAR_BOTTOM);
  pOldBitmap = bitmapDC.SelectObject(&bitmap);
  memDC.BitBlt(clientRect.left+1,nHeight+26,11,11,&bitmapDC,0,0,
               SRCCOPY);
  bitmapDC.SelectObject(pOldBitmap);
  bitmap.DeleteObject();
  pOldBitmap = NULL;

  //draw the thumb control
  bitmap.LoadBitmap(IDB_vertical_SCROLLBAR_THUMB);
  pOldBitmap = bitmapDC.SelectObject(&bitmap);
  memDC.BitBlt(clientRect.left,clientRect.top+nThumbTop,12,26,
               &bitmapDC,0,0,SRCCOPY);
  bitmapDC.SelectObject(pOldBitmap);
  bitmap.DeleteObject();
  pOldBitmap = NULL;

}

Next, I wrote a function that will update the scrollbar thumb graphic's position based on the ScrollPos of the CListCtrl

void CSkinverticalScrollbar::UpdateThumbPosition()
{
  CRect clientRect;
  GetClientRect(&clientRect);

  double nPos    = pList->GetScrollPos(SB_VERT);
  double nMax    = pList->GetScrollLimit(SB_VERT);
  double nHeight = (clientRect.Height()-98);
  double nVar    = nMax;

  dbThumbInterval = nHeight/nVar;

  double nNewdbValue = (dbThumbInterval * nPos);
  int nNewValue      = (int)nNewdbValue;


  nThumbTop = 36+nNewValue;

  LimitThumbPosition();

  Draw();
}

void CSkinverticalScrollbar::LimitThumbPosition()
{
  CRect clientRect;
  GetClientRect(&clientRect);

  if(nThumbTop+26 > (clientRect.Height()-37))
  {
    nThumbTop = clientRect.Height()-62;
  }

  if(nThumbTop < (clientRect.top+36))
  {
    nThumbTop = clientRect.top+36;
  }
}

Then, I wrote code to handle the mouse events for when the user drag and drops the thumb control to scroll the list and for when they click (or click and hold down) on the scrollbar arrows.

void CSkinverticalScrollbar::PageDown()
{
  pList->SendMessage(WM_VSCROLL, MAKELONG(SB_PAGEDOWN,0),NULL);
  UpdateThumbPosition();
}

void CSkinverticalScrollbar::PageUp()
{
  pList->SendMessage(WM_VSCROLL, MAKELONG(SB_PAGEUP,0),NULL);
  UpdateThumbPosition();
}

void CSkinverticalScrollbar::ScrollUp()
{
  pList->SendMessage(WM_VSCROLL, MAKELONG(SB_LINEUP,0),NULL);
  UpdateThumbPosition();
}

void CSkinverticalScrollbar::ScrollDown()
{
  pList->SendMessage(WM_VSCROLL, MAKELONG(SB_LINEDOWN,0),NULL);
  UpdateThumbPosition();
}

void CSkinverticalScrollbar::OnLButtonDown(UINT nFlags,
                                           CPoint point)
{
  SetCapture();
  CRect clientRect;
  GetClientRect(&clientRect);

  int nHeight = clientRect.Height() - 37;


  CRect rectUpArrow(0,11,12,37);
  CRect rectDownArrow(0,nHeight,12,nHeight+26);
  CRect rectThumb(0,nThumbTop,12,nThumbTop+26);

  if(rectThumb.PtInRect(point))
  {
    bMouseDown = true;
  }

  if(rectDownArrow.PtInRect(point))
  {
    bMouseDownArrowDown = true;
    SetTimer(2,250,NULL);
  }

  if(rectUpArrow.PtInRect(point))
  {
    bMouseDownArrowUp = true;
    SetTimer(2,250,NULL);
  }

  CStatic::OnLButtonDown(nFlags, point);
}

void CSkinverticalScrollbar::OnLButtonUp(UINT nFlags,
                                         CPoint point)
{
  UpdateThumbPosition();
  KillTimer(1);
  ReleaseCapture();

  bool bInChannel = true;

  CRect clientRect;
  GetClientRect(&clientRect);
  int nHeight = clientRect.Height() - 37;
  CRect rectUpArrow(0,11,12,37);
  CRect rectDownArrow(0,nHeight,12,nHeight+26);
  CRect rectThumb(0,nThumbTop,12,nThumbTop+26);



  if(rectUpArrow.PtInRect(point) && bMouseDownArrowUp)
  {
    ScrollUp();
    bInChannel = false;
  }

  if(rectDownArrow.PtInRect(point) && bMouseDownArrowDown)
  {
    ScrollDown();
    bInChannel = false;
  }

  if(rectThumb.PtInRect(point))
  {
    bInChannel = false;
  }

  if(bInChannel == true)
  {
    if(point.y > nThumbTop)
    {
      PageDown();
    }
    else
    {
      PageUp();
    }
  }

  bMouseDown          = false;
  bDragging           = false;
  bMouseDownArrowUp   = false;
  bMouseDownArrowDown = false;

  CStatic::OnLButtonUp(nFlags, point);
}

void CSkinverticalScrollbar::OnMouseMove(UINT nFlags,
                                         CPoint point)
{
  CRect clientRect;
  GetClientRect(&clientRect);

  if(bMouseDown)
  {

    int nPreviousThumbTop = nThumbTop;
    nThumbTop = point.y-13;    //-13 so mouse is in middle of
                               //thumb

    double nMax = pList->GetScrollLimit(SB_VERT);
    int nPos    = pList->GetScrollPos(SB_VERT);

    double nHeight  = clientRect.Height()-98;
    double nVar     = nMax;
    dbThumbInterval = nHeight/nVar;

    //figure out how many times to scroll total from top
    //then minus the current position from it

    int nScrollTimes = (int)((nThumbTop-36)/dbThumbInterval)-nPos;

    CSize size;
    size.cx = 0;
    size.cy = nScrollTimes*13;    //13 is the height of each row
                                  //at current font
                                  //I can't figure out how to grab
                                  //this value dynamically

    pList->Scroll(size);


    LimitThumbPosition();

    Draw();

  }
  CStatic::OnMouseMove(nFlags, point);
}

void CSkinverticalScrollbar::OnTimer(UINT nIDEvent)
{
  if(nIDEvent == 1)
  {
    if(bMouseDownArrowDown)
    {
      ScrollDown();
    }

    if(bMouseDownArrowUp)
    {
      ScrollUp();
    }
  }
  else if(nIDEvent == 2)
  {
    if(bMouseDownArrowDown)
    {
      KillTimer(2);
      SetTimer(1, 50, NULL);
    }

    if(bMouseDownArrowUp)
    {
      KillTimer(2);
      SetTimer(1, 50, NULL);
    }
  }
  CStatic::OnTimer(nIDEvent);
}

How I Customized the CListCtrl

To customize the CListCtrl, I needed to subclass the CHeaderCtrl using the CSkinHeaderCtrl class that I made, as well as hide the original scrollbars and then insert the ones I made in their place.

So, first I subclassed the CHeaderCtrl by overriding PreSubclassWindow and adding the following code.

void CSkinListCtrl::PreSubclassWindow()
{
  //use our custom CHeaderCtrl
  m_SkinHeaderCtrl.SubclassWindow(GetHeaderCtrl()->m_hWnd);

  CListCtrl::PreSubclassWindow();
}

Next, I wrote an Init function that creates the scrollbars at runtime and ensures that the original scrollbars are hidden. I had to add code to take into account the size of the titlebar to place the CStatic scrollbars in the correct position. If the window's appearance changes, I needed to ensure the scrollbars stayed in the correct place. The code here is not perfect. If you change the style of the dialog, you may have to modify this code to get the scrollbars to be in the correct position in relation to the CListCtrl.

//CSkinListCtrl.h file
#include "SkinHeaderCtrl.h"
#include "SkinHorizontalScrollbar.h"
#include "SkinverticalScrollbar.h"

...

public:
  CSkinHeaderCtrl m_SkinHeaderCtrl;
  CSkinverticalScrollbar m_SkinverticalScrollbar;
  CSkinHorizontalScrollbar m_SkinHorizontalScrollbar;

//CSkinListCtrl.cpp file
void CSkinListCtrl::Init()
{
  //A way to hide scrollbars
  InitializeFlatSB(this->m_hWnd);
  FlatSB_EnableScrollBar(this->m_hWnd, SB_BOTH, ESB_DISABLE_BOTH);

  //GetSystemMetrics to find out height difference of the
  //titlebar so we can position scrollbars properly without
  //having to worry about the windows' appearances affecting it

  CWnd* pParent = GetParent();

  CRect windowRect;
  GetWindowRect(&windowRect);

  int nTitleBarHeight = 0;

  if(pParent->GetStyle() & WS_CAPTION)
    nTitleBarHeight = GetSystemMetrics(SM_CYSIZE);


  int nDialogFrameHeight = 0;
  int nDialogFrameWidth  = 0;
  if((pParent->GetStyle() & WS_BORDER))
  {
    nDialogFrameHeight = GetSystemMetrics(SM_CYDLGFRAME);
    nDialogFrameWidth  = GetSystemMetrics(SM_CYDLGFRAME);
  }

  if(pParent->GetStyle() & WS_THICKFRAME)
  {
    nDialogFrameHeight+=1;
    nDialogFrameWidth+=1;
  }


  //Create scrollbars at runtime
  m_SkinverticalScrollbar.Create(NULL, WS_CHILD|SS_LEFT|SS_NOTIFY|
                                 WS_VISIBLE|WS_GROUP,CRect(
                                 windowRect.right
                                            -nDialogFrameWidth,
                                 windowRect.top-nTitleBarHeight-n
                                            DialogFrameHeight-1,
                                 windowRect.right+12-nDialog
                                            FrameWidth,
                                 windowRect.bottom+11
                                            -nTitleBarHeight
                                            -nDialogFrameHeight),
                                 pParent);

  m_SkinHorizontalScrollbar.Create(NULL, WS_CHILD|SS_LEFT|
                                   SS_NOTIFY|WS_VISIBLE|
                                   WS_GROUP,CRect(
                                   windowRect.left-nDialog
                                              FrameWidth,
                                   windowRect.bottom
                                              -nTitleBarHeight
                                              -nDialogFrameHeight
                                              -1,
                                   windowRect.right+1
                                              -nDialogFrameWidth,
                                   windowRect.bottom+11
                                              -nTitleBarHeight
                                              -nDialogFrameHeight),
                                   pParent);
  m_SkinverticalScrollbar.pList   = this;
  m_SkinHorizontalScrollbar.pList = this;
}

Next, I added code to ensure that the thumb position of the scrollbars position themselves properly after the list scrolls in any way (keyboard commands, mouse wheel, etc.)

BOOL CSkinListCtrl::OnMouseWheel(UINT nFlags, short zDelta,
                                 CPoint pt)
{
  m_SkinverticalScrollbar.UpdateThumbPosition();
  m_SkinHorizontalScrollbar.UpdateThumbPosition();

  return CListCtrl::OnMouseWheel(nFlags, zDelta, pt);
}


void CSkinListCtrl::OnKeyDown(UINT nChar, UINT nRepCnt,
                              UINT nFlags)
{
  m_SkinverticalScrollbar.UpdateThumbPosition();
  m_SkinHorizontalScrollbar.UpdateThumbPosition();

  CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}

void CSkinListCtrl::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags)
{
  m_SkinverticalScrollbar.UpdateThumbPosition();
  m_SkinHorizontalScrollbar.UpdateThumbPosition();

  CListCtrl::OnKeyUp(nChar, nRepCnt, nFlags);
}

Finally, I added to code to ensure the list does not flicker when scrolling by overriding the OnEraseBkgnd and OnPaint handlers. I also have code in the CSkinListCtrl class to make sure the highlight color of the rows stays a certain color rather than taking on the system colors, but I won't show that here. You can just look at the source code.

BOOL CSkinListCtrl::OnEraseBkgnd(CDC* pDC)
{
  m_SkinverticalScrollbar.UpdateThumbPosition();
  m_SkinHorizontalScrollbar.UpdateThumbPosition();
  return FALSE;
  //return CListCtrl::OnEraseBkgnd(pDC);
}


void CSkinListCtrl::OnPaint()
{
  CPaintDC dc(this);
  CRect rect;
  GetClientRect(&rect);
  CMemDC memDC(&dc, rect);

  CRect headerRect;
  GetDlgItem(0)->GetWindowRect(&headerRect);
  ScreenToClient(&headerRect);
  dc.ExcludeClipRect(&headerRect);


  CRect clip;
  memDC.GetClipBox(&clip);
  memDC.FillSolidRect(clip, RGB(76,85,118));

  SetTextBkColor(RGB(76,85,118));

  m_SkinverticalScrollbar.UpdateThumbPosition();
  m_SkinHorizontalScrollbar.UpdateThumbPosition();


  DefWindowProc(WM_PAINT, (WPARAM)memDC->m_hDC, (LPARAM)0);
}

Points of Interest

  • The method of hiding the scrollbars in this article still allows scrolling.
  • Windows display appearances should not affect the list in any way.
  • It is quite easy to skin the CHeaderCtrl using the OnPaint method coupled with overriding the DrawItem handler.
  • Be aware of the hardcoded value in the CSkinScrollbar class that holds the value of how many items can be shown at one time in your CListCtrl, given its current height and font. This value has to be changed to reflect how many items can be shown at once in your CListCtrl.
  • Also, be aware that the CSkinHeaderCtrl and CSkinScrollbar classes are hard coded to work with the size and type of bitmaps I am using. The code in these classes will have to updated to work with the bitmaps you want to use.
  • I saw messages all over the message boards asking how to customize a CHeaderCtrl and how to add customized scrollbars to a CListCtrl and none of the posts were answered. This discouraged me, but I learned not to let that get to me. Just because it hasn't been done before or the source code hasn't been posted doesn't mean you can't be the first!! Never give up!

Things to Improve

  1. Make the CSkinScrollbar and CSkinHeaderCtrl classes work with any size of bitmap without the need for changing code in these classes.
  2. Make the code in the CSkinList::Init function compensate for any style of dialog, so if you increase the border size of the dialog or remove the titlebar the scrollbars will position themselves accordingly instead of us having to change code in the Init function manually to make it work.
  3. Somehow have the CSkinList::Init function run by itself so that we don't have to call m_SkinList.Init(); ourselves.
  4. Grab the height of each row (give the font style, size, and weight) dynamically, instead of using a hardcoded value.
  5. Add support for rollover images on the scrollbar arrows, thumb control, and column headers.

Credits

Neat Stuff to do in List Controls Using Custom Draw—By Michael Dunn
http://www.codeproject.com/listctrl/lvcustomdraw.asp

How to hide CListCtrl scrollbars—By Filbert Fox
http://www.codeguru.com

Flicker Free Drawing In MFC—By Keith Rule
http://www.thecodeproject.com/gdi/flickerfree.asp

Downloads

Download demo project - 87 Kb


Comments

  • Repaint Problem

    Posted by Inthi on 02/24/2006 08:14am

    Hi, I am creating SkinList Ctrl using Create method as shown below m_pUserListCtrl = new CSkinListCtrl(); bReturn=m_pUserListCtrl->Create(WS_CHILD|WS_VISIBLE|LVS_REPORT|LVS_NOCOLUMNHEADER|LVS_SINGLESEL,rect,this,0x28); DWORD dwStyle = m_pUserListCtrl->GetExtendedStyle()|LVS_EX_FULLROWSELECT|LVS_SHOWSELALWAYS|LVS_EX_SUBITEMIMAGES; m_pUserListCtrl->SetExtendedStyle(dwStyle); I am facing below listed problems 1) if I use WS_BORDER in Create,If an Item is selected the borders are getting overlapped. 2) If I minimize and maximize the Dialog the List control is not repainted. (Case: if any item is selected in listcontrol and then the dialog box is minimized and then maximized this case is causing problem , I used Invalidate(TRUE) in OnCustomDrawList() it was working fine for this case but the CPU useage was going to 100%, I also tried to invalidate the list control during SC_MAXIMIZE and SC_RESTORE of dialog but still it is not working)

    Reply
  • Umed here : Sorry but this did not helped me....

    Posted by fun2oos@rediffmail.com on 05/03/2005 11:31am

    I used this in Windows not in dialogs, but its not working as it shud...
    I used it like this-
    
    ///////////
    class CMainWindow : public CFrameWnd    // <<===
    {
    private:
    	CEdit m_WndName;
    //	CColorEdit m_cEdit;
    
    	CSkinListCtrl	m_SkinList;   // <<===
    ......
    };
    ////////
    
    and used all other files as u explained in the article, but sorry, it was not working...only scrollbars were visible no listbox visible there in window..
    
    Any help will be gratefull..

    Reply
  • That's good.

    Posted by Legacy on 01/07/2004 12:00am

    Originally posted by: Akram

    I modified the skin list contrl to show icon on the right
    side. Also I added code to handle the owner draw property of list control.

    Reply
  • It can compile properly, but I can't run it.

    Posted by Legacy on 09/23/2003 12:00am

    Originally posted by: justin77

    I do it myself step by step,as what you said.
    It can compile properly, but I can't run it.

    void CSkinListCtrl::PreSubclassWindow()
    {
    //use our custom CHeaderCtrl
    ##??## m_SkinHeaderCtrl.SubclassWindow(GetHeaderCtrl()->m_hWnd);

    CListCtrl::PreSubclassWindow();
    }
    "0x00403365" instruction cited "0x00000020"memory,
    this memory can't be read.

    Could you help me?
    If your want to know more details about it, please email me


    • just like this and you can run it

      Posted by changy on 06/17/2007 09:49pm

      GetHeaderCtrl()->m_hWnd is a illegal handle here. Place subClassWindow function in OnCreate()

      Reply
    • but scrollbars....

      Posted by fun2oos@rediffmail.com on 05/04/2005 01:27am

      but scrollbars r not displayed attached to list ctrl. they r separated apart...try to run it for few times...

      Reply
    • Solution

      Posted by DPS on 12/28/2004 04:02pm

      This problem let my hairs grayed, too. The solution is very simple. You should delete the complete PreSubclassWindow() function. Then call a dedicated SubclassWindow() after creation the control. Example: m_liste.Create(WS_VISIBLE | LVS_REPORT | LVS_SHOWSELALWAYS,CRect(0,0,0,0),this, ID_CLIENT_LIST); m_liste.m_SkinHeaderCtrl.SubclassWindow(m_liste.GetHeaderCtrl()->m_hWnd); First create the control (m_liste) Than subclass the Header. Now it works.

      Reply
    • Me too

      Posted by liulluil on 08/20/2004 10:02pm

      I also confront this problem.If the SkinListCtrl is created in Dialog Template and use ClassWizzard, that' OK;but when I use Create() to create it by myself,the error occurs.

      Reply
    Reply
  • What are you doing?

    Posted by Legacy on 09/04/2003 12:00am

    Originally posted by: Caprice

    You really think that LoadBitmap, CreateFontIndirect inside of OnPaint can be a kind of something useful?
    And the skinning is just a fashion. Instead of abuse the list control you can write your own control from zero with functionalities that you need. And you tried to implement it but with MFC and propose to use it with stupid wizards. Sorry, if I did not understand something.

    Reply
  • Excellent Indeed !!!

    Posted by Legacy on 08/21/2003 12:00am

    Originally posted by: Sanjeev

    It is a good article and will save plenty of hours for others who are willing to have a similar look for their application!!!
    

    Reply
  • Excellent Article with great work!!

    Posted by Legacy on 08/19/2003 12:00am

    Originally posted by: Mohini Sathe

    Excellent work, also an ideal article.
    I like the way you create your own ListControl that gives advanced UI. But loading Bitmap took lot of time, so initially while starting application took lot of time.
    Also inserting large no. of records with bitmap loading will be a tough job.

    Reply
  • good

    Posted by Legacy on 08/18/2003 12:00am

    Originally posted by: wwdz99

    a good article

    Reply
  • Quality work... but only work with "Report mode" only!!

    Posted by Legacy on 08/16/2003 12:00am

    Originally posted by: Imran

    Good effort to civilised Microsoft CListCtrl control under
    
    MFC. But the entire code works effectively when control
    only configured under design enviornment i.e. List Control
    properties->Report mode.
    There are further three mode i.e. Icon, Small Icon, List
    modes does not supported by this code, and if control
    configured then it crash.
    There is little extra coding work involve to get around
    this remedy!

    Reply
  • Very good but BK color is not having good UI

    Posted by Legacy on 08/15/2003 12:00am

    Originally posted by: Ajay

    Article is excellent! I have compiled and ran but did not see how it does that. Skinning is very well but the client color are not having good UI. Please increase readibility by setting fonts and back/fore colors.

    Thanx.

    Reply
  • Loading, Please Wait ...

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

  • "Security" is the number one issue holding business leaders back from the cloud. But does the reality match the perception? Keeping data close to home, on premises, makes business and IT leaders feel inherently more secure. But the truth is, cloud solutions can offer companies real, tangible security advantages. Before you assume that on-site is the only way to keep data safe, it's worth taking a comprehensive approach to evaluating risks. Doing so can lead to big benefits.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds