
The Solution
The sample project in the download has a derived CListBox named CCustomListBox contained in CustomListBox.cpp and .h. I discussed building a class derived from an MFC class in the
Custom Dialog Article previously posted, so I won't go into all that here. The cool part about building the new class is there are only two lines of text to modify to use the new one or old one:
The dialog class that displays the output of the project is named CustomCListBoxDlg. In the .h file for this class, line 11 has an include of the new derived class, and line 29 is where the actual ListBox is declared in the AFX_DATA section.
By commenting out line 11, and changing line 29 to be CListbox m_lListTest, the standard CListBox will be used.
Teaching our ListBox new tricks
This is actually the fun part of the project, where we get to define what our new control will do for us. I would prefer to have horizontal scroll bars appear and work similarly to the way our vertical ones worked by default, so that is my target.
The obvious choice of methods we need to handle in our new class is "AddString". Allowing Visual Studio to display the help file for AddString, we add a new member function at the end of CustomListBox.cpp:
int CDJListBox::AddString(LPCTSTR lpszItem)
{
return CListBox::AddString(lpszItem);
}
After adding the AddString declaration to the .h file, a build of the project should have the effect that nothing has changed and that's perfect, it means we haven't purturbed the system... yet.
Here is the complete AddString method, and discussion follows:
1 int CCustomListBox::AddString(LPCTSTR lpszItem)
2 {
3 int nRet = CListBox::AddString(lpszItem);
4
5 SCROLLINFO scrollInfo;
6 memset(&scrollInfo, 0, sizeof(SCROLLINFO));
7 scrollInfo.cbSize = sizeof(SCROLLINFO);
8 scrollInfo.fMask = SIF_ALL;
9 GetScrollInfo(SB_VERT, &scrollInfo, SIF_ALL);
10
11 int nScrollWidth = 0;
12 if(GetCount() > 1 && ((int)scrollInfo.nMax >= (int)scrollInfo.nPage))
13 {
14 nScrollWidth = GetSystemMetrics(SM_CXVSCROLL);
15 }
16
17 SIZE sSize;
18 CClientDC myDC(this);
19
20 CFont* pOldFont = myDC.SelectObject(GetFont());
21
22 GetTextExtentPoint32(myDC.m_hDC, lpszItem, strlen(lpszItem), &sSize);
23
24 m_nMaxWidth = max(m_nMaxWidth, (int)sSize.cx);
25
26 SetHorizontalExtent(m_nMaxWidth + 3);
27
28 myDC.SelectObject(pOldFont);
29
30 return nRet;
31 }
First, from one call to the AddString method to the next, we need to keep track of the width of the longest line in the display. Add an integer member variable to the CustomListBox.h file. I used
m_nMaxWidth, and set it's value to 0 in the constructor.
[line 3]Because the Vertical ScrollBar appears courtesy of ClistBox, we first add our string the the base class, and save the return value for our return.
[line 5-9]Next we are going to request information about the Vertical ScrollBar by using a GetScrollInfo call. The information requested is returned to us in a SCROLLINFO structure that we have to set up. The setup is done in lines 5 through 8 and the call is made in line 9.
[line 11-15]By watching the SCROLLINFO structure through the debugger, I have found that to get a valid result in the nMax and nPage variables, I need to already have one entry in the listbox. nPage keeps track of the number of items on a logical ‘page’ of our listbox, and nMax is the total number of items. Once nMax is equal to (or greather than) nPage, we get a Vertical ScrollBar, and need to know the ScrollBar's width. I preset an integer nScrollWidth to be zero, then fill that same integer with the width of the scrollbar if it is displayed.
[line 17, 18]Next I declare a SIZE variable (line 17), to be used in the GetTextExtent call. I also create a Device Context for the client of the ListBox (line 18).
Getting the Correct Font
This is the point in the code that resolves an on-going problem with text extents. We select either by default, or purposefully, a font for each dialog. This is seen in the dialog editor by right-mousing on the dialog frame, and selecting properties. The default is MS Sans Serif, 8 points. My assumption was that if I get the CClientDC for the ListBox, that the selected font would be represented by the CClientDC. This has always given me unsatisfactory results, and that unsatisfactory code is running in many applications!
While working on this article in 1999, I decided to solve this puzzle. As it turns out, the only way I could change the length returned by
GetTextExtentPoint32() was to change from small to large fonts in the Control Panel. That told me that the font selected at dialog creation time was not ‘selected into’ the CClientDC. I further found that if I inserted a GetFont() call at this point in the code, and then selected the returned CFont pointer into my CClientDC, I magically get the correct value.
[line 20]To continue, then, in line 20 we first get a pointer to a CFont object which contains the font information for our ListBox display. This CFont() object is selected into our CClientDC, and the return value is the prvious CFont() object which is saved as pOldFont.
[line 22-24]Next, I use the GetTextExtendPoint32 function to fill in the SIZE structure declared above. I am interested in the cx member, which is the width of the ListBox entry. The current maximum width is compared to the line width, and adjusted appropriately (line 24).
[line 26]All that remains now is to tell our scrollbar how big to display itself. This is done by calling SetHorizontalExtent(). As it turns out, if the width sent to this function is the same or less than the width of the ListBox itself, the Horizontal ScrollBar will be turned off. This is the action we were wanting. One more piece of magic needs to be done, however. If you examine the data in a CListBox very closely, you’ll notice a ‘gutter’ to the left of the displayed text. This gutter is 3 pixels. To get our extent to work correctly, those 3 pixels need to be added to the max width. If you would also like a 3 pixel gutter to the right, add 3 more.
[line 28]Before we exit, we need to clean up the CClientDC by selecting our font back out. This is done by selecting the old one back in (line 28).
Compiling and executing this code should display the required display of scrollbars in our CListBox.
Enhancements
Some enhancements that I have found useful are in addition to handling
AddString(), to also handle
InsertString() and
DeleteString(). The sample code shows radio buttons and a check box on the main dialog display. To get the interaction correct between the controls and the CListBox, I had to handle the
RemoveAll() method.
