分类:
2007-08-04 16:43:12
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: .
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:
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.
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:
Before we begin, let's add the #include
to ListDemoDlg.h. Even the ClassWizard warns you about this.
From here on, we will proceed as following:
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.
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:
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
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.
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.
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(); }
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:
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): .
If you run it, you will get the following when its focused and the mouse is over:
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.
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
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:
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)
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:
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.