How to Skin CListCtrl, Including Scrollbars a...

来源:百度文库 编辑:神马文学网 时间:2024/04/24 19:02:47
How to Skin CListCtrl, Including Scrollbars and Column Headers

 
Environment: VC6 SP5, Windows 2000 SP3, Windows 9xIntroduction
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.
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. 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. 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 I could just try to skin the existing CListCtrl.
Obviously, I chose the latter because time is of the essence.
Post a commentEmail ArticlePrint ArticleShare Articles Digg
del.icio.us
Newsvine
Facebook
Google
LinkedIn
MySpace
Reddit
Slashdot
StumbleUpon
Technorati
Twitter
Windows Live
YahooBuzz
FriendFeed
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 ".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 firstm_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.hpublic: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 functionvoid CSkinHeaderCtrl::OnPaint(){CPaintDC dc(this); // device context for paintingCRect 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; vThat 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; iNext, 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//thumbdouble 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 itint 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 dynamicallypList->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 CHeaderCtrlm_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 filevoid CSkinListCtrl::Init(){//A way to hide scrollbarsInitializeFlatSB(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 itCWnd* 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 runtimem_SkinverticalScrollbar.Create(NULL, WS_CHILD|SS_LEFT|SS_NOTIFY|WS_VISIBLE|WS_GROUP,CRect(windowRect.right-nDialogFrameWidth,windowRect.top-nTitleBarHeight-nDialogFrameHeight-1,windowRect.right+12-nDialogFrameWidth,windowRect.bottom+11-nTitleBarHeight-nDialogFrameHeight),pParent);m_SkinHorizontalScrollbar.Create(NULL, WS_CHILD|SS_LEFT|SS_NOTIFY|WS_VISIBLE|WS_GROUP,CRect(windowRect.left-nDialogFrameWidth,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
Make the CSkinScrollbar and CSkinHeaderCtrl classes work with any size of bitmap without the need for changing code in these classes. 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. Somehow have the CSkinList::Init function run by itself so that we don't have to call m_SkinList.Init(); ourselves. Grab the height of each row (give the font style, size, and weight) dynamically, instead of using a hardcoded value. 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.aspDownloads
Download demo project - 87 Kb