Advanced MFC Programming Guide
Advanced MFC Programming Guide
Supporting Document
Table of Contents
Summary:
CHAPTER 2. MENU
2.1 Message WM_COMMAND and UPDATE_COMMAND_UI
Handling WM_COMMAND Command
Enabling & Disabling a Command
Changing Menu Text
ii
Checking a Menu Item
Summary
Summary
iii
CHAPTER 4. BUTTONS
4.1 Bitmap Button: Automatic Method
Button States
Owner-Draw Bitmap Button
Automatic Method
Sample
4.3 Subclass
Implementing Subclass
Bitmap Button
Summary
5.5 Slider
Including Slider Control in the Application
Handling Slider Related Messages
iv
Trapping Double Clicking Message
Retrieving the Contents of an Item
Message WM_DESTROY
5.14 Drag-n-Drop
Handling New Messages
New Member Variables and Functions
Node Copy
TVN_BEGINDRAG
WM_MOUSEMOVE
WM_LBUTTONUP
v
Using Animate Control and Progress Control
Timer
Custom Resource
Sample Implementation
Summary:
6.4 Sizes
Initial Size
Dialog Box Unit
Tracking Size and Maximized Size
Sample
Summary:
vi
CHAPTER 7. COMMON DIALOG BOXES
7.1 File Open and Save Dialog Box
Implementing a Standard File Open Dialog Box
Structure OPENFILENAME
File Extension Filter
Retrieving File Names
File Open
File Save
Summary:
vii
CHAPTER 8. DC, PEN, BRUSH AND PALETTE
8.0 Device Context & GDI Objects
Situation
Device Context
GDI Objects
Obtaining DC
Using DC with GDI Objects
8.1 Line
Creating Pen
Drawing Mode
Storing Data
Recording One Line
8.3 Curve
Summary:
CHAPTER 9. FONT
viii
9.1 Outputting Text Using Different Fonts
9.7 One Line Text Editor, Step 4: Caret Moving & Cursor Shape
New Functions
Moving Caret Using Keyboard
Moving Caret Using Mouse
Cursor Shape
Handling WM_LBUTTONDOWN to Move Caret
9.9 One Line Text Editor, Step 6: Cut, Copy and Paste
Global Memory
Clipboard Funcitons
Deleting Selected Text
Message Handlers for Cut, Copy Paste Commands
Summary:
ix
Sample 10.1\GDI
Sample 10.1-2\GDI
x
Summary
11.4 Tracker
Implementing Tracker
Moving and Resizing Tracker
Customizing Cursor Shape
New Tool
If Mouse Clicking Doesn’t Hit the Tracker
If Mouse Clicking Hits the Tracker
11.6 Region
Basics
Region Creation
Using Region
Sample
11.7 Path
Basics
Path & Region
Sample 11.7-1\GDI
Obtaining Path
Sample 11.7-2\GDI
xi
Clipboard DIB Format
Preparing DIB Data
Cut & Copy
Paste
Summary
Summary
xii
13.2 Creating Applications without Using Document/View Structure
How Application, Document and View Are Bound Together
Creating Window
Sample 13.2\Gen
Excluding Classes from Build
13.9 Z-Order
13.10 Hook
Hook Installation
System Wide Hook
Variables in DLL
Defining Data Segment
DLL Implementation
Sample 13.6\Hook
xiii
13.12 Memory Sharing Among Processes
Problem with Global Memory
File Mapping
File Mapping Functions
Samples
Summary
xiv
Functions Implementing Comparisons
Using Parameter to Find an Item
Comparing Two Items by File Names
Notification LVN_COLUMNCLICK
Summary
xv
Program Manager
Summary
Summary
INDEX
xvi
Chapter 1. Tool Bar and Dialog Bar
Chapter 1
T
ool bar and dialog bar are used extensively in all types of applications. They provide users with a
better way of executing application commands. Generally a tool bar comprises a series of buttons;
each button represents a specific command. A command implemented by the tool bar can be linked
directly to a menu command, in which case the two items share a same command ID. Both menu
and tool bar handle WM_COMMAND message for executing commands. Besides, they also handle
UPDATE_COMMAND_UI message to set the state of a button or a menu item. In fact, this message is very
effective in enabling, disabling, and setting checked or unchecked state for a command.
While tool bar usually contains bitmap buttons, dialog bar can include many other type of controls that
can be used in a dialog box, such as edit control, spin control, etc. Both tool bar and dialog bar can be
implemented either as floated or docked, this gives users more choices in customizing the user interface of
an application.
In MFC, classes that can be used to implement the tool bar and dialog bar are CToolBar and
CDialogBar respectively. Both of them are derived from class CControlBar, which implements bar
creation, command message mapping, control bar docking and floating (both tool bar and dialog bar are
called control bar). Besides the default attributes, class CToolBar further supports bitmap button creation,
automatic size adjustment for different states (docked or floated). A dialog bar can be treated as a dialog
box (There is one difference here: a dialog bar can be either docked or floated, a dialog box does not have
this feature): its implementation is based on a dialog template; all the common controls supported by dialog
box can also be used in a dialog bar; their message mapping implementations are exactly the same.
A standard SDI or MDI application created by Application Wizard will have a default dockable tool
bar. From now on we will discuss how to add extra tool bars and dialog bars, how to implement message
mapping for the controls contained in a control bar, and how to customize their default behavior.
1
Chapter 1. Tool Bar and Dialog Bar
The Application Wizard does an excellent job in adding a very powerful tool bar. Nevertheless, as a
programmer, we are kept from knowing what makes all these happen. If we need to make changes to the
default tool bar (for example, we want it to be docked to the bottom border instead of top border at the
beginning), which part of the source code should we modify? Obviously, we need to understand the
essentials of tool bar implementation in order to customize it.
Like menu, generally tool bar is implemented in the mainframe window. When creating a mainframe
menu, we need to prepare a menu resource, use class CMenu to declare a variable, then use it to load the
menu resource. Creating a tool bar takes similar steps: we need to prepare a tool bar resource, use class
CToolBar to declare a variable, which can be used to load the tool bar resource. After the tool bar resource
is loaded successfully, we can call a series of member functions of CToolBar to create the tool bar and
customize its styles.
After creating a standard SDI or MDI application using Application Wizard (with “Docking toolbar”
check box checked in step 4, see Figure 1-1), we will find that a CToolBar type variable is declared in class
CMainFrame:
Figure 1-1: Let Application Wizard add a default dockable tool bar
The newly declared variable is m_wndToolBar. By tracing this variable, we will find out how the tool
bar is implemented.
2
Chapter 1. Tool Bar and Dialog Bar
1) To make the tool bar dockable, we need to call function CToolBar::EnableDocking(…) and pass
appropriate flags to it indicating which borders the tool bar could be docked (We can make the tool bar
dockable to all four borders, or only top border, bottom border, etc.)
1) To dock the tool bar, we need to call function CMainFrame::DockControlBar(…). If we have more than
one tool bar or dialog bar, this function should be called for each of them.
We need above five steps to implement a tool bar and set its attributes.
Message Mapping
Since tool bars are used to provide an alternate way of executing commands, we need to implement
message mapping for the controls contained in a tool bar. This will allow the message handlers to be called
automatically as the user clicks a tool bar button. The procedure of implementing message mapping for a
tool bar control is exactly the same with that of a menu item. In MFC, this is done through declaring an
afx_msg type member function and adding macros such as ON_COMMAND and ON_UPDATE_COMMAND_UI.
The message mapping could be implemented in any of the four default classes derived from CWinApp,
CFrameWnd, CView and CDocument. Throughout this book, we will implement most of the message
mappings in document. This is because for document/view structure, document is the center of the
application and should be used to store data. By executing commands within the document, we don’t bother
to obtain data from other classes from time to time.
The following lists necessary steps of implementing message mapping:
Most of the time message mapping could be implemented through using Class Wizard. In this case we
only need to select a command ID and confirm the name of message handler. Although Class Wizard does
an excellent job in implementing message mapping, sometimes we still need to add it manually because
Class Wizard is not powerful enough to handle all cases.
3
Chapter 1. Tool Bar and Dialog Bar
1) Execute Insert | Resource… command from the menu (or press CTRL+R keys). We will be prompted
to select resource type from a dialog box. If we highlight “toolbar” node and click the button labeled
“New”, a new blank tool bar resource “IDR_TOOLBAR1” will be added to the project. Since default ID
doesn’t provide us much implication, usually we need to modify it so that it can be easily understood.
In the samples, the newly added tool bar resource ID is changed to IDR_COLOR_BUTTON. This can be
implemented by right clicking on “IDR_TOOLBAR1” node in WorkSpace window, and selecting
“Properties” item from the popped up menu. Now a property sheet whose caption is “Toolbar
properties” will pop up, which contains an edit box that allows us to modify the resource ID of the tool
bar.
1) Using the edit tools supplied by the Developer Studio, add four buttons to the tool bar, paint bitmaps
with red, green, blue and yellow colors, change their IDs to ID_BUTTON_RED, ID_BUTTON_GREEN,
ID_BUTTON_BLUE, ID_BUTTON_YELLOW. The tool bar bitmap window could be activated by double
clicking on the tool bar IDs contained in the WorkSpace window, the graphic tools and color can be
picked from “Graphics” and “Colors” windows. If they are not available, we can enable them by
customizing the Developer Studio environment by executing Tools | Customize… command from the
menu.
The new variable is m_wndColorButton, it is added right after other two variables that are used to
implement the default tool bar and status bar.
Next, we can open file “MainFrm.cpp” and go to function CMainFrame::OnCreateClient(…). In
Developer Studio, the easiest way to locate a member function is to right click on the function name in
“WorkSpace” window, then select “Go to Definition” menu item from the popped up menu. Let’s see how
the default tool bar is created:
……
if (!m_wndToolBar.Create(this) ||
!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
{
TRACE0("Failed to create toolbar\n");
return -1;
}
……
Function CToolBar::Create(…) is called first to create the tool bar window. Then
CToolBar::LoadToolBar(…) is called to load the bitmaps (contained in tool bar resource IDR_MAINFRAME).
When calling function CToolBar::Create(…), we need to specify the parent window of the tool bar by
providing a CWnd type pointer (Generally, a tool bar must be owned by another window). Because this
function is called within the member function of class CMainFrame, we can use “this” as the pointer of
parent window.
The following code fragment shows how the styles of the default tool bar are set:
m_wndToolBar.SetBarStyle
(
m_wndToolBar.GetBarStyle() | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC
);
Function CToolBar::SetBarStyle(…) sets tool bar’s styles, which can be a combination of different
style flags using bit-wise OR operation. Because we do not want to lose the default styles, first function
4
Chapter 1. Tool Bar and Dialog Bar
CToolBar::GetBarStyle() is called to retrieve the default tool bar styles, then new styles are combined
with the old ones using bit-wise OR operation. In the above code fragment, three new styles are added to
the tool bar: first, flag CBRS_TOOLTIPS will enable tool tips to be displayed when the mouse cursor passes
over a tool bar button and stay there for a few seconds; second, flag CBRS_FLYBY will cause the status bar to
display a flyby about this button (For details of tool tip and flyby, see section 1.11); third, flag
CBRS_SIZE_DYNAMIC will allow the user to dynamically resize the tool bar, if we do not specify this style,
the dimension of the tool bar will be fixed.
The following statement enables a dockable tool bar:
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
Function CToolBar::EnableDocking(…) makes the tool bar dockable. Here, flag CBRS_ALIGN_ANY
indicates that the tool bar may be docked to any of the four boarders of the frame window. We may change
it to CBRS_ALIGN_TOP, CBRS_ALIGN_BOTTOM, CBRS_ALIGN_LEFT, or different combinations of these flags,
whose meanings are self-explanatory.
The dockable tool bar still can’t be docked if the frame window does not support this feature. We must
call function CFrameWnd::EnableDocking(…) to support docking in the frame window and call
CFrameWnd::DockControlBar(…) for each control bar to really dock it. The following code fragment shows
how the two functions are called for the default tool bar:
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_wndToolBar);
if (!m_wndToolBar.Create(this) ||
!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))
{
TRACE0("Failed to create toolbar\n");
return -1;
}
if (!m_wndColorButton.Create(this) ||
!m_wndColorButton.LoadToolBar(IDR_COLOR_BUTTON))
{
if (!m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators(indicators,
sizeof(indicators)/sizeof(UINT)))
{
TRACE0("Failed to create status bar\n");
return -1;
}
m_wndToolBar.SetBarStyle(m_wndToolBar.GetBarStyle() |
CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC);
m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);
m_wndColorButton.SetBarStyle(m_wndColorButton.GetBarStyle() |
CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC);
m_wndColorButton.EnableDocking(CBRS_ALIGN_ANY);
EnableDocking(CBRS_ALIGN_ANY);
DockControlBar(&m_wndToolBar);
5
Chapter 1. Tool Bar and Dialog Bar
DockControlBar(&m_wndColorButton);
return 0;
}
By compiling and executing the sample application at this point, we can see that the tool bar has been
created. The tool bar can be docked to one of the four borders of the mainframe window or be floated. If we
dock the tool bar to either left or right border, we will see that the tool bar will automatically have a vertical
layout. This feature is supported by class CToolBar, we don’t need to add any line of code in order to have
it.
}
break;
}
{
……
}
There are many types of messages, so parameter message (second parameter of the above function)
could be any of the predefined values. If we want to trap mouse clicking events on the tool bar buttons, we
need to handle WM_COMMAND message. We can see that within the WM_COMMAND case of switch statement in
6
Chapter 1. Tool Bar and Dialog Bar
the above example, parameter wParam is checked (It holds WPARAM parameter of the message). By
comparing it with the IDs of our buttons, we are able to find out which command is being executed by the
user.
MFC handles Windows message in a different way. Because MFC applications are built upon
classes, it is more convenient to handle messages within class member functions instead of one big callback
function. In MFC, this is achieved through message mapping: we can implement the functions that will be
used to execute commands, and use macros defined in MFC to direct the messages into these member
functions.
As mentioned before, doing message mapping generally takes three steps: declaring afx_msg type
member functions, using ON_COMMAND and ON_UPDATE_COMMAND_UI macros to implement mappings, and
implementing the member functions.
For WM_COMMAND type message, the message handling functions do not have any parameters and should
return void type value (For other type of messages, the format of the functions may be different). The
message mapping can be implemented by using ON_COMMAND macro, which has the following format:
For example, if we have a member function OnButtonRed() in class CBarDoc, and we want to map
WM_COMMAND message to this function when the user clicks red button (whose ID is ID_BUTTON_RED), we can
implement message mapping as follows:
BEGIN_MESSAGE_MAP(CBarDoc, CDocument)
ON_COMMAND(ID_BUTTON_RED, OnButtonRed)
END_MESSAGE_MAP()
Message mapping macros must be done between BEGIN_MESSAGE_MAP and END_MESSAGE_MAP. Please
note that if we want member functions of other classes to receive the same message, we must implement
message mapping for each class separately.
Class Wizard is designed to help us deal with message mapping. It provides us with a way of adding
message handlers very easily: all we need to do is picking up messages and confirming the names of the
member functions. The following descriptions list necessary steps of adding a message handler for button
ID_BUTTON_RED in class CBarDoc through using Class Wizard (also see Figure 1-2):
1) In the Developer Studio, execute command View | ClassWizard… (or press CTRL+W keys).
1) From the popped up property sheet, click “Message Maps” tab (if the current page is not “Message
Maps”).
1) From “Class name” combo box, select “CBarDoc” if it is not the default class name (If the file being
edited is “BarDoc.cpp”, the default class name should be “CBarDoc”).
4) From “Object Ids” window, highlight “ID_BUTTON_RED”.
1) From “Messages” window, highlight “COMMAND”.
1) Click “Add Function” button.
1) From the popped up dialog box, confirm the function name that will be used as the message handler
(we may change the name according to our preference).
1) The function will be added to window “Member functions”. Now we can repeat steps 4 through 7 to
add message handlers for other IDs. When finished, we need to click “OK” button.
After dismissing the Class Wizard, the functions just added will be shown in the Developer Studio. By
default, the message handlers are empty at the beginning, and we can add anything we want. For example,
if we want a message box to pop up telling the color of the button when the user clicks it with mouse, we
may implement the ID_BUTTON_RED message hander as follows:
void CBarDoc::OnButtonRed()
{
AfxMessageBox("Red");
}
7
Chapter 1. Tool Bar and Dialog Bar
Step 3: Select
CBarDoc class
Step 2: Click on
Message Maps
Step 6: Click
tab
Add Function
button
Step 5: Select
Step 4: Highlight COMMAND
ID_BUTTON_RED
When finished,
click OK button
Similarly, we can write message handlers for other three buttons. In sample application 1.1-2\Bar,
message handlers for all the four buttons on tool bar IDR_COLOR_BUTTON are implemented. If the user clicks
any of the four buttons, a message box will pop up telling its color.
8
Chapter 1. Tool Bar and Dialog Bar
The function has only one parameter, which is the pointer to a CCmdUI object. Class CCmdUI handles
user-interface updating for tool bar buttons and menu items. Some most commonly used member functions
are listed in the following table:
Function Usage
CCmdUI::Enable(…) Enable or disable a control
CCmdUI::SetCheck(… Set or remove the check state of a control
)
CCmdUI::SetRadio(… Set check state of a control, remove check state of all other controls in the
)
group
From time to time, the operating system will send user-interface update command messages to the
application, if there exists macros implementing the above-mentioned message mapping for any control
contained in the tool bar, the control’s state can be set within the corresponding message handler.
For a concrete example, if we want to disable ID_BUTTON_RED button under certain situations, we can
declare a member function OnUpdateButtonRed(…) in class CBarDoc as follows (of course, we can also
handle this message in other three classes):
BEGIN_MESSAGE_MAP(CBarDoc, CDocument)
ON_UPDATE_COMMAND_UI(ID_BUTTON_RED, OnUpdateButtonRed)
END_MESSAGE_MAP()
Usually we use a Boolean type variable as the flag, which represents “certain situations” in the above
if statement. We can toggle this flag in other functions, this will cause the button state to be changed
automatically. By doing this, the button’s state is synchronized with the variable.
To set check state for a button, all we need to do is calling function CCmdUI::SetCheck(…) instead of
CCmdUI::Enable(…) in the message handler.
Sample
Sample 1.2\Bar demonstrates how to make the four color buttons behave like radio buttons. At any
time, one and only one button will be set to checked state (it will recess and give the user an impression
that it is being held down).
To implement this feature, a member variable m_uCurrentBtn is declared in class CBarDoc. The value
of this variable could be set to any ID of the four buttons in the member functions (other values are not
allowed). In the user-interface update command message handler of each button, we check if the value of
m_nCurrentBtn is the same with the corresponding button’s ID. If so, we need to set check for this button,
otherwise, we remove its check.
9
Chapter 1. Tool Bar and Dialog Bar
The following lists the steps of how to implement these message handlers:
1) Open file “BarDoc.h”, declare a protected member variable m_uCurrentBtn in class CBarDoc:
2) Go to file “BarDoc.cpp”, in CBarDoc’s constructor, initialize m_uCurrentBtn red button’s resource ID:
CBarDoc::CBarDoc()
{
m_uCurrentBtn=ID_BUTTON_RED;
}
This step is necessary because we want one of the buttons to be checked at the beginning.
3) Implement UPDATE_COMMAND_UI message mapping for four button IDs. This is almost the same with
adding ON_COMMAND macros, which could be done through using Class Wizard. The only difference
between two implementations is that they select different message types from “Message” window (see
step 5 previous section). Here we should select “UPDATE_COMMAND_UI” instead of
“COMMAND”.
1) Implement the four message handlers as follows:
One thing to mention here is that CCmdUI has a public member variable m_nID, which stores the ID of
the control that is about to be updated. We can compare it with variable CBarDoc::m_uCurrentBtn and set
the appropriate state of the control.
With the above implementation, the red button will be checked from the beginning. We need to change
the value of variable m_uCurrentBtn in order to check another button. This should happen when the user
clicks on any of the four buttons, which will cause the application to receive a WM_COMMAND message. In the
sample, this will cause the message handlers CBarDoc::OnButtonRed(), CBarDoc::OnButtonBlue()… to be
called. Within these member functions, we can change the value of m_uCurrentBtn to the coresponding
command ID in order to check that button:
void CBarDoc::OnButtonBlue()
{
m_uCurrentBtn=ID_BUTTON_BLUE;
}
void CBarDoc::OnButtonGreen()
{
m_uCurrentBtn=ID_BUTTON_GREEN;
}
void CBarDoc::OnButtonRed()
{
10
Chapter 1. Tool Bar and Dialog Bar
m_uCurrentBtn=ID_BUTTON_RED;
}
void CBarDoc::OnButtonYellow()
{
m_uCurrentBtn=ID_BUTTON_YELLOW;
}
The message box implementation is removed here. By executing the sample application and clicking
on any of the four color buttons, we will see that at any time, one and only one button will be in the
checked state.
CBarDoc::CBarDoc()
{
m_bBtnRed=FALSE;
m_bBtnGreen=FALSE;
m_bBtnBlue=FALSE;
m_bBtnYellow=FALSE;
}
Two types of message handlers (altogether eight member functions) are rewritten. The following
shows the implementation of two member functions for button ID_BUTTON_RED:
void CBarDoc::OnButtonRed()
{
m_bBtnRed=!m_bBtnRed;
}
If we execute the application at this point, we will see that the four color buttons behave like check
boxes.
11
Chapter 1. Tool Bar and Dialog Bar
Function CButton::SetButtonInfo(…)
Although this is a simple way to implement “check box” buttons, sometimes it is not efficient. Suppose
we have ten buttons that we expect to behave like check boxes, for every button we need to add a Boolean
type variable and implement a UPDATE_COMMAND_UI message handler. Although this is nothing difficult, it is
not the most efficient way of doing it.
Class CToolBar has a member function that can be used to set the button styles. The function allows us
to set button as a push button, separator, check box, or the start of a group of check boxes. We can also use
it to associate an image with a button contained in the tool bar. The following is the format of this function:
To use this function, we need to provide the information about the button, the style flags, and the
image information. Parameter nIndex indicates which button we are gong to customize. It is a zero-based
index, and button 0 is the left most button or separator on the tool bar (a separator is also considered a
button). Parameter nID specifies which command ID we want to associate with this button. Parameter
nStyle could be one of the following values, which indicates button’s style:
Flag Meaning
TBBS_BUTTON push button
TBBS_SEPARATOR separator
TBBS_CHECKBOX check box
TBBS_GROUP start of a group
TBBS_CHECKGROUP start of a check box group
The last parameter iImage indicates which image will be used to create the bitmap button. This is also
a zero-based number, which indicates the image index of the tool bar resource. In our case, the tool bar
resource contains four images, which are simply painted red, green, blue and yellow. The images are
indexed according to their sequence, which means the red image is image 0, the green image is image 1,
and so on.
When we create a tool bar resource, it seems that a button’s command ID and the associated image are
fixed from the beginning. Actually both of them can be modified through calling the above function. We
can assign any command ID and image to any button. Also, we can change a button to a separator. In a
normal application, there is no need to call this function, so the button’s command ID and image are set
according to the tool bar resource.
We have no intention of changing the default arrangement of the buttons. What we need to do here is
modifying the button’s style, which is set to TBBS_BUTTON by default. Sample 1.3-2\Bar demonstrates how
to modify this style. It is based on sample 1.3-1\Bar.
To implement the new sample, first we need to delete four old UPDATE_COMMAND_UI message handlers.
This can be done through using Class Wizard, which will delete the declaration of message handlers and
the message mapping macros. We need to remove the implementation of the functions by ourselves.
We can set the button’s style after the tool bar is created. This can be implemented in function
CMainFrame::OnCreate(…). The following portion of this function shows what is added in the sample
application:
With this modification, all four buttons will behave like check boxes. Similarly, if we want them to
behave like push buttons, we just need to use style flag TBBS_BUTTON.
The state of a button can be retrieved by another member function of CToolBar. It lets us find out a
button’s command ID, and current state (checked or unchecked, the associate image):
12
Chapter 1. Tool Bar and Dialog Bar
At any time, we can call this function to obtain the information about buttons. No additional variable is
needed to remember their current states.
The method discussed here can also be used to create radio buttons. In order to do so, we need to use
TBBS_CHECKGROUP instead of TBBS_CHECKBOX flag when calling function CToolBar::SetButtonInfo(…).
Contiguous IDs
In the previous sections, we implemented message handler for every control. If we want to handle both
WM_COMMAND and UPDATE_COMMAND_UI messages, we need to add two message handlers for each control.
Although Class Wizard can help us with function declaration and adding mapping macros, we still need to
type in code for every member function. If we have 20 buttons on the tool bar, we may need to implement
40 message handlers. So here the question is, is there a more efficient way to implement message mapping?
The answer is yes. As long as the button IDs are contiguous, we can write a single message handler
and direct all the messages to it. To implement this, we need to use two new macros: ON_COMMAND_RANGE
and ON_UPDATE_COMMAND_UI_RANGE, which correspond to ON_COMMAND and ON_UPDATE_COMMAND_UI
respectively. The formats of the two macros are:
When we create tool bar resource, the control IDs are generated contiguously according to the
sequence of creation. For example, if we first create the blue button, then the green button, the two IDs will
have the following relationship:
ID_BUTTON_GREEN = ID_BUTTON_BLUE+1
Modifying an ID
Sometimes we don’t know if the IDs of the tool bar buttons have contiguous values, because most of
the time we use only symbolic IDs and seldom care about the actual values of them. If the IDs do not meet
our requirement and we still want to use the above message mapping macros, we need to modify the ID
values by ourselves.
By default, all the resource IDs are defined in file “resource.h”. Although we could open it with a text
editor and make changes, there is a better way to do so. First, an ID value could be viewed in the Developer
Studio by executing View | Resource symbols… command. This command will bring up a dialog box that
contains all the resource IDs used by the application (Figure 1-3).
If we want to make change to any ID value, first we need to highlight that ID, then click the button
labeled “Change…”. After that, a “Change Symbol” dialog box will pop up, if the ID is used for more than
one purpose, we need to select the resource type in “Used by” window (This happens when this ID is used
for both command ID and string ID, in which case the string ID may be used to implement flyby and tool
tip. See Figure 1.9). In our sample, there is only one type of resource that uses the button IDs, so we do not
need to make any choice. Now click “View Use” button (Figure 1-4), which will bring up “Toolbar Button
Properties” property sheet. Within “General” page, we can change the ID’s value by typing in a new
number in the window labeled “ID”. For example, if we want to change the value of ID_BUTTON_RED to
13
Chapter 1. Tool Bar and Dialog Bar
32770, we just need to type in an equal sign and a number after the symbolic ID so that this edit window
has the following contents (Figure 1-5):
ID_BUTTON_RED=32770
R e so u rc e ID
ID v a lu e
F ig u r e 1 -3 . V ie w re so u rc e ID s a n d th e ir v a lu e s
With this method, we can easily change the values of four resource IDs (ID_BUTTON_RED,
ID_BUTTON_GREEN, ID_BUTTON_BLUE, ID_BUTTON_YELLOW) and make them contiguous. After this we can
map all of them to a single member function instead of implementing message handlers for each ID.
Unfortunately, Class Wizard doesn’t do range mappings, so we have to implement it by ourselves.
Sample 1.4\Bar demonstrates how to implement this kind of mapping. It is based upon sample 1.2\Bar,
which already contains the default message mapping macros:
BEGIN_MESSAGE_MAP(CBarDoc, CDocument)
//{{AFX_MSG_MAP(CBarDoc)
ON_COMMAND(ID_BUTTON_BLUE, OnButtonBlue)
ON_COMMAND(ID_BUTTON_GREEN, OnButtonGreen)
ON_COMMAND(ID_BUTTON_RED, OnButtonRed)
ON_COMMAND(ID_BUTTON_YELLOW, OnButtonYellow)
ON_UPDATE_COMMAND_UI(ID_BUTTON_BLUE, OnUpdateButtonBlue)
ON_UPDATE_COMMAND_UI(ID_BUTTON_GREEN, OnUpdateButtonGreen)
ON_UPDATE_COMMAND_UI(ID_BUTTON_RED, OnUpdateButtonRed)
ON_UPDATE_COMMAND_UI(ID_BUTTON_YELLOW, OnUpdateButtonYellow)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
The following lists the necessary steps of changing message mapping from the original implementation
to using contiguous IDs:
1) Delete the above eight message handlers along with the message mapping macros added by the Class
Wizard.
1) Declare two new functions that will be used to process WM_COMMAND and ON_COMMAND_RANGE messages
in class CBarDlg as follows:
3) Open file “BarDoc.cpp”, find BEGIN_MESSAGE_MAP and END_MESSAGE_MAP macros of class CBarDoc, add
the message mappings as follows:
BEGIN_MESSAGE_MAP(CBarDoc, CDocument)
//{{AFX_MSG_MAP(CBarDoc)
//}}AFX_MSG_MAP
ON_COMMAND_RANGE(ID_BUTTON_RED, ID_BUTTON_YELLOW, OnButtons)
14
Chapter 1. Tool Bar and Dialog Bar
Please compare the above code with the implementation in section 1.2. When we ask Class Wizard to
add message mapping macros, it always adds them between //{{AFX_MSG comments. Actually, these
comments are used by the Class Wizard to locate macros. To distinguish between the work done by
ourselves and that done by Class Wizard, we can add the statements outside the two comments.
m_wndToolBar.SetBarStyle
(
m_wndToolBar.GetBarStyle() | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC
);
This allows the size of a tool bar to change dynamically. When the bar is floating or docked to top or
bottom border of the client area, the buttons will have a horizontal layout. If the bar is docked to left or
right border, they will have a vertical layout.
Sometimes we may want to fix the size of the tool bar, and disable the dynamic layout feature. This
can be achieved through specifying CBRS_SIZE_FIXED flag instead of CBRS_SIZE_DYNAMIC flag when calling
function CToolBar::SetBarStyle(…).
15
Chapter 1. Tool Bar and Dialog Bar
By default, the buttons on the tool bar will have a horizontal layout. If we fix the size of the tool bar,
its initial layout will not change throughout application’s lifetime. This will cause the tool bar to take up too
much area when it is docked to either left or right border of the client area (Figure 1-6).
Instead of fixing the layout this way, we may want to wrap the tool bar from the second button, so the
width and height of the tool bar will be roughly the same at any time (Figure 1-7).
Figure 1-7. Fixing the layout this way will let a tool bar with
fixed size take less area when it is docked to any border
We can call function CToolBar::SetButtonStyle(…) to implement the wrapping. This function has
been discussed in section 1.3. However, there we didn’t discuss the flag that can be used to wrap the tool
bar from a specific button. This style is TBBS_WRAPPED, which is not documented in MFC.
Sample 1.5\Bar is based on sample 1.4\Bar that demonstrates this feature. The following shows the
changes made to the original CMainFrame::OnCreate(…) function:
1) Replace CBRS_SIZE_DYNAMIC with CBRS_SIZE_FIXED when setting the tool bar style. The following
statement shows this change:
m_wndColorButton.SetBarStyle
(
m_wndColorButton.GetBarStyle() | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_FIXED
);
m_wndColorButton.SetButtonStyle
(
16
Chapter 1. Tool Bar and Dialog Bar
1, m_wndColorButton.GetButtonStyle(1) | TBBS_WRAPPED
);
To avoid losing default styles, in the second step, function CToolBar::GetButtonStyle(…) is first
called to retrieve the original styles, which are bit-wise ORed with the new style before calling function
CToolBar::SetButtonStyle(…).
1) Change the blue button to a separator with a width of 150 after the tool bar is created. For this purpose,
the following statement is added to function CMainFrame::OnCreate(…):
Here the first parameter indicates that we want to modify the third button. The second parameter is the
blue button’s resource ID. The fourth parameter specifies the width of the separator. If we compile and
execute the sample at this point, we will see that the blue button does not exist anymore. Instead, a
blank space with width of 150 is added between the third and fourth button. This is the place where we
will create the combo box.
2) Use CComboBox to declare a variable m_wndComboBox in class CMainFrame as follows:
17
Chapter 1. Tool Bar and Dialog Bar
Function CComboBox::Create(…) has four parameters. We must specify combo box’s style, size &
position, parent window, along with the control ID in order to call this function. The following is the format
of this function:
BOOL CComboBox::Create(DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID);
We can use the first parameter to set the styles of a combo box. A combo box can have different styles,
in our sample, we just want to create a very basic drop down combo box (For other types of combo boxes,
see Chapter 5). The following code fragment shows how this function is called within CMainFrame::
OnCreate(…):
……
m_wndColorButton.SetButtonInfo(2, ID_BUTTON_BLUE, TBBS_SEPARATOR, 150);
m_wndColorButton.GetItemRect(2, rect);
rect.bottom=rect.top+150;
if(!m_wndComboBox.Create(WS_CHILD | CBS_DROPDOWN |
CBS_AUTOHSCROLL | WS_VSCROLL | CBS_HASSTRINGS,
rect, &m_wndColorButton, ID_BUTTON_BLUE))
{
return -1;
}
m_wndComboBox.ShowWindow(SW_SHOW);
……
Function CToolBar::GetItemRect(…) is called in the second statement of above code to retrieve the
size and position of the separator. After calling this function, the information is stored in variable rect,
which is declared using class CRect.
A drop down combo box contains two controls: an edit box and a list box. Normally the list box is not
shown. When the user clicks on the drop down button of the combo box, the list box will be shown.
Because the size of the combo box represents its total size when the list box is dropped down (Figure 1-8),
we need to extend the vertical dimension of the separator before using it to set the size of the combo box.
The third statement of above code sets the rectangle’s vertical size to 150. So when our combo box is
dropped down, its width and the height will be roughly the same.
The fourth statement of above code creates the combo box. Here a lot of styles are specified, whose
meanings are listed below:
The above styles are the most commonly used ones for a combo box. For details about this control,
please refer to chapter 5.
18
Chapter 1. Tool Bar and Dialog Bar
Because the blue button will not be pressed to execute command anymore, we use ID_BUTTON_BLUE as
the ID of the combo box. Actually, we can specify any other number so long as it is not used by other
controls.
Finally, we must call function CWnd::ShowWindow(…) and pass SW_SHOW flag to it to show any window
created dynamically.
By compiling and executing the sample at this point, we will see that the blue button has been changed
to a combo box.
Overridden CalcDynamicLayout(…)
{
Change the combo box to button or vice versa if necessary;
CToolBar::CalcDynamicLayout(…);
}
The default implementation of this function is called after the button information is set correctly. By
doing this way, the tool bar can always have the best layout appearance.
19
Chapter 1. Tool Bar and Dialog Bar
We need to know when the tool bar will change from horizontal layout to vertical layout, or vice versa.
This can be judged from the parameters passed to function CControlBar::CalcDynamicLayout(…). Let’s
take a look at the function prototype first:
The function has two parameters, the second parameter dwMode indicates what kind of size is being
retrieved. It can be the combination of many flags, in this section, we need to know only two of them:
Flag Meanings
LM_HORZDOCK The horizontal dock dimension is being retrieved
LM_VERTDOCK The vertical dock dimension is being retrieved
What we need to do in the overridden function is examining the LM_HORZDOCK bit and LM_VERTDOCK bit
of dwMode parameter and setting the button information correspondingly.
To override the member function of CToolBar, we must first derive a new class from it, then
implement a new version of this function in the newly created class. Sample 1.7\Bar demonstrates how to
change the button’s style dynamically, it is based on sample 1.6\Bar.
First, we need to declare the new class, in the sample, this class is named CColorBar:
protected:
CComboBox m_wndComboBox;
//{{AFX_MSG(CColorBar)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
Instead of declaring a CComboBox type variable in class CMainFrame, here we implement the declaration
in the derived class. This is a better implementation because the combo box should be made the child
window of the tool bar. By embedding the variable here, we can make it a protected variable so that it is not
accessible from outside the class.
20
Chapter 1. Tool Bar and Dialog Bar
Three functions are added to change the tool bar’s style. Function CColorBar::AddComboBox()
changes the blue button to a separator and creates the combo box window:
BOOL CColorBar::AddComboBox()
{
CRect rect;
GetItemRect(2, rect);
rect.bottom=rect.top+150;
if
(
!m_wndComboBox.Create
(
WS_CHILD | CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | CBS_HASSTRINGS,
rect,
this,
ID_BUTTON_BLUE
)
)return FALSE;
else return TRUE;
}
This is the same with what we did in function CMainFrame::OnCreate(…) in the previous section. The
only difference is that when creating the combo box within the member function of CMainFrame, the combo
box’s parent window needs to be set to m_wndColorButton. Here, since the combo box variable is
embedded in the parent window’s class, we need to use this pointer to indicate the parent window.
Function CColorBar::ShowComboBox() and CColorBar::HideComboBox() change the combo box to
the button and vice versa. They should be called just before the default layout is about to be carried out:
BOOL CColorBar::ShowComboBox()
{
CRect rect;
SetButtonInfo(2, ID_BUTTON_BLUE, TBBS_SEPARATOR, 150);
if(m_wndComboBox.m_hWnd != NULL)
{
m_wndComboBox.ShowWindow(SW_SHOW);
}
return TRUE;
}
BOOL CColorBar::HideComboBox()
{
SetButtonInfo(2, ID_BUTTON_BLUE, TBBS_BUTTON, 2);
if(m_wndComboBox.m_hWnd != NULL)m_wndComboBox.ShowWindow(SW_HIDE);
return TRUE;
}
Before calling the base class version of this function, we examine LM_HORZDOCK bit of dwMode
parameter, if it is set, we call function CColorBar::ShowComboBox() to change the button to the combo
box. If not, we call function CColorBar::HideComboBox() to change the combo box back to the default
button.
It is relatively easy to use this class: we just need to include the header file of CColorBar class in file
“MainFrm.h”, then change the prototype of m_wndColorBar from CToolBar to CColorBar. Because the
combo box is embedded in CColorBar class, we need to remove variable wndComboBox declared in the
previous section. In function CMainFrame::OnCreate(), instead of creating the combo box by ourselves,
we can just call the member function of CColorBar. Here is how the combo box is created using this new
method:
21
Chapter 1. Tool Bar and Dialog Bar
……
m_wndColorButton.AddComboBox();
m_wndColorButton.ShowComboBox();
……
We can see that the original five statements have been reduced to two statements.
Now we can compile and execute the sample again to see the behavior of the combo box.
……
if
22
Chapter 1. Tool Bar and Dialog Bar
(
!m_wndDialogBar.Create
(
this,
IDD_DIALOG_COLORBAR,
CBRS_BOTTOM | CBRS_TOOLTIPS | CBRS_FLYBY,
IDD_DIALOG_COLORBAR
)
)
{
TRACE0("Failed to create toolbar\n");
return -1;
}
……
Click here to
set styles
Change window
style to “Child”
Don’t let a dialog
bar have border
Figure 1-10. Set dialog bar styles
Combo box
IDC_COMBO
Edit box
Button IDC_BUTTON_A and IDC_EDIT
IDC_BUTTON_B
3) Enable docking by calling function CDialogBar::EnableDocking(…), dock the dialog bar by calling
function CMainFrame::DockControlBar(…):
……
m_wndDialogBar.EnableDocking(CBRS_ALIGN_ANY);
……
DockControlBar(&m_wndDialogBar);
……
Because class CDialogBar is derived from CControlBar, in step 3, when we call function
CDialogBar::EnableDocking(…) to enable docking for the dialog bar, we are actually calling function
CControlBar::EnableDocking(…). This is the same with that of tool bar. Because of this, both tool bar and
dialog bar have the same docking properties.
By compiling and executing the sample at this point, we can see that the dialog bar is implemented,
which is docked to the bottom border at the beginning. We can drag the dialog bar and dock it to other
borders. As we do this, we may notice the difference between dialog bar and tool bar: while the size of the
tool bar will be automatically adjusted when it is docked differently (horizontally or vertically), the size of
dialog will not change under any condition. The reason for this is that a dialog bar usually contains
irregular controls, so it is relatively difficult to adjust its size automatically. By default, dynamic size
adjustment is not supported by class CDialogBar. If we want our dialog bar to support this feature, we need
to override function CDialogBar::CalcDynamicLayout(…).
To prevent a dialog bar from taking up too much area when it is docked to left or right border, we can
put restriction on the dialog bar so that it can only be docked to top or bottom border. To implement this,
23
Chapter 1. Tool Bar and Dialog Bar
we can change the style flag from CBRS_ALIGN_ANY to CBRS_ALIGN_TOP | CBRS_ALIGN_BOTTOM when
calling function CDialogBar::EnableDocking(…) in step 3 discussed above.
protected:
//{{AFX_MSG(MCDialogBar)
afx_msg void OnSize(UINT nType, int cx, int cy);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
Besides constructor, the only things included in this class are two functions. As we already know,
MCDialogBar::CalcDynamicLayout(…) will be used to support dynamic layout. Another afx_msg type
function MCDialogBar::OnSize(…) is a message handler, which will be used to resize the edit control
24
Chapter 1. Tool Bar and Dialog Bar
contained in the dialog bar. By doing this, we can see that whenever the size of the dialog bar is adjusted,
the size of the edit box will also change accordingly. This will let the edit box fit well within the dialog bar.
The new class can be added by opening new files (“.h” and “.cpp” files) and typing in the new class
and function implementations. Then we can execute Project | Add To Project | Files… command to add
the newly created files to the project. However, if we do so, we can not use Class Wizard to add member
variables and functions to the class. In this case, we need to implement message mapping manually. If this
is our choice, we must make sure that DECLARE_MESSAGE_MAP() macro is included in the class, and
BEGIN_MESSAGE_MAP, END_MESSAGE_MAP macros are included in the implementation file (“.cpp” file) so that
the class will support message mapping.
We can also use Class Wizard to add new class. In order to do this, after invoking the Class Wizard,
we can click button labeled “Add Class…” then select “New…” from the popup menu. This will invoke a
dialog box that lets us add a new class to the project. We can type in the new class name, select the header
and implementation file names, and designate base class name. Unfortunately, CDialogBar is not in the list
of base classes. A workaround is that we can select CDialog as the base class, after the class is generated,
delete the unnecessary functions, and change all CDialog keywords to CDialogBar in both header and
implementation files.
Here nType indicates how the window’s size will be changed (is it maximized, minimized…), cx and
cyindicate the new window size.
It is not very difficult to add message mapping macro, we can either add it manually, or ask Class
Wizard to do it for us:
BEGIN_MESSAGE_MAP(MCDialogBar, CDialogBar)
//{{AFX_MSG_MAP(MCDialogBar)
ON_WM_SIZE()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
Please note that we do not need to specify function name when using macro ON_WM_SIZE. Instead, we
must use OnSize to name the message handler of WM_SIZE.
To change a window’s size, we can call function CWnd::MoveWindow(…):
We need to provide new position and size in order to call this function. Because the function is a
member of CWnd, we need to first obtain a pointer to the edit window then use it to call this function.
For controls contained in a dialog box, their window pointers can be obtained by calling function
CWnd::GetDlgItem(…). This function requires a valid control ID:
The function returns a CWnd type pointer. With this pointer, we can call any member functions of CWnd
to retrieve its information or change the properties of the control.
Because we want to set the edit control’s size according to the parent window’s size (dialog bar), we
need to find a way of retrieving a window’s dimension. This can be implemented by calling another
member function of CWnd:
25
Chapter 1. Tool Bar and Dialog Bar
It is very easy to use this function. We can just declare a CRect type variable, and pass its pointer to the
above function when calling it. After this, the position and size of the window will be stored in the variable.
The following shows how message WM_SIZE is handled in the sample:
GetClientRect(rectWnd);
ptrWnd=GetDlgItem(IDC_EDIT);
if(ptrWnd != NULL)
{
ptrWnd->MoveWindow
(
rectWnd.left+15,
rectWnd.top+15,
rectWnd.Width()-30,
rectWnd.Height()-30
);
}
}
We can not use parameter cx and cy to resize the edit control directly because after the dialog bar gets
this information, its layout may change again. The ultimate dimension of the dialog bar depends on both cx,
cy and the layout algorithm. So before adjusting the size of edit control, we have to call
CDialogBar::OnSize(…) first to let the dialog bar adjust its own size, then call CWnd::GetClientRect(…)
to retrieve the final dimension of the dialog bar.
The rest part of this function can be easily understood: we first obtain a window pointer to the edit
control and store it in pointer ptrWnd, then use it to call function CWnd::MoveWindow(…) to resize the edit
control.
Dynamic Layout
Now we need to implement function MCDialogBar::CalcDynamicLayout(…). Like what we did in
section 1.7, here we need to return a custom layout size when the function is called for retrieving either
horizontal or vertical docking size. The following is our layout algorithm: when the bar is docked
horizontally, we set its width to the horizontal size of the mainframe window’s client area, and set its height
to the dialog bar’s floating vertical size; when it is docked vertically, we set its height to the vertical size of
the mainframe window’s client area, and set its width to the dialog bar’s floating horizontal size.
Parameter dwMode of this function is used to tell what types of dimension is be inquired. If either
LM_VERTDOCK or LM_HORZDOCK bit is set, we need to return a custom docking size. In this case, we can use
another bit LM_HORZ to check if the dialog bar is docked horizontally or vertically. If this bit is set, the
horizontal docking size is being inquired, otherwise the vertically docking size is being inquired.
The floating size can be obtained from a public variable: CDialogBar::m_sizeDefault. By default,
this variable is initialized to the dialog template size, and is updated when the user changes the size of the
dialog bar when it is floating. So this variable always represents the floating size of the dialog bar.
The following is the implementation of this function:
ptrWnd=(CMainFrame *)(AfxGetApp()->m_pMainWnd);
ptrWnd->GetClientRect(rect);
if((dwMode & LM_VERTDOCK) || (dwMode & LM_HORZDOCK))
{
size.cx=(dwMode & LM_HORZ) ? rect.Width():m_sizeDefault.cx;
size.cy=(dwMode & LM_HORZ) ? m_sizeDefault.cy:rect.Height();
return size;
}
return CDialogBar::CalcDynamicLayout(nLength, dwMode);
26
Chapter 1. Tool Bar and Dialog Bar
First, we obtain the dimension of mainframe window’s client area. For this purpose, first a window
pointer to the mainframe window is obtained, then function CMainFrame::GetClientRect(…) is called to
retrieve its dimension. Here the pointer to the mainframe window is obtained from a public member
variable of class CWinApp. In MFC, every application has a CWinApp derived class, which contains a pointer
m_pMainWnd pointing to the mainframe window. For any application, the pointer to the CWinApp object can
be obtained anywhere in the program by calling function AfxGetApp(). Using this method, we can easily
find the mainframe window of any MFC application.
Because the application supports status bar and tool bar, part of its client area may be covered by the
control bar. So we need to deduct the covered area when calculating the dimension of the client area. For
this purpose, in CMainFrame, function CWnd::GetClientRect(…) is overridden. Within the overridden
function, the client area is adjusted if either the status bar or the tool bar is present:
CFrameWnd::GetClientRect(lpRect);
if(m_wndToolBar.IsWindowVisible())
{
m_wndToolBar.GetClientRect(rect);
lpRect->bottom-=rect.Height();
}
if(m_wndStatusBar.IsWindowVisible())
{
m_wndStatusBar.GetClientRect(rect);
lpRect->bottom-=rect.Height();
}
}
27
Chapter 1. Tool Bar and Dialog Bar
Tool tip
Flyby
Figure 1-13. Add flyby and tool tip string for tool bar control
For dialog bar, we don’t have the place to input this string in the property sheet. So we need to edit
string resource directly. This can also be implemented very easily. In the Developer Studio, if we execute
command Insert | Resource…(or press CTRL+R keys), an “Insert Resource” dialog box will pop up. To
add a string resource, we need to highlight node “String Table” and press “New” button. After this, a new
window with the string table will pop up. By scrolling to the bottom of the window and double clicking an
empty entry, a “String Properties” property sheet will pop up, which can be used to add a new string
resource (Figure 1-14).
28
Chapter 1. Tool Bar and Dialog Bar
Sample 1.10\Bar demonstrates how to implement flybys and tool tips. It is based on sample 1.8\Bar.
Actually, the only difference between the two projects is that some new string resources are added to
sample 1.10\Bar. In sample 1.10\Bar, following string resources are added:
After executing this sample, we can put the mouse cursor over the controls contained in the tool bar or
dialog bar. By doing this, the flyby and tool tip will pop up after the cursor stays there for a short while.
This function has three parameters: the first one is a pointer to the control bar that we want to turn on
or off. The second is a Boolean type variable. If it is TRUE, the control bar will be turned on; if it is
FALSE, the control bar will be turned off. The third parameter is also Boolean type, it specifies if this
action should be taken immediately.
Because we need to know the current state of the control bar (on or off) to determine whether we
should hide or show it, we need to call another member function of CWnd to see if the control bar is
currently hidden:
BOOL CWnd::IsWindowVisible( );
This function returns a TRUE or FALSE value, from which we know the control bar’s current state.
29
Chapter 1. Tool Bar and Dialog Bar
Sample 1.11\Bar supports this new feature, it is based on sample 1.10\Bar. For both tool bar and dialog
bar, a new command is added to the main menu, which can be used to toggle the control bar between on
and off state.
The following shows necessary steps for implementing the new commands:
1) Add two menu items View | Color Bar and View | Dialog Bar to the mainframe menu IDR_MAINFRAME,
whose IDs are ID_VIEW_COLORBAR and ID_VIEW_DIALOGBAR respectively.
1) Use Class Wizard to add WM_COMMAND and UPDATE_COMMAND_UI type message handlers for the above
IDs in class CMainFrame. The newly added functions are CMainFrame::OnViewColorBar(),
CMainFrame:: OnViewDialogBar(), CMainFrame::OnUpdateViewColorBar(…) and CMainFrame::
OnUpdateViewDialogBar(…).
1) Implement four WM_COMMAND type message handlers. The function used to handle WM_COMMAND message
for command ID_VIEW_COLORBAR is implemented as follows:
void CMainFrame::OnViewColorBar()
{
BOOL bShow;
bShow=m_wndColorButton.IsWindowVisible() ? FALSE:TRUE;
ShowControlBar(&m_wndColorButton, bShow, FALSE);
}
To indicate the status of control bars, it is desirable to check the corresponding menu item when the
control bar is available, and remove the check when it becomes hidden. This is exactly the same with the
behavior of the default tool bar IDR_MAINFRAME. In the sample, the menu item states are handled by
trapping message UPDATE_COMMAND_UI and the check is set or removed by calling function
CCmdUI::SetCheck(…). The following is the implementation of one of the above message handlers (see
Chapter 2 for more about menu customization):
It is exactly the same with setting or removing check for a tool bar button.
With the above implementations, the application can be executed again. We can dismiss the control bar
either by executing menu command or by clicking “X” button located at the upper-right corner of the
control bar when it is floating. In both cases, the control bar can be turned on again by executing
corresponding menu command. We can also dock or float the control bar and turn it off, and see if the
original state will remain unchanged after it is turned on later.
Summary:
1. To add an extra tool bar, first we need to add a tool bar resource, then declare a CToolBar type variable
in CMainFrame class. Within function CMainFrame::OnCreate(…), we can call the member functions of
CToolBar and CMainFrame to create the tool bar window and set docking styles.
1. The dialog bar can be added in the same way, however, we need to use dialog-template resource and
class CDialogBar to implement it.
1. We can trap WM_COMMAND message for executing command and trap UPDATE_COMMAND_UI for updating
button state. Use ON_COMMAND and ON_UPDATE_COMMAND_UI macros to implement message mapping.
1. We can use ON_COMMAND_RANGE and ON_UPDATE_COMMAND_UI_RANGE macros to map a contiguous range
of command IDs to one member function.
1. When the size of a tool bar is fixed, we can set TBBS_WRAPPED flag for a button to let the tool bar wrap
after that button.
1. To customize the dynamic layout feature of tool bar and dialog bar, we need to override function
CalcDynamicLayout(…).
1. To add a combo box to a tool bar, first we need to set a button to separator with specified width, then
we need to create the combo box dynamically.
30
Chapter 1. Tool Bar and Dialog Bar
1. Flyby and tool tip can be activated by setting CBRS_TOOLTIP and CBRS_FLYBY flags then preparing a
string resource using the exact same ID with the control.
1. To toggle control bar on and off, we can call function CFrameWnd::ShowControlBar(). We need to use
function CWnd::IsWindowVisible() to check if the control bar is currently available.
31
Chapter 2. Menu
Chapter 2
Menu
M
enu is very important for all types of applications, it provides a primary way of letting user
execute application commands. If we create applications using Application Wizard, mainframe
menus will be implemented automatically for all SDI and MDI applications. For dialog-based
applications, system menus will also be implemented, which can be used to execute system
commands (Move the application window, resize it, minimize, maximize and close the application). Some
user-friendly applications also include right-click pop up menus.
This chapter discusses in-depth topics on using and customizing menus, which include: how to
customize the behavior of standard menus, how to make change to standard menu interface, how to
implement owner-draw menu, how to create right-click menu, how to customize the system menu and
implement message mapping for system menu items.
File
New…
Open…
Save…
Save As…
Separator
Recent File
Separator
Exit
Edit
32
Chapter 2. Menu
Undo
Separator
Cut
Copy
Paste
View
Toolbar
Status Bar
Help
About…
By clicking “Edit” sub-menu, we will see that all the commands contained there are disabled. If we
edit the menu resource and add additional commands, they will not become functional until we add
message handlers for them.
1) In file “MenuDoc.h”, three member functions are declared in the class, they will be used to handle
ID_EDIT_COPY, ID_EDIT_CUT and ID_EDIT_PASTE command execution:
2) In file “MenuDoc.cpp”, message mapping macros are added to associate the member functions with
the command IDs:
BEGIN_MESSAGE_MAP(CMenuDoc, CDocument)
//{{AFX_MSG_MAP(CMenuDoc)
ON_COMMAND(ID_EDIT_COPY, OnEditCopy)
ON_COMMAND(ID_EDIT_CUT, OnEditCut)
33
Chapter 2. Menu
ON_COMMAND(ID_EDIT_PASTE, OnEditPaste)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
void CMenuDoc::OnEditCopy()
{
}
void CMenuDoc::OnEditCut()
{
}
void CMenuDoc::OnEditPaste()
{
}
When first added, these functions are empty. We have to add our own code in order to support
command execution.
By compiling and executing the sample application at this point, we will see that Edit | Copy, Edit |
Cut and Edit | Paste menu items are all enabled. This is because three blank message handlers have just
been added.
CMenuDoc::CMenuDoc()
{
m_bPasteAvailable=FALSE;
}
The value of CmenuDoc::m_bPasteAvailable is set to TRUE when user executes either Edit | Copy or
Edit | Cut command:
void CMenuDoc::OnEditCopy()
{
m_bPasteAvailable=TRUE;
}
void CMenuDoc::OnEditCut()
34
Chapter 2. Menu
{
m_bPasteAvailable=TRUE;
}
Now we will use CMenuDoc::m_bPasteAvailable to enable Edit | Paste menu item when it becomes
TRUE. In MFC, menu items are updated through handling UPDATE_COMMAND_UI messages. When the state
of a menu command needs to be updated, UPDATE_COMMAND_UI message will be automatically sent to the
application. If there exists a corresponding message handler, it will be called for updating the
corresponding menu item. Otherwise, the menu item will remain unchanged.
Adding an UPDATE_COMMAND_UI message handler is the same with adding a WM_COMMAND message
handler. After invoking the Class Wizard, we need to select the class name, the command ID, and highlight
“UPDATE_COMMAND_UI” instead of “WM_COMMAND” in “Messages” window. Finally, we need to
click button “Add Function”.
After adding the message handler for ID_EDIT_PASTE command, we will have a new member function
declared in class CMenuDoc, and a new message mapping macro added to the class implementation file. In
addition, we will have an empty function that could be modified to implement message handling:
The only parameter to this function is a pointer to CCmdUI type object. Here, class CCmdUI has several
member functions, which can be used to set the state of the menu item. To enable or disable the menu item,
we can call function CCmdUI::Enable(…). The function has only one Boolean type parameter, we can pass
TRUE to enable the menu item and pass FALSE to disable it. Because the state of Edit | Paste command
depends upon variable CMenuDoc::m_bPasteAvailable, we can use it to set the state of menu item:
By compiling and executing the sample application at this point, we will see that Edit | Paste
command is disabled at the beginning, and after we execute either Edit | Cut or Edit | Copy command, it
will be enabled.
35
Chapter 2. Menu
button will make it recess, setting check for a menu item will put a check mark at the left side of the menu
item.
We need a new Boolean type variable to indicate the status of “data”. In the sample application, this
variable is CMenuDoc::m_bDataPasted, which is initialized to FALSE in the constructor. The following
functions show how its value is changed under different situations:
void CMenuDoc::OnEditCopy()
{
m_bPasteAvailable=TRUE;
m_bDataPasted=FALSE;
}
void CMenuDoc::OnEditCut()
{
m_bPasteAvailable=TRUE;
m_bDataPasted=FALSE;
}
void CMenuDoc::OnEditPaste()
{
m_bDataPasted=TRUE;
}
In function OnUpdateEditPaste(…), the menu item is checked only when flag CMenuDoc::
m_bDataPasted is TRUE:
The text of the menu item is also change to “Data pasted” when the menu item is checked.
The last thing need to be mentioned here is another member function of class CCmdUI: CCmdUI::
SetRadio(…). Like CCmdUI::SetCheck(…), this function will put a check mark on a menu item. The
difference between two functions is that CCmdUI::SetRadio(…) makes menu items behave like radio
buttons: when this function is called to check one item, all other items in the same group will be unchecked
automatically. Calling function CCmdUI::SetCheck(…) does not affect other menu items.
36
Chapter 2. Menu
37
Chapter 2. Menu
Step 4: Highlight
CMenuView Step 6: Click “Add
Function” button
Step 5: Locate
WM_RBUTTONDOWN
and highlight it
When finished, click
“OK” button
In the sample application, the menu resource is stored by a numerical ID (IDR_MENU_POPUP). We can
also assign a string ID to it by inputting a quoted text in the edit box labeled with “ID”.
We need to use CMenu to declare a variable that will be used to load the menu resource. Normally the
right-click menu will be initiated after right-clicking event has been detected. Then the mouse’s activities
will be tracked by the menu until the user executes one of the menu commands or dismisses the menu.
Because all these things can be handled within the message handler, the variable used to implement menu
can be declared as a local variable. In the sample, the menu resource is loaded as follows:
menu.LoadMenu(IDR_MENU_POPUP);
CView::OnRButtonDown(nFlags, point);
}
Generally, one menu contains several sub-menus, and each sub-menu contains several menu items. For
right click menu, only one sub-menu (instead of whole menu) will be implemented each time the user
clicks mouse’s right button. Because of this, in the sample application, menu IDR_MENU_POPUP contains
only one sub-menu. To obtain a pointer to the sub-menu, we can call function CMenu::GetSubMenu(…),
which has the following format:
CMenu *Cmenu::GetSubMenu(int nPos) const;
Parameter nPos indicates which sub-menu we are trying to obtain. In a menu resource, the left-most
sub-menu is indexed 0, next sub-menu indexed 1, and so on. In the sample application, sub-menu that
contains items “Pop Up Item 1”… is located at position 0.
This function returns a CMenu type pointer that could be used to further access each item contained in
the sub-menu. Before the menu is displayed, we may want to set the state of each menu item: we can
enable, disable, set check or change text for a menu item. Please note that for a right-click menu, we do not
need to handle message UPDATE_COMMAND_UI in order to set the states of menu items. Instead, there exist
two member functions that can be used:
The above two functions can be used to enable/disable, set/remove check for a menu item. When
calling the two functions, we can reference a menu item by using either its command ID or its position.
Normally we can pass a command ID to nIDEnableItem or nIDCheckItem parameter. If we want to
38
Chapter 2. Menu
reference an item by its position (0 based, for example, in the sample application, ID__POPUPITEM1’s
position is 0, and ID__POPUPITEM2’s position is 1…), we need to set MF_BYPOSITION bit of nEnable or
nCheck parameter.
The menu can be activated and tracked by calling function CMenu::TrackPopupMenu(…):
BOOL CMenu::TrackPopupMenu
(
UINT nFlags, int x, int y, CWnd* pWnd, LPCRECT lpRect=NULL
);
This function has 5 parameters. The first parameter nFlags lets us set styles of the menu (Where
should the menu be put, which mouse button will be tracked). The most commonly used combination is
TPM_LEFTALIGN | TPM_RIGHTBUTTON, which aligns menu’s left border according to parameter x, and tracks
mouse’s right button activity (because we are implementing a right-click menu). The second parameter y
decides the vertical position of the menu’s top border. Please note that when message WM_RBUTTONDOWN is
received, position of current mouse cursor will be passed to one of the parameters of function
OnRButtonDown(…) as a CPoint type object. To make right-click menu easy to use, we can pass this
position to function CMenu::TrackPopupMenu(…), which will create a pop up menu at the position of
current mouse cursor. The fourth parameter is a CWnd type pointer, which indicates which window owns the
pop up menu. In the sample, because the menu is implemented in the member function of class CMenuView,
we can use this pointer to indicate the menu owner. The final parameter discribes a rectangle within which
the user can click the mouse without dismissing the pop up menu. We could set it to NULL, in which case
the menu will be dismissed if the user clicks outside the pop up menu.
menu.LoadMenu(IDR_MENU_POPUP);
ptrMenu=menu.GetSubMenu(0);
ptrMenu->EnableMenuItem(ID__POPUPITEM1, MF_GRAYED);
ptrMenu->EnableMenuItem(ID__POPUPITEM2, MF_ENABLED);
ptrMenu->CheckMenuItem(ID__POPUPITEM3, MF_UNCHECKED);
ptrMenu->CheckMenuItem(ID__POPUPITEM4, MF_CHECKED);
ClientToScreen(&point);
ptrMenu->TrackPopupMenu
(
TPM_LEFTALIGN|TPM_RIGHTBUTTON,
point.x,
point.y,
this,
NULL
);
CView::OnRButtonDown(nFlags, point);
}
After implementing the right-click menu, we still need to call function CView::OnRButtonDown(…).
This is to make sure that the application does not lose any default property implemented by class CView.
In the above function, before CMenu::TrackPopupMenu(…) is called, function
CWnd::ClientToScreen() is used to convert the coordinates of a point from the client window to the
desktop window (the whole screen). When point parameter is passed to CMenuView::OnRButtonDown(…), it
is assumed to be measured in the coordinates system of the client window, which means (0, 0) is located at
the upper-left corner of the client window. When we implement a menu, function
CMenu::TrackPopupMenu(…) requires coordinates to be measured in the desktop window system, which
means (0, 0) is located at the upper-left corner of the screen. Function CWnd::ClientToScreen(…) can
convert the coordinates of a point between the two systems. This function is frequently used when we need
to convert coordinates from one window to another.
39
Chapter 2. Menu
By compiling and executing the application at this point, we will see that the right-click menu is
implemented successfully.
BEGIN_MESSAGE_MAP(CMenuDoc, CDocument)
//{{AFX_MSG_MAP(CMenuDoc)
//}}AFX_MSG_MAP
ON_COMMAND(ID__POPUPITEM1, OnPopUpItem1)
ON_COMMAND(ID__POPUPITEM2, OnPopUpItem2)
ON_COMMAND(ID__POPUPITEM3, OnPopUpItem3)
ON_COMMAND(ID__POPUPITEM4, OnPopUpItem4)
END_MESSAGE_MAP()
void CMenuDoc::OnPopUpItem1()
{
AfxMessageBox("Pop up menu item 1");
}
void CMenuDoc::OnPopUpItem2()
{
AfxMessageBox("Pop up menu item 2");
}
void CMenuDoc::OnPopUpItem3()
{
AfxMessageBox("Pop up menu item 3");
}
void CMenuDoc::OnPopUpItem4()
{
AfxMessageBox("Pop up menu item 4");
}
With the above implementation, we are able to execute the commands contained in the right-click pop
up menu.
40
Chapter 2. Menu
Sometimes it is desirable to change the contents of a menu dynamically. For example, if we create an
application that supports many commands, we may want to organize them into different groups. Sometimes
we want to enable a group of commands, sometimes we want to disable them.
Although we can handle UPDATE_COMMAND_UI message to enable or disable commands, sometimes it is
more desirable if we can remove the whole sub-menu instead of just graying the menu text. Actually, sub-
menu and menu item can all be modified dynamically: we can either add or delete a sub-menu or menu
item at any time; we can also change the text of a menu item, move a sub-menu or menu item, or add a
separator between two menu items. All these things can be implemented at run-time.
Menu Struture
The structure of menu Æ sub menu Æ menu item is like a tree. At the topmost level (the root), the
menu comprises several sub-menus. Each sub-menu also comprises several items, which could be a normal
command or another sub-menu. For example, in application Explorer (file browser in Windows95), its
first level menu comprises five sub-menus: File, Edit, View, Tool, and Help. If we examine File sub-
menu, we will see that it comprises eight items: New, separator, Create Shortcut, Delete, Rename,
Properties, separator and Close. Here, item New is another sub-menu, which comprises several other
menu items. This kind of structure can continue. As long as our program needs, we can organize our menu
into many different levels.
In MFC, class CMenu should be used this way. With a CMenu type pointer to a menu object, we have the
access to only the menu items at certain level. If we want to access a menu item at a lower level, we first
need to access the sub-menu that contains the desired menu item.
This can be explained by the previous “Explorer” example: suppose we have a CMenu type pointer to
the main menu, we can use it to access the first level menu items: File, Edit, View, Tool, and Help. This
means we can use the pointer to disable, enable or set text for any of the above items, but we can not use it
to make change to the items belonging to other levels, for example, New item under File sub-menu. To
access this item, we need to first obtain a CMenu type pointer to File sub-menu, then use it to modify item
File | New.
BOOL CMenu::InsertMenu
(
UINT nPosition, UINT nFlags, UINT nIDNewItem=0, LPCTSTR lpszNewItem=NULL
);
This function has five parameters. The first parameter, nPosition, indicates where we want our new
menu item to be inserted. It could be an absolute position, 0, 1, 2…, or a command ID of the menu item. In
the former case, MF_BYPOSITION bit of second parameter nFlags must be set. In the latter case,
MF_BYCOMMAND bit must be set. Since not all menu items have a command ID (such as a separator), using
position to indicate a menu item is sometimes necessary.
Generally, we can insert three types of items: a menu item with specified command ID, a separator or a
sub-menu. To insert a menu item, we need to pass the command ID to nIDNewItem parameter, then use the
final parameter lpszNewItem to specify the text of this menu item. If we want to insert a separator, we must
set MF_SEPARATOR bit of parameter nFlag. In this case the rest two parameters nIDNewItem and
lpszNewItem will be ignored, so we can pass any value to them. If we want to insert a sub-menu, we must
pass a menu handle to parameter nIDNewItem, and use lpszNewItem to set the text for the menu item.
41
Chapter 2. Menu
In Windows programming, handle is a very important concept. Many types of resources are managed
through using handles. A handle is just a unique number that can be used to reference a block of memory.
After an object (program, resource, dynamically allocated memory block, etc.) is loaded into the memory,
it will be assigned a unique handle that can be used to access this object. As a programmer, we don’t need
to know the exact value of a handle. When accessing an object, instead of using handle’s absolute value, we
can just use the variable that stores the handle.
Different handles have different prototypes, for a menu object, its prototype is HMENU.
In MFC, this is further simplified. When we call a member function to load an object into the memory,
the handle will be automatically saved to a member variable. Later if we need this handle, we can just call a
member function to retrieve it.
In the case of class CMenu, after calling function CMenu::LoadMenu(…), we can obtain the handle of the
menu resource by calling function CMenu::GetSafeHmenu().
For example, in sample 2.2\Menu, after menu resource IDR_MENU_POUP is loaded into the memory, we
could obtain the handle of its first sub-menu and store it to an HMENU type variable as follows:
CMenu menu;
CMenu *ptrMenu;
HMENU hMenu;
menu.LoadMenu(IDR_MENU_POPUP);
ptrMenu=menu.GetSubMenu(0);
hMenu=ptrMenu->GetSafeHmenu();
The meanings of nPosition and nFlags parameters are similar to those of function CMenu::
InsertMenu(…).
There is another similar function: CMenu::DeleteMenu(…), which can also remove a menu item or sub-
menu. However, if we use this function to delete a sub-menu, the menu resource will be released from the
memory. In this case, if we wand to use the sub-menu again, we need to reload the menu resource.
Sample Implementation
Sample 2.3\Menu demonstrates how to add and delete menu items dynamically. It is a standard SDI
application generated by Application Wizard, with all the default settings. In this sample, there are two
commands Edit | Insert Dynamic Menu and Edit | Delete Dynamic Menu. If we execute the first
command, a new sub-menu will be added between File and Edit sub-menus. We can use the second
command to remove this dynamically added sub-menu.
The first step is to add two menu items to IDR_MAINFRAME menu resource. In the sample, two
commands are added to Edit sub-menu, their description text are “Insert Dynamic Menu” and “Delete
Dynamic Menu” respectively, and their command IDs are ID_EDIT_INSERTDYNAMICMENU and
ID_EDIT_DELETEDYNAMICMENU. Both of them have WM_COMMAND and UPDATE_COMMAND_UI message handlers
in class CMenuDoc, whose function names are OnEditInsertDynamicMenu,
OnUpdateEditInsertDynamicMenu, OnEditDeleteDynamicMenu and OnUpdateEditDeleteDynamicMenu.
Because we want to disable command ID_EDIT_DELETEDYNAMICMENU and enable command
ID_EDIT_INSERTDYNAMICMENU before the sub-menu is inserted, and reverse this after the menu is inserted,
another Boolean type variable m_bSubMenuOn is declared in class CMenuDoc, which will be used to indicate
the state of the inserted menu. It is initialized to FALSE in the constructor.
Preparing the menu resource that will be used to implement dynamic sub-menu is the same with what
we did in the previous sample. Here a resource IDR_MENU_POPUP is added to the application, whose content
is the same with the resource created in sample 2.2\Menu.
In this case, we could not use a local variable to load the menu, because once the menu is inserted, it
may exist for a while before the user removes it. If we still use a local variable, it will go out of scope after
the messagae hander returns. In the sample, a CMenu type variable is declared in class CMenuDoc, which is
used to load the menu resource in the constructor.
The following shows the modified class CMenuDoc:
42
Chapter 2. Menu
The following is the constructor within which the menu resource is loaded and m_bSubMenuOn is
initialized:
CMenuDoc::CMenuDoc()
{
m_menuSub.LoadMenu(IDR_MENU_POPUP);
m_bSubMenuOn=FALSE;
}
The following shows two UPDATE_COMMAND_UI message handlers where two menu commands are
enabled or disabled:
At last, we must implement two WM_COMMAND message handlers. First, we need to find a way of
accessing mainframe menu IDR_MAINFRAME of the application. In MFC, a menu associated with a window
can be accessed by calling function CWnd::GetMenu(), which will return a CMenu type pointer. Once we get
this pointer, we can use it to access any of its sub-menus.
The mainframe window pointer can be obtained by calling function AfxGetMainWnd() anywhere in the
program. An alternate way is to call AfxGetApp() to obtain a CWinApp type pointer, then access its public
member m_pMainWnd. We could use CMenu type pointer to insert or remove a sub-menu dynamically.
The following shows two message handlers that are used to insert or remove the sub-menu:
void CMenuDoc::OnEditInsertDynamicMenu()
{
CMenu *pTopMenu=AfxGetMainWnd()->GetMenu();
CMenu *ptrMenu=m_menuSub.GetSubMenu(0);
pTopMenu->InsertMenu
(
1, MF_BYPOSITION | MF_POPUP, (UINT)ptrMenu->GetSafeHmenu(), "&Dynamic Menu"
);
AfxGetMainWnd()->DrawMenuBar();
m_bSubMenuOn=TRUE;
}
void CMenuDoc::OnEditDeleteDynamicMenu()
{
CMenu *pTopMenu=AfxGetMainWnd()->GetMenu();
pTopMenu->RemoveMenu(1, MF_BYPOSITION);
AfxGetMainWnd()->DrawMenuBar();
m_bSubMenuOn=FALSE;
}
When inserting sub-menu, flag MF_BYPOSITION is used. This is because the first level menu items do
not have command IDs.
43
Chapter 2. Menu
After the menu is inserted or removed, we must call function CWnd::DrawMenuBar() to let the menu be
updated. Otherwise although the content of the menu is actually changed, it will not be reflected to the user
interface until the update is triggered by some other reasons.
BOOL CMenu::SetMenuItemBitmaps
(
UINT nPosition, UINT nFlags, const CBitmap* pBmpUnchecked,
const CBitmap* pBmpChecked
);
The first two parameters of this function indicate which menu item we are working with. Their
meanings are the same with that of functions such as CMenu::EnableMenuItem(…). When calling this
function, we can use either a command ID or an absolute position to identify a menu item. The third and
fourth parameters are pointers to bitmaps (CBitmap type objects), one for checked state, one for unchecked
state.
Standard checks
Bitmap checks
Like menu, bitmap can also be prepared as resource then be loaded at program’s runtime. We can edit
a bitmap in Developer Studio, and save it as application’s resource. Adding a bitmap resource is the same
with adding other types of resources: we can execute Insert | Resource… command, then select Bitmap
from the popped up dialog box. The newly added resource will be assigned a default ID, it could also be
changed by the programmer.
To load a bitmap resource into the memory, we need to use class CBitmap. This procedure is similar to
loading a menu resource: first we need to use CBitmap to declare a variable, then call function
CBitmap::LoadBitmap(…) to load the resource. For example, if we have a CBitmap type variable bmp, and
our bitmap resource’s ID is IDB_BITMAP, we can load the bitmap as follows:
bmp.LoadBitmap(IDB_BITMAP);
When calling function CMenu::SetMenuItemBitmaps(…), we can pass the pointers of CBitmap type
variables to its parameters.
Sample 2.4\Menu demonstrates bitmap check implementation. It is based on sample 2.3\Menu, which
adds check bitmaps to menu item ID__POPUPITEM1 and ID__POPUPITEM2. Two bitmap resources
IDB_BITMAP_CHECK and IDB_BITMAP_UNCHECK are used to indicate menu item’s checked and unchecked
states respectively. Both bitmaps have a size of 15×15, which is a suitable size for normal menu items. If
we use bigger bitmaps, they might be chopped to fit into the area of menu item.
In the sample, two new CBitmap type variables m_bmpCheck and m_bmpUnCheck are declared in class
CMenuDoc, which are used to load the bitmap resources:
44
Chapter 2. Menu
CMenuDoc::CMenuDoc()
{
CMenu *ptrMenu;
m_menuSub.LoadMenu(IDR_MENU_POPUP);
m_bmpCheck.LoadBitmap(IDB_BITMAP_CHECK);
m_bmpUnCheck.LoadBitmap(IDB_BITMAP_UNCHECK);
ptrMenu=m_menuSub.GetSubMenu(0);
ptrMenu->SetMenuItemBitmaps(0, MF_BYPOSITION, &m_bmpUnCheck, &m_bmpCheck);
ptrMenu->SetMenuItemBitmaps(1, MF_BYPOSITION, &m_bmpUnCheck, &m_bmpCheck);
m_bSubMenuOn=FALSE;
}
In the sample, bitmaps are prepared for both checked and unchecked states for a menu item. When
calling function CMenu::SetMenuItemBitmaps(…), if either of the bitmaps is not provided (the
corresponding parameter is NULL), nothing will be displayed for that state. If both parameters are NULL,
the default tick mark will be used for the checked state.
System Menu
By default, every application has a system menu, which is accessible through left clicking on the small
icon located at the left side of application’s caption bar, or right clicking on the application when it is in
icon state. The system menu can be customized to meet special requirement. Especially, we can add and
delete menu items dynamically just like a normal menu.
We already know how to access an application’s standard menu. Once we obtained a CMenu type
pointer to the application’s standard menu, we can feel free to add new menu items, remove menu items,
and change their attributes dynamically.
System menu is different from a standard menu. We need to call another function to obtain a pointer to
it. In MFC, the function that can be used to access system menu is CWnd::GetSystemMenu(…). Please note
45
Chapter 2. Menu
that we must call this function for a window that has an attached system menu. For an SDI or MDI
application, system menu is attached to the mainframe window. For a dialog box based application, the
system menu is attached to the dialog window.
Unlike user implemented commands, system commands (commands on the system menu) are sent
through WM_SYSCOMMAND rather than WM_COMMAND message. If we implement message handlers to receive
system commands, we need to use ON_WM_SYSCOMMAND macro.
New Functions
Function CWnd::GetSystemMenu(…) has only one Boolean type parameter:
Although we can call this function to obtain a pointer to the system menu and manipulate it, the
original default system menu can be reverted at any time by calling this function and passing a TRUE value
to its bRevert parameter. In this case, function’s returned value has no meaning and should not be treated
as a pointer to a menu object. We need to pass FALSE to this parameter in order to obtain a valid pointer to
the system menu.
Function CMenu::ModifyMenu(…) allows us to change any menu item to a separator, a sub-menu, a
bitmap menu item. It can also be used to modify a menu item’s text. This member function has two
versions:
BOOL CMenu::ModifyMenu
(
UINT nPosition, UINT nFlags, UINT nIDNewItem=0, LPCTSTR lpszNewItem = NULL
);
BOOL CMenu::ModifyMenu
(
UINT nPosition, UINT nFlags, UINT nIDNewItem, const CBitmap* pBmp
);
The first version of this function allows us to change a menu item to a text item, a separator, or a sub-
menu. The second version allows us to change a menu item to a bitmap item. For the second version,
parameter nIDNewItem specifies the new command ID, and parameter pBmp is a pointer to a CBitmap object,
which must contain a valid bitmap resource.
Menu Modification
In the sample application, a bitmap resource ID_BITMAP_QUESTION is prepared for implementing
bitmap menu item. This bitmap contains a question mark. There is no restriction on the bitmap size,
because the size of menu item will be adjusted automatically to let the image fit in.
To load the image, a new CBitmap type variable m_bmpQuestion is declared in class CMainFrame, and
bitmap resource ID_BITMAP_QUESTION is loaded in the constructor of class CMainFrame:
46
Chapter 2. Menu
CToolBar m_wndToolBar;
CBitmap m_bmpQuestion;
……
};
CMainFrame::CMainFrame()
{
m_bmpQuestion.LoadBitmap(IDB_BITMAP_QUESTION);
}
The pointer to the system menu is obtained in function CMainFrame::OnCreate(…). First, system
menu’s fifth item (a separator) is modified to a bitmap menu item, then another new command “Resume
standard system menu” is inserted before this bitmap menu item. The IDs of the two commands are
ID_QUESTION and ID_RESUME respectively.
Although we can use any numerical values as the command IDs of the newly added menu items, they
should not be used by other resources of the application. The best way to prevent this from happening is to
generate two new string resources in the application, and use ID_QUESTION and ID_RESUME as their
symbolic IDs. Because Developer Studio will always allocate unused values for new resources, we can
avoid sharing IDs with other resources by using this method.
The following shows how we access the system menu and make changes to its items:
return 0;
}
ON_WM_SYSCOMMAND()
Of course, we can ask Class Wizard to do the mapping for us. Before using it to add the above message
handler to CMainFrame class, we need to make following changes to the settings of Class Wizard: first click
“Class info” tab of the Class Wizard, then select “Window” from the window “Message filter” (Figure 2-
4). The default message filter for CMainFrame frame window is “Topmost frame”, and WM_SYSCOMMAND will
not be listed in the message list. After this modification, we can go back to “Message Maps” page, and
choose “WM_SYSCOMMAND” from messages window. To add the message handler, we simply need to
click “Add function” button (make sure the settings in other windows are correct). After this, the new
function OnSysCommand(…) will be added to the application.
Here is how this function is declared in class CMainFrame:
class CMainFrame
{
……
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
……
};
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
……
47
Chapter 2. Menu
ON_WM_SYSCOMMAND()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
Message handler CMainFrame::OnSysCommand(…) has two parameters, first of which is the command
ID, and the second is LPARAM message parameter. In our case, we only need to use the first parameter,
because it tells us which command is being executed. If the command is ID_RESUME, we need to call
function CMenu::GetSystemMenu(…) to resume the default system menu; if the command is ID_QUESTION,
we pop up a message box:
When we highlight a bitmap menu item by selecting it using mouse, the bitmap will be inversed. We
have no way of modifying this property because function CMenu::ModifyMenu(…) requires only one
bitmap, which will be used to implement menu item’s normal state. The other states of a bitmap menu item
will be drawn using the default implementations. Normally this is good enough. However, sometimes we
may want to use different bitmaps to represent a menu item’s different states: selected, unselected, checked,
unchecked, grayed or enabled.
Also, sometimes we may want to paint a menu item dynamically. Suppose we need to create a “Color”
menu item: the menu item represents a color that the user can use, and this color can be modified to
represent any available color in the system. To implement this type of menu item, we can paint it using the
currently selected color. In this case it is not appropriate to create the menu item using bitmap resources:
there may exist thousands of available colors in the system, and it is just too inconvenient to prepare a
bitmap resource for each possible color.
The owner-draw menu can help us build more powerful user interface. With this type of menu, we can
draw the menu items dynamically, using different bitmaps to represent different states of the menu item.
We can also change the associated bitmaps at any time.
48
Chapter 2. Menu
By default, the menu is drawn by the system. We can change this attribute by specifying MF_OWNERDRAW
style for a menu. Any menu with this style will be drawn by its owner. Since a menu’s owner is usually the
mainframe window (in SDI and MDI applications), we can add code to class CMainFrame to implement
dynamic menu drawing. Actually, the menu drawing has two associated messages: WM_MEASUREITEM and
WM_DRAWITEM. When a menu item with MF_OWNERDRAW style needs to be drawn, the menu sends out the
above two messages to the mainframe window. The mainframe window finds out the pointer to the
corresponding menu and calls functions CMenu::MeasureItem(…) and CMenu::DrawItem(…). Here, the first
function is used to retrieve the dimension of the menu item, which can be used to calculate its layout. The
second function implements menu drawing. We need to override it in order to implement custom interface.
One simple and most commonly used way to provide graphic interface is to prepare a bitmap resource
then load and draw it at run time. Please note that this is different from preparing a bitmap resource and
calling CMenu::ModifyMenu(…) to associate the bitmap with a menu item. If we implement drawing by
ourselves, we can manipulate drawing details. For example, when drawing the bitmap, we can change the
size of the image, add a text over it. If we assign the bitmap to the menu item, we lose the control over the
details of painting the bitmap.
Drawing a Bitmap
To draw a bitmap, we need to understand some basics on graphic device interface (GDI). This topic
will be thoroughly discussed from chapter 8 through 12, here is just a simple discussion on bitmap drawing.
In Windows operating system, when we want to output objects (a pixel, a line or a bitmap) to the screen,
we can not write directly to the screen. Instead, we must write to the device context (DC). A device context
is a data structure containing information about the drawing attributes of a device (typically a display or a
printer). We can use DC to write text, draw pixels, lines and bitmaps to the devices. In MFC the device
context is supported by class CDC, which has many functions that can let us draw different types of objects.
We can implement bitmap drawing by obtaining a target DC and calling member functions of CDC. The
target DC is usually obtained from a window.
A DC can select a lot of GDI objects, such pen, brush, font and bitmap. Pen can be different type of
pens, and brush can be different types of brushes. A DC can select any pen or brush as its current tool, but
at any time, a DC can select only one pen and one brush. If we want to draw a pixel or a line, we can select
the appropriate pen into the target DC and use it to implement drawing. A DC can also select bitmap for
drawing. However, to paint a bitmap, we can not select it into the target DC and draw it directly. The
normal way of painting a bitmap is to prepare a compatible memory DC (which is a block of memory with
the same attributes of the target DC), select the bitmap into the memory DC, and copy the bitmap from the
memory DC to the target DC.
We will learn more about DC and bitmap drawing in later chapters. For the time being, we can neglect
the drawing details.
49
Chapter 2. Menu
public:
MCMenu();
virtual ~MCMenu();
virtual void MeasureItem(LPMEASUREITEMSTRUCT);
virtual void DrawItem(LPDRAWITEMSTRUCT);
};
MCMenu::MCMenu() : CMenu()
{
m_bmpQuestion.LoadBitmap(IDB_BITMAP_QUESTION);
m_bmpQuestionSel.LoadBitmap(IDB_BITMAP_QUESTIONSEL);
m_bmpSmile.LoadBitmap(IDB_BITMAP_SMILE);
m_bmpSmileSel.LoadBitmap(IDB_BITMAP_SMILESEL);
}
switch(lpMeasureItemStruct->itemData)
{
case MENUTYPE_SMILE:
{
m_bmpSmile.GetBitmap(&bm);
break;
}
case MENUTYPE_QUESTION:
{
m_bmpQuestion.GetBitmap(&bm);
break;
}
}
lpMeasureItemStruct->itemWidth=bm.bmWidth;
lpMeasureItemStruct->itemHeight=bm.bmHeight;
}
In this function, MENUTYPE_SMILE and MENUTYPE_QUESTION are user-defined macros that represent the
type of menu items. First we examine member itemData and decide the type of the menu item. For
different types of menu items, the corresponding bitmap sizes are retrieved and set to members itemWidth
and itemHeight of structure MEASUREITEMSTRUCT. This size will be sent to the system and be used to
calculate the layout of the whole sub-menu.
50
Chapter 2. Menu
Member Explanation
CtlType Tells what kind of object is being drawn. Since this structure is also used to for owner-draw
button, combo box, list box, we need to check this member and make sure it is ODT_MENU.
hDC A handle to the target device context. From this handle, we can obtain a CDC type pointer to
the target device context.
itemState Specifies the state of the current menu item. It could be ODS_CHECKED, ODS_DISABLED,
ODS_FOCUS, ODS_GRAYED or ODS_SELECTED, whose meanings are easy to guess. In the sample
application, we take care only default and checked states.
itemData Same as structure MEASUREITEMSTRUCT, we need to check this member to decide what kind of
owner-draw menu item is being painted.
rcItem A rectangle specifies where we should implement drawing.
ptrDC=CDC::FromHandle(lpDrawItemStruct->hDC);
dcMem.CreateCompatibleDC(ptrDC);
51
Chapter 2. Menu
}
}
ptrBmpOld=dcMem.SelectObject(ptrBmp);
rect=lpDrawItemStruct->rcItem;
ptrDC->BitBlt
(
rect.left,
rect.top,
rect.Width(),
rect.Height(),
&dcMem,
0,
0,
SRCCOPY
);
dcMem.SelectObject(ptrBmpOld);
}
First, we check if the item is a menu. If not, we call the same function implemented by the base class
and return. If so, first we obtain a CDC type pointer to the target device by calling function
CDC::FromHandle(…). Then, we create a compatible memory DC with target DC, which will be used to
draw the bitmap. Next, we check the menu item’s state and type by looking at itemState and itemData
members of structure DRAWITEMSTRUCT, and choose different bitmaps according to different situations. At
last we select the appropriate bitmap into the memory DC, copy the bitmap to target device using function
CDC::BitBlt(…). This function has many parameters: the first four are position and size on the target
device; the fifth parameter is the pointer to the memory DC; the last parameter specifies drawing mode
(SRCCOPY will copy the source bitmap to the target device). Finally, we must select the bitmap out of the
memory DC, and resume its original state.
The original variable m_menuSub is used to load the menu resource, whose first sub-menu is obtained
by calling function CMenu::GetSubMenu(…) and attached to variable m_menuModified. By doing this, the
system will call the member functions of class MCMenu instead of CMenu when the owner-draw menu needs
to be painted. To change a menu item’s default style, function CMenu::ModifyMenu(…) is called and
MF_OWNERDRAW flag is specified:
CMenuDoc::CMenuDoc()
{
CMenu *ptrMenu;
m_menuSub.LoadMenu(IDR_MENU_POPUP);
m_bmpCheck.LoadBitmap(IDB_BITMAP_CHECK);
m_bmpUnCheck.LoadBitmap(IDB_BITMAP_UNCHECK);
ptrMenu=m_menuSub.GetSubMenu(0);
m_menuModified.Attach(ptrMenu->GetSafeHmenu());
ptrMenu->ModifyMenu
(
0,
MF_BYPOSITION | MF_ENABLED | MF_OWNERDRAW, ID__POPUPITEM1,
(LPCTSTR)MENUTYPE_SMILE
);
ptrMenu->ModifyMenu
(
1,
52
Chapter 2. Menu
In the above code, menu IDR_MENU_POPUP is loaded into m_menuSub, then the first sub-menu is obtained
and attached to variable m_menuModified. Here, function CMenu::Attach(…) requires a HMENU type
parameter, which can be obtained by calling function CMenu::GetSafeHmenu(). When calling function
CMenu::ModifyMenu(…), we pass integer instead of string pointer to its last parameter. This does not matter
because the integer provided here will not be treated as memory address, instead, it will be passed to
itemData member of structure MEASUREITEMSTRUCT (and DRAWITEMSTRUCT) to indicate the type of the
owner-drawn menu items.
Because the popup menu is attached to variable CMenuDoc::m_menuModified, we need to detach it
before application exits. The best place of implementing this is in class MCMenu’s destructor, when the menu
object is about to be destroyed:
MCMenu::~MCMenu()
{
Detach();
}
Now we can compile the sample project and execute it. By executing command Edit | Insert Dynamic
Menu and expanding Dynamic Menu then selecting the first two menu items, we will see that both
selected and unselected states of them will be implemented by our own bitmaps.
Bitmap is not restricted to only indicating selected and normal menu states. With a little effort, we
could also use bitmap to implement other menu states: grayed, checked, and unchecked. This will make our
menu completely different from a menu implemented by plain text.
void CMenuDoc::OnFileNew()
{
CMenu menu;
menu.LoadMenu(IDR_MAINFRAME_ACTIVE);
AfxGetMainWnd()->SetMenu(&menu);
AfxGetMainWnd()->DrawMenuBar();
53
Chapter 2. Menu
menu.Detach();
}
void CMenuDoc::OnFileOpen()
{
CMenu menu;
menu.LoadMenu(IDR_MAINFRAME_ACTIVE);
AfxGetMainWnd()->SetMenu(&menu);
AfxGetMainWnd()->DrawMenuBar();
menu.Detach();
}
void CMenuDoc::OnFileClose()
{
CMenu menu;
menu.LoadMenu(IDR_MAINFRAME);
AfxGetMainWnd()->SetMenu(&menu);
AfxGetMainWnd()->DrawMenuBar();
menu.Detach();
}
In the above three member functions, we use a local variable menu to load the menu resource. It will be
destroyed after the function exits. So before the variable goes out of scope, we must call function
CMenu::Detach() to release the loaded menu resource so that it can continue to be used by the application.
Otherwise, the menu resource will be destroyed automatically.
Summary
1) In order to execute commands, we need to handle message WM_COMMAND. In order to update user
interfaces of menu, we need to handle message UPDATE_COMMAND_UI.
1) To implement right-click menu, we need to first prepare a menu resource, then trap WM_RBUTTONDOWN
message. Within the message handler, we can use CMenu type variable to load the menu resource, and
call CMenu::TrackPopupMenu(…) to activate the menu and track mouse activities. Before the menu is
shown, we can call functions CMenu::EnableMenuItem(…) and CMenu::CheckMenuItem(…) to set the
states of the menu items.
1) To add or remove a menu item dynamically, we need to call functions CMenu::InsertMenu(…) and
CMenu::RemoveMenu(…).
1) With function CMenu::SetMenuItemBitmaps(…), we can use bitmap images to implement checked and
unchecked states for a menu item.
1) A window’s standard menu can be obtained by calling function CWnd::GetMenu(), and the
application’s system menu can be obtained by calling function CWnd::GetSysMenu(…).
1) We can change a normal menu item to a bitmap menu item, a separator, or a sub-menu by calling
function CMenu::ModifyMenu(…).
1) Owner-draw menu can be implemented by setting MF_OWNERDRAW style then overriding functions
CMenu::MeasureItem(…) and CMenu::DrawItem(…).
1) We can change the whole menu attached to a window by calling function CWnd::SetMenu(…).
54
Chapter 3. Splitter Window
Chapter 3
Splitter Window
A
splitter window resides within the frame window. It is divided into several panes, each pane can
have a different size. Splitter window provides the user with several different views for monitoring
data contained in the document at the same time. Normally, the size of each pane can be adjusted
freely, this gives the user a better view of data. There are two types of splitter windows: Dynamic
Splitter Window and Static Splitter Window. For a dynamic splitter window, all views within the splitter
window are of the same type. The user can create new panes or remove old panes on the fly. For a static
splitter window, the views could be of different types and the number of panes has to be fixed at the
beginning. In this case, the user can not add or delete views after the program has started.
Both SDI and MDI applications can have splitter windows. In an SDI application, the splitter window
is embedded in the mainframe window. In an MDI application, it is embedded in the child frame window.
To create static splitter window, first we need to declare CSplitterWnd type variable(s) in the frame
window class, then in frame window’s OnCreateClient(…) member function, call functions
CSplitterWnd::CreateStatic(…) and CSplitterWnd::CreateView(…). Here, function CSplitterWnd::
CreateStatic(…) is used to split the window into several panes and CSplitterWnd::CreateView(…) is
used to attach a view to each pane.
55
Chapter 3. Splitter Window
The other pane of the splitter window is implemented using edit view. The new class for this window
is derived from CEditView, and its name is CSpwEView.
To split a window, we need to call function CSplitterWnd::CreateStatic(…), which has five
parameters:
BOOL CSplitterWnd::CreateStatic
(
CWnd *pParentWnd, int nRows, int nCols,
DWORD dwStyle=WS_CHILD | WS_VISIBLE,
UINT nID=AFX_IDW_PANE_FIRST
);
The first parameter pParentWnd is a CWnd type pointer that points to the parent window. Because a
splitter window is always the child of frame window, this parameter can not be set to NULL. The second
and third parameters specify the number of rows and columns the splitter window will have. The fourth
parameter dwStyle specifies the styles of splitter window, whose default value is WS_CHILD | WS_VISIBLE.
The fifth parameter, nID, identifies which splitter window is being created. This is necessary because
within one frame window, we can create several nested splitter windows. For the root splitter window (The
splitter window whose parent window is the frame window), this ID must be AFX_IDW_PANE_FIRST. For
other nested splitter windows, this ID need to be obtained from the parent splitter windows by calling
function CSplitterWnd::IdFromRowCol(…), and passing appropriate column and row coordinates to it. The
following is the format of this function:
56
Chapter 3. Splitter Window
BOOL CSplitter.CreateView
(
int row, int col,
CRuntimeClass *pViewClass, SIZE sizeInit, CCreateContext *pContext
);
The first two parameters specify which pane is being created. The third parameter specifies what kind
of view will be used to create this pane. Usually macro RUNTIME_CLASS must be used to obtain a
CRuntimeClass type pointer. The fifth parameter is a creation context used to create the view. Within
CMainFrame::OnCreateClient(…), the creation context is passed through the second parameter of this
function.
In the sample application, we first use m_wndSpMain to call function CSplitterWnd::
CreateStatic(…) to split the client window into a 2×1 splitter window. Then, we use this variable to call
CSplitterWnd::CreateView(…) and pass two 0s to the first two parameters of this function (This specifies
(0, 0) coordinates). This will attach a new view to the left pane of the splitter window. Next we use
m_wndSpSub to call CSplitterWnd::CreateStatic(…) to further split the right pane into a 1×2 splitter
window, and call CSplitterWnd::CreateView(…) twice to create views for the two panes. At last, instead
of calling function CMainFrame::OnCreateClient(…), a TRUE value is returned. This can prevent the
default client window from being created.
The following steps show how the static splitter window is implemented in the sample:
Variable m_wndSpMain will be used to split the mainframe client window into a 2×1 splitter window,
and m_wndSpSub will be used to further split the right column into a 1×2 splitter window.
2) In function CMainFrame::OnCreateClient(…), create splitter windows and attach views to each pane:
if
(
!m_wndSpMain.CreateView
(
0, 0, RUNTIME_CLASS(CSpwView), CSize(100, 100), pContext
)
)
{
TRACE0("Failed to create first pane\n");
return FALSE;
57
Chapter 3. Splitter Window
if
(
!m_wndSpSub.CreateStatic
(
&m_wndSpMain, 2, 1,
WS_CHILD | WS_VISIBLE, m_wndSpMain.IdFromRowCol(0, 1)
)
)
{
TRACE0("Failed to create nested splitter\n");
return FALSE;
}
if
(
!m_wndSpSub.CreateView
(
0, 0, RUNTIME_CLASS(CSpwFView), CSize(50, 50), pContext
)
)
{
TRACE0("Failed to create second pane\n");
return FALSE;
}
if
(
!m_wndSpSub.CreateView
(
1, 0, RUNTIME_CLASS(CSpwEView), CSize(50, 50), pContext
)
)
{
TRACE0("Failed to create third pane\n");
return FALSE;
}
return TRUE;
}
For an MDI application, everything is almost the same except that here CChildFrame replaces class
CMainFrame. We can create an MDI application, declare m_wndSpMain and m_wndSpSub variables in class
CChildFrame, and add code to CChildFrame::OnCreateClient(…) to create splitter windows. The code
required here is exactly the same with implementing splitter window in an SDI application. Sample
3.1\MDI\Spw demonstrates this.
BOOL CSplitterWnd::Create
(
CWnd* pParentWnd,
int nMaxRows, int nMaxCols,
SIZE sizeMin,
CCreateContext* pContext,
DWORD dwStyle=WS_CHILD | WS_VISIBLE |WS_HSCROLL | WS_VSCROLL | SPLS_DYNAMIC_SPLIT,
UINT nID=AFX_IDW_PANE_FIRST
);
58
Chapter 3. Splitter Window
columns. The maximum values of nMaxRows and nMaxCols parameters are both 2, which means that a
window can be split to have at most 2×2 panes.
The Application Wizard has a built-in feature to add dynamic splitter window to the applications. In
step 4 of the Application Wizard, if we press “Advanced…” button, an “Advanced Options” property sheet
will pop up. By clicking “Window styles” tab then checking “Use split window” check box, code will be
automatically added to the application for implementing dynamic split window (static splitter window can
not be created this way).
It is also simple to implement splitter window manually. Like creating static split window, first we
need to declare a CSplitterWnd type variable in class CMainFrame (In MDI applications, we need to do this
in class CChildFrame). Then we can use Class Wizard to override function OnCreateClient(…). Within the
overridden function, we can call CSplitterWnd::Create(…) to create splitter window.
Sample 3.2\Spw demonstrates how to create dynamic splitter window in an SDI application. The
application is created from Application Wizard with all default settings. Then a new variable m_wndSp is
declared in class CMainFrame, which will be used to implement the splitter window. In function
CMainFrame::OnCreateClinet(…), the splitter window is created as follows:
In the above code, we did not pass any value to dwStyle and nID parameters of function
CSplitterWnd::Create(…),so the default values are used.
Split box
Split box
Figure 3-3. Double clicking on any of the split boxes will divide
the window into panes dynamically
59
Chapter 3. Splitter Window
This behavior could be customized. For example, sometimes by double clicking on the split bar, we
want to resize the two panes instead of deleting one of them. This feature gives the user much convenience
for changing the size of each pane bit by bit.
Split bar
Figure 3-4. Double clicking on the split bar will delete one of the
two panes divided by the split bar
In the above functions, parameters row and col are used to identify a pane with specified row and
column indices, cyIdeal and cxIdeal are the ideal size of a pane, cyMin and cxMin indicate minimum size
of it.
When the splitter window is being displayed, each pane’s dimension is decided from its ideal size.
According to the current size of the frame window, some panes may be set to their ideal sizes, but some
may not (This depends on how much space is left for that pane). In any case, a pane’s actual size should not
be smaller than its minimum size. This is why we need both ideal size and minimum size to set a row or
column’s dimension.
The number of rows and columns a splitter window currently has can be obtained by calling other two
member functions of CSplitterWnd:
int CSplitterWnd::GetRowCount();
int CSplitterWnd::GetColumnCount();
60
Chapter 3. Splitter Window
The class does nothing but overriding two functions. The implementation of function MCSplitterWnd
::DeleteRow(…) is listed as follows:
nNumRows=GetRowCount();
if(nNumRows < 2)
{
CSplitterWnd::DeleteRow(rowDelete);
}
else
{
int nCyCur, nCyMin;
Since the maximum number of rows that can be implemented in a dynamic split window is 2, we will
call the default implementation of this function (the corresponding function of the base class) if the number
of rows is not 2. Otherwise, we first obtain the size of upper pane (pane 0), enlarge its vertical size, and set
its current size. Then the current size of lower pane is reduced, if its ideal size is smaller than its minimum
size after change, we simply call function CSplitterWnd::DeleteRow(…) to delete this row. If the panes are
resized instead of being deleted, we call function CSplitterWnd::RecalcLayout() to update the new
layout.
Function MCSplitterWnd::DeleteColumn(int colDelete) is implemented in the same way, except
that here we call all the functions dealing with column instead of row.
61
Chapter 3. Splitter Window
MCSplitterWnd m_wndSp;
……
}
After these changes, by compiling and executing the application again, we will see that the split bar
behaves differently.
Drawing Functions
Class CSplitterWnd has two member functions that can be overridden to customize the appearance of
split bar, split box, split border, and split tracker. The functions are CSplitterWnd::OnDrawSplitter(…)
and CSplitter::OnInvertTracker(…) respectively, which have the following formats:
Function CSplitterWnd::OnDrawSplitter(…) is called when either the split bar, split box or split
border needs to be painted. It has three parameters, the first of which is a pointer to the target device DC,
which will be used to draw the objects. The second parameter is an enumerate type, which indicates what
type of object is being drawn. This parameter could be either CSplitterWnd::splitBox, CSplitterWnd::
splitBar, or CSplitterWnd::splitBorder, which indicates different splitter window objects. The third
parameter specifies a rectangle region within which the object will be drawn.
Function CSplitterWnd::OnInvertTracker(…) is called when the user clicks the mouse on the split
bar and drags it to resize the panes contained in the splitter window. In this case, a tracker will appear on
the screen and move with the mouse. By default, the tracker is a grayed line. By overriding this function,
we could let the tracker have a different appearance.
Sample
Sample 3.4\Spw demonstrates how to customize these styles. It is based on sample 3.3\Spw. First, two
functions are declared in class MCSplitterWnd to override the default implementation:
protected:
virtual void OnDrawSplitter(CDC*, CSplitterWnd::ESplitType, const CRect&);
virtual void OnInvertTracker(const CRect& rect);
};
void MCSplitterWnd::OnDrawSplitter
(
CDC* pDC,
ESplitType nType,
const CRect& rect
)
{
CBrush brush;
CBrush *ptrBrushOld;
if(pDC == NULL)
{
CSplitterWnd::OnDrawSplitter(pDC, nType, rect);
return;
}
switch(nType)
{
62
Chapter 3. Splitter Window
case CSplitterWnd::splitBox:
{
VERIFY(brush.CreateSolidBrush(RGB(255, 0, 0)));
break;
}
case CSplitterWnd::splitBar:
{
VERIFY(brush.CreateSolidBrush(RGB(0, 255, 0)));
break;
}
case CSplitterWnd::splitIntersection:
case CSplitterWnd::splitBorder:
{
CSplitterWnd::OnDrawSplitter(pDC, nType, rect);
return;
}
}
ptrBrushOld=pDC->SelectObject(&brush);
pDC->Rectangle(rect);
pDC->SelectObject(ptrBrushOld);
}
In the above function, first parameter pDC is checked. If it is not an available DC, we do nothing but
calling the default implementation of the base class. Otherwise, the object type is checked. We will go on to
implement the customization if the object is either a split bar or a split box.
The simplest way to fill a rectangle with certain pattern is to use brush. A brush can be different types:
solid, hatched, etc. It could also be initialized with any color. To use a brush, we need to first create brush,
then select it into the device context. If we draw a rectangle with this DC, the interior of the rectangle will
be automatically filled with the currently selected brush, and its border will be drawn using the currently
selected pen. After using the brush, we must select it out of the DC.
Brush selection can be implemented by calling function CDC::SelectObject(…). This function will
return a pointer to the old brush. After using the brush, we can call this function again and pass the old
brush to it. This will let the old brush be selected into the DC so the new brush is selected out.
When creating a brush, we need to use RGB macro to indicate the brush color. The three parameters of
RGB macro indicate the intensity of red, green and blue colors.
A rectangle can be drawn by calling function CDC::Rectangle(…). We need to pass a CRect type
variable to indicate the position and size of the rectangle.
In the sample, function MCSplitterWnd::OnInvertTracker(…)is implemented as follows:
ASSERT_VALID(this);
ASSERT(!rect.IsRectEmpty());
ASSERT((GetStyle() & WS_CLIPCHILDREN) == 0);
pDC=GetDC();
brush.CreateSolidBrush(RGB(255, 0, 0));
ptrBrushOld=pDC->SelectObject(&brush);
pDC->PatBlt(rect.left, rect.top, rect.Width(), rect.Height(), PATINVERT);
pDC->SelectObject(ptrBrushOld);
brush.DeleteObject();
ReleaseDC(pDC);
}
There is no CDC type pointer passed to this function. However, for any window, its DC could always be
obtained by calling function CWnd::GetDC(). This function will return a pointer to window’s device
context. After we use the DC, we must release it by calling function CWnd::ReleaseDC(…). In function
MCSplitterWnd::OnDrawSplitter(…), first a solid brush with red color is created, then we select it into the
DC, call function CDC::PatBlt(…) to fill the interior of the rectangle using the selected brush.
Function CDC::PatBlt(…) allows us to create a pattern on the device. We can choose different color
output mode: we can copy the brush color to the destination, or we can combine brush color with the color
on the target device using bit-wise operations. The first four parameters of function CDC::PatBlt(…)
indicate the position and size of the rectangle within which we can output the pattern. The fifth parameter
indicates the output mode. In the sample we use PATINVERT drawing mode, this will combine the
63
Chapter 3. Splitter Window
destination color and brush color using bit-wise XOR operation. With this mode, the tracker can be easily
erased if it is drawn twice.
Since we use PATINVERT mode to paint the tracker, its color will become the complement color of red
when the user resizes panes using the mouse.
protected:
BOOL m_bResizable;
……
}
A WM_LBUTTONDOWN message handler is added to the application. This includes function declaration,
adding ON_WM_LBUTTONDOWN message mapping macro, and the implementation of member function. Before
adding message mapping, we need to make sure that DECLARE_MESSAGE_MAP macro is included in the class.
This will enable massage mapping for the class. The following lists necessary steps for implementing the
above message mapping:
1) Declare an afx_msg type member function OnLButtonDown(…) in the class. This function is originally
declared in class CWnd, here we must declare it again in order to override it:
64
Chapter 3. Splitter Window
{
……
protected:
……
afx_msg void OnLButtonDown(UINT, CPoint);
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(MCSplitterWnd, CSplitterWnd)
//{{AFX_MSG_MAP(MCSplitterWnd)
//}}AFX_MSG_MAP
ON_WM_LBUTTONDOWN()
END_MESSAGE_MAP()
Summary
1) To implement static splitter window, we need to derive a class from CView (or other type of view
classes) for each pane, then declare a CSplitterWnd type variable in class CMainFrame. In function
CMainFrame::OnCreateClient(…), we need to call CSplitterWnd::CreateStatic(…) to create the
splitter window and call CSplitterWnd::CreateView(…) to attach a view to each pane.
1) Static splitter window can be nested. This means instead of attaching a view, we can use CSplitterWnd
to further create splitter window within a pane.
1) Creating dynamic splitter window is simple. In order to do this, we need to declare a CSplitterWnd
type variable in class CMainFrame; then in CMainFrame::OnCreateClient(…), we need to call function
CSplitterWnd::Create(…).
1) We can override functions CSplitterWnd::DeleteRow(…) and CSplitterWnd::DeleteColumn(…) to
customize the behavior of split bars.
1) We can override functions CSplitterWnd::OnDrawSplitter(…) and CSplitterWnd::
OnInvertTracker(…) to customize the appearance of split bar, split box, split border and tracker.
1) To disable split bar tracking, we need to call CWnd::OnLButtonDown(…) instead of CSplitterWnd::
OnLbuttonDown(…) when handling message WM_LBUTTONDOWN.
65
Chapter 4. Button
Chapter 4
Buttons
B
utton is one of the most commonly used controls, almost every application needs to include one or
more buttons. It seems that it is very easy to implement a standard button in a dialog box: all we
need to do is adding a button resource to the dialog template, then using Class Wizard to generate
message handlers. However, it is not very easy to further customize button’s default properties.
In this chapter, we will discuss how to implement different type of customized buttons. At the end of
this chapter, we will be able to include some eye-catching buttons in our applications.
Button States
Before customizing button’s default feature, it is important for us to understand some basics of buttons.
Every button has four states. When a button is not clicked, is in “up” state (the most common state). When
it is pressed down, it is in “down” state. To emphasis a button’s 3D effect, a default button will recess when
it is pressed by the mouse. Also, a button could be disabled, in this state, the button will not respond to any
mouse clicking (As the default implementation, when a button is disabled, it will be drawn with “grayed”
effect). Finally, a button has a “focused” or “unfocused” state. In the “focused” state, the button is an active
window, and is accessible through using keyboard (ENTER or downward ARROW key). For the default
implementation, a rectangle with dashed border will be drawn over a button’s face when it has the current
focus.
66
Chapter 4. Button
Check here to
create bitmap
button
Although every button has four states, we do not need to provide four bitmaps all the time. If one of
the bitmaps is not available, class CBitmapButton will draw the button’s corresponding state using the
default bitmap, which is the bitmap associated with button’s “up” state. So the bitmap used to represent a
button’s “up” state is required all the time and can not be omitted.
Automatic Method
We have two ways of associating bitmap images with different states of a bitmap button: we can either
let class CBitmapButton handle this automatically or we can do it manually. To use the automatic method,
the IDs of all four bitmap resources must be text strings, and must be formed by suffixing one of the
following four letters to the button’s caption text: ‘U’, ‘D’, ‘F’, ‘X’. These letters represent “up”, “down”,
“focused” and “disabled” state respectively. By naming the resource IDs this way, the rest thing we need to
do is calling function CBitmapButton::AutoLoad() in the dialog box’s initialization stage (within member
function CDialog::OnInitDialog()). Please note that we cannot call this function in the constructor of
class CDialog. At that time, the dialog box window is still not created (Therefore, the buttons are still not
available), and the bitmaps cannot be associated with the button correctly.
Sample
Sample 4.1\Btn demonstrates how to create bitmap button using automatic method. It is a dialog-based
application that is generated by the Application Wizard. First, the ID of default dialog template is changed
to IDD_DIALOG_BTN. Also, the “OK” and “Cancel” buttons originally included in the template are deleted.
Then a new button IDC_PLAY is added, whose caption text is set to “Play” (Figure 4-2). Since the button
will be drawn using the bitmaps, it doesn’t matter how big the button resource is. Besides this, we need to
set button’s style to “Owner draw”.
Figure 4-2. Add a button in the dialog template to create bitmap button
Two bitmap resources are added to the application whose IDs are “PLAYU” and “PLAYD”
respectively (Figure 4-3). They correspond to button’s “up” and “down” states. In addition, the sizes of the
two bitmaps are exactly the same.
A CBitmapButton type variable is declared in class CBtnDlg to implement this bitmap button:
67
Chapter 4. Button
BOOL CBtnDlg::OnInitDialog()
{
……
m_btnPlay.AutoLoad(IDC_PLAY, this);
……
}
Function CBitmapButton::AutoLoad(…) has two parameters, first of which is the button’s resource ID,
and the second is a pointer to the button’s parent window.
In the sample application only two bitmap images are prepared. We may add two other bitmaps whose
IDs are “PLAYF” and “PLAYX”. Then we can enable or disable the button to see what will happen to the
button’s interface.
68
Chapter 4. Button
can find out whether the current state of the check box is “Checked” or “Unchecked”. Based on this
information, we can decide which bitmap should be used.
Sample 4.2\Btn demonstrates the above method. It is based on sample 4.1\Btn. In the sample, three
new buttons are added: one of them is implemented as a check box; the rest are implemented as radio
buttons. The following describes how the bitmap check box and radio buttons are implemented in the
sample:
1) Add a check box and two radio buttons to the dialog template. Name the IDs of new controls
IDC_CHECK, IDC_RADIO_A and IDC_RADIO_B respectively. In the property sheet that lets us customize
control’s properties, check “Bitmap” check box (Figure 4-4).
2) Add two bitmap resources, one for checked state and one for unchecked state. Their resource IDs are
ID_BITMAP_CHECK and ID_BITMAO_UNCHECK respectively. The bitmaps must have a same size.
3) Declare two CBitmap type variables m_bmpCheck and m_bmpUnCheck in class CBtnDlg, in function
CBtnDlg::OnInitDlg(), call CBitmap::LoadBitmap(…) to load the two bitmap resources. Then call
function CButton::SetBitmap(…) to set bitmap for the check box and radio buttons. In the sample, all
of the new controls are initialized to unchecked state (In order to do this, we need to associate buttons
with m_bmpUnCheck instead of m_bmpCheck). The following code fragment shows the modified class
CBtnDlg and the function CBtnDlg::OnInitDialog(…):
BOOL CBtnDlg::OnInitDialog()
{
……
m_bmpCheck.LoadBitmap(IDB_BITMAP_CHECK);
m_bmpUnCheck.LoadBitmap(IDB_BITMAP_UNCHECK);
((CButton *)GetDlgItem(IDC_CHECK))->SetBitmap
(
(HBITMAP)m_bmpUnCheck.GetSafeHandle()
);
((CButton *)GetDlgItem(IDC_RADIO_A))->SetBitmap
(
(HBITMAP)m_bmpUnCheck.GetSafeHandle()
);
((CButton *)GetDlgItem(IDC_RADIO_B))->SetBitmap
(
(HBITMAP)m_bmpUnCheck.GetSafeHandle()
);
……
}
4) Declare a new member function CBtnDlg::SetCheckBitmap(…). We will use it to set a button’s bitmap
according to its current state. The function has one parameter nID that identifies the control. Within the
function, first the button’s current state is examined, if it is checked, we call CButton::SetBitmap(…)
to associate it with IDB_BITMAP_CHECK; otherwise we use bitmap IDB_BITMAP_UNCHECK. The following
is the implementation of this function:
69
Chapter 4. Button
bCheck=((CButton *)GetDlgItem(nID))->GetCheck();
((CButton *)GetDlgItem(nID))->SetBitmap
(
bCheck ? (HBITMAP)m_bmpCheck.GetSafeHandle():
(HBITMAP)m_bmpUnCheck.GetSafeHandle()
);
Invalidate(FALSE);
}
5) Use Class Wizard to implement three WM_COMMAND message handlers for IDC_CHECK, IDC_RADIO_A and
IDC_RADIO_B. Within each handler, we call CBtnDlg::SetCheckBitmap(…) to set appropriate bitmaps.
Because two radio buttons should be treated as a group (if one is checked, the other one will be
unchecked automatically), we need to set both button’s bitmaps within each message handler:
void CBtnDlg::OnCheck()
{
SetCheckBitmap(IDC_CHECK);
}
void CBtnDlg::OnRadioA()
{
SetCheckBitmap(IDC_RADIO_A);
SetCheckBitmap(IDC_RADIO_B);
}
void CBtnDlg::OnRadioB()
{
SetCheckBitmap(IDC_RADIO_B);
SetCheckBitmap(IDC_RADIO_A);
}
In step 3, when calling CWnd::GetDlgItem(…), we pass the control’s resource ID to the function to
obtain a pointer to the control and use it to call function CButton::SetBitmap(…). Because
CWnd::GetDlgItem(…) will return a CWnd type pointer, we must first cast it to CButton type pointer before
calling the member function of CButton.
Function Cbutton::SetBitmap(…) has an HBITMAP type parameter, which requires a valid bitmap
handle. A bitmap handle can be obtained by calling function CBitmap::GetSafeHandle(), of course, the
returned handle is valid only after the bitmap is loaded.
In step 4, function CButton::GetCheck() is called to retrieve button’s current state (checked or
unchecked). The function returns a Boolean type value, if the returned value is TRUE, the button is being
checked, otherwise it is not checked.
After these modifications, the bitmap check box and radio buttons will become functional.
4.3 Subclass
In section 4.1, we used automatic method to create bitmap buttons. This requires us to create owner-
draw buttons with special caption text, which will be used to name the bitmap resource IDs. For simple
cases, this is a very convenient method. However, if we implement bitmap buttons this way, it is difficult
for us to customize them at runtime.
Implementing Subclass
Class CBitmapButton gives us another member function that can be used to associate bitmaps with an
owner-draw button: CBitmapButton::LoadBitmaps(…). This function has two versions, the first version
allows us to load bitmaps with string IDs, the second version allows us to load bitmaps with integer IDs.
To use this function, we must first implement subclass for the owner-draw button. “Subclass” is a very
powerful technique in Windows programming. It allows us to write a procedure, attach it to a window,
70
Chapter 4. Button
and use it to intercept messages sent to this window then process it. By doing this, we are able to customize
the window’s behavior within the procedure.
Subclass is supported by class CWnd, so theoretically all windows (including client window, dialog box,
dialog common controls...) can be “subclassed”. There are two functions to implement subclass, one is
CWnd::SubclassWindow(…), which allows us to customize the normal behavior of a window. Here we will
use the other one: CWnd::SubclassDlgItem(…), which is specially designed to implement subclass for the
common controls contained in a dialog box.
In MFC, implementing subclass is very simple. We don’t need to write a special procedure to handle
the intercepted messages. All we need to do is designing a class as usual, adding message handlers for the
messages we want to process, and implementing the message handlers. Then we can declare a variable
using the newly designed class, and call function CWnd::SubclassDlgItem(…) to implement subclass.
Function CWnd::SubclassDlgItem(…) has two parameters:
Parameter nID indicates which control we are dealing with, and pParent is the pointer to the control’s
parent window.
Class CBitmapButton uses subclass to change the default behavior of a button. If we use automatic
method to load the bitmaps, the subclass procedure is transparent to the programmer. However, if we want
to load the bitmaps by ourselves, we must implement subclass first.
Bitmap Button
Sample 4.3\Btn demonstrates how to associate bitmaps with an owner draw button by calling function
CBitmapButton::LoadBitmaps(…). It is based on sample 4.2\Btn. There is nothing new in this sample,
except that button IDC_PLAY is implemented differently.
In the previous samples, variable CBtnDlg::m_btnPlay is declared as a CBitmapButton type variable.
In the new sample, instead of using automatic method to load the bitmaps, we first implement the subclass
then load the bitmaps manually in function CBitmapButton::LoadBitmaps(…):
BOOL CBtnDlg::OnInitDialog()
{
CDialog::OnInitDialog();
……
m_btnPlay.SubclassDlgItem(IDC_PLAY, this);
m_btnPlay.LoadBitmaps
(
“PLAYU”,
“PLAYD”,
NULL,
NULL
);
m_btnPlay.SizeToContent();
……
return TRUE;
}
Here, function CBitmapButton::AutoLoad(…) is replaced by three new functions. The first function
added is CWnd::SubclassDlgItem(…). The second function is CBitmapButton::LoadBitmaps(…). This
function has four parameters, which are the bitmap IDs corresponding to button’s “Up”, “Down”,
“Focused” and “Disabled” states respectively. They could be either string IDs or integer IDs. The last
function is CBitmap::SizeToContent(), which allows us to set bitmap button’s size to the size of the
associated bitmaps. If we don’t call this function, the bitmaps may not fit well into the button.
Now we can remove or modify bitmap button IDC_PLAY’s caption text “Play. Actually, it doesn’t
matter if the button has caption text or not. By compiling the application and executing it at this point, we
will see that the bitmap button implemented here is exactly the same as the one implemented in the
previous sample.
71
Chapter 4. Button
A rectangle with
dashed border will
appear whenever the
control has current
focus
Figure 4-5. Bitmap check box and radio buttons implemented using class CButton
To improve this, we can use class CBitmapButton to create both check box and radio button. By doing
this, the button’s focused state will be implemented using the bitmap image provided by the programmer
instead of drawing a rectangle with dashed border over button’s face. Since class CBitmapButton supports
only push button implementation, we need to change the bitmap by ourselves to imitate check box and
radio button.
Sample 4.4\Btn is based on sample 4.3\Btn. In this sample, three new buttons are added to the dialog
template: one will be implemented as a check box; the other two will be implemented as radio buttons. All
of them will be based on class CBitmapButton.
First, we need a Boolean type variable for each check box and radio button to represent its current
state. This variable toggles between TRUE and FALSE, indicating if the button is currently checked or
unchecked. When the state of a button changes, we re-associate the button with an alternate bitmap and
paint the bitmap button again.
Since we use push button to implement check box and radio button, we can not call function
CButton::GetCheck(…) to examine if the button is currently checked or not. This is because a push button
will automatically resume to the “up” state after the mouse is released.
In the sample application, three new buttons are added to the dialog template IDD_BTN_DIALOG, and
their corresponding IDs are IDC_BMP_CHECK, IDC_BMP_RADIO_A, IDC_BMP_RADIO_B respectively. Also, they
all have a “Owner draw” style. Besides the new controls, two new bitmap resources are also added to the
application, which will be used to implement button’s “Checked” and “Unchecked” states. The IDs of the
new bitmap resources are IDB_BITMAP_BTNCHECK and IDB_BITMAP_BTNUNCHECK. The difference between the
new bitmaps and two old ones (whose IDs are IDB_BITMAP_CHECK and IDB_BITMAP_UNCHECK) is that the
new bitmaps have a 3-D effect. In sample 4.2\Btn, the check box and radio buttons are implemented using
class CButton, which automatically adds 3-D effect to the controls. Since we want the controls to be
implemented solely by programmer-provided bitmaps, we need to add 3-D effect by ourselves.
In the sample application, three new CBitmapButton type variables are declared in class CBtnDlg. Also,
a new member function SetRadioBitmap() is added to associate bitmaps with the two radio buttons. This
function will be called when one of the radio buttons is clicked by mouse. For the check box, associating
bitmap with it is relatively simple, so it is implemented within the message handler. Besides this, a new
Boolean type variable CBtnDlg::m_bBmpCheck is declared to indicate the current state of the check box, and
an unsigned integer CBtnDlg::m_uBmpRadio is declared to indicate which radio button is being selected.
For each button, WM_COMMAND message handler is added through using Class Wizard. These message
handlers are CBtnDlg::OnBmpCheck(), CBtnDlg::OnBmpRadioA() and CBtnDlg::OnBmpRadioB()
respectively. The following code fragment shows the new members added to the class:
72
Chapter 4. Button
CBitmapButton m_btnCheck;
CBitmapButton m_btnRadioA;
CBitmapButton m_btnRadioB;
CBitmap m_bmpCheck;
CBitmap m_bmpUnCheck;
BOOL m_bBmpCheck;
UINT m_uBmpRadio;
……
afx_msg void OnBmpCheck();
afx_msg void OnBmpRadioA();
afx_msg void OnBmpRadioB();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
In the dialog initialization stage, subclass is implemented for buttons, then the corresponding bitmaps
are loaded:
BOOL CBtnDlg::OnInitDialog()
{
……
m_btnCheck.SubclassDlgItem(IDC_BMP_CHECK, this);
m_btnCheck.LoadBitmaps(IDB_BITMAP_BTNUNCHECK);
m_btnCheck.SizeToContent();
m_btnRadioA.SubclassDlgItem(IDC_BMP_RADIO_A, this);
m_btnRadioA.LoadBitmaps(IDB_BITMAP_BTNUNCHECK);
m_btnRadioA.SizeToContent();
m_btnRadioB.SubclassDlgItem(IDC_BMP_RADIO_B, this);
m_btnRadioB.LoadBitmaps(IDB_BITMAP_BTNUNCHECK);
m_btnRadioB.SizeToContent();
……
}
For each button, first we implement subclass for it, then we load the bitmap by calling function
CBitmapButton::LoadBitmaps(…). Because we provide only one bitmap for each control, state transition
of these buttons (e.g., normal state to focused state) will not be reflected to the interface unless we add
extra code to handle it.
For check box IDC_BMP_CHECK, when it is clicked by the mouse’s left button, we need to toggle the
value of variable CBtnDlg::m_bCheck, load the corresponding bitmap and paint the button again. The
following is the WM_DOMMAND message handler for check box:
void CBtnDlg::OnBmpCheck()
{
m_bBmpCheck=m_bBmpCheck ? FALSE:TRUE;
m_btnCheck.LoadBitmaps
(
m_bBmpCheck ?
IDB_BITMAP_BTNCHECK:IDB_BITMAP_BTNUNCHECK
);
Invalidate(FALSE);
}
Radio buttons are slightly different. When one radio button is checked, the other button should be
unchecked. So within both CBtnDlg::OnBmpRadioA() and CBtnDlg::OnBmpRadioB(), function CBtnDlg::
SetRadioBitmap() is called to set bitmaps for both radio buttons:
void CBtnDlg::OnBmpRadioA()
{
73
Chapter 4. Button
m_uBmpRadio=IDC_BMP_RADIO_A;
SetRadioBitmap();
}
void CBtnDlg::OnBmpRadioB()
{
m_uBmpRadio=IDC_BMP_RADIO_B;
SetRadioBitmap();
}
void CBtnDlg::SetRadioBitmap()
{
m_btnRadioA.LoadBitmaps
(
(m_uBmpRadio == IDC_BMP_RADIO_A) ?
IDB_BITMAP_BTNCHECK:IDB_BITMAP_BTNUNCHECK
);
m_btnRadioB.LoadBitmaps
(
(m_uBmpRadio == IDC_BMP_RADIO_B) ?
IDB_BITMAP_BTNCHECK:IDB_BITMAP_BTNUNCHECK
);
Invalidate(FALSE);
}
When the user clicks one of the radio buttons, its resource ID is assigned to variable
CBtnDlg::m_uBmpRadio. Then in function CBtnDlg::SetRadioBitmap(), this variable is compared to both
radio button IDs. Bitmap IDB_BITMAP_BTNCHECK will be associated to the button whose ID is currently
stored in variable CBtnDlg::m_uBmpRadio. For the other button, bitmap IDB_BITMAP_BTNUNCHECK will be
loaded.
The appearance of our new check box and radio buttons is almost the same with that of old ones
implemented in sample 4.2\Btn. However, here the rectangle with dashed border will not be put over a
button’s face when the button has the current focus (Figure 4-6).
Transparent Background
Up to now all the buttons created by us have a rectangular shape. It is relatively difficult to create a
button with irregular shapes (e.g., a round button). Even for bitmap buttons, their associated bitmaps are
always rectangular.
We may think that by setting the bitmap’s background color to the color of the dialog box, the bitmap
button may look like a non-rectangular button. For example, in the previous samples, button IDC_PLAY is
made up of two regions: its foreground is the triangular region, and rest area can be treated as its
background (Figure 4-7). We can change the background color to the color of the dialog box so that this
area appears transparent to us when the bitmap button is implemented.
However, this is not the best solution. In Windows operating system, the color of dialog box can be
customized. The user can double click “Display” icon contained in the “Control Panel” window and set the
colors for many objects, which include title bar, backdrop..., and so on. So actually we have no way of
knowing the color of dialog box beforehand. If we implement a bitmap button and set its background color
to the dialog box color in our system, it may not work properly in another system.
74
Chapter 4. Button
Background
Foreground
To draw a bitmap with transparent background, we need to use “mask” when painting the bitmap. We
can imagine a mask as a matrix with the same dimension of the bitmap image, and each element in the
matrix corresponds to a pixel contained in the bitmap. The elements in the matrix may be either “0” or “1”.
When we paint the bitmap, only those pixels with “0” masks are output to the device (we can also use “1”
to indicate the pixels that need to be drawn).
When programming the application, we can prepare two bitmaps of exactly the same size: one stores
the normal image, the other one stores mask. The mask bitmap is made up of only two types of pixels:
black and white. This is because for black color, its RGB elements are all 0s; for white color, the elements
are all 1s. By implementing the mask bitmap like this, the background area of the mask bitmap is white and
the foreground area is black.
Windows allows us to output the pixels of a bitmap to the target device using different operation
mode. We can copy a pixel directly to the target device, we can also combine a pixel contained in the
bitmap with the pixel contained in the target device using various mode: bit-wise OR, AND, and XOR. For
example, if we combine red color (RGB(255, 0, 0)) with green color (RGB(0, 255, 0)) using bit-wise AND
operation, the output will be yellow color (RGB(255, 255, 0)).
When painting the bitmap, we first need to output the normal bitmap to the target device using bit-wise
XOR drawing mode, then output the mask bitmap to the target device at the same position using bit-wise
AND mode. Finally we can output the normal bitmap again using bit-wise XOR mode. By doing this, the
output bitmap’s background will become transparent.
The reason for this is simple. Before bitmap is painted, the target device may contain a uniform color
or any image pattern. After normal bitmap is first painted, the foreground area of the target device will
become the combination of source image pattern and target image pattern. After we output mask bitmap
using bit-wise AND operation, the foreground area of the target device will become black. This means on
the target device, every pixel in the foreground area is zero now. Since we use bit-wise XOR mode to
output normal image in the last step, and XORing anything with zero will not change the source, the
foreground area on the target device will finally contain the pattern of the source image. For mask area, the
second ANDing operation doesn’t make any change to it because ANDing a pixel with white color (all 1s)
doesn’t change that pixel. So the overall operations on the mask region is equivalent to two consecutive
XOR operations, which will resume all the pixels in this region to their original colors.
However there is still a slight problem here: if we draw the source and mask bitmaps directly to the
target device using the method mentioned above, we will see a quick flicker on the mask area of the target
device. Although it lasts only for a very short period, it is an undesirable effect. To overcome this, we can
prepare a bitmap in the memory, copy the target image pattern to this bitmap, do the transparent painting on
the memory bitmap, and copy the final result back to the target. Since the background area of the memory
bitmap has the same pattern with the background area of the target device, this final copy will not cause
any flicker.
To customize the drawing behavior of bitmap button, we need to override function
CBitmapButton::OnDrawItem(…). For an owner-draw button, this function will be called when a button
needs to be updated. Actually, menu and combo box also use similar functions. We can create an owner
draw menu or combo box by overriding the member functions with the same name. For these functions,
75
Chapter 4. Button
their parameters are all pointers to DRAWITEMSTRUCT type object. This structure stores information such as
button’s current state (i.e. focused, disabled), the device context that can be used to implement drawing,
and the rectangle indicating where we can output the image pattern.
New Class
Sample 4.5\Btn is based on sample 4.4\Btn, it demonstrates how to create bitmap buttons with
transparent background. In this sample, a new class MCBitmapButton is derived from CBitmapButton.
Besides the default properties inherited from base class, the following new features are added to this class:
1) The new class handles transparent background drawing automatically. Programmer can prepare a
black-and-white mask bitmap and associate it with the bitmap button together with other required
bitmaps. The drawing will be taken care in the member function of the class. Programmer doesn’t need
to add extra code.
2) The mask bitmap can be loaded along with other images by calling either AutoLoad(…) or
LoadBitmaps(…).
2) Function AutoLoad(…) is overridden to load the mask image automatically. In this case, the mask
bitmap must have a string ID that is created by suffixing an ‘M’ character to button’s caption text. For
example, if we want to create mask bitmap for a button whose caption text is “PLAY”, the ID of the
mask bitmap must be “PLAYM”. If we load the mask bitmap using automatic method, there is no
difference between using the new class and CBitmapButton.
2) Mask bitmap could also be loaded by calling function LoadBitmaps(…). The overridden function has
five parameters, the last of which is the ID of the mask bitmap.
2) If the mask bitmap is not present, the bitmap will be output directly to the target device using the
default implementation.
In the sample, a mask bitmap “PLAYM” is added to the application. It will be used to draw button
IDC_PLAY with transparent background.
The new class derived from CBitmapButton is MCBitmapButton. In this class, a new CBitmap type
variable m_bitmapMask is added to load the mask bitmap, also, functions AutoLoad(…) and LoadBitmaps(…)
are overridden. The following code fragment shows this new class:
protected:
CBitmap m_bitmapMask;
virtual void DrawItem(LPDRAWITEMSTRUCT lpDIS);
DECLARE_DYNAMIC(MCBitmapButton)
DECLARE_MESSAGE_MAP()
};
Function LoadBitmaps(…) has two versions, one is used to load bitmaps with string IDs, the other is
used to load bitmaps with integer IDs. Both functions have five parameters.
76
Chapter 4. Button
BOOL MCBitmapButton::LoadBitmaps
(
LPCTSTR lpszBitmapResource,
LPCTSTR lpszBitmapResourceSel,
LPCTSTR lpszBitmapResourceFocus,
LPCTSTR lpszBitmapResourceDisabled,
LPCTSTR lpszBitmapResourceMask
)
{
BOOL bAllLoaded;
m_bitmapMask.DeleteObject();
bAllLoaded=CBitmapButton::LoadBitmaps
(
lpszBitmapResource,
lpszBitmapResourceSel,
lpszBitmapResourceFocus,
lpszBitmapResourceDisabled
);
if(lpszBitmapResourceMask != NULL)
{
if(!m_bitmapMask.LoadBitmap(lpszBitmapResourceMask))
{
TRACE0(“Failed to load bitmap for normal background image.\n”);
bAllLoaded=FALSE;
}
}
return bAllLoaded;
}
77
Chapter 4. Button
The caption text of the button can be retrieved by calling function CWnd::GetWindwoText(…). Actually,
every window can have a caption text, which can be an empty string, or any text. We can use this function
to retrieve the caption text of any window, for example, a title tar.
We need to call functions CWnd::SubclassDlgItem(…) and CBitmapButton::SizeToContent() to
change the button’s default properties. Actually, the above two functions are also called in function
CBitmapButton::AutoLoad(…).
Three DCs are declared here. To draw a bitmap, we must create a memory DC, select the bitmap into it
and copy the bitmap from the memory DC to the target DC. The target could be either a device or a
memory block (we could copy bitmap between two memory DCs). A DC can select only one bitmap at any
time.
When there is no mask bitmap, variable memDC is used to perform normal bitmap drawing: it is used to
select the normal bitmap, and copy it directly to the target DC. When there is a mask bitmap, memDC will be
used along with memDCMask to implement transparent background drawing.
Variable memDCImage is used to act as the target memory DC and implement transparent background
drawing. It will be used in conjunction with bmpImage, which will be selected into memDCImage. To draw the
bitmap, first we need to copy the image pattern from the target device to the memory bitmap, then copy the
source bitmap to the memory bitmap (perform AND and XOR drawings). Finally, we can output the result
from the memory bitmap to the target device.
Variable bmpImage is used to create bitmap in memory.
Variable memDCMask is used to select mask bitmap image.
Pointer pDC will be used to store the pointer of the target device context that is created from hDC
member of structure DRAWITEMSTRUCT.
Pointers pBitmap and pBitmapMask will be used to store the pointers to the normal bitmap (could be
one of the bitmaps indicating the four states of the button) and the mask bitmap respectively.
The other three CBitmap pointers pOld, pOldMask and pOldImage are used to select the bitmaps out of
the DCs (When the bitmaps are being selected into the DCs, these pointers are used to store the bitmaps
78
Chapter 4. Button
selected out of the DCs. After bitmap drawing is finished, we can select old bitmaps back into the DCs, this
will select our bitmaps out of the DCs automatically).
Variable state is used to store the current state of button.
The following portion of function MCBitmapButton::DrawItem(…) shows how to choose appropriate
bitmaps:
……
ASSERT(lpDIS != NULL);
ASSERT(m_bitmap.m_hObject != NULL);
pBitmap=&m_bitmap;
if(m_bitmapMask.m_hObject != NULL)
{
pBitmapMask=&m_bitmapMask;
}
else pBitmapMask=NULL;
state=lpDIS->itemState;
if
(
(state & ODS_SELECTED) &&
(m_bitmapSel.m_hObject != NULL)
)
{
pBitmap=&m_bitmapSel;
}
else if
(
(state & ODS_FOCUS) &&
(m_bitmapFocus.m_hObject != NULL)
)
{
pBitmap=&m_bitmapFocus;
}
else if
(
(state & ODS_DISABLED) &&
(m_bitmapDisabled.m_hObject != NULL)
)
{
pBitmap=&m_bitmapDisabled;
}
……
First pBitmap is assigned the address of variable m_bitmap, which holds the default bitmap. Then we
check if the mask bitmap exists, if so, we assign its address to pBitmapMask. The current state of the button
is read into variable state, whose ODS_SELECTED, ODS_FOCUS and ODS_DISABLED bits are examined in turn.
If any of them is set, the corresponding bitmap’s address will be stored in pBitmap.
The following portion of this function creates the memory DCs and selects relative bitmaps into
different DCs:
……
pDC=CDC::FromHandle(lpDIS->hDC);
memDC.CreateCompatibleDC(pDC);
if(pBitmapMask != NULL)
{
memDCMask.CreateCompatibleDC(pDC);
memDCImage.CreateCompatibleDC(pDC);
}
pOld=memDC.SelectObject(pBitmap);
if(pBitmapMask != NULL)
{
pOldMask=memDCMask.SelectObject(pBitmapMask);
pBitmap->GetBitmap(&bm);
bmpImage.CreateCompatibleBitmap(pDC, bm.bmWidth, bm.bmHeight);
pOldImage=memDCImage.SelectObject(&bmpImage);
}
……
First, the address of the target DC is obtained from the DC handle by calling function CDC::
FromHandle(…). Then the memory DC that will select source image is created by calling function
CDC::CreateCompatibleDC(…). Since the bitmap could be copied only between compatible DCs, each time
we create a memory DC, we need to make sure that it is compatible with the target DC. Next, if the mask
bitmap exists, we create three DCs: memDC for normal bitmap, memDCMask for mask bitmap and memDCImage
79
Chapter 4. Button
for memory target bitmap (It will act as temparory target device DC). In this case, we also create a memory
bitmap using variable bmpImage, which is selected into memDCImage (This bitmap must also be compatible
with the DC that will select it). In the above implementation, we call function CBitmap::GetBitmap(…) to
obtain the dimension information of a bitmap and call function CBitmap::CreateCompatibleBitmap(…) to
create compatible memory bitmap). The mask bitmap is selected into memDCMask. The normal bitmap is
always selected into memDC.
The following portion of function MCBitmapButton::DrawItem(…) draws the bitmap by copying
normal and mask bitmaps among different DCs:
……
rect.CopyRect(&lpDIS->rcItem);
if(pBitmapMask == NULL)
{
pDC->BitBlt
(
rect.left,
rect.top,
rect.Width(),
rect.Height(),
&memDC,
0,
0,
SRCCOPY
);
}
else
{
memDCImage.BitBlt
(
0,
0,
rect.Width(),
rect.Height(),
pDC,
rect.left,
rect.top,
SRCCOPY
);
memDCImage.BitBlt
(
0,
0,
rect.Width(),
rect.Height(),
&memDC,
0,
0,
SRCINVERT
);
memDCImage.BitBlt
(
0,
0,
rect.Width(),
rect.Height(),
&memDCMask,
0,
0,
SRCAND
);
memDCImage.BitBlt
(
0,
0,
rect.Width(),
rect.Height(),
&memDC,
0,
0,
SRCINVERT
);
pDC->BitBlt
(
rect.left,
rect.top,
rect.Width(),
80
Chapter 4. Button
rect.Height(),
&memDCImage,
0,
0,
SRCCOPY
);
}
memDC.SelectObject(pOld);
if(pBitmapMask != NULL)
{
memDCMask.SelectObject(pOldMask);
memDCImage.SelectObject(pOldImage);
}
}
Bitmap copy is implemented by calling function CDC::BitBlt(…). This function will copy the selected
bitmap from one DC to another. If there is no mask bitmap, we copy the normal bitmap (selected by memDC)
directly to the target DC (pointed by pDC). Otherwise, first we copy the image pattern from the target device
(pDC) to memory bitmap (selected by memDCImage). Then we copy normal bitmap and mask bitmap
(selected by memDC and memDCMask) to this memory bitmap three times, using different operation modes,
and copy the final result to the target DC (pDC). At last, we select the bitmaps out of DCs.
A button that is
aware of mouse
position
Figure 4-8. New button implemented in sample 4.6\Btn
81
Chapter 4. Button
There must be a DECLARE_MESSAGE_MAP macro in the class in order to let it support message mapping.
The message mapping macros are added to the implementation file as follows:
BEGIN_MESSAGE_MAP(MCBitmapButton, CBitmapButton)
ON_WM_LBUTTONUP()
END_MESSAGE_MAP()
We see that the mouse position is passed to parameter point of this function.
Because the commands are generally handled in the parent window of the button, we need to resend
mouse clicking information from the button to class CBtnDlg. In the sample application, this is
implemented through sending a user- defined message.
User-Defined Message
User defined messages can be treated the same with other standard Windows messages: they can be
sent from one window to another, and we can add message handlers for them. All user-defined messages
must have a message ID equal to or greater than WM_USER.
In the sample, a new message WM_BTNPOS is defined in file “MButton.h”:
By doing this, WM_BTNPOS becomes a message that can be used in the application. Please note that this
message can not be sent to other applications. If we want to send user-defined message among different
applications, we need to register that message to the system.
In function MCBitmapButton::OnLButtonUp(…), user defined message WM_BTNPOS is sent to the parent
window, with the current mouse position stored in LPARAM parameter:
First the default implementation of function OnLButtonUp(…)is called. Then a CWnd type pointer of
parent window is obtained by calling function CWnd::GetParent(). Class CWnd has several member
functions that can be used to send Windows message, the most commonly used ones are CWnd::
82
Chapter 4. Button
SendMessage(…) and CWnd::PostMessage(…). The difference between the two functions is that after
sending out the message, CWnd::SendMessage(…) does not return until the message has been processed by
the target window, and CWnd::PostMessage(…) returns immediately after the message has been sent out. In
the sample, function CWnd::PostMessage(…) is used to send WM_BTNPOS message.
All messages in Windows have two parameters, WPARAM and LPARAM. For Win32 applications, both
WPARAM and LPARAM are 32-bit integers. They can be used to send additional information.
In MFC, usually message parameters are passed as arguments to message handlers, so they are rarely
noticed. For example, for message WM_LBUTTONDOWN, its WPARAM parameter is used to indicate if any of
CTRL, ALT, or SHIFT key is held down when the mouse button is up. In the message handler, this
information is mapped to the first parameter of CWnd::OnLButtonUp(…). Again, its LPARAM parameter
contains the information of current mouse position, which is mapped to the second parameter of
CWnd::OnLButtonUp(…). Both CWnd::SendMessage(…) and CWnd::PostMessage(…) have three parameters,
the first of which specifies message ID, and the rest two are WPARAM and LPARAM parameters. If we don’t
want to send additional message, we can pass 0 to both of them.
In the sample, we need to use both parameters: the parent window needs to know the control ID of the
button; also, it needs to know the current mouse position.
The button’s ID can be retrieved by calling function CWnd::GetDlgCtrlID(), it will be sent through
WPARAM parameter to the button’s parent. The x and y coordinates of mouse cursor can be combined together
to form an LPARAM parameter by using MAKELPARAM macro. Here, macro MAKELPARAM can combine two 16-
bit numbers to form a 32-bit message. If we provide two 32-bit numbers, only the lower 16 bits will be
used (Of course, screen coordinates won’t use more than 16 bits).
The message is received and processed in class CBtnDlg. In MFC, general message can be mapped to a
member function by using ON_MESSAGE macro. This type of message handler has two parameters, one for
receiving WPARAM information and the other for receiving LPARAM information. Also, it must return a LONG
type value.
The following code fragment shows how member function OnBtnPos(…) is declared in class CBtnDlg
(It will be used to receive WM_BTNPOS message):
BEGIN_MESSAGE_MAP(CBtnDlg, CDialog)
……
ON_MESSAGE(WM_BTNPOS, OnBtnPos)
END_MESSAGE_MAP()
The control ID and the mouse information can be extracted within the message handler as follows:
uID=wParam;
pt.x=LOWORD(lParam);
pt.y=HIWORD(lParam);
return (LONG)TRUE;
}
Sample
Sample 4.6\Btn has a four-arrow bitmap button. First a button resource is added to the dialog template,
whose ID is IDC_PLAY_POS and caption text is “PLAYPOS” (bitmaps will be loaded through automatic
83
Chapter 4. Button
method). Two new bitmap resources “PLAYPOSU” and “PLAYPOSD” are also added to the application,
which will be used to draw button’s “up” and “down” states.
We need to know the sizes and the positions of four arrows within the bitmap button so we can judge if
the mouse cursor is over any of the arrows. Within class CBtnDlg, a CRect type array with size of 4 is
declared for this purpose. Their values are initialized in function CBtnDlg::OnInitDialog(). Also an
MCBitmapButton type variable m_btnPlayPos is declared to implement this new button:
BOOL CBtnDlg::OnInitDialog()
{
……
m_btnPlayPos.AutoLoad(IDC_PLAY_POS, this);
m_btnPlayPos.GetClientRect(rect);
x=rect.Width()/2;
y=rect.Height()/2;
m_rectBtnPos[0]=CRect(x-2, rect.top, x+2, y);
m_rectBtnPos[1]=CRect(rect.left, y-2, x, y+2);
m_rectBtnPos[2]=CRect(x-2, y, x+2, rect.bottom);
m_rectBtnPos[3]=CRect(x, y-2, rect.right, y+2);
……
}
Here, we call function CWnd::GetClientRect() to retrieve the button size. We need to calculate the
sizes and positions of the arrows after bitmaps have been loaded. This is because a button will be resized
according to the bitmap size after it is initialized.
Function CBtnDlg::OnBtnPos(…) is implemented just for the purpose of demonstration: if any of the
four arrows is pressed, a message box will pop up displaying a different message:
if(wParam == IDC_PLAY_POS)
{
pt.x=LOWORD(lParam);
pt.y=HIWORD(lParam);
if(m_rectBtnPos[0].PtInRect(pt))AfxMessageBox(“Hit upward arraow”);
if(m_rectBtnPos[1].PtInRect(pt))AfxMessageBox(“Hit leftward arraow”);
if(m_rectBtnPos[2].PtInRect(pt))AfxMessageBox(“Hit downward arraow”);
if(m_rectBtnPos[3].PtInRect(pt))AfxMessageBox(“Hit rightward arraow”);
}
return (LONG)TRUE;
}
The application is now ready for compile. Based on this method, we can implement bitmap buttons
with more complicated functionality.
84
Chapter 4. Button
Setting Capture
In this section, we are going to create a very special button. It is not a push button, nor a check box or
radio button. The button has two states: normal and highlighted. Generally, the button will stay in normal
state. When the mouse cursor is within the button’s rectangle (with no mouse button being clicked), the
button will become highlighted, and if the mouse moves out of the rectangle, the button will resume to
normal state.
To implement this type of button, we need to trap mouse messages and implement handlers. One of the
messages we need to handle is WM_MOUSEMOVE, which will be sent to a window if the mouse cursor is
moving over the button. We need to respond to this message and set button’s state to “highlighted” when
the mouse cursor first comes into the button’s rectangle. From now on, we need to keep an eye on the
mouse’s movement, if the mouse moves out of the rectangle, we need to resume button’s normal state.
However, since message WM_MOUSEMOVE will only be sent to a window when the mouse cursor is within
it, it is difficult to be informed of the event that mouse has just left the button’s window. This is because
once the mouse leaves, the button will not be able to receive WM_MOUSEMOVE message anymore.
To help us solve this type of problems, Windows provides a technique that can be used to track
mouse’s activities after it leaves a window. This technique is called Capture. By using this method, we
could capture all the mouse-related messages to one specific window, no matter where the mouse is.
We can call function CWnd::SetCapture() to set capture for a window. The capture can also be
released by calling function ::ReleaseCapture(), which is a Win32 API function. Besides using this
function, the capture can also be removed by the operating system under certain conditions. If this happens,
the window that is losing capture will receive a WM_CAPTURECHANGED message.
New Class
Sample 4.7\Btn demonstrates how to implement “mouse sensitive button”. In the application, a new
class MCSenButton is created for this purpose, and is defined as follows:
protected:
BOOL m_bCheck;
afx_msg void OnMouseMove(UINT, CPoint);
afx_msg void OnCaptureChanged(CWnd *);
DECLARE_DYNAMIC(MCSenButton)
DECLARE_MESSAGE_MAP()
};
The class contains only a constructor, two message handlers, and a Boolean type variable m_bCheck.
Variable m_bCheck will be used to indicate the button’s current state: it is TRUE if the button is currently
highlighted, and is FALSE if the button is in the normal state. Within the constructor, this variable is
initialized to FALSE:
MCSenButton::MCSenButton() : MCBitmapButton()
{
m_bCheck=FALSE;
}
IMPLEMENT_DYNAMIC(MCSenButton, MCBitmapButton)
BEGIN_MESSAGE_MAP(MCSenButton, MCBitmapButton)
ON_WM_MOUSEMOVE()
ON_WM_CAPTURECHANGED()
85
Chapter 4. Button
END_MESSAGE_MAP()
Here, ON_WM_MOUSEMOVE and ON_WM_CAPTURECHANGED are message mapping macros defined by MFC.
The following two functions handle above two messages:
GetClientRect(rect);
if(!rect.PtInRect(point))
{
ReleaseCapture();
m_bCheck=FALSE;
SetState(FALSE);
Invalidate();
}
}
}
Implementation
In the sample, four new buttons are added to the application. The IDs of these new buttons are
IDC_MOUSE_SEN_1, IDC_MOUSE_SEN_2, IDC_MOUSE_SEN_3 and IDC_MOUSE_SEN_4 respectively. An
MCSenButton type array m_btnBmp (The array size is 4) is declared in class CBtnDlg and initialized in
function CBtnDlg::OnInitDialog() as follows:
BOOL CBtnDlg::OnInitDialog()
{
CDialog::OnInitDialog();
CRect rect;
int x, y;
int i;
……
for(i=IDC_MOUSE_SEN_1; i<=IDC_MOUSE_SEN_4; i++)
86
Chapter 4. Button
{
m_btnBmp[i-IDC_MOUSE_SEN_1].SubclassDlgItem(i, this);
m_btnBmp[i-IDC_MOUSE_SEN_1].LoadBitmaps
(
IDB_BITMAP_BTNUNCHECK, IDB_BITMAP_BTNCHECK
);
m_btnBmp[i-IDC_MOUSE_SEN_1].SizeToContent();
}
……
}
Here, we use subclass instead of automatic method to load bitmaps. Also, we use
IDB_BITMAP_BTNUNCHECK and IDB_BITMAP_BTNCHECK to implement button’s normal and highlighted states
respectively.
Because mouse related messages are handled within the member functions of class MCSenButton, once
we declare variables within it, the buttons will automatically become mouse sensitive. There is no need for
us to write extra code for handling mouse messages outside the class.
Summary
1) We can use class CBitmapButton to implement bitmap buttons. To use this class, we need to prepare 1
to 4 bitmap resources indicating button’s different states, then use class CBitmapButton to declare
variables, and call either CBitmapButton::AutoLoad(…) or CBitmapButton::LoadBitmaps(…) to
associate the bitmap resources with the buttons.
2) To use function CBitmapButton::AutoLoad(…), the bitmap resources must have string IDs, and must
be created by suffixing ‘U’, ‘D’, ‘F’ or ‘X’ to the button’s caption text.
3) Buttons, check boxes and radio buttons implemented by class CButton can display user-provided
bitmaps by calling function CButton::LoadBitmap(…). With this method, the button could be
associated with only one image at any time. Also, its focused state will be indicated by drawing a dash-
bordered rectangle over button’s face.
4) We can call function CBitmapButton::LoadBitmaps(…) at any time to change the associated bitmaps.
This provides us a way of implementing check box and radio button using push button.
5) Irregular shape button can be implemented by drawing images with transparency. We can prepare a
normal image and a black-and-white mask image. When drawing the button, only the unmasked region
of the normal image should be output to the target device.
6) A button can handle mouse-related messages. If we want to know the mouse position when a button is
being pressed, we can trap WM_LBUTTONUP message.
5) We can implement mouse sensitive button by handling WM_MOUSEMOVE message and setting window
capture.
87
Chapter 5. Common Controls
Chapter 5
Common Controls
I
n this chapter we will discuss some common controls that can be included in a dialog box. These
controls include spin control, progress bar control, slider control, tree control, tab control, animate
control and combo box. These controls can all be included in a dialog template as resources. Besides,
all the controls have corresponding MFC classes that can be used to implement them.
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.
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
88
Chapter 5. Common Controls
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).
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).
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
89
Chapter 5. Common Controls
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:
BOOL CCCtlDlg::OnInitDialog()
{
……
(
(CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_VER)
)->SetBuddy(GetDlgItem(IDC_EDIT_VER));
(
(CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_HOR)
)->SetBuddy(GetDlgItem(IDC_EDIT_HOR));
return TRUE;
}
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.