In Developer Studio, Class Wizard has some features that can be used to add member variables and message handlers for the common controls. This simplifies the procedure of writing source code.
5.1 Spin Control
Spin control is a rectangular button with two arrows pointing to opposite directions (either vertically or horizontally), it is one of the most commonly used controls in a dialog box. Usually a spin is used together with another control, in most cases this control is an edit box (Though not common, this control can also be a button or a static control). By clicking on one of the arrows, the contents in the accompanying control will change accordingly indicating current position of the spin control.
The control used together with the spin control is called spin's Buddy Control. In MFC, it is very easy to use spin control along with edit box, they are specially designed to cooperate together.
Using Spin Control with Edit Box
By default, a spin control should be associated with an edit box. Usually this type of edit box contains a number indicating the current position of spin. If we make no modification, the range of this number will be from 0 to 100. If the spin's orientation is vertical, pressing the downward arrow will cause the number to increment. If the spin's orientation is horizontal, pressing the leftward arrow will have the same effect.
When adding a spin control resource, we must set several styles in order to make it work correctly. In the property page whose caption is "Spin properties", by clicking "Styles" tab, we will see all the customizable styles (Figure 5-1). Here, style "Auto buddy" will allow spin's buddy to be automatically selected (By enabling this style, we do not need to set spin's buddy within the program). If we check this selection, the window prior to the spin control in the Z order will be used as the spin's buddy window.
(Figure 5-1 omitted)
To let buddy window be automatically selected, we must first add resource for the buddy control then resource for the spin control. For example, if we want to use an edit box together with a spin control, we can add edit box resource first, then add spin control next. We can check controls' Z order by executing command Layout | Tab order (or pressing CTRL+D keys). The Z-order of the controls can be reordered by clicking them one by one according to the new sequence.
In the left-bottom corner of "Spin properties" property sheet, there is a combo box labeled "Alignment". This allows us to specify how the spin will be attached to its buddy window when being displayed. If we select "Unattatched" style, the spin and the buddy control will be separated. Usually we select either "Left" or "Right" style to attach the spin to the left or right side of the buddy control. In this case, the size and position of the spin control in the dialog template has no effect on its real size and position in the runtime, its layout will be decided according to the size and position of the buddy control.
Also, there is a "Set buddy integer" check box. If this style is set, the spin will automatically send out message to its buddy control (must be an edit box) and cause it to display a series of integers when the spin's position is changed. By default, the integer contained in the edit box will increment or decrement with a step of 1. If we want to customize this (For example, if we want to change the step or want to display floating point numbers), we should uncheck this style and set the buddy's text within the program.
Sample 5.1-1\CCtl demonstrates how to use spin control with edit control and set buddy automatically. The sample is a standard dialog based application generated by Application Wizard, with all default settings. The resource ID of the main dialog template is IDD_CCTL_DIALOG, which contains two spin controls and two edit boxes. Both spins have "Auto buddy" and "Set buddy integer" styles. Also, their alignment styles are set to "Right" (Figure 5-2).
(Figure 5-2 omitted)
Without adding a single line of code, we can compile the project and execute it. The spin controls and the edit controls will work together to let us select integers (Figure 5-3).
(Figure 5-3 omitted)
In MFC, spin control is implemented by class CSpinButtonCtrl. We need to call various member functions of this class in order to customize the properties of the spin control. In sample 5.1-2\CCtl, the control's buddy is set by calling function CSpinButtonCtrl::SetBuddy(...) instead of using automatic method. The best place to set a spin's buddy is in the dialog box's initialization stage. This corresponds to calling function CDialog::OnInitDialog().
Sample 5.1-2\CCtl is based on sample 5.1-1\CCtl. Here, style "Auto buddy" is removed for two spin controls. Also, some changes are nade to set the spin buddies manually.
There are two ways of accessing a specific spin: we can use a spin's ID to call function CWnd::GetDlgItem(...), which will return CWnd type pointer to the spin control; or we can add a CSpinButtonCtrl type variable for the spin control (through using Class Wizard). The following code fragment shows how the buddy of the two spin controls are set using the first method:
(Code omitted)
Since CWnd::GetDigItem(...) returns a CWnd type pointer, we need to first cast it to CSpinButtonCtrl type pointer in order to call any member function of class CSpinButtonCtrl. The only parameter that needs to be passed to function CSpinButtonCtrl::SetBuddy(...) is a CWnd type pointer to the buddy control, which can also be obtained by calling function CWnd::GetDlgItem(...).
Spin controls implemented in sample 5.1-2\CCtl behaves exactly the same with those implemented in sample 5.1-1\CCtl.
5.2 Customizing the Properties of Spin Control
We can customize a spin control's properties in function CDialog::OnInitDialog(). The following three functions are the most commonly used ones for doing customization:
(Table omitted)
Sample 5.2\CCtl is based on sample 5.1-1\CCtl. In this sample, the vertical spin is customized to display hexadecimal integers, whose range is set from 0x0 to 0xC8 (0 to 200), and its initial position is set to 0x64 (100). The horizontal spin still displays decimal integers, its range is from 50 to 0, and the initial position is 25. The following portion of function CCCtlDlg::OnInitDialog() shows the newly added code:
(Code omitted)
5.3 Displaying Text Strings in the Buddy Window
Sometimes we want the buddy to display text strings rather than numerical numbers. For example, we may prefer the text displayed in the buddy window to be "One", "Two", "Three"... rather than "1", "2", "3".... To customize this style, we could not use "Set buddy integer" style anymore. Instead, we need to write our own message handlers and set the buddy control's text by ourselves.
When the position of a spin has changed, the parent window of the spin control will receive a UDN_DELTAPOS message. From this message, we can get the current position of the spin control, along with the proposed change to the current position. Based on this information, we can decide what we should display in the buddy control window.
Sample 5.3\CCtl demonstrates how to display text strings in a buddy window. It is based on sample 5.2\CCtl, with a new spin control IDC_SPIN_STR and an edit box IDC_EDIT_STR added to the application. The edit control will display text strings "Zero", "One", "Two",..., "Nine" instead of integers. The buddy of spin IDC_SPIN_STR is set automatically.
The UDN_DELTAPOS message handler can be added through following steps: 1) Invoke Class Wizard, click "Messages Maps" tab. 2) Select "CCCtlDlg" class from "Class name" window, then highlight "IDC_SPIN_STR" in "Object IDs" window. 3) There will be two messages contained in "Messages" window, we need to highlight "UDN_DELTAPOS" and press "Add function" button. The newly added function will look like follows:
void CCCtlDlg::OnDeltaposSpinStr(NMHDR* pNMHDR, LRESULT* pResult)
{
NM_UPDOWN* pNMUpDown = (NM_UPDOWN*)pNMHDR;
*pResult = 0;
}
The first parameter here is a NMHDR type pointer. This is a structure that contains Windows( notification messages. A notification message is sent to the parent window of a common control to notify the changes on that control. It is used to handle events such as mouse left button clicking, left button double clicking, mouse right button clicking, and right button double clicking performed on a common control. Many types of common controls use this message to notify the parent window. For spin control, after receiving this message, we need to cast the pointer type from NMHDR to NM_UPDOWN. Here structure MN_UPDOWN is defined as follows:
typedef struct _NM_UPDOWN { nmud
NMHDR hdr; // notification message header
int iPos; // current position
int iDelta; // proposed change in position
} NM_UPDOWNW;
In the structure, member iPos specifies the current position of the spin control, and iDelta indicates the proposed change on spin's position. We can calculate the new position of the spin control by adding up these two members.
The following function shows how the buddy's text is set after receiving the message:
(Code omitted)
The buddy's text is set by calling function CWnd::SetWindowText(...). Here variable szNumber is a two- dimensional character array which stores strings "Zero", "One", "Two"..."Nine". First we calculate the current position of the spin control and store the result in an integer type variable nNewPos. Then we use it as an index to table szNumber, find the appropriate string, and use it to set the text of the edit control.
In dialog box's initialization stage, we need to set the range and position of the spin control. Since the edit box will display nothing by default, we also need to set its initial text:
(Code omitted)
With the above implementation, the spin's buddy control will display text instead of numbers.
5.4 Bitmap Button Buddy
String text is not the only appearance a buddy control can have. We can also implement a buddy that displays bitmaps. Because bitmap button can be easily implemented to display images, we can use it to implement spin's buddy control rather than using edit box.
Sample 5.4\CCtl demonstrates how to implement bitmap button buddy. It is based on sample 5.3\CCtl, with a new spin control IDC_SPIN_BMP and a new bitmap button IDC_BUTTON_BMP added to the application.
The procedure of creating a bitmap button buddy is almost the same with creating an edit box buddy. The only difference here is that instead of creating an edit box resource, we need to add a button resource, and set its "Owner draw" style.
In the sample, four bitmaps are prepared to implement the bitmap button. All of them have integer resource IDs, which are listed as follows: IDB_BITMAP_SMILE_1, IDB_BITMAP_SMILE_2, IDB_BITMAP_SMILE_3, IDB_BITMAP_SMILE_4.
A CBitmapButton type variable is declared in class CCCtlDlg. In the dialog box's initialization stage, functions CWnd::SubclassDlgItem(...), CBitmapButton::LoadBitmaps(...) and CBitmapButton:: SizeToContent() are called to initialize the bitmap button. Also, the range of the spin control is set from 0 to 3, and its initial position is set to 0:
(Code omitted)
The initially selected bitmap is IDC_BITMAP_SMILE_1. We should not load bitmaps for other states ("down", "focused" and "disabled") because the purpose of this button is to display images rather than executing commands. We need to change the currently loaded image upon receiving UDN_DELTAPOS notification. To change button's associated bitmap, in the sample application, a UDN_DELTAPOS message handler is added for IDC_SPIN_BMP, which is implemented as follows:
(Code omitted)
Although we say that the bitmap button is the buddy of the spin control, in the above implementation we see that they do not have special relationship. A spin control needs a buddy only in the case when we want the text in the buddy window to be updated automatically. If we implement this in UDN_DELTAPOS message handler, the buddy loses its meaning because we can actually set text for any control. Although this is true, here we still treat the bitmap button as the buddy of spin control because the bitmap button is under the control of the spin.
5.5 Slider
A slider is a control that allows the user to select a value from pre-defined range using mouse or keyboard. A slider can be customized to have many different styles: we can put tick marks on it, set its starting and ending ranges, make the tick marks distributed linearly or non-linearly. Besides these attributes, we can also set the line size and page size of a slider, which decide the minimum distance the slider moves when the user clicks mouse on slider's rail or hit arrow keys of the keyboard.
Including Slider Control in the Application
Sample 5.5\CCtl demonstrates how to use the slider control. It is a standard dialog based application generated by the Application Wizard. In the dialog box, three different sliders are implemented, they are used to show how to set tick marks, page size, line size, and implement other customizations.
The tick marks can be added to the slider automatically. When we add a slider resource to the dialog template, we can find two check boxes, "Tick mark" and "Auto ticks", in the property page that lets us customize slider's properties (Figure 5-4). If we check the former check box, the slider will be able to have tick marks, if we check the latter, tick marks will be added automatically.
(Figure 5-4 omitted)
In MFC, slider can be implemented through using class CSliderCtrl. We need to call its member functions in order to customize the slider.
To let the tick marks be set automatically, besides setting "Tick mark" and "Auto ticks" styles, we must also specify slider's range. A slider's range can be set by calling either function CSliderCtrl:: SetRange(...) alone or CSliderCtrl::SetRangeMin(...) together with CSliderCtrl::SetRangeMax(...)in the dialog box's initialization stage. The format of the above three functions are listed as follows:
void CSliderCtrl::SetRange(int nMin, int nMax, BOOL bRedraw = FALSE);
void CSliderCtrl::SetRangeMax (int nMax, BOOL bRedraw = FALSE);
void CSliderCtrl::SetRangeMin(int nMin, BOOL bRedraw = FALSE);
By default, the distance between two neighboring tick marks is 1. To change this, we may call function CSliderCtrl::SetTicFreq(...) to set the frequency of the tic marks. If the slider does not have "Auto ticks" style, we must call function CSliderCtrl::SetTic(...) to set tick marks for the slider. Because this function allows us to specify the position of a tic mark, we can use it to set non-linearly distributed tic marks.
Two other properties that can be modified are slider's page size and line size. Here, page size represents the distance the slider will move after the user clicks mouse on its rail. The line size represents the distance the slider will move if the user hits left arrow or right arrow key when the slider has the current focus (Figure 5-5). Two member functions of class CSliderCtrl can be used to set the above two sizes: CSliderCtrl::SetPageSize(...) and CSliderCtrl::SetLineSize(...). The default page size is 1/5 of the total slider range and the default line size is 1.
In sample 5.5\CCtl, there are three sliders, whose IDs are IDC_SLIDER_AUTOTICK, IDC_SLIDER_TICK and IDC_SLIDER_SEL respectively. Here, slider IDC_SLIDER_AUTOTICK has "Tick marks" and "Auto ticks" styles, slider IDC_SLIDER_TICK has only one "Tick marks" style, and slider IDC_SLIDER_SEL has "Tick marks", "Auto ticks", and "Enable selection" styles.
Three sliders are initialized in function CCCtlDlg::OnInitDialog(). The following portion of this function sets the range, tick marks, page size and line size for each slider:
(Code omitted)
Since slider IDC_SLIDER_AUTOTICK has "Auto ticks" style, we don't need to set the tick marks. In the sample, the range of this slider is set from 0 to 10, and the tick mark frequency is set to 2. The tick marks will appear at 0, 2, 4, 6... 10. Also, since no page size and line size are specified here, they will be set to the default values (2 and 1). For IDC_SLIDER_TICK, its range is set from 0 to 50, and function CSliderCtrl::SetTic(...) is called to set non-linearly distributed tick marks. Here, a loop is used to set all the tick marks, which will appear at 0, 4, 8, 16.... Slider IDC_SLIDER_SEL also has "Auto ticks" style, its range is set from 50 to 100, page size set to 40 and line size set to 10. This slider also has "Enable selection" style, and function CSliderCtrl::SetSelection(...) is called to draw a selection on the slider's rail. The range of the selection is from 60 to 90.
Handling Slider Related Messages
Another feature we want to add to the application is trapping events generated by the sliders. When a slider moves, we may want to know its current position and make changes to other settings. For example, in a multimedia application, we can use slider to control the volume of speakers.
In Windows( system, there is no special message defined for the slider. Instead, a slider shares same messages with scroll bar. For horizontal sliders, we need to trap WM_HSCROLL message; for vertical sliders, we need to trap WM_VSCROLL message.
Similar to UDN_DELTAPOS message, in a dialog box, message WM_HSCROLL or WM_VSCROLL can be added through using Class Wizard. The default message handler for WM_HSCROLL will look like this:
void CCCtlDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
CDialog::OnHScroll(nSBCode, nPos, pScrollBar);
}
There are three parameters in this function. The first parameter nSBCode indicates user's scrolling request, which includes left-scroll, right-scroll, left-page-scroll, right-page-scroll, etc. If we want to customize the behavior of a slider, we need to check this parameter. Parameter nPos is used to specify the current slider's position under some situations (it is not valid all the time). The third parameter pScrollBar is a window pointer to slider or scroll bar control. We can use it to check which slider is being changed, then make the corresponding response. The slider's current position can be obtained by calling function CSliderCtrl::GetPos(). In the sample application, since all sliders are horizontal, only WM_HSCROLL message is handled:
(Code omitted)
Function CWnd::GetDlgCtrlID() is called to retrieve the control ID of the slider. We use this ID to call function CWnd::GetDlgItem(...) and get the address of slider control. If the control happens to be one of our sliders, we call function CSliderCtrl::GetPos() to retrieve its current position, and output the slider ID along with its current position to the debug window. In order to see the activities of the sliders, the application must be executed in the debug mode within Developer Studio.
5.6 List Box
List box is a control that contains a list of objects such as file names and strings that can be selected by mouse clicking. When we create a list box, there are many styles that can be customized (Figure 5-6). For example, if we check "Horizontal scroll" style, the list box will automatically implement a horizontal scroll bar if any of its string is too long to be fully displayed in the list window. If we check "Vertical scroll" style, the list box will add a vertical scroll if the vertical size of the list box is not big enough for displaying all items. Some other important styles that a list box can have are "Multi-column", "Sort", "Selection" and "Owner draw".
(Figure 5-6 omitted)
Usually a list box has a single column. If we set "Multi-column" style, the list box can have multiple horizontal columns. Originally the list box will be empty, when we start to add new items, they will be added to the first column (column 0). If the first column is full, instead of creating a vertical scroll bar and continue to add items to this column, the list box will create a new column and begin to fill it. This step will be repeated until all items are filled into the list box. Here, the width of each column is always the same. Because a multiple-column list box will always try to extend horizontally rather than vertically, it is important to let this type of list box have a horizontal scroll bar.
The "Sort" style will be set by default. If we remain this style, all the strings contained in the list box will be alphabetically sorted. For the "Selection" styles, we have several choices. A "Single" style list box allows only one item in the list box to be selected at any time. A "Multiple" style list box allows several items to be selected at the same time. If we enable this style, the user can use SHIFT and CTRL keys together to select several items. Besides these two, there is also an "Extended" style. If we enable it, the items can be selected or deselected through mouse dragging. Finally, list box with "Owner draw" style allows us to implement it so that the list box can contain non-string items. In this case, we need to provide custom list box interface.
Sample 5.6\CCtl demonstrates basic styles of list box. It is a dialog-based application created by Application Wizard. There are three list boxes implemented in the application, whose IDs are IDC_LIST, IDC_LIST_MULCOL and IDC_LIST_DIR respectively. The styles of IDC_LIST are all set to default, it is a single selection, single column, sorted list box with a vertical scroll bar. The styles of IDC_LIST_MULCOL are multiple-column, multiple-selection, it does not support sort. The styles of IDC_LIST are also set to default, except that it supports "extended selection" style. To access these list boxes, three CListCtrl type member variables m_listBox, m_listMCBox and m_listDir are declared in class CCtlDlg through using Class Wizard (Figure 5-7).
(Figure 5-7 omitted)
Unless we initialize the content of these list boxes, they will be empty at the beginning. Like other common controls, initialization procedure of list box is usually implemented in function CDialog:: OnInitDlalog(). To fill a list box with strings, we need to call function CListBox::AddString(...). Strings will be added starting from item 0, 1, 2... and so on (If a list box has a sorted style, the string will be sorted automatically). Besides this function, we can also use function Clistbox::InsertString(...) to insert a new string before certain item instead of adding it to the end of the list. The following code fragment shows how the content of list boxes IDC_LIST and IDC_MULCOL are filled:
(Code omitted)
If we do not specify the column width for a multiple-column list box, the default column width will be used. We may set this width by calling function CListBox::SetColumnWidth(...). In the sample 5.6\CCtl, the column width of IDC_LIST_MULCOL is set 50 (pixels) as follows:
BOOL CCtlDlg::OnInitDialog()
{
......
m_listMCBox.AddString("Item 20");
m_listMCBox.SetColumnWidth(50);
......
}
All the columns will have the same width. For list box IDC_LIST_DIR, instead of filling each entry with a string, we can let it display a list of directories and file names for the current working directory. This can be implemented by calling function CListBox::Dir(...), which has the following format:
int CListBox::Dir(UINT attr, LPCTSTR lpszWildCard);
Here, the first parameter specifies file attributes, which can be used to specify what type of files can be added to the list. The following is a list of some attributes that are commonly used:
(Table omitted)
The second parameter is a string, which can be used to set file filter. In the sample, this function is called as follows:
BOOL CCtlDlg::OnInitDialog()
{
......
m_listDir.Dir(0x10, "*.*");
return TRUE;
}
Here, value 0x10 is passed to the first parameter of function CListBox::Dir(...) to let normal files along with directories be listed, also, we use "*.*" wildcards to allow all types of names to be added to the list.
When testing the sample application, we can use mouse along with SHIFT and CONTROL keys to select items. Also, we can drag mouse over items to test extended selection style.
5.7 Handling List Box Messages
Like other common controls, list box has its own messages that are related to mouse or keyboard activities.
Trapping Double Clicking Message
In the previous sample, it may be helpful to trap mouse double clicking message for list box IDC_LIST_DIR. When the user double clicks a directory, we can change the contents of the list box, fill it with the file and directory names contained under the directory being clicked. This feature is implemented in sample 5.7\CCtl, which is based on sample 5.6\CCtl.
The double clicking message of a list box is LBN_DBLCLK. We can easily add handler for this message through using Class Wizard: after invoking the Class Wizard, by clicking "Message maps" tab and selecting class "CCtlDlg", three IDs of the list boxes will be listed in "Object IDs" window. We can highlight "IDC_LIST_DIR", then select "LBN_DBCLK" message from "Messages" window, and press button "Add function". If we accept the default function name, a new function CtlDlg::OnDblclkListDir() will be added to the application.
Retrieving the Contents of an Item
After receiving the double clicking message, we need to obtain the string contained in the item that was clicked. For a single selection list box, the current selected item can be retrieved by calling function CListBox::GetCurSel() (After being double clicked, the item must become currently selected). This function will return a zero based index indicating which item is currently being selected. Then we can call function CListBox::GetText(...) to retrieve the string contained in the item. Since list IDC_LIST_DIR is a multiple-selection list box, retrieving text from the selected items is a little different. In this case, first function CListBox::GetSelCount() must be called to retrieve the number of items that are currently being selected. According to this value, we must allocate enough buffers for storing indices of the selected items. Then we can call function CListBox::GetSelItems(...) and pass the buffer's address to it for receiving indices of all the selected items. For each index, we can call CListBox::GetText(...) to retrieve its string.
After a double clicking, one and only one item in the list box will be selected. In this case, we can skip the first step because CListBox::GetSelCount() will surely return 1. If a list item represents directory, a pair of square brackets "[]" will be added to the directory name. So we can judge if the item being double clicked contains a file name or a directory name by examining if the string starts and ends with square brackets. If we allow drive names to be displayed, the items containing drive names will be displayed in the format of "[-X-]", where X represents the drive name. In our samples, this situation is not considered).
The following is the implementation of function CCtlDlg::OnDblclkListDir(). This function examines the clicked item. If the item contains a directory name, we need to update the contents of the list box (File and directory names under the directory being clicked will be retrieved and filled into the list box):
(Code omitted)
First function CListCtrl::GetSelItems(...) is called to retrieve the currently selected item. The result is stored to a local variable nIndex. Then text string of the selected item is obtained by calling function CListCtrl::GetText(...). If the string starts with "[", it is a directory, and we extract the directory name by calling function CString::Mid(...). Then contents of the list box are cleared by calling function CListCtrl::ResetContent(). Next, the current working directory is changed by calling function _chdir(...). Finally, the list box is filled with the new directory and file names by calling function CListCtrl::Dir(...). Since function _chdir(...) is not an MFC function, we need to include "direct.h" header file in order to use it.
Message WM_DESTROY
Besides directory changing, another new feature is also implemented in the sample application: we can use mouse to highlight any item contained in the other two list boxes. When the dialog box is closed, a message box will pop up displaying all the items that are currently being selected.
Before a window is destroyed, it will receive a WM_DESTROY message, so we can handle this message to do clean up work. In our case, this is the best place to retrieve the final state of the list boxes. Please note that we can not do this in the destructor of class CCtlDlg, because at that time the dialog box window and all its child window have already been destroyed. If we try to access them, it will cause the application to malfunction.
Message handler WM_DESTROY can be added by using Class Wizard through following steps: 1) Click "Message maps" tab and choose "CCtlDlg" class in window "Class name". 2) Highlight "CCtlDlg" in window "Object IDs". 3) In "Messages" window, find "WM_DESTROY" message and click "Add function" button. After the above steps, a new function CCtlDlg::OnDestroy() will be added to the application.
We will retrieve all the text strings of the selected items for three list boxes and display them in a message box. For list box IDC_LIST_BOX this is easy, because it allows only single selection. We can call function CListBox::GetCurSel() to obtain the index of the selected item and call CListBox::GetText(...) to retrieve the text:
(Code omitted)
Function CListBox::GetCurSel() will return value LB_ERR if nothing is being currently selected or the list box has a multiple-selection style. If there is a selected item, we use CString type variable szStrList to retrieve the text of that item.
For list box IDC_LIST_MULCOL and IDC_LIST_DIR, things become a little complicated become both of them allow multiple-selection. We need to first find out how many items are being selected, then allocate enough buffers for storing the indices of the selected items, and use a loop to retrieve the text of each item. Each time a new string is obtained, it is appended to the end of szStrList. The following code fragment shows how the text of all the selected items is retrieved for list box IDC_LIST_MULCOL:
(Code omitted)
In this function, first the number of selected items is retrieved by calling function CListBox::GetSelCount(), and the retrieved value is saved to variable nSelSize. If the size is not zero, we allocate an integer type array with size of nSelSize. Then by calling function CListBox::GetSelItems(...), we fill this buffer with the indices of selected items. Next, a loop is used to retrieve the text of each item. The procedure of retrieving selected text for list box IDC_LIST_DIR is the same.
5.8 Combo Box
Combo box is another type of common control that allows the user to select one object from a list. While a list box allows multiple selections, a combo box allows only single selection at any time. A combo box is made up of two other controls: an edit box and a list box. There are three types of combo boxes: 1) Simple combo box: the list box is placed below the edit box, and is displayed all the time; the edit box displays the currently selected item in the list box. 2) Drop down combo box: the list box is hidden most of the time; when the user clicks the drop-down arrow button located at the right corner of the edit box, the list box is shown and can be used to select an item. In both 1) and 2), the edit box can be used to input a string. 3) Drop list combo box: it is the same with drop down combo box, except that its edit box cannot be used to input string.
Using a combo box is more or less the same with that of a list box. We must first create combo box resources in the dialog template, set appropriate styles, then in the dialog's initialization stage (in function CDialog::OnInitDialog()), initialize the combo box. We can add message handlers to trap mouse or keyboard related events for combo box. Two most important messages for combo boxes are CBN_CLOSEUP and CBN_SELCHANGE. The first message indicates that the user has clicked the drop-down arrow button, made a selection from the list box, and the drop down list is about to be closed. The second message indicates that the user has selected a new item.
In MFC, combo box is supported by class CComboBox. Like CListBox, class CComboBox has a function CComboBox::AddString(...) which can be used to initialize the contents of its list box. Besides this, we can also initialize the contents of a list box when designing dialog template. In the property sheet whose caption is "Combo Box Properties", by clicking "Data" tab, we will have a multiple-line edit box that can be used to input initial data for combo box. We can use CTRL+RETURN keys to begin a new line (Figure 5-8).
(Figure 5-8 omitted)
Class CComboBox has two functions that allow us to change the contents contained in the list box dynamically: CComboBox::InsertString(...) and CComboBox:: DeleteString(...).
When designing drop-down combo box, we must set its vertical size, otherwise it will be set to the default value zero. In this case, there will be no space for the list box to be dropped down when the user clicks drop down button. To set this size, we can click the drop-down button in the dialog template. After doing this, a resizable tracker will appear. The initial size of a combo box can be adjusted by dragging the tracker's border (Figure 5-9).
(Figure 5-9 omitted)
Implementing Combo Boxes
Sample 5.8\CCtl demonstrates the basics of combo box. It is a standard dialog-based application generated by Application Wizard. Three different combo boxes are implemented in the sample, whose IDs are IDC_COMBO_SIMPLE, IDC_COMBO_DROPDOWN and IDC_COMBO_DROPLIST respectively. For these combo boxes, IDC_COMBO_SIMPLE is a "Simple" type, its items are initialized to "Item 1", "Item 2"... "Item 4" when designing the dialog template; IDC_COMBO_DROPDOWN is a "Drop down" type, and no initialization is done in the resource; IDC_COMBO_DROPLIST is a "Drop list" type, its contents are also initialized as "Item 1", "Item 2"... "Item 4" like IDC_COMBO_SIMPLE. All other styles are set as default, this will let all three combo boxes have vertical scroll bars automatically, and their items be sorted alphabetically.
Three static text controls IDC_STATIC_SIMPLE, IDC_STATIC_DROPDOWN and IDC_STATIC_DROPLIST are added below each combo box. We will use them to display the current selection of the corresponding combo box dynamically.
Three CComboBox type member variables, m_cbSimple, m_cbDropDown and m_cbDropList, are declared in class CCCtlDlg. They will be used to access the combo boxes This can be implemented through using Class Wizard as follows: 1) Invoke Class Wizard, click "Member variables" tab, select "CCCtlDlg" from window "Class name". 2) Highlight the ID of the combo box (IDC_COMBO_SIMPLE, IDC_COMBO_DROPDOWN or IDC_COMBO_DROPLIST), press "Add variable" button. 3) Select "Control" category and input the variable name.
In function CCCtlDlg::OnInitDialog(), the contents of combo box IDC_COMBO_DROPDOWN are initialized through calling function CComboBox::AddString(...):
(Code omitted)
Handling Messages CBN_CLOSEUP and CBN_SELCHANGE
We need to implement message handlers for CBN_CLOSEUP or CBN_SELCHANGE in order to respond to mouse's events. For combo box IDC_COMBO_DROPDOWN and IDC_COMBO_DROPLIST, we know that the selection is changed if we receive message CBN_CLOSEUP. For combo box IDC_COMBO_SIMPLE, we need to use CBN_SELCHANGE because the list box will not close after a new selection is made.
Message handlers can be easily added through using Class Wizard as follows: 1) Invoke Class Wizard, click "Message maps" tab and select "CCCtlDlg" from window "Class name". 2) Highlight the appropriate combo box ID in window "Object IDs". 3) In window "Messages", highlight the appropriate message (CBN_CLOSEUP for IDC_COMBO_DROPDOWN and IDC_COMBO_DROPLIST, CBN_SELCHANGE for IDC_COMBO_SIMPLE). 3) Click button "Add function" and confirm the member function name.
These functions will be called when the user makes a new selection from the list box of a combo box. We can retrieve the index of the current selection of a combo box by calling function CComboBox:: GetCurSel(), and further retrieve the text of that item by calling function CComboBox::GetLBText(...). At last we can call function CWnd::SetWindowText(...) to display the updated content of the selected item in one of the static text controls. The implementations of three message handlers are almost the same. The following is one of them:
(Code omitted)
First index of the current selection is retrieved and stored in variable nSel. Then we check if the returned value is CB_ERR. This is possible if there is nothing being currently selected. If the returned value is a valid index, we call function CComboBox::GetLBText(...) to retrieve the text string and store it in CString type variable szStr. Finally function CWnd::GetDlgItem(...) is called to obtain the pointer to the static text window, and CWnd::SetWindowText(...) is called to update its contents.
5.9 Trapping RETURN key strokes for the Combo Box
Problem & Workaround
One feature we may want to add to the combo boxes is to let the user dynamically add new items through using their edit boxes. We can let the user input a string into the edit box of a drop-down or simple combo box, then hit the RETURN key to add the input to list item. However, in a dialog box, the RETURN key (also the ESC key) is used to exit the application by default. Even if we add message handler for combo box to trap RETURN keystrokes, it still can not receive this message because after the message reaches the dialog box, the application will exit. The message has no chance to be further routed to the child windows of a dialog box.
If we want to process RETURN keystroke events, we need to intercept the message before it is processed by the dialog box. In MFC, there is a function CWnd::PreTranslateMessage(...) that can be overridden for this purpose. This function will be called just before a message is about to be processed by the destination window. Since CDialog is derived from CWnd, we can trap any message sent to the dialog box if we override the above function. This function has the following format:
BOOL CWnd::PreTranslateMessage(MSG *pMsg);
Its only parameter is a pointer to MSG type object:
typedef struct tagMSG { // msg
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
From this structure, we know which window is going to receive the message (from member hwnd), what kind of message it is (from member message). Also, we can obtain the message parameters from members wParam and lParam. If the message is not the one we want to intercept, we can just forward the message to its original destination by calling the base class version of this function.
Function CWnd::PreTranslateMessage(...)
Sample 5.9\CCtl demonstrates how to trap RETURN keystrokes for combo box. It is based on sample 5.8\CCtl. First, function PreTranslateMessage(...) is overridden. This function can be added by using Class Wizard through following steps: 1) Open Class Wizard, click "Message Maps" tab, select "CCCtlDlg" from "Class name" window. 2) Highlight "CCCtlDlg" in window "Object IDs". 3) Locate and highlight "PreTranslateMessage" in window "Messages". 4) Press "Add function" button.
The default member function looks like the following:
BOOL CCCtlDlg::PreTranslateMessage(MSG *pMsg)
{
return CDialog::PreTranslateMessage(pMsg);
}
If we do not want to process the message, we need to call function CDialog:: PreTranslateMessage(...) to let the dialog box process it as usual. Otherwise we need to return a TRUE value to give the operating system an impression that the message has been processed properly.
In the overridden function, first we need to check if the message is WM_KEYDOWN and the key being pressed is RETURN:
(Code omitted)
Message WM_KEYDOWN is a standard Windows( message for non-system key strokes, and VK_RETURN is a standard virtue key code defined for RETUN key (For a list of virtual key codes, see appendix A). Some local variables are declared at the beginning. They will be used throughout this function.
Accessing the Edit Box of a Combo Box
We need to find out which combo box has the current focus in order to decide if we should process this message. If the item that has the current focus is either IDC_COMBO_DROPDOWN or IDC_COMBO_SIMPLE, we will update the corresponding list items.
In Windows( operating system, windows are managed through using handles. Like menu and bitmap resources, a window handle is also a number which could be used to identify a window. Each window's handle has a different value. As a programmer, we do not need to know the exact value of the handle, however, we can use handle to access or identify a window.
In MFC, there is a function CWnd::GetFocus(), which can be used to obtain a pointer to the child window that has the current focus. From this pointer, we can obtain that window's handle. Then we can compare the handle obtained from function CWnd::GetFocus() with the handles of combo boxes. If there is a hit, we could update the content of that combo box.
Unfortunately, since a combo box is made up of two controls: an edit box and a list box, if we are trying to input characters into the combo box, it is the edit box that has the current focus. Thus if we call CWnd::GetFocus() to obtain handle of the window that has the current focus, we will actually get the handle of the edit box. The edit box is the child window of the combo box window, and it has a different handle with its parent. So comparing the handle of the edit box with the handles of the combo boxes will never result in any hit. The correct step would be: for each combo box, obtaining the handle of its edit box, then comparing it with the handle of the focused window. This will eventually result in a hit.
Class CWnd has a member function that can be used to find a window's child windows:
CWnd *CWnd::GetWindow(UINT nCmd);
Here nCmd specifies what kind of window is being looked for. To enumerate all the child windows, we need to call this function using GW_CHILD flag to find the first child window, then, use GW_HWNDNEXT to call the same function repeatedly until it returns a NULL value. This will enumerate all the sibling windows of the first child window.
There are still problems here: function CWnd::GetWindow(...) returns a CWnd type pointer, we can not obtain further information about that window (i.e. is it an edit box or a list box?). Since a combo box has two child windows, although we can access both of them with the above-mentioned method, we do not know which one is the edit box.
In Windows(, before a new type of window is created, it must register a special class name to the system. Every window has its own class name, which could be used to tell the window's type. In the case of combo box, its edit box's class name is "Edit" and its list box's class name is "ComboLBox". Please note that this class name has nothing to do with MFC classes. It is used by the operating system to identify the window types rather than a programming implementation.
In MFC, the procedure of creating windows is handled automatically, so we never bother to register class names for the windows being created, therefore, we seldom need to know the class names of our windows.
A window's class name can be retrieved from its handle by calling an API function:
int ::GetClassName(HWND hWnd, LPTSTR lpClassName, int nMaxCount);
The first parameter hWnd is the handle of window whose class name is being retrieved; the second parameter lpClassName is the pointer to a buffer where the class name string can be put; the third parameter nMaxCount specifies the length of this buffer.
We can access the first child window of the combo box, see if its class name is "Edit". If not, the other child window must be the edit box. This is because a combo box has only two child windows.
A window's handle can be obtained by calling function CWnd::GetSafeHwnd(). If the window that has the current focus is the edit box of a combo box when RETURN is pressed, we need to notify the parent window about this event. In the sample, a user defined message is used to implement this notification:
#define WM_COMBO_RETURN WM_USER+1000
The following portion of function CCCtlDlg::PreTranslateMessage(...) shows how to retrieve the handles of the edit boxes and compare them with the handle of the focused window:
(Code omitted)
First the handle of currently focused window is stored in variable hwndFocus. If it is a valid window handle, we use m_cbDropDown to get the first child window of IDC_COMBO_DROPDOWN. Then this child window's class name is retrieved by calling function ::GetClassName(...). If the class name is "Edit", we compare its handle with the focused window handle. Otherwise we need to get the handle of the other child window before doing the comparison. This will assure that the handle being compared is the handle of the edit box. If the edit box has the current focus, we post the user defined message WM_COMBO_RETURN, whose WPARAM parameter is assigned the ID of combo box. Finally a TRUE value is returned to prevent the dialog box from further processing this message.
Message WM_COMBO_RETURN is processed in class CCCtlDlg. The member function used to trap this message is CCCtlDlg::OnComboReturn(...). The following code fragment shows how this function is declared and message mapping is implemented:
Function declaration:
class CCCtlDlg : public CDialog
{
......
protected:
......
afx_msg LONG OnComboReturn(UINT, LONG);
DECLARE_MESSAGE_MAP()
};
Message mapping macros:
BEGIN_MESSAGE_MAP(CCCtlDlg, CDialog)
......
ON_MESSAGE(WM_COMBO_RETURN, OnComboReturn)
END_MESSAGE_MAP()
Function implementation:
(Code omitted)
In this message handler, we first obtain a pointer to the combo box using the ID passed through WPARAP message parameter. Then we use above-mentioned method to get the pointer to the edit box (a child window of combo box), and assign it to variable ptrEdit. Then we use this pointer to call function CWnd:: GetWindowText(...) to retrieve the text contained in the edit box window. If the edit box is not empty (this is checked by calling function CString::IsEmpty()), we select all the text in the edit box by calling function CEdit::SetSel(...), which has the following format:
void CEdit::SetSel(int nStartChar, int nEndChar, BOOL bNoScroll = FALSE);
The first two parameters of this function allow us to specify a range indicating which characters are to be selected. If we pass 0 to nStartChar and -1 to nEndChar, all the characters in the edit box will be selected. Then we use a loop to check if the text contained in the edit box is identical to any item string in the list box. In case there is no hit, we will add this string to the list box by calling function CComboBox::AddString(...). Finally, a TRUE value is returned before this function exits.
Using this method, we can also trap other keystrokes such as DELETE, ESC to the combo box. This will make the application easier to use.
5.10 Implementing Subclass for the Edit Box of a Combo Box
Under certain conditions we may want to put restrictions on the contents of the list items. For example, sometimes we may want the combo box to hold only numerical characters ('0'-'9'), and sometimes we may expect it to hold only alphabetical characters ('a'-'z', 'A'-'Z'). In these cases, we may want to customize the properties of the edit box so that only a special set of characters can be accepted. If we are creating an edit box resource in dialog template, this can be easily achieved by setting its customizable styles. But for the edit box of a combo box, we can not customize its styles before it is created, so the edit box contained in a combo box will have only the default styles.
To customize the behavior of the edit box in a combo box, we need to use "subclass" technique. We can design our own class to intercept and process the messages sent to the edit box. Sample 5.10\CCtl demonstrates how to customize the edit box that belongs to a combo box. It is based on sample 5.9\CCtl, with two combo boxes customized as follows: combo box IDC_COMBO_SIMPLE allows only numerical characters to be input into the edit box; combo box IDC_COMBO_DROPDOWN accepts only alphabetic characters.
Designing New Classes
Before implementing subclass, we need to design two classes that have the above-mentioned new properties. In the sample, MCNumEdit and MCCharEdit are added for this purpose. Both of them are derived from class CEdit. In Developer Studio, a new class can be easily added by using Class Wizard through following steps: 1) Execute command Insert | New Class to invoke the Class Wizard. 2) Input the class name, select the header file and implemantation file name. 3) Select base class name.
To customize the input attributes of an edit box, we need to handle WM_CHAR message, which is used to indicate that a character is being input into the control. This message handler can also be added through using Class Wizard after it is invoked as follows: 1) Click "Message Map" tab, select "MCNumEdit" or "MCCharEdit" class name in window "Class name". 3) Highlight "MCNumEdit" or "MCCharEdit" in window "Object IDs". 4) Locate and highlight "WM_CHAR" in window "Messages". 5) Click "Add function" button.
The following is one of the two functions generated by Class Wizard:
void MCNumEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags);
}
This function has three parameters. The first parameter nChar indicates the value of the key, which provides us with the information of which key being pressed. The Second parameter indicates the repeat count, and the third parameter holds extra information about the keystrokes.
If we want the keystroke to be processed normally, we need to call the base class version of this function. If we do not call this function, the input will have no effect on the edit box. The following code fragment shows two message handlers implemented in the sample:
(Code omitted)
Class MCNumEdit accepts characters '0'-'9' and backspace key, class MCCharEdit accepts characters 'A'-'Z', 'a'-'z' and backspace key.
Implementing Subclass
To use the two classes, we need to include their header files and use them to declare two new variables in class CCCtlDlg:
......
#include "CharEdit.h"
#include "NumEdit.h"
......
class CCCtlDlg : public CDialog
{
......
protected:
......
MCCharEdit m_editChar;
MCNumEdit m_editNum;
......
};
In the dialog box's initialization stage, we need to implement subclass and change the default behavior of the edit boxes. Remember in the previous chapter, function CWnd::SubclassDlgItem(...) is used to implement subclass for an item contained in a dialog box. Although the edit box within a combo box is a indirect child window of the dialog box, it is not created from dialog template. So here we must call function CWnd::SubclassWindow(...) to implement subclass. The following is the format of this function:
BOOL CWnd::SubclassWindow(HWND hWnd);
Here, parameter hWnd is the handle of the window whose behavior is to be customized. From sample 5.9\CCtl, we know how to obtain the handle of the edit box that belongs to a combo box. The following is the procedure of implementing subclass for IDC_COMBO_DROPDOWN combo box:
(Code omitted)
With the above implementation, the combo box is able to filter out the characters we do not want.
5.11 Owner Draw List Box and Combo Box
Like menu, list box and combo box do not have to bear plain text interface all the time. Sometimes we can customize them to display images. In the previous samples, when implementing a list box or a combo box, we always select "No" for the "Owner draw" style. Actually, the "Owner draw" style can be set to other two selections: "Fixed" and "Variable". For a "fixed" type owner-draw list box or combo box, each item contained in the list box must have a same height. For a "variable" type of owner draw list box or combo box, this height can be variable. Like the menu, the owner-draw list box or combo box are drawn by their owner. The owner will receive message WM_MEASUREITEM and WM_DRAWITEM when the list box or the combo box needs to be updated. For "fixed" type owner draw list box or combo box, WM_MEASUREITEM is sent when it is first created and the returned size will be used for all items. For "variable" type owner-draw list box or combo box, this message is sent for each item separately. Message WM_DRAWITEM will be sent when the interface of list box or combo box needs to be updated.
Owner-Draw Styles
Sample 5.11\CCtl demonstrates owner-draw list box and combo box. It is a dialog based application generated by Application Wizard. There are only two common controls contained in the dialog box: a list box IDC_LIST and a combo box IDC_COMBO. The list box supports "Fixed" owner-draw style, and the combo box supports "Variable" owner-draw style. The "Sort" style is not applicable to an owner-draw list box or combo-box, because their items will not contain characters.
Preparing Bitmaps
Six bitmap resources are added to the application for list box and combo box drawing. Among them, IDB_BITMAP_SMILE_1, IDB_BITMAP_SMILE_2, IDB_BITMAP_SMILE_3 and IDB_BITMAP_SMILE_4 have the same dimension, they will be used for implementing owner-draw list box. Bitmaps IDB_BITMAP_BUTTON_SEL and IDB_BITMAP_BUTTON_UNSEL have a different size with the above four bitmaps, they will be used together with IDB_BITMAP_BIG_SMILE_1 and IDB_BITMAP_BIG_SMILE_2 to implement owner-draw combo box.
Identifying Item Types
The following macros are defined for different item types:
#define COMBO_BUTTON 0
#define COMBO_BIGSMILE 1
#define LIST_SMILE_1 0
#define LIST_SMILE_2 1
#define LIST_SMILE_3 2
#define LIST_SMILE_4 3
Each macro represents a different bitmap. We will use these macros to set item data for list box and combo box. Since the item data will be sent along with message WM_DRAWITEM, we can use it to identify item types. This is the same with owner-draw menu.
Two CComboBox type variables m_cbBmp and m_lbBmp are declared in class CCCtlDlg through using Class Wizard, they will be used to access the list box and the combo box. In function CCCtlDlg::OnInitDialog(), the list box and the combo box are initialized as follows:
(Code omitted)
Instead of adding a real string, we pass predefined integers to function CComboBox::AddString(...) and CListBox::AddString(...) For owner-draw list box and combo box, these integers will not be used as buffer addresses for obtaining strings. Instead, they will be sent along with message WM_MEASUREITEM to inform us the item type.
Handling Message WM_MEASUREITEM
The standard message handlers for WM_MEASUREITEM and WM_DRAWITEM are CWnd::OnMeasureItem(...) and CWnd::OnDrawItem(...) respectively, they can be added through using Class Wizard.
The following is the format of function CWnd::OnMeasureItem(...):
void CWnd::OnMeasureItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct);
This function is called to retrieve the size of item. It has two parameters, the first parameter nIDCtl indicates the ID of control whose item's size is being retrieved. The second parameter is a pointer to a DRAWITEMSTRUCT object, and we will use its itemData member to identify the type of the item. Since the value of this member is set in the dialog's initialization stage by calling function CComboBox::AddString(...), it must be one of our predefined macros (LIST_SMILE_1, LIST_SMILE_2...). In the overridden function, we need to check the value of nIDCtl and lpDrawItemStruct->itemData, load the corresponding bitmap resource into a CBitmap type variable, call function CBitmap::GetBitmap(...) to retrieve the dimension of the bitmap, and use it to set both lpDrawItemStrut->itemWidth and lpDrawItemStrut->itemHeight:
(Code omitted)
Handling Message WM_DRAWITEM
The following is the format of function CWnd::OnDrawItem(...):
void CWnd::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct);
It also has two parameters. Like CWnd::OnMeasureItem(...), the first parameter of this function is the control ID, and the second parameter is a pointer to a DRAWITEMSTRUCT type object. This structure contains all the information we need to draw an item of list box or combo box: the DC handle, the item's state, the item data, the position and size where the drawing should be applied. The following portion of the overridden function shows how to load correct bitmap by examining nIDCtl and lpDrawItemStruct->itemData:
(Code omitted)
Five local variables are declared: bmp is used to load the bitmap; dcMemory is used to create memory DC and implement image copying; ptrBmpOld is used to restore the original state of dcMemory; ptrDC is used to store the target DC pointer, which is obtained from hDC member of structure DRAWITEMSTRUCT; bm is used to store the information (including dimension) of the bitmap; rect is used to store the position and size where the bitmap should be copied.
From the above source code we can see, if the control is IDC_LIST, we load one of the four bitmaps (IDB_BITMAP_SMILE_1, IDB_BITMAP_SMILE_2, IDB_BITMAP_SMILE_3 or IDB_BITMAP_SMILE_4) according to the value of lpDrawItemStruct->itemData. If the control is IDC_COMBO, we load IDB_BITMAP_BUTTON_SEL or IDB_BITMAP_BIG_SMILE_1 if the item is selected; and load IDB_BITMAP_BUTTON_UNSEL or IDB_BITMAP_BIG_SMILE_2 if the item is not selected. Here, ODS_SELECTED bit of member lpDrawItemStruct->itemState is checked to retrieve item's state.
The following portion of function CCCtlDlg::OnDrawItem(...) draws the bitmap:
(Code omitted)
Only after the bitmap is loaded successfully will we draw the list box or combo box item. First function CDC::FromHandle(...) is called to obtain a CDC type pointer from HDC handler. Then we create a memory DC (compatible with target DC) and select bmp into this DC. Next, function CDC::BitBlt(...) is called to copy the bitmap from memory DC to target DC. For list box items, there is no special bitmaps for their selected states. In case if an item is selected, the corresponding normal bitmap will be drawn using DSTINVERT mode. This will cause every pixel of the bitmap to change to its complement color. When we pass DSTINVERT to function CDC::BitBlt(...), its fifth argument can be set to NULL.
5.12 Tree Control
Tree control allows us to organize objects into a tree structure. One good example of this type of applications would be a file manager. A tree control can be implemented in both a view window and a dialog box. To implement tree control in a view, we can implement the view using class CTreeView. To implement tree control in a dialog box, we need to use CTreeCtrl class. In this section we will focus on dialog box implementation of tree control.
Like other common controls, we can add tree control resources to the dialog template when designing application's resource. The tree control will have an ID, which could be used to access the control (by either calling function CWnd::GetDlgItem(...) or adding CTreeCtrl type member variable).
Image List
We can associate a bitmap image with each node contained in the tree control. This will make the tree control more intuitive. For example, in a file manager application, we may want to use different images to represent different file types: folder, executable file, DLL file, etc. Before using the images to implement the tree control, we must first prepare them. For tree control (also list control and tab control), these images must be managed by Image List, which is supported by class CImageList in MFC.
Class CImageList can keep and manage a collection of images with the same size. Each image in the list is assigned a zero-based index. After an image list is created successfully, it can be selected into the tree control. We can associate a node with any image contained in the image list. Here image drawing is handled automatically.
If we provide mask bitmaps, only the unmasked portion of the images will be drawn for representing nodes. A mask bitmap must contain only black and white colors. Besides preparing mask bitmaps by ourselves, we can also generate mask bitmaps from the normal images.
To use class CImageList, first we need to declare a CImageList type variable. If we create an image list dynamically by using "new" operator, we need to release the memory when it is no longer in use. Before adding images to the list, we need to call function CImageList::Create(...) to initialize it. This function has several versions, the following is one of them:
BOOL CImageList::Create(int cx, int cy, UINT nFlags, int nInitial, int nGrow);
Here cx and cy indicate the dimension of all images, nInitial represents the number of initial bitmaps that will be included in the image list, nGrow specifies the number of bitmaps that can be added later. Parameter nFlags indicates bitmap types, it could be ILC_COLOR, ILC_COLOR4, ILC_COLOR8, etc., which specify the bitmap format of the images. For example, ILC_COLOR indicates default bitmap format, ILC_COLOR4 indicates 4-bit DIB format (16-color), ILC_COLOR8 indicates 8-bit DIB format (256-color). We can combine ILC_MASK with any of these bitmap format flags to let the image be drawn with transparency.
The images can be added by calling function CImageList::Add(...). Again, this function has three versions:
int CImageList::Add(CBitmap *pbmImage, CBitmap *pbmMask);
int CImageList::Add(CBitmap *pbmImage, COLORREF crMask);
int CImageList::Add(HICON hIcon);
The image list can be created from either bitmaps or icons. For the first version of this function, the second parameter is a pointer to the mask bitmap that will be used to implement transparent background drawing. The second version allows us to specify a background color that can be used to generate a mask bitmap from the normal image. Here parameter crMask will be used to create the mask bitmap: all pixels in the source bitmap that have the same color with crMask will be masked when the bitmap is being drawn, and their colors will be set to the current background color. We can choose a background color by calling function CImageList::SetBkColor(...).
To use image list with a tree control, we need to call function CTreeCtr::SetImageList(...) to assign it to tree control. Then, when creating a node for the tree control, we can use the bitmap index to associate any node with this image.
Adding Nodes
At the beginning, the tree control does not contain any node. Like other common controls, we can initialize it in function CDialog::OnInitDialog(). To add a node to the tree, we need to call function CTreeCtrl::InsertItem(...).
This function also has several versions. The following is the one that has the simplest format:
int CTreeCtrl::InsertItem(LPTV_INSERTSTRUCT lpInsertStruct);
The only parameter to this function is a TV_INSERTSTRUCT type pointer:
typedef struct _TV_INSERTSTRUCT{
HTREEITEM hParent;
HTREEITEM hInsertAfter;
TV_ITEM item;
} TV_INSERTSTRUCT;
In a tree control, nodes are managed through handles. After a node is created, it will be assigned an HTREEITEM type handle. Each node has a different handle, so we can use the handle to access a specific node. In the above structure, member hParent indicates which node is the parent of the new node. If we assign NULL to this member, the new node will become the root node. Likewise, member hInsertAfter is used to indicate where the new node should be inserted. We can specify a node handle, or we can use predefined parameters TVI_FIRST, TVI_LAST or TVI_SORT to insert the new node after the first node, last node or let the nodes be sorted automatically.
Member item is a TV_ITEM type object, and the structure contains the information of the new node:
typedef struct _TV_ITEM {
UINT mask;
HTREEITEM hItem;
UINT state;
UINT stateMask;
LPSTR pszText;
int cchTextMax;
int iImage;
int iSelectedImage;
int cChildren;
LPARAM lParam;
} TV_ITEM;
In order to add new nodes, we need to understand how to use the following four members of this structure: mask, pszText, iImage and iSelectedImage.
Member mask indicates which of the other members in the structure contain valid data. Besides mask, every member of this structure has a corresponding mask flag listed as follows:
(Table omitted)
In order to use members pszText, iImage and iSelectedImage, we need to set the following bits of member mask:
TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_TEXT
Member pszText is a pointer to a null-terminated string text that will be used to label this node. Member iImage and iSelectedImage are indices to two images contained in the image list that will be used to represent the node's normal and selected state respectively.
By calling function CTreeCtrl::InsertItem(...) repeatedly, we could create a tree structure with desired number of nodes.
Sample
Sample 5.12\CCtl demonstrates how to use tree control in a dialog box. It is a dialog based application generated by Application Wizard. There is only one tree control IDC_TREE in the dialog template. To access it, a member variable CCCtlDlg::m_treeCtrl is added for IDC_TREE through using Class Wizard.
To create the image list, five bitmap resources are prepared, whose IDs are IDB_BITMAP_CLOSEDFOLDER, IDB_BITMAP_DOC, IDB_BITMAP_LEAF, IDB_BITMAP_OPENFOLDER and IDB_BITMAP_ROOT respectively. These bitmaps have the same dimension.
In function CCCtlDlg::OnInitDlalog(), the image list is created as follows:
(Code omitted)
A CBitmap type local variable bmp is declared to load the bitmap resources. First, function CImageList::Create(...) is called to create the image list. Here macro BMP_SIZE_X and BMP_SIZE_Y are defined at the beginning of the implementation file, they represent the dimension of the bitmaps:
#define BMP_SIZE_X 16
#define BMP_SIZE_Y 15
We use ILC_MASK flag to let the bitmaps be drawn with transparent background. Originally the image list has five bitmaps, it will not grow later (The fourth and fifth parameter of function CImageList::Create(...) are 5 and 0 respectively).
Next we use variable bmp to load each bitmap resource and add it to the list. When calling function CImageList::Add(...), we pass a COLORREF type value to its second parameter (RGB macro specifies the intensity of red, green and blue colors, and returns a COLORREF type value). This means all the white color in the image will be treated as the background. In the sample application, the background color is set to white:
m_pilCtrl->SetBkColor(RGB(255, 255, 255));
We can also change the values contained in the RGB macro to set the background to other colors.
Besides this method, we can also prepare all the images in one bitmap resource (just like the tool bar resource), and call the following versions of function CImageList::Create(...) to create the image list:
BOOL CImageList::Create(UINT nBitmapID, int cx, int nGrow, COLORREF crMask);
BOOL CImageList::Create(LPCTSTR lpszBitmapID, int cx, int nGrow, COLORREF crMask);
Here nBitmapID or lpszBitmapID specifies the bitmap resource ID, and cx specifies the horizontal dimension of an individual image. With this parameter, the system knows how to divide one big image into several small images.
After creating the image list, function CTreeCtrl::SetImageList(...) is called to assign the image list to the tree control:
......
m_treeCtrl.SetImageList(m_pilCtrl, TVSIL_NORMAL);
......
Since the image list is created dynamically, we need to release it when it is no longer in use. The best place to destroy the image list is in CDialog::OnDestroy(), when the dialog box is about to be destroyed. This function is the handler of WM_DESTROY message, which could be easily added through using Class Wizard. The following is the implementation of this function in the sample:
(Code omitted)
We call function CImageList::GetImageList(...) to obtain the pointer to the image list, then call CImageList::DeleteImageList() to delete the image list. Please note that this function releases only the images stored in the list, it does not delete CImageList type object. After the image list is deleted, we still need to use keyword "delete" to delete this object.
In the sample, a tree with the structure showed in Figure 5-10 is created.
This tree has 7 nodes. Node "Root" is the root node, it has one child node "Doc". Node "Doc" has a child node "Folder", and node "Folder" has four child nodes "Leaf1", "Leaf2", "Leaf3" and "Leaf4". The following portion of function CCCtlDlg::OnInitDialog() shows how the node "Root" is created in the sample:
(Code omitted)
Variable tvInsertStruct is declared at the beginning of function CCCtlDlg::OnInitDialog(), it is a TV_INSERTSTRUCT type object. To create a specific node, we must stuff this object with node information and call function CTreeCtrl::InsertItem(...). This function returns a handle to the newly created node, which is stored in variable hTreeItem and will be used to create its child node. The following portion of function CCCtlDlg::OnInitDialog() shows how the child node is created:
(Code omitted)
This procedure is exactly the same for other nodes. For different nodes, the only difference of this procedure is that each node has different parent node, uses different image index and text string. For all nodes, their normal states and selected states are represented by the same image (member iImage and iSelectedImage are assigned the same image index), so the image will not change if we select a node.
With the above implementations, the tree control can work. By compiling and executing the application at this point, we will see a tree with seven nodes, which are represented by different labels and images. A node can be expanded or collapsed with mouse clicking if it has child node(s).
5.13 Handling Tree Control Messages
There are many messages associated with the tree control. We need to write message handlers for the tree control in order to customize its default behavior. In sample 5.13\CCtl we will demonstrates two methods of customizing a tree control: 1) How to change a node's associated image dynamically. 2) How to enable label editing.
Sample 5.13\CCtl is based on sample 5.12\CCtl. In this sample the image associated with node "Folder" will be changed automatically according to its current state (expanded or collapsed). If it is expanded, image IDB_BITMAP_OPENFOLDER will be associated with this node; if it is collapsed, image IDB_BITMAP_CLOSED_FOLDER will be used. Also, the application supports dynamic label editing: if the user clicks mouse's left button on the label of a node, that node will enter editing mode, and we can edit the text string as if we were using an edit box.
The messages associated with node expanding and collapsing are TVN_ITEMEXPANDING and TVN_ITEMEXPANDED. The former message is sent when a node is about to be expanded or collapsed, and the latter message is sent after such action is completed. In our case, we need to handle the former message to change a node's image before its state changes.
Handling TVN_ITEMEXPANDING to Change a Node's Associated Image
In MFC, message TVN_ITEMEXPANDING can be mapped to a member function as follows:
void CTreeCtrl::OnItemexpanding(NMHDR *pNMHDR, LRESULT *pResult)
{
NM_TREEVIEW *pNMTreeView = (NM_TREEVIEW*)pNMHDR;
*pResult = 0;
}
Variable pNMTreeView is a pointer to NM_TREEVIEW type object obtained from the message parameters, it contains the information about the node being clicked:
typedef struct _NM_TREEVIEW{
NMHDR hdr;
UINT action;
TV_ITEM itemOld;
TV_ITEM itemNew;
POINT ptDrag;
} NM_TREEVIEW;
The most important member of this structure is action, it could be either TVE_EXPAND (indicating the node is about to expand) or TVE_COLLAPSE (indicating the node is about to collapse). Two other useful members are itemOld and itemNew, both of them are TV_ITEM type objects and contain old and new states of the node respectively. We can check iImage member of itemNew to see if the associated image is 2 or 3 (Indices 2 and 3 correspond to image IDB_BITMAP_CLOSED_FOLDER and IDB_BITMAP_OPENFOLDER respectively, which indicate that the node represents a folder. In the sample, we will not change other node's image when they are being expanded or collapsed), if so, we need to call function CTreeCtrl::SetItemImage(...) to change the image of the node if necessary.
We can handle this message either within class CTreeCtrl or CDialog. Handling the message in CTreeCtrl has the advantage that once the feature is implemented, we can reuse this class in other applications without adding additional code.
In the sample, a new class MCTreeCtrl is designed for this purpose. It is added to the application through using Class Wizard. Also, message handlers MCTreeCtrl::OnItemexpanding(...) and MCTreeCtrl::OnEndlabeledit(...) are added to dynamically change node's associated images and enable label editing (Label editing will be discussed later).
The following is the implementation of function MCTreeCtrl::OnItemexpanding(...):
(Code omitted)
If the node is about to expand and its associated image is 2, we associate image 3 with this node. This is implemented through calling function CTreeCtrl::SetItemImage(...), which has the following format:
BOOL CTreeCtrl::SetItemImage(HTREEITEM hItem, int nImage, int nSelectedImage);
The first parameter of this function is the handle of tree control, which can be obtained from pNMTreeView->itemNew.hItem. Similarly, if the node is about to collapse and its associated image is 3, we call this function to associate image 2 with this node.
Handling TVN_ENDLABELEDIT to Enable Label Editing
The next feature we want to add is label editing. If we are familiar with "Explorer" application in Windows95(, we know that the file or directory names (which are node labels) can be edited dynamically by single clicking on it.
The first step of enabling label editing is to set "Edit labels" style when adding tree control resource to the dialog template. The following lsts necessary steps of doing this: 1) Invoke "Tree Control Properties" property sheet, click "Styles" tab. 2) Check "Edit labels" check box (Figure 5-11).
Label editing will be enabled if this style is selected. However, if we do not add code to change the label at the end of editing, the label will remain unchanged after it is edited. To make this happen, we must handle message TVN_ENDLABELEDIT.
Standard TVN_ENDLABELEDIT message handler added by Class Wizard will have the following format:
void MCTreeCtrl::OnEndlabeledit(NMHDR *pNMHDR, LRESULT *pResult)
{
TV_DISPINFO *pTVDispInfo = (TV_DISPINFO*)pNMHDR;
*pResult=0;
}
Here pTVDispInfo is a pointer to TV_DISPINFO type object, which can be obtained from the message parameter. The most useful member of TV_DISPINFO is item, which is a TV_ITEM type object. Three members of item contain valid information: hItem, lParam, and pszText. We could use hItem to identify the node and use pszText to obtain the updated text string. If pszText is a NULL pointer, this means the editing is canceled (Label editing can be canceled through pressing ESC key). Otherwise it will contain a NULL-terminated string. The following is the implementation of this message handler:
(Code omitted)
If the editing is not canceled, we need to call function CTreeCtrl::SetItemText(...) to set the node's new text, which has the following format:
BOOL CTreeCtrl::SetItemText(HTREEITEM hItem, LPCTSTR lpszItem);
This function is similar to CTreeCtrl::SetItemImage(...). Its first parameter is the handle of tree control, and the second parameter is a string pointer to the new label text.
There are other messages associated with label editing, one useful message is TVN_BEGINLABELEDIT, which will be sent when the editing is about to begin. We can handle this message to disable label editing for certain nodes. In the message handler, if we assign a non-zero value to the content of pResult, the edit will stop. Otherwise the label editing will go on as usual.
Using the New Class
In the new sample, variable CCCtlDlg::m_treeCtrl is declared by class MCTreeCtrl instead of CTreeCtrl. First the header file that contains class MCTreeCtrl is included in file "CCtlDlg.h", then the declaration of member variable CCCtlDlg::m_treeCtrl is changed:
class CCCtlDlg : public CDialog
{
......
//{{AFX_DATA(CCCtlDlg)
enum { IDD = IDD_CCTL_DIALOG };
MCTreeCtrl m_treeCtrl;
That's all we need to do in order to add new features to the sample.
When editing a label, we can not press RETURN key to end the editing. This is because in a dialog box, RETURN is used to close the dialog box by default. If we want to change this feature, we need to override function CDialog::PreTranslateMessage(...) and intercept RETURN key stroke messages as we did for combo box in sample 5.9\CCtl.
5.14 Drag-n-Drop
Another nice feature we can add to tree control is to change the tree structure by dragging and dropping. By implementing this, we can copy or move one node (and all its child nodes) to another place with few mouse clicks.
Sample 5.14\CCtl demonstrates drag-n-drop implementation. It is base on sample 5.13\CCtl with new messages handled in class MCTreeCtrl.
Handling New Messages
To implement node dragging and dropping, we need to handle the following three messages: TVN_BEGINDRAG, WM_MOUSEMOVE and WM_LBUTTONUP. The first message is sent when the user starts node dragging. After receiving this message, we need to prepare node dragging. Message WM_MOUSEMOVE should be handled when an item is being dragged around: when the mouse cursor hits a possible target node, we need to highlight it to remind the user that the source node could be dropped here. When we receive message WM_LBUTTONUP, we need to check if the node can be copied to the new place, if so, we need to implement node copy (or move).
In the sample, message handlers of TVN_BEGINDRAG, WM_MOUSEMOVE and WM_LBUTTONUP are added through using Class Wizard. The default TVN_BEGINDRAG message handler has the following format:
void MCTreeCtrl::OnBegindrag(NMHDR *pNMHDR, LRESULT *pResult)
{
NM_TREEVIEW *pNMTreeView=(NM_TREEVIEW *)pNMHDR;
*pResult=0;
}
Here, several issues must be considered when a node is being dragged around:
1) To determine which node is being clicked for dragging after receiving message TVB_BEGINDRAG, we can call API function ::GetCursorPos(...) to retrieve the current position of mouse cursor, call function CWnd::ScreenToClient(...) to convert its coordinates, and call CTreeCtrl::HitTest(...) to obtain the handle of the node that is being clicked.
2) We must provide a dragging image that will be drawn under the mouse cursor to give the user an impression that the node is being "dragged". An easiest way of preparing this image is to call function CTreeCtrl::CreateDragImage(...), which will create dragging image using the bitmap associated with this node. This function will return a CImageList type pointer, which could be further used to implement dragging. We can also create our own customized image list for dragging operation, the procedure of creating this type of image list is the same with creating a normal image list.
3) We can call function CImageList::SetDragCursorImage(...) to combine an image contained in the image list with the cursor to begin dragging.
4) We must lock the tree control window when a node is being dragged around to avoid any change happening to the tree structure (When a node is being dragged, the tree should not change). When we want to do a temporary update (For example, when the dragging image enters a node and we want to highlight that node to indicate that the source can be dropped there), we must first unlock the window, then implement the update. If we want the dragging to be continued, we must lock the window again.
5) Function CImageList::EnterDrag(...) can be called to enter dragging mode and lock the tree control window. Before we make any change to the tree control window (For example, before we highlight a node), we need to call function CImageList::LeaveDrag(...) to unlock the tree control window. After the updates, we need to call CImageList::EnterDrag(...) again to lock the window. This will prevent the tree control from being updated when a node is being dragged around.
6) We can show or hide the dragging image by calling function CImageList::DragShowNolock(...) without locking the tree control window. This function is usually called before CImageList::SetDragCursorImage(...) is called.
7) To begin dragging, we need to call CImageList::BeginDrag(...); to move the dragging image to a specified position, we can call CImageList::DragMove(...); to end dragging, we need to call CImageList::EndDrag().
8) We can highlight a node by calling function CTreeCtrl::SelectDropTarget(...).
The following is a list of prototypes of the above-mentioned functions:
BOOL CImageList::DragShowNolock(BOOL bShow);
(Table omitted)
BOOL CImageList::BeginDrag(int nImage, CPoint ptHotSpot);
(Table omitted)
BOOL CImageList::DragMove(CPoint pt);
(Table omitted)
BOOL CImageList::DragEnter(CWnd *pWndLock, CPoint point);
(Table omitted)
BOOL CImageList::DragLeave(CWnd *pWndLock);
(Table omitted)
BOOL CTreeCtrl::SelectDropTarget(HTREEITEM hItem);
(Table omitted)
When the mouse button is released, we need to check if the source node can be copied to the target node. In the sample, we disable copying under the following three conditions: 1) The source node is the same with the target node. 2) The target node is a descendent node of the source node. 3) The target node does not have any child node. By setting these restrictions, a node can only be copied to become the child of its parent node (direct or indirect).
We can use function CTreeCtrl::GetParentItem(...) to decide if one node is the descendent of another node:
HTREEITEM CTreeCtrl::GetParentItem(HTREEITEM hItem);
This function will return an HTREEITEM handle, which specifies the parent of node hItem. By repeatedly calling this function we will finally get a NULL return value (This indicates that the root node was encountered). Using this method, we can easily find out a list of all nodes that are parents of a specific node.
New Member Variables and Functions
To implement drag-n-drop, several new variables and functions are declared in class MCTreeCtrl:
(Code omitted)
Here, Boolean type variable m_bIsDragging is used to indicate if the drag-n-drop activity is undergoing. Pointer m_pilDrag will be used to store the dragging image. Variables m_hTreeDragSrc and m_hTreeDragTgt are used to store the handles of source and target nodes respectively. We can use them to implement copying right after the source node is dropped. Function MCTreeCtrl::IsDescendent(...) is used to judge if one node is the descendent node of another, and MCTreeCtrl::CopyItemTo(...) will copy one node (and all its descendent nodes) to another place.
Node Copy
When copying a node, we want to copy not only the node itself, but also all its descendent nodes. Since we do not know how many descendents a node have beforehand, we need to call function MCTreeCtrl::CopyItemTo(...) recursively until all the descendent nodes are copied. The following is the implementation of this function:
(Code omitted)
This function copies node hTreeDragSrc along with all its descendent nodes, and make them the child nodes of hTreeDragTgt. First we call function CTreeCtrl::GetItem(...) to retrieve source node's information. We must pass a TV_ITEM type pointer to this function, and the corresponding object will be filled with the information of the specified node. Here, we use member item of structure TV_INSERTSTRUCT to receive a node's information (Variable tvInsertStruct is declared by TV_INSERTSTRUCT, it will be used to create new nodes). When calling this function, member mask of TV_ITEM structure specifies which member should be filled with the node's information. In our case, we want to know the handle of this node, the associated images, the text of the label, the current state (expanded, highlighted, etc.), and if the node has any child node. So we need to set the following bits of member mask: TVIF_CHILDREN, TVIF_HANDLE, TVIF_IMAGE, TVIF_SELECTEDIMAGE, TVIF_TEXT and TVIFF_STATE. Note we must provide our own buffer to receive the label text. In the function, szBuf is declared as a char type array and its address is stored in pszText member of TV_ITEM. Then we use tvInsertStruct to create a new node. Since we have already stuffed item member with valid information, here we only need to assign the target handle (stored in hTreeDragTgt) to hParent, and assign TVI_LAST to hInsertAfter. This will make the new node to become the child of the target node, and be added to the end of all child nodes under the target node. Next we check if this node has any child node. If so, we find out all the child nodes and call this function recursively to copy all the child nodes. For this step, we use the newly created node as the target node, this will ensure that the original tree structure will not change after copying.
In the final step, we call function CTreeCtrl::GetChileItem(...) to find out a node's first child node, then call function CTreeCtrl::GetNextitem(...) repeatedly to get the rest child nodes. The two functions will return NULL if no child node is found.
TVN_BEGINDRAG
Now we need to implement TVN_BEGINDRAG message handler. First, we need to obtain the node that was clicked by the mouse cursor. To obtain the current position of mouse cursor, we can call API function ::GetCursorPos(...). Since this position is measured in the screen coordinate system, we need to further call function CWnd::ScreenToClient(...) to convert the coordinates to the coordinate system of the tree control window. Then we can set variable m_bIsDragging to TRUE, and call function CTreeCtrl::HitTest(...) to find out if the mouse cursor is over any node:
(Code omitted)
Next, we need to obtain dragging image list for this node. The dragging image list is created by calling function CTreeCtrl::CreateDragImage(...). After this, the address of the image list object is stored in variable m_pilDrag. If the image list is created successfully, we call several member functions of CImageList to display the dragging image and enter dragging mode. If not, we should not start dragging, and need to set the content of pResult to a non-zero value, this will stop dragging:
(Code omitted)
WM_MOUSEMOVE
Then we need to implement WM_MOUSEMOVE message handler. Whenever the mouse cursor moves to a new place, we need to call function CImageList::DragMove(...) to move the dragging image so that the image will always follow the mouse's movement. We need to check if the mouse hits a new node by calling function CTreeCtrl::HitTest(...). If so, we must leave dragging mode by calling function CImageList:: DragLeave(...), highlight the new node by calling function CTreeCtrl::SelectDropTarget(...), and enter dragging mode again by calling function CTreeCtrl::DragEnter(...). The reason for doing this is that when dragging is undergoing, the tree control window is locked and no update could be implemented successfully. The following is the implementation of this message handler:
(Code omitted)
WM_LBUTTONUP
Finally we need to implement WM_LBUTTONUP message handler. In this handler, we must first leave dragging mode and end dragging. This can be implemented by calling functions CImageList:: DragLeave(...) and CImageList::EndDrag() respectively. Then, we need to delete dragging image list object:
(Code omitted)
The following code fragment shows how to judge if the source node can be copied to become the child of the target node:
(Code omitted)
If the source and target are the same node, or target node does not have any child node, or source node is the parent node (including indirect parent) of the target node, the copy should not be implemented. Otherwise, we need to call function MCTreeCtrl::CopyItem(...) to implement node copy:
(Code omitted)
If we want the node to be moved instead of being copied, we can delete the source node after copying it. The source node and all its child nodes will be deleted by calling function CTreeCtrl::DeleteItem(...).
Functions CWnd::SetCapture() and ::ReleaseCapture() are also called in MCTreeCtrl:: OnBegindrag(...) and MCTreeCtrl::OnLButtonUp(...) respectively to set and release the window capture. By doing this, we can still trap mouse messages even if it moves outside the client window when dragging is undergoing.
That's all we need to do for implementing drag-n-drop copying. By compiling and executing the sample application at this point, we will be able to copy nodes through mouse dragging. With minor modifications to the above message handlers, we can easily implement both node copy and move as follows: when CTRL key is held down, the node can be copied through drag-n-drop, when there is no key held down, node will be moved.
5.15 List Control
A list control is another type of control that can be used to manage a list of objects. Rather than storing items in a tree structure, a list control simply organize them into an array. There is no parent or child node in a list control.
A list control can be viewed in different styles: 1) Normal icon style ¾ each item is represented by a big icon. 2) Small icon style ¾ each item is represented by a small icon. 3) List style ¾ all items are represented by small icons contained in a vertical list. 4) Report style ¾ the details of all items are listed in several vertical lists.
In MFC, list control is supported by class CListCtrl. Implementing list control is similar to implementing tree control: the list control resource can be created in dialog template, then the list control can be initialized in the dialog's initialization stage. Each item in the list control can be associated with one or more images, they will be used to represent the item in different styles. Usually we need to associate two images for an item: one big image for normal style, and a small image for other three styles. In general case, we need to prepare two image lists to create a list control.
LV_COLUMN and LV_ITEM
The procedure of initializing list control is similar to that of tree control. First we need to create two image lists: one for normal icon style; one for small icon style. Then we need to call function CListCtrl::SelectImageList(...) to associate the image lists with the list control. The following is the format of this function:
CImageList *CListCtrl::SetImageList(CImageList *pImageList, int nImageList);
Here pImageList is a pointer to the image list, and nImageList specifies the type of image list: it could be LVSIL_NORMAL or LVSIL_SMALL, representing which style the image list will be used for.
After the image list is set, we need to add columns for the list control (Figure 5-12). This can be implemented by calling function CListCtrl::InsertColumn(...), which has the following format:
int CListCtrl::InsertColumn(int nCol, const LV_COLUMN* pColumn);
The function has two parameters. The first one indicates which column is to be added (0 based index), and the second one is a pointer to LV_COLUMN type object:
typedef struct _LV_COLUMN {
UINT mask;
int fmt;
int cx;
LPSTR pszText;
int cchTextMax;
int iSubItem;
} LV_COLUMN;
Here, member mask indicates which of the other members contain valid values, this is the same with structure LV_ITEM. Member fmt indicates the text alignment for the column, it can be LVCFMT_LEFT, LVCFMT_RIGHT, or LVCFMT_CENTER. Member cx indicates the width of the column, and iSubItem indicates its index. Member pszText is a pointer to the text string that will be displayed for each column. Finally, cchTextMax specifies the size of buffer pointed by pszText.
After columns are created, we need to add list items. For each list item, we need to insert a sub-item in each column. For example, if there are three columns and 4 list items, we need to add totally 12 sub-items.
To add a sub-item, we need to stuff an LV_ITEM type object then call function CListCtrl:: InsertItem(...), which has the following format:
int CListCtrl::InsertItem(const LV_ITEM* pItem);
The following is the format of structure LV_ITEM:
typedef struct _LV_ITEM {
UINT mask;
int iItem;
int iSubItem;
UINT state;
UINT stateMask;
LPSTR pszText;
int cchTextMax;
int iImage;
LPARAM lParam;
} LV_ITEM;
The usage of this structure is similar to that of structure TV_ITEM. For each item, we need to use this structure to add every sub-item for it. Usually only the sub-items contained in the first column will have an associated image (when being displayed in report style), so we need to set image for each item only once. Member iItem and iSubItem specify item index and column index respectively.
Sample
Sample 5.15\CCtl demonstrates how to use list control. It is a dialog-based application generated by Application Wizard. In this sample, a four-item list is implemented, which can be displayed in one of the four styles. When it is displayed in report style, the control has four columns. The first column lists four shapes: square, rectangle, circle, triangle. The second column lists the formula for calculating their perimeter, and the third column lists the formula for calculating their area.
Creating Image Lists
In the dialog template, the list control has an ID of IDC_LIST. In order to access this control, a CListCtrl type variable m_listCtrl is added to class CCCtldlg through using Class Wizard.
Four icon resources are added to the application for creating image lists. Their IDs are IDI_ICON_SQUARE, IDI_ICON_RECTANGLE, IDI_ICON_CIRCLE and IDI_ICON_TRIANGLE respectively. In the previous samples, we created image list from bitmap resource all the time. Actually, it can also be created from icon resources as well.
In function CCCtlDlg::OnInitDialog(), first two image lists are created and selected into the list control:
(Code omitted)
We could use the same icon to create both 32(32 and 16(16 image lists. When creating the 16(16 image list, the images will be automatically scaled to the size specified by the image list. Since we allocate memory for creating image list in dialog's initialization stage, we need to release it when the dialog box is being destroyed. For this purpose, a WM_DESTROY message handler is added through using Class Wizard, within which the image lists are deleted as follows:
(Code omitted)
If we release the memory used by image lists this way, we must set "Share image list" style for the list control. This allows image list to be shared among different controls. If we do not set this style, the image list will be destroyed automatically when the list control is destroyed. In this case, we don't have to release the memory by ourselves. To set this style, we need to invoke "List control properties" property sheet, go to "More styles" page, and check "Share image list" check box (Figure 5-13).
(Figure 5-13 omitted)
Creating Columns
First we need to create three columns, whose titles are "Shape", "Perimeter", and "Area" respectively. The following portion of function CCCtlDlg::OnInitDialog() creates each column:
(Code omitted)
The client window's dimension is retrieved by calling function CWnd::GetClientRect(...) and then stored in variable rect. The horizontal size of each column is set to 1/3 of the width of the client window.
Creating Sub-items
Since there are totally three columns, for each item, we need to create three sub-items. The following is the portion of function CCCtlDlg::OnInitDialog() that demonstrates creating one sub-item:
(Code omitted)
Function CListCtrl::InsertItem(...) is called to add a list item and set its first sub-item. The rest sub-items should be set by calling function CListCtrl::SetItem(...). For these sub-items, we don't need to set image again, so LVIF_IMAGE flag is not applied when function CListCtrl::SetItem(...) is called.
Changing List Style Dynamically
The style of the list control can be set in property sheet "List control properties" before the application is compiled(see Figure 5-14). But, sometimes we may want to provide the user with the power of changing this style dynamically. When the program is running, we can call API function ::SetWindwoLong(...) to change the application's style. For list control, we can choose from one of the following styles: LVS_ICON, LVS_SMALLICON, LVS_LIST and LVS_REPORT.
In the sample, four radio buttons are added to the dialog template for selecting different styles. Their IDs are IDC_RADIO_ICON, IDC_RADIO_SMALLICON, IDC_RADIO_LIST and IDC_RADIO_REPORT respectively. We need to handle BN_CLICKED message for the four radio buttons in order to respond to mouse events. These message handlers are added through using Class Wizard. Within the member functions, the style of the list control is changed according to which radio button is being clicked. The following is one of the message handlers that sets the style of the list control to "Normal Icon":
(Code omitted)
First, the list control's old style is retrieved by calling function ::GetWindowLong(...), and is bit-wisely ANDed with LVS_TYPEMASK, which will turn off all the style bits. Then style LVS_ICON is added to the window style (through bit-wise ORing), and function ::SetWindowLong(...) is called to update the new style. Both function ::GetWindowLong(...) and ::SetWindowLong(...) require a window handle, it could be obtained by calling function CWnd:: GetSafeHwnd().
The list control and tree control can also be implemented in SDI and MDI applications. In this case, we need to use classes derived from CListView or CTreeView. Although the creating procedure is a little different from that of a dialog box, the properties of the controls are exactly the same for two different types of applications. We will further explore list control and tree control in chapter 15.
5.16 Tab Control
In the previous sample, we used radio buttons to let the user set the style of list control dynamically. An alternate way of doing this is to use tab control, which is widely used in various types of applications. Usually a tab control is used together with dialog box to implement property sheets, which can let the user easily switch among different property pages. This topic will be discussed in a chapter 7. Here, we will discuss some basics on how to implement tab control and handle its messages.
Using Tab Control
In MFC, tab control can be implemented by using class CTabCtrl. A tab control can be associated with an image list, so we can display both image and text on each tab. The steps of using a tab control is very similar to that of list control and tree control: first we need to add tab control resource to the dialog template; then in the dialog's initialization stage, we need to create the image list, select it into the tab control, and initialize the tab control. The function that can be used to assign image list to a tab control is CTabCtrl::SetImageList(...), which has the following format:
CImageList *CTabCtrl::SetImageList(CImageList *pImageList);
The function that can be used to add an item to the tab control is CTabCtrl::InsertItem(...):
BOOL CTabCtrl::InsertItem(int nItem, TC_ITEM *pTabCtrlItem);
The first parameter of this function is the index of the tab (zero based), and the second parameter is a pointer to TC_ITEM type object. Before calling this function, we need to stuff structure TC_ITEM with tab's information:
typedef struct _TC_ITEM {
UINT mask;
UINT lpReserved1;
UINT lpReserved2;
LPSTR pszText;
int cchTextMax;
int iImage;
LPARAM lParam;
} TC_ITEM;
We need to use three members of this structure in order to create a tab with both text and image: mask, pszText and iImage. Member mask indicates which of the other members of this structure contains valid data; member pszText is a pointer to string text; and iImage is an image index to the associated image list. We see that using this structure is very similar to that of TV_ITEM and LV_ITEM.
To respond to tab control's activities, we need to add message handlers for it. The most commonly used messages of tab control are TCN_SELCHANGE and TCN_SELCHANGING, which indicate that the current selection has changed or the current selection is about to change respectively.
Sample 5.16\CCtl demonstrates how to use tab control, it is based on sample 5.15\CCtl. In this sample, four radio buttons are replaced by a tab control IDC_TAB (see Figure 5-14). Also, message handlers of radio buttons are removed. In order to access the tab control, a CTabCtrl type control variable m_tabCtrl is added to class CCCtlDlg through using Class Wizard. Beside this, four bitmap resources IDB_BITMAP_ICON, IDB_BITMAP_SMALLICON, IDB_BITMAP_LIST and IDB_BITMAP_REPORT are added to the application to create image list for tab control.
In function CCCtlDlg::OnInitDialog(), first the image list is created and assigned to the tab control. Next, four items are added to the tab control:
(Code omitted)
Macro TAB_BMP_WIDTH and TAB_BMP_HEIGHT are defined as the width and height of the bitmaps. In function CCCtlDlg::Destroy(), the following statements are added to delete the tab items and the image list used by the tab control:
(Code omitted)
Handling Tab Control Message
We trap message TCN_SELCHANGE to respond to the changes on the tab control. After receiving this message, we call function CTabCtrl::GetCurSel() to obtain the newly selected item, then call function ::SetWindowLong(...) to set the style of the list control accordingly. In the sample, function CCCtlDlg::OnSelchangeTab(...) is added to class CCCtlDlg through using Class Wizard for handling this message. It is implemented as follows:
(Code omitted)
With the above implementation, we can change the list control's style dynamically through using the tab control.
5.17 Animate Control and Progress Control
Using Animate Control and Progress Control
Animate and progress controls are very useful, both of them can be implemented in a dialog box very easily. In MFC, the classes used to implement animate and progress controls are CAnimateCtrl and CProgressCtrl respectively.
Sample 5.17\CCtl demonstrates how to use two types of controls. It is a dialog-based application generated by Application Wizard.
Like any other common controls, the first step of using animate and progress controls is to add their resources to the dialog template. There are very few styles that can be customized, and the meanings of them are all self-explanatory. To access the controls, we can use Class Wizard to add member variables for them.
For animate control, the functions we need to call for implementing animation are CAnimateCtrl::Open(...) and CAnimateCtrl::Play(...). The first function lets us open an animation resource either from a file or from an AVI resource. The second function lets us play the loaded AVI data.
For progress control, we need to call function CProgressCtrl::SetRange(...) to set the upper and lower limits, call CProgressCtrl::SetStep(...) to specify the incremental step, and call CProgressCtrl:: StepIt() to advance the current position of progress bar. Each time we call this function, the progress bar will advance one step. In order to let the progress bar advance continuously, we need to link it to some events that happen all the time. In the sample, a timer is used to generate this type of events.
Timer
Timer is a very useful resource in Windows( operating system. Once we set the timer and specify the time out period, it will start to count down and send us a WM_TIMER message when time out happens. The timer can be set within any CWnd derived class by calling function CWnd::SetTimer(...). Timers with different IDs are independent upon one another, so we can set more than one timer to handle complex situation.
The following is the prototype of function CWnd::SetTimer(...):
UINT CWnd::SetTimer
(
UINT nIDEvent, UINT nElapse,
void(CALLBACK EXPORT *lpfnTimer)(HWND, UINT, UINT, DWORD)
);
The function has three parameters. Parameter nIDEvent is an event ID. This ID can be any integer, and we need to use different ID for different event in order to distinguish between them. Parameter nElapse specifies time out period, whose unit is millisecond. Parameter lpfnTimer is a pointer to a callback function that will be used to handle time out message. We can also pass NULL to this parameter and add WM_TIMER message handler to receive this message.
In the sample, the IDs of the animate control and progress control are IDC_ANIMATE and IDC_PROGRESS. Also, the variables used to access them are m_animateCtrl and m_progressCtrl respectively.
Custom Resource
The AVI data can be included in the application as a resource. However, Developer Studio does not support this kind of resource directly. So we have to treat it as a custom resource. We can create AVI resource from a "*.avi" file through following steps: 1) Execute Insert | Resource command, then click "Import" button from the "Insert resource" dialog box. 2) From the popped up "File open" dialog box, browse and select a "*.avi" file and open it (we can use "5.17\CCtl\search.avi" or any other "*.avi" file for this purpose). 3) When we are asked to provide the resource type, input "AVI". 4) Name the resource ID as IDR_AVI.
Sample Implementation
In the dialog box's initialization stage, we need to initialize the animate control, progress control and set timer as follows:
(Code omitted)
First, function CAnimateCtrl::Open(...) is called to open the animation resource, then function CAnimateCtrl::Play(...) is called to play the AVI data. When doing this, we pass 0 to its first parameter and -1 to the second parameter, this will let the animation be played from the first frame to the last frame. The third parameter is also -1, this means the animation will be played again and again without being stopped.
Then we initialize the range of progress control from 0 to 50, incremental step 2, and a timer with time out period of 500 milliseconds is set.
Message WM_TIMER can be handled by adding message handlers, this can be easily implemented through using Class Wizard. In the sample, this member function is implemented as follows:
(Code omitted)
The only parameter of this function is nIDEvent, which indicates the ID of the timer that has just timed out. If we have two or more timers set within the same window, by examining this ID we know which timer has timed out. In the sample, when timer times out, we simply call function CProgressCtrl::StepIt() to advance the progress bar one step forward.
Summary:
1) A spin control must work together with another control, which is called the "Buddy" of the spin control. Usually the "Buddy" is an edit box, but it could be any other types of controls such as button or static control.
2) To set buddy automatically, we must make sure that the buddy window is the previous window of the spin control in Z-order.
3) The buddy can also be set by calling function CSpinButtonCtrl::SetBuddy(...).
4) If we set "Set buddy integer" style, the spin control will notify the buddy control to update its contents whenever the position of the spin control changes. If we set this style, the buddy edit box can display only integers.
5) If we want to customize the behavior of buddy control, we need to handle message UDN_DELTAPOS. This message will be sent when the position of the spin control changes. By doing this, we can let the buddy control display text strings or bitmap images.
6) Slider control shares the same message with scroll bars. By handling message WM_HSCROLL (for horizontally orientated sliders) and WM_VSCROLL (for vertically orientated sliders), we can trap the mouse activities on the slider.
7) List box can be implemented in different styles: single selection, multiple selection, extended selection. By default, the items in the list box will contain only characters, and they will be alphabetically sorted. These styles can be changed by calling member functions of CListCtrl.
8) A list box can be used to display directories and files contained in a specific directory by calling function CListCtrl::Dir(...).
9) We can handle LBN_... type messages to customize the default behavior of a list control.
10) A combo box is composed of an edit box and a list box. Because they are not created by MFC code, we can not access them through the normal method.
11) To trap RETURN, ESC keys for combo box, we need to override function CWnd:: PreTranslateMessage(...).
12) To implement subclass for edit box contained in a combo box, we need to call function CWnd:: SubclassWindow(...) instead of CWnd::SubclassDlgItem(...).
13) To create owner-draw list box or combo box, first we need to set "Owner draw" style, then override WM_MEASUREITEM and WM_DRAWITEM message handlers.
14) Image list is used by tree control, list control and tab control. Once an image list is assigned to a control, the images contained in the list can be accessed through their zero-based indices.
15) To use a tree control, we can create its resource in the dialog template. Then in the dialog's initialization stage, we can create the tree structure. A tree item can be added to the control by stuffing a TV_ITEM type object, then calling function CTreeCtrl::InsertItem(...).
16) We need to handle message TVN_ITEMEXPANDING or TVN_ITEMEXPANDED to customize a tree control's expanding and collapsing behaviors.
17) We need to set "Edit labels" style and handle TVN_ENDLABELEDIT message to enable label editing for tree control.
18) We need to handle messages TVN_BEGINDRAG, WM_MOSUEMOVE, and WM_LBUTTONUP to enable drag-n-drop for tree control.
19) The list box can be displayed in four different styles: Normal (big) icon style, small icon style, list style, and report style. We can select one style when the list box resource is being created. If we want to change the style dynamically, we need to call function ::SetWindowLong(...).
20) Because list control can be used to represent items in different styles, usually we need to prepare two image lists (big icon and small icon) for a list control.
21) To create a list control, we need to create columns first. For each column, we need to create sub-items for all the items contained in the list.
22) To create a column for a list control, we need to stuff an LV_COLUMN type object and call function CListCtrl::InsertColumn(...). To create an item, we need to stuff an LV_ITEM type object and call function CListCtrl::InsertItem(...). To set the rest sub-items, we need to stuff LV_ITEM type objects and call function CListCtrl::SetItem(...).
23) To use tab control, first we need to create tab control resource in the dialog template. Then in the dialog's initialization stage, we need to create and select the image list, stuff TV_ITEM type objects and call function CTabCtrl::InsertItem(...) to add items.
24) The animate control can be used to play AVI data. Because this is not a standard resource supported in Developer Studio, we need to create custom resource if we want to include AVI data in an application as a resource.
25) The progress control is used to indicate the progress of events. In order to synchronize the progress bar with the events, we need to advance the progress bar within the event's message handler.