Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / MFC
Article

Control Subclassing

Rate me:
Please Sign up or sign in to vote.
4.94/5 (78 votes)
7 May 2003CPOL20 min read 556.1K   7.9K   295   88
This article explains how to subclass controls so that they act and look the way you desire. It uses a listbox as an example.

Sample Image

Introduction

Although Windows comes with a great variety of common controls such Edit controls and Combo Boxes, many times their functionality is limited, their appearance unsuitable, or they simply do not fit our needs. To solve this problem, a new custom control can be created or we can subclass an existing one, in many cases reducing the amount of work.

There are two types of subclassing, instance and global. Instance subclassing means that each control or instance is individually altered. On the contrary, a hook is created during global subclassing so that several instances, if not all, are modified in some respect at run-time. An example of a global subclassing is to add skins to all buttons created in a software or on all of its threads while an instance type would be to manually create a single CEdit control so that numbers are shown in red.

In this tutorial, we will subclass an instance of the CListBox class, as seen above, to allow it to have the ability to change its colors, include icons, have a custom scrollbar, and a MouseOver effects. If time permits, I will soon write an article on how to perform global subclassing. For more information on subclassing, you may want to check Chris Maunder's tutorial: Create your own controls - the art of subclassing.

The Birth of a New Class

First we must start a new project so that we can develop and debug the new class. After it is polished, it can be easily added to any project. In my case, I created an MFC Dialog and named it ListDemo. Now we must create a new derived class with CListBox as its base. Go to the menu View->Classwizard and then click on Add Class -> New Button. Then type the class name CListBoxEx and choose CListBox as the base name. This is the name of the new class; it may be anything you wish. The red circles show where you should go:

Image 2

Now click OK and we are ready to begin. Go to the Dialog Editor and add a list box, the one that we will subclass. We will use this list to test our code. Right click and go to properties and in the Styles tab, where it says Owner Draw, set it to Variable. Whenever we want to subclass a control, we must make it Owner Drawn. Since a list box consists of several items, we set it to variable so that a function, MeasureItem, is called to retrieve the height of each item and therefore, we can modify it. We must check Has strings since the one we are making can have strings, not only icons. Now uncheck Vertical Scroll because we do not wish for the usual scroll bar to appear.

Image 3

Go again to the ClassWizard and click on the Member Variables tab. Choose the ID of the list box and click on Add Variable. Make sure you select the Category as control and the Variable type as CListBoxEx. I named mine m_DemoList:

Image 4

Before we begin, let's add the #include <ListBoxEx.h> to ListDemoDlg.h. Even the ClassWizard warns you about this.

From here on, we will proceed as following:
Coding: The Fun Part

  • The ListBox Frame: The Border and MouseOver Effect.
  • The Background
  • The ListBox Items: Text and Bitmap
  • Scrollbars

Coding: The Fun Part

Now we are ready to undertake the great journey into the mystifying code. Not really. Thanks to subclassing, it is all relatively easy. In windowless controls such as the one we are using, the Create and PreCreateWindow messages along with many others are not called. Therefore, we must rely on functions such as the constructor and PresubclassWindow for initialization procedures. I prefer the latter because calling certain Window functions such as m_bEnabled = IsWindowEnabled(); , on the constructor, will result in an illegal operation since the contol's HWND (m_hWnd)is still NULL. However, PresubclassWindow is called after the object is attached to the window. Therefore, let's add a handler for this virtual function.

On the ClassWizard, select CListBoxEx as the Class Name and scroll through the messages until you find PresubclassWindow, select it, and double-click on it or click Add Function. We get:

void CListBoxEx::PreSubclassWindow() 
{
    // TODO: Add your specialized code here and/or call the base class
    
    CListBox::PreSubclassWindow();
}

We are now ready to draw the borders.

1. The ListBox Frame

We want to make a border so that it looks as a normal one but when the mouse cursor enters the list box, it will change, thus making it more interactive. You may want to check the picture on the top. As you can see, the border that surrounds the first list, which has the mouse cursor over, seems to be darker and 2D while the second list gives the impression that it is 3D and pushed backwards.

The first thing that we must do is to create a variable that will keep track of whether the mouse is over. Let's call it m_bOver and make it type BOOL (TRUE/FALSE). We should declare it in the protected section because we do not want sources other than those derived from it or itself to have direct access to the variable. There are two easy ways to do it. You may either declare it under protected in ListBoxEx.h or in VC++ 6, click on ClassView on the workspace, and right click on CListBoxEx and then click on Add Member Variable:

Image 5

We must now make a function to draw the borders. Use the following declaration under protected: void DrawBorders (); or do the same to add a variable, but instead click on Add Member Function. We get the following implementation:

void CListBoxEx::DrawBorders()
{

}

We now start typing the code:

void CListBoxEx::DrawBorders()
{
    //Gets the Controls device context used for drawing
    CDC *pDC=GetDC();
    
    //Gets the size of the control's client area
    CRect rect;
    GetClientRect(rect);
    
    /*
    Inflates the size of rect by the size of the default border
    Suppose rect is (0,0,100,200) and the default border is 2 pixels,
    after InflateRect, rect should be (-2,-2, 102,202) and the border
    will be drawn from -2 to 0, -2 -> 0, 102->100, 202->200.
    */
    rect.InflateRect(CSize(GetSystemMetrics(SM_CXEDGE),
        GetSystemMetrics(SM_CYEDGE)));
    
    //Draws the edge of the border depending on whether the mouse is
    //over or not    
    if (m_bOver)pDC->DrawEdge(rect,EDGE_BUMP ,BF_RECT );
    else pDC->DrawEdge(rect,EDGE_SUNKEN,BF_RECT ); 
    
    ReleaseDC(pDC); //Frees the DC
}

The function DrawEdge is generally used to draw borders. For instance, EDGE_BUMP is used to draw the default listbox border, with the inner section sunken. Others commonly used are EDGE_ETCHED,EDGE_SUNKEN, and EDGE_RAISED.

The code above won't have any effect yet. We still have to determine when the mouse enters and when it leaves so that we can modify m_bOver. We also need to call DrawBorders(). Since we'll be using m_bOver, let's initialize it. Add m_bOver = FALSE; on PreSubclassWindow.

One of the various methods to figure out when the mouse enters is to use the message handler for WM_MOUSEMOVE and if m_bOver is FALSE, then it means that it entered for the first time. We must then make m_bOver = TRUE and call the DrawBorders() function to change the border style. To do this, go to the ClassWizard and add a function for WM_MOUSEMOVE. Then we add the rest of the code. Finally we get:

void CListBoxEx::OnMouseMove(UINT nFlags, CPoint point) 
{
    // TODO: Add your message handler code here and/or call default
    
    //If m_bOver==FALSE, and this function is called, it means that the
    //mouse entered.    
    if (!m_bOver){ 
        m_bOver=TRUE; //Now the mouse is over
        DrawBorders(); //Self explanatory
    }
    
    CListBox::OnMouseMove(nFlags, point);
}

If you run the program now, you'll notice that the border changes when the mouse enters but not when it leaves. That's because we must determine when to set m_bOver to FALSE and redraw them.

However, it is not that simple to determine when the mouse leaves the area since the listbox won't be notified of outside movement. So far, I know of three ways to do this: using a timer, capturing the mouse, or manually adding a OnMouseLeave function. Employing the timer is explained in the article mentioned at the beginning. When the mouse enters,on OnMouseMove, we could call SetCapture() and every time it moves, use PtInRect to see if its above the listbox. If it's not, then we ReleaseCapture() and set m_bOver=FALSE;. Sounds too easy to be true. Well, it is and it is not. This method can be used in other controls that do not have list of items or similar things. Although we could use it in a listbox, it would require more knowledge and extra work. In order to prevent an application from monopolyzing the cursor, Windows automatically releases capture once it changes focus. For this reason, once an item is selected, our capture will relinquish. Therefore, I will focus on the third technique.

Since ther is no macro implemented for this message, we will have to do a little bit of work. Find:

BEGIN_MESSAGE_MAP(CListBoxEx, CListBox)

and after ON_WM_MOUSEMOVE(), insert ON_MESSAGE(WM_MOUSELEAVE,OnMouseLeave).

BEGIN_MESSAGE_MAP(CListBoxEx, CListBox)
    //{{AFX_MSG_MAP(CListBoxEx)
    ON_WM_MOUSEMOVE()
    ON_MESSAGE(WM_MOUSELEAVE,OnMouseLeave) //Add this
    //}}AFX_MSG_MAP
END_MESSAGE_MAP()

Notice that no semicolon is needed. What we basically did was use the ON_MESSAGE macro so that when the control receives WM_MOUSELEAVE, it will go to the function OnMouseLeave (Can be any name). We must now create the declaration. Under the protected ListBoxEx.h section, go to:

//{{AFX_MSG(CListBoxEx)
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
//}}AFX_MSG

Then insert afx_msg LRESULT OnMouseLeave(WPARAM wParam, LPARAM lParam);

//{{AFX_MSG(CListBoxEx)
afx_msg void OnMouseMove(UINT nFlags, CPoint point);
afx_msg LRESULT OnMouseLeave(WPARAM wParam, LPARAM lParam); //Add this
//}}AFX_MSG

We proceed by implementing the function on ListBoxEx.cpp:

LRESULT CListBoxEx::OnMouseLeave(WPARAM wParam, LPARAM lParam){

    m_bOver=FALSE;
    DrawBorders();
    
    return 0;
}

Since the mouse is no longer over, we set m_bOver to FALSE and call DrawBorders() to notice the change. If we run this program now, we'll see nothing happens. That's because the message is not being sent to the control. We must therefore tell windows to notify us. For this, we use the TrackMouseEvent function which takes as a parameter the structure TRACKMOUSEEVENT (we must declare it). The dwFlags in this structure must contain TME_LEAVE. Let's add it on OnMouseMove whenever the mouse enters:

if (!m_bOver){ 
    m_bOver=TRUE; //Now the mouse is over
    DrawBorders(); //Self explanatory

    //Add here...
    TRACKMOUSEEVENT track; //Declares structure
    track.cbSize=sizeof(track);
    track.dwFlags=TME_LEAVE; //Notify us when the mouse leaves
    track.hwndTrack=m_hWnd; //Assigns this window's hwnd
    TrackMouseEvent(&track); //Tracks the events like WM_MOUSELEAVE
}

To conclude this section, add #define _WIN32_WINNT 0x0400 prior to #include <afxwin.h> in StdAfx.h in order to make the TrackMouseEvent function available. This only works under Windows 32 bits and the NT framework. If your application is aimed toward the old 16 bits, you must use timers or capture the mouse.

Image 6

2. The Background

Setting a color for the background is one of the easiest things. Instead of providing an RGB color, we create a brush using CBrush. This is even better because instead of a background color, patterns can be easily created and bitmaps loaded.

In order to change the color, we must have CBrush variable. Create one, CBrush m_BkBrush; in the protected section in the header file. We do not need to set it to a default value because windows will use the default brush if it NULL. We can now add a function so that its parent window can change the color. In order for it use it, we must declare it under public. Remember that we also wish to change the color of the cell that's highlighted. Let's kill two birds with one stone and also include this value as a parameter.

//Declare under public in header file

void SetBkColor( COLORREF crBkColor, COLORREF crSelectedColor =
    GetSysColor(COLOR_HIGHLIGHT));

Now when we wish to change the color, we can simply call SetBkColor. The second parameter is optional. If the it is not entered, it will be set to the default highlight color, i think is dark blue without desktop themes, retrieved by GetSysColor. We'll use the second one in the next section. In the implementation of the function, we must create a brush with the current color and Invalidate() to force repaint.

void CListBoxEx::SetBkColor(COLORREF crBkColor,COLORREF crSelectedColor)
{
    //Deletes previous brush. Must do in order to create a new one    

    m_BkBrush.DeleteObject(); 
    //Sets the brush the specified background color
    m_BkBrush.CreateSolidBrush(crBkColor); 
    Invalidate(); //Forces Redraw
}

The WM_CTLCOLOR is sent before the control is drawn. Its return value is the brush that we'll be used to draw the background of the window. Therefore, we must intercept it to return m_BkBrush. In the ClassWizard add a handler for =WM_CTLCOLOR. We are using the reflected message (=) because this way, the parent receives the message to draw it and reflects it back for us to do the job. Initially, the return value is NULL. We must change NULL to m_BkBrush.

We should also make sure that we change the brush if the control is not enabled.

HBRUSH CListBoxEx::CtlColor(CDC* pDC, UINT nCtlColor) 
{
    // TODO: Change any attributes of the DC here
    if (!IsWindowEnabled()){
    CBrush br(GetSysColor(COLOR_INACTIVEBORDER));
    return br;
    }
    
    // TODO: Return a non-NULL brush if the parent's handler should not
    // be called
    return m_BkBrush;

}

The last thing is to delete the brush when it exits. We will do this in the destructor (~CListBoxEx()).

CListBoxEx::~CListBoxEx()
{
    m_BkBrush.DeleteObject(); //Deletes the brush
}

Now that all's done, we must try it. On InitDialog() in ListDemoDlg.cpp, we add:

m_DemoList.SetBkColor(RGB(0,0,128)); 

This will set the background to dark blue.

Image 7

There may be times when the control is enabled or disabled at run-time. As a result, we should receive the WM_ENABLE message, which indicates that the control's enabled state has changed. Use the ClassWizard to add a function for this. We should force redraw when it changes state.

void CListBoxEx::OnEnable(BOOL bEnable) 
{
    CListBox::OnEnable(bEnable);
    
    // TODO: Add your message handler code here
    Invalidate();
}

3. The Items

This is probably the longest section. We have several goals. Among them are life, liberty, and the pursue of happiness. Anyways, we want to make the list so it displays colored text and display a bitmap for each item, if desired. When the user makes a selection, the selected item must also be highlighted.

We are going to declare various variables now. We need one to track the color of the text, the color of the text when highlighted, the size of each item, the background color of the item when selected, and the dimensions of the bitmaps to be used. All of these should be under the protected section.

short m_ItemHeight; //Height of each item
COLORREF m_crTextHlt; //Color of the text when highlighted
COLORREF m_crTextClr; //Color of the text
COLORREF m_HBkColor; //Color of the highlighted item background
int m_BmpWidth; //Width of the bitmap
int m_BmpHeight; //Height of the bitmap

We then set them to an initial value under PreSubclassWindow:

m_bOver = FALSE;
m_ItemHeight=18; 
m_crTextHlt=GetSysColor(COLOR_HIGHLIGHTTEXT);
m_crTextClr=GetSysColor(COLOR_WINDOWTEXT);
m_HBkColor=GetSysColor(COLOR_HIGHLIGHT);
m_BmpWidth=16;
m_BmpHeight=16;

To set the height of each item, we must overwrite OnMeasureItem. On the ClassWizard, add a function for MeasureItem. Then, set the field itemHeight to m_ItemHeight:

void CListBoxEx::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct) 
{
    // TODO: Add your code to determine the size of specified item
    lpMeasureItemStruct->itemHeight=m_ItemHeight;

}

We need to now add a function which allows the modification of m_ItemHeight. Let's call it void SetItemHeight (int newHeight) and let it be public.

void CListBoxEx::SetItemHeight(int newHeight)
{
    m_ItemHeight=newHeight;
    Invalidate();
}

Before we begin painting each individual item, we must create a function that takes a string and the resource ID of the bitmap. We will call it void AddItem(UINT IconID, LPCTSTR lpszText). Since it is meant to be used by any other code, we will make it public. This function would be similar to AddString but would allow us to add bitmaps too. IconID is the ID of the bitmap resource, such as IDB_MYBITMAP, and the other one is the text to be displayed next to the image.

We will use the AddString function to add the text. However, we need another to somehow associate that item with the ID so that in DrawItem (explained later), we can draw the bitmap. AddString and InsertString return the index of the current item. Therefore, we will use SetItemData to associate the index with the resource. We can then easily obtain the ID for the specified index.

void CListBoxEx::AddItem(UINT IconID, LPCTSTR lpszText)
{
    //Adds a string ans assigns nIndex the index of the current item
    int nIndex=AddString(lpszText);
    //If no error, associates the index with the bitmap ID
    if (nIndex!=LB_ERR&&nIndex!=LB_ERRSPACE)
        SetItemData(nIndex, IconID);
}

To expand its usability, we would also like to give it the ability to insert items in a specified index, thus shifting down the rest of the cells. This one would be similar to AddItem execpt it will receive one more variable, the index where to inserted it. The prototype will be void InsertItem(int nIndex, UINT nID, LPCTSTR lpszText), and the implementation:

void CListBoxEx::InsertItem(int nIndex, UINT nID, LPCTSTR lpszText)
{
    int result=InsertString(nIndex,lpszText); //Inserts the string
    
    //Associates the ID with the index

    if (result!=LB_ERR||result!=LB_ERRSPACE) SetItemData(nIndex,nID); 
}

If you looked carefully at the image of the three listboxes at the beginning, you should have seen that normal text can be added, just like in any list box. Also, you can add text that is indented but does not have any picture. To do this, you have to enter a special value as the ID. Let's make so that if the ID is NO_BMP_ITEM or NULL, the text will be as in a normal listbox. However, if BLANK_BMP is passed to this ID argument, the text will be indented to match the ones with bitmaps, but it will not display any bitmap. Add this in the header file, before the class begins:

#define NO_BMP_ITEM 0
#define BLANK_BMP 1

Note: We assign 0 to NO_BMP_ITEM because 0 is also NULL. Therefore, to display normal text, bitmap ID can be either NULL or NO_BMP_ITEM. Any number would work for BLANK_BMP.

The message =WM_DRAWITEM is sent when each item needs to be drawn. For instance, in a 10 item list, it t will be called 10 times, each time with information on the item to be currently drawn. Add a handler for the reflected message =WM_DRAWITEM. We get:

void CListBoxEx::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) 
{
    // TODO: Add your message handler code here

}

The structure lpDrawItemStruct conatins all the information on the item to be drawn.

Here's how we will draw the item:

Image 8

We first need to get the DC (Device Context) from the structure, along with several other information. Add:

CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC); //Gets the item DC
//Retrieves the ID added using SetItemData
UINT nID=(UINT)lpDrawItemStruct->itemData;
CRect rect=lpDrawItemStruct->rcItem; //Gets the rect of the item
UINT action=lpDrawItemStruct->itemAction; //What it wants to do
UINT state=lpDrawItemStruct->itemState; //The item current state
COLORREF TextColor=m_crTextClr; //Text color that we'll use

The field itemAction contains what we must do and itemState what the state should be after we perform the operations. To exemplify this, suppose an item is selected and has the focus, but the user clicks on an Edit box. The focus must change. In this case, action would be !focus (remove focus) and state would be not focused. We also declared a variable that will have the color of the text to be drawn.

Here are the actions and states that we should take into account. At first they are a bit confusing but with practice, you should understand them. Insert:

//Action statements
if ((state & ODS_SELECTED) &&
    (action & ODA_SELECT))
    //Used when an item needs to be selected
{
    //Since it will be highlighted, we create a brush with the
    //highlighted color
    CBrush brush(m_HBkColor); 
    //Draws the highlighted rect    
    pDC->FillRect(rect, &brush);

}

if (!(state & ODS_SELECTED) &&    (action & ODA_SELECT))
    //The item needs to be deselected
{
    //We draw the background color    
    pDC->FillRect(rect, &m_BkBrush);
}

if ((action & ODA_FOCUS) && (state & ODS_FOCUS)&&(state&ODS_SELECTED)){
    //It has the focus,
    //Draws a 3D focus rect    
    pDC->Draw3dRect(rect,RGB(255,255,255),RGB(0,0,0)); 
    TextColor=m_crTextHlt;    
}

    
if ((action & ODA_FOCUS) && !(state & ODS_FOCUS)&&(state&ODS_SELECTED)){
    
    //If the focus needs to be removed.
    CBrush brush(m_HBkColor); 
    pDC->FillRect(rect, &brush);
    TextColor=m_crTextHlt;    
}
//If the control is disabled
if (state&ODS_DISABLED) TextColor=GetSysColor(COLOR_3DSHADOW);

Now we must retrieve the text to display, and set its color and background mode.

CString text;
GetText(lpDrawItemStruct->itemID, text); //Gets the item text
pDC->SetTextColor(TextColor); //No need to explain
pDC->SetBkMode(TRANSPARENT); //Sets text background transparent

We are now ready to draw the bitmap and display the text.

if (nID!=NO_BMP_ITEM){ //If the item has a bitmap
    
    CDC dcMem; //New device context used as the source DC
    //Creates a deice context compatible to pDC
    dcMem.CreateCompatibleDC(pDC); 
    
    CBitmap bmp; //Bitmap object
    //Loads the bitmap with the specified resource ID
    bmp.LoadBitmap(nID); 
    //Saves the old bitmap object so that the GDI resources are not
    //depleted
    CBitmap* oldbmp=dcMem.SelectObject(&bmp); 
    
    if (nID!=BLANK_BMP) //Draws the bitmap if it is not blank
        //Copies the bitmap to the screen
        pDC->BitBlt(rect.left+5,rect.top,m_BmpWidth,m_BmpHeight,
            &dcMem,0,0,SRCCOPY); 
    
    //Selects the saved bitmap object
    dcMem.SelectObject(oldbmp); 
    
    bmp.DeleteObject(); //Deletes the bitmap
    
    //Displays the text 
    pDC->TextOut(rect.left+10+m_BmpWidth,rect.top,text); 
}
//Displays the text without indenting it
else pDC->TextOut(rect.left+5,rect.top,text);

We are done with most of the code in this section. Nonetheless, we are still missing some member functions such as that to change the color of the text.

Go back to void CListBoxEx::SetBkColor(COLORREF crBkColor,COLORREF crSelectedColor) and add m_HBkColor=crSelectedColor; in order to change the highlighted color.

In the header file, add the following prototype in the public entity, void SetTextColor(COLORREF crTextColor, COLORREF crHighlight);. The function code should be:

void CListBoxEx::SetTextColor(COLORREF crTextColor, COLORREF crHighlight)
{
    m_crTextClr=crTextColor;
    m_crTextHlt=crHighlight;
    Invalidate();
}

Finally, we need to be able to alter the dimensions of the bitmap. Declare the appropriate prototype for:

void CListBoxEx::SetBMPSize(int Height, int Width)
{
    m_BmpHeight=Height;
    m_BmpWidth=Width;
    Invalidate();
}

Done! It's time to test it:

Go back to ListDemoDlg.cpp and OnInitialUpdate() delete m_DemoList.SetBkColor(RGB(0,0,128)); Now add the following:

m_DemoList.SetBkColor(RGB(0,0,128),RGB(190,0,0));
m_DemoList.SetBMPSize(16,30);
m_DemoList.SetTextColor(RGB(0,255,10),RGB(255,255,0));
m_DemoList.SetItemHeight(17);
m_DemoList.AddString("Hey World");
m_DemoList.AddItem (IDB_COOL,"Hello World!");
m_DemoList.AddItem (NO_BMP_ITEM,"Hi World!");
m_DemoList.InsertItem(2,BLANK_BMP, "Greetings");

This examines every function we have made so far. The ID IDB_COOL is a bitmap I created. Its width is 30 pixels and its height 16. Here's the picture I created (Not creative, but works for our example): Image 9.

If you run it, you will get the following when its focused and the mouse is over:

Image 10

The items are sorted except when you use InsertItem. If you want to create your own sorting algorithm, you should overwrite CompareItem.

We can now continue into the final part, the scrollbar.

4. Scrollbars

For simplicity purposes. the scrollbars that we are going to make are going to be static, always shown regardless of whether they are needed. I don't think we are using the correct term since they don't have bars but who cares. As we all know, we must draw them. However, the problem is how to do it so that it is within the listbox rect and does not cover any item. There's a simple solution, we can resize the client area. This can be done by receiving the message WM_NCCALCSIZE. Add a function for it, and we get:
void CListBoxEx::OnNcCalcSize(BOOL bCalcValidRects,
    NCCALCSIZE_PARAMS FAR* lpncsp) 
{
    // TODO: Add your message handler code here and/or call default
    
    CListBox::OnNcCalcSize(bCalcValidRects, lpncsp);
}

The argument lpncsp contains three rects. The first one (rgrc[0]) is that of the client rect. The others ones are not very useful in most cases. Suppose we want one scrollbar to be 16 pixels in height, we would add:

lpncsp->rgrc[0].top += 16; //Top
lpncsp->rgrc[0].bottom -= 16; //Bottom

Image 11

In most cases, this function will not be called automatically. We will call the SetWindowsPos so the WM_NCCALCSIZE is sent. It will only works if we pass the flag SWP_FRAMECHANGED to the function. Since the scrollbar is part of the nonclient area, we will add a handler for WM_NCPAINT. This will be executed when the nonclient area needs to be painted. We should also draw the borders. SetWindowPos will only be called the first time the nonclient area is drawn.

void CListBoxEx::OnNcPaint() 
{
    // TODO: Add your message handler code here

    static BOOL before=FALSE;
    if (!before) {
        //If first time, the OnNcCalcSize function will be called
        SetWindowPos(NULL,0,0,0,0,
            SWP_FRAMECHANGED|SWP_NOMOVE|SWP_NOSIZE); 
        before=TRUE;
    }
    DrawBorders();

    // Do not call CListBox::OnNcPaint() for painting messages
}

It it now time to create a protected function that draws the scrollbars: void DrawScrolls(UINT WhichOne, UINT State);. As you can see, it has two parameters. The first one tells which scroll to draw (Down or Up) and the other one the state, like pressed. To make it easier, let's #define a few things in the header file. You'll have something like this in the header file:

// ListBoxEx.h : header file
//
#define NO_BMP_ITEM 0
#define BLANK_BMP 1


#define SC_UP 2 //Up scroll
#define SC_DOWN 3 //Down Scroll


#define SC_NORMAL NULL //Normal scroll
#define SC_PRESSED DFCS_PUSHED //The scroll is pressed
#define SC_DISABLED DFCS_INACTIVE //The scroll is disabled
/////////////////////////////////////////////////////////////////////////////
// CListBoxEx window

Things like DFCS_PUSHED are the states for a function that we will use next: DrawFrameControl. You make think of SC_PRESSED as a clone with a different name. And now the implementation of DrawScrolls:

void CListBoxEx::DrawScrolls(UINT WhichOne, UINT State)
{

    CDC *pDC=GetDC();
    CRect rect;
    GetClientRect(rect); //Gets the dimensions
    
    //If the window is not enabled, set state to disabled

    if (!IsWindowEnabled())State=SC_DISABLED; 
    
    //Expands the so that it does not draw over the borders    

    rect.left-=GetSystemMetrics(SM_CYEDGE); 
    rect.right+=GetSystemMetrics(SM_CXEDGE);
    
    if (WhichOne==SC_UP){ //The one to draw is the up one
        
        //Calculates the rect of the up scroll
        rect.bottom=rect.top-GetSystemMetrics(SM_CXEDGE);
        rect.top=rect.top-16-GetSystemMetrics(SM_CXEDGE);
        
        //Draws the scroll up
        pDC->DrawFrameControl(rect,DFC_SCROLL,State|DFCS_SCROLLUP);
    }
    else{ //Needs to draw down
        
        rect.top=rect.bottom+GetSystemMetrics(SM_CXEDGE);;
        rect.bottom=rect.bottom+16+GetSystemMetrics(SM_CXEDGE);
        
        pDC->DrawFrameControl(rect,DFC_SCROLL,State|DFCS_SCROLLDOWN);
        
    }    
    ReleaseDC(pDC);
}

DrawFrameControl is generally used to draw the controls like scrollbars and buttons in owner drawn controls. Now go back to OnNcPaint and add (outside the if statement):

DrawScrolls(SC_UP,SC_NORMAL);
DrawScrolls(SC_DOWN,SC_NORMAL);

We should get:

Image 12

Now we must make it scroll and change the appearance of the scrollbar when it is pressed. Since it is not a border or default scroll bar, non-client messages such as NcLButtonDown will not work by default. Therefore, we must tweak around with the code a little. Although most of them do not work, WM_NCHITTEST, which indicates that there is mouse movement, will be sent when the mouse enters the scroll. For this reason, add a message handler for it.

The return value is where the mouse located. In order to use OnNcLButtonDown to see when the left button is pressed, we will fake that the mouse is over the original scrollbar. If it is one the top scrollbar, we will return HTVSCROLL and if in the bottom, HTHSCROLL. In this function, the mouse position is relative to the parent rather than the client area. As a result, we must convert them.

UINT CListBoxEx::OnNcHitTest(CPoint point) 
{
    // TODO: Add your message handler code here and/or call default
    
    CRect rect,top,bottom; 
    //Gets the windows rect, relative to the parent, so rect.left and
    //rect.top might not be 0.
    GetWindowRect(rect); 
    ScreenToClient(rect); //Converts the rect to the client
    
    //Calculates the rect of the bottom and top scrolls
    top=bottom=rect;
    top.bottom=rect.top+16;
    bottom.top=rect.bottom-16;
    
    //Obtains where the mouse is
    UINT where = CListBox::OnNcHitTest(point); 

    

    //Converts the point so its relative to the client area    
    ScreenToClient(&point);     
    if (where == HTNOWHERE) //If mouse is not in a place it recognizes
        if (top.PtInRect(point))
            //Check to see if the mouse is on the top
            where = HTVSCROLL;
        else if (bottom.PtInRect(point))
            //Check to see if its on the bottom
            where=HTHSCROLL; 
        
        return where; //Returns where it is
}

Add a handler for WM_NCLBUTTONDOWN. This will now be called when the mouse is pressed on the scrollbar and we can check nHitTest to see which one was pressed.

We will use the SendMessage function to fake the click of the real vertical scrollbar. We could also use ScrollWindow. If you click and hold the left button on a scrollbar such as that in your browser, you'll notice that it will scroll as long as you do not release the button. To make this a reality, we will use a timer, which will scroll every 100 milliseconds. In reality, on most systems, it will be 110 milliseconds. This is because the hardware timer in which Windows work ticks once every 54.9 seconds (approximately). Therefore, Windows will round the value passed to SetTimer up to the next multiple of 55 milliseconds.

We will have:

void CListBoxEx::OnNcLButtonDown(UINT nHitTest, CPoint point) 
{
    // TODO: Add your message handler code here and/or call default

    if (nHitTest==HTVSCROLL) //Up scroll Pressed
    {
        DrawScrolls(SC_UP,SC_PRESSED);
         //Scroll up 1 line
        SendMessage(WM_VSCROLL,MAKEWPARAM(SB_LINEUP,0),0);
        
        SetTimer(1,100,NULL); //Sets the timer ID 1
    }
    else if (nHitTest==HTHSCROLL) //Down scroll Pressed
    {
        DrawScrolls(SC_DOWN,SC_PRESSED);
        //Scroll down 1 line
        SendMessage(WM_VSCROLL,MAKEWPARAM(SB_LINEDOWN,0),0); 
        SetTimer(2,100,NULL); //Sets the timer ID 2
    }
    
    CListBox::OnNcLButtonDown(nHitTest, point);
}

Of course, we must now add a WM_TIMER function. We know that if the ID is one, we will scroll up. Otherwise, scroll down. Also, if when the timer is called, the left button is no longer pressed, we must obliterate the timer and redraw the normal scroll.

void CListBoxEx::OnTimer(UINT nIDEvent) 
{
    // TODO: Add your message handler code here and/or call default
    
    //Gets the state of the left button to see if it is pressed
    short result=GetKeyState(VK_LBUTTON);
    
    if (nIDEvent==1){ //Up timer
        
        //If it returns negative then it is pressed
        if (result<0){
            SendMessage(WM_VSCROLL,MAKEWPARAM(SB_LINEUP,0),0);
        }
        else { //No longer pressed
            
            KillTimer(1);
            DrawScrolls(SC_UP,SC_NORMAL);        
        }
    }
    else { //Down timer
        
        //If it returns negative then it is pressed
        if (result<0){
            SendMessage(WM_VSCROLL,MAKEWPARAM(SB_LINEDOWN,0),0);
        }
        else {
            KillTimer(2);
            DrawScrolls(SC_DOWN,SC_NORMAL);        
        }
    }
    
    CListBox::OnTimer(nIDEvent);
}

At last, we are finished with CListBoxEx.

We should now check to see if the scrollbars work correctly. Therefore, let's add more items. You can do a loop in InitialUpdate in ListDemoDlg.cpp to test it or add more things. I modified InitialUpdate to repeat them:

m_DemoList.SetBkColor(RGB(0,0,128),RGB(190,0,0));
m_DemoList.SetBMPSize(16,30);
m_DemoList.SetTextColor(RGB(0,255,10),RGB(255,255,0));
m_DemoList.SetItemHeight(17);

for (int i=0;i<=5;i++){
    m_DemoList.AddString("Hey World");
    m_DemoList.AddItem (IDB_COOL,"Hello World!");
    m_DemoList.AddItem (NO_BMP_ITEM,"Hi World!");
    m_DemoList.InsertItem(2,BLANK_BMP, "Greetings");
    m_DemoList.AddItem (IDB_COOL,"Vacation's Great!");

}

While clicking the down scroll bar, we should get something like this: (*I unchecked the Sort property)

Image 13

You should remember that throughout the code, we added a few lines in case the control is disabled. Go back to OnEnable and add:

//SC_NORMAL will be changed to SC_DISABLED if the window is disabled
DrawScrolls(SC_UP,SC_NORMAL);
DrawScrolls(SC_DOWN,SC_NORMAL);

We must disable the control and see if it work. We'll get this:

Image 14

Conclusion

Now that the code is complete, it can be easily integrated into other projects by adding the source code files to the project. Then you should include the header file and instead of creating a CListbox variable, create a CListBoxEx. This has to be done manually because the new class will not appear in the ClassWizard. Don't lose hope yet, there's a trick to use the ClassWizard. Close the project and in its folder, you will find a file with a CLW extension. Delete it and open the project again. Now go to the ClassWizard, and you'll be asked to rebuild it. Then you'll be able to use this updated version of the wizard and add a the listbox variable as shown in the beginning of this article.

As you should have seen, subclassing is not as difficult as it might have seemed at first. It just requires a little bit of knowledge, patience, and practice. Sometimes, you need to rely on other tools when subclassing. For instance, there are many cases in which you might not be sure what messages a section might be receiving. For this, you can use Spy++ found in the Tools menu. Other times, the TRACE macro is really useful for catching small bugs that lurk behind most code. Now you should be able to apply this knowledge to other controls since the framework is almost identical.

Have fun programming.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Generalsa Pin
skmishra198624-Nov-09 2:30
skmishra198624-Nov-09 2:30 
GeneralFantastic... Pin
sona_ta18-Oct-09 22:21
sona_ta18-Oct-09 22:21 
GeneralMy vote of 2 Pin
levietdung88111-Aug-09 15:05
levietdung88111-Aug-09 15:05 
GeneralIS VERY GOOD Pin
xiasongchuan19-Oct-08 16:19
xiasongchuan19-Oct-08 16:19 
GeneralRe: IS VERY GOOD Pin
levietdung88111-Aug-09 15:17
levietdung88111-Aug-09 15:17 
GeneralModifyStyle(0,BS_OWNERDRAW); Pin
Danial Kahani15-Mar-08 1:11
Danial Kahani15-Mar-08 1:11 
Generalthank you Pin
wipehindy26-Feb-08 12:39
wipehindy26-Feb-08 12:39 
GeneralBug - Control Size issue ... Pin
VEMS26-Dec-07 9:12
VEMS26-Dec-07 9:12 
GeneralRe: Bug - Control Size issue ... Pin
VEMS27-Dec-07 2:36
VEMS27-Dec-07 2:36 
GeneralVery Good Pin
Sayyed Mostafa Hashemi14-Nov-07 3:43
Sayyed Mostafa Hashemi14-Nov-07 3:43 
Generalalpha channel for selected state Pin
ShiriA6-Sep-06 15:00
ShiriA6-Sep-06 15:00 
GeneralThankyou........ Pin
cfilorux24-May-06 11:09
cfilorux24-May-06 11:09 
GeneralHelp - how to subclassing CListView Pin
inbakumar.G12-Dec-05 4:16
inbakumar.G12-Dec-05 4:16 
GeneralCustom combo Pin
tuxyboy18-Sep-05 23:44
tuxyboy18-Sep-05 23:44 
GeneralThe name of the SubClass Pin
Alex Evans19-Feb-05 18:15
Alex Evans19-Feb-05 18:15 
GeneralRe: The name of the SubClass Pin
jrocnuck28-Mar-05 15:43
jrocnuck28-Mar-05 15:43 
GeneralRe: The name of the SubClass Pin
Alex Evans28-Mar-05 16:48
Alex Evans28-Mar-05 16:48 
GeneralRe: The name of the SubClass Pin
Alex Evans28-Mar-05 17:28
Alex Evans28-Mar-05 17:28 
GeneralRe: The name of the SubClass Pin
Alex Evans28-Mar-05 17:37
Alex Evans28-Mar-05 17:37 
GeneralDrawFrameControl Pin
xiuguang9-Dec-04 6:08
xiuguang9-Dec-04 6:08 
GeneralUsing the CListBoxEx in .NET 2003 Pin
Daed3-Nov-04 23:29
Daed3-Nov-04 23:29 
GeneralUse CDC *pDC=GetDC() or CDC* pDC = CDC::FromHandle(........) to get device context Pin
ic@25-Sep-04 12:01
ic@25-Sep-04 12:01 
GeneralRe: Use CDC *pDC=GetDC() or CDC* pDC = CDC::FromHandle(........) to get device context Pin
Anonymous29-Sep-04 16:00
Anonymous29-Sep-04 16:00 
GeneralRe: Use CDC *pDC=GetDC() or CDC* pDC = CDC::FromHandle(........) to get device context Pin
Eric Sanchez29-Sep-04 16:03
Eric Sanchez29-Sep-04 16:03 
GeneralRe: Use CDC *pDC=GetDC() or CDC* pDC = CDC::FromHandle(........) to get device context Pin
ic@30-Sep-04 11:12
ic@30-Sep-04 11:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.