Advanced MFC Programming
Advanced MFC Programming
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.
Sample 5.2\CCtl is based on sample 5.1-1\CCtl. In this sample, the vertical spin is customized to
display hexadecimal integers, whose range is set from 0x0 to 0xC8 (0 to 200), and its initial position is set
to 0x64 (100). The horizontal spin still displays decimal integers, its range is from 50 to 0, and the initial
position is 25. The following portion of function CCCtlDlg::OnInitDialog() shows the newly added code:
BOOL CCCtlDlg::OnInitDialog()
{
……
(
(CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_VER)
)->SetBuddy(GetDlgItem(IDC_EDIT_VER));
((CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_VER))->SetRange(0, 200);
((CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_VER))->SetBase(16);
((CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_VER))->SetPos(100);
(
(CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_HOR)
)->SetBuddy(GetDlgItem(IDC_EDIT_HOR));
((CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_HOR))->SetRange(50, 0);
((CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_HOR))->SetBase(10);
((CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_HOR))->SetPos(25);
90
Chapter 5. Common Controls
return TRUE;
}
*pResult = 0;
}
The first parameter here is a NMHDR type pointer. This is a structure that contains Windows
notification messages. A notification message is sent to the parent window of a common control to notify
the changes on that control. It is used to handle events such as mouse left button clicking, left button double
clicking, mouse right button clicking, and right button double clicking performed on a common control.
Many types of common controls use this message to notify the parent window. For spin control, after
receiving this message, we need to cast the pointer type from NMHDR to NM_UPDOWN. Here structure
MN_UPDOWN is defined as follows:
In the structure, member iPos specifies the current position of the spin control, and iDelta indicates
the proposed change on spin’s position. We can calculate the new position of the spin control by adding up
these two members.
The following function shows how the buddy’s text is set after receiving the message:
nNewPos=pNMUpDown->iPos+pNMUpDown->iDelta;
if(nNewPos >= 0 && nNewPos <= 9)
{
91
Chapter 5. Common Controls
GetDlgItem(IDC_EDIT_STR)->SetWindowText(szNumber[nNewPos]);
}
*pResult=0;
}
The buddy’s text is set by calling function CWnd::SetWindowText(…). Here variable szNumber is a two-
dimensional character array which stores strings “Zero”, “One”, “Two”…”Nine”. First we calculate the
current position of the spin control and store the result in an integer type variable nNewPos. Then we use it
as an index to table szNumber, find the appropriate string, and use it to set the text of the edit control.
In dialog box’s initialization stage, we need to set the range and position of the spin control. Since the
edit box will display nothing by default, we also need to set its initial text:
BOOL CCCtlDlg::OnInitDialog()
{
CDialog::OnInitDialog();
……
((CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_STR))->SetRange(0, 9);
((CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_STR))->SetPos(0);
GetDlgItem(IDC_EDIT_STR)->SetWindowText("Zero");
return TRUE;
}
With the above implementation, the spin’s buddy control will display text instead of numbers.
BOOL CCCtlDlg::OnInitDialog()
{
CDialog::OnInitDialog();
……
m_bmpBtn.SubclassDlgItem(IDC_BUTTON_BMP, this);
m_bmpBtn.LoadBitmaps(IDB_BITMAP_SMILE_1);
m_bmpBtn.SizeToContent();
((CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_BMP))->SetRange(0, 3);
((CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_BMP))->SetPos(0);
return TRUE;
}
The initially selected bitmap is IDC_BITMAP_SMILE_1. We should not load bitmaps for other states
(“down”, “focused” and “disabled”) because the purpose of this button is to display images rather than
executing commands. We need to change the currently loaded image upon receiving UDN_DELTAPOS
notification. To change button’s associated bitmap, in the sample application, a UDN_DELTAPOS message
handler is added for IDC_SPIN_BMP, which is implemented as follows:
92
Chapter 5. Common Controls
{
int nNewPos;
NM_UPDOWN *pNMUpDown=(NM_UPDOWN*)pNMHDR;
nNewPos=pNMUpDown->iPos+pNMUpDown->iDelta;
switch(nNewPos)
{
case 0:
{
m_bmpBtn.LoadBitmaps(IDB_BITMAP_SMILE_1);
break;
}
case 1:
{
m_bmpBtn.LoadBitmaps(IDB_BITMAP_SMILE_2);
break;
}
case 2:
{
m_bmpBtn.LoadBitmaps(IDB_BITMAP_SMILE_3);
break;
}
case 3:
{
m_bmpBtn.LoadBitmaps(IDB_BITMAP_SMILE_4);
break;
}
}
m_bmpBtn.Invalidate();
*pResult=0;
}
Although we say that the bitmap button is the buddy of the spin control, in the above implementation
we see that they do not have special relationship. A spin control needs a buddy only in the case when we
want the text in the buddy window to be updated automatically. If we implement this in UDN_DELTAPOS
message handler, the buddy loses its meaning because we can actually set text for any control. Although
this is true, here we still treat the bitmap button as the buddy of spin control because the bitmap button is
under the control of the spin.
5.5 Slider
A slider is a control that allows the user to select a value from pre-defined range using mouse or
keyboard. A slider can be customized to have many different styles: we can put tick marks on it, set its
starting and ending ranges, make the tick marks distributed linearly or non-linearly. Besides these
attributes, we can also set the line size and page size of a slider, which decide the minimum distance the
slider moves when the user clicks mouse on slider’s rail or hit arrow keys of the keyboard.
93
Chapter 5. Common Controls
To let the tick marks be set automatically, besides setting “Tick mark” and “Auto ticks” styles, we
must also specify slider’s range. A slider’s range can be set by calling either function CSliderCtrl::
SetRange(…) alone or CSliderCtrl::SetRangeMin(…) together with CSliderCtrl::SetRangeMax(…)in the
dialog box’s initialization stage. The format of the above three functions are listed as follows:
By default, the distance between two neighboring tick marks is 1. To change this, we may call function
CSliderCtrl::SetTicFreq(…) to set the frequency of the tic marks. If the slider does not have “Auto
ticks” style, we must call function CSliderCtrl::SetTic(…) to set tick marks for the slider. Because this
function allows us to specify the position of a tic mark, we can use it to set non-linearly distributed tic
marks.
Two other properties that can be modified are slider’s page size and line size. Here, page size
represents the distance the slider will move after the user clicks mouse on its rail. The line size represents
the distance the slider will move if the user hits left arrow or right arrow key when the slider has the current
focus (Figure 5-5). Two member functions of class CSliderCtrl can be used to set the above two sizes:
CSliderCtrl::SetPageSize(…) and CSliderCtrl::SetLineSize(…). The default page size is 1/5 of the
total slider range and the default line size is 1.
In sample 5.5\CCtl, there are three sliders, whose IDs are IDC_SLIDER_AUTOTICK, IDC_SLIDER_TICK
and IDC_SLIDER_SEL respectively. Here, slider IDC_SLIDER_AUTOTICK has “Tick marks” and “Auto ticks”
styles, slider IDC_SLIDER_TICK has only one “Tick marks” style, and slider IDC_SLIDER_SEL has “Tick
marks”, “Auto ticks”, and “Enable selection” styles.
Three sliders are initialized in function CCCtlDlg::OnInitDialog(). The following portion of this
function sets the range, tick marks, page size and line size for each slider:
BOOL CCCtlDlg::OnInitDialog()
{
……
((CSliderCtrl *)GetDlgItem(IDC_SLIDER_AUTOTICK))->SetRange(0, 10);
((CSliderCtrl *)GetDlgItem(IDC_SLIDER_AUTOTICK))->SetTicFreq(2);
((CSliderCtrl *)GetDlgItem(IDC_SLIDER_TICK))->SetRange(0, 50);
for(i=0; i<=50; i+=i)
{
((CSliderCtrl *)GetDlgItem(IDC_SLIDER_TICK))->SetTic(i);
if(i == 0)i=2;
}
94
Chapter 5. Common Controls
return TRUE;
}
Since slider IDC_SLIDER_AUTOTICK has “Auto ticks” style, we don’t need to set the tick marks. In the
sample, the range of this slider is set from 0 to 10, and the tick mark frequency is set to 2. The tick marks
will appear at 0, 2, 4, 6… 10. Also, since no page size and line size are specified here, they will be set to
the default values (2 and 1). For IDC_SLIDER_TICK, its range is set from 0 to 50, and function
CSliderCtrl::SetTic(…) is called to set non-linearly distributed tick marks. Here, a loop is used to set all
the tick marks, which will appear at 0, 4, 8, 16…. Slider IDC_SLIDER_SEL also has “Auto ticks” style, its
range is set from 50 to 100, page size set to 40 and line size set to 10. This slider also has “Enable
selection” style, and function CSliderCtrl::SetSelection(…) is called to draw a selection on the slider’s
rail. The range of the selection is from 60 to 90.
There are three parameters in this function. The first parameter nSBCode indicates user’s scrolling
request, which includes left-scroll, right-scroll, left-page-scroll, right-page-scroll, etc. If we want to
customize the behavior of a slider, we need to check this parameter. Parameter nPos is used to specify the
current slider’s position under some situations (it is not valid all the time). The third parameter pScrollBar
is a window pointer to slider or scroll bar control. We can use it to check which slider is being changed,
then make the corresponding response. The slider’s current position can be obtained by calling function
CSliderCtrl::GetPos(). In the sample application, since all sliders are horizontal, only WM_HSCROLL
message is handled:
switch(pScrollBar->GetDlgCtrlID())
{
case IDC_SLIDER_AUTOTICK:
{
szStr="IDC_SLIDER_AUTOTICK";
nSliderPos=((CSliderCtrl *)pScrollBar)->GetPos();
break;
}
case IDC_SLIDER_TICK:
{
szStr="IDC_SLIDER_TICK";
nSliderPos=((CSliderCtrl *)pScrollBar)->GetPos();
break;
}
case IDC_SLIDER_SEL:
{
szStr="IDC_SLIDER_SEL";
nSliderPos=((CSliderCtrl *)pScrollBar)->GetPos();
break;
95
Chapter 5. Common Controls
}
default:
{
szStr="None";
}
}
TRACE("Slider %s, Current Pos=%d\n", (LPCSTR)szStr, nSliderPos);
CDialog::OnHScroll(nSBCode, nPos, pScrollBar);
}
Function CWnd::GetDlgCtrlID() is called to retrieve the control ID of the slider. We use this ID to call
function CWnd::GetDlgItem(…) and get the address of slider control. If the control happens to be one of our
sliders, we call function CSliderCtrl::GetPos() to retrieve its current position, and output the slider ID
along with its current position to the debug window. In order to see the activities of the sliders, the
application must be executed in the debug mode within Developer Studio.
Usually a list box has a single column. If we set “Multi-column” style, the list box can have multiple
horizontal columns. Originally the list box will be empty, when we start to add new items, they will be
added to the first column (column 0). If the first column is full, instead of creating a vertical scroll bar and
continue to add items to this column, the list box will create a new column and begin to fill it. This step will
be repeated until all items are filled into the list box. Here, the width of each column is always the same.
Because a multiple-column list box will always try to extend horizontally rather than vertically, it is
important to let this type of list box have a horizontal scroll bar.
The “Sort” style will be set by default. If we remain this style, all the strings contained in the list box
will be alphabetically sorted. For the “Selection” styles, we have several choices. A “Single” style list box
allows only one item in the list box to be selected at any time. A “Multiple” style list box allows several
items to be selected at the same time. If we enable this style, the user can use SHIFT and CTRL keys
together to select several items. Besides these two, there is also an “Extended” style. If we enable it, the
items can be selected or deselected through mouse dragging. Finally, list box with “Owner draw” style
allows us to implement it so that the list box can contain non-string items. In this case, we need to provide
custom list box interface.
Sample 5.6\CCtl demonstrates basic styles of list box. It is a dialog-based application created by
Application Wizard. There are three list boxes implemented in the application, whose IDs are IDC_LIST,
IDC_LIST_MULCOL and IDC_LIST_DIR respectively. The styles of IDC_LIST are all set to default, it is a single
selection, single column, sorted list box with a vertical scroll bar. The styles of IDC_LIST_MULCOL are
multiple-column, multiple-selection, it does not support sort. The styles of IDC_LIST are also set to default,
except that it supports “extended selection” style. To access these list boxes, three CListCtrl type member
96
Chapter 5. Common Controls
variables m_listBox, m_listMCBox and m_listDir are declared in class CCtlDlg through using Class
Wizard (Figure 5-7).
Unless we initialize the content of these list boxes, they will be empty at the beginning. Like other
common controls, initialization procedure of list box is usually implemented in function CDialog::
OnInitDlalog(). To fill a list box with strings, we need to call function CListBox::AddString(…). Strings
will be added starting from item 0, 1, 2… and so on (If a list box has a sorted style, the string will be sorted
automatically). Besides this function, we can also use function Clistbox::InsertString(…) to insert a
new string before certain item instead of adding it to the end of the list. The following code fragment shows
how the content of list boxes IDC_LIST and IDC_MULCOL are filled:
BOOL CCtlDlg::OnInitDialog()
{
……
m_listBox.AddString("List box item 1");
m_listBox.AddString("List box item 2");
m_listBox.AddString("List box item 3");
m_listBox.AddString("List box item 4");
m_listMCBox.AddString("Item 1");
m_listMCBox.AddString("Item 2");
……
m_listMCBox.AddString("Item 19");
m_listMCBox.AddString("Item 20");
……
}
Step 5. Click
Add Variable… Step 1: Execute
View | ClassWizard…
Step 6. Select class
name, input variable
name
Step 3. Select
CCtrl class
Step 4. Select one of
the IDs Step 2. Click
Member Variables
Step 7. Click OK buttons
If we do not specify the column width for a multiple-column list box, the default column width will be
used. We may set this width by calling function CListBox::SetColumnWidth(…). In the sample 5.6\CCtl,
the column width of IDC_LIST_MULCOL is set 50 (pixels) as follows:
BOOL CCtlDlg::OnInitDialog()
{
……
m_listMCBox.AddString("Item 20");
m_listMCBox.SetColumnWidth(50);
……
}
All the columns will have the same width. For list box IDC_LIST_DIR, instead of filling each entry with
a string, we can let it display a list of directories and file names for the current working directory. This can
be implemented by calling function CListBox::Dir(…), which has the following format:
Here, the first parameter specifies file attributes, which can be used to specify what type of files can be
added to the list. The following is a list of some attributes that are commonly used:
Attribute Meaning
97
Chapter 5. Common Controls
The second parameter is a string, which can be used to set file filter. In the sample, this function is
called as follows:
BOOL CCtlDlg::OnInitDialog()
{
……
m_listDir.Dir(0x10, "*.*");
return TRUE;
}
Here, value 0x10 is passed to the first parameter of function CListBox::Dir(…) to let normal files
along with directories be listed, also, we use “*.*” wildcards to allow all types of names to be added to the
list.
When testing the sample application, we can use mouse along with SHIFT and CONTROL keys to
select items. Also, we can drag mouse over items to test extended selection style.
98
Chapter 5. Common Controls
clicked contains a file name or a directory name by examining if the string starts and ends with square
brackets. If we allow drive names to be displayed, the items containing drive names will be displayed in the
format of “[-X-]”, where X represents the drive name. In our samples, this situation is not considered).
The following is the implementation of function CCtlDlg::OnDblclkListDir(). This function
examines the clicked item. If the item contains a directory name, we need to update the contents of the list
box (File and directory names under the directory being clicked will be retrieved and filled into the list
box):
void CCtlDlg::OnDblclkListDir()
{
CString szStr;
int nIndex;
First function CListCtrl::GetSelItems(…) is called to retrieve the currently selected item. The result
is stored to a local variable nIndex. Then text string of the selected item is obtained by calling function
CListCtrl::GetText(…). If the string starts with “[”, it is a directory, and we extract the directory name by
calling function CString::Mid(…). Then contents of the list box are cleared by calling function
CListCtrl::ResetContent(). Next, the current working directory is changed by calling function
_chdir(…). Finally, the list box is filled with the new directory and file names by calling function
CListCtrl::Dir(…). Since function _chdir(…) is not an MFC function, we need to include “direct.h”
header file in order to use it.
Message WM_DESTROY
Besides directory changing, another new feature is also implemented in the sample application: we can
use mouse to highlight any item contained in the other two list boxes. When the dialog box is closed, a
message box will pop up displaying all the items that are currently being selected.
Before a window is destroyed, it will receive a WM_DESTROY message, so we can handle this message to
do clean up work. In our case, this is the best place to retrieve the final state of the list boxes. Please note
that we can not do this in the destructor of class CCtlDlg, because at that time the dialog box window and
all its child window have already been destroyed. If we try to access them, it will cause the application to
malfunction.
Message handler WM_DESTROY can be added by using Class Wizard through following steps: 1) Click
“Message maps” tab and choose “CCtlDlg” class in window “Class name”. 2) Highlight “CCtlDlg” in
window “Object IDs”. 3) In “Messages” window, find “WM_DESTROY” message and click “Add
function” button. After the above steps, a new function CCtlDlg::OnDestroy() will be added to the
application.
We will retrieve all the text strings of the selected items for three list boxes and display them in a
message box. For list box IDC_LIST_BOX this is easy, because it allows only single selection. We can call
function CListBox::GetCurSel() to obtain the index of the selected item and call CListBox::GetText(…)
to retrieve the text:
void CCtlDlg::OnDestroy()
{
CString szStrList;
CString szStr;
int nIndex;
int nSelSize;
int i;
LPINT lpIndices;
nIndex=m_listBox.GetCurSel();
if(nIndex != LB_ERR)
99
Chapter 5. Common Controls
{
szStrList="Content of list box:";
m_listBox.GetText(nIndex, szStr);
szStrList+="\n\t";
szStrList+=szStr;
szStr.Empty();
}
……
}
Function CListBox::GetCurSel() will return value LB_ERR if nothing is being currently selected or the
list box has a multiple-selection style. If there is a selected item, we use CString type variable szStrList
to retrieve the text of that item.
For list box IDC_LIST_MULCOL and IDC_LIST_DIR, things become a little complicated become both of
them allow multiple-selection. We need to first find out how many items are being selected, then allocate
enough buffers for storing the indices of the selected items, and use a loop to retrieve the text of each item.
Each time a new string is obtained, it is appended to the end of szStrList. The following code fragment
shows how the text of all the selected items is retrieved for list box IDC_LIST_MULCOL:
void CCtlDlg::OnDestroy()
{
……
nSelSize=m_listMCBox.GetSelCount();
if(nSelSize != LB_ERR && nSelSize != 0)
{
lpIndices=new int[nSelSize];
ASSERT(m_listMCBox.GetSelItems(nSelSize, lpIndices) != LB_ERR);
szStrList+="\n\n";
szStrList+="Content of multi-column list box:";
for(i=0; i<nSelSize; i++)
{
szStrList+="\n\t";
m_listMCBox.GetText(*(lpIndices+i), szStr);
szStrList+=szStr;
szStr.Empty();
}
delete []lpIndices;
}
……
}
In this function, first the number of selected items is retrieved by calling function
CListBox::GetSelCount(), and the retrieved value is saved to variable nSelSize. If the size is not zero,
we allocate an integer type array with size of nSelSize. Then by calling function
CListBox::GetSelItems(…), we fill this buffer with the indices of selected items. Next, a loop is used to
retrieve the text of each item. The procedure of retrieving selected text for list box IDC_LIST_DIR is the
same.
100
Chapter 5. Common Controls
and CBN_SELCHANGE. The first message indicates that the user has clicked the drop-down arrow button,
made a selection from the list box, and the drop down list is about to be closed. The second message
indicates that the user has selected a new item.
In MFC, combo box is supported by class CComboBox. Like CListBox, class CComboBox has a function
CComboBox::AddString(…) which can be used to initialize the contents of its list box. Besides this, we can
also initialize the contents of a list box when designing dialog template. In the property sheet whose caption
is “Combo Box Properties”, by clicking “Data” tab, we will have a multiple-line edit box that can be used
to input initial data for combo box. We can use CTRL+RETURN keys to begin a new line (Figure 5-8).
Class CComboBox has two functions that allow us to change the contents contained in the list box
dynamically: CComboBox::InsertString(…) and CComboBox:: DeleteString(…).
When designing drop-down combo box, we must set its vertical size, otherwise it will be set to the
default value zero. In this case, there will be no space for the list box to be dropped down when the user
clicks drop down button. To set this size, we can click the drop-down button in the dialog template. After
doing this, a resizable tracker will appear. The initial size of a combo box can be adjusted by dragging the
tracker’s border (Figure 5-9).
101
Chapter 5. Common Controls
window “Class name”. 2) Highlight the ID of the combo box (IDC_COMBO_SIMPLE, IDC_COMBO_DROPDOWN or
IDC_COMBO_DROPLIST), press “Add variable” button. 3) Select “Control” category and input the variable
name.
In function CCCtlDlg::OnInitDialog(), the contents of combo box IDC_COMBO_DROPDOWN are
initialized through calling function CComboBox::AddString(…):
BOOL CCCtlDlg::OnInitDialog()
{
CDialog::OnInitDialog();
……
m_cbDropDown.AddString("Item 1");
m_cbDropDown.AddString("Item 2");
m_cbDropDown.AddString("Item 3");
m_cbDropDown.AddString("Item 4");
return TRUE;
}
void CCCtlDlg::OnCloseupComboDropdown()
{
CString szStr;
int nSel;
nSel=m_cbDropDown.GetCurSel();
if(nSel != CB_ERR)
{
m_cbDropDown.GetLBText(nSel, szStr);
}
GetDlgItem(IDC_STATIC_DROPDOWN)->SetWindowText(szStr);
}
First index of the current selection is retrieved and stored in variable nSel. Then we check if the
returned value is CB_ERR. This is possible if there is nothing being currently selected. If the returned value
is a valid index, we call function CComboBox::GetLBText(…) to retrieve the text string and store it in
CString type variable szStr. Finally function CWnd::GetDlgItem(…) is called to obtain the pointer to the
static text window, and CWnd::SetWindowText(…) is called to update its contents.
102
Chapter 5. Common Controls
From this structure, we know which window is going to receive the message (from member hwnd),
what kind of message it is (from member message). Also, we can obtain the message parameters from
members wParam and lParam. If the message is not the one we want to intercept, we can just forward the
message to its original destination by calling the base class version of this function.
Function CWnd::PreTranslateMessage(…)
Sample 5.9\CCtl demonstrates how to trap RETURN keystrokes for combo box. It is based on sample
5.8\CCtl. First, function PreTranslateMessage(…) is overridden. This function can be added by using
Class Wizard through following steps: 1) Open Class Wizard, click “Message Maps” tab, select
“CCCtlDlg” from “Class name” window. 2) Highlight “CCCtlDlg” in window “Object IDs”. 3) Locate and
highlight “PreTranslateMessage” in window “Messages”. 4) Press “Add function” button.
The default member function looks like the following:
103
Chapter 5. Common Controls
CEdit *ptrEdit;
int nVirtKey;
char szClassName[256];
if(pMsg->message == WM_KEYDOWN)
{
nVirtKey=(int)pMsg->wParam;
if(nVirtKey == VK_RETURN)
{
}
}
……
Message WM_KEYDOWN is a standard Windows message for non-system key strokes, and VK_RETURN is
a standard virtue key code defined for RETUN key (For a list of virtual key codes, see appendix A). Some
local variables are declared at the beginning. They will be used throughout this function.
Here nCmd specifies what kind of window is being looked for. To enumerate all the child windows, we
need to call this function using GW_CHILD flag to find the first child window, then, use GW_HWNDNEXT to call
the same function repeatedly until it returns a NULL value. This will enumerate all the sibling windows of
the first child window.
There are still problems here: function CWnd::GetWindow(…) returns a CWnd type pointer, we can not
obtain further information about that window (i.e. is it an edit box or a list box?). Since a combo box has
two child windows, although we can access both of them with the above-mentioned method, we do not
know which one is the edit box.
In Windows, before a new type of window is created, it must register a special class name to the
system. Every window has its own class name, which could be used to tell the window’s type. In the case
of combo box, its edit box’s class name is “Edit” and its list box’s class name is “ComboLBox”. Please
note that this class name has nothing to do with MFC classes. It is used by the operating system to identify
the window types rather than a programming implementation.
In MFC, the procedure of creating windows is handled automatically, so we never bother to register
class names for the windows being created, therefore, we seldom need to know the class names of our
windows.
A window’s class name can be retrieved from its handle by calling an API function:
104
Chapter 5. Common Controls
The first parameter hWnd is the handle of window whose class name is being retrieved; the second
parameter lpClassName is the pointer to a buffer where the class name string can be put; the third
parameter nMaxCount specifies the length of this buffer.
We can access the first child window of the combo box, see if its class name is “Edit”. If not, the other
child window must be the edit box. This is because a combo box has only two child windows.
A window’s handle can be obtained by calling function CWnd::GetSafeHwnd(). If the window that has
the current focus is the edit box of a combo box when RETURN is pressed, we need to notify the parent
window about this event. In the sample, a user defined message is used to implement this notification:
……
if(nVirtKey == VK_RETURN)
{
hwndFocus=GetFocus()->GetSafeHwnd();
if(hwndFocus == NULL)return CDialog::PreTranslateMessage(pMsg);
ptrEdit=(CEdit *)m_cbDropDown.GetWindow(GW_CHILD);
hwndEdit=ptrEdit->GetSafeHwnd();
::GetClassName(hwndEdit, szClassName, 256);
if(memcmp(szClassName, "Edit", sizeof("Edit")))
{
ptrEdit=(CEdit *)ptrEdit->GetWindow(GW_HWNDNEXT);
hwndEdit=ptrEdit->GetSafeHwnd();
}
if(hwndFocus == hwndEdit)
{
PostMessage(WM_COMBO_RETURN, (WPARAM)IDC_COMBO_DROPDOWN, (LPARAM)0);
return TRUE;
}
}
……
First the handle of currently focused window is stored in variable hwndFocus. If it is a valid window
handle, we use m_cbDropDown to get the first child window of IDC_COMBO_DROPDOWN. Then this child
window’s class name is retrieved by calling function ::GetClassName(…). If the class name is “Edit”, we
compare its handle with the focused window handle. Otherwise we need to get the handle of the other child
window before doing the comparison. This will assure that the handle being compared is the handle of the
edit box. If the edit box has the current focus, we post the user defined message WM_COMBO_RETURN, whose
WPARAM parameter is assigned the ID of combo box. Finally a TRUE value is returned to prevent the dialog
box from further processing this message.
Message WM_COMBO_RETURN is processed in class CCCtlDlg. The member function used to trap this
message is CCCtlDlg::OnComboReturn(…). The following code fragment shows how this function is
declared and message mapping is implemented:
Function declaration:
BEGIN_MESSAGE_MAP(CCCtlDlg, CDialog)
……
ON_MESSAGE(WM_COMBO_RETURN, OnComboReturn)
END_MESSAGE_MAP()
105
Chapter 5. Common Controls
Function implementation:
ptrCombo=(CComboBox *)GetDlgItem(wParam);
ptrEdit=(CEdit *)ptrCombo->GetWindow(GW_CHILD);
hwndEdit=ptrEdit->GetSafeHwnd();
::GetClassName(hwndEdit, szClassName, 256);
if(memcmp(szClassName, "Edit", sizeof("Edit")))
{
ptrEdit=(CEdit *)ptrEdit->GetWindow(GW_HWNDNEXT);
hwndEdit=ptrEdit->GetSafeHwnd();
}
ptrEdit->GetWindowText(szStr);
if(!szStr.IsEmpty())
{
ptrEdit->SetSel(0, -1);
nSize=ptrCombo->GetCount();
bHit=FALSE;
for(i=0; i<nSize; i++)
{
ptrCombo->GetLBText(i, szStrLB);
if(szStrLB == szStr)
{
bHit=TRUE;
break;
}
}
}
if(bHit == FALSE)
{
ptrCombo->AddString(szStr);
if(wParam == IDC_COMBO_DROPDOWN)
{
GetDlgItem(IDC_STATIC_DROPDOWN)->SetWindowText(szStr);
}
if(wParam == IDC_COMBO_SIMPLE)
{
GetDlgItem(IDC_STATIC_SIMPLE)->SetWindowText(szStr);
}
}
return (LRESULT)TRUE;
}
In this message handler, we first obtain a pointer to the combo box using the ID passed through WPARAP
message parameter. Then we use above-mentioned method to get the pointer to the edit box (a child
window of combo box), and assign it to variable ptrEdit. Then we use this pointer to call function CWnd::
GetWindowText(…) to retrieve the text contained in the edit box window. If the edit box is not empty (this is
checked by calling function CString::IsEmpty()), we select all the text in the edit box by calling function
CEdit::SetSel(…), which has the following format:
The first two parameters of this function allow us to specify a range indicating which characters are to
be selected. If we pass 0 to nStartChar and -1 to nEndChar, all the characters in the edit box will be
selected. Then we use a loop to check if the text contained in the edit box is identical to any item string in
the list box. In case there is no hit, we will add this string to the list box by calling function
CComboBox::AddString(…). Finally, a TRUE value is returned before this function exits.
Using this method, we can also trap other keystrokes such as DELETE, ESC to the combo box. This
will make the application easier to use.
106
Chapter 5. Common Controls
This function has three parameters. The first parameter nChar indicates the value of the key, which
provides us with the information of which key being pressed. The Second parameter indicates the repeat
count, and the third parameter holds extra information about the keystrokes.
If we want the keystroke to be processed normally, we need to call the base class version of this
function. If we do not call this function, the input will have no effect on the edit box. The following code
fragment shows two message handlers implemented in the sample:
107
Chapter 5. Common Controls
Class MCNumEdit accepts characters ‘0’-‘9’ and backspace key, class MCCharEdit accepts characters
‘A’-‘Z’, ‘a’-‘z’ and backspace key.
Implementing Subclass
To use the two classes, we need to include their header files and use them to declare two new variables
in class CCCtlDlg:
……
#include "CharEdit.h"
#include "NumEdit.h"
……
In the dialog box’s initialization stage, we need to implement subclass and change the default behavior
of the edit boxes. Remember in the previous chapter, function CWnd::SubclassDlgItem(…) is used to
implement subclass for an item contained in a dialog box. Although the edit box within a combo box is a
indirect child window of the dialog box, it is not created from dialog template. So here we must call
function CWnd::SubclassWindow(…) to implement subclass. The following is the format of this function:
Here, parameter hWnd is the handle of the window whose behavior is to be customized. From sample
5.9\CCtl, we know how to obtain the handle of the edit box that belongs to a combo box. The following is
the procedure of implementing subclass for IDC_COMBO_DROPDOWN combo box:
BOOL CCCtlDlg::OnInitDialog()
{
CEdit *ptrEdit;
HWND hwndEdit;
char szClassName[256];
CDialog::OnInitDialog();
……
ptrEdit=(CEdit *)m_cbDropDown.GetWindow(GW_CHILD);
hwndEdit=ptrEdit->GetSafeHwnd();
::GetClassName(hwndEdit, szClassName, 256);
if(memcmp(szClassName, "Edit", sizeof("Edit")))
{
ptrEdit=(CEdit *)ptrEdit->GetWindow(GW_HWNDNEXT);
hwndEdit=ptrEdit->GetSafeHwnd();
}
m_editChar.SubclassWindow(hwndEdit);
……
}
With the above implementation, the combo box is able to filter out the characters we do not want.
108
Chapter 5. Common Controls
item contained in the list box must have a same height. For a “variable” type of owner draw list box or
combo box, this height can be variable. Like the menu, the owner-draw list box or combo box are drawn by
their owner. The owner will receive message WM_MEASUREITEM and WM_DRAWITEM when the list box or the
combo box needs to be updated. For “fixed” type owner draw list box or combo box, WM_MEASUREITEM is
sent when it is first created and the returned size will be used for all items. For “variable” type owner-draw
list box or combo box, this message is sent for each item separately. Message WM_DRAWITEM will be sent
when the interface of list box or combo box needs to be updated.
Owner-Draw Styles
Sample 5.11\CCtl demonstrates owner-draw list box and combo box. It is a dialog based application
generated by Application Wizard. There are only two common controls contained in the dialog box: a list
box IDC_LIST and a combo box IDC_COMBO. The list box supports “Fixed” owner-draw style, and the
combo box supports “Variable” owner-draw style. The “Sort” style is not applicable to an owner-draw list
box or combo-box, because their items will not contain characters.
Preparing Bitmaps
Six bitmap resources are added to the application for list box and combo box drawing. Among them,
IDB_BITMAP_SMILE_1, IDB_BITMAP_SMILE_2, IDB_BITMAP_SMILE_3 and IDB_BITMAP_SMILE_4 have the
same dimension, they will be used for implementing owner-draw list box. Bitmaps
IDB_BITMAP_BUTTON_SEL and IDB_BITMAP_BUTTON_UNSEL have a different size with the above four bitmaps,
they will be used together with IDB_BITMAP_BIG_SMILE_1 and IDB_BITMAP_BIG_SMILE_2 to implement
owner-draw combo box.
#define COMBO_BUTTON 0
#define COMBO_BIGSMILE 1
#define LIST_SMILE_1 0
#define LIST_SMILE_2 1
#define LIST_SMILE_3 2
#define LIST_SMILE_4 3
Each macro represents a different bitmap. We will use these macros to set item data for list box and
combo box. Since the item data will be sent along with message WM_DRAWITEM, we can use it to identify
item types. This is the same with owner-draw menu.
Two CComboBox type variables m_cbBmp and m_lbBmp are declared in class CCCtlDlg through using
Class Wizard, they will be used to access the list box and the combo box. In function
CCCtlDlg::OnInitDialog(), the list box and the combo box are initialized as follows:
BOOL CCCtlDlg::OnInitDialog()
{
……
m_cbBmp.AddString((LPCTSTR)COMBO_BUTTON);
m_cbBmp.AddString((LPCTSTR)COMBO_BIGSMILE);
m_cbBmp.AddString((LPCTSTR)COMBO_BUTTON);
m_lbBmp.AddString((LPCTSTR)LIST_SMILE_1);
m_lbBmp.AddString((LPCTSTR)LIST_SMILE_2);
m_lbBmp.AddString((LPCTSTR)LIST_SMILE_3);
m_lbBmp.AddString((LPCTSTR)LIST_SMILE_4);
return TRUE;
}
Instead of adding a real string, we pass predefined integers to function CComboBox::AddString(…) and
CListBox::AddString(…) For owner-draw list box and combo box, these integers will not be used as
buffer addresses for obtaining strings. Instead, they will be sent along with message WM_MEASUREITEM to
inform us the item type.
109
Chapter 5. Common Controls
This function is called to retrieve the size of item. It has two parameters, the first parameter nIDCtl
indicates the ID of control whose item’s size is being retrieved. The second parameter is a pointer to a
DRAWITEMSTRUCT object, and we will use its itemData member to identify the type of the item. Since the
value of this member is set in the dialog’s initialization stage by calling function
CComboBox::AddString(…), it must be one of our predefined macros (LIST_SMILE_1, LIST_SMILE_2…). In
the overridden function, we need to check the value of nIDCtl and lpDrawItemStruct->itemData, load the
corresponding bitmap resource into a CBitmap type variable, call function CBitmap::GetBitmap(…) to
retrieve the dimension of the bitmap, and use it to set both lpDrawItemStrut->itemWidth and
lpDrawItemStrut->itemHeight:
switch(nIDCtl)
{
case IDC_LIST:
{
bmp.LoadBitmap(IDB_BITMAP_SMILE_1);
bmp.GetBitmap(&bm);
lpMeasureItemStruct->itemWidth=bm.bmWidth;
lpMeasureItemStruct->itemHeight=bm.bmHeight;
break;
}
case IDC_COMBO:
{
switch(lpMeasureItemStruct->itemData)
{
case COMBO_BUTTON:
{
bmp.LoadBitmap(IDB_BITMAP_BUTTON_SEL);
bmp.GetBitmap(&bm);
lpMeasureItemStruct->itemWidth=bm.bmWidth;
lpMeasureItemStruct->itemHeight=bm.bmHeight;
break;
}
case COMBO_BIGSMILE:
{
bmp.LoadBitmap(IDB_BITMAP_BIG_SMILE_1);
bmp.GetBitmap(&bm);
lpMeasureItemStruct->itemWidth=bm.bmWidth;
lpMeasureItemStruct->itemHeight=bm.bmHeight;
break;
}
}
break;
}
}
}
It also has two parameters. Like CWnd::OnMeasureItem(…), the first parameter of this function is the
control ID, and the second parameter is a pointer to a DRAWITEMSTRUCT type object. This structure contains
all the information we need to draw an item of list box or combo box: the DC handle, the item’s state, the
item data, the position and size where the drawing should be applied. The following portion of the
110
Chapter 5. Common Controls
overridden function shows how to load correct bitmap by examining nIDCtl and lpDrawItemStruct-
>itemData:
if(nIDCtl == IDC_LIST)
{
switch(lpDrawItemStruct->itemData)
{
case LIST_SMILE_1:
{
bmp.LoadBitmap(IDB_BITMAP_SMILE_1);
break;
}
case LIST_SMILE_2:
{
bmp.LoadBitmap(IDB_BITMAP_SMILE_2);
break;
}
case LIST_SMILE_3:
{
bmp.LoadBitmap(IDB_BITMAP_SMILE_3);
break;
}
case LIST_SMILE_4:
{
bmp.LoadBitmap(IDB_BITMAP_SMILE_4);
break;
}
}
}
if(nIDCtl == IDC_COMBO)
{
switch(lpDrawItemStruct->itemData)
{
case COMBO_BUTTON:
{
if(lpDrawItemStruct->itemState & ODS_SELECTED)
{
bmp.LoadBitmap(IDB_BITMAP_BUTTON_SEL);
}
else
{
bmp.LoadBitmap(IDB_BITMAP_BUTTON_UNSEL);
}
break;
}
case COMBO_BIGSMILE:
{
if(lpDrawItemStruct->itemState & ODS_SELECTED)
{
bmp.LoadBitmap(IDB_BITMAP_BIG_SMILE_1);
}
else
{
bmp.LoadBitmap(IDB_BITMAP_BIG_SMILE_2);
}
break;
}
}
}
……
Five local variables are declared: bmp is used to load the bitmap; dcMemory is used to create memory
DC and implement image copying; ptrBmpOld is used to restore the original state of dcMemory; ptrDC is
used to store the target DC pointer, which is obtained from hDC member of structure DRAWITEMSTRUCT; bm is
used to store the information (including dimension) of the bitmap; rect is used to store the position and
size where the bitmap should be copied.
111
Chapter 5. Common Controls
From the above source code we can see, if the control is IDC_LIST, we load one of the four bitmaps
(IDB_BITMAP_SMILE_1, IDB_BITMAP_SMILE_2, IDB_BITMAP_SMILE_3 or IDB_BITMAP_SMILE_4) according to
the value of lpDrawItemStruct->itemData. If the control is IDC_COMBO, we load IDB_BITMAP_BUTTON_SEL
or IDB_BITMAP_BIG_SMILE_1 if the item is selected; and load IDB_BITMAP_BUTTON_UNSEL or
IDB_BITMAP_BIG_SMILE_2 if the item is not selected. Here, ODS_SELECTED bit of member
lpDrawItemStruct->itemState is checked to retrieve item’s state.
The following portion of function CCCtlDlg::OnDrawItem(…) draws the bitmap:
……
if((nIDCtl == IDC_COMBO || nIDCtl == IDC_LIST) && bmp.GetSafeHandle())
{
bmp.GetBitmap(&bm);
ptrDC=CDC::FromHandle(lpDrawItemStruct->hDC);
dcMemory.CreateCompatibleDC(ptrDC);
rect=lpDrawItemStruct->rcItem;
ptrBmpOld=dcMemory.SelectObject(&bmp);
ptrDC->BitBlt
(
rect.left,
rect.top,
rect.Width(),
rect.Height(),
&dcMemory,
0,
0,
SRCCOPY
);
if((lpDrawItemStruct->itemState & ODS_SELECTED) && nIDCtl == IDC_LIST)
{
ptrDC->BitBlt
(
rect.left,
rect.top,
bm.bmWidth,
bm.bmHeight,
NULL,
0,
0,
DSTINVERT
);
}
dcMemory.SelectObject(ptrBmpOld);
}
}
Only after the bitmap is loaded successfully will we draw the list box or combo box item. First
function CDC::FromHandle(…) is called to obtain a CDC type pointer from HDC handler. Then we create a
memory DC (compatible with target DC) and select bmp into this DC. Next, function CDC::BitBlt(…) is
called to copy the bitmap from memory DC to target DC. For list box items, there is no special bitmaps for
their selected states. In case if an item is selected, the corresponding normal bitmap will be drawn using
DSTINVERT mode. This will cause every pixel of the bitmap to change to its complement color. When we
pass DSTINVERT to function CDC::BitBlt(…), its fifth argument can be set to NULL.
112
Chapter 5. Common Controls
Image List
We can associate a bitmap image with each node contained in the tree control. This will make the tree
control more intuitive. For example, in a file manager application, we may want to use different images to
represent different file types: folder, executable file, DLL file, etc. Before using the images to implement
the tree control, we must first prepare them. For tree control (also list control and tab control), these images
must be managed by Image List, which is supported by class CImageList in MFC.
Class CImageList can keep and manage a collection of images with the same size. Each image in the
list is assigned a zero-based index. After an image list is created successfully, it can be selected into the tree
control. We can associate a node with any image contained in the image list. Here image drawing is
handled automatically.
If we provide mask bitmaps, only the unmasked portion of the images will be drawn for representing
nodes. A mask bitmap must contain only black and white colors. Besides preparing mask bitmaps by
ourselves, we can also generate mask bitmaps from the normal images.
To use class CImageList, first we need to declare a CImageList type variable. If we create an image
list dynamically by using “new” operator, we need to release the memory when it is no longer in use.
Before adding images to the list, we need to call function CImageList::Create(…) to initialize it. This
function has several versions, the following is one of them:
BOOL CImageList::Create(int cx, int cy, UINT nFlags, int nInitial, int nGrow);
Here cx and cy indicate the dimension of all images, nInitial represents the number of initial bitmaps
that will be included in the image list, nGrow specifies the number of bitmaps that can be added later.
Parameter nFlags indicates bitmap types, it could be ILC_COLOR, ILC_COLOR4, ILC_COLOR8, etc., which
specify the bitmap format of the images. For example, ILC_COLOR indicates default bitmap format,
ILC_COLOR4 indicates 4-bit DIB format (16-color), ILC_COLOR8 indicates 8-bit DIB format (256-color). We
can combine ILC_MASK with any of these bitmap format flags to let the image be drawn with transparency.
The images can be added by calling function CImageList::Add(…). Again, this function has three
versions:
The image list can be created from either bitmaps or icons. For the first version of this function, the
second parameter is a pointer to the mask bitmap that will be used to implement transparent background
drawing. The second version allows us to specify a background color that can be used to generate a mask
bitmap from the normal image. Here parameter crMask will be used to create the mask bitmap: all pixels in
the source bitmap that have the same color with crMask will be masked when the bitmap is being drawn,
and their colors will be set to the current background color. We can choose a background color by calling
function CImageList::SetBkColor(…).
To use image list with a tree control, we need to call function CTreeCtr::SetImageList(…) to assign it
to tree control. Then, when creating a node for the tree control, we can use the bitmap index to associate
any node with this image.
Adding Nodes
At the beginning, the tree control does not contain any node. Like other common controls, we can
initialize it in function CDialog::OnInitDialog(). To add a node to the tree, we need to call function
CTreeCtrl::InsertItem(…).
This function also has several versions. The following is the one that has the simplest format:
113
Chapter 5. Common Controls
TV_ITEM item;
} TV_INSERTSTRUCT;
In a tree control, nodes are managed through handles. After a node is created, it will be assigned an
HTREEITEM type handle. Each node has a different handle, so we can use the handle to access a specific
node. In the above structure, member hParent indicates which node is the parent of the new node. If we
assign NULL to this member, the new node will become the root node. Likewise, member hInsertAfter is
used to indicate where the new node should be inserted. We can specify a node handle, or we can use
predefined parameters TVI_FIRST, TVI_LAST or TVI_SORT to insert the new node after the first node, last
node or let the nodes be sorted automatically.
Member item is a TV_ITEM type object, and the structure contains the information of the new node:
In order to add new nodes, we need to understand how to use the following four members of this
structure: mask, pszText, iImage and iSelectedImage.
Member mask indicates which of the other members in the structure contain valid data. Besides mask,
every member of this structure has a corresponding mask flag listed as follows:
In order to use members pszText, iImage and iSelectedImage, we need to set the following bits of
member mask:
Member pszText is a pointer to a null-terminated string text that will be used to label this node.
Member iImage and iSelectedImage are indices to two images contained in the image list that will be
used to represent the node’s normal and selected state respectively.
By calling function CTreeCtrl::InsertItem(…) repeatedly, we could create a tree structure with
desired number of nodes.
Sample
Sample 5.12\CCtl demonstrates how to use tree control in a dialog box. It is a dialog based application
generated by Application Wizard. There is only one tree control IDC_TREE in the dialog template. To access
it, a member variable CCCtlDlg::m_treeCtrl is added for IDC_TREE through using Class Wizard.
To create the image list, five bitmap resources are prepared, whose IDs are
IDB_BITMAP_CLOSEDFOLDER, IDB_BITMAP_DOC, IDB_BITMAP_LEAF, IDB_BITMAP_OPENFOLDER and
IDB_BITMAP_ROOT respectively. These bitmaps have the same dimension.
In function CCCtlDlg::OnInitDlalog(), the image list is created as follows:
BOOL CCCtlDlg::OnInitDialog()
{
TV_INSERTSTRUCT tvInsertStruct;
CBitmap bmp;
114
Chapter 5. Common Controls
HTREEITEM hTreeItem;
CImageList *pilCtrl;
……
pilCtrl=new CImageList();
pilCtrl->Create(BMP_SIZE_X, BMP_SIZE_Y, ILC_MASK, 5, 0);
bmp.LoadBitmap(IDB_BITMAP_ROOT);
pilCtrl->Add(&bmp, RGB(255, 255, 255));
bmp.DeleteObject();
bmp.LoadBitmap(IDB_BITMAP_DOC);
pilCtrl->Add(&bmp, RGB(255, 255, 255));
bmp.DeleteObject();
……
pilCtrl->SetBkColor(RGB(255, 255, 255));
m_treeCtrl.SetImageList(pilCtrl, TVSIL_NORMAL);
}
A CBitmap type local variable bmp is declared to load the bitmap resources. First, function
CImageList::Create(…) is called to create the image list. Here macro BMP_SIZE_X and BMP_SIZE_Y are
defined at the beginning of the implementation file, they represent the dimension of the bitmaps:
#define BMP_SIZE_X 16
#define BMP_SIZE_Y 15
We use ILC_MASK flag to let the bitmaps be drawn with transparent background. Originally the image
list has five bitmaps, it will not grow later (The fourth and fifth parameter of function
CImageList::Create(…) are 5 and 0 respectively).
Next we use variable bmp to load each bitmap resource and add it to the list. When calling function
CImageList::Add(…), we pass a COLORREF type value to its second parameter (RGB macro specifies the
intensity of red, green and blue colors, and returns a COLORREF type value). This means all the white color
in the image will be treated as the background. In the sample application, the background color is set to
white:
We can also change the values contained in the RGB macro to set the background to other colors.
Besides this method, we can also prepare all the images in one bitmap resource (just like the tool bar
resource), and call the following versions of function CImageList::Create(…) to create the image list:
Here nBitmapID or lpszBitmapID specifies the bitmap resource ID, and cx specifies the horizontal
dimension of an individual image. With this parameter, the system knows how to divide one big image into
several small images.
After creating the image list, function CTreeCtrl::SetImageList(…) is called to assign the image list
to the tree control:
……
m_treeCtrl.SetImageList(m_pilCtrl, TVSIL_NORMAL);
……
Since the image list is created dynamically, we need to release it when it is no longer in use. The best
place to destroy the image list is in CDialog::OnDestroy(), when the dialog box is about to be destroyed.
This function is the handler of WM_DESTROY message, which could be easily added through using Class
Wizard. The following is the implementation of this function in the sample:
……
void CCCtlDlg::OnDestroy()
{
CImageList *pilCtrl;
pilCtrl=m_treeCtrl.GetImageList(TVSIL_NORMAL);
pilCtrl->DeleteImageList();
delete pilCtrl;
115
Chapter 5. Common Controls
m_treeCtrl.DeleteAllItems();
CDialog::OnDestroy();
}
……
We call function CImageList::GetImageList(…) to obtain the pointer to the image list, then call
CImageList::DeleteImageList() to delete the image list. Please note that this function releases only the
images stored in the list, it does not delete CImageList type object. After the image list is deleted, we still
need to use keyword “delete” to delete this object.
In the sample, a tree with the structure showed in Figure 5-10 is created.
This tree has 7 nodes. Node “Root” is the root node, it has one child node “Doc”. Node “Doc” has a
child node “Folder”, and node “Folder” has four child nodes “Leaf1”, “Leaf2”, “Leaf3” and “Leaf4”. The
following portion of function CCCtlDlg::OnInitDialog() shows how the node “Root” is created in the
sample:
……
tvInsertStruct.hParent=NULL;
tvInsertStruct.hInsertAfter=TVI_LAST;
tvInsertStruct.item.iImage=0;
tvInsertStruct.item.iSelectedImage=0;
tvInsertStruct.item.pszText="Root";
tvInsertStruct.item.mask=TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_TEXT;
hTreeItem=m_treeCtrl.InsertItem(&tvInsertStruct);
ASSERT(hTreeItem);
……
……
tvInsertStruct.hParent=hTreeItem;
tvInsertStruct.hInsertAfter=TVI_LAST;
tvInsertStruct.item.mask=TVIF_IMAGE | TVIF_SELECTEDIMAGE| TVIF_TEXT;
tvInsertStruct.item.iImage=1;
tvInsertStruct.item.iSelectedImage=1;
tvInsertStruct.item.pszText="Doc";
hTreeItem=m_treeCtrl.InsertItem(&tvInsertStruct);
ASSERT(hTreeItem);
……
This procedure is exactly the same for other nodes. For different nodes, the only difference of this
procedure is that each node has different parent node, uses different image index and text string. For all
116
Chapter 5. Common Controls
nodes, their normal states and selected states are represented by the same image (member iImage and
iSelectedImage are assigned the same image index), so the image will not change if we select a node.
With the above implementations, the tree control can work. By compiling and executing the
application at this point, we will see a tree with seven nodes, which are represented by different labels and
images. A node can be expanded or collapsed with mouse clicking if it has child node(s).
*pResult = 0;
}
Variable pNMTreeView is a pointer to NM_TREEVIEW type object obtained from the message parameters,
it contains the information about the node being clicked:
The most important member of this structure is action, it could be either TVE_EXPAND (indicating the
node is about to expand) or TVE_COLLAPSE (indicating the node is about to collapse). Two other useful
members are itemOld and itemNew, both of them are TV_ITEM type objects and contain old and new states
of the node respectively. We can check iImage member of itemNew to see if the associated image is 2 or 3
(Indices 2 and 3 correspond to image IDB_BITMAP_CLOSED_FOLDER and IDB_BITMAP_OPENFOLDER
respectively, which indicate that the node represents a folder. In the sample, we will not change other
node’s image when they are being expanded or collapsed), if so, we need to call function
CTreeCtrl::SetItemImage(…) to change the image of the node if necessary.
We can handle this message either within class CTreeCtrl or CDialog. Handling the message in
CTreeCtrl has the advantage that once the feature is implemented, we can reuse this class in other
applications without adding additional code.
In the sample, a new class MCTreeCtrl is designed for this purpose. It is added to the application
through using Class Wizard. Also, message handlers MCTreeCtrl::OnItemexpanding(…) and
117
Chapter 5. Common Controls
MCTreeCtrl::OnEndlabeledit(…) are added to dynamically change node’s associated images and enable
label editing (Label editing will be discussed later).
The following is the implementation of function MCTreeCtrl::OnItemexpanding(…):
If the node is about to expand and its associated image is 2, we associate image 3 with this node. This
is implemented through calling function CTreeCtrl::SetItemImage(…), which has the following format:
The first parameter of this function is the handle of tree control, which can be obtained from
pNMTreeView->itemNew.hItem. Similarly, if the node is about to collapse and its associated image is 3, we
call this function to associate image 2 with this node.
*pResult=0;
}
118
Chapter 5. Common Controls
Here pTVDispInfo is a pointer to TV_DISPINFO type object, which can be obtained from the message
parameter. The most useful member of TV_DISPINFO is item, which is a TV_ITEM type object. Three
members of item contain valid information: hItem, lParam, and pszText. We could use hItem to identify
the node and use pszText to obtain the updated text string. If pszText is a NULL pointer, this means the
editing is canceled (Label editing can be canceled through pressing ESC key). Otherwise it will contain a
NULL-terminated string. The following is the implementation of this message handler:
if(pTVDispInfo->item.pszText != NULL)
{
SetItemText
(
pTVDispInfo->item.hItem,
pTVDispInfo->item.pszText
);
}
*pResult=0;
}
If the editing is not canceled, we need to call function CTreeCtrl::SetItemText(…) to set the node’s
new text, which has the following format:
This function is similar to CTreeCtrl::SetItemImage(…). Its first parameter is the handle of tree
control, and the second parameter is a string pointer to the new label text.
There are other messages associated with label editing, one useful message is TVN_BEGINLABELEDIT,
which will be sent when the editing is about to begin. We can handle this message to disable label editing
for certain nodes. In the message handler, if we assign a non-zero value to the content of pResult, the edit
will stop. Otherwise the label editing will go on as usual.
5.14 Drag-n-Drop
Another nice feature we can add to tree control is to change the tree structure by dragging and
dropping. By implementing this, we can copy or move one node (and all its child nodes) to another place
with few mouse clicks.
Sample 5.14\CCtl demonstrates drag-n-drop implementation. It is base on sample 5.13\CCtl with new
messages handled in class MCTreeCtrl.
119
Chapter 5. Common Controls
*pResult=0;
}
Here, several issues must be considered when a node is being dragged around:
1) To determine which node is being clicked for dragging after receiving message TVB_BEGINDRAG, we
can call API function ::GetCursorPos(…) to retrieve the current position of mouse cursor, call
function CWnd::ScreenToClient(…) to convert its coordinates, and call CTreeCtrl::HitTest(…) to
obtain the handle of the node that is being clicked.
1) We must provide a dragging image that will be drawn under the mouse cursor to give the user an
impression that the node is being “dragged”. An easiest way of preparing this image is to call function
CTreeCtrl::CreateDragImage(…), which will create dragging image using the bitmap associated with
this node. This function will return a CImageList type pointer, which could be further used to
implement dragging. We can also create our own customized image list for dragging operation, the
procedure of creating this type of image list is the same with creating a normal image list.
1) We can call function CImageList::SetDragCursorImage(…) to combine an image contained in the
image list with the cursor to begin dragging.
1) We must lock the tree control window when a node is being dragged around to avoid any change
happening to the tree structure (When a node is being dragged, the tree should not change). When we
want to do a temporary update (For example, when the dragging image enters a node and we want to
highlight that node to indicate that the source can be dropped there), we must first unlock the window,
then implement the update. If we want the dragging to be continued, we must lock the window again.
1) Function CImageList::EnterDrag(…) can be called to enter dragging mode and lock the tree control
window. Before we make any change to the tree control window (For example, before we highlight a
node), we need to call function CImageList::LeaveDrag(…) to unlock the tree control window. After
the updates, we need to call CImageList::EnterDrag(…) again to lock the window. This will prevent
the tree control from being updated when a node is being dragged around.
1) We can show or hide the dragging image by calling function CImageList::DragShowNolock(…)
without locking the tree control window. This function is usually called before
CImageList::SetDragCursorImage(…) is called.
1) To begin dragging, we need to call CImageList::BeginDrag(…); to move the dragging image to a
specified position, we can call CImageList::DragMove(…); to end dragging, we need to call
CImageList::EndDrag().
1) We can highlight a node by calling function CTreeCtrl::SelectDropTarget(…).
Parameter Meaning
bShow TRUE to show the dragging image, FALSE to hide it. This function does not lock the tree
control window.
120
Chapter 5. Common Controls
Parameter Meaning
nDrag Index of the image in the list that will be used as the dragging image.
PtHotSpot Position of the hot spot in the image, this position will be used to do hit test when the
image is being dragged around.
Parameter Meaning
nImage Index of the image in the list that will be used as the dragging image.
ptHotSpot Initial position to start dragging.
Parameter Meaning
pt New position where the dragging image can be put.
Parameter Meaning
pWndLock Pointer to the window that should be locked during the dragging.
point Position where the dragging image can be displayed.
Parameter Meaning
pWndLock The window that has been locked during the dragging.
Parameter Meaning
hItem Handle of the node that is to be highlighted.
When the mouse button is released, we need to check if the source node can be copied to the target
node. In the sample, we disable copying under the following three conditions: 1) The source node is the
same with the target node. 2) The target node is a descendent node of the source node. 3) The target node
does not have any child node. By setting these restrictions, a node can only be copied to become the child
of its parent node (direct or indirect).
We can use function CTreeCtrl::GetParentItem(…) to decide if one node is the descendent of
another node:
This function will return an HTREEITEM handle, which specifies the parent of node hItem. By
repeatedly calling this function we will finally get a NULL return value (This indicates that the root node
was encountered). Using this method, we can easily find out a list of all nodes that are parents of a specific
node.
121
Chapter 5. Common Controls
protected:
BOOL m_bIsDragging;
CImageList *m_pilDrag;
HTREEITEM m_hTreeDragSrc;
HTREEITEM m_hTreeDragTgt;
……
};
Here, Boolean type variable m_bIsDragging is used to indicate if the drag-n-drop activity is
undergoing. Pointer m_pilDrag will be used to store the dragging image. Variables m_hTreeDragSrc and
m_hTreeDragTgt are used to store the handles of source and target nodes respectively. We can use them to
implement copying right after the source node is dropped. Function MCTreeCtrl::IsDescendent(…) is
used to judge if one node is the descendent node of another, and MCTreeCtrl::CopyItemTo(…) will copy
one node (and all its descendent nodes) to another place.
Node Copy
When copying a node, we want to copy not only the node itself, but also all its descendent nodes. Since
we do not know how many descendents a node have beforehand, we need to call function
MCTreeCtrl::CopyItemTo(…) recursively until all the descendent nodes are copied. The following is the
implementation of this function:
tvInsertStruct.item.mask=
(
TVIF_CHILDREN |
TVIF_HANDLE |
TVIF_IMAGE |
TVIF_SELECTEDIMAGE |
TVIF_TEXT |
TVIF_STATE
);
tvInsertStruct.item.pszText=szBuf;
tvInsertStruct.item.cchTextMax=sizeof(szBuf);
tvInsertStruct.item.hItem=hTreeDragSrc;
GetItem(&tvInsertStruct.item);
tvInsertStruct.hParent=hTreeDragTgt;
tvInsertStruct.hInsertAfter=TVI_LAST;
hTreeItem=InsertItem(&tvInsertStruct);
if(tvInsertStruct.item.cChildren != 0)
{
hTreeDragSrc=GetChildItem(hTreeDragSrc);
while(TRUE)
{
if(hTreeDragSrc == NULL)break;
CopyItemTo(hTreeItem, hTreeDragSrc);
hTreeDragSrc=GetNextItem(hTreeDragSrc, TVGN_NEXT);
}
}
}
This function copies node hTreeDragSrc along with all its descendent nodes, and make them the child
nodes of hTreeDragTgt. First we call function CTreeCtrl::GetItem(…) to retrieve source node’s
information. We must pass a TV_ITEM type pointer to this function, and the corresponding object will be
filled with the information of the specified node. Here, we use member item of structure TV_INSERTSTRUCT
to receive a node’s information (Variable tvInsertStruct is declared by TV_INSERTSTRUCT, it will be used
to create new nodes). When calling this function, member mask of TV_ITEM structure specifies which
member should be filled with the node’s information. In our case, we want to know the handle of this node,
the associated images, the text of the label, the current state (expanded, highlighted, etc.), and if the node
has any child node. So we need to set the following bits of member mask: TVIF_CHILDREN, TVIF_HANDLE,
122
Chapter 5. Common Controls
TVIF_IMAGE, TVIF_SELECTEDIMAGE, TVIF_TEXT and TVIFF_STATE. Note we must provide our own buffer to
receive the label text. In the function, szBuf is declared as a char type array and its address is stored in
pszText member of TV_ITEM. Then we use tvInsertStruct to create a new node. Since we have already
stuffed item member with valid information, here we only need to assign the target handle (stored in
hTreeDragTgt) to hParent, and assign TVI_LAST to hInsertAfter. This will make the new node to
become the child of the target node, and be added to the end of all child nodes under the target node. Next
we check if this node has any child node. If so, we find out all the child nodes and call this function
recursively to copy all the child nodes. For this step, we use the newly created node as the target node, this
will ensure that the original tree structure will not change after copying.
In the final step, we call function CTreeCtrl::GetChileItem(…) to find out a node’s first child node,
then call function CTreeCtrl::GetNextitem(…) repeatedly to get the rest child nodes. The two functions
will return NULL if no child node is found.
TVN_BEGINDRAG
Now we need to implement TVN_BEGINDRAG message handler. First, we need to obtain the node that
was clicked by the mouse cursor. To obtain the current position of mouse cursor, we can call API function
::GetCursorPos(…). Since this position is measured in the screen coordinate system, we need to further
call function CWnd::ScreenToClient(…) to convert the coordinates to the coordinate system of the tree
control window. Then we can set variable m_bIsDragging to TRUE, and call function
CTreeCtrl::HitTest(…) to find out if the mouse cursor is over any node:
Next, we need to obtain dragging image list for this node. The dragging image list is created by calling
function CTreeCtrl::CreateDragImage(…). After this, the address of the image list object is stored in
variable m_pilDrag. If the image list is created successfully, we call several member functions of
CImageList to display the dragging image and enter dragging mode. If not, we should not start dragging,
and need to set the content of pResult to a non-zero value, this will stop dragging:
……
ASSERT(m_pilDrag == NULL);
m_pilDrag=CreateDragImage(m_hTreeDragSrc);
if(m_pilDrag != NULL)
{
m_pilDrag->DragShowNolock(TRUE);
m_pilDrag->SetDragCursorImage(0, CPoint(0, 0));
m_pilDrag->BeginDrag(0, CPoint(0,0));
m_pilDrag->DragMove(pt);
m_pilDrag->DragEnter(this, pt);
SetCapture();
*pResult=0;
}
else
{
m_bIsDragging=FALSE;
*pResult=1;
}
}
WM_MOUSEMOVE
Then we need to implement WM_MOUSEMOVE message handler. Whenever the mouse cursor moves to a
new place, we need to call function CImageList::DragMove(…) to move the dragging image so that the
123
Chapter 5. Common Controls
image will always follow the mouse’s movement. We need to check if the mouse hits a new node by
calling function CTreeCtrl::HitTest(…). If so, we must leave dragging mode by calling function
CImageList:: DragLeave(…), highlight the new node by calling function
CTreeCtrl::SelectDropTarget(…), and enter dragging mode again by calling function
CTreeCtrl::DragEnter(…). The reason for doing this is that when dragging is undergoing, the tree control
window is locked and no update could be implemented successfully. The following is the implementation
of this message handler:
if(m_bIsDragging)
{
ASSERT(m_pilDrag != NULL);
m_pilDrag->DragMove(point);
if((hitem=HitTest(point, &flags)) != NULL)
{
m_pilDrag->DragLeave(this);
SelectDropTarget(hitem);
m_hTreeDragTgt=hitem;
m_pilDrag->DragEnter(this, point);
}
}
CTreeCtrl::OnMouseMove(nFlags, point);
}
WM_LBUTTONUP
Finally we need to implement WM_LBUTTONUP message handler. In this handler, we must first leave
dragging mode and end dragging. This can be implemented by calling functions CImageList::
DragLeave(...) and CImageList::EndDrag() respectively. Then, we need to delete dragging image list
object:
The following code fragment shows how to judge if the source node can be copied to become the child
of the target node:
……
if
(
m_hTreeDragSrc != m_hTreeDragTgt &&
GetChildItem(m_hTreeDragTgt) != NULL &&
!IsDescendent(m_hTreeDragTgt, m_hTreeDragSrc)
)
……
If the source and target are the same node, or target node does not have any child node, or source node
is the parent node (including indirect parent) of the target node, the copy should not be implemented.
Otherwise, we need to call function MCTreeCtrl::CopyItem(…) to implement node copy:
……
{
CopyItemTo(m_hTreeDragTgt, m_hTreeDragSrc);
//DeleteItem(m_hTreeDragTgt);
}
else ::MessageBeep(0);
::ReleaseCapture();
124
Chapter 5. Common Controls
m_bIsDragging=FALSE;
SelectDropTarget(NULL);
}
CTreeCtrl::OnLButtonUp(nFlags, point);
}
If we want the node to be moved instead of being copied, we can delete the source node after copying
it. The source node and all its child nodes will be deleted by calling function CTreeCtrl::DeleteItem(…).
Functions CWnd::SetCapture() and ::ReleaseCapture() are also called in MCTreeCtrl::
OnBegindrag(…) and MCTreeCtrl::OnLButtonUp(…) respectively to set and release the window capture. By
doing this, we can still trap mouse messages even if it moves outside the client window when dragging is
undergoing.
That’s all we need to do for implementing drag-n-drop copying. By compiling and executing the
sample application at this point, we will be able to copy nodes through mouse dragging. With minor
modifications to the above message handlers, we can easily implement both node copy and move as
follows: when CTRL key is held down, the node can be copied through drag-n-drop, when there is no key
held down, node will be moved.
Here pImageList is a pointer to the image list, and nImageList specifies the type of image list: it could
be LVSIL_NORMAL or LVSIL_SMALL, representing which style the image list will be used for.
After the image list is set, we need to add columns for the list control (Figure 5-12). This can be
implemented by calling function CListCtrl::InsertColumn(…), which has the following format:
The function has two parameters. The first one indicates which column is to be added (0 based index),
and the second one is a pointer to LV_COLUMN type object:
125
Chapter 5. Common Controls
int cchTextMax;
int iSubItem;
} LV_COLUMN;
Here, member mask indicates which of the other members contain valid values, this is the same with
structure LV_ITEM. Member fmt indicates the text alignment for the column, it can be LVCFMT_LEFT,
LVCFMT_RIGHT, or LVCFMT_CENTER. Member cx indicates the width of the column, and iSubItem indicates
its index. Member pszText is a pointer to the text string that will be displayed for each column. Finally,
cchTextMax specifies the size of buffer pointed by pszText.
Columns
After columns are created, we need to add list items. For each list item, we need to insert a sub-item in
each column. For example, if there are three columns and 4 list items, we need to add totally 12 sub-items.
To add a sub-item, we need to stuff an LV_ITEM type object then call function CListCtrl::
InsertItem(…), which has the following format:
The usage of this structure is similar to that of structure TV_ITEM. For each item, we need to use this
structure to add every sub-item for it. Usually only the sub-items contained in the first column will have an
associated image (when being displayed in report style), so we need to set image for each item only once.
Member iItem and iSubItem specify item index and column index respectively.
Sample
Sample 5.15\CCtl demonstrates how to use list control. It is a dialog-based application generated by
Application Wizard. In this sample, a four-item list is implemented, which can be displayed in one of the
four styles. When it is displayed in report style, the control has four columns. The first column lists four
shapes: square, rectangle, circle, triangle. The second column lists the formula for calculating their
perimeter, and the third column lists the formula for calculating their area.
126
Chapter 5. Common Controls
BOOL CCCtlDlg::OnInitDialog()
{
CCCtlApp *ptrApp;
LV_COLUMN lvCol;
LV_ITEM lvItem;
int nItem;
CRect rect;
CImageList *pilBig;
CImageList *pilSmall;
……
ptrApp=(CCCtlApp *)AfxGetApp();
pilBig=new CImageList();
pilSmall=new CImageList();
pilBig->Add(ptrApp->LoadIcon(IDI_ICON_SQUARE));
pilBig->Add(ptrApp->LoadIcon(IDI_ICON_RECTANGLE));
pilBig->Add(ptrApp->LoadIcon(IDI_ICON_CIRCLE);
pilBig->Add(ptrApp->LoadIcon(IDI_ICON_TRIANGLE));
pilSmall->Add(ptrApp->LoadIcon(IDI_ICON_SQUARE));
pilSmall->Add(ptrApp->LoadIcon(IDI_ICON_RECTANGLE));
pilSmall->Add(ptrApp->LoadIcon(IDI_ICON_CIRCLE));
pilSmall->Add(ptrApp->LoadIcon(IDI_ICON_TRIANGLE));
m_listCtrl.SetImageList(pilBig, LVSIL_NORMAL);
m_listCtrl.SetImageList(pilSmall, LVSIL_SMALL);
……
We could use the same icon to create both 32×32 and 16×16 image lists. When creating the 16×16
image list, the images will be automatically scaled to the size specified by the image list. Since we allocate
memory for creating image list in dialog’s initialization stage, we need to release it when the dialog box is
being destroyed. For this purpose, a WM_DESTROY message handler is added through using Class Wizard,
within which the image lists are deleted as follows:
void CCCtlDlg::OnDestroy()
{
CImageList *pilCtrl;
pilCtrl=m_listCtrl.GetImageList(LVSIL_NORMAL);
pilCtrl->DeleteImageList();
delete pilCtrl;
pilCtrl=m_listCtrl.GetImageList(LVSIL_SMALL);
pilCtrl->DeleteImageList();
delete pilCtrl;
m_listCtrl.DeleteAllItems();
CDialog::OnDestroy();
}
If we release the memory used by image lists this way, we must set “Share image list” style for the list
control. This allows image list to be shared among different controls. If we do not set this style, the image
list will be destroyed automatically when the list control is destroyed. In this case, we don’t have to release
the memory by ourselves. To set this style, we need to invoke “List control properties” property sheet, go to
“More styles” page, and check “Share image list” check box (Figure 5-13).
127
Chapter 5. Common Controls
Creating Columns
First we need to create three columns, whose titles are “Shape”, “Perimeter”, and “Area” respectively.
The following portion of function CCCtlDlg::OnInitDialog() creates each column:
……
GetClientRect(rect);
lvCol.pszText="Perimeter";
lvCol.iSubItem=1;
lvCol.cx=rect.Width()/3;
m_listCtrl.InsertColumn(1, &lvCol);
lvCol.pszText="Area";
lvCol.iSubItem=2;
lvCol.cx=rect.Width()/3;
m_listCtrl.InsertColumn(2, &lvCol);
……
The client window’s dimension is retrieved by calling function CWnd::GetClientRect(…) and then
stored in variable rect. The horizontal size of each column is set to 1/3 of the width of the client window.
Creating Sub-items
Since there are totally three columns, for each item, we need to create three sub-items. The following is
the portion of function CCCtlDlg::OnInitDialog() that demonstrates creating one sub-item:
……
lvItem.mask=LVIF_TEXT | LVIF_IMAGE;
lvItem.iItem=0;
lvItem.iSubItem=0;
lvItem.pszText="Square";
lvItem.iImage=0;
nItem=m_listCtrl.InsertItem(&lvItem);
lvItem.mask=LVIF_TEXT;
lvItem.iItem=nItem;
lvItem.pszText="4*a";
lvItem.iSubItem=1;
m_listCtrl.SetItem(&lvItem);
lvItem.pszText="a^2";
lvItem.iSubItem=2;
m_listCtrl.SetItem(&lvItem);
……
Function CListCtrl::InsertItem(…) is called to add a list item and set its first sub-item. The rest sub-
items should be set by calling function CListCtrl::SetItem(…). For these sub-items, we don’t need to set
image again, so LVIF_IMAGE flag is not applied when function CListCtrl::SetItem(…) is called.
128
Chapter 5. Common Controls
In the sample, four radio buttons are added to the dialog template for selecting different styles. Their
IDs are IDC_RADIO_ICON, IDC_RADIO_SMALLICON, IDC_RADIO_LIST and IDC_RADIO_REPORT respectively.
We need to handle BN_CLICKED message for the four radio buttons in order to respond to mouse events.
These message handlers are added through using Class Wizard. Within the member functions, the style of
the list control is changed according to which radio button is being clicked. The following is one of the
message handlers that sets the style of the list control to “Normal Icon”:
void CCCtlDlg::OnRadioIcon()
{
LONG lStyle;
lStyle=GetWindowLong(m_listCtrl.GetSafeHwnd(), GWL_STYLE);
lStyle&=~(LVS_TYPEMASK);
lStyle|=LVS_ICON;
SetWindowLong(m_listCtrl.GetSafeHwnd(), GWL_STYLE, lStyle);
}
First, the list control’s old style is retrieved by calling function ::GetWindowLong(…), and is bit-wisely
ANDed with LVS_TYPEMASK, which will turn off all the style bits. Then style LVS_ICON is added to the
window style (through bit-wise ORing), and function ::SetWindowLong(…) is called to update the new
style. Both function ::GetWindowLong(…) and ::SetWindowLong(…) require a window handle, it could be
obtained by calling function CWnd:: GetSafeHwnd().
The list control and tree control can also be implemented in SDI and MDI applications. In this case, we
need to use classes derived from CListView or CTreeView. Although the creating procedure is a little
different from that of a dialog box, the properties of the controls are exactly the same for two different
types of applications. We will further explore list control and tree control in chapter 15.
129
Chapter 5. Common Controls
template; then in the dialog’s initialization stage, we need to create the image list, select it into the tab
control, and initialize the tab control. The function that can be used to assign image list to a tab control is
CTabCtrl::SetImageList(…), which has the following format:
The function that can be used to add an item to the tab control is CTabCtrl::InsertItem(…):
The first parameter of this function is the index of the tab (zero based), and the second parameter is a
pointer to TC_ITEM type object. Before calling this function, we need to stuff structure TC_ITEM with tab’s
information:
We need to use three members of this structure in order to create a tab with both text and image: mask,
pszText and iImage. Member mask indicates which of the other members of this structure contains valid
data; member pszText is a pointer to string text; and iImage is an image index to the associated image list.
We see that using this structure is very similar to that of TV_ITEM and LV_ITEM.
To respond to tab control’s activities, we need to add message handlers for it. The most commonly
used messages of tab control are TCN_SELCHANGE and TCN_SELCHANGING, which indicate that the current
selection has changed or the current selection is about to change respectively.
Sample 5.16\CCtl demonstrates how to use tab control, it is based on sample 5.15\CCtl. In this sample,
four radio buttons are replaced by a tab control IDC_TAB (see Figure 5-14). Also, message handlers of radio
buttons are removed. In order to access the tab control, a CTabCtrl type control variable m_tabCtrl is
added to class CCCtlDlg through using Class Wizard. Beside this, four bitmap resources IDB_BITMAP_ICON,
IDB_BITMAP_SMALLICON, IDB_BITMAP_LIST and IDB_BITMAP_REPORT are added to the application to create
image list for tab control.
In function CCCtlDlg::OnInitDialog(), first the image list is created and assigned to the tab control.
Next, four items are added to the tab control:
BOOL CCCtlDlg::OnInitDialog()
{
……
TC_ITEM tc_item;
CImageList *pilTab;
……
CBitmap bmp;
……
pilTab=new CImageList();
VERIFY(pilTab->Create(TAB_BMP_WIDTH, TAB_BMP_HEIGHT, ILC_COLOR, 4, 0));
bmp.LoadBitmap(IDB_BITMAP_ICON);
pilTab->Add(&bmp, RGB(127, 127, 127));
bmp.DeleteObject();
bmp.LoadBitmap(IDB_BITMAP_SMALLICON);
pilTab->Add(&bmp, RGB(127, 127, 127));
bmp.DeleteObject();
bmp.LoadBitmap(IDB_BITMAP_LIST);
pilTab->Add(&bmp, RGB(127, 127, 127));
bmp.DeleteObject();
bmp.LoadBitmap(IDB_BITMAP_REPORT);
pilTab->Add(&bmp, RGB(127, 127, 127));
bmp.DeleteObject();
m_tabCtrl.SetImageList(pilTab);
tc_item.mask=TCIF_TEXT | TCIF_IMAGE;
tc_item.pszText="Icon";
tc_item.iImage=0;
m_tabCtrl.InsertItem(0, &tc_item);
130
Chapter 5. Common Controls
tc_item.pszText="Small Icon";
tc_item.iImage=1;
m_tabCtrl.InsertItem(1, &tc_item);
tc_item.pszText="List";
tc_item.iImage=2;
m_tabCtrl.InsertItem(2, &tc_item);
tc_item.pszText="Report";
tc_item.iImage=3;
m_tabCtrl.InsertItem(3, &tc_item);
return TRUE;
}
Macro TAB_BMP_WIDTH and TAB_BMP_HEIGHT are defined as the width and height of the bitmaps. In
function CCCtlDlg::Destroy(), the following statements are added to delete the tab items and the image
list used by the tab control:
void CCCtlDlg::OnDestroy()
{
CImageList *pilCtrl;
pilCtrl=m_tabCtrl.GetImageList();
pilCtrl->DeleteImageList();
m_tabCtrl.DeleteAllItems();
……
}
lStyle=GetWindowLong(m_listCtrl.GetSafeHwnd(), GWL_STYLE);
lStyle&=~(LVS_TYPEMASK);
switch(m_tabCtrl.GetCurSel())
{
case 0:
{
lStyle|=LVS_ICON;
break;
}
case 1:
{
lStyle|=LVS_SMALLICON;
break;
}
case 2:
{
lStyle|=LVS_LIST;
break;
}
case 3:
{
lStyle|=LVS_REPORT;
break;
}
}
SetWindowLong(m_listCtrl.GetSafeHwnd(), GWL_STYLE, lStyle);
*pResult=0;
}
With the above implementation, we can change the list control’s style dynamically through using the
tab control.
131
Chapter 5. Common Controls
Timer
Timer is a very useful resource in Windows operating system. Once we set the timer and specify the
time out period, it will start to count down and send us a WM_TIMER message when time out happens. The
timer can be set within any CWnd derived class by calling function CWnd::SetTimer(…). Timers with
different IDs are independent upon one another, so we can set more than one timer to handle complex
situation.
The following is the prototype of function CWnd::SetTimer(…):
UINT CWnd::SetTimer
(
UINT nIDEvent, UINT nElapse,
void(CALLBACK EXPORT *lpfnTimer)(HWND, UINT, UINT, DWORD)
);
The function has three parameters. Parameter nIDEvent is an event ID. This ID can be any integer, and
we need to use different ID for different event in order to distinguish between them. Parameter nElapse
specifies time out period, whose unit is millisecond. Parameter lpfnTimer is a pointer to a callback
function that will be used to handle time out message. We can also pass NULL to this parameter and add
WM_TIMER message handler to receive this message.
In the sample, the IDs of the animate control and progress control are IDC_ANIMATE and IDC_PROGRESS.
Also, the variables used to access them are m_animateCtrl and m_progressCtrl respectively.
Custom Resource
The AVI data can be included in the application as a resource. However, Developer Studio does not
support this kind of resource directly. So we have to treat it as a custom resource. We can create AVI
resource from a “*.avi” file through following steps: 1) Execute Insert | Resource command, then click
“Import” button from the “Insert resource” dialog box. 2) From the popped up “File open” dialog box,
browse and select a “*.avi” file and open it (we can use “5.17\CCtl\search.avi” or any other “*.avi” file for
this purpose). 3) When we are asked to provide the resource type, input “AVI”. 4) Name the resource ID as
IDR_AVI.
132
Chapter 5. Common Controls
Sample Implementation
In the dialog box’s initialization stage, we need to initialize the animate control, progress control and
set timer as follows:
BOOL CCCtlDlg::OnInitDialog()
{
……
ASSERT(m_animateCtrl.Open(IDR_AVI));
ASSERT(m_animateCtrl.Play(0, -1, -1));
m_progressCtrl.SetRange(0, 50);
m_progressCtrl.SetStep(2);
SetTimer(PROGRESS_CTRL, 500, NULL);
return TRUE;
}
First, function CAnimateCtrl::Open(…) is called to open the animation resource, then function
CAnimateCtrl::Play(…) is called to play the AVI data. When doing this, we pass 0 to its first parameter
and -1 to the second parameter, this will let the animation be played from the first frame to the last frame.
The third parameter is also -1, this means the animation will be played again and again without being
stopped.
Then we initialize the range of progress control from 0 to 50, incremental step 2, and a timer with time
out period of 500 milliseconds is set.
Message WM_TIMER can be handled by adding message handlers, this can be easily implemented
through using Class Wizard. In the sample, this member function is implemented as follows:
The only parameter of this function is nIDEvent, which indicates the ID of the timer that has just timed
out. If we have two or more timers set within the same window, by examining this ID we know which
timer has timed out. In the sample, when timer times out, we simply call function
CProgressCtrl::StepIt() to advance the progress bar one step forward.
Summary:
1) A spin control must work together with another control, which is called the “Buddy” of the spin
control. Usually the “Buddy” is an edit box, but it could be any other types of controls such as button
or static control.
2) To set buddy automatically, we must make sure that the buddy window is the previous window of the
spin control in Z-order.
1) The buddy can also be set by calling function CSpinButtonCtrl::SetBuddy(…).
1) If we set “Set buddy integer” style, the spin control will notify the buddy control to update its contents
whenever the position of the spin control changes. If we set this style, the buddy edit box can display
only integers.
1) If we want to customize the behavior of buddy control, we need to handle message UDN_DELTAPOS.
This message will be sent when the position of the spin control changes. By doing this, we can let the
buddy control display text strings or bitmap images.
1) Slider control shares the same message with scroll bars. By handling message WM_HSCROLL (for
horizontally orientated sliders) and WM_VSCROLL (for vertically orientated sliders), we can trap the
mouse activities on the slider.
133
Chapter 5. Common Controls
1) List box can be implemented in different styles: single selection, multiple selection, extended selection.
By default, the items in the list box will contain only characters, and they will be alphabetically sorted.
These styles can be changed by calling member functions of CListCtrl.
1) A list box can be used to display directories and files contained in a specific directory by calling
function CListCtrl::Dir(…).
1) We can handle LBN_… type messages to customize the default behavior of a list control.
1) A combo box is composed of an edit box and a list box. Because they are not created by MFC code,
we can not access them through the normal method.
1) To trap RETURN, ESC keys for combo box, we need to override function CWnd::
PreTranslateMessage(…).
1) To implement subclass for edit box contained in a combo box, we need to call function CWnd::
SubclassWindow(…) instead of CWnd::SubclassDlgItem(…).
1) To create owner-draw list box or combo box, first we need to set “Owner draw” style, then override
WM_MEASUREITEM and WM_DRAWITEM message handlers.
1) Image list is used by tree control, list control and tab control. Once an image list is assigned to a
control, the images contained in the list can be accessed through their zero-based indices.
1) To use a tree control, we can create its resource in the dialog template. Then in the dialog’s
initialization stage, we can create the tree structure. A tree item can be added to the control by stuffing
a TV_ITEM type object, then calling function CTreeCtrl::InsertItem(…).
1) We need to handle message TVN_ITEMEXPANDING or TVN_ITEMEXPANDED to customize a tree control’s
expanding and collapsing behaviors.
1) We need to set “Edit labels” style and handle TVN_ENDLABELEDIT message to enable label editing for
tree control.
1) We need to handle messages TVN_BEGINDRAG, WM_MOSUEMOVE, and WM_LBUTTONUP to enable drag-n-drop
for tree control.
1) The list box can be displayed in four different styles: Normal (big) icon style, small icon style, list
style, and report style. We can select one style when the list box resource is being created. If we want
to change the style dynamically, we need to call function ::SetWindowLong(…).
1) Because list control can be used to represent items in different styles, usually we need to prepare two
image lists (big icon and small icon) for a list control.
1) To create a list control, we need to create columns first. For each column, we need to create sub-items
for all the items contained in the list.
1) To create a column for a list control, we need to stuff an LV_COLUMN type object and call function
CListCtrl::InsertColumn(…). To create an item, we need to stuff an LV_ITEM type object and call
function CListCtrl::InsertItem(…). To set the rest sub-items, we need to stuff LV_ITEM type objects
and call function CListCtrl::SetItem(…).
1) To use tab control, first we need to create tab control resource in the dialog template. Then in the
dialog’s initialization stage, we need to create and select the image list, stuff TV_ITEM type objects and
call function CTabCtrl::InsertItem(…) to add items.
1) The animate control can be used to play AVI data. Because this is not a standard resource supported in
Developer Studio, we need to create custom resource if we want to include AVI data in an application
as a resource.
1) The progress control is used to indicate the progress of events. In order to synchronize the progress bar
with the events, we need to advance the progress bar within the event’s message handler.
134
Chapter 6. Dialog Box
Chapter 6
Dialog Box
D
ialog box is very common for all types of applications, it is used in almost every program. Usually
a dialog box is built from resources: we design everything in the dialog template, then use a
CDialog derived class to implement it. We can call function CDialog::DoModal() to invoke the
dialog box, use member variables to access the common controls, and add message handlers to
process mouse or keyboard related events.
In this chapter we will discuss some topics on customizing normal dialog boxes. By using the methods
introduced in this chapter, we are able to make our dialog boxes more user friendly.
……
//{{AFX_DATA(CMLDialog)
enum { IDD = IDD_DIALOG };
//}}AFX_DATA
……
If no ID is assigned to this member, the dialog box will not be created correctly.
A modeless dialog box allows the user to switch to other windows without dismissing it first. Because
of this, the variable used to implement the modeless dialog box should not go out of scope in the dialog
box’s lifetime. Usually we need to use member variable declared in the class to create modeless dialog box
rather than using a local variable.
We can not call function CDialog::DoModal() to implement modeless dialog box, because this
function is designed solely for modal dialog box. The correct functions that should be used for modeless
dialog box are CDialog::Create(…) and CWnd::ShowWindow(…).
The following shows the prototypes of function CDialog::Create(…):
135
Chapter 6. Dialog Box
Both versions of this function have two parameters, the first of which is the template ID (either an
integer ID or a string ID), the second is a CWnd type pointer which specifies the parent window of the dialog
box.
Function CWnd::ShowWindow(…) has the following format:
It has only one parameter, which can be set to SW_HIDE, SW_MINIMIZE, SW_SHOW... and so on to display
the window in different styles.
For example, if class CMyDialog is derived from CDialog, and the dialog template ID is IDD_DIALOG,
we can declare a variable m_dlg in any class (for example, CDocument) then do the following in a member
function to implement a modeless dialog box:
m_dlg.Create(IDD_DIALOG);
m_dlg.ShowWindow(SW_SHOW);
Sample
Sample 6.1\DB demonstrates how to implement modeless dialog box. It is a standard SDI application
created by Application Wizard. To make the modeless creation procedure simpler, a member function
DoModeless() is implemented in the derived class so that it can be used just like function CDialog::
DoModal().
Please note that when the user clicks “OK” or “Cancel” button to dismiss the dialog box, the window
will become hidden rather than being destroyed. The window will be destroyed only when the variable goes
out of scope (e.g. when we use delete keyword to release the buffers if they are allocated by new key word,
or after function returns if the variable is declared locally). So even after the dialog box is closed by
clicking “OK” or “Cancel” button, it still can be restored by calling function CWind::ShowWindow(…) using
SW_SHOW parameter.
In the sample application, a dialog template IDD_DIALOG_MODELESS is prepared for modeless dialog box
implementation. A new class CMLDialog is derived from CDialog, and a CWnd type pointer m_pParent along
with a member function DoModeless() are declared in the class:
When we implement a modal dialog box using class CDialog, the parent window needs to be specified
only in the constructor. When calling CDialog::Create(…) to implement a modeless dialog box, we need
to specify the parent window again even if it has been passed to the constructor. To let function
DoModeless() has the same format with function CDialog::DoModal(), we store the pointer to the parent
window in variable CMLDialog::m_pParent so that it can be used later in function DoModeless(). In the
sample, function CMLDialog::DoModeless() is implemented as follows:
136
Chapter 6. Dialog Box
void CMLDialog::DoModeless()
{
if(GetSafeHwnd() == NULL)
{
Create(IDD, m_pParent);
ShowWindow(SW_SHOW);
CenterWindow();
}
else
{
if(IsWindowVisible() == FALSE)
{
ShowWindow(SW_SHOW);
}
}
}
Since this function could be called after the dialog box has been invoked, first we need to check if a
valid window has been created by calling function CWnd::GetSafeHwnd(). If the returned value is NULL,
the window has not been created yet. In this case, we should call function CDialog::Create(…) to create
the window. If the returned value is not NULL, there are two possibilities: the window may be currently
active or hidden. We can call function CWnd::IsWindowVisible() to check the dialog box’s visual state. If
the dialog box is hidden, we should call CWnd::ShowWindow(…) to activate it.
In the sample, a command Dialog Box | Modeless is added to the mainframe menu IDR_MAINFRAME,
whose ID is ID_DIALOGBOX_MODELESS. Also, a WM_COMMAND message handler is added for this command
through using Class Wizard, and the corresponding member function is CDBDoc::OnDialogboxModeless().
The following is its implementation:
void CDBDoc::OnDialogboxModeless()
{
m_dlgModeless.DoModeless();
}
With the new class, it is equally easy to implement a modeless or modal dialog box. The only
difference between creating two type of dialog boxes is that for modeless dialog box, the variable can not
be declared locally.
137
Chapter 6. Dialog Box
The default dialog box template IDD_DIALOG_DB will not be used, so it is also deleted from the
application resources. The following is the modified class:
//{{AFX_DATA(CDBDlg)
//}}AFX_DATA
//{{AFX_VIRTUAL(CDBDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX);
//}}AFX_VIRTUAL
protected:
HICON m_hIcon;
//{{AFX_MSG(CDBDlg)
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
We also need to find all the keyword CDialog in the implementation file of CDBDlg and change them to
CPropertySheet. The changes should happen in the following functions: the constructor of CDBDlg,
function DoDataExchange(…), OnInitDialog(), OnSysCommand(…), OnPaint(…), and message mapping
macros.
Next we need to create each single page. The procedure of creating a property page is the same with
creating a dialog box, except that when adding new class for a dialog box template, we must derive it from
class CPropertyPage. In the sample, three dialog templates are added to the application, their IDs are
ID_DIALOG_PAGE1, ID_DIALOG_PAGE2 and ID_DIALOG_PAGE3 respectively. Three classes CPage1, CPage2 and
CPage3 are also added through using Class Wizard, which are all derived from CPropertyPage. When
doing this, we need to provide the ID of the corresponding dialog template.
In class CDBDlg, a new member variable is declared for each page:
#include "Page.h"
……
class CDBDlg : public CPropertySheet
{
……
protected:
……
CPage1 m_page1;
CPage2 m_page2;
CPage3 m_page3;
……
};
138
Chapter 6. Dialog Box
These are the necessary steps for implementing property sheet. For each property page, we can also
add message handlers for the controls, the procedure of which is the same with that of a standalone dialog
box.
By default, the property sheet will be implemented in “tab” mode: there will be a tab control in the
property sheet, which can be used to select property pages. The property sheet can also be implemented in
“wizard” mode, in which case tab control will be replaced by two buttons (labeled with “Previous” and
“Next”). In this mode, the pages can only be selected sequentially through button clickings.
To enable wizard mode, all we need to do is calling function CPropertySheet::
SetWizardMode()after all the pages have been added. For example, if we want to enable wizard mode in
the sample, we should implement the constructor of CDBDlg as follows:
Sample 6.2-2\DB is the same with sample 6.2-1\DB, except that the property sheet is implemented in
wizard mode.
If we need to implement a property sheet dialog box in an SDI or MDI application, most of the steps
are still the same. We can start by creating a new CPropertySheet based class, then adding dialog
templates and CPropertyPage based classes, using them to declare new variables in CPropertySheet
derived class, calling function CPropertySheet::AddPage(…) in its constructor. We can call function
CPropertySheet::DoModal() at anytime to invoke the property sheet.
public:
virtual ~CMLPropertySheet();
139
Chapter 6. Dialog Box
void DoModeless();
protected:
CWnd *m_pParentWnd;
CPage1 m_page1;
CPage2 m_page2;
CPage3 m_page3;
//{{AFX_MSG(CMLPropertySheet)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
CMLPropertySheet::CMLPropertySheet
(
UINT nIDCaption, CWnd* pParentWnd, UINT iSelectPage
)
:CPropertySheet(nIDCaption, pParentWnd, iSelectPage)
{
m_pParentWnd=pParentWnd;
AddPage(&m_page1);
AddPage(&m_page2);
AddPage(&m_page3);
}
CMLPropertySheet::CMLPropertySheet
(
LPCTSTR pszCaption, CWnd* pParentWnd, UINT iSelectPage
)
:CPropertySheet(pszCaption, pParentWnd, iSelectPage)
{
m_pParentWnd=pParentWnd;
AddPage(&m_page1);
AddPage(&m_page2);
AddPage(&m_page3);
}
void CMLPropertySheet::DoModeless()
{
if(GetSafeHwnd() == NULL)
{
Create(m_pParentWnd);
ShowWindow(SW_SHOW);
CenterWindow();
}
else
{
if(IsWindowVisible() == FALSE)
{
ShowWindow(SW_SHOW);
}
}
}
Everything is the same with that of sample 6.1\DB, except that here we need to call function
CPropertySheet::Create(…) instead of CDialog::Create(…).
By now class CMLPropertySheet is ready for use. We can declare a CMLPropertySheet type pointer in
class CDBDoc:
The constructor and destructor of class CDBDoc are modified to initialize and release the buffers if
necessary:
140
Chapter 6. Dialog Box
CDBDoc::CDBDoc()
{
m_ptrDlg=NULL;
}
CDBDoc::~CDBDoc()
{
if(m_ptrDlg != NULL)delete m_ptrDlg;
}
It is possible that the variable is not initialized when the application is closed, so we need to check if
m_ptrDlg is NULL before deleting it.
Finally, function CDBDoc::OnPropertysheetModeless() is implemented as follows:
void CDBDoc::OnPropertysheetModeless()
{
if(m_ptrDlg == NULL)
{
m_ptrDlg=new CMLPropertySheet("Modeless Property Sheet", AfxGetMainWnd());
}
m_ptrDlg->DoModeless();
}
6.4 Sizes
In this section we are going to discuss some window sizes that is important for dialog boxes.
Initial Size
The initial size is the dimension of a dialog box when it first pops up. By default, a dialog box’s initial
size is determined from the font used by the dialog box and the size of its dialog template. If we want to
make change to its initial size, we can call either CWnd::SetWindowPos(…) or CWnd::MoveWindow(…) within
function CDialog::OnInitDialog(). The difference between above two functions is that CWnd::
SetWindowPos(…) allows us to change a window’s X-Y position and Z-order, while CWnd::MoveWindow(…)
allows us to move the window only in the X-Y plane.
141
Chapter 6. Dialog Box
The size of
dialog template
is shown on the
status bar
These sizes can all be customized. To provide user defined sizes, we can override function CWnd::
OnGetMinMaxInfo(…), which will be called when any of the above sizes is needed by the system. We can
provide our own sizes within the overridden function.
Function CWnd::OnGetMinMaxInfo(…) has the following format:
Here members ptMinTrackSize and ptMaxTrackSize specify the minimum and maximum tracking
sizes, ptMaxSize specifies maximized size, and ptMaxPosition specifies the upper-left corner position of a
window when it is first maximized.
Sample
Sample 6.4\DB demonstrates how to customize these sizes. It is a dialog based application created
from Application Wizard. In the sample, the dialog’s minimum tracking size is set to its dialog template
size. Also, the maximum tracking size and the maximized size are customized.
To let the dialog box be able to maximize and minimize, we must set the two styles: “Minimize Box”,
“Maximize Box”. To let it be able to resize, we must also set “Resizing” style (Figure 6-2).
In the sample, message handler of WM_GETMINMAXINFO is added through using Class Wizard. The
function is implemented as follows:
142
Chapter 6. Dialog Box
rect.left=0;
rect.top=0;
rect.right=MIN_X_SIZE;
rect.bottom=MIN_Y_SIZE;
MapDialogRect(&rect);
lpMMI->ptMinTrackSize.x=
(
rect.right+::GetSystemMetrics(SM_CXSIZEFRAME)*2
);
lpMMI->ptMinTrackSize.y=
(
rect.bottom+::GetSystemMetrics(SM_CYCAPTION)+
::GetSystemMetrics(SM_CXSIZEFRAME)*2
);
lpMMI->ptMaxTrackSize.x=lpMMI->ptMinTrackSize.x+100;
lpMMI->ptMaxTrackSize.y=lpMMI->ptMinTrackSize.y+100;
sizeScreen.cx=::GetSystemMetrics(SM_CXFULLSCREEN);
sizeScreen.cy=::GetSystemMetrics(SM_CYFULLSCREEN);
lpMMI->ptMaxPosition.x=0;
lpMMI->ptMaxPosition.y=0;
lpMMI->ptMaxSize.x=sizeScreen.cx/2;
lpMMI->ptMaxSize.y=sizeScreen.cy/2;
}
Here MIN_X_SIZE and MIN_Y_SIZE are defined as the dialog template size that is read from Developer
Studio when the dialog resource is being edited. Because this size is the client area size of the dialog box
(when a dialog box is created, caption bar, borders will be added), we need to add the dimensions of
caption bar and border in order to make the dialog size exactly the same with its template size. The
dimensions of caption bar and border can be retrieved by calling API function ::GetSystemMetrics(…)
with appropriate parameters passed to it. This function allows us to retrieve many system configuration
settings. The following is the function prototype and a list of commonly used parameters:
In the sample, the maximized size of the dialog is set to 1/4 of the desk top screen size. When the
application is first maximized, it will be positioned at top-left corner (0, 0).
The dialog box’s initial size is set in function CDialog::OnInitDialog():
BOOL CDBDlg::OnInitDialog()
143
Chapter 6. Dialog Box
{
……
RECT rect;
rect.left=0;
rect.top=0;
rect.right=MIN_X_SIZE;
rect.bottom=MIN_Y_SIZE;
MapDialogRect(&rect);
rect.right+=50;
rect.bottom+=50;
MoveWindow(&rect);
CenterWindow();
return TRUE;
}
The dialog box’s initial size is a little bigger than its minimum tracking size.
The above sizes are not unique to dialog boxes. In fact, any window has the above sizes, and can be
customized with the same method.
Background Drawing
Generally all dialog boxes have a gray background. Sometimes it is more desirable to change dialog
box’s background to a custom color, or, we may want to paint the background using a repeated pattern. To
customize a dialog box’s background, we need to handle message WM_ERASEBKGND, and draw the custom
background after receiving this message. All classes that are derived from CWnd will inherit function
CWnd::OnEraseBkgnd(…), which has the following format:
Here, pointer pDC can be used to draw anything on the target window. For example, we can create
solid brush and paint the background with a custom color, or we can create pattern brush, and paint the
background with certain pattern. Of course, bitmap can also be used here: we can draw our own bitmap
repeatedly until all of the dialog box area is covered by the bitmap patterns.
Sample
Sampel 6.5\DB demonstrates background customization. It is a standard dialog-based application
generated from Application Wizard. In the sample, instead of using a uniform color, the dialog box paints
its background with a bitmap image (Figure 6-3).
144
Chapter 6. Dialog Box
Because WM_ERASEBKGND is not listed as a dialog box message, first we need to customize the filter
settings for this application. We can do this by invoking Class Wizard, clicking “Class Info” tab then
changing the default setting in combo box “Message Filter” from “Dialog” to “Window”. By going back to
“Message maps” page now, we can find WM_ERASEBKGND in “Message” window, and add a message handler
for it. The function name should be CDBDlg::OnEraseBkgnd(…).
In the sample, a bitmap resource IDB_BITMAP_DLGBGD is added to the application, which will be used to
draw the background of the dialog box. In function CDBDlg::OnEraseBkgnd(…), this bitmap is painted
repeatedly until all dialog box area is covered by it:
bmp.LoadBitmap(IDB_BITMAP_DLGBGD);
bmp.GetBitmap(&bm);
GetClientRect(rect);
nHor=rect.Width()/bm.bmWidth+1;
nVer=rect.Height()/bm.bmHeight+1;
dcMemory.CreateCompatibleDC(pDC);
ptrBmpOld=dcMemory.SelectObject(&bmp);
for(i=0; i<nHor; i++)
{
for(j=0; j<nVer; j++)
{
pDC->BitBlt
(
i*bm.bmWidth,
j*bm.bmHeight,
bm.bmWidth,
bm.bmHeight,
&dcMemory,
0,
0,
SRCCOPY
);
}
}
dcMemory.SelectObject(ptrBmpOld);
return TRUE;
}
First function CBitmap::LoadBitmap(…) is called to load the bitmap resource, then its dimension is
retrieved by calling function CBitmap::GetBitmap(…). Next, function CWnd::GetClientRect(…) is called
to obtain the size of the client area of the dialog box. Then we calculate the number of loops required to
repeat drawing in both horizontal and vertical directions in order to cover all the client area. The results are
stored in two local variables nHor and nVer. Then, a memory DC is created, and the bitmap image is
selected into this DC. Next, function CDC::BitBlt(…) is called enough times to paint the bitmap to
different locations of the dialog box. Finally a TRUE value is returned to prevent the background from
being updated by the default implementation.
145
Chapter 6. Dialog Box
{
return CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
}
This function has three parameters. The first parameter is a pointer to the device context of the target
window; the second is a pointer to the common control contained in the dialog box whose background is to
be customized; the third parameter specifies the control type, which could be CTLCOLOR_BTN,
CTLCOLOR_EDIT, CTLCOLOR_LISTBOX..., indicating that the control is a button, an edit box, a list box, and so
on.
We can return a brush handle that can be used to paint the background of the control. We can also let
the control to have a transparent background, in this case we must return a NULL brush handle.
In order to demonstrate how to customize the background of the common controls, in the sample, an
edit box, a check box, two radio buttons, a static text, a scroll bar, a list box and a simple combo box are
added to the application. Also, WM_CTLCOLOR message handler is added and the corresponding function
CDBDlg::OnCtlColor(…) is implemented as follows:
Stock Objects
In the above implementation, the background of different controls is painted using different brushes:
the button and static control have a transparent background; the background of the list box and scroll bar is
painted with a gray brush; the background of the message box is painted with a light gray brush. Here, all
the brushes are obtained through calling function ::GetStockObject(…) rather than being created by
ourselves.
In Windows, there are a lot of predefined stock objects that can be used. These objects include
brushes, pens, fonts and palette. The predefined brushes include white brush, black brush, gray brush, light
gray brush, dark gray brush, and null (hollow) brush. Function ::GetStockObject(…) will return a GDI
object handle (a brush handle in our sample). If we attach the returned handle to a GDI object, we must
detach it instead of deleting the object when it is no longer useful.
146
Chapter 6. Dialog Box
Foreground color
A Background color
Figure 6-5. All controls have transparent background, but their text doesn’t
We can call function CDC::SetBkMode(…) and use TRANSPARENT flag to set transparent background
drawing mode for text, otherwise it will be drawn with the default background color.
The background of a 3-D looking pushdown button can not be changed this way. Also, if we include
drop down or drop list combo box, the background color of its list box will not be customized by this
method because it is not created as the child window of the dialog box. To modify it, we need to derive new
class from CComboBox and override its OnCtlColor(…) member function.
147
Chapter 6. Dialog Box
Coordinates Conversion
Every window can be moved and resized by calling function CWnd::MoveWindow(…) or CWnd::
SetWindowPos(…). Also, a window’s size and position can be retrieved by calling function CWnd::
GetClientRect(…) and CWnd::GetWindowRect(…). The points retrieved using the former function are
measured in the client window’s coordinate system, and the points retrieved from the latter function are
measured in the screen (desktop) coordinate system. To convert coordinates from one system to another,
we can call function CWnd::MapWindowPoints(…) or CWnd::ScreenToClient(…).
For example, if there are two windows: window A and window B, which are attached two CWnd type
variables wndA and wndB. If we want to know the size and position of window A measured in window B’s
coordinate system, we can first obtain the size and position of window A in its local coordinate system, and
then convert them to window B system as follows:
wndA.GetClientRect(rect);
wndA.MapWindowPoints(&wndB, rect);
Or we can find the position and size of window A in the screen coordinate system, and call CWnd::
ScreenToClient(…) to convert them to window B’s coordinate system:
wndA.GetWindowRect(rect);
wndB.ScreenToClient(rect);
Sample
When the user resizes a window, a WM_SIZE message will be sent to that window. We can handle this
message to resize and move the controls contained in the dialog template.
Sample 6.6\DB demonstrates how to resize the common controls contained in the form view
dynamically. It is a standard SDI application generated from the Application Wizard. When generating the
application, CFormView is selected as the base class of the view in the last step. After the application is
generated, the following controls are added to the dialog template: an edit box, a static group control, two
buttons. The IDs of these controls are IDC_EDIT, IDC_STATIC_GRP, IDC_BUTTON_A and IDC_BUTTON_B
respectively.
If we compile and execute the application at this point, the application will behave awkwardly because
if we resize the window, the sizes/positions of the controls will not change, this may make the window not
well balanced (Figure 6-6).
We need to remember the original sizes and positions of the embedded controls and base their new
sizes and positions on them. In the sample, four CRect type variables are declared in class CDBView for this
purpose:
148
Chapter 6. Dialog Box
protected:
CRect m_rectA;
CRect m_rectB;
CRect m_rectEdit;
CRect m_rectStaticGrp;
BOOL m_bSizeAvailable;
……
}
Also, a Boolean type variable m_bSizeAvailable is added to indicate if the original positions and sizes
of the controls have been recorded.
There is no OnInitDialog() member function for class CFormView. The similar one is CView::
OnInitialUpdate(). This function is called when the view is first created and is about to be displayed. We
can record the positions and sizes of the controls in this function.
Variable m_bSizeAvailable is initialized to FALSE in the constructor of class CDBView:
CDBView::CDBView()
: CFormView(CDBView::IDD)
{
//{{AFX_DATA_INIT(CDBView)
//}}AFX_DATA_INIT
m_bSizeAvailable=FALSE;
}
Member function OnInitialUpdate() can be added to class CDBView through using Class Wizard. In
the sample, this function is implemented as follows:
void CDBView::OnInitialUpdate()
{
CFormView::OnInitialUpdate();
GetDlgItem(IDC_BUTTON_A)->GetWindowRect(m_rectA);
ScreenToClient(m_rectA);
GetDlgItem(IDC_BUTTON_B)->GetWindowRect(m_rectB);
ScreenToClient(m_rectB);
GetDlgItem(IDC_EDIT)->GetWindowRect(m_rectEdit);
ScreenToClient(m_rectEdit);
GetDlgItem(IDC_STATIC_GRP)->GetWindowRect(m_rectStaticGrp);
ScreenToClient(m_rectStaticGrp);
m_bSizeAvailable=TRUE;
}
if(m_bSizeAvailable == TRUE)
{
CRect rectA;
CRect rectB;
CRect rectEdit;
CRect rectStaticGrp;
CRect rectDlg(0, 0, m_rectA.right+10, m_rectStaticGrp.bottom+10);
GetDlgItem(IDC_BUTTON_A)->GetClientRect(rectA);
GetDlgItem(IDC_BUTTON_B)->GetClientRect(rectB);
GetDlgItem(IDC_EDIT)->GetWindowRect(rectEdit);
ScreenToClient(rectEdit);
GetDlgItem(IDC_STATIC_GRP)->GetWindowRect(rectStaticGrp);
ScreenToClient(rectStaticGrp);
rectA.OffsetRect(0, m_rectA.top);
rectB.OffsetRect(0, m_rectB.top);
if(cx > rectDlg.Width())
{
rectA.OffsetRect(cx-(rectDlg.right-m_rectA.left), 0);
rectB.OffsetRect(cx-(rectDlg.right-m_rectB.left), 0);
149
Chapter 6. Dialog Box
rectEdit.right=cx-(rectDlg.right-m_rectEdit.right);
rectStaticGrp.right=cx-(rectDlg.right-m_rectStaticGrp.right);
}
else
{
rectA.OffsetRect(m_rectA.left, 0);
rectB.OffsetRect(m_rectB.left, 0);
rectEdit.right=m_rectEdit.right;
rectStaticGrp.right=m_rectStaticGrp.right;
}
GetDlgItem(IDC_BUTTON_A)->MoveWindow(rectA);
GetDlgItem(IDC_BUTTON_B)->MoveWindow(rectB);
GetDlgItem(IDC_EDIT)->MoveWindow(rectEdit);
GetDlgItem(IDC_STATIC_GRP)->MoveWindow(rectStaticGrp);
}
}
The new horizontal and vertical sizes of the client window (CDBView) is passed through parameters cx
and cy. First we create a rectangle whose dimension is equal to the dimension of the dialog template. Then
we compare its horizontal size to cx, and vertical size to cy. If cx is greater than the template’s horizontal
size, we move button A and button B in the horizontal direction, increase the horizontal size of edit box and
static group control. If cx is not greater than the template’s horizontal size, we put button A and button B to
their original positions, set the horizontal sizes of edit box and static group control to their initial horizontal
sizes (this is why we need to know each control’s initial size and position). The same thing is done for
vertical sizes. Finally, function CWnd::MoveWindow(…) is called to carry out the resize and reposition.
With the above implementation, the form view will have a well balanced appearance all the time.
Here the first parameter id indicates the window that sent this notification, which is useless to us. The
second parameter is a NMHDR type pointer, which must be cast to TOOLTIPTEXT type in order to process a tool
tip notification. Structure TOOLTIPTEXT has the following format:
150
Chapter 6. Dialog Box
typedef struct {
NMHDR hdr;
LPTSTR lpszText;
WCHAR szText[80];
HINSTANCE hinst;
UINT uflags;
} TOOLTIPTEXT, FAR *LPTOOLTIPTEXT;
The ID of the target control (whose tool tip text is being retrieved) can be obtained from member hdr.
From this ID we can obtain the resource string that is prepared for the tool tip. There are three ways to
provide a tool tip string: 1) Prepare our own buffer that contains the tool tip text and assign the buffer’s
address to member lpszText. 2) Copy the tool tip text directly to member szText. 3) Stores the tool tip text
in a string resource, assign its ID to member lpszText. In the last case, we need to assign member hinst
the instance handle of the application, which can be obtained from function AfxGetResourceHandle().
Member uflsgs indicates if the control is a window or not.
Recall when we create tool bars and dialog bars in the first chapter, tool tips were all implemented in a
very simple way: we provide a string resource whose ID is exactly the same with the control ID, and
everything else will be handled automatically. When handling message TOOLTIPTEXT for dialog box, we
can also let the tool tip be implemented in a similar way. In order to do this, we can assign the resource ID
of the control to member lpszText and the application instance handle to member hinst. If there exists a
string resource whose ID is the same with the control ID, that string will be used to implement the tool tip.
Otherwise, nothing will be displayed because the string can not be found.
Sample
Sample 6.7\DB demonstrates how to implement tool tips for the controls contained in a dialog box. It
is based on sample 6.6\DB, with tool tips enabled for the following three controls: ID_EDIT, ID_BUTTON_A
and ID_BUTTON_B (Although sample 6.6\DB is a form view based application, the tool tip implementation is
the same with that of a dialog box).
Three string resources are added to the application, whose IDs are IDC_EDIT, IDC_BUTTON_A and
IDC_BUTTON_B. They will be used to implement tool tips for the corresponding edit box and buttons. In
function CDBView::OnInitialUpdate(), the tool tips are enabled as follows
void CDBView::OnInitialUpdate()
{
……
EnableToolTips(TRUE);
}
Message handler of TTN_NEEDTEXT must be added manually. First we need to declare a member
function OnToolTipNotify() in class CDBView:
BEGIN_MESSAGE_MAP(CDBView, CFormView)
……
ON_NOTIFY_EX(TTN_NEEDTEXT, 0, OnToolTipNotify)
END_MESSAGE_MAP()
Here, TTN_NEEDTEXT is sent through message WM_NOTIFY. Macro ON_NOTIFY_EX allows more than one
object to process the specified message. If we use this macro, our message handler must return TRUE if the
message is processed. If we do not process the message, we must return FALSE so that other objects can
151
Chapter 6. Dialog Box
continue to process this message. Please note that in the above message mapping, the second parameter
should always be 0.
Member function CDBView::OnToolTipNotify(…) is implemented as follows:
First the ID of the control is obtained. If the control is a window, this ID will be a valid handle. We can
retrieve the control’s resource ID by calling fucntion ::GetDlgCtrlID(…). Next, the resource ID is
assigned to member lpszText, and the application’s instance handle is assigned to member hinst.
With this method, we can only implement a tool tip which contains maximum of 80 characters. To
implement longer tool tips, we need to provide our own buffer and assign its address to member lpszText.
In this case, we do not need to assign the application’s instance handle to member hinst.
After adding the above implementation, we can just add string resources whose IDs are the same with
the resource IDs of the controls. By doing this, the tool tip will automatically implemented for them.
Frame Window
In a standard SDI or MDI application, tool bar and status bar can be implemented by declaring
CToolBar and CStatusBar type variables in class CMainFrame (They will be created in function
CMainFrame::OnCreate(…)). In a dialog-based application, the frame window is the dialog box itself, so we
need to embed CToolBar and CStatusBar type variables in the CDialog derived class and create them in
function CDialog::OnInitDialog().
However, unlike CFrameWnd, class CDialog is not specially designed to work together with status bar
and tool bar, so it lacks some features that are owned by class CFrameWnd: first, it does not support
automatic tool tip implementation, so we have to write TTN_NEEDTEXT message handler for displaying tool
tips; second, it does not support flyby implementation, so we also need to add other message handlers in
order to enable flybys.
152
Chapter 6. Dialog Box
Resource ID String
ID_BUTTON_YELLOW This is yellow button\nYellow Button
ID_BUTTON_GREEN This is green button\nGreen Button
ID_BUTTON_RED This is red button\nRed Button
ID_BUTTON_BLUE This is blue button\nBlue Button
The sub-string before character ‘\n’ will be used to implement flyby, and the sub-string after that will
be used to implement tool tip. We will see that by letting the control and the string resource share a same
ID, it is easier for us to implement both flybys and tool tips.
New CToolBar and CStatusBar type variables are declared in class CDBDlg:
Status Bar
A status bar is divided into several panes, each pane displays a different type of information. We can
create as many panes as we like. When implementing a status bar, we must provide each pane with an ID.
We can use these IDs to access each individual pane, and output text or graphic objects. Usually these IDs
are stored in a global integer array. In the sample, the following array is declared for the status bar:
The status bar will have only two panes. Usually the first pane of the status bar is used to display
flybys (In the idle state, “Ready” will be displayed in it). One property of status bar is that if we implement
a string resource whose ID is the same with the ID of a pane contained in a status bar, the string will be
automatically displayed in it when the application is idle. So here we can add two string resources whose
IDs are AFX_IDS_IDLEMESSAGE and IDS_MESSAGE respectively. Since Developer Studio does not allow us to
add a string resource starting with “AFX_”, we may copy this string resource from any standard SDI
application (An SDI application has string resource AFX_IDS_IDLEMESSAGE if it is generated by Application
Wizard).
BOOL CDBDlg::OnInitDialog()
{
……
if
153
Chapter 6. Dialog Box
(
!m_wndToolBar.Create(this) ||
!m_wndToolBar.LoadToolBar(IDD)
)
{
TRACE0("Failed to create toolbar\n");
return -1;
}
if
(
!m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators
(
indicators,
sizeof(indicators)/sizeof(UINT)
)
)
{
TRACE0("Failed to create status bar\n");
return -1;
}
m_wndStatusBar.SetPaneInfo
(
0,
m_wndStatusBar.GetItemID(0),
SBPS_STRETCH,
NULL
);
m_wndToolBar.SetBarStyle
(
m_wndToolBar.GetBarStyle() |
CBRS_TOOLTIPS | CBRS_FLYBY
);
RepositionBars
(
AFX_IDW_CONTROLBAR_FIRST,
AFX_IDW_CONTROLBAR_LAST,
0
);
return TRUE;
}
Here, the procedure of creating the tool bar and status bar is almost the same with what we need to do
for a standard SDI and MDI application in function CMainFrame::OnCreate(). The difference is that when
implementing them in a dialog box, there is no need to set docking/floating properties for the control bars.
Function CWnd::RepositionBars(…) is also called to calculate the position of control bars then and
reposition them according to the dimension of the client area. If we do not call this function, the status bar
and tool bar may be randomly placed and thus can not be seen. When calling this function, we can use
AFX_IDW_CNTROLBAR_FIRST and AFX_IDW_CONTROLBAR_LAST instead of providing actual IDs of the control
bars.
void CWnd::RepositionBars
(
UINT nIDFirst, UINT nIDLast, UINT nIDLeftOver, UINT nFlag=CWnd::reposDefault,
LPRECT lpRectParam=NULL, LPCRECT lpRectClient=NULL
);
The function has six parameters, among them, nFlag, lprectParam and lpRectClient all have default
values. When we called this function in the previous step, all the default values were used. This will pass
CWnd::reposDefault to parameter nFlag, which will cause the default layout to be performed. If we pass
154
Chapter 6. Dialog Box
CWnd::reposQuery to parameter nFlag, we can prepare a CRect type object and pass its address to
lpRectParam to receive the new client area dimension (The client area is calculated with the consideration
of control bars, their sizes are deducted from the original dimension of the client area). This operation will
not let the layout be actually carried out. Based on the retrieved size, we can adjust the size of the dialog
box and move the controls so that we can leave enough room to accommodate the tool bar and the status
bar.
The following is the updated implementation of function CDBDlg::OnInitDialog():
BOOL CDBDlg::OnInitDialog()
{
……
CRect rectOld;
CRect rectNew;
CRect rect;
CPoint ptOffset;
CWnd *pWndCtrl;
GetClientRect(rectOld);
if
(
!m_wndToolBar.Create(this) ||
!m_wndToolBar.LoadToolBar(IDD)
)
{
TRACE0("Failed to create toolbar\n");
return -1;
}
if
(
!m_wndStatusBar.Create(this) ||
!m_wndStatusBar.SetIndicators
(
indicators,
sizeof(indicators)/sizeof(UINT)
)
)
{
TRACE0("Failed to create status bar\n");
return -1;
}
m_wndStatusBar.SetPaneInfo
(
0,
m_wndStatusBar.GetItemID(0),
SBPS_STRETCH,
NULL
);
m_wndToolBar.SetBarStyle
(
m_wndToolBar.GetBarStyle() |
CBRS_TOOLTIPS | CBRS_FLYBY
);
RepositionBars
(
AFX_IDW_CONTROLBAR_FIRST,
AFX_IDW_CONTROLBAR_LAST,
0,
CWnd::reposQuery,
rectNew
);
ptOffset.x=rectNew.left-rectOld.left;
ptOffset.y=rectNew.top-rectOld.top;
pWndCtrl=GetWindow(GW_CHILD);
while(pWndCtrl)
{
pWndCtrl->GetWindowRect(rect);
ScreenToClient(rect);
rect.OffsetRect(ptOffset);
pWndCtrl->MoveWindow(rect, FALSE);
pWndCtrl=pWndCtrl->GetNextWindow();
}
GetWindowRect(rect);
rect.right+=rectOld.Width()-rectNew.Width();
155
Chapter 6. Dialog Box
rect.bottom+=rectOld.Height()-rectNew.Height();
MoveWindow(rect);
RepositionBars
(
AFX_IDW_CONTROLBAR_FIRST,
AFX_IDW_CONTROLBAR_LAST,
0
);
return TRUE;
}
First function CWnd::GetClientRect() is called and the dimension of client window is stored in
rectOld. After the control bars are created, we call function CWnd::RepositionBars(…) and use flag
CWnd::reposQuery to obtain the new layout dimension with the consideration of two control bars. The new
layout dimension is stored in variable rectNew. The offset is calculated by deducting rectOld from
rectNew. To access all the controls in the dialog box, we first call CWnd::GetWindow(…) using GW_CHILD
flag to obtain the first child window of the dialog box, then call CWnd::GetNextWindow() repeatedly to find
all the other child windows. Each child window is moved according to the offset dimension. Finally the
dialog box is resized by calling function CWnd::RepositionBars(…) using the default parameters.
BEGIN_MESSAGE_MAP(CDBDlg, CDialog)
……
ON_WM_QUERYDRAGICON()
//}}AFX_MSG_MAP
ON_NOTIFY_EX(TTN_NEEDTEXT, 0, OnToolTipText)
ON_MESSAGE(WM_SETMESSAGESTRING, OnSetMessageString)
ON_MESSAGE(WM_POPMESSAGESTRING, OnPopMessageString)
END_MESSAGE_MAP()
UINT nID=pNMHDR->idFrom;
if(pNMHDR->code == TTN_NEEDTEXT && (pTTT->uFlags & TTF_IDISHWND))
{
nID=((UINT)(WORD)::GetDlgCtrlID((HWND)nID));
}
if(nID != 0)
{
szTip.LoadString(nID);
156
Chapter 6. Dialog Box
szTip=szTip.Right(szTip.GetLength()-szTip.Find('\n')-1);
lstrcpy(pTTT->szText, szTip);
*pResult = 0;
return TRUE;
}
*pResult=-1;
return FALSE;
}
First the target window handle is obtained from idFrom member of structure NMHDR, and the ID of the
button is retrieved by calling function ::GetDlgCtrlID(…). Then a string with the same ID is loaded from
the resource into a CString type variable, and the sub-string after the ‘\n’ character is copied into szText
member of structure TOOLTIPTEXT. The sub-string before this character will be used to implement flyby.
The rest two functions are implemented as follows:
if(nIDMsg)
{
if(strMsg.LoadString(nIDMsg) != 0)m_wndStatusBar.SetPaneText(0, strMsg);
}
return nIDMsg;
}
The control ID is sent through WPAMAM parameter of the message, so in the first function, we just use
this ID to load a string from the resource, and display it in the first pane of the status bar by calling function
CStatusBar::SetPaneText(…). For the second function, if there is no pop up message, we just return 0.
Otherwise we call the first function to display an appropriate message.
void CDBDoc::OnDialogBardialog()
{
CBarDialog dlg;
dlg.DoModal();
}
157
Chapter 6. Dialog Box
If we do this, the tool bar and the status bar will be added to the dialog box. Also, the tool tips will
work. However, there will be no flyby displayed in the status bar.
Problem
The reason for this is that when the application tries to display a flyby on the status bar, it will always
try to put it on the status bar of the top-most parent window, which is the mainframe window in an SDI or
MDI application. If the top-most window is inactive, the flyby will not be displayed.
When we invoke a modal dialog box, the mainframe window will always be inactivated. This is the
reason why the flyby will be displayed neither on the status bar of the dialog box nor on the status bar of
the mainframe window.
Work Around
One fix to this problem is to override the member function that is used to display the flyby. If we study
the source code of class CControlBar, we will find that the flyby display is implemented as follows: after
the mouse cursor enters the tool bar, a timer with time out period of 300 millisecond will be started. When
this timer times out, the application checks to see if the cursor is still within the tool bar. If so, it kills the
timer, starts another timer with a time out period of 200 millisecond. Next, it finds out the ID of the control
that is under the mouse cursor and sends WM_SETMESSAGESTRING message to the mainframe window (if it is
active).
The time out event is handled by function CControlBar::OnTimer(), we can override it and send
WM_SETMESSAGESTRING message to the dialog box window.
The following is the original implementation of function CControlBar::OnTimer():
Note that in the above code fragment, function CWnd::GetTopLevelParent() is used to obtain the
window where flyby should be displayed. If we replace it with CWnd::GetParent(), everything will be
fixed.
Overriding CToolBar::OnTimer(…)
In the sample a new class CDlgToolBar is added to the application, whose base class is CToolBar.
Within the new class, function OnTimer(…) is declared to override the default implementation:
DECLARE_MESSAGE_MAP()
};
158
Chapter 6. Dialog Box
This is just a copy of function CControlBar::OnTimer(…), except that here function CWnd::
GetTopLevelParent() is replaced by CWnd::GetParent().
An Alternate Solution
But this is not the best solution. Because the current implementation of function CControlBar::
OnTimer(…) is not guaranteed to remain unchanged in the future, there is a possibility that the above
implementation will become incompatible with future versions of MFC.
The best solution is to set our own timer and simulate the default behavior of control bar. We can
bypass all the implementation in the base class and set up our own 300 millisecond timer when the mouse
159
Chapter 6. Dialog Box
cursor first enters the tool bar. When this timer times out, we check if the cursor is still within the tool bar.
If so, we kill the timer and set another 200 millisecond timer. Whenever the timer times out, we check the
position of mouse cursor and send WM_SETMESSAGESTRING message to the window that contains the tool bar.
Sample 6.8-3\DB demonstrates this method. It is based on sample 6.8-2\DB, with the WM_TIMER
message handler removed from the application.
To detect mouse movement, we need to override function CWnd::PreTranslateMessage(…). Also, two
timer IDs are defined to set timers:
The above IDs can be any integers. A Boolean type variable m_bTimerOn is declared in class
CDlgToolBar. It will be used to indicate if the timer is currently
enabled or not.
Variable m_bTimerOn is initialized in the constructor:
CDlgToolBar::CDlgToolBar():CToolBar()
{
m_bTimerOn=FALSE;
}
If the message is WM_MOUSEMOVE and the timer is off, this indicates that the mouse cursor has just
entered the tool bar. We need to set timer ID_TIMER_DLGWAIT. Also, we need to set flag m_bTimerOn to
TRUE.
Function CDlgToolBar::OnTimer(…) is implemented as follows:
::GetCursorPos(&point);
ScreenToClient(&point);
nHit=OnToolHitTest(point, NULL);
if(nHit >= 0)
{
GetParent()->SendMessage(WM_SETMESSAGESTRING, nHit);
if(nIDEvent == ID_TIMER_DLGWAIT)
{
KillTimer(ID_TIMER_DLGWAIT);
SetTimer(ID_TIMER_DLGCHECK, 200, NULL);
}
}
if(nHit < 0)
{
KillTimer(nIDEvent);
m_bTimerOn=FALSE;
GetParent()->SendMessage(WM_POPMESSAGESTRING, AFX_IDS_IDLEMESSAGE);
}
}
else CToolBar::OnTimer(nIDEvent);
}
We call function ::GetCursorPos(…) to retrieve the current mouse cursor position, then call function
CWnd::ScreenToClient(…) to convert it to the tool bar coordinate system. Next we call CWnd::
OnToolHitTest(…) to obtain the control ID of the button, then send WM_SETMESSAGESTRING message to the
160
Chapter 6. Dialog Box
parent of the control bar. In case the current timer is ID_TIMER_DLGWAIT, we kill it and set timer
ID_TIMER_DLGCHECK with a time out period of 200 millisecond. If the mouse cursor is not within the
toolbar, this indicates that it has just been moved outside the control bar. In this case, we need to send
message WM_POPMESSAGESTRING to the parent window, then kill the timer.
With this implementation, the flybys will work as if they were implemented in a standard control bar.
Summary:
1) To implement modeless dialog box, we need to declare CDialog type member variable and call
CDialog::Create(…) instead of function CDialog::DoModal().
1) When a modeless dialog box is dismissed, the window becomes hidden rather than being destroyed. So
if the user invoke the dialog again, we need to call function CWnd::ShowWindow(…) to activate the
window rather than create it again.
1) We can decide the visual state of a window by calling function CWnd::IsWindowVisible(…).
1) Property sheet can be implemented as follows: 1) Derive a class from CPropertySheet. 2) Add dialog
template for each property page. 3) Implement a CPropertyPage derived class for each dialog template
created in step 2). 4) Use the classes created in step 3) to declare variables in the class derived from
CPropertySheet. 5) In the constructor of CPropertySheet derived class, call CPropertySheet::
AddPage(…) for each page.
1) A property sheet can have either standard style or wizard style. To enable wizard style, we need to call
function CPropertySheet::SetWizardMode().
1) To convert a dialog template dimension (measured in dialog box unit) to its actual screen size
(measured in screen pixels), we need to call function CDialog::MapDialogRect(…).
1) Tracking sizes and maximized size of a window can be set by handling message WM_GETMINMAXINFO.
1) The background of a window can be customized by handling message WM_ERASEBKGND.
1) The background of controls contained in a dialog box can be customized by handling message
WM_CTLCOLOR. When handling this message, we can provide a NULL (hollow) brush to make the
background transparent.
1) Tool tips can be added for controls contained in a dialog box by calling function
CWnd::EnableToolTips(…) and handling notification TTN_NEEDTEXT.
1) Tool bar and status bar can also be implemented in a dialog box. We must move the controls contained
in the dialog box to accommodate the control bars. Also, we need to handle messages TTN_NEEDTEXT,
WM_SETMESSAGESTRING and WM_POPMESSAGESTRING in order to implement tool tips and flybys.
161
Chapter 7. Common Dialog Boxes
Chapter 7
C
ommon dialog boxes are very useful in Windows programming. We can use these dialog boxes to
select files, colors, fonts, set up printer, do search and replace. Since these kind of operations are
very common for all applications, common dialogs are pre-implemented by the operating system.
We do not need to create them from dialog template if we want to use one of these dialog boxes.
CFileDialog dlg(TRUE);
dlg.DoModal();
That’s all we need to do. Since class CFileDialog does not have a default constructor, we must pass at
least a Boolean type value to the first parameter of its constructor. If this value is TRUE, the dialog box
will be an “Open” dialog box, if it is FALSE, the dialog box will be a “Save As” dialog box. Because the
dialog template is already implemented by the operating system, we don’t even need to design a single
button for it. However, with the above simple implementation, what we can create is a very general file
open dialog box: it does not have file filter, it does not display default file name, also, the initial directory is
always the current working directory.
Structure OPENFILENAME
To customize the default behavior of file dialog box, we need to add extra code. Fortunately, this class
is designed so that its properties can be easily changed by the programmer. We can make changes to its
default file extension filter, default file name. We can also enable or disable certain controls in the dialog
box, or even use our own dialog template.
Class CFileDialog has a very important member variable: m_ofn. It is declared by structure
OPENFILENAME, which has the following format:
162
Chapter 7. Common Dialog Boxes
LPTSTR lpstrFileTitle;
DWORD nMaxFileTitle;
LPCTSTR lpstrInitialDir;
LPCTSTR lpstrTitle;
DWORD Flags;
WORD nFileOffset;
WORD nFileExtension;
LPCTSTR lpstrDefExt;
DWORD lCustData;
LPOFNHOOKPROC lpfnHook;
LPCTSTR lpTemplateName;
} OPENFILENAME;
It has 20 members, which can all be used to customize the dialog box. In this and the following
sections, we are going to see how to use them to change the default behavior of the file dialog box.
In the above statement, “CPP File(*.cpp)\0*.cpp\0” is the first filter and “HTML File(*.htm)
\0*.htm\0” is the second filter.
A filter can select more than one type of files. If we specify this type of filter, the different file types
should be separated by a ‘;’ character. For example, in the above example, if we want the first filter to
select both “*.cpp” and “*.h” file, its filter string should be “*.cpp;*.h”.
Besides the standard filter, we can also specify a custom filter. In the file dialog boxes, the custom
filter will always be displayed in the first place of the filter list. To specify a custom filter, we can store the
filter string in a buffer, use member lpstrCustomFilter to store the buffer’s address, and use member
nMaxCustFilter to store the buffer’s size.
If we have a list of filters, we can use only one of them at any time. Member nFilterIndex lets us
specify which filter will be used as the initial one. Here the index to the first file filter is 1.
File Open
Sample 7.1\CDB demonstrates how to use file dialog box and how to customize its standard styles. It
is a standard SDI application generated by Application Wizard. After the application is generated, a new
sub- menu File Dialog Box is added to mainframe menu IDR_MAINFRAME between View and Help. Two
new commands File Dialog Box | File Open and File Dialog Box | File Save are added to this sub-menu,
whose IDs are ID_FILEDIALOGBOX_FILEOPEN and ID_FILEDIALOGBOX_FILESAVE respectively. Two
WM_COMMAND message handlers are added to class CCDBDoc (the document class) for the new commands
through using Class Wizard, the corresponding function names are
CCDBDoc::OnFiledialogboxFileopen() and CCDBDoc::OnFiledialogboxFilesave() respectively.
163
Chapter 7. Common Dialog Boxes
In the sample, command File Dialog Box | File Open is used to invoke a file open dialog box which
has two filters and one custom filter. The message handler is implemented as follows:
void CCDBDoc::OnFiledialogboxFileopen()
{
CFileDialog dlg(TRUE);
CString szStr;
dlg.m_ofn.lpstrFilter="Source
Files(*.C;*.CPP;*.H;*.HPP)\0*.C;*.CPP;*.H;*.HPP\0Document(*.DOC;*.HTML)\0*.DOC;*.HTML\0Al
l(*.*)\0*.*\0";
dlg.m_ofn.nFilterIndex=1;
dlg.m_ofn.lpstrCustomFilter="DIB Files(*.BMP)\0*.BMP\0";
dlg.m_ofn.nMaxCustFilter=26;
if(dlg.DoModal() == IDOK)
{
szStr="File Name: ";
szStr+=dlg.GetFileName();
AfxMessageBox(szStr);
szStr.Empty();
szStr="Path Name: ";
szStr+=dlg.GetPathName();
AfxMessageBox(szStr);
}
}
Before we call function CFileDialog::DoModal(), the default behavior of file dialog box is modified.
Here, three file filters are specified: the first filter selects “*.c”, “*.cpp”, “*.h” and “*.hpp” files; the second
filter selects “*.doc” and “*.htm” files; the third filter selects all files. Since “1” is assigned to member
nFilterIndex, the filter that is used initially would be “*.C;*.CPP;*.H;*.HPP”.
A custom filter is also specified, which will select only files with “*.bmp” extension.
After calling function CFileDialog::DoModal(), if the user has picked up a file and clicked “OK”
button, both file name and path name will be displayed in a message box.
File Save
When we ask the user to save a file, there are two more things that should be considered. First, we
need to specify a default file name that will be used to save the data. Second, when the user uses “*.*” file
filter, we may need to provide a default file extension.
We can specify the default file name by using members lpstrFile and nMaxFile of structure
OPENFILENAME. We can store the default file name in a buffer, assign the buffer’s address to member
lpstrFile and the buffer’s size to member nMaxFile. With this implementation, when the file save dialog
box is invoked, the default file name will appear in “File Name” edit box. Also, we can use member
lpstrDefExt to specify a default file extension. Please note that the maximum size of an extension string is
3 (If the string contains more than 3 characters, only the first three characters will be used).
The following is the WM_COMMAND message handler for command File Dialog Box | File Save:
void CCDBDoc::OnFiledialogboxFilesave()
{
char szFile[256];
CFileDialog dlg(FALSE);
CString szStr;
sprintf(szFile, "TestFile");
dlg.m_ofn.lpstrFile=szFile;
dlg.m_ofn.nMaxFile=sizeof(szFile);
dlg.m_ofn.lpstrDefExt="DIB";
dlg.m_ofn.lpstrFilter="Source
Files(*.C;*.CPP;*.H;*.HPP)\0*.C;*.CPP;*.H;*.HPP\0Document(*.DOC;*.HTML)\0*.DOC;*.HTML\0Al
l(*.*)\0*.*\0";
dlg.m_ofn.nFilterIndex=1;
if(dlg.DoModal() == IDOK)
{
szStr="File Name: ";
szStr+=dlg.GetFileName();
AfxMessageBox(szStr);
szStr.Empty();
szStr="Path Name: ";
szStr+=dlg.GetPathName();
AfxMessageBox(szStr);
164
Chapter 7. Common Dialog Boxes
}
}
In the sample, the default file name is set to “TestFile” (stored in buffer szFile). Also, default file
extension is “DIB”. All other settings are the same with the file open dialog box implemented above.
The Explorer style file dialog box can display long file name, the old style dialog box will convert all
long file names to 8.3 format (8 characters of file name + dot + 3 characters of extension). By default, class
CFileDialog will implement Explorer-style file dialog box. If we want to create old style file dialog box,
we must set changes to member Flags of structure OPENFILENAME.
Member Flags is a DWORD type value, which contains many 1-bit flags that can be set or cleared to
change the styles of the file dialog box. By default, its OFN_EXPLORER bit is set, and this will let the dialog
box have Explorer-style. If we set this bit to 0, the dialog box will be implemented in the old style.
Bit Description
OFN_SHOWHELP Set this bit to enable “Help” button on the dialog box. If we specify this style, a
help page will pop up giving users hints on file selection after the user presses
“Help” button.
OFN_HIDEREADONLY Set this bit to 0 to enable a Read only check box on the dialog box so that the
user can specify if the file should be opened only for read.
OFN_ALLOWMULTISELECT Set to allow multiple files to be selected at the same time.
165
Chapter 7. Common Dialog Boxes
Sample
Sample 7.2\CDB demonstrates these styles. It is based on sample 7.1\CDB, with two new commands
File Dialog Box | Customized File Open and File Dialog Box | Customize File Open Old added to the
application. The IDs of the two commands are ID_FILEDIALOGBOX_CUSTOMIZEDFILEOPEN and
ID_FILEDIALOGBOX_CUSTOMIZEFILEOPENOLD respectively. Message handlers are added for them through
using Class Wizard, the corresponding member functions are CCDBDoc::
OnFiledialogboxCustomizedfileopen() and CCDBDoc::OnFiledialogboxCustomizefileopenold().
For dialog box invoked by command File Dialog Box | Customized File Open, multiple file selection
is enabled. Also, the dialog box has a “Help” button and a “Read only” check box. The following is the
implementation of this command:
void CCDBDoc::OnFiledialogboxCustomizedfileopen()
{
CFileDialog dlg(TRUE);
CString szStr;
POSITION posi;
LPSTR lpstr;
dlg.m_ofn.lpstrFilter="Source
Files(*.C;*.CPP;*.H;*.HPP)\0*.C;*.CPP;*.H;*.HPP\0Document(*.DOC;*.HTML)\0*.DOC;*.HTML\0Al
l(*.*)\0*.*\0";
dlg.m_ofn.nFilterIndex=1;
dlg.m_ofn.lpstrCustomFilter="DIB Files(*.BMP)\0*.BMP\0";
dlg.m_ofn.nMaxCustFilter=26;
dlg.m_ofn.Flags|=OFN_ALLOWMULTISELECT | OFN_SHOWHELP;
dlg.m_ofn.Flags&=~OFN_HIDEREADONLY;
dlg.m_ofn.lpstrTitle="Explorer Style Open";
if(dlg.DoModal() == IDOK)
{
szStr="File Name: ";
lpstr=dlg.m_ofn.lpstrFile+dlg.m_ofn.nFileOffset;
while(*lpstr != '\0')
{
szStr+=lpstr;
szStr+="; ";
lpstr+=strlen(lpstr);
lpstr++;
}
AfxMessageBox(szStr);
szStr.Empty();
szStr="Path Name: ";
posi=dlg.GetStartPosition();
while(posi != NULL)
{
166
Chapter 7. Common Dialog Boxes
szStr+=dlg.GetNextPathName(posi);
szStr+="; ";
}
AfxMessageBox(szStr);
}
}
Two flags OFN_ALLOWMULTISELECT and OFN_SHOWHELP are set, this will enable multiple file selection
and display the “Help” button. Also, flag OFN_HIDEREADONLY is disabled, this will display “Read only”
check box in the dialog box. If the dialog box returns value IDOK (This indicates the user has pressed “OK”
button), the first file name is obtained by doing the following:
lpstr=dlg.m_ofn.lpstrFile+dlg.m_ofn.nFileOffset;
Because the file names are separated by ‘\0’ characters, we can use the following statement to access
next file name:
lpstr+=strlen(lpstr);
void CCDBDoc::OnFiledialogboxCustomizefileopenold()
{
……
LPSTR lpstr;
LPSTR lpstrNx;
char buf[256];
……
dlg.m_ofn.Flags&=~OFN_LONGNAMES;
dlg.m_ofn.lpstrTitle="Old Style Open";
if(dlg.DoModal() == IDOK)
{
szStr="File Name: ";
lpstr=dlg.m_ofn.lpstrFile+dlg.m_ofn.nFileOffset;
while(TRUE)
{
lpstrNx=strchr(lpstr, ' ');
if(lpstrNx == NULL)
{
szStr+=lpstr;
break;
}
else
{
memset(buf, 0, 256);
strncpy(buf, lpstr, lpstrNx-lpstr);
szStr+=buf;
}
szStr+="; ";
lpstr=lpstrNx;
lpstr++;
}
AfxMessageBox(szStr);
……
}
Instead of checking ‘\0’, SPACE characters are checked between file names. Because SPACE
character is not the end of a null-terminated string, we have to calculate the length for each file name.
If we compile and execute the application at this point, the dialog boxes invoked by the two newly
added commands should let us select multiple files by using mouse along with CTRL or SHIFT key.
167
Chapter 7. Common Dialog Boxes
New Style
If we are writing code for Windows 95 or Windows NT4.0, things become very simple. There are
some API shell functions that can be called to implement a “folder selection” dialog box with just few
simple steps. We can call function ::SHBrowseForFolder(…) to implement dialog box that let the user
select folder, and call function ::SHGetPathFromIDList(…) to retrieve the folder that has been selected by
the user.
The following is the prototype of function ::SHBrowseForFolder(…):
WINSHELLAPI LPITEMIDLIST WINAPI ::SHBrowseForFolder(LPBROWSEINFO lpbi);
It has only one parameter, which is a pointer to structure BROWSEINFO. The structure lets us set the
styles of folder selection dialog box, it has eight members:
Member hwndOwner is the handle of the window that will be the parent of folder selection dialog box, it
can be NULL (In this case, the folder selection dialog box does not belong to any window). Member
pidlRoot specifies which folder will be treated as “root” folder, if it is NULL, the “desktop” folder is used
as the root folder. We must provide a buffer with size of MAX_PATH and assign its address to member
pszDisplayName for receiving folder name. Member lpszTitle lets us provide a customized title for folder
selection dialog box. Members lpfn, lParam and iImage let us further customize the behavior of dialog box
by specifying a user-implemented callback function. Generally we can use the default implementation, in
which case these members can be set to NULL. Member ulFlags lets us set the styles of folder selection
dialog box. For example, we can enable computer and printer selections by setting
BIF_BROWSEFORCOMPUTER and BIF_BROWSEFORPRINTER flags.
Function ::SHBrowseForFolder(…) returns a pointer to ITEMIDLIST type object, which can be passed
to function ::SHGetPathFromIDList(…) to retrieve the selected folder name:
We need to pass the pointer returned by function ::SHBrowserForFolder(…) to pidl parameter, then
provide our own buffer whose address is passed to parameter pszPath for retrieving directory name.
Because these functions are shell functions, the buffers returned from them can not be released using
normal method. Instead, it should be released by shell’s task allocator.
Shell’s task allocator can be obtained by calling function ::SHGetMalloc(…):
168
Chapter 7. Common Dialog Boxes
By calling this function, we can access shell’s IMalloc interface (a shell’s interface that is used to
allocate, free and manage memory). We need to pass the address of a pointer to this function, then we can
use method IMalloc::Free(…) to released the buffers allocated by the shell.
The following is the implementation of command File Dialog Box | Dir Dialog:
void CCDBDoc::OnFiledialogboxDirdialog()
{
BROWSEINFO bi;
char szBuf[MAX_PATH];
LPITEMIDLIST pidl;
LPMALLOC pMalloc;
CString szStr;
if(::SHGetMalloc(&pMalloc) == NOERROR)
{
bi.hwndOwner=NULL;
bi.pidlRoot=NULL;
bi.pszDisplayName=szBuf;
bi.lpszTitle=_T("Select Directory");
bi.ulFlags=BIF_RETURNFSANCESTORS | BIF_RETURNONLYFSDIRS;
bi.lpfn=NULL;
bi.lParam=0;
if((pidl=::SHBrowseForFolder(&bi)) != NULL)
{
if(::SHGetPathFromIDList(pidl, szBuf))
{
szStr="Selected Directory: ";
szStr+=szBuf;
AfxMessageBox(szStr);
}
pMalloc->Free(pidl);
}
pMalloc->Release();
}
}
First function ::SHGetMalloc(…) is called to retrieve a pointer to shell’s lMalloc interface, which will
be used to release the memory allocated by the shell. Then, structure BROWSEINFO is filled. Both hwndOwner
and pidlRoot are assigned NULL. By doing this, the dialog will have no parent, and the desktop folder will
be used as its root folder. The title of dialog box is changed to “Select Directory”. For member ulFlags,
two bits BIF_RETURNFSANCESTORS and BIF_RETURNONLYFSDIRS are set to 1. This will allow the user to select
only file system ancestors and file system directories. The returned buffer’s address is stored in pointer
pidl, which is passed to function ::SHBrowseForFolder(…) for retrieving the directory name. The selected
directory name is stored in buffer szBuf, and is displayed in a message box. Finally, memory allocated by
the shell is released by shell’s lMalloc interface.
Old Style
If we are writing code for Win32 applications, the above-mentioned method does not work. We must
use the old style file dialog box to implement folder selection.
The default dialog box has two list boxes, one is used for displaying directory names, the other for
displaying file names. One way to implement directory selection dialog box is to replace the standard
dialog template with our own. To avoid any inconsistency, we must include all the controls contained in the
standard template in the custom dialog template (with the same resource IDs), hide the controls that we
don’t want, and override the default class to change its behavior.
To use a user-designed dialog template, we must: 1) Prepare a custom dialog template that has all the
standard controls. 2) Set OFN_ENABLETEMPLATE bit for member Flags of structure OPENFILENAME, assign
custom dialog template name to member lpTemplateName (If the dialog has an integer ID, we need to use
MAKEINTRESOURCE macro to convert it to a string ID). 3) Assign the instance handle of the application to
member hInstance, which can be obtained by calling function AfxGetInstanceHandle().
The standard file dialog box has two list boxes that are used to display files and directories, two combo
boxes to display drives and file types, an edit box to display file name, a “Read only” check box, several
static controls to display text, and “OK”, “Cancel” and “Help” buttons. The following table lists their IDs
and functions:
169
Chapter 7. Common Dialog Boxes
edt1 stc1
stc3 IDOK
lst2
lst1
IDCANCEL
pshHelp
stc4
stc2
We must design a dialog template that contains exactly the same controls in order to replace the default
template with it. This means that the custom dialog template must have static controls with IDs of 1088,
1089, 1090..., list boxes with IDs of 1120 and 1121, combo boxes with IDs of 1136 and 1137, and so on.
Although we can not delete any control, we can resize the dialog template and change the positions of the
controls so that it will fit our use.
To let user select only folders, we need to hide following controls (along with the static control that
contains text “Directories”): stc1, stc2, stc3, edt1, lst1, cmb1. We can call function
CWnd::ShowWindow(…) and pass SW_HIDE to its parameter in the dialog initialization stage to hide these
controls. Figure 7-3 shows the custom dialog template IDD_DIR implemented in the sample.
We must also fill edit box edt1 with a dummy string, because if it is empty or filled with “*.*”, the file
dialog box will not close when the user presses “OK” button.
By default, clicking on “OK” button will not close the dialog box if the currently highlighted folder is
not expanded. In this case, clicking on “OK” button will expand the folder, and second clicking will close
the dialog box. To overcome this, we need to call function CFileDialog::OnOK() twice when the “OK”
button is clicked, this will close the dialog box under any situation.
170
Chapter 7. Common Dialog Boxes
We need to override class CFileDialog in order to change its default behavior. In the sample
application, new class MCDialogBox is added by Class Wizard, whose base class is selected as CDialogBox.
Three new member functions are added: constructor, MCFileDialog::OnOK() and MCFileDialog::
OnInitDialog(). The following is this new class:
public:
MCFileDialog
(
BOOL bOpenFileDialog=FALSE,
LPCTSTR lpszDefExt=NULL,
LPCTSTR lpszFileName=NULL,
DWORD dwFlags=OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
LPCTSTR lpszFilter=NULL,
CWnd* pParentWnd=NULL
);
protected:
//{{AFX_MSG(MCFileDialog)
virtual BOOL OnInitDialog();
virtual void OnOK();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
Like class CFileDialog, the constructor of MCFileDialog has six parameters. The first parameter
specifies if the dialog box is an “Open File” or a “Save As” dialog box, the rest parameters specify default
file extension, default file name, style flags, filter, and parent window.
The constructor is implemented as follows:
MCFileDialog::MCFileDialog
(
BOOL bOpenFileDialog,
LPCTSTR lpszDefExt,
LPCTSTR lpszFileName,
DWORD dwFlags,
LPCTSTR lpszFilter,
CWnd* pParentWnd
) : CFileDialog
(
bOpenFileDialog,
lpszDefExt,
lpszFileName,
dwFlags,
lpszFilter,
pParentWnd
)
{
}
171
Chapter 7. Common Dialog Boxes
Nothing is done in this function except calling the default constructor of the base class. Function
MCFileDialog::InitDialog() is implemented as follows:
BOOL MCFileDialog::OnInitDialog()
{
CListBox *ptrListBox;
GetDlgItem(stc1)->ShowWindow(SW_HIDE);
GetDlgItem(stc2)->ShowWindow(SW_HIDE);
GetDlgItem(stc3)->ShowWindow(SW_HIDE);
GetDlgItem(edt1)->ShowWindow(SW_HIDE);
GetDlgItem(lst1)->ShowWindow(SW_HIDE);
GetDlgItem(cmb1)->ShowWindow(SW_HIDE);
GetDlgItem(65535)->ShowWindow(SW_HIDE);
SetDlgItemText(edt1, "DummyString");
GetDlgItem(lst2)->SetFocus();
CFileDialog::OnInitDialog();
ptrListBox=(CListBox *)GetDlgItem(lst2);
ptrListBox->SetCurSel(0);
return FALSE;
}
The controls that are useless for picking up folder are set hidden, and edt1 edit box is filled with a
dummy string (it can be any string). The initial focus is set to the list box which will be used to display the
directories. After calling function OnInitDialog() of base class (this will implement default dialog
initialization), the first directory in the list box is highlighted.
The implementation of function MCFileDialog::OnOK() is very simple:
void MCFileDialog::OnOK()
{
CFileDialog::OnOK();
CFileDialog::OnOK();
}
The standard file dialog template can be found in file “Commdlg.dll”, which is usually located under
system directory. A copy of this file can be also found under Chap7\.
In the sample, command File Dialog Box | Dir Dialog Old is implemented as follows:
void CCDBDoc::OnFiledialogboxDirdialogold()
{
CString szStr;
MCFileDialog dlg
(
FALSE,
NULL,
NULL,
OFN_SHOWHELP | OFN_HIDEREADONLY |
OFN_OVERWRITEPROMPT | OFN_ENABLETEMPLATE,
NULL,
AfxGetApp()->m_pMainWnd
);
dlg.m_ofn.hInstance=AfxGetInstanceHandle();
dlg.m_ofn.lpTemplateName=MAKEINTRESOURCE(IDD_DIR);
dlg.m_ofn.Flags&=~OFN_EXPLORER;
dlg.m_ofn.Flags&=~OFN_LONGNAMES;
dlg.m_ofn.lpstrTitle="Directory Dialog";
if(dlg.DoModal() == IDOK)
{
dlg.m_ofn.lpstrFile[dlg.m_ofn.nFileOffset-1]=0;
szStr="Selected Dir: ";
szStr+=dlg.m_ofn.lpstrFile;
AfxMessageBox(szStr);
}
}
Flag OFN_EXPLORER must be disabled in order to implement old style file dialog box. The name of
custom dialog template is assigned to member lpTemplateName of structure OPENFILENAME. After calling
function MCFileDialog::DoModal(), the directory name is retrieved from member lpstrFile of structure
OPENFILENAME. Since member nFileOffset specifies position where file name starts, the characters before
172
Chapter 7. Common Dialog Boxes
this address is the directory name followed by a SPACE character. We can change SPACE to ‘\0’ character
to let the string ends by the directory name.
This method is only available for the current version of Windows, it may change in the future.
Whenever possible, we should use the first method to implement folder selection.
Notification CDN_SELCHANGE
File selection activities are sent to the dialog box through WM_NOTIFY message. This message is
primarily used by common controls to send notifications to the parent window. For example, in a file dialog
box, if the user has changed the file selection, a CDN_SELCHANGE notification will be sent. Since message
WM_NOTIFY is used for many purposes, after receiving it, we need to check LPARAM parameter and decide if
the notification is CDN_SELCHANGE or not. In CWnd derived classes, WM_NOTIFY message is handled by
function CWnd:OnNotify(…), we can override it to customize the default behavior of the file dialog box.
The following is the format of function CWnd::OnNotify(…):
To decide if this notification is CDN_SELCHANGE or not, first we need to cast lParam parameter to an
OFNOTIFY type pointer. The following is the format of structure OFNOTIFY:
From its member code, we can judge if the current notification is CDN_SELCHANGE or not.
173
Chapter 7. Common Dialog Boxes
Sample
Sample 7.4\CDB demonstrates how to add a file preview window to Explorer-style file dialog box. If
the user selects a file with “*.cpp” extension after the file dialog is activated, the contents of the file will be
displayed in the preview window before it is opened.
In the sample, a new command File Dialog Box | Custom File Dlg is added to the application, whose
command ID is ID_FILEDIALOGBOX_CUSTOMFILEDLG. Also, a WM_COMMAND message handler is added for this
command through using Class Wizard, whose correspondung member function is CCDBDoc::
OnFiledialogboxCustomfiledlg().
A new dialog template IDD_COMDLG32 is also added, it contains a static text control stc32, and an edit
box control IDC_EDIT (Figure 7-4). This edit box has “Disabled” and “Multiline” styles. This will
implement a read only edit box that can display multiple lines of text. A new class MCCusFileDialog is
derived from FileDialog. In this class, function OnNotity(…) is overridden.
The following is the definition of class MCCusFileDialog:
public:
MCCusFileDialog(BOOL bOpenFileDialog,
LPCTSTR lpszDefExt = NULL,
LPCTSTR lpszFileName = NULL,
DWORD dwFlags = OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
LPCTSTR lpszFilter = NULL,
CWnd* pParentWnd = NULL);
protected:
//{{AFX_MSG(MCCusFileDialog)
//}}AFX_MSG
virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult);
DECLARE_MESSAGE_MAP()
};
Stc32 IDC_EDIT
pofn=(LPOFNOTIFY)lParam;
if(pofn->hdr.code == CDN_SELCHANGE)
{
szStr=GetPathName();
if(GetFileExt().CompareNoCase("CPP") == 0)
{
if(file.Open(szStr, CFile::modeRead) == FALSE)
{
file.Abort();
return TRUE;
}
file.Read(szBuf, 255);
file.Close();
((CEdit *)GetDlgItem(IDC_EDIT))->SetWindowText(szBuf);
}
else ((CEdit *)GetDlgItem(IDC_EDIT))->SetWindowText(NULL);
174
Chapter 7. Common Dialog Boxes
return TRUE;
}
First we check if the notification is CDN_SELCHANGE, if so, we retrieve the path name and the file
extension by calling function CFileDialog::GetPathName() and CFileDialog::GetFileExt()
respectively. If the file extension is “*.cpp”, we call function CFile::Open(…) to open this file. When
making this call, we use flag CFile::modeRead to open it as a read only file. Then function
CFile::Read(…) is called to read the first 255 characters of the file into buffer szBuf. Finally, function
CWnd::SetWindowText(…) is called to display these characters.
Command File Dialog Box | Custom File Dlg is implemented as follows:
void CCDBDoc::OnFiledialogboxCustomfiledlg()
{
MCCusFileDialog dlg(TRUE);
dlg.m_ofn.lpTemplateName=MAKEINTRESOURCE(IDD_COMDLG32);
dlg.m_ofn.Flags|=OFN_ENABLETEMPLATE;
dlg.DoModal();
}
The file dialog box is implemented using class MCCusFileDialog. To use the custom dialog template,
OFN_ENABLETEMPLATE bit is set for member Flags, and the name of the custom dialog template is assigned
to member lpTemplateName of structure OPENFILENAME. Function CFileDialog::DoModal() is called as
usual.
If we want to modify the size and relative position of the preview window, we need to override
function OnInitDialog(), and call function CWnd::MoveWindow(…) there to resize the edit box.
Introduction
A color dialog box lets the user choose one or several colors from a pool of available colors. In
Windows, a valid color may be formed by combining red, green and blue colors with different intensities.
There are altogether 16,777,216 possible colors in the system.
In MFC, color dialog box is supported by class CColorDialog. To create a color dialog box, we need to
use class CColorDialog to declare a variable, then call CColorDialog::DoModal() to activate the dialog
box.
In the color dialog box, there are four ways to choose a color (Figure 7-5):
The selected color is shown in “Color | Solid” window. When a dialog box is implemented, there are
two things that we can customize: the original selected color and the custom colors. If we do not change
anything, the default selected color is black (RGB(0, 0, 0)). There are altogether 16 custom colors. By
default, they are all initialized to white (RGB(255, 255, 255)) at the beginning.
CColorDialog::CcolorDialog
175
Chapter 7. Common Dialog Boxes
(
COLORREF clrInit=0, DWORD dwFlags=0, CWnd *pParentWnd=NULL
);
Color matrix
Selected color
There are three parameters here, the first of which is the default selected color, the second is the flags
that can be used to customize the dialog box, and the third is a pointer to the parent window.
Class CColorDialog has a member m_cc, which is a CHOOSECOLOR structure:
typedef struct {
DWORD lStructSize;
HWND hwndOwner;
HWND hInstance;
COLORREF rgbResult;
COLORREF* lpCustColors;
DWORD Flags;
LPARAM lCustData;
LPCCHOOKPROC lpfnHook;
LPCTSTR lpTemplateName;
} CHOOSECOLOR;
We can change the custom colors by assigning the address of a COLORREF type array with size of 16 to
member lpCustColors of this structure. The colors in the array will be used to initialize the custom colors.
After the dialog box is closed, we can call function CColorDialog::GetColor() to retrieve the
selected color. Also, if we’ve initialized custom colors, we can obtain the updated values by accessing
member lpCustColors.
Sample
Sample 7.5\CDB demonstrates how to use color dialog box. First a function ColorDialog(…) is added
to class CCDBDoc, which will implement a color dialog box whose default selected color and custom colors
are customized. The following is its definition:
This function has two parameters, the first one specifies the initially selected color, and the second one
specifies the style flags. We will show how to use different style flags later, for the time being we just set
all bits to 0. The function is implemented as follows:
176
Chapter 7. Common Dialog Boxes
CString szStrRGB;
int i;
dlg.m_cc.Flags|=dwFlags;
for(i=0; i<16; i++)
{
color[i]=RGB
(
(i/4+1)*64-1,
((i/2)%2 == 0) ? 127:255,
(i%2 == 0) ? 127:255
);
}
dlg.m_cc.lpCustColors=color;
if(dlg.DoModal() == IDOK)
{
colorRlt=dlg.GetColor();
szStr.Format
(
"Color returned by GetColor:\n\tR=%d, G=%d, B=%d",
(int)GetRValue(colorRlt),
(int)GetGValue(colorRlt),
(int)GetBValue(colorRlt)
);
AfxMessageBox(szStr);
szStr.Empty();
szStr="Custom colors:";
for(i=0; i<16; i++)
{
szStrRGB.Empty();
szStrRGB.Format
(
"\n\tColor(%d) R=%d, G=%d, B=%d",
i,
(int)GetRValue(color[i]),
(int)GetGValue(color[i]),
(int)GetBValue(color[i])
);
szStr+=szStrRGB;
}
AfxMessageBox(szStr);
}
}
We first create a color dialog box and use colorInit to initialize the selected color. Like
OPENFILENAME, structure CHOOSECOLOR also has a Flags member that can be used to set the styles of color
dialog box. In the above function new flags contained in parameter dwFlags are added to the default flags
through bit-wise OR operation. Variable color is a COLORREF type array with a size of 16. We use a loop to
fill each of its elements with a different color and pass the address of the array to member lpCustColors of
structure CHOOSECOLOR. After calling CColorDialog::DoModal(), function CColorDialog::GetColor() is
called to retrieve the selected color, whose RGB values are displayed in a message box. Besides this, the
RGB values of custom colors are also displayed.
In the sample, a new sub-menu Color Dialog Box is added to IDR_MAINFRAME menu, and a command
Color Dialog Box | Initialize is created. The ID of this command is ID_COLORDIALOGBOX_INITIALIZE,
also, a WM_COMMAND message handler CCDBDoc::OnColordialogboxInitialize() is added through using
Class Wizard.
The following is the implementation of this command:
void CCDBDoc::OnColordialogboxInitialize()
{
ColorDialog(RGB(255, 0, 0));
}
The selected color is initialized to red, and no additional styles are specified.
Full Open
Now we are going to customize the styles of color dialog box. First, we can create a fully opened
dialog box by setting CC_FULLOPEN bit for member Flags of CHOOSECOLOR structure. Also, we can prevent
the dialog from being fully opened by setting CC_PREVENTFULLOPEN bit. In the sample, two menu
177
Chapter 7. Common Dialog Boxes
commands Color Dialog Box | Disable Full Open and Color Dialog Box | Full Open are added, in their
corresponding message handlers, the color dialog boxes with different styles are implemented:
void CCDBDoc::OnColordialogboxDisablefullopen()
{
ColorDialog(RGB(0, 255, 0), CC_PREVENTFULLOPEN);
}
void CCDBDoc::OnColordialogboxFullopen()
{
ColorDialog(RGB(0, 255, 0), CC_FULLOPEN);
}
Now we can compile the application and try color dialog boxes with different styles.
COLOR_LUMSCROLL
COLOR_BOX1 COLOR_RAINBOW
COLOR_HUE
COLOR_SAT
COLOR_CUSTOM1 COLOR_LUM
COLOR_CURRENT
COLOR_RED
COLOR_GREEN
COLOR_MIX COLOR_BLUE
COLOR_ADD
178
Chapter 7. Common Dialog Boxes
702 Static COLOR_LUMSCROLL Displays possible amounts of white and black in color
703 Edit COLOR_HUE Specifies the hue of the selected color
704 Edit COLOR_SAT Specifies the saturation of the selected color
705 Edit COLOR_LUM Specifies the luminosity of the selected color
706 Edit COLOR_RED Specifies the amount of red in the selected color
707 Edit COLOR_GREEN Specifies the amount of green in the selected color
708 Edit COLOR_BLUE Specifies the amount of blue in the selected color
709 Static COLOR_CURRENT Displays the selected color
710 Static COLOR_RAINBOW Displays the color matrix
712 Push button COLOR_ADD Add the selected color to custom color
719 Push button COLOR_MIX Open the color box fully
720 Static COLOR_BOX1 Displays basic colors
721 Static COLOR_CUSTOM1 Displays custom colors
1 Default push IDOK OK button
button
2 Push button IDCANCEL Cancel button
1038 Push button 1038 Help button
The standard dialog template can be copied from file “Commdlg.dll”. By default, all the controls in
this template will have a numerical ID. In order to make them easy to use, we can assign each ID a symbol,
this can be done by inputting a text ID and assigning the control’s original ID value to it in “ID” window of
the property sheet that is used for customizing the styles of a control. For example, when we open “Text
Properties” property sheet for control 720, its “ID” window shows “720”. We can change it to
“COLOR_BOX1=720”. By doing this, the control will have an ID symbol “COLOR_BOX1”, whose value is
720. In the sample, most of the controls are assigned ID symbols.
We can hide certain unnecessary controls by calling function CWnd::ShowWindow(…) in dialog box’s
initialization stage. However, there is an easier approach to it: we can resize the dialog template and move
the unwanted controls outside the template (Figure 7-7). By doing this, these controls will not be shown in
the dialog box, and therefore, can not respond to mouse clicking events.
These controls
are move outside
the dialog
template
However, in Developer Studio, a control is confined within the dialog template and is not allowed to
be moved outside it. A workaround for this is to edit the resource file directly. Actually, a dialog template
is based on a text format resource file. In our sample, all type of resources are stored in file “CDB.rc”. By
opening it in “Text” mode, we can find the session describing the color dialog template:
179
Chapter 7. Common Dialog Boxes
59,14,85,86
……
END
The first four lines specify the properties of this dialog template, which include its dimension, styles,
caption, and font. Between “BEGIN” and “END” keywords, all controls included in the template are listed.
Each item is defined with a type description followed by a series of styles. In the above example, the first
item is a static text control (LTEXT), which contains text “Basic Colors” ("&Basic Colors:"). It has an ID
of 65535 (-1), located at (4, 4), and its dimension is (140, 9).
In the above example, if we change a control’s horizontal coordinate to a value bigger than 150
(Because the dimension of the dialog template is 150×124), it will be moved outside the template.
In the sample, two such dialog templates are prepared: “CHOOSECOLOR” and “CHOOSECUSCOLOR”. In
“CHOOSECOLOR”, static text control COLOR_BOX1 is inside the template, and the area within this control will
be used to draw base colors. For “CHOOSECUSCOLOR”, static text control COLOR_CUSTOM1 is inside the
template, the area within this control will be used to draw custom colors (COLOR_BOX1 and COLOR_CUSTOM1
are used to define an area where the controls will be created dynamically for displaying colors). In both
cases, frame COLOR_CURRENT is inside the template, which will be used to display the current selected color.
Commands Implementation
Function CCDBDoc::OnColordialogboxChoosebasecolor() is implemented as follows:
void CCDBDoc::OnColordialogboxChoosebasecolor()
{
CColorDialog dlg;
dlg.m_cc.Flags|=CC_ENABLETEMPLATE;
dlg.m_cc.Flags|=CC_FULLOPEN;
dlg.m_cc.hInstance=(HWND)AfxGetInstanceHandle();
dlg.m_cc.lpTemplateName="CHOOSECOLOR";
dlg.DoModal();
}
There is nothing special for this function. The only thing we need to pay attention to is that we must set
CC_FULLOPEN flag in order to display currently selected color. Otherwise control COLOR_CURRENT will not
work.
void CCDBDoc::OnColordialogboxChoosecostumcolor()
{
CColorDialog dlg;
int i;
COLORREF color[16];
We must provide custom colors. Otherwise they will all be initialized to white. With a color dialog box
whose color matrix window can not be used, the custom colors provide the only way to let user pick up a
color.
180
Chapter 7. Common Dialog Boxes
Basics
Font dialog box lets user select a special font with different style combinations: boldface, italic,
strikeout or underline. The font size and color can also be set in the dialog box.. In MFC, the class that is
used to implement font dialog box is CFontDialog. To create a standard font dialog box, all we need to do
is declaring a CFontDialog type variable and using it to call function CFontDialog::DoModal(). Like
classes CFileDialog and CColorDialog, class CFontDialog also contains a member variable that allows us
to customize the default styles of color dialog box. This variable is m_cf, which is declared by structure
CHOOSEFONT:
typedef struct {
DWORD lStructSize;
HWND hwndOwner;
HDC hDC;
LPLOGFONT lpLogFont;
INT iPointSize;
DWORD Flags;
DWORD rgbColors;
LPARAM lCustData;
LPCFHOOKPROC lpfnHook;
LPCTSTR lpTemplateName;
HINSTANCE hInstance;
LPTSTR lpszStyle;
WORD nFontType;
WORD ___MISSING_ALIGNMENT__;
INT nSizeMin;
INT nSizeMax;
} CHOOSEFONT;
There are several things that can be customized here. First, we can change the default font size range.
By default, a valid size for all fonts is ranged from 8 to 72. We can set a narrower range by setting
CF_LIMITSIZE bit of member Flags and assigning lower and higher boundaries to members nSizeMin and
nSizeMax respectively. We can also specify font’s initial color by setting CF_EFFECTS bit of member Flags
and assigning a COLORREF value to member rgbColors (the initial color must be one of the standard colors
defined in the font dialog box such as red, green, cyan, black...).
Structure LOGFONT
Also, we can specify an initially selected font (with specified size and styles) by assigning a LOGFONT
type pointer to member lpLogFont. Here, structure LOGFONT is used to describe a font:
A font has many properties. The most important ones are face name, height, and font styles (italic,
bolded, underlined or strikeout). In structure LOGFONT, these styles are represented by the following
members: lfFaceName, lfWeight, lfItalic, lfUnderline and lfStrikeOut. A face name is the name of
the font, it distinguishes one font from another. Every font has its own face name, such as “Arial”,
“System” and “MS Serif”. The weight of a font specifies how font is emphasized, its range is from 0 to
181
Chapter 7. Common Dialog Boxes
1000. Usually we use predefined values such as FW_BOLD (font is bolded) and FW_NORMAL (font is not
bolded) for convenience. Member lfItalic, lfUnderline and lfStrikeOut are all Boolean type, by
setting them to TRUE, we can let the font to become italic, underlined, or strikeout. Member nSizeMin and
nSizeMax can be used to restrict the size of a font.
In order to use LOGFONT object to initialize a font dialog box, we must first set
CF_INITTOLOGFONTSTRUCT bit for member Flags of structure CHOOSEFONT, then assign the LOGFONT type
pointer to member lpLogFont.
Sample
Sample 7.7\CDB demonstrates how to use font dialog box. It is based on sample 7.6\CDB with a new
command Font Dialog Box | Initialize added to the application. The ID of this command is
ID_FONTDIALOGBOX_INITIALIZE, and its WM_COMMAND message handler is CCDBDoc::
OnFontdialogboxInitialize(). The command is implement as follows:
void CCDBDoc::OnFontdialogboxInitialize()
{
LOGFONT lf;
CFontDialog dlg;
CString szStr;
COLORREF color;
memset(&lf, 0, sizeof(LOGFONT));
lf.lfItalic=TRUE;
lf.lfUnderline=TRUE;
lf.lfStrikeOut=TRUE;
lf.lfWeight=FW_BOLD;
strcpy(lf.lfFaceName, "Times New Roman");
The initially selected font is “Times New Roman”, whose color is yellow (RGB(255, 255, 0)), and
has the following styles: italic, underlined, strikeout, bolded. The range of the font size is restricted
between 20 and 48. After the user clicks button “OK”, the properties of the selected font are retrieved
through member functions of class CFontDialog, and are displayed in a message box.
182
Chapter 7. Common Dialog Boxes
box. 2) Set CF_ENABLETEMPLATE bit for member Flags of structure CHOOSEFONT, and assign custom template
name to member lpTemplateName. 3) Assign application’s instance handle to member hInstance.
The standard dialog template can be found in file “Commdlg.dll”. It can be opened from Developer
Studio in “Resources” mode.
All IDs of the controls are numbers. When writing code to access the controls, we can use these
numbers directly, or we can assign each control a text ID like what we did for sample 7.6\CDB. Actually,
these IDs are all pre-defined, whose symbolic IDs can be found in file “Dlgs.h” (we can find this file under
~DevStudio\VC\Include\ directory). We can check a control’s ID value and search through this file to find
out its symbolic ID. For example, in font dialog box, the combo box under window “Font:” has an ID of
1136 (0x470). In file “Dlgs.h”, we can find:
……
//
// Combo boxes.
//
#define cmb1 0x0470
#define cmb2 0x0471
#define cmb3 0x0472
……
Value 0x0470 is used by cmb1, so we can access this combo box through using symbol cmb1.
Sample 7.8\CDB demonstrates how to use custom dialog template to implement font dialog box. It is
based on sample 7.7\CDB. In the sample, a new command Font Dialog Box | Customize is added to the
application, whose command ID is ID_FONTDIALOGBOX_INITIALIZE. If we execute this command, a font
dialog box whose color selection feature is disabled will be invoked.
In order to implement this dialog box, we need to disable the following two controls: stc4 (Static
control containing string “Color”, whose ID value is 1091) and cmb4 (Combo box for color selection,
whose ID value is 1139).
In the sample, the custom dialog template is IDD_FONT. It contains all the standard controls.
A new class MCFontClass is added to the application through using Class Wizard, its base class is
CFontClass. In the derived class, function OnInitDialog is overridden, within which the above two
controls are disabled:
BOOL MCFontDialog::OnInitDialog()
{
CFontDialog::OnInitDialog();
GetDlgItem(stc4)->ShowWindow(SW_HIDE);
GetDlgItem(cmb4)->ShowWindow(SW_HIDE);
return TRUE;
}
void CCDBDoc::OnFontdialogboxCustomize()
{
MCFontDialog dlg;
dlg.m_cf.lpTemplateName=MAKEINTRESOURCE(IDD_FONT);
dlg.m_cf.Flags|=CF_ENABLETEMPLATE;
if(dlg.DoModal() == IDOK)
{
}
}
With the above implementation, the font dialog box will not contain color selection feature.
183
Chapter 7. Common Dialog Boxes
Tricks
It is difficult to implement modeless common dialog boxes. This is because all the common dialog
boxes are designed to work in the modal style. Therefore, if we call function Create(…) instead of
DoModal(), although the dialog box will pop up, it will not behave like a common dialog box. This is
because function Create(…) is not overridden in a class such as CColorDialog, CFontDialog.
We need to play some tricks in order to implement modeless common dialog boxes. By looking at the
source code of MFC, we can find that within function CColorDialog::DoModal() or CFontDialog::
DoModal(), the base class version of DoModal() is not called. Instead, API functions ::ChooseColor(…)and
::ChooseFont(…) are used to implement common dialog boxes.
There is no difference between the common dialog boxes implemented by API functions and MFC
classes. Actually, using API function is fairly simple. For example, if we want to implement a color dialog
box, we can first prepare a CHOOSECOLOR object, then use it to call function ::ChooseColor(…). But here we
must initialize every member of CHOOSECOLOR in order to let the dialog box have appropriate styles.
By using this method we can still create only modal common dialog boxes. A “modeless” common
dialog box can be implemented by using the following method:
1) Before creating the common dialog box, first implement a non-visible modeless window.
1) Create the modal common dialog box, use the modeless window as its parent.
Although the common dialog box is modal, its parent window is modeless. So actually we can switch
away from the common dialog box (and its parent window) without closing it. Because the common dialog
box’s parent is invisible, this gives the user an impression that the common dialog box is modeless.
But if we call function DoModal() to implement the common dialog box, we are not allowed to switch
away even it has a modeless parent window. We must call API functions to create this type of common
dialog boxes.
Hook Function
We can provide hook function when implementing common dialog boxes using API functions. In
Windows programming, a hook function is used to intercept messages sent to a window, thus by handling
these messages we can customize a window’s behavior. Both structure CHOOSEFONT and CHOOSECOLOR have
a member lpfnHook that can be used to store a hook function’s address when the common dialog box is
being implemented. If a valid hook function is provided, when a message is sent to the dialog box, the hook
function will be called first to process the message. To enable hook function, we also need to set
CF_ENABLEHOOK or CC_ENABLEHOOK bit for member Flags of structure CHOOSEFONT or CHOOSECOLOR. If the
message is not processed in the hook function, the dialog’s default behavior will not change.
A hook procedure usually looks like the following:
UINT APIENTRY HookProc(HWND hdlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_INITDIALOG:
{
……
break;
}
}
……
}
The first parameter is the handle of the destination window where the message is being sent. The
second parameter specifies the type of message. The third and fourth parameters are WPARAM and LPARAM
parameters of the message respectively. For different events, the messages sent to this procedure are
184
Chapter 7. Common Dialog Boxes
different. For example, in the dialog’s initialization stage, message WM_INITDIALOG will be sent, if we want
to add other initialization code, we could implement it under “case WM_INITDIALOG” statement.
In MFC, this procedure is encapsulated by message mapping. We don’t have to write hook procedure,
because we can map any message to a specific member function. For example, in MFC, message
WM_INITDIALOG is always mapped to function OnInitDialog(), so we can always do our initialization work
within this member function.
When we use MFC classes to implement common dialog boxes, there is a hook function
_AfxCommDlgProc(…) behind us. We never need to know its existence. However, we use it indirectly
whenever a common dialog box is created. By looking at MFC source code, we will find that in the
constructors of common dialog boxes, the address of this function is assigned to member lpfnHook.
To make full use of MFC resource, instead of creating a new hook function, we can use
_AfxCommDlgProc(…) when calling API functions to implement common dialog boxes.
So long as we have a valid window handle, we can attach it to a MFC class declared variable.
Obtaining Handle
When we call function ::ChooseColor(…) or ::Choosefont(…), no window handle will be returned.
The only place we can obtain dialog’s handle is in the hook function (through parameter hdlg). We can
attach this handle to a CColorDialog or CFontDialog declared variable after receiving message
WM_INITDIALOG.
UINT CALLBACK CommonHook(HWND hDlg, UINT iMsg, UINT wParam, LONG lParam)
{
if(iMsg == WM_INITDIALOG)
{
CWnd *pDlg;
pDlg=(CWnd *)((LPCHOOSEFONT)lParam)->lCustData;
pDlg->Attach(hDlg);
}
185
Chapter 7. Common Dialog Boxes
The code listed above shows how to trap WM_INITDIALOG message and attach the window handle to a
variable declared outside the hook function. Also, the default hook procedure is called to let other messages
be processed normally. Here, lpFontfn is a global pointer that stores the address of the hook procedure.
Sample Implementation
Sample 7.9\CDB demonstrates how to implement modeless common dialog boxes. It is based on
sampele 7.8\CDB, with two new commands added to the application: Color Dialog Box | Modeless and
Font Dialog Box | Modeless, both of which can be used to invoke modeless common dialog boxs. For the
former command, the user can select a color and switch back to the main window to see the effect without
dismissing the dialog box. The IDs of the two commands are ID_COLORDIALOGBOX_MODELESS and
ID_FONTDIALOGBOX_MODELESS, and their WM_COMMAND message handlers are CCDBDoc::
OnColordialogboxModeless() and CCDBDoc::OnFontdialogboxModeless() respectively.
A dummy dialog box is added to the application, whose resource ID is IDD_DIALOG_DUMMY. It will be
used as the parent window of the common dialog boxes. Since this window is always hidden after being
invoked, it does not matter what controls are included in the dialog template. The class that will be used to
implement this dialog box is MCDummyDlg.
Two new variables are declared in class CCDBDoc for implementing modeless color dialog box:
MCDummyDlg *m_pColorDmDlg;
CColorDialog *m_pColorDlg;
……
}
Here, m_pColorDmDlg will be used to create dummy window, and m_pColorDlg will be used to create
color dialog box.
At the beginning of file “CDBDoc. Cpp”, a global hook procedure and a pointer are declared:
UINT CALLBACK ColorHook(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
UINT (CALLBACK *lpColorfn)(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
Function ColorHook is the custom hook procedure, and pointer lpColorfn will be used to store the
address of the default hook procedure.
In function CCDBDoc::OnColordialogboxModeless(), first we need to initialize m_pColorDlg and
m_pColorDmDlg, then create the dummy window:
void CCDBDoc::OnColordialogboxModeless()
{
m_pColorDlg=new MCMLColorDlg();
m_pColorDmDlg=new MCDummyDlg();
m_pColorDmDlg->Create(IDD_DIALOG_DUMMY);
m_pColorDmDlg->ShowWindow(SW_HIDE);
……
Function CDialog::Create() is called to create a modeless dialog box, and function CWnd::
ShowWindow(…) (using parameter SW_HIDE) is called to hide this window.
Before the color dialog box is created, we must make some changes to structure CHOOSECOLOR:
……
m_pColorDlg->m_cc.lpCustColors=rgbColors;
m_pColorDlg->m_cc.lCustData=(LONG)m_pColorDlg;
m_pColorDlg->m_cc.Flags|=CC_ENABLETEMPLATE | CC_FULLOPEN;
m_pColorDlg->m_cc.hwndOwner=m_pColorDmDlg->GetSafeHwnd();
m_pColorDlg->m_cc.lpTemplateName="CHOOSECUSCOLOR";
m_pColorDlg->m_cc.hInstance=(HWND)AfxGetInstanceHandle();
……
186
Chapter 7. Common Dialog Boxes
The address of m_pColorDlg is stored as custom data, which will be sent to the hook function. The
dummy window is designated as the parent window of the color dialog box and its handle is assigned to
member hwndOwner of structure CHOOSECOLOR. A global COLORREF type array rgbColors is declared, which
will be used to initialize the custom colors in the color dialog box. Also, custom dialog template
"CHOOSECUSCOLOR" is used, which will allow the user to choose color from only custom colors.
The address of default hook procedure (which is contained in member lpfnHook of structure
CHOOSECOLOR after the constructor of class CColorDlg is called) is stored in global variable lpColorfn, and
the new hook procedure address is assigned to lpfnHook. Finally, API function ::ChooseColor(…) is called
to invoke the color dialog box:
……
lpColorfn=m_pColorDlg->m_cc.lpfnHook;
m_pColorDlg->m_cc.lpfnHook=ColorHook;
::ChooseColor(&m_pColorDlg->m_cc);
}
In the hook procedure, after receiving message WM_INITDIALOG, we can obtain the value of
m_pColorDlg from LPARAM parameter and attach the color dialog box’s window handle to it:
UINT CALLBACK ColorHook(HWND hDlg, UINT iMsg, UINT wParam, LONG lParam)
{
switch(iMsg)
{
case WM_INITDIALOG:
{
CColorDialog *pDlg;
pDlg=(CColorDialog *)((LPCHOOSECOLOR)lParam)->lCustData;
pDlg->Attach(hDlg);
break;
}
}
return lpColorfn(hDlg, iMsg, wParam, lParam);
}
However, there are still some problems left to be solved. Since the dialog box is modeless now, we can
execute command Color Dialog Box | Modeless again when the dialog box is being used. Also, in the new
situation, the user is able to exit the application without closing the dialog box first.
To avoid the dummy dialog box and the color dialog box from being created again while they are
active, we have to check m_pColorDlg and m_pColorDmDlg variables. First, if they are NULL, it means the
variables have not been initialized, we need to allocate buffers and create the window. If they are not
NULL, there are two possibilities: 1) The dialog box is currently active. 2) The dialog box is closed.
Obviously we don’t need to do anything for the first case. For the second case, we need to reinitialize the
two variables and create the window again. Since the window handle is attached to the variable in the hook
procedure, we need to detach it before releasing the allocated buffers. For the above reasons, the following
is added to the beginning of function CCDBDoc::OnColordialogboxModeless():
void CCDBDoc::OnColordialogboxModeless()
{
if(m_pColorDlg != NULL)
{
if(::IsWindow(m_pColorDlg->GetSafeHwnd()) == TRUE)return;
if(m_pColorDlg->GetSafeHwnd() != NULL)m_pColorDlg->Detach();
delete m_pColorDlg;
if(m_pColorDmDlg != NULL)delete m_pColorDmDlg;
m_pColorDmDlg=NULL;
m_pColorDlg=NULL;
}
……
}
We need to destroy the windows by ourselves if the user exits the application without first closing the
color dialog box. We can override a member function OnCloseDocument(), which will be called when the
document is about to be closed. This function can be easily added through using the Class Wizard. The
following shows how it is implemented in the sample:
187
Chapter 7. Common Dialog Boxes
void CCDBDoc::OnCloseDocument()
{
if(m_pColorDlg != NULL)
{
if(m_pColorDlg->m_hWnd != NULL)m_pColorDlg->Detach();
delete m_pColorDlg;
}
if(m_pColorDmDlg != NULL)delete m_pColorDmDlg;
m_pColorDmDlg=NULL;
m_pColorDlg=NULL;
CDocument::OnCloseDocument();
}
CCDBDoc::CCDBDoc()
{
……
m_colorCur=RGB(0, 255, 255);
}
Here we check if the new color is the same with the old color, if not, we update member m_colorCur,
and repaint the client window by calling function CDocument::UpdateAllViews(…).
When initializing the color dialog box, we need to use m_colorCur to set the initially selected color
before the color dialog box is created. The following change is made for this purpose:
188
Chapter 7. Common Dialog Boxes
Before change:
void CCDBDoc::OnColordialogboxModeless()
{
……
m_pColorDlg=new CColorDialog();
……
}
After change:
void CCDBDoc::OnColordialogboxModeless()
{
……
m_pColorDlg=new CColorDialog(m_colorCur);
……
}
Function CCDBView::OnDraw(…) is modified to paint the client window with color CCDBDoc::
m_colorCur:
CCDBDoc *pDoc=GetDocument();
ASSERT_VALID(pDoc);
GetClientRect(rect);
color=pDoc->GetCurrentColor();
pDC->FillSolidRect(rect, color);
}
First we find out the size of the client window, then call function CCDBDoc::GetCurrentColor() to
retrieve the current color, and call function CDC::FillSolidRect() to fill the window with this color.
When the user selects a new color, we need to call function CCDBDoc::SetCurrentColor(…) to update
the current color. In order to do this, we need to trap message WM_LBUTTONUP in the hook procedure, obtain
the selected color and update variable CCDBDoc::m_colorCur. For this purpose: the following is added to
function ColorHook(…):
UINT CALLBACK ColorHook(HWND hDlg, UINT iMsg, UINT wParam, LONG lParam)
{
switch(iMsg)
{
……
case WM_LBUTTONUP:
{
CColorDialog *pDlg;
CCDBDoc *pDoc;
pDoc=(CCDBDoc *)((CFrameWnd *)AfxGetMainWnd())->GetActiveDocument();
ASSERT(pDoc != NULL);
pDlg=(CColorDialog *)CWnd::FromHandle(hDlg);
ASSERT(pDlg != NULL);
pDoc->SetCurrentColor
(
RGB
(
pDlg->GetDlgItemInt(COLOR_RED, NULL, FALSE),
pDlg->GetDlgItemInt(COLOR_GREEN, NULL, FALSE),
pDlg->GetDlgItemInt(COLOR_BLUE, NULL, FALSE),
)
);
break;
}
……
}
Since ColorHook(…) is not a member function of class CCDBDoc, we can not access its member function
directly from the hook procedure. So here AfxGetMainWnd() is called first to obtain the mainframe window,
189
Chapter 7. Common Dialog Boxes
then CFrameWnd::GetActiveDocument() is called to obtain the active document attached to it. This method
can also be used to access the active document from other classes.
When calling function CCDBDoc::SetCurrentColor(…), we use IDs COLOR_RED, COLOR_GREEN and
COLOR_BLUE to retrieve the current values contained in the edit boxes (see Figure 7-6). In a standard color
dialog box, these edit boxes will be shown only when the dialog box is fully opened. Although this is not
the case in the sample, we still can retrieve the contents of them even they can not be seen. Also, we use
CDialog::GetDlgItemInt(…) to convert characters to integers when retrieving the color values.
In the sample, modeless font dialog is implemented in a similar way.
Summary:
1) Three classes that can be used to implement common file dialog box, common color dialog box and
common font dialog box are CFileDialog, CColorDialog and CFontDialog respectively. To
implement a standard common dialog box, we need to use the corresponding class to initialize a
variable, then call function DoModal() to invoke the dialog box.
1) We can customize the default behavior of common dialog boxes by modifying the members of
structure OPENFILENAME, CHOOSECOLOR or CHOOSEFONT.
1) File dialog box can be implemented either in an “Explorer” style or an “Old” style.
1) There are some shell functions that can be called to implement folder selection dialog box. If we want
to implement the old-style interface, we must use custom dialog template and override class
CFileDialog.
1) To use custom dialog template, we need to first design a dialog template that contains all the standard
controls, then set “enable template” flag and assign the template name to member lpTemplateName.
1) To add extra controls to an “Explorer” style file dialog box, we need to design a dialog template that
contains static control with ID of stc32. We do not need to replicate all the standard controls.
1) MFC classes do not support modeless common dialog boxes. To implement this type of dialog boxes,
we need to create a modeless parent window for the common dialog box and hide the parent window
all the time. This will give the user an impression that the common dialog box is modeless. Also, we
need to call API functions instead of MFC member functions to create the common dialog box.
190
Chapter 8. DC, Pen, Brush and Palette
Chapter 8
Situation
GDI is a standard interface between the programmer and physical devices. It provides many functions
that can be used to output various objects to the hardware (e.g. a display or a printer). GDI is very
important because, as a programmer, we may want our applications to be compatible with as many
peripherals as possible. For example, almost every application need to write to display, and many
applications also support printer output. The problem here is that since a program should be able to run on
different types of devices (low resolution displays, high resolution displays with different color depth, etc.),
it is impossible to let the programmer know the details of every device and write code to support it
beforehand.
The solution is to introduce GDI between the hardware and the programmer. Because it is a standard
interface, the programmer doesn’t have to have any knowledge on the hardware in order to operate it. As
long as the hardware supports standard GDI, the application should be able to execute correctly.
Device Context
As a programmer, we do not output directly to hardware such as display or printer. Instead, we output
to an object that will further realize our intention. This object is called device context (DC), it is a
Windows object that contains the detailed information about hardware. When we call a standard GDI
function, the DC implements it according to hardware attributes and configuration.
Suppose we want to put a pixel at specific logical coordinates on the display. If we do not have GDI,
we need the following information of the display in order to implement this simple operation:
1) Video memory configuration. We need this information in order to convert logical coordinates to
physical buffer address.
1) Device type. If the device is a palette device, we need to convert a RGB combination to an index to the
color table and use it to specify a color. If the device is a non-palette device, we can use the RGB
combination directly to specify a color.
Because the actual devices are different form one type to another, it is impossible for us to gather
enough information to support all the devices in the world. So instead of handling it by the programmer,
GDI functions let us use logical coordinates and RGB color directly, the conversion will be implemented
by the device driver.
191
Chapter 8. DC, Pen, Brush and Palette
GDI Objects
In Windows, GDI objects are tools that can be used together with device context to perform various
drawings. They are designed for the convenience of programmers. The following is a list of some
commonly used GDI objects:
GDI Usage
Object
Pen Used to draw line, curve, the border of a rectangle, ellipse, polygon, etc.
Brush Used to fill the interior of a rectangle, ellipse, polygon with a specified pattern.
Font Used to manage a variety of fonts that can be used to output text.
Palette Used to manage colors on a palette device.
Bitmap Used to manage image creating, drawing, manipulating, etc.
Region Used to manage an irregular shape area that can confine the drawing within a specified
region.
The above GDI objects, along with device context, are all managed through handles. We can use the
handle of an object to identify or access it. Besides the handles, every GDI object has a corresponding MFC
class. The following is a list of their handle types and classes:
Obtaining DC
As a programmer, most of the time we need to output to a specific window rather than the whole
screen. A DC can be obtained from any window in the system, and can be used to call GDI functions.
There are many ways to obtain DC from a window, the following is an incomplete list:
1) Call function CWnd::GetDC(). This function will return a CDC type pointer that can be used to perform
drawing operations within the window.
1) Declare CClientDC type variable and pass a CWnd type pointer to its constructor. Class CClientDC is
designed to perform drawing operations in the client area of a window.
1) Declare CWndowDC type variable and pass a CWnd type pointer to its constructor. Class CWindowDC is
designed to perform drawing operations in the whole window (including client area and non-client
area).
1) In MFC, certain member functions are designed to update application’s interface (i.e. CView::
OnDraw(…)). These functions will automatically be called when a window needs to be updated. For this
kind of functions, the device context will be passed through one of function’s parameters.
1) If we know all the information, we can create a DC by ourselves.
1) Obtain or create a DC that can be used to perform drawing operations on the target window.
1) Create or obtain an appropriate GDI (pen, brush, font…) object.
192
Chapter 8. DC, Pen, Brush and Palette
1) Select the GDI object into the DC, use a pointer to store the old GDI object.
1) Perform drawing operations.
1) Select the old GDI object into the DC, this will select the new GDI object out of the DC.
1) Destroy the GDI object if necessary (If the GDI object was created in step 2 and will not be used by
other DCs from now on).
The following sections will discuss how to use specific GDI objects to draw various kind of graphic
objects.
8.1 Line
Creating Pen
Sample 8.1\GDI demonstrates how to create a pen and use it to draw lines. The sample is a standard
SDI application generated from Application Wizard.
To draw a line, we need the following information: starting and ending points, width of the line,
pattern of the line, and color. There are several types of pens that can be created: solid pen, dotted pen,
dashed pen. Besides drawing patterns, each pen can have a different color and different width. So if we
want to draw two types of lines, we need to create two different pens.
In MFC, pen is supported by class CPen, it has a member function CPen::CreatePen(…), which can be
used to create a pen. This function has several versions, the following is one of them:
Parameter nPenStyle specifies the pen style, it can be any of the following: PS_SOLID, PS_DASH,
PS_DOT, PS_DASHDOT, PS_DASHDOTDOT, PS_NULL, etc. The meanings of these styles are self-explanatory. The
second parameter nWidth specifies width of the pen. Please note that if we create pen with a style other than
PS_SOLID, this width can not exceed 1 device unit. Parameter crColor specifies color of the pen, which can
be specified using RGB macro.
In MFC’s document/view structure, the data should be stored in CDocument derived class and the
drawing should be carried out in CView derived class (for SDI or MDI applications). Since CView is derived
from CWnd, we can obtain its DC by either calling function CWnd::GetDC() or declare a CClientDC type
variable using the window’s pointer. To select a GDI object into the DC, we need to call function
CDC::SelectObject(…). This function returns a pointer to a GDI object of the same type that is being
selected by the DC. Before we delete the GDI object, we need to use this pointer to select the old GDI
object into the DC so that the new DC will be selected out of the DC.
We can not delete a GDI object when it is being selected by a DC. If we do so, the application will
become abnormal, and may cause GPF (General protection fault) error.
Drawing Mode
Besides drawing lines, sample 8.1\GDI also demonstrates how to implement an interactive
environment that lets the user use mouse to draw lines anywhere within the client window. In the sample,
the user can start drawing by clicking mouse’s left button, and dragging the mouse with left button held
down, releasing the button to finish the drawing. When the user is dragging the mouse, dotted outlines will
be drawn temporarily on the window. After the ending point is decided, the line will be actually drawn
(with red color and a width of 1). In order to implement this, the following messages are handled in the
application: WM_LBUTTONDOWN, WM_MOUSEMOVE and WM_LBUTTONUP.
Before the left mouse button is released, we have to erase the old outline before drawing a new one in
order to give the user an impression that the outline “moves” with the mouse cursor. The best way to
implement this type of operations is using XOR drawing mode. With this method, we can simply draw an
object twice in order to remove it from the device.
XOR bit-wise operation is very powerful, we can use it to generate many special drawing effects.
Remember when we perform a drawing operation, what actually happens in the hardware level is that data
193
Chapter 8. DC, Pen, Brush and Palette
is filled into the memory. The original data contained in the memory indicates the original color of pixels
on the screen (if we are dealing with a display device). The new data can be filled with various modes: it
can be directly copied into the memory; it can be first combined with the data contained in the memory,
then the combining result is copied into the memory. In the latter case, the combination could be bit-wise
AND, OR, XOR, or simply a NOT operation on the original data. So by applying different drawing modes,
the output color doesn’t have to be the color of the pen selected by the DC. It could be either the
combination of the two (original color and the pen color), or it could be the complement of the original
color.
If we draw an object twice using XOR operation mode, the output color will be the original color on
the target device. This can be demonstrated by the following equation:
A^B^B=A^(B^B)=A^0=A
Here A is the original color, and B is the new color. The above equation is valid because XORing any
number with itself results in 0, and XORing a number with 0 does not change this number.
When using a pen, we can select various drawing modes. The following table lists some modes that
can be used for drawing objects with a pen (In the table, P represents pen color, O represents original color
on the target device, B represents black color, W represents white color, and the following symbols
represent bit-wise NOT, AND, OR and XOR operations respectively: ~, &, |, ^):
To set the drawing mode, we need to call function CDC::SetROP2(…), which has the following format:
The function has only one parameter, which specifies the new drawing mode. It could be any of the
modes listed in the above table.
194
Chapter 8. DC, Pen, Brush and Palette
Storing Data
When the user finishes drawing a line, the starting and ending points will be stored in the document. At
this time, instead of drawing the new line directly to the window, we need to update the client window and
let the function CView::OnDraw(…) be called. In this function, all the lines added previously will be drawn
again, so the client will always become up-to-date.
We must override function CView::OnDraw(…) to implement client window drawing for an application.
This is because the window update may happen at any time. For example, when the application is restored
from the icon state, the whole portion of the window will be painted again. The system will draw the non-
client area such as caption bar and borders, and it is the application’s responsibility to implement client area
drawing, which should be carried out in function CView::OnDraw(…) In MFC, if the application does not
implement this, the client window will be simply painted with the default color. As a programmer, it is
important for us to remember that when we output something to the window, it will not be kept there
forever. We must redraw the output whenever the window is being updated.
This forces us to store every line that has been drawn by the user in document, and redraw all of them
when necessary. This is the basic document/view structure of MFC: storing data in CDocument derived
class, and representing it in function CView::OnDraw(…).
In the sample, the lines are stored in an array declared in the document class CGDIDoc. Also, some new
functions are declared to let the data be accessible from the view:
A CPtrArray type variable m_paLines is declared for storing lines. Class CPtrArray allows us to add
and delete pointer type objects dynamically. This class encapsulates memory allocation and release, so we
do not have to worry about memory management when adding new elements. Three new public member
functions are also declared, among them CGDIDoc::AddLine(…) can be used to add a new line to
m_paLines, CGDIDoc::GetLine(…) can be used to retrieve a specified line from m_paLines, and CGDIDoc::
GetNumOfLines() can be used to retrieve the total number of lines stored in m_paLines. Here lines are
stored in CRect type variables, usually this class is used to store rectangles. In the sample, the rectangle’s
upper-left and bottom-right points are used to represent a line.
Although class CPtrArray can manage the memory allocation and release for storing pointers, it does
not release the memory for the stored objects. So in class CGDIDoc’s destructor, we need to delete all the
objects stored in the array as follows:
CGDIDoc::~CGDIDoc()
{
while(m_plLines.GetSize())
{
delete m_plLines.GetAt(0);
m_pLines.RemoveAt(0);
}
}
Here we just delete the first object and remove it from the array repeatedly until the size of the array
becomes 0.
In CView derived class, we can call function CView::GetDocument() to obtain a pointer to the
document and use it to access all the public variables and functions declared there.
195
Chapter 8. DC, Pen, Brush and Palette
Variables m_ptStart and m_ptEnd are used to record the starting and ending points of a line,
m_bCapture indicates if the window has the capture. Variable m_bNeedErase indicates if there is an old
outline that needs to be erased. After the user presses down the left button, the initial mouse position will be
stored in variable m_ptStart. Then as the user moves the mouse with the left button held down, m_ptEnd
will be used to store the updated mouse position. A new line is defined by both m_ptStart and m_ptEnd.
The only situation that we don’t need to erase the old line is when m_ptEnd is first updated (before this, it
does not contain valid data).
Two Boolean type variables are initialized in the constructor of CGDIView:
CGDIView::CGDIView()
{
m_bCapture=TRUE;
m_bNeedErase=FALSE;
}
Message handlers of WM_LBUTTONDOWN, WM_MOUSEMOVE and WM_LBUTTONUP can all be added through
using Class Wizard. In the sample, these member functions are CGDIView::OnLButtonDown(…),
CGDIView:: OnMouseMove(…) and CGDIView::OnLButtonUp(…) respectively.
Function CGDIView::OnLButtonDown(…) is implemented as follows:
When the left button is pressed down, we need to store the current mouse position in m_ptStart. The
second parameter of CView::OnLButtonDown(…) is the current mouse position measured in window’s own
coordinate system, which can be stored directly to m_ptStart. After that, we can set window capture and
set flag m_bCapture.
When mouse is moving, we need to check if the left button is being held down. This can be
implemented by examining the first parameter (nFlags) of function CView::OnMouseMove(…). If its
MK_LBUTTON bit is set, the left button is being held down. We can also check other bits to see if SHIFT,
CTRL key or mouse right button is being held down.
If the left button is being held down, we need to draw the outline using the dotted pen. If there already
exists an old outline, we need to erase it before putting a new one. To draw a line, we need to first call
function CDC::MoveTo(…) to move the DC’s origin to the starting point of the line, then call
CDC::LineTo(…) to complete drawing. We don’t need to call CDC::MoveTo(…) each time when drawing
several connected line segments continuously. After function CDC::LineTo(…) is called, the DC’s origin
196
Chapter 8. DC, Pen, Brush and Palette
will always be updated to the end point of that line. The following function shows how the mouse moving
activities are handled in the sample:
We declare CClientDC type variable to obtain the device context of the client window. First function
CPen::CreatePen(…) is called to create a dotted pen with black color. Then this pen is selected into the
device context and the old pen is stored in ptrPenOld. Next, the device context’s drawing mode is set to
R2_XORPEN, and the original drawing mode is stored in nMode. If this is the first time the function is called
after the user pressed mouse’s left button, m_bNeedErase flag must be FALSE. In this case we need to set it
to TRUE. Otherwise if m_bNeedErase is TRUE, we need to first draw the old outline (This will erase the
outline). Next the current mouse position is stored in variable m_ptEnd, and CDC::MoveTo(…),
CDC::LineTo(…) are called to draw the new outline. Finally device context’s old drawing mode is resumed.
Also, the old pen is selected back into the DC (which will also select the new pen out of the DC).
For WM_LBUTTONUP message handler, we also need to erase the old line if necessary. Because the new
line is fixed now, we need to add new data to the document and update the client window. The following
function shows how this message is handled:
ptrDoc=(CGDIDoc *)GetDocument();
m_ptEnd=point;
ptrDoc->AddLine(CRect(m_ptStart.x, m_ptStart.y, m_ptEnd.x, m_ptEnd.y));
m_bNeedErase=FALSE;
Invalidate();
if(m_bCapture == TRUE)
{
::ReleaseCapture();
m_bCapture=FALSE;
}
197
Chapter 8. DC, Pen, Brush and Palette
CView::OnLButtonUp(nFlags, point);
}
Here a dotted pen is created again to erase the old line. Then function CView::GetDocument() is called
to obtain a pointer to the document. After the line information is retrieved from the mouse position,
function CGDIDoc::AddLine(…) is called to add new data to the document. Then flag m_bNeedErase is
cleared, and window capture is released. Finally function CWnd::Invalidate() is called to update the client
window. By default, this action will cause function CView::OnDraw(…) to be called.
For SDI and MDI applications, function CView::OnDraw(…) will be added to the project at the
beginning by Application Wizard. In order to implement our own interface, we need to rewrite this member
function as follows:
ASSERT_VALID(pDoc);
In the above function CPen::CreatePen(…) is called to create a red solid pen whose width is 1 device
unit. Since the window DC is passed as a parameter to this funciton, we do not need to call CWnd::GetDC()
or declare CClientDC type variable to obtain window’s device context. After the pen is selected into the
DC, the drawing mode is set to R2_COPYPEN, which will output the pen’s color to the target device. Next
function CGDIDoc::GetNumOfLines() is called to retrieve the total number of lines stored in the document.
Then a loop is used to draw every line in the client window. Finally the DC’s original drawing mode is
resumed and the new pen is selected out of it.
CBrush brush;
198
Chapter 8. DC, Pen, Brush and Palette
brush.Attach(::GetStockObject(GRAY_BRUSH));
brush.Detach();
We need to detach the object before the GDI variable goes out of scope.
When a rectangle is not finally fixed, we may want to draw only its border and leave its interior
unpainted. To implement this, we can select a NULL (hollow) brush into the device context. A hollow
brush can be obtained by calling function ::GetStockObject(…) using HOLLOW_BTRUSH or NULL_BRUSH flag.
Sample 8.2-1\GDI demonstrates how to implement an interactive environment to let the user draw
rectangles. It is an SDI application generated by Application Wizard. Like what we implemented in sample
8.1\GDI, first some member variables and functions are declared in the document for storing rectangles:
CGDIDoc::~CGDIDoc()
{
while(m_paRects.GetSize())
{
delete m_paRects.GetAt(0);
m_paRects.RemoveAt(0);
}
}
In class CGDIView, some new variables are declared, they will be used to record rectangles, erasing
state and window capture state:
CGDIView::CGDIView()
{
m_bNeedErase=FALSE;
m_bCapture=FALSE;
}
Message handlers for WM_LBUTTONDOWN, WM_MOUSEMOVE and WM_LBUTTONUP are added to class CGDIView
through using Class Wizard. They are implemented as follows:
199
Chapter 8. DC, Pen, Brush and Palette
ptrDoc=(CGDIDoc *)GetDocument();
pen.CreatePen(PS_DASH, 1, RGB(0, 0, 0));
ptrPenOld=dc.SelectObject(&pen);
brush.Attach(::GetStockObject(HOLLOW_BRUSH));
ptrBrushOld=dc.SelectObject(&brush);
nMode=dc.SetROP2(R2_XORPEN);
dc.Rectangle(m_rectDraw);
dc.SetROP2(nMode);
dc.SelectObject(ptrBrushOld);
dc.SelectObject(ptrPenOld);
pen.DeleteObject();
brush.Detach();
m_bNeedErase=FALSE;
m_rectDraw.right=point.x;
m_rectDraw.bottom=point.y;
ptrDoc->AddRect(m_rectDraw);
if(m_bCapture == TRUE)
{
::ReleaseCapture();
m_bCapture=FALSE;
}
Invalidate();
CView::OnLButtonUp(nFlags, point);
}
CView::OnMouseMove(nFlags, point);
}
200
Chapter 8. DC, Pen, Brush and Palette
int nNumOfRects;
int i;
CGDIDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
With the above implementation, the application will be able to let the user draw rectangles.
With only minor modifications we can let the application draw ellipses. Sample 8.2-2\GDI
demonstrates how to implement interactive ellipse drawing. It is based on sample 8.2-1\GDI.
To draw an ellipse, we need to call function CDC::Ellipse(…)and pass a CRect type value to it. This is
exactly the same with calling function CDC::Rectangle(…). So in the previous sample, if we change all the
“Rectangle” keywords to “Ellipse”, the application will be implemented to draw ellipses instead of
rectangles.
In sample 8.2-2\GDI, function CDC::Ellipse(…) is called within CGDIView::OnMouseMove(…) and
CGDIView::OnDraw(…). The following shows the modified portion of two functions:
8.3 Curve
We can call function CDC::PolyBezier(…) to draw curves. This function has two parameters:
201
Chapter 8. DC, Pen, Brush and Palette
The first parameter lpPoints is a pointer to an array of points, which specify the control points that
can be used for drawing a curve. The second parameter nCount specifies how many control points are
included in the array. We need at least four control points to draw a curve, although the last three points
could be the same (In this case, a straight line will be drawn).
Sample 8.3\GDI demonstrates how to implement an interactive environment that can let the user draw
curves. It is a standard SDI application generated by Application Wizard. First new variable and functions
are declared in the document, which will be used to store the data of curves:
We use a CDWordArray type variable m_dwaPts to record control points, this class can be used to record
DWORD type value, which is 32-bit integer. Because a point contains two integers, we need two DWORD type
variables to store one point. So in CGDIDoc::AddPoint(…), function CDWordArray::Add(…) is called twice
to add both x and y coordinates to the array. Function CGDIDoc::GetNumOfPts() returns the number of
control points, which is obtained through dividing the size of the array by 2. Function
CGDIDoc::GetOnePt() returns a specified control point, which is obtained from two consecutive elements
contained in array m_dwaPts.
Curve drawing is implemented in function CGDIView::OnDraw(…) as follows:
Since we need 4 control points to draw a curve, the number of curves we can draw should be equal to
the number of points stored in array CGDIDoc::m_dwaPts divided by 4. In the function, a loop is used to
draw each single curve. Within each loop, four control points are retrieved one by one, and stored in a local
CPoint type array pt. After all four control points are retrieved, function CDC::PolyBezier(…) is called to
draw the curve. Here, we also need to create a pen and select it into the DC before any drawing operation is
performed.
The rest thing we need to implement is recording control points. In order to do this, we need to handle
two mouse related messages: WM_LBUTTONUP and WM_MOUSEMOVE. In the sample, their message handlers are
202
Chapter 8. DC, Pen, Brush and Palette
added through using Class Wizard, the corresponding member functions are CGDIView::OnLButtonUp(…)
and CGDIView::OnMouseMove(…) respectively.
Since a curve needs four control points, we use mouse’s left button up event to record them. In the
application, a counter is implemented to count how many control points have been added. Before a new
curve is added, this counter is set to 0. As we receive message WM_LBUTTONUP, the counter will be
incremented by 1. As it reaches 4, we finish recording the control points, store the data in the document and
update the client window.
In the sample, to implement curve drawing, some new variables are declared in class CGDIView as
follows:
We are familiar with variables m_bCapture and m_bNeedErase. Here, variable m_ptCurve will be used
to record temporary control points, and m_nCurrentPt will act as a counter.
Some of the variables are initialized in the constructor:
CGDIView::CGDIView()
{
m_nCurrentPt=0;
m_bNeedErase=FALSE;
m_bCapture=FALSE;
}
Since m_nCurrentPt starts from 0, we need to count till it reaches 3. In function CGDIView::
OnLButtonUp(…), if the value of m_nCurrentPt becomes 3, we will call CGDIDoc::AddPoint(…) four times
to add the points stored in array m_ptCurve to the document, then update the client window. We also need
to reset flags m_nCurrentPt, m_bNeedErase and m_bCapture because the drawing is now complete. The
following is a portion of function CGDIView::OnLButtonUp(…) that demonstrates how to record the last
point and store data in the document:
If m_nCurrentPt is not 3, we need to record the point, erase the previous curve outline and draw the
new one if necessary:
……
}
203
Chapter 8. DC, Pen, Brush and Palette
else
{
if(m_nCurrentPt != 0)
{
dc.PolyBezier(m_ptCurve, 4);
m_ptCurve[m_nCurrentPt]=point;
for(i=m_nCurrentPt+1; i<4; i++)
{
m_ptCurve[i]=m_ptCurve[m_nCurrentPt];
}
dc.PolyBezier(m_ptCurve, 4);
m_nCurrentPt++;
}
else
{
m_ptCurve[m_nCurrentPt++]=point;
SetCapture();
m_bCapture=TRUE;
}
}
dc.SetROP2(nMode);
dc.SelectObject(ptrPenOld);
CView::OnLButtonUp(nFlags, point);
}
If m_nCurrentPt is 0, this means it is the first control point. In this case, we do not need to draw or
erase anything. However, we need to set the window capture.
If m_nCurrentPt is not 0, we need to erase the previous curve, record the new point, assign its value to
the rest points (This is for the convenience of drawing curve outline, because when we draw a curve, we
always need four control points), and draw the curve outline.
Then the implementation of CGDIView::OnMouseMove(…) becomes easy. We just need to check if there
is an existing curve outline. If so, we need to erase it before drawing a new curve outline. Otherwise we just
draw the curve outline directly and set m_bNeedErase flag:
And that is all we need to do. If we compile and execute the application at this point, we will be able to
draw curves using mouse.
204
Chapter 8. DC, Pen, Brush and Palette
205
Chapter 8. DC, Pen, Brush and Palette
This function outputs a text string at the specified x-y coordinates. The string is stored in str
parameter. Text color and background color can be set by using functions CDC::SetTextColor(…) and
CDC::SetBkColor(…). Background mode (specifies if text has transparent or opaque background) can be set
by using function CDC::SetBkMode(…).
ptStart
This function draws a chord formed from an ellipse and a line segment. Parameter lpRect specifies the
bounding rectangle of an ellipse, ptStart and ptEnd specify the starting and ending points of a line
segment (they do not have to be on the ellipse). The border of the chord will be drawn using the currently
selected pen, and the interior will be filled with the currently selected brush (Figure 8-1).
This function draws a rectangle specified by lpRect parameter. The rectangle will have a dotted
border. If we call this function twice for the same rectangle, the rectangle will be removed. This is because
when drawing the rectangle, the function uses bit-wise XOR mode.
The function draws a rectangle specified by lpRect with rounded corners. Parameter point specifies
width and the height of the ellipse that will be used to draw rounded corners (Figure 8-2). The border of the
rectangle will be drawn with currently selected pen and its interior will be filled with the currently selected
brush.
point.y
point.x
The function draws a pie that is formed from an ellipse and two line segments. The ellipse is specified
by parameter lpRect, and the two line segments are formed by the center of ellipse and ptStart, ptEnd
respectively (Figure 8-3). The pie will be drawn in the counterclockwise direction. The border of the pie
will be drawn using currently selected pen and its interior will be filled with the currently selected brush.
206
Chapter 8. DC, Pen, Brush and Palette
This function draws a polygon. Parameter lpPoints is an array of points specifying the vertices of the
polygon. Parameter nCount indicates the number of vertices. The border of the polygon will be drawn using
currently selected pen and its interior will be filled with the currently selected brush.
ptEnd
lpRect
This function draws a rectangle specified by lpRect. Its upper and left borders will be drawn using
color specified by clrTopLeft, and its bottom and right borders will be drawn using color specified by
clrBottomRight.
There is a slight difference between the two functions. For function CDC::FloodFill(…), the filling
starts from the point that is specified by parameters x and y using the brush being selected by the DC, and
stretches out to all directions until a border whose color is the same with parameter crColor is encountered.
The second function allows us to select filling mode, which is defined by parameter nFillType. Here we
have two choices: FLOODFILLBORDER and FLOODFILLSURFACE. The first filling mode is exactly the same with
that of function CDC::FloodFill(…). For the second mode, the filling starts from the point specified by
parameters x and y, and stretches out to all directions, fills all the area that has the same color with
crColor, until a border with different color is encountered.
Samples 8.5-1\GDI and 8.5-2\GDI demonstrate how to implement flood fill. They are based on sample
8.1\GDI. In the samples a new command is added to the application, it can be used by the user to fill any
closed area with gray color. This closed area can be formed from the lines drawn by the user.
First a button ID_FLOODFILL is added to the tool bar IDR_MAINFRAME. We will use this button to
indicate if the application is in the line drawing mode or flood filling mode. By default the button will stay
in its normal state, at this time the user can use mouse to draw lines in the client window. If the user clicks
this button, the application will toggle to flood filling mode, at this time, if the user clicks mouse within the
client window, flood filling will happen.
In the sample, both WM_COMMAND and UPDATE_COMMAND_UI message handlers are added for command
ID_FLOODFILL, the corresponding functions are CGDIDoc::OnFloodfill() and CGDIDoc::
OnUpdateFloodfill(…) respectively. Also, a Boolean type variable m_bFloodFill is declared in class
207
Chapter 8. DC, Pen, Brush and Palette
CGDIDoc, which is used to indicate the current mode of the application (line drawing mode or flood filling
mode):
CGDIDoc::CGDIDoc()
{
m_bFloodFill=FALSE;
}
void CGDIDoc::OnFloodfill()
{
m_bFloodFill=m_bFloodFill ? FALSE:TRUE;
}
void CGDIDoc::OnUpdateFloodfill(CCmdUI* pCmdUI)
{
pCmdUI->SetCheck(m_bFloodFill);
}
We need to implement flood filling in response to left button up events when CGDIDoc::m_bFloodFill
is TRUE. So we need to modify function CGDIView::OnLButtonDown(…). In the sample, we first check flag
CGDIDoc::m_bFloodFill. If it is set, we create a gray brush, select it into the DC and implement the flood
filling. Otherwise we prepare for line drawing as we did before:
ptrDoc=GetDocument();
if(ptrDoc->GetFloodFill() == TRUE)
{
brush.CreateSolidBrush(RGB(127, 127, 127));
ptrBrushOld=dc.SelectObject(&brush);
dc.FloodFill(point.x, point.y, RGB(255, 0, 0));
dc.SelectObject(ptrBrushOld);
}
else
{
m_ptStart=point;
SetCapture();
m_bCapture=TRUE;
}
CView::OnLButtonDown(nFlags, point);
}
208
Chapter 8. DC, Pen, Brush and Palette
CClientDC dc(this);
CPen pen;
CPen *ptrPenOld;
int nMode;
CGDIDoc *ptrDoc;
ptrDoc=(CGDIDoc *)GetDocument();
if((nFlags & MK_LBUTTON) && ptrDoc->GetFloodFill() == FALSE)
{
……
}
ptrDoc=(CGDIDoc *)GetDocument();
if(ptrDoc->GetFloodFill() == FALSE)
{
……
}
8.5-1\GDI:
8.5-2\GDI:
We call function CDC::GetPixel(…) to retrieve the color of the pixel specified by point, and fill the
area that has the same color with it. The flood filling will stretch out to all directions until borders with
different colors are reached. For samples 8.5-1\GDI and 8.5-2\GDI, we don’t see much difference between
function CDC::ExtFloodFill(…) and CDC::FloodFill(…). However, in the sample, if the lines can be
drawn with different colors, we need to use CDC::ExtFloodFill(…) rather than CDC::FloodFill(…) to
implement flood filling.
Sample 8.6\GDI demonstrates how to create and use pattern brush. It is based on sample 8.2-1\GDI. In
the new sample, pattern brush is used to fill the interior of the rectangles instead of solid brush.
209
Chapter 8. DC, Pen, Brush and Palette
First an 8×8 bitmap resource IDB_BITMAP_BRUSH is added to the application. Here function CGDIView::
OnDraw(…) is modified as follows: 1) A new local CBitmap type variable bmp is declared, which is used to
call function CBitmap::LoadBitmap(…) for loading bitmap IDB_BITMAP_BRUSH. 2) The original statement
brush.CreateSolidBrush(…) is changed to brush.CreatePatternBrush(…). The following is the modified
function CGDIView::OnDraw(…):
With the above change, we can see the effect of pattern brush.
210
Chapter 8. DC, Pen, Brush and Palette
Color Approximation
Since the index has only 8 bits, the size of this color table is limited to contain no more than 256
colors. In Windows, in order to maintain some standard colors (i.e., the caption bar color, border color,
menu color, etc.), some entries of this color table are reserved for solely storing system colors. The colors
stored in these entries are called Static Colors. For the rest entries of the color table, any application may
fill them with custom colors. When we specify an R, G, B combination and use it to draw geometrical
objects, the actual color appeared on the screen depends on the available colors contained in the color table.
If the specified color can not be found in the color table, Windows uses two different approaches to do
the color approximation: for brush, it uses dithering method to simulate the specified color using the colors
that can be found in the color table (For example, if color gray can not be found in the color table, the
system combines black and white to simulate it); for pen, the nearest color that can be found in the color
table will be used instead of the specified color.
Sample
Sample 8.7-1\GDI and 8.7-2\GDI demonstrate two different color approximation approaches. Sample
8.7-1\GDI is a standard SDI application generated from Application Wizard. In function CGDIView::
OnDraw(…), the client window area is painted with blue colors that gradually change from dark blue (black)
to bright blue. The GDI object used here is brush.
We need to create 256 different brushes using colors from RGB(0, 0, 0), RGB(0, 0, 1), RGB(0, 0,
2)... to RGB(0, 0, 255). Also, we need to divide the client window into 256 rows. For each row, a
different brush can be used to fill it. This will generate a visual effect that the color changes gradually from
dark blue to bright blue. The following is the modified function CGDIView::OnDraw(…):
First function CWnd::GetClientRect(…) is called to retrieve the dimension of the client window,
which is stored in variable rect. Then the rectangle’s vertical size is shrunk to 1/256 of its original size.
Next a 256-step loop is used to fill the client window. Within each loop, a different blue brush is created,
and function CDC::FillRect(…) is called to fill the rectangle. Before calling this function, we do not need
to select the brush into DC, this is because the second parameter of function CDC::FillRect(…) is a CBrush
type pointer (The DC selection happens within the function). The difference between function CDC::
Rectangle(…) and CDC::FillRect(…) is that the former will draw a border with the currently selected pen
while the latter does not draw the border.
Sample 8.7-2\GDI is based on sample 8.7-1\GDI. Here within function CGDIView::OnDraw(…), pens
with different blue colors are created and used to draw lines for painting each row. The following is the
modified function CGDIView::OnDraw(…):
211
Chapter 8. DC, Pen, Brush and Palette
Instead of creating brushes, solid pens are created to paint the client window area. The width of these
pens is the same with the height of each row, and their colors change gradually from dark blue to bright
blue. Within each loop, before calling functions to draw a line, we select the pen into DC, after the line is
drawn, we select the pen out of the DC. Because a new pen is created in each loop, function
CPen::DeleteObject() is called at the end of the loop. This will destroy the pen so that variable pen can be
initialized again.
Results
Figure 8-5 and Figure 8-6 show the results from the two color approximation approaches (Please note
that if the samples are executed on non-palette device, we may not see the approximation effect).
User can avoid color approximation by increasing the color depth of the system. One simple way to do
so is to reduce the screen resolution (e.g. from 1024×768 to 800×600): after the total number of pixels is
reduced, the number of colors supported by the system will probably increase.
As a programmer, we need to prepare for the worst situation and make our application least susceptible
to the system setting. To achieve this, we need to implement local palette.
212
Chapter 8. DC, Pen, Brush and Palette
Palette
Palette is another type of GDI objects, it encapsulates a color palette that can be used to store custom
colors in an application. Although programmer can use logical palette like using an actual palette, it is not a
real palette. Within any system, there is only one existing palette, which is the physical palette (or system
palette). This is why the palettes created in the applications are called logical palettes. The advantage of
using a logical palette is that the colors defined in the logical palette can be mapped to the system palette in
an efficient way so that least color distortion can be achieved. There is no guarantee that all the colors
implemented in the logical palette will be displayed without color distortion. Actually, the ultimate color
realizing ability depends on the hardware. For example, if our hardware support only 256 colors and we
implement a 512 logical palette, some colors defined in the logical palette will inevitably be distorted if we
display all the 512 colors on the screen at the same time.
Color Mapping
When we implement a logical palette, operating system maps the colors contained in the logical palette
to the system palette using the following method: for every entry in the logical palette, the system first finds
out if there exists a color in the system palette that is exactly the same with the color contained in this entry.
If so, it will be mapped to the corresponding entry of the system palette. If no such entry is found in the
system palette, the system will find out if there is any entry in the system palette that is not occupied by any
logical palette. If such an entry exists, the color in the logical palette will be filled into that entry. If there is
213
Chapter 8. DC, Pen, Brush and Palette
no such entry available, the system find out the nearest color in the system palette and map the entry in the
logical palette to it.
For a 256 color system, the operating system reserve 20 static colors as system colors, which can not
be used to fill new colors. This assures that the default colors of window border, title, button do not change
when we implement logical palettes. These static colors always occupy the first 10 and last 10 entries of the
system palette. The rest 236 entries can be used fill any color.
The only parameter of this function is a LOGPALETTE type pointer. The following is the format of
structure LOGPALETTE:
This structure has three members: palVersion specifies the Windows version of this structure, which
must be set to 0x300; palNumEntries specifies the total number of entries contained in this palette, and
palNumEntries is the first element of a PALETTEENTRY type array, which stores the color table. Structure
PALETTEENTRY has four members:
Members peRed, peGreen and peBlue specify RGB intensities, and peFlags can be assigned NULL if
we want to create a normal palette (we will see how to set this flag to create a special palette later). To
create a logical palette, we need to allocate enough buffers for storing LOGPALETTE structure and colors, and
call function CPalette::CreatePalette(…).
214
Chapter 8. DC, Pen, Brush and Palette
The first parameter is a CPalette type pointer, which stands for the logical palette we are going to use.
The second parameter is a Boolean type variable indicating if the palette will be selected as a background
palette or not.
We need to select the palette out of the DC after using it.
Realizing Palette
The color mapping does not happen if we do not realize the logical palette. We need to realize the
palette in the following situations: 1) After the palette is created and about to be used. 2) After the colors in
the logical palette have changed. 3) After the colors contained in the system palette have changed. 4) After
the application has regained the focus.
To realize the palette, we need to call function CDC::RealizePalette() to force the colors in the
logical palette to be mapped to the system palette. The format of this function is very simple:
UINT CDC::RealizePalette();
The returned value indicates how many entries were mapped to the system palette successfully.
Macro PALETTEINDEX
When a logical palette is selected into the DC, we need to use index to the color table to reference a
color in the palette. In this case, we can use macro PALETTEINDEX to convert an index to R, G, B
combination.
Sample
Sample 8.8-1\GDI and 8.8-2\GDI are based on sample 8.7-1\GDI and 8.7-2\GDI respectively. They
demonstrate how to implement logical palette to avoid color distortion. In the two samples, the logical
palettes contain all colors that will be used to paint the client window. These colors will be mapped to the
colors contained in the system palette.
In both samples, first a CPalette type variable m_palDraw is declared in class CGDIView as follows:
CGDIView::CGDIView()
{
int i;
LPLOGPALETTE lpLogPal;
In the function a LOGPALETTE type pointer is declared, we need to use it to store the buffers allocated
for creating the logical palette. The buffers are allocated through using “new” method. The following is the
formulae that is used to calculate the size of the total bytes needed for creating logical palette:
215
Chapter 8. DC, Pen, Brush and Palette
Next the palette entries are filled with 256 colors ranging from dark blue to bright blue. After the
logical palette is created, these buffers can be released.
The following is the updated function CGDIView::OnDraw(…) in sample 8.8-1\GDI:
CGDIDoc *pDoc=GetDocument();
ASSERT_VALID(pDoc);
ptrPalOld=pDC->SelectPalette(&m_palDraw, FALSE);
pDC->RealizePalette();
GetClientRect(rect);
rect.bottom+=255;
rect.bottom/=256;
for(i=0; i<256; i++)
{
brush.CreateSolidBrush(PALETTEINDEX(i));
pDC->FillRect(rect, &brush);
brush.DeleteObject();
rect.OffsetRect(0, rect.Height());
}
pDC->SelectPalette(ptrPalOld, FALSE);
}
Each time the client window needs to be painted, we first call function CDC::SelectPalette(…) to
select the logical palette into the device context and call function CDC::RealizePalette() to let the colors
in the logical palette be mapped to the system palette. When creating the brushes, instead of using RGB
macro to specify an R, G, B combination, we need to use PALETTEINDEX macro to indicate a color contained
in the logical palette.
Sample 8.8-2\GDI is implemented almost in the same way: first a CPalette type variable m_palDraw is
declared in class CGDIView, and the palette is created in its constructor. In function CGDIView::OnDraw(…),
before the client window is painted, the logical palette is selected into the device context. The following is
the modified function CGDIView::OnDraw(…):
CGDIDoc *pDoc=GetDocument();
ASSERT_VALID(pDoc);
ptrPalOld=pDC->SelectPalette(&m_palDraw, FALSE);
pDC->RealizePalette();
GetClientRect(rect);
rect.bottom+=255;
rect.bottom/=256;
for(i=0; i<256; i++)
{
pen.CreatePen(PS_SOLID, rect.Height(), PALETTEINDEX(i));
ptrPenOld=pDC->SelectObject(&pen);
pDC->MoveTo(0, rect.top+rect.Height()/2);
pDC->LineTo(rect.right, rect.top);
pDC->SelectObject(ptrPenOld);
pen.DeleteObject();
rect.OffsetRect(0, rect.Height());
}
pDC->SelectPalette(ptrPalOld, FALSE);
}
216
Chapter 8. DC, Pen, Brush and Palette
This function is similar to that of sample 8.8-1\GDI: when creating a pen, we use PALETTEINDEX macro
to indicate a color rather than using an R, G, B combination.
The samples will improve a lot with the above implementations (If the samples are executed on non-
palette device, there will be no difference between the samples here and those in the previous section).
However, we may still notice a tiny step between two contiguous blues. This is because for a video device
of this type (256 color), usually the color depth is 18 bits on the hardware level. This means red, green and
blue colors each uses only 6 bits (instead of 8 bits). So instead of displaying 256-level blue colors, only 64-
level blue colors can be implemented.
CGDIView::CGDIView()
{
LPLOGPALETTE lpLogPal;
int i;
217
Chapter 8. DC, Pen, Brush and Palette
The procedure above is almost the same with creating a normal logical palette. The only difference
here is that when stuffing structure PALETTEENTRY, the low order word of the structure is filled with an
index to the system palette entry (from 0 to 255), and member peFlags is set to PC_EXPLICIT.
Function CGDIView::OnDraw(…) is implemented as follows:
CGDIDoc* pDoc=GetDocument();
ASSERT_VALID(pDoc);
ASSERT(m_palSys.GetSafeHandle());
ptrPalOld=pDC->SelectPalette(&m_palSys, FALSE);
pDC->RealizePalette();
GetClientRect(rect);
rect.right/=16;
rect.bottom/=16;
The client area is divided into 256 (16×16) rectangles. For each rectangle, a brush using a specific
logical palette entry is created. Then function CDC::Rectangle(…) is called to draw the rectangle. Because
no pen is selected into the DC, the default pen will be used to draw the border of the rectangles.
The application can be compiled and executed at this point. There are 256 rectangles in the client
window, each represents one color contained in the system palette. By paying attention to the first 10 and
last 10 entries, we will notice that the colors contained in these entries never change (because they are static
colors). Other colors will change from time to time as we open and close graphic applications. We can use
samples 8.8-1\GDI or 8.8-2\GDI to test this.
Please note that if the sample is executed on non-palette device, the logical palette will not represent
the system palette. This is because on the hardware level, palette does not exist at all.
Flag PC_RESERVED
Another interesting flag we can use when creating a logical palette is PC_RESERVED, which can be used
to implement palette animation. If we have a logical palette with this flag set for some entries, the colors in
these entries will only be mapped to the unused entries of the system palette. If the mapping is successful,
when we change the colors in the logical palette, the entries in the system palette will also change. If any
portion of window is painted with such entries, this change will affect that portion. This is the reason why a
logical entry with PC_RESERVED flag can not be mapped to an occupied entry.
The following table lists the difference between a normal logical palette entry and an entry with
PC_RESERVED flag:
218
Chapter 8. DC, Pen, Brush and Palette
While we can change the color of any area in a window by painting it again (using a different brush or
pen), the above mentioned method has two advantages:
1) If we have several areas painted with the same color, the new method will cause all of them to change
at the same time once the old color is replaced with a new one in the logical palette. For the traditional
method, we have to draw each area one by one to make this change, it takes longer time.
1) If we draw each area one by one, the change takes place in software level. For the new method, the
color is filled to the system palette directly (This change happens at the hardware level), which is
extremely fast.
Animation
With this method, it is very easy to implement an animation effect. For example, considering an array
of four rectangles that are filled with the following four different colors respectively: red, green, blue and
black. If we paint the four rectangles with green, blue, black, red next time, and blue, black, red, green next
next time, and so on…, this will give us an impression that the rectangles are doing rotating shift. One way
to implement this effect is to redraw four rectangles again and again using different colors (which means
using different entries to draw the same rectangle again and again). Another way is to switch the colors in
the palette directly.
Sample 8-10\GDI demonstrates how to implement palette animation. It is a standard SDI application
generated from Application Wizard. In the sample, the client area is divided into 236 columns, each row is
painted with a different color. The colors in the logical palette will be shifting all the time, and we will see
that the colors in the client window will also shift accordingly.
Among 256 system palette entries, only 236 of them contain non-static colors, so in the sample, a
logical palette with 236 entries is created. The colors contained in this palette change gradually from red to
green, and from green to blue. Figure 8-7 shows the RGB combination of each entry (i.e., entry 0 contains
RGB(255, 0, 0), entry 79 contains RGB(0, 255, 0)…).
Sample
First three variables are declared in class CGDIView:
219
Chapter 8. DC, Pen, Brush and Palette
Variable m_palAni is used to create the animation palette, and array m_palEntry will be used to
implement color shifting. When we shift the palette entries, it would be much faster if we use function
memcpy(…) to copy all the entries in just one stroke. To achieve this, we keep all the colors in a
PALETTEENTRY type array whose size is twice the logical palette size minus one. The original 236 colors are
stored in entries 0, 1, 2… to 235, and entries 236, 237, 238…470 store the same colors as those contained
in entries 0, 1, 2… 234. Also, we use variable m_nEntryID to indicate the current starting entry of the
logical palette. For example, if m_nEntryID is 2, the logical palette should be filled with colors contained in
entries 2 to 237 of m_palEntry. If m_nEntryID reaches 236, we need to reset it to 0. Figure 8-8
demonstrates this procedure.
First, 236 different colors are filled into entries from 0 to 235 for variable m_palEntry in the
constructor of class CGDIView as follows:
CGDIView::CGDIView()
{
int i;
LPLOGPALETTE lpLogPal;
220
Chapter 8. DC, Pen, Brush and Palette
……
memcpy
(
(BYTE *)&m_palEntry[236],
(BYTE *)&m_palEntry[0],
235*sizeof(PALETTEENTRY)
);
……
Next, we use entries from 0 to 235 contained in m_palEntry to create logical palette (using variable
m_palAni), and assign 0 to variable m_nEntryID:
……
lpLogPal=(LPLOGPALETTE) new BYTE[sizeof(LOGPALETTE)+235*sizeof(PALETTEENTRY)];
lpLogPal->palVersion=0x300;
lpLogPal->palNumEntries=236;
m_nEntryID=0;
}
Note when filling array m_palEntry, we need to assign PC_RESERVED flag to member peFlags of
structure PALETTEENTRY for every element. This flag will be copied to array pointed by lpLogPal when we
actually create the palette. As we refill the palette entries again and again, these flags should remain
unchanged all the time. Otherwise, the entries can not be changed dynamically.
To realize the animation, we need to implement a timer, and change the palette when it times out. The
best place to start the timer is in function CView::OnInitialUpdate(), where the view is just created. In
the sample, this function is added through using Class Wizard, and is implemented as follows:
void CGDIView::OnInitialUpdate()
{
SetTimer(TIMER_ANIMATE, 100, NULL);
CView::OnInitialUpdate();
}
Macro TIMER_ANIMATE is defined at the beginning of the implementation file, which acts as the ID of
the timer:
221
Chapter 8. DC, Pen, Brush and Palette
Next, a WM_TIMER type message handler is added to class CGDIView through using Class Wizard, which
is implemented as follows:
ptrPalOld=dc.SelectPalette(&m_palAni, FALSE);
dc.RealizePalette();
m_nEntryID++;
if(m_nEntryID >= 236)m_nEntryID=0;
m_palAni.AnimatePalette(0, 236, (LPPALETTEENTRY)&m_palEntry[m_nEntryID]);
dc.SelectObject(ptrPalOld);
CView::OnTimer(nIDEvent);
}
Like other drawing operations, before implementing palette animation, we must select the palette into
target DC and realize it. After the animation is done, we need to select the palette out of DC. The palette
animation is implemented through calling function CPalette::AnimatePalette(…), which has the
following format:
void CPalette::AnimatePalette
(
UINT nStartIndex, UINT nNumEntries, LPPALETTEENTRY lpPaletteColors
);
The first parameter nStarIndex is the index indicating the first entry that will be filled with a new
color, the second parameter nNumEntries indicates the total number of entries whose color will be changed,
and the last parameter is a PALETTEENTRY type pointer which indicates buffers containing new colors.
If we call this function and change the contents of a logical palette, only those entries with
PC_RESERVED flags will be affected.
The last thing we need to do is painting the client area using animation palette in function CGDIView::
OnDraw(…):
CGDIDoc *pDoc=GetDocument();
ASSERT_VALID(pDoc);
ptrPalOld=pDC->SelectPalette(&m_palAni, FALSE);
pDC->RealizePalette();
GetClientRect(rect);
rect.right+=rect.right/2;
rect.right/=236;
for(i=0; i<236; i++)
{
pDC->FillSolidRect(rect, PALETTEINDEX(i));
rect.OffsetRect(rect.Width(), 0);
}
pDC->SelectPalette(ptrPalOld, FALSE);
}
This is very straight forward, we just divide the client area into 236 columns and paint each column
using one color contained in the logical palette. Note in function CGDIView::OnTimer(…), after palette
animation is implemented, we do not need to call function CWnd::Invalidate() to update the client
window.
Now the application can be compiled and executed. If we execute this application along with sample
8.9\GDI, we will see how system palette changes when the animation is undergoing (We can put sample
8.9\GDI in background to monitor the system palette): as the colors in the client window shifts, the colors
in the system palette will also change.
Palette animation can also be used to implement special visual effect on bitmaps such as fade out. This
function only works on palette type video device.
222
Chapter 8. DC, Pen, Brush and Palette
Please note that if the sample is executed on non-palette device, the animation effect can not be seen.
This is because there is no palette on the hardware level.
By passing different flags to parameter nIndex, we can retrieve different attributes of a device. The
following is a list of some important flags that can be used:
Sample 8.11\GDI demonstrates how to check the abilities of a device. It is a standard SDI application
generated from Application Wizard. In the sample, ability checking is implemented in the initialization
stage of the client window.
For this purpose, function CGDIView::OnInitialUpdate() is added to the application through using
Application Wizard. In this function, various device abilities are checked and the result is displayed in a
message box:
void CGDIView::OnInitialUpdate()
{
CClientDC dc(this);
CString szStr;
CString szBuf;
int nRasterCaps;
int nNumOfColors;
int nNumOfReserved;
int nColorRes;
CView::OnInitialUpdate();
223
Chapter 8. DC, Pen, Brush and Palette
{
szStr+="\tIt is a palette device\n";
nNumOfColors=dc.GetDeviceCaps(SIZEPALETTE);
szBuf.Format("\tThe device supports %d colors\n", nNumOfColors);
szStr+=szBuf;
szBuf.Empty();
nNumOfReserved=dc.GetDeviceCaps(NUMRESERVED);
szBuf.Format("\tThere are %d static colors\n", nNumOfReserved);
szStr+=szBuf;
nColorRes=dc.GetDeviceCaps(COLORRES);
szBuf.Format("\tColor resolution is %d bits\n", nColorRes);
szStr+=szBuf;
}
else
{
szStr+="\tIt is not a palette device\n";
}
szBuf.Empty();
szBuf.Format
(
"\tHorizontal size is %d mm, %d pixels\n",
dc.GetDeviceCaps(HORZSIZE),
dc.GetDeviceCaps(HORZRES)
);
szStr+=szBuf;
szBuf.Empty();
szBuf.Format
(
"\tVertical size is %d mm, %d pixels\n",
dc.GetDeviceCaps(VERTSIZE),
dc.GetDeviceCaps(VERTRES)
);
szStr+=szBuf;
AfxMessageBox(szStr);
}
Here we check if the device is a palette device or not. If it is a palette device, we further find out the
maximum number of colors it supports, the number of static colors reserved, and the actual color resolution
for each pixel. Also, the device’s horizontal and vertical sizes are retrieved.
This function is especially useful in finding out if the device is a palette device or not. With this
information, we can decide if the logical palette should be used in the application.
The following lists the capabilities of certain device:
It is a palette device
The device supports 256 colors
There are 20 static colors
Color resolution is 18 bits
Horizontal size is 270 mm, 1024 pixels
Vertical size is 203 mm, 768 pixels
Summary:
1) Before drawing anything to a window, we must first obtain its device context. There are many ways of
obtaining a window’s DC (Calling function CWnd::GetDC(), declaring CClientDC or CWindowDC type
variables, etc.).
1) A client DC can be used to paint a window’s client area, and a window DC can be used to paint the
whole window (client and non-client area).
1) Pen can be used to draw line, the border of rectangle, polygon, ellipse, etc. A pen can have different
styles: solid pen, dotted pen, dashed pen, etc.
1) Brush can be used to fill the interior of rectangle, polygon, ellipse, etc. A brush can have different
patterns: solid, hatched, etc.
1) We can use an 8×8 image to create pattern brush.
1) On a palette device, there are two color approaching methods: dithering and using the nearest color.
1) Logical palette can be used to avoid color distortion. To use a logical palette, we need to create it,
select it into DC, and realize the palette.
224
Chapter 8. DC, Pen, Brush and Palette
1) System palette can be monitored by creating a logical palette whose entries are set to PC_EXPLICIT
flags.
1) Palette animation can be implemented by creating a logical palette whose entries are set to
PC_RESERVED flag.
1) The abilities of a device can be retrieved by calling function CDC::GetDeviceCaps().
225
Chapter 9. Font
Chapter 9
Font
F
ont is another very important GDI object, every application deals with font. Usually a system
contains some default fonts that can be used by all the applications. Besides these default fonts, we
can also install fonts provided by the thirty party. For word processing applications, using font is a
complex issue. There are many things we need to take care. For example, when creating this type of
applications, we need to think about the following issues: how to display a font with different styles; how to
change the text alignment; how to add special effects to characters.
BOOL CFont::CreateFont
(
int nHeight, int nWidth,
int nEscapement, int nOrientation, int nWeight,
BYTE bItalic, BYTE bUnderline, BYTE cStrikeOut,
BYTE nCharSet, BYTE nOutPrecision, BYTE nClipPrecision, BYTE nQuality,
BYTE nPitchAndFamily, LPCTSTR lpszFacename
);
The first function has many parameters and the second one needs only a LOGFONT type pointer. The
results of the two member functions are exactly the same, every style we need to specify for a font in the
first function has a corresponding member in structure LOGFONT:
226
Chapter 9. Font
Here, member lfFaceName specifies the font name; lfHeight and lfWidth specify font size; lfWeight,
lfItalic, lfUnderline and lfStrikeOut specify font styles. Besides these styles, there are two other
styles that can be specified: lfEscapement and lfOrientation.
Under Windows 95, lfEscapement and lfOrientation must be assigned the same value when a font
is being created. If they are non-zero, the text will have an angle with respect to the horizontal border of the
window when it is displayed (Figure 9-1). To display text this way, we must assign the angle to both
lfEscapement and lfOrientation when creating the font, the unit of the angle is one tenth of a degree.
Please note that only True Type fonts can have such orientation.
Escapement angle
After a font is created, it can be selected into DC for outputting text. After the text output is over, it
must be selected out of the DC. This procedure is exactly the same with other GDI objects.
Sample 9.1\GDI demonstrates how to create and use a font with specified styles. It is a standard SDI
application generated by Application Wizard. In the sample, the user can choose any available font in the
system and set its styles (bold, italic, underline, etc). The face name of the font will be displayed in the
client window using the selected font, and the user can also set the escapement of the font.
Two variables are added to class CGDIDoc: CGDIDoc::m_fontDraw and CGDIDoc::m_colorFont. The
first variable is declared as a CFont type variable, it will be used to create the font. The second variable is
declared as a COLORREF type variable, it will be used to store the color of the text.
Besides font color, we also need to consider the background color of the text. A text can be displayed
with either a transparent or opaque background. In the latter case, we can set the background to different
colors. In order to display text in different styles, another Boolean type variable CGDIDoc::
m_bTransparentBgd is declared, it will be used to indicate if the background is transparent or opaque.
The following is the modified class CGDIDoc:
Besides the three new member variables, there are also three new member functions added to the class.
These functions allow the information stored in CGDIDoc to be accessible outside the class, they are
CGDIDoc ::GetCurrentFont(), CGDIDoc::GetFontColor() and CGDIDoc::GetBgdStyle().
The above variables are initialized in the constructor of class CGDIDoc:
CGDIDoc::CGDIDoc()
{
LOGFONT lf;
CClientDC dc(NULL);
CFont *ptrFt;
ptrFt=dc.GetCurrentFont();
ptrFt->GetLogFont(&lf);
ASSERT(m_fontDraw.CreateFontIndirect(&lf));
227
Chapter 9. Font
m_colorFont=RGB(0, 0, 0);
m_bTransparentBgd=TRUE;
}
When a DC is created, it selects the default font, pen, brush and other GDI objects. So here we create a
DC that does not belong to any window, and call function CDC::GetCurrentFont() to obtain its currently
selected font (which is the default font). Then function CFont::GetLogFont(…) is called to retrieve the font
information, which is stored in a LOGFONT type object. With this object, we can create a system default font
by calling function CFont::CreateFontIndirect(…). By default, the font color is set to black and the text
background mode is set to transparent.
We need to provide a way of letting user modify the font styles. This can be easily implemented by
using a font common dialog box. In the sample, two commands are added to the application: Font | Select
and Font | Escapement and Orientation, whose IDs are ID_FONT_SELECT and ID_FONT_STYLE
respectively. Also, message handlers are added through using Class Wizard, the corresponding functions
are CGDIDoc::OnFontStyle() and CGDIDoc::OnFontSelect().
Function CGDIDoc::OnFontSelect() lets the user select a font, set its styles, and specify the text color.
It is impelemented as follows:
void CGDIDoc::OnFontSelect()
{
CFontDialog dlg;
LOGFONT lf;
if(dlg.DoModal() == IDOK)
{
dlg.GetCurrentFont(&lf);
if(m_fontDraw.GetSafeHandle() != NULL)m_fontDraw.DeleteObject();
ASSERT(m_fontDraw.CreateFontIndirect(&lf));
m_colorFont=dlg.GetColor();
UpdateAllViews(NULL);
}
}
A font common dialog is implemented to let the user pick up a font. If a font is selected, function
CFontDialog::GetCurrentFont(…) is called to retrieve the information of the font, which is stored in a
LOGFONT type object. Because member m_fontDraw is already initialized, we need to delete the old font
before creating a new one. The font is created by calling function CFont::CreateFontIndirect(…). After
this the color of the font is retrieved by calling function CFontDialog::GetColor(), and stored in the
variable CGDIDoc::m_colorFont. Finally function CDocument::UpdateAllViews(…) is called to update the
client window of the application.
Since font common dialog box does not contain escapement and orientation choices, we have to
implement an extra dialog box to let the user set them. In the sample, dialog template IDD_DIALOG_STYLE is
added for this purpose. Within this template, besides the default “OK” and “Cancel” buttons, there are two
other controls included in the dialog box: edit box IDC_EDIT_ESP, which allows the user to set escapement
angle; check box IDC_CHECK, which allows the user to select text background style (transparent or opaque).
A new class CStyleDlg is added for this dialog template, within which two variables m_lEsp (long type)
and m_bBgdStyle (Boolean type) are declared. Both of them are added through using Class Wizard, and are
associated with controls IDC_EDIT_ESP and IDC_CHECK respectively.
Command Font | Escapement and Orientation is implemented as follows:
void CGDIDoc::OnFontStyle()
{
LOGFONT lf;
CStyleDlg dlg;
m_fontDraw.GetLogFont(&lf);
dlg.m_lEsp=lf.lfEscapement;
dlg.m_bBgdStyle=m_bTransparentBgd;
if(dlg.DoModal() == IDOK)
{
lf.lfOrientation=lf.lfEscapement=dlg.m_lEsp;
m_bTransparentBgd=dlg.m_bBgdStyle;
if(m_fontDraw.GetSafeHandle() != NULL)m_fontDraw.DeleteObject();
ASSERT(m_fontDraw.CreateFontIndirect(&lf));
UpdateAllViews(NULL);
}
228
Chapter 9. Font
First function CFont::GetLogFont(…) is called to retrieve the information of the current font, which is
then stored in a LOGFONT type object. Before the dialog box is invoked, its members CStyleDlg::m_lEsp
and CStyleDlg::m_bBgdStyle are initialized so that the current font’s escapement angle and background
style will be displayed in the dialog box. After function CDialog::DoModal() is called, the new value of
CStyleDlg::m_lEsp is stored back to members lfEscapement and lfOrientation of structure LOGFONT,
and the new value of CStyleDlg::m_bBgdStyle is stored to variable CGDIDoc::m_bTransparentBgd. Then
the old font is deleted and the new font is created. Finally, function CGDIDoc::UpdateAllViews(…) is called
to update the client window.
When we call function CDocument::UpdateAllViews(…), the associated view’s member function
OnDraw(…) will be called automatically. So we need to modify this function to display the font specified by
variable CGDIDoc::m_fontDraw. The following is the implementation of function CGDIView::OnDraw():
CGDIDoc* pDoc=GetDocument();
ASSERT_VALID(pDoc);
ptrFt=pDoc->GetCurrentFont();
ASSERT(ptrFt != NULL);
if(ptrFt->GetSafeHandle() != NULL)
{
ptrFt->GetLogFont(&logFont);
ptrFtOld=pDC->SelectObject(ptrFt);
pDC->SetTextColor(pDoc->GetFontColor());
pDC->SetBkMode(pDoc->GetBgdStyle() ? TRANSPARENT:OPAQUE);
pDC->SetBkColor((~pDoc->GetFontColor())&0x00FFFFFF);
}
GetClientRect(rect);
pDC->TextOut(rect.Width()/4, rect.Height()/4, logFont.lfFaceName);
if(ptrFt->GetSafeHandle() != NULL)pDC->SelectObject(ptrFtOld);
}
First, function CGDIDoc::GetCurrentFont() is called to retrieve the currently selected font from the
document, then function CFont::GetLogFont(…) is called to retrieve the information of this font (the face
name of the font will be used as the output string). Next, the font is selected into the target DC. Also,
CGDIDoc::GetFontColor() is called to retrieve the current font color, and CDC::SetTextColor(…) is called
to set the text foreground color. Then, CGDIDoc::GetBgdStyle(…) is called to see if the text should be
drawn with an opaque or transparent background, and CDC::SetBkMode(…) is called to set the background
style. Next, the text background color is set to the inverse of the foreground color by calling function
CDC::SetBkColor(…) (If text background style is transparent, this operation has no effect). Finally, function
CDC::TextOut(…) is called to display font’s face name in the client window, and the font is selected out of
the DC.
The default font displayed in the client window should be “System”, which is not a True Type font. To
see how a text can be displayed with different escapement angles, we need to choose a True Type font such
as “Arial”. Please note that the unit of the escapement angle is on tenth of a degree, so if we want to display
the text vertically, escapement angle should be set to 900.
Font Types
There are three type of fonts in Windows system: raster font, vector font and True Type font. The
difference among them is how the character glyph is stored for each type of fonts. For raster fonts, the
glyph is simply a bitmap; for vector fonts, the glyph is a collection of end points that define the line
segments; for true type fonts, the glyph is a collection of line and curve commands. The raster fonts can not
229
Chapter 9. Font
be drawn in a scaled size, they are device dependent (the size of the output depends on the resolution of the
device). The vector fonts and true Type Fonts are device independent because they are scalable, however,
drawing True Type fonts is faster than drawing vector fonts. This is because the glyph of True Type fonts
contains commands and hints.
int EnumFontFamilies
(
HDC hdc, LPCTSTR lpszFamily, FONTENUMPROC lpEnumFontFamProc, LPARAM lParam
);
The first parameter hdc is the handle to a device context, which can be obtained from any window.
The second parameter lpszFamily specifies the font family name that will be enumerated, it could be
any of “Decorative”, “Dontcare”, “Modern”, “Roman”, “Script” and “Swiss”. To enumerate all fonts in the
system, we just need to pass NULL to it.
The third parameter is a pointer to callback function, which will be used to implement the actual
enumeration. This function must be provided by the programmer.
The final parameter lParam is a user-defined parameter that allows us to send information to the
callback function.
The callback function must have the following format:
Each time a new font family is enumerated, this function is called by the system. So the function will
be called for all the available font types (e.g. if there are three types of fonts in the system, this funciton
will be called three times by the system). The font’s information is stored in an ENUMLOGFONT type object
that is pointed by lpelf, and the font type is specified by FontType parameter. We can check
RASTER_FONTTYPE or TRUETYPE_FONTTYPE bit of FontType to judge if the font is a raster font or a true type
font. The final parameter lParam will be used to store the information that is passed through the user
defined parameter (lParam in the function ::EnumFontFamilies(…)).
Sample 9.2\GDI demonstrates how to enumerate all the valid fonts in the system. It is a standard SDI
application generated by Application Wizard. The application will display all the available fonts in the
client window after it is executed. Because there are many types of fonts, the view class of this application
is derived from CScrollView (This can be set in the final step of Application Wizard).
First, an int type array is declared in class CGDIView:
The first element of this array will be used to record the number of raster fonts in the system, the
second and the third elements will be used to store the number of vector and true type fonts respectively.
The array is initialized in the constructor as follows:
CGDIView::CGDIView()
{
m_nFontCount[0]=m_nFontCount[1]=m_nFontCount[2]=0;
}
We need to create the callback function in order to implement the enumeration. In the sample, a global
function ::EnumFontFamProc(…) is declared as follows (This function can also be declared as a static
member function):
230
Chapter 9. Font
We will use user-defined parameter lParam in the function ::EnumFontFamilies(…) to pass the
address of CGDIView::m_nFontCount into the callback function, so that we can fill the font’s information
into this array when the enumeration is undergoing. In the callback function, the address of CGDIView::
m_nFontCount is received by parameter pFontCount, which is then cast to an integer type pointer. The font
type is retrieved by examining parameter FontType, if the font is a raster font, the first element of
CGDIView:: m_nFontCount will be incremented; if the font is a true type font, the third element will be
incremented; in the rest case, the font must be a vector font, and the second element will be incremented.
The best place to implement the font enumeration is in function CView::OnInitialUpdate(), when the
view is first created. In the sample, a client DC is created and function ::EnumFontFamilies(…) is called.
When doing this, we pass the address of CGDIView::m_nFontCount as a user-defined parameter:
void CGDIView::OnInitialUpdate()
{
CClientDC dc(this);
CScrollView::OnInitialUpdate();
CSize sizeTotal=CSize(100, 80);
ASSERT(dc.GetSafeHdc());
::EnumFontFamilies
(
dc.GetSafeHdc(),
(LPCTSTR)NULL,
(FONTENUMPROC)EnumFontFamProc,
(LPARAM)m_nFontCount
);
SetScrollSizes(MM_TEXT, sizeTotal);
}
Still, we need to display the result in function CView::OnDraw(…). In the sample, this function is
implemented as follows:
CString szStr;
int nYPos;
nYPos=10;
szStr.Format("Number of raster fonts: %d", m_nFontCount[0]);
pDC->TextOut(10, nYPos, szStr);
nYPos+=20;
231
Chapter 9. Font
nYPos+=20;
}
We just display three lines of text indicating how many fonts are contained in the system for each
different font family.
Enumerating Font
Apart from the above information (how many fonts there are for each font family), we may further
want to know the exact properties of every font type (i.e., face name). To implement this, we need to
allocate enough memory to store the information of all fonts. Here, the size of this buffer depends on the
number of fonts whose properties are to be retrieved. Since each font need a LOGFONT structure to store all
its information, we can use the following formulae to calculate the required buffer size:
For this purpose, in the sample, another two variables are declared in class CGDIView as follows:
Array m_lpLf will be used to store LOGFONT information, and m_ptrFont will be used to store CFont
type variables. The variables are initialized in the constructor as follows:
CGDIView::CGDIView()
{
……
m_lpLf[0]=m_lpLf[1]=m_lpLf[2]=NULL;
m_ptrFont=NULL;
}
We need to provide another callback function to retrieve the actual information for each font type. In
the sample, this callback function is declared and implemented as follows:
232
Chapter 9. Font
return TRUE;
}
Three static variables are declared here to act as the counters for each type of fonts. When this function
is called, the information of the font is copied from lplf to the buffers allocated in CGDIView::
OnInitialUpdate(), whose address is passed through user-defined parameter.
In function CGDIView::OnInitialUpdate(), after the font families are enumerated, we need to allocate
enough memory, implement the enumeration again for every single type of font:
void CGDIView::OnInitialUpdate()
{
CClientDC dc(this);
CScrollView::OnInitialUpdate();
CSize sizeTotal=CSize(100, 80);
int i;
CFont *ptrFont;
ASSERT(dc.GetSafeHdc());
::EnumFontFamilies
(
dc.GetSafeHdc(),
(LPCTSTR)NULL,
(FONTENUMPROC)EnumFontFamProc,
(LPARAM)m_nFontCount
);
if(m_nFontCount[0] != 0)
{
m_lpLf[0]=(LPLOGFONT) new BYTE[sizeof(LOGFONT)*m_nFontCount[0]];
}
if(m_nFontCount[1] != 0)
{
m_lpLf[1]=(LPLOGFONT) new BYTE[sizeof(LOGFONT)*m_nFontCount[1]];
}
if(m_nFontCount[2] != 0)
{
m_lpLf[2]=(LPLOGFONT) new BYTE[sizeof(LOGFONT)*m_nFontCount[2]];
}
::EnumFontFamilies
(
dc.GetSafeHdc(),
(LPCTSTR)NULL,
(FONTENUMPROC)EnumFontProc,
(LPARAM)m_lpLf
);
m_ptrFont=new CFont[m_nFontCount[0]+m_nFontCount[1]+m_nFontCount[2]];
ptrFont=m_ptrFont;
for(i=0; i<m_nFontCount[0]; i++)
{
if((m_lpLf[0]+i)->lfHeight < 10)(m_lpLf[0]+i)->lfHeight=10;
ptrFont->CreateFontIndirect(m_lpLf[0]+i);
ptrFont++;
sizeTotal.cy+=(m_lpLf[0]+i)->lfHeight;
}
for(i=0; i<m_nFontCount[1]; i++)
{
if((m_lpLf[1]+i)->lfHeight < 10)(m_lpLf[1]+i)->lfHeight=10;
ptrFont->CreateFontIndirect(m_lpLf[1]+i);
ptrFont++;
sizeTotal.cy+=(m_lpLf[1]+i)->lfHeight;
}
for(i=0; i<m_nFontCount[2]; i++)
{
if((m_lpLf[2]+i)->lfHeight < 10)(m_lpLf[2]+i)->lfHeight=10;
ptrFont->CreateFontIndirect(m_lpLf[2]+i);
ptrFont++;
sizeTotal.cy+=(m_lpLf[2]+i)->lfHeight;
}
SetScrollSizes(MM_TEXT, sizeTotal);
}
After obtaining the information for each type of font, we create a font using this information by calling
function CFont::CreateFontIndirect(…). The addresses of these font objects are stored in array
CGDIView::m_ptrFont.
233
Chapter 9. Font
In function CGDIView::OnDraw(…), the face names of all fonts are output to the client window:
CString szStr;
CFont *ptrFtOld;
CFont *ptrFont;
int nYPos;
int i;
nYPos=10;
szStr.Format("Number of raster fonts: %d", m_nFontCount[0]);
pDC->TextOut(10, nYPos, szStr);
nYPos+=20;
ptrFont=m_ptrFont;
for(i=0; i<m_nFontCount[0]; i++)
{
ASSERT(ptrFont != NULL);
ASSERT(ptrFont->GetSafeHandle() != NULL);
ptrFtOld=pDC->SelectObject(ptrFont);
pDC->TextOut(10, nYPos, (m_lpLf[0]+i)->lfFaceName);
nYPos+=(m_lpLf[0]+i)->lfHeight;
pDC->SelectObject(ptrFtOld);
ptrFont++;
}
nYPos+=20;
For each font family, all the font face names are listed. Three loops are used for this purpose. Within
each loop, one of the enumerated font is selected into the target DC, and function CDC::TextOut(…) is
called to output the font’s face name to the window. To avoid text from overlapping one another, a local
variable nYPos is used as the vertical orgin of the output text, which will increment each time after a line of
text is output to the window.
Because the memory is allocated at the initialization stage, we need to free it when the application
exits. In the sample, WM_DESTROY message handler is added to class CGDIView through using Class Wizard,
and the corresponding member function is implemented as follows:
void CGDIView::OnDestroy()
{
CScrollView::OnDestroy();
234
Chapter 9. Font
The application is now ready to enumerate all the available fonts in the system.
Function CDC::ExtTextOut(…)
Usually we use function CDC::TextOut(…) to output text. There is another powerful function CDC::
ExtTextOut(…), which allows us to output the text to a specified rectange. We can use either transparent or
opaque drawing mode. In the latter case, we can also specify a background color. Besides this, we can set
the distances between neighboring characters of the text. One version of function CDC::ExtTextOut(…) has
the following format:
BOOL CDC::ExtTextOut
(
int x, int y,
UINT nOptions, LPCRECT lpRect, const CString &str, LPINT lpDxWidths
);
Like CDC::TextOut(…), the first two parameters x and y specify the position of the output text. The
third parameter nOptions indicates the drawing mode, it could be any type of combination between
ETO_CLIPPED and ETO_OPAQUE flags (Either flag bit can be set or not set, altogether there are four
possibilities). Style ETO_CLIPPED allows us to output text within a specified rectangle, and restrict the
drawing within the rectangle even if the size of the text is bigger than the rectangle. In this case, all interior
part of the rectangle not occupied by the text is treated as background. The third parameter is a CString
type value that specifies the actual text we want to output. The last parameter is a pointer to an array of
integers, which specify the distances between origins of two adjacent characters. This gives us the control
of placing each character within a text string to a specified place. If we pass NULL to this parameter, the
default spacing method will be applied.
A very typical use of this function is to implement a progress bar with percentage displayed in it
(Figure 9-2). The progress bar is divided into two parts. For one part the text color is white and the
background color is blue, for the other part the text color is blue and the background color is white.
New Class
Sample 9.3\GDI demonstrates how to implement this percentage bar. It is a standard SDI application
generated from Application Wizard.
First a new class CPercent is added to the application through using Class Wizard, this class will be
used to implement the percentage bar. Here, the base class is selected as CStatic.
The purpose of choosing CStatic as the base class is that by doing this, we can easily use subclass
method to change a static control contained in dialog box to a percentage bar. Of course, we can choose
other type of controls such as CButton to change a button to a percentage bar.
Two variables m_nRange and m_nCurPos along with two functions are added to class CPercent. Also,
WM_PAINT message handler is added to the class through using Class Wizard, and the corresponding
member funtion is OnPaint(). The following is this new class:
235
Chapter 9. Font
public:
virtual ~CPercent();
protected:
int m_nRange;
int m_nCurPos;
//{{AFX_MSG(CPercent)
afx_msg void OnPaint();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
Variable m_nRange indicates the range of the percentage bar, and m_nCurPos indicates the current
position. They are initialized in the constructor:
CPercent::CPercent()
{
m_nRange=100;
m_nCurPos=0;
}
void CPercent::OnPaint()
{
CPaintDC dc(this);
CRect rect;
CRect rectHalf;
CString szStr;
CSize sizeExtent;
COLORREF colorTextOld;
COLORREF colorBkOld;
if(m_nRange != 0)
{
GetClientRect(rect);
szStr.Format("%d%%", m_nCurPos*100/m_nRange);
colorTextOld=dc.SetTextColor(RGB(255, 255, 255));
colorBkOld=dc.SetBkColor(RGB(0, 0, 255));
sizeExtent=dc.GetTextExtent(szStr);
……
236
Chapter 9. Font
Next, the dimension of the left side rectange of the percentage bar is stored in local variable rectHalf.
Then function CDC::ExtTextOut(…) is called to draw the left part of the percentage bar (Mode
ETO_CLIPPED is used here, it will restrict the drawing within the rectangle). Because ETO_OPAQUE flag is also
used, the text will be drawn with white color and the rest part of rectangle (specified by rectHalf) will all
be painted blue:
……
sizeExtent=dc.GetTextExtent(szStr);
rectHalf=rect;
rectHalf.right=rect.Width()*m_nCurPos/m_nRange;
dc.ExtTextOut
(
(rect.Width()-sizeExtent.cx)/2,
(rect.Height()-sizeExtent.cy)/2,
ETO_OPAQUE|ETO_CLIPPED,
rectHalf,
szStr,
NULL
);
……
Then we swap the text and background colors, store the right side rectangle in variable rectHalf, and
call CDC::ExtTextOut(…) again to draw the rest part of the percentage bar:
……
rectHalf.left=rectHalf.right+1;
rectHalf.right=rect.right;
dc.SetTextColor(RGB(0, 0, 255));
dc.SetBkColor(RGB(255, 255, 255));
dc.ExtTextOut
(
(rect.Width()-sizeExtent.cx)/2,
(rect.Height()-sizeExtent.cy)/2,
ETO_OPAQUE|ETO_CLIPPED,
rectHalf,
szStr,
NULL
);
dc.SetTextColor(colorTextOld);
dc.SetBkColor(colorBkOld);
}
}
The last two statements resume the original text color and background color for the device context.
237
Chapter 9. Font
Frame border
is gray
Variable m_perBar will be used to implement percentange bar, and m_nPercent will be used to record
the current position of the percentage bar.
Variable m_nPercent is initialized in the constructor:
In the sample, WM_INITDIALOG message handler is added to the application through using Class Wizard,
and funtion CProgDlg::OnInitDialog() is implemented as follows:
BOOL CProgDlg::OnInitDialog()
{
CDialog::OnInitDialog();
m_perBar.SubclassDlgItem(IDC_STATIC_PROG, this);
SetTimer(TIMER_ID, 100, NULL);
return TRUE;
}
Control IDC_STATIC_PROG is changed to a progress bar through implementing subclass, then a timer is
started to generate events that will be handled to advance the percentage bar.
To handle time out events, in the sample, a WM_TIMER message handler is added throgh using Class
Wizard. The corresponding member function CProgDlg::OnTimer(…) is implemented as follows:
If timer times out, we advance the percentage bar one step forward (1%); if the percentage bar reaches
100%, we reset it to 0%.
We must destroy timer when the application exits. The best place of doing this is when we receive
WM_DESTROY message. This message handler can also be added through using Class Wizard. In the sample,
the corresponding member function is implemented as follows:
238
Chapter 9. Font
void CProgDlg::OnDestroy()
{
CDialog::OnDestroy();
KillTimer(TIMER_ID);
}
For the purpose of testing the percentage bar, a new command Dialog | Progress is added to the
application, whose command ID is ID_DIALOG_PROGRESS. A WM_COMMAND message handler is added to class
CGDIDoc for this command, and the corresponding member function CGDIDoc::OnDialogProgress() is
implemneted as follows:
void CGDIDoc::OnDialogProgress()
{
CProgDlg dlg;
dlg.DoModal();
}
After all these implementations, we can execute command Dialog | Progress to test the percentage bar.
public:
CString GetText(){return m_szText;}
CFont *GetFont(){return &m_ftDraw;}
//{{AFX_VIRTUAL(CGDIDoc)
public:
……
};
Variable m_szText will be used to store the text string, and m_ftDraw will be used to store the font used
for text drawing. Functions CGDIDoc::GetText() and CGDIDoc::GetFont() provide a way of accessing the
two member variables outside class CGDIDoc.
Because we still do not have an interactive input environment, in the constructor, variable m_szText is
initialized to a fixed string:
CGDIDoc::CGDIDoc()
{
m_szText="This is just a test string";
}
239
Chapter 9. Font
BOOL CGDIDoc::OnNewDocument()
{
CClientDC dc(NULL);
LOGFONT lf;
CFont *ptrFont;
if(!CDocument::OnNewDocument())return FALSE;
ptrFont=dc.GetCurrentFont();
ptrFont->GetLogFont(&lf);
VERIFY(m_ftDraw.CreateFontIndirect(&lf));
return TRUE;
}
This function will be called when the document is initialized. Within the function, variable m_ftDraw is
used to create a default font. Since the document is always created before the view, creating the font in this
function will guarantee that m_ftDraw will be a valid font when the view is created. This procedure can also
be done in the constructor.
To let the user select different types of fonts, a new command Dialog | Font is added to application’s
mainframe menu IDR_MAINFRAME. The resource ID of this command is ID_DIALOG_FONT and the
corresponding message handler is CGDIDoc::OnDialogFont(), which is implemented as follows:
void CGDIDoc::OnDialogFont()
{
CFontDialog dlg;
LOGFONT lf;
if(dlg.DoModal() == IDOK)
{
dlg.GetCurrentFont(&lf);
m_ftDraw.DeleteObject();
VERIFY(m_ftDraw.CreateFontIndirect(&lf));
UpdateAllViews(NULL);
}
}
After a new font is selected by the user, we delete the old font and create a new one, then call function
CDocument::UpdateAllViews(…) to update the client window.
On the view side, we need to modify function CGDIView::OnDraw(…). In this function, the text string
and the font are retrieved from the document, and are used to draw text in the client window:
CGDIDoc *pDoc=GetDocument();
ASSERT_VALID(pDoc);
szText=pDoc->GetText();
ptrFontOld=pDC->SelectObject(pDoc->GetFont());
pDC->TextOut(0, 0, szText);
pDC->SelectObject(ptrFontOld);
}
With the above implementation, the application will display a static string. Although we still can not
input any character, the font for drawing the text can be changed through executing Dialog | Font
command.
Caret Functions
Caret is a very important feature for text editor, it indicates the current editing position. This makes the
interface more user friendly. Because there are many types of fonts in the system, and for each font, the
240
Chapter 9. Font
width of different characters may vary, we need to make careful calculation before moving the caret to the
next position.
Every class derived from the CWnd supports caret, the steps of implementing caret are as follows: 1)
Create a caret with specific style. 2) Show the caret. 3) Destroy the caret before the window is destroyed.
The following three member functions can be used to create a caret:
The first member function allows us to create a solid caret, here parameters nWidth and nHeight
specify the dimension of the caret. Similarly, the second function can be used to create a gray caret. The
last function can create a caret from a bitmap so that the caret can have a custom pattern.
After the caret is created, we can call function CWnd::ShowCaret() to display the caret or call function
CWnd::HideCaret() to hide the caret.
The difficult thing on managing caret is to set its position. Because every character may have a
different width, when the user presses arrow keys, we can not advance the caret with fixed distance each
time. We must move the caret forward or backward according to the width of the character located before
(or after) the caret. In order to do this, we can either calculate the new caret position each time, or we can
store the starting position of each character in a table and obtain the caret position from it whenever the
caret needs to be moved. In the sample, the latter solution is used.
Sample
Sample 9.5\GDI demonstrates how to implemnt caret. It is based on sample 9.4\GDI.
First, some new variables and functions are added to class CGDIDoc for caret implementation:
public:
CString GetText(){return m_szText;}
CFont *GetFont(){return &m_ftDraw;}
void ForwardCaret();
void BackwardCaret();
POINT GetCaretPosition();
int GetCaretVerSize(){return m_nCaretVerSize;}
CGDIView *CGDIDoc::GetCGDIView();
//{{AFX_VIRTUAL(CGDIDoc)
public:
……
};
Two variables m_nCaretIndex and m_nCaretVerSize are added. The first variable is the index
indicating the position of the caret. The second variable is the caret’s vertical size. This is necessary
because for fonts with different sizes, we need to create different carets, whose vertical size should be the
same with the current font’s height.
Five functions are added to the class, among them, function CGDIDoc::GetCaretVerSize() provides
us a way of obtaining the current caret’s vertical size in class CGDIView; and function CGDIDoc::
GetCaretPosition() converts the caret index to a position within the client window (The function returns
a POINT type value). It is implemented as follows:
POINT CGDIDoc::GetCaretPosition()
{
POINT pt;
CClientDC dc(NULL);
CFont *ptrFtOld;
CString szStr;
241
Chapter 9. Font
CSize sizeExtent;
ASSERT(m_ftDraw.GetSafeHandle());
ptrFtOld=dc.SelectObject(&m_ftDraw);
szStr=m_szText.Left(m_nCaretIndex);
sizeExtent=dc.GetTextExtent(szStr);
dc.SelectObject(ptrFtOld);
pt.x=sizeExtent.cx;
pt.y=0;
return pt;
}
The caret position is calculated through using function CDC::GetTextExtent(…), which will return the
vertical and a horizontal size of a text string. We need a DC to select the current font in order to calculate
the text dimension. In the sample, first a DC that does not belong to any window is created, then the current
font is selected into this DC, and function CString::Left() is called to obtain a sub-string whose last
character is located at the current caret position. The horizontal size obtained from function CDC::
GetTextExtent(…) for the sub-string is the caret’s horizontal position. Because we have only one line text,
the vertical position of the caret is always 0.
This function may be called within the member functions of CGDIView to retrieve the current caret
position. The other two functions, CGDIDoc::ForwardCaret() and CGDIDoc::BackwardCaret() can be
called to move the caret forward or backward. They are implemented as follows:
void CGDIDoc::ForwardCaret()
{
m_nCaretIndex++;
if(m_nCaretIndex > m_szText.GetLength())
{
m_nCaretIndex=m_szText.GetLength();
}
GetCGDIView()->RedrawCaret();
}
void CGDIDoc::BackwardCaret()
{
m_nCaretIndex--;
if(m_nCaretIndex < 0)
{
m_nCaretIndex=0;
}
GetCGDIView()->RedrawCaret();
}
Instead of calculating the actual position of the caret, we just increment or decrement the caret index.
The range of this index is from 0 to the total number of characters (If there are five characters, we have six
possible positions for displaying the caret). If the index goes beyond the limit, we set it back to the
boundary value.
At the end of above two functions, function CGDIDoc::GetCGDIView() is called to access class
CGDIView, then CGDIView::RedrawCaret() is called to update the caret. This will cause the caret to be
displayed in a new position. To access a view from the document, we need to call function CDocument::
GetFirstViewPosition() and then call CDocument::GetNextView(…) repeatedly until we get the correct
view. For an SDI application, we need to call this function only once. However, some applications may
have more than one view attached to the document (Like an MDI application). In this case, we need to use
RUNTIME_CLASS macro to judge if the class is the one we are looking for. In the sample, CGDIDoc::
GetCGDIView() is implemented as a general function, it can also be used in an MDI application to obtain a
specific view from the document. The following is its implementation:
CGDIView *CGDIDoc::GetCGDIView()
{
POSITION pos;
CGDIView *ptrView;
pos=GetFirstViewPosition();
do
{
ptrView=(CGDIView *)GetNextView(pos);
}while(!ptrView->IsKindOf(RUNTIME_CLASS(CGDIView)));
return ptrView;
242
Chapter 9. Font
CGDIDoc::CGDIDoc()
{
m_szText="This is just a test string";
m_nCaretIndex=0;
}
Also, when the document is first created or when a new font is selected, we need to update the value of
m_nCaretVerSize, so functions CGDIDoc::OnNewDocument() and CGDIDoc::OnDialogFont() are updated
as follows:
BOOL CGDIDoc::OnNewDocument()
{
CClientDC dc(NULL);
LOGFONT lf;
CFont *ptrFont;
if(!CDocument::OnNewDocument())return FALSE;
ptrFont=dc.GetCurrentFont();
ptrFont->GetLogFont(&lf);
VERIFY(m_ftDraw.CreateFontIndirect(&lf));
return TRUE;
}
BOOL CGDIDoc::OnNewDocument()
{
CClientDC dc(NULL);
LOGFONT lf;
CFont *ptrFont;
TEXTMETRIC tm;
if(!CDocument::OnNewDocument())return FALSE;
ptrFont=dc.GetCurrentFont();
ptrFont->GetLogFont(&lf);
VERIFY(m_ftDraw.CreateFontIndirect(&lf));
dc.GetOutputTextMetrics(&tm);
m_nCaretVerSize=tm.tmHeight;
return TRUE;
}
void CGDIDoc::OnDialogFont()
{
CFontDialog dlg;
LOGFONT lf;
if(dlg.DoModal() == IDOK)
{
dlg.GetCurrentFont(&lf);
m_ftDraw.DeleteObject();
VERIFY(m_ftDraw.CreateFontIndirect(&lf));
UpdateAllViews(NULL);
}
}
void CGDIDoc::OnDialogFont()
{
CFontDialog dlg;
LOGFONT lf;
243
Chapter 9. Font
TEXTMETRIC tm;
CClientDC dc(NULL);
CFont *ptrFtOld;
if(dlg.DoModal() == IDOK)
{
dlg.GetCurrentFont(&lf);
m_ftDraw.DeleteObject();
VERIFY(m_ftDraw.CreateFontIndirect(&lf));
ptrFtOld=dc.SelectObject(&m_ftDraw);
dc.GetOutputTextMetrics(&tm);
dc.SelectObject(ptrFtOld);
m_nCaretVerSize=tm.tmHeight;
GetCGDIView()->CreateNewCaret(TRUE);
UpdateAllViews(NULL);
}
}
Function CGDIView::RedrawCaret() will erase the current caret and draw it at the new position.
Function CGDIView::CreateNewCaret() will create a new caret and destroy the old one if necessary. The
following code fragment shows their implementations:
void CGDIView::RedrawCaret()
{
CGDIDoc *ptrDoc;
ptrDoc=(CGDIDoc *)GetDocument();
HideCaret();
SetCaretPos(ptrDoc->GetCaretPosition());
ShowCaret();
}
if(bDestroy == TRUE)::DestroyCaret();
ptrDoc=(CGDIDoc *)GetDocument();
CreateSolidCaret(0, ptrDoc->GetCaretVerSize());
SetCaretPos(ptrDoc->GetCaretPosition());
ShowCaret();
}
In both functions, we retrieve the caret position from the document. Before moving the caret, we first
call function CWnd::HideCaret() to hide the caret. After setting the new position, we call
CWnd::ShowCaret() to show the caret again. Also, function CWnd::CreateSolidCaret(…) is called to
create the caret, since we pass 0 to its horizontal dimension, the horizontal size of the caret will be set to the
default size. The vertical size is retrieved from the document.
We need to create the caret once the view is created, so the default function CGDIView::
OnInitialUpdate() is modified as follows:
244
Chapter 9. Font
void CGDIView::OnInitialUpdate()
{
CScrollView::OnInitialUpdate();
CSize sizeTotal;
sizeTotal.cx=sizeTotal.cy=100;
SetScrollSizes(MM_TEXT, sizeTotal);
CreateNewCaret(FALSE);
}
Here we just call function CGDIView::CreateNewCaret(…) and pass a FALSE value to its parameter
because there is no caret needs to be destroyed.
Now we must respond to the events of left arrow and right arrow key strokes. As we know, when a key
is pressed, the system will send WM_KEYDOWN message to the application, with the key code stored in WPARAM
parameter. Under Windows, all keys are defined as virtual keys, so there is no need for us to check the
actual code sent from the keyboard. In order to know which key was pressed, we can examine WPARAM
parameter after WM_KEYDOWN message is received. Here, the virtual key code of the left arrow key and right
arrow key are VK_LEFT and VK_RIGHT respectively.
In the sample, WM_KEYDOWN message handler is added to class CGDIView through using Class Wizard,
and the corresponding function CGDIView::OnKeyDown(…) is implemented as follows:
ptrDoc=(CGDIDoc *)GetDocument();
ASSERT(ptrDoc);
switch(nChar)
{
case VK_LEFT:
{
ptrDoc->BackwardCaret();
break;
}
case VK_RIGHT:
{
ptrDoc->ForwardCaret();
break;
}
}
CScrollView::OnKeyDown(nChar, nRepCnt, nFlags);
}
In this function, WPARAM parameter is mapped to nChar parameter. If the key stroke is from left arrow
key, we call CGDIDoc::BackwardCaret() to move the caret leftward. If the key stroke is from right arrow
key, we call CGDIDoc::ForwardCaret() to move the caret rightward.
245
Chapter 9. Font
Function CGDIDoc::AddChar(…) allows us to insert characters to the string at the position indicated by
the caret, and function CGDIDoc::DeleteChar(…) allows us to delete the character before or after the caret.
Let’s first take a look at the implementation of function CGDIDoc::AddChar(…):
szStr=m_szText;
m_szText=m_szText.Left(m_nCaretIndex);
szStr=szStr.Right(szStr.GetLength()-m_nCaretIndex);
for(i=0; i<uRepCnt; i++)m_szText+=(TCHAR)uChar;
m_szText+=szStr;
UpdateAllViews(NULL);
ForwardCaret();
}
We divide the text string into two parts, the first part is the sub-string before the caret, and the second
part is the sub-string after the caret. The new characters are inserted between the two sub-strings. Parameter
uChar indicates the new character, and uRepCnt specifies how many characters will be added. After the
character is added, we update the view and move the caret forward.
For function CGDIDoc::DeleteChar(…), it can be used for two situations: one corresponds to “BACK
SPACE” key stroke, the other corresponds to “DELETE” key stroke. If parameter bBefore is true, the
character before the current caret should be deleted. Otherwise, the character after it needs to be deleted.
The following is the implementation of function CGDIDoc::DeleteChar(…):
if(bBefore == TRUE)
{
if(m_nCaretIndex != 0)
{
szStr=m_szText;
m_szText=m_szText.Left(m_nCaretIndex-1);
szStr=szStr.Right(szStr.GetLength()-m_nCaretIndex);
m_szText+=szStr;
UpdateAllViews(NULL);
BackwardCaret();
}
}
else
{
if(m_nCaretIndex != m_szText.GetLength())
{
szStr=m_szText;
m_szText=m_szText.Left(m_nCaretIndex);
szStr=szStr.Right(szStr.GetLength()-m_nCaretIndex-1);
m_szText+=szStr;
UpdateAllViews(NULL);
}
}
}
To delete the character before the current caret, we divide the text into two sub-strings, delete the last
character of the first sub-string, and re-combine them. Then we update the view, and move caret one
character left. When deleting the character after the caret, we do not need to change the position of the
caret.
Message WM_CHAR
Now we need to use the above two member functions. In the sample, message WM_CHAR is handled to
implement keyboard input. The difference between WM_CHAR and WM_KEYDOWN messages is that WM_CHAR is
sent only for printable characters along with the following five keys: ESCAPE, TAB, BACK SPACE and
ENTER. Message WM_KEYDOWN will be sent for all types of key strokes.
246
Chapter 9. Font
ptrDoc=(CGDIDoc *)GetDocument();
if(nChar != VK_RETURN && nChar != VK_ESCAPE && nChar != VK_TAB)
{
if(nChar == VK_BACK)ptrDoc->DeleteChar(TRUE);
else ptrDoc->AddChar(nChar, nRepCnt);
}
CScrollView::OnChar(nChar, nRepCnt, nFlags);
}
We neglect the ENTER, TAB and ESCAPE key strokes. For BACK SPACE key stroke, we delete the
character before the current caret. For all other cases, we insert character at the current caret position.
The DELETE key stroke can not be detected by this message handler, we need to trap and handle it in
function CGDIView::OnKeyDown(…):
Of course the printable key strokes will also be detected by this message handler. However, if we
handle character input in this function, we need to first check if the character is printable. This will make
the program a little bit complex.
9.7 One Line Text Editor, Step 4: Caret Moving & Cursor Shape
Sample 9.7\GDI is based on sample 9.6\GDI.
New Functions
Besides moving the caret before or after one character at a time, we sometimes need to move the caret
to the next or the previous word. This will give the user a faster way of putting the caret at the appropriate
position. The method of moving caret to the next or previous word is almost the same with moving it to the
next or previous character, the only difference between them is how to calculate the new caret position.
Because words are separated by blanks, if we want to move caret one word leftward or rightward, we can
just find the previous or next blank, calculate the distance, then move the caret to the new position.
Also, we may want to let the user use HOME key and END key to move the caret to the beginning or
the end of the text. Still, almost every text editor supports changing the caret position with a single mouse
clicking.
The following new functions are declared in class CGDIDoc to implement above-mentioned
functionalities:
247
Chapter 9. Font
void HomeCaret();
void EndCaret();
void ForwardCaretToBlank();
void BackwardCaretToBlank();
void SetCaret(CPoint);
……
}
As implied by the function names, CGDIDoc::HomeCaret() will move the caret to the beginning of the
text, CGDIDoc::EndCaret() will move the caret to the end of the text. The implementation of these two
functions is very simple, all we need to do is setting m_nCaretIndex to a proper value then updating the
caret:
void CGDIDoc::HomeCaret()
{
m_nCaretIndex=0;
GetCGDIView()->RedrawCaret();
}
void CGDIDoc::EndCaret()
{
m_nCaretIndex=m_szText.GetLength();
GetCGDIView()->RedrawCaret();
}
void CGDIDoc::ForwardCaretToBlank()
{
CString szSub;
int nFwd;
szSub=m_szText.Right(m_szText.GetLength()-m_nCaretIndex);
nFwd=szSub.Find(' ');
if(nFwd == -1)EndCaret();
else
{
m_nCaretIndex+=nFwd+1;
GetCGDIView()->RedrawCaret();
}
}
void CGDIDoc::BackwardCaretToBlank()
{
CString szSub;
int nBkd;
szSub=m_szText.Left(m_nCaretIndex-1);
nBkd=szSub.ReverseFind(' ');
if(nBkd == -1)HomeCaret();
else
{
m_nCaretIndex-=szSub.GetLength()-nBkd;
GetCGDIView()->RedrawCaret();
}
}
Within the two functions, a local variable szSub is used to store the sub-string before or after the caret,
and function CString::Find(…) or CString::ReverseFind(…) is called to find the position of the nearest
blank. In case the blank is not found, we need to move the caret to the beginning or end of the text. If it is
found, we just increment or decrement m_nCaretIndex by an appropriate value.
On the view side, we need to move the caret when any of the following keys is pressed together with
CTRL: HOME, END, Left and Right ARROW. These keystroke events can be trapped by handling
WM_KEYDOWN message. To detect if the CTRL key is held down, we can call API function
::GetKeyState(…) to check the key state.
Function ::GetKeyState(…) can be used to check the current state of any key. We need to pass the
virtual key code (such as VK_CONTROL, VK_SHIFT…) to this function when making the call. The returned
value is a SHORT type integer. The high order of this value indicates if the key is held down, the lower order
indicates if the key is toggled, which is applicable to keys such as CAPS LOCK, NUM LOCK or INSERT.
248
Chapter 9. Font
ptrDoc=(CGDIDoc *)GetDocument();
ASSERT(ptrDoc);
switch(nChar)
{
case VK_LEFT:
{
nKeyState=GetKeyState(VK_CONTROL);
if(HIBYTE(nKeyState) != 0)
{
ptrDoc->BackwardCaretToBlank();
}
else ptrDoc->BackwardCaret();
break;
}
case VK_RIGHT:
{
nKeyState=GetKeyState(VK_CONTROL);
if(HIBYTE(nKeyState) != 0)
{
ptrDoc->ForwardCaretToBlank();
}
else ptrDoc->ForwardCaret();
break;
}
case VK_END:
{
ptrDoc->EndCaret();
break;
}
case VK_HOME:
{
ptrDoc->HomeCaret();
break;
}
……
}
Changes are made to VK_LEFT and VK_RIGHT cases. First we call ::GetKeyState(…) using virtual key
code VK_CONTROL and extract the high order byte from the return value. If it is non-zero, function
CGDIDoc:: BackwardCaretToBlank() or CGDIDoc::ForwardCaretToBlank() is called to move the caret to
the nearest blank. Other wise we move the caret one character leftward or rightward.
In case the key is VK_END or VK_HOME, we call function CGDIDoc::EndCaret() or CGDIDoc
::HomeCaret() to move the caret to the beginning or end of the text.
249
Chapter 9. Font
CSize sizeExtent;
CClientDC dc(NULL);
ptrFtOld=dc.SelectObject(&m_ftDraw);
nStrLen=m_szText.GetLength();
nIndex=0;
nFinalDist=point.x;
for(i=1; i<=nStrLen; i++)
{
szStr=m_szText.Left(i);
sizeExtent=dc.GetTextExtent(szStr);
nDist=abs(point.x-sizeExtent.cx);
if(nDist < nFinalDist)
{
nFinalDist=nDist;
nIndex=i;
}
}
dc.SelectObject(ptrFtOld);
m_nCaretIndex=nIndex;
GetCGDIView()->RedrawCaret();
}
In order to find out all the possible caret positions, we need to obtain the dimension of different sub-
strings. For example, the caret positions of text “abcde” can be calculated from the dimensions of following
sub-strings: “a”, “ab”, “abc”, “abcd”, “abcde”. In the above function, first a DC that does not belong to any
window is created, then the current font is selected into this DC, and function CDC::GetTextExtent(…) is
called to obtain the dimension of each sub-string. Because we have only one line text, only the horizontal
size is meaningful to us.
A loop is implemented to do the comparison. For the nth loop, we create a sub-string that contains
text’s first character to nth character, obtain its dimension, and calculate the distance from the position of
the last character of the sub-string to the current position of mouse cursor. After the loop finishes, we
choose the smallest distance and set m_nCaretIndex to the corresponding caret index.
Cursor Shape
For a text editor, when the mouse is over its editable area, usually the mouse cursor should be changed
to an insertion cursor. This indicates that the user can input text at this time. This feature can also be
implemented in our sample application.
To set mouse cursor’s shape, we need to call API function ::SetCursor(…). The input parameter to
this function is an HCURSOR type handle.
A cursor can be prepared as a resource and then be loaded before being used. After the cursor is
loaded, we can pass its handle to function ::SetCursor(…) to change the current cursor shape. Besides the
cursor prepared by the user, there also exist some standard cursors that can be loaded directly.
We can call function CWinApp::LoadCursor(…) to load a user-defined cursor, and call function
CWinApp::LoadStandardCursor(…) to load a standard cursor. The following table lists some of the
standard cursors:
Cursor ID Meaning
IDC_ARROW Arrow cursor
IDC_IBEAM Text-insertion cursor
IDC_WAIT Hourglass cursor
IDC_CROSS Cross-hair cursor
In the sample, an HCURSOR type variable is declared in class CGDIView, and the insertion cursor is
loaded in function CGDIView::OnInitialUpdate():
250
Chapter 9. Font
void CGDIView::OnInitialUpdate()
{
……
m_hCur=AfxGetApp()->LoadStandardCursor(IDC_IBEAM);
}
We need to respond to WM_SETCURSOR message in order to change the cursor shape. By handling this
message, we have a chance to customize the default cursor when it is within the client window of the
application. Upon receiving this message, we can check if the mouse position is over the editable text. If so,
we need to change the cursor by calling API function ::SetCursor(…). In this case, we need to return a
TRUE value and should not call the default message handler. If the cursor should not be changed, we need
to call the default message handler to let the cursor be set as usual.
To check out if the cursor is over editable text, we need to know the text dimension all the time. In the
previous steps, we already have a variable CGDIDoc::m_nCaretVerSize that is used to store the vertical size
of the caret (also the text), so here we just need another variable to store the horizontal size of the text. In
the sample, a new variable n_nTextHorSize is declared in class CGDIDoc for this purpose:
public:
……
int GetCaretVerSize(){return m_nCaretVerSize;}
int GetTextHorSize(){return m_nTextHorSize;}
}
Besides the new variable, function CGDIDoc::GetTextHorSize() is also added, which lets us access
the value of CGDIDoc::m_nTextHorSize outside class CGDIDoc.
We need to set the value of m_nTextHorSize when the document is first initialized and when the font
size is changed (In the following two functions, m_nTextHorSize is assigned a new value):
BOOL CGDIDoc::OnNewDocument()
{
……
sizeExtent=dc.GetTextExtent(m_szText);
m_nCaretVerSize=sizeExtent.cy;
m_nTextHorSize=sizeExtent.cx;
}
void CGDIDoc::OnDialogFont()
{
……
if(dlg.DoModal() == IDOK)
{
……
sizeExtent=dc.GetTextExtent(m_szText);
dc.SelectObject(ptrFtOld);
m_nCaretVerSize=sizeExtent.cy;
m_nTextHorSize=sizeExtent.cx;
……
}
}
Message handler of WM_SETCURSOR can be added through using Class Wizard. The corresponding
function CGDIView::OnSetCursor(…) is implemented as follows:
::GetCursorPos(&pt);
ScreenToClient(&pt);
ptrDoc=(CGDIDoc *)GetDocument();
rect.right=ptrDoc->GetTextHorSize();
251
Chapter 9. Font
rect.bottom=ptrDoc->GetCaretVerSize();
if(rect.PtInRect(pt) == TRUE)
{
::SetCursor(m_hCur);
return TRUE;
}
return CScrollView::OnSetCursor(pWnd, nHitTest, message);
}
ptrDoc=(CGDIDoc *)GetDocument();
if(::GetCursor() == m_hCur)ptrDoc->SetCaret(point);
CScrollView::OnLButtonDown(nFlags, point);
}
In this function, first we check if the mouse cursor is the insertion cursor. If not, it means that the
mouse is not over the text string. If so, we call function CGDIDoc::SetCaret(…) and pass current mouse
position to it. This will cause the caret to move to the new position.
public:
……
int GetTextHorSize(){return m_nTextHorSize;}
int GetSelIndexBgn(){return m_nSelIndexBgn;}
int GetSelIndexEnd(){return m_nSelIndexEnd;}
……
}
252
Chapter 9. Font
CGDIDoc::CGDIDoc()
{
……
m_nCaretIndex=0;
m_nSelIndexBgn=-1;
m_nSelIndexEnd=-1;
}
CGDIDoc *pDoc=GetDocument();
ASSERT_VALID(pDoc);
szText=pDoc->GetText();
ptrFontOld=pDC->SelectObject(pDoc->GetFont());
nSelIndexBgn=pDoc->GetSelIndexBgn();
nSelIndexEnd=pDoc->GetSelIndexEnd();
if
(
nSelIndexBgn == -1 ||
nSelIndexEnd == -1 ||
(nSelIndexBgn == nSelIndexEnd)
)
{
pDC->TextOut(0, 0, szText);
}
else
{
if(nSelIndexEnd < nSelIndexBgn)
{
nSel=nSelIndexBgn;
nSelIndexBgn=nSelIndexEnd;
nSelIndexEnd=nSel;
}
uTextAlign=pDC->SetTextAlign(TA_UPDATECP);
szStr=szText.Left(nSelIndexBgn);
pDC->TextOut(0, 0, szStr);
colorFg=pDC->GetTextColor();
colorBk=pDC->GetBkColor();
pDC->SetTextColor(colorBk);
pDC->SetBkColor(colorFg);
szStr=szText.Mid(nSelIndexBgn, nSelIndexEnd-nSelIndexBgn);
pDC->TextOut(0, 0, szStr);
pDC->SetTextColor(colorFg);
pDC->SetBkColor(colorBk);
szStr=szText.Mid(nSelIndexEnd);
pDC->TextOut(0, 0, szStr);
pDC->SetTextAlign(uTextAlign);
}
pDC->SelectObject(ptrFontOld);
}
Two local variables nSelIndexBgn and nSelIndexEnd are declared here, they are used to store the
values of CGDIDoc::m_nSelIndexBgn and CGDIDoc::m_nSelIndexEnd retrieved from the document. If the
253
Chapter 9. Font
value of CGDIDoc::m_nSelIndexEnd is less than the value of CGDIDoc::m_nSelIndexBgn (In this case, the
selection is made from right to left), we need to swap their values.
If there is no currently selected text, we simply call CDC::TextOut(…) as usual to output the plain text.
Otherwise we swap the two indices if m_nSelIndexEnd is less than m_nSelIndexBgn, and set the text
alignment by calling function CDC::SetTextAlign(…) using TA_UPDATECP flag. This will cause the output
origin to be updated to the end of the text after each CDC::TextOut(…) call. If we do not set this alignment,
the coordinates passed to function CDC::TextOut(…) indicate a position relative to the upper-left corner of
the window. With TA_UPDATECP alignment style, when we call function CDC::TextOut(…), the coordinates
passed to this function will be interpreted as a position relative to the new origin (which is the end of the
text that is output by function CDC::TextOut(…) last time). This is very useful if we want to output several
segments of strings. In the sample, the old alignment flag is stored in variable uTextAlign, and is restored
after the text is output.
We divide the text string into three segments: the first segment starts from the beginning of the text and
ends at the beginning of the selection. We output this sub-string using normal text and background colors.
The second segment is the selected portion, before drawing this sub-string we need to swap the text and
background colors so that the selected part will be drawn highlighted. The rest part is the third sub-string,
which is also drawn using the normal text and background colors. Each time function CDC::TextOut(…) is
called, the output coordinates are specified as (0, 0). If the alignment flag is not set to TA_UPDATECP, the
three segments will all be drawn starting from the same origin.
254
Chapter 9. Font
GetCGDIView()->RedrawCaret();
}
We want to use this function to set both m_nSelIndexBgn and m_nSelIndexEnd, so that it can be called
in response to any of the three mouse messages. If the value of m_nSelIndexBgn is -1, it means there is no
currently selected text. In this case, we need to update m_nSelIndexBgn (This is the situation that the left
button of the mouse is pressed down). In other cases (If m_nSelIndexBgn is not -1, the function must be
called in response to mouse moving or left button up event), we need to update the value of
m_nSelIndexEnd, then update the client window.
Old Version:
ptrDoc=(CGDIDoc *)GetDocument();
if(::GetCursor() == m_hCur)ptrDoc->SetCaret(point);
CScrollView::OnLButtonDown(nFlags, point);
}
New Version:
ptrDoc=(CGDIDoc *)GetDocument();
ptrDoc->ResetSelection();
if(::GetCursor() == m_hCur)ptrDoc->SetCaret(point);
CScrollView::OnLButtonDown(nFlags, point);
}
The only thing added to this function is resetting the selection indices stored in the document. Other
changes are implemented in the updated function CGDIView::SetCaret(…).
Two other message handlers for WM_LBUTTONUP and WM_MOUSEMOVE are added through using Class
Wizard. The corresponding functions are CGDIView::OnLButtonUp(…) and CGDIView::OnMouseMove(…)
respectively.
Function CGDIView::OnMouseMove(…) is implemented as follows:
We find out if the left button is held down when the mouse is moving by checking MK_LBUTTON bit of
parameter nFlags. If so, function CGDIDoc::SetCaret(…) is called to set the selection and update the client
window. Function CGDIView::OnLButtonUp(…) is implemented exactly the same except that we don’t need
to check the status of left button here:
ptrDoc=(CGDIDoc *)GetDocument();
if(::GetCursor() == m_hCur)ptrDoc->SetCaret(point);
255
Chapter 9. Font
CScrollView::OnLButtonUp(nFlags, point);
}
With the above knowledge, it is very easy for us to implement selection by using left/right ARROW
key when SHIFT key is held down. To implement this, we need to check SHIFT key’s status when either
left or right ARROW key is pressed. If it is held, we can update the selection indices and redraw the client
window.
9.9 One Line Text Editor, Step 6: Cut, Copy and Paste
Global Memory
Cut, copy and paste are supported almost by every application. They provide a way to exchange data
among different applications. We must use globally shared data to implement clipboard data transfer. Once
we send some data to the clipboard, it becomes public and can be accessed by all the programs. Any
process in the system can clear the clipboard.
Because of this, we must allocate global memory to store our data in the clipboard. In Windows
programming, the following API functions can be used to manage global memory:
Global memory is also managed through using handle. Unlike memory allocated using new key word,
function ::GlobalAlloc(…) returns an HGLOBAL type handle to the allocated memory block if we allocate
non-fixed global memory.
Generally, before accessing a non-fixed block of global memory, we must lock it by calling function
::GlobalLock(…), which will return the address of the memory block. After reading from or writing to this
memory block, we need to call function ::GlobalUnlock(…) to unlock the memory again. We can free a
block of global memory by calling function ::GlobalFree(…) (We can not free a block of memory when it
is being locked).
We can also allocate fixed global memory, in which case the address of the memory will be returned
by function ::GlobalAlloc(…) directly, and we do not need lock or unlock operation in order to access the
memory.
Parameter nFlags of function ::GlobalAlloc(…) specifies how the memory will be allocated. There
are many possible choices. For example, we can make the memory movable or fixed, and fill all the buffers
with zero. The following is a list of some commonly used flags:
Flag Meaning
GMEM_FIXED Allocate fixed global memory
GMEM_MOVEABLE Allocate movable global memory
GMEM_ZEROINIT Initialize all the buffers to zero
GMEM_SHARE Used for clipboard implementation or DDE
GHND Same with GMEM_MOVEABLE | GMEM_ZEROINIT
GPTR Same with GMEM_FIXED | GMEM_ZEROINIT
The most commonly used flag is GHND, which specifies that the memory block should be movable and
all buffers should be initialized to zeros. For the clipboard usage, we also need to specify flag GMEM_SHARE,
which will enhance the performance of clipboard operation.
Actually, in Win32 programming, the memory block allocated by one process can not be shared by
other processes. Flag GMEM_SHARE exists just for the backward compatibility purpose. For a general
application, we can not allocate a block of memory using flag GMEM_SHARE and share it with other
applications. In Win32, this flag is solely used for clipboard and DDE (see chapter 15) implementation.
256
Chapter 9. Font
The memory size that will be allocated is specified by dwBytes parameter of function
::GlobalAlloc(…).
Apart from these functions, there is another set of functions whose functionality is exactly the same,
the only difference is that they have a different set of function names:
Everything is exactly the same except that all the “Global” keywords are changed to “Local” here.
These functions are originated from the old Win16 programming, which uses 16-bit memory mode. In that
case the memory can be allocated either from the local heap or global heap. In Win32 programming, there
is only one heap, so two sets of functions become exactly the same. They exist just for the compatibility
purpose. We can use either of them in our program. We can even call ::GlobalAlloc(…) to allocate
memory and release it using ::LocalFree(…).
Clipboard Funcitons
To copy our own data to the clipboard, we need to first prepare the data. The following lists the
necessary steps for allocating memory blcok and fill it with our data: 1) Allocate enough buffers by calling
function ::GlobalAlloc(…). 2) Lock the memory by calling function ::GlobalLock(…), which will return
a pointer that can be used to access the memory buffers. 3) Fill these buffers with data. 4) Call
::GlobalUnlock(…) to unlock the memory.
We need to use a series of functions in order to put the data to the clipboard: 1) First we need to call
function CWnd::OpenClipboard(…), which will let the clipboard be owned by our application (Only the
window that owns the clipboard can modify the data contained in the clipboard, any other application is
forbidden to access the clipboard during this period). 2) Before putting any data to the clipboard, we must
call ::EmptyClipboard() to clear any existing data. 3) We can call ::SetClipboardData() to put new data
to the clipboard. 4) Finally we need to call ::CloseClipboard() to close the clipboard, this will let the
clipboard be accessible to other windows.
When calling function ::SetClipboardData(…), besides passing the handle of the global memory, we
also need to specify the data format. There are many standard clipboard data formats such as CF_TEXT,
CF_DIB, which represent text data and bitmap data respectively. We can also define our own data format by
calling function ::RegisterClipboardFormat(…).
To copy data from the clipboard, we need to open the clipboard first, then call function
::GetClipboardData(), which will return a global memory handle. With this handle, we can call
::GlobalLock(…) to lock the memory, copy the data from the global memory to our own buffers, call
::GlobalUnlock(…) to unlock the memory, and close the clipboard. We can not free the global memory
obtained from the clipboard because after the clipboard is closed, some other applications may also want to
access it.
257
Chapter 9. Font
BOOL CGDIDoc::DeleteSelection()
{
CString szStr;
int nSel;
int nNum;
if
(
m_nSelIndexEnd == -1 ||
m_nSelIndexBgn == -1 ||
m_nSelIndexEnd == m_nSelIndexBgn
)return FALSE;
szStr=m_szText.Left(nSel);
m_szText=m_szText.Mid(nSel+nNum);
m_szText=szStr+m_szText;
m_nCaretIndex=nSel;
UpdateAllViews(NULL);
GetCGDIView()->RedrawCaret();
m_nSelIndexBgn=m_nSelIndexEnd=-1;
return TRUE;
}
When there is no currently selected text, the function does nothing. Otherwise we proceed to delete the
selected text.
Because the ending selection index may be less than the beginning selection index, first we set the
value of local variable nSel to the smaller selection index, and set the number of selected characters to
another local variable nNum. Then the unselected text is combined together, and the caret index is adjusted.
Next the caret and the client window are updated. Finally, both selection indices are set to -1, this indicates
that currently there is no text being selected.
We can call this function when DELETE key is pressed to delete the selected text, also we can call it
when the selected text is being cut to the clipboard. In the sample, function CGDIDoc::DeleteChar() is
modified as follows:
if(DeleteSelection() == TRUE)return;
if(bBefore == TRUE)
{
……
}
Since this member function may be called when either BACK SPACE or DELETE key is pressed, we
need to delete the selected text in both cases. If deleting the selected text is successful, the function will
return. Otherwise it means there is no currently selected text, so we go on to delete a single character.
258
Chapter 9. Font
Two functions are implemented exactly the same. For function CGDIDoc::OnUpdateEditPaste(…), we
need to check if there is data available in the clipboard, if so, the command will be enabled. This checking
can be implemented by calling function ::IsClipboardFormatAvailable(…) with appropriate data format
passed to it. The function will return FALSE if there is no data present in the clipboard for the specified
data format. The following is the implementation of funcition CGDIDoc::OnUpdateEditPaste(…):
void CGDIDoc::OnEditCopy()
{
CString szStr;
int nSel;
int nNum;
HANDLE hMem;
LPSTR lpStr;
First, we assign the smaller of the two selection indicies to variable nSel, and the number of selected
characters to variable nNum. Then we copy the selected text to a CString type variable szStr. Next, we
allocate a memory block, lock it, copy the string from szStr to the new buffers. Then we unlock the
memory, open the clipboard, clear it, and copy the data to the clipboard. Finally we close the clipboard.
The implmentation of Edit | Cut command is almost the same except that we must delete the selected
text after copying the data to the clipboard. So function CGDIDoc::OnEditCut() is implemented as follows:
void CGDIDoc::OnEditCut()
{
OnEditCopy();
DeleteSelection();
}
259
Chapter 9. Font
For Edit | Paste command, everything is the reverse. We need to open the clipboard, obtain data from
the clipboard, lock the global memory, copy the data to local buffers, unlock the global memory, and insert
the new string to the text at the current caret position. The following is the implementation of this
command:
void CGDIDoc::OnEditPaste()
{
HANDLE hMem;
LPSTR lpStr;
CString szStr;
DeleteSelection();
GetCGDIView()->OpenClipboard();
hMem=::GetClipboardData(CF_TEXT);
ASSERT(hMem != NULL);
lpStr=(LPSTR)::GlobalLock(hMem);
ASSERT(lpStr);
szStr=lpStr;
m_nCaretIndex+=szStr.GetLength();
::GlobalUnlock(hMem);
::CloseClipboard();
m_szText=m_szText.Left(m_nCaretIndex)+szStr+m_szText.Mid(m_nCaretIndex);
UpdateAllViews(NULL);
GetCGDIView()->RedrawCaret();
}
Now the application can exchange data with another application that supports clipboard.
Function CDocument::UpdateAllViews(…)
Function CDocument::UpdateAllViews(…) has three parameters, two of which have default values:
By default, the update message will be sent to view, this will cause function CView::OnUpdate(…) to
be called:
260
Chapter 9. Font
Defining Hints
Our next task is to divide the updating events into different categories and calculate the rectangle for
each situation. The following is a list of situations when only a portion of the client window needs to be
updated:
The last two situations are a little complicated. When the user makes selections, the newly selected
area may be smaller or larger than the old selected area. In either case, we only need to update the changed
area to avoid flickering (Figure 9-5).
Because of this, we need to add new variables to remember the old selection indices. In the sample,
two new variables and some functions are declared in class CGDIDoc as follows:
public:
……
int GetSelIndexBgnOld(){return m_nSelIndexBgnOld;}
int GetSelIndexEndOld(){return m_nSelIndexEndOld;}
int GetCaretIndex(){return m_nCaretIndex;}
……
}
Variables m_nSelIndexBgnOld and m_nSelIndexEndOld are used to remember the old selection indices,
functions GetSelIndexBgnOld() and GetSelIndexEndOld() are used to obtain their values outside class
CGDIDoc. Because we also need to know the value of m_nCaretIndex when updating the client window,
another function GetCaretIndex() is also added for retrieving its value.
O ld selection: O ld selection:
D ifference: D ifference:
261
Chapter 9. Font
CGDIDoc::CGDIDoc()
{
……
m_nSelIndexEndOld=-1;
}
In the sample, some macros are defined as follows to indicate different updating situations when
function CDocument::UpdateAllViews(…) is called:
Old Version:
New Version:
Old Version:
New Version:
262
Chapter 9. Font
Flag HINT_INPUT will cause all the characters after the caret to be updated.
The following shows the modifications made to funciton CGDIDoc::DeleteChar(…):
Old Version:
New Version:
Flag HINT_DELCHAR_AFTER will cause all the characters after the caret to be updated, and
HINT_DELCHAR_BEFORE will cause the character before the caret along with all the characters after the caret
to be updated.
The following shows the modifications made to function CGDIDoc::DeleteSelection():
Old Version:
BOOL CGDIDoc::DeleteSelection()
{
……
m_nCaretIndex=nSel;
UpdateAllViews(NULL);
GetCGDIView()->RedrawCaret();
m_nSelIndexBgn=m_nSelIndexEnd=-1;
return TRUE;
}
New Version:
263
Chapter 9. Font
BOOL CGDIDoc::DeleteSelection()
{
……
m_nCaretIndex=nSel;
UpdateAllViews(NULL, HINT_DELETE_SELECTION);
GetCGDIView()->RedrawCaret();
m_nSelIndexBgn=m_nSelIndexEnd=-1;
m_nSelIndexBgnOld=m_nSelIndexEndOld=-1;
return TRUE;
}
Flag HINT_DELETE_SELECTON will cause the selected text and the characters after the selection to be
updated.
The following shows the modifications made to function CGDIDoc::OnEditPaste():
Old Version:
void CGDIDoc::OnEditPaste()
{
……
UpdateAllViews(NULL);
GetCGDIView()->RedrawCaret();
}
New Version:
void CGDIDoc::OnEditPaste()
{
……
UpdateAllViews(NULL, HINT_PASTE);
GetCGDIView()->RedrawCaret();
}
Flag HINT_PASTE will cause all the characters after the caret to be updated.
The following shows the modifications made to inline function CGDIDoc::ResetSelection():
Old Version:
class CGDIDoc : public CDocument
{
……
void ResetSelection()
{
m_nSelIndexBgn=m_nSelIndexEnd=-1;
UpdateAllViews(NULL);
}
……
}
New Version:
Flag HINT_UNSELECTION will cause only the selected area to be updated. Because both m_nSelIndexBgn
and m_nSelIndexEnd should be set to -1 to indicate that there is no selected text anymore, we need to use
two other variables (m_nSelIndexBgnOld and m_nSelIndexEndOld) to store the old selection indices.
264
Chapter 9. Font
Overriding CView::OnUpdate(…)
On the view side, function OnUpdate(…) can be added through using Class Wizard. In this function, we
need to know the current values of CGDIDoc::m_nSelIndexBgn, CGDIDoc::m_nSelIndexEnd, CGDIDoc::
m_nSelIndexBgnOld, CGDIDoc::m_nSelIndexEndOld and CGDIDoc::m_nCaretIndex in order to decide
which part of the text should be updated. We also need to obtain the current font and text string in order to
calculate the actual rectangle for implementing update.
If parameter lHint is NULL, it means that all client area needs to be updated. In this case, we call the
default implementation of this function and return. The following is a portion of funciton CGDIView::
OnUpdate(…) which implements this:
if(lHint == 0)
{
CScrollView::OnUpdate(pSender, lHint, pHint);
return;
}
ptrDoc=(CGDIDoc *)GetDocument();
ASSERT(ptrDoc != NULL);
ptrFontOld=dc.SelectObject(ptrDoc->GetFont());
GetClientRect(rect);
rect.bottom=ptrDoc->GetCaretVerSize();
szText=ptrDoc->GetText();
nSelIndexBgn=ptrDoc->GetSelIndexBgn();
nSelIndexEnd=ptrDoc->GetSelIndexEnd();
nSelIndexEndOld=ptrDoc->GetSelIndexEndOld();
……
If parameter lHint is non-null, we need to obtain the current font, text string and selection indices
from the document, and calculate the area that should be updated. Here variable rect stores a rectangle that
covers all of the text (within the window).
In the case when hint is one of HINT_DELCHAR_AFTER, HINT_PASTE and HINT_INPUT, we need to update
all the characters after the caret:
……
switch(lHint)
{
case HINT_DELCHAR_AFTER:
case HINT_PASTE:
case HINT_INPUT:
{
nIndex=ptrDoc->GetCaretIndex();
szText=szText.Left(nIndex);
sizeExtent=dc.GetTextExtent(szText);
rect.left=sizeExtent.cx;
InvalidateRect(rect);
break;
}
……
The caret index is retrieved from the document and stored to variable nIndex. Then the sub-string
before the caret is extracted and stored to variable szText. Its dimension is calculated by calling function
CDC::GetTextExtent(…), and the left border of rect is changed so that it covers only the characters after
the caret. Finally, function CWnd::InvalidateRect(…) is called and rect is passed to one of its parameters.
265
Chapter 9. Font
If the hint is HINT_DELCHAR_BEFORE, we need to update the character before the caret and all the
characters after the caret:
……
case HINT_DELCHAR_BEFORE:
{
nIndex=ptrDoc->GetCaretIndex();
if(nIndex != 0)nIndex--;
szText=szText.Left(nIndex);
sizeExtent=dc.GetTextExtent(szText);
rect.left=sizeExtent.cx;
InvalidateRect(rect);
break;
}
……
If the hint is HINT_DELETE_SELECTION, we need to update the selected text as well as the characters
after the selection:
……
case HINT_DELETE_SELECTION:
{
nIndex=min(nSelIndexBgn, nSelIndexEnd);
szText=szText.Left(nIndex);
sizeExtent=dc.GetTextExtent(szText);
rect.left=sizeExtent.cx;
InvalidateRect(rect);
break;
}
……
If the hint is HINT_UNSELECTION, we need to update only the selected text. Since both CGDIDoc::
m_nSelIndexBgn and CGDIDoc::m_nSelIndexEnd are -1 now, we must use CGDIDoc::m_nSelIndexBgnOld
and CGDIDoc::m_nSelIndexEndOld to calculate the rectangle:
……
case HINT_UNSELECTION:
{
nSelIndexBgn=ptrDoc->GetSelIndexBgnOld();
nSelIndexEnd=ptrDoc->GetSelIndexEndOld();
nSel=min(nSelIndexBgn, nSelIndexEnd);
nNum=max(nSelIndexBgn, nSelIndexEnd)-nSel;
rect.left=(dc.GetTextExtent(szText.Left(nSel))).cx;
szText=szText.Mid(nSel, nNum);
rect.right=(dc.GetTextExtent(szText)).cx+rect.left;
InvalidateRect(rect, FALSE);
break;
}
……
……
case HINT_SELECTION:
{
if(nSelIndexBgn != -1 && nSelIndexEnd != -1)
{
if(nSelIndexEndOld == -1)
{
nSel=min(nSelIndexBgn, nSelIndexEnd);
nNum=max(nSelIndexBgn, nSelIndexEnd)-nSel;
}
else
{
nSel=min(nSelIndexEnd, nSelIndexEndOld);
nNum=max(nSelIndexEnd, nSelIndexEndOld)-nSel;
}
rect.left=(dc.GetTextExtent(szText.Left(nSel))).cx;
szText=szText.Mid(nSel, nNum);
rect.right=(dc.GetTextExtent(szText)).cx+rect.left;
266
Chapter 9. Font
InvalidateRect(rect, FALSE);
}
break;
}
……
nSelIndexBgn=-1
nSelIndexEnd=-1
nSelIndexBgnOld=-1
nSelIndexEndOld=-1
nSelIndexBgn=2
nSelIndexEnd=3
nSelIndexBgnOld=-1
nSelIndexEndOld=-1
nSelIndexBgn=2
nSelIndexEnd=4
nSelIndexBgnOld=2
nSelIndexEndOld=3
Explanation:
1) Nothing is selected.
2) The third character is selected, since nSelIndexEndOld is -1, the changed area
should be calculated from nSelIndexBgn and nSelIndexEnd.
3) The third and fourth characters are selected. Since nSelIndexEndOld is not -1,
the changed area should be calculated from nSelIndexEndOld and
nSelIndexBgn.
Figure 9-6. Update only changed area
If we pass FALSE to the second parameter of CWnd::InvalidateRect(…), the client area will be
updated without being erased. This can further reduce flickering.
Summary:
1) Font can be created from structure LOGFONT. We need to provide the following information when
creating a special font: face name, font size (height and width). To add special effects to the text, we
need to know if the font is bolded, italic, underlined or strikeout. Also, we can change character’s
orientation by setting font’s escapement.
1) All the fonts contained in the system can be enumerated by calling function ::EnumFontFamilies(…).
We need to provide a callback function to receive information for each type of font.
1) Function CDC::ExtTextOut(…) can output a text string within a specified rectangle, the area outside the
rectangle will not be affected no matter what the text size is. When we call this function, all the area
not covered by the text within the rectangle is treated as the background of the text.
1) To implement caret within a window, first we need to create the caret by using one of the following
functions: CWnd::CreateSolidCaret(…), CWnd::CreateGrayCaret(…), CWnd::CreateCaret(…). Then
we can show or hide the caret by calling either function CWnd::ShowCaret() or CWnd::HideCaret().
1) Keyboard input events can be trapped by handling WM_KEYDOWN or WM_CHAR message.
1) Mouse cursor can be changed by handling message WM_SETCURSOR. We can load a user designed cursor
resource by calling function CWinApp::LoadCursor(…). We can also load a standard cursor by calling
function CWinApp::LoadStandardCursor(…).
1) If we call function CDC::SetTextAlign(…) using TA_UPDATECP flag, the window origin will be updated
to the end of the text each time funciton CDC::TextOut(…) is called.
1) To use global memory, we need to call ::GlobalAlloc(…) to allocate the buffers, call
::GlobalLock(…) to lock the memory before accessing it, call ::GlobalUnlock(…) to stop accessing
the memory, and call ::GlobalFree(…) to release the memory.
1) To access the clipboard, we need to call CWnd::OpenClipboard(…) to open the clipboard, call
::EmptyClipboard() to clear any existing data, call ::SetClipboardData() to put data to the
clipboard, and call ::CloseClipboard() to close the clipboard. To get data from the clipboard, after
267
Chapter 9. Font
opening it, we need to call function ::GetClipboardData() to obtain a global memory handle, which
can be used for accessing the data contained in the clipboard.
1) We can pass hints to function CDocument::UpdateAllViews(…) to indicate different updating
situations. The hint can be received in function CView::OnUpdate(…). If we want only a portion of the
client window to be updated, we can specify the area with a CRect type variable and use it to call
function CWnd::InvalidateRect(…) instead of default function CWnd::Invalidate(…).
268
Chapter 10. Bitmap
Chapter 10
Bitmap
F
rom this chapter we are going to deal with another GDI object bitmap, which is a very complex
issue in Windows programming. There are many topics on how to use bitmaps, how to avoid color
distortion, how obtain palette from bitmap, how convert from one bitmap format to another, and how
to manipulate image pixels.
Samples in this chapter are specially designed to work on 256-color palette device. To customize them
for non-palette devices, we can just eleminate logical palette creation and realization procedure.
Drawing DDB
There are several ways to include bitmap image in an application. The simplest one is to treat it as
bitmap resource. To load the image, we can call function CBitmap::LoadBitmap(…) and pass the bitmap
resource ID to it.
After the bitmap is loaded, we need to output it to the target device (such as screen). The procedure of
outputting a bitmap to a target device is different from using a pen or brush to draw a line or fill a
rectangle: we cannot select bitmap into the target DC and draw the bitmap directly. Instead, we must create
a compatible memory DC and select the bitmap into it, then copy the bitmap from memory DC to the target
DC.
The functions that can be used to copy a bitmap between two DCs are CDC::BitBlt(…) and
CDC::StretchBlt(…). The former function allows us to copy the bitmap in 1:1 ratio, and the latter one
allows us to enlarge or reduce the dimension of the original image. Lets first take a look at the first member
function:
BOOL CDC::BitBlt
(
int x, int y, int nWidth, int nHeight,
CDC *pSrcDC,
int xSrc, int ySrc,
DWORD dwRop
269
Chapter 10. Bitmap
);
There are eight parameters, first four of them specify the origin and size of the target bitmap that will
be drawn. Here x and y can be any position in the target device, also, nWidth and nHeight can be less than
the dimension of source image (In this case, only a portion of the source image will be drawn). The fifth
parameter is a pointer to the source DC. The seventh and eighth parameters specify the origin of the source
bitmap. Here, we can select any position in the source bitmap as origin. The last parameter specifes the
bitmap drawing mode. We can draw a bitmap using many modes, for example, we can copy the original
bitmap to the target, turn the output black or white, do bit-wise OR, AND or XOR operation between
source bitmap and target bitmap.
Creating Memory DC
A memory DC used for copying bitmap image must be compatible with the target DC. We can call
function CDC::CreateCompatibleDC(…) to create this type of DC. The following is the format of this
function:
The only parameter to this function (pDC) must be a pointer to the target DC.
Sample 10.1\GDI
Sample 10.1-1\GDI demonstrates how to use function CDC::BitBlt(…). It is a standard SDI
application generated by Application Wizard, and its view is based on class CScrollView. In the sample,
first a bitmap resource is added to the application, whose ID is IDB_BITMAP.
A CBitmap type variable is declared in class CGDIDoc, it will be used to load this bitmap:
public:
CBitmap *GetBitmap(){return &m_bmpDraw;}
//{{AFX_VIRTUAL(CGDIDoc)
……
}
Variable m_bmpDraw will be used to load the bitmap, and function GetBitmap() will be used to access it
outside class CGDIDoc. Bitmap IDB_BITMAP is loaded in the constructor of class CGDIDoc:
CGDIDoc::CGDIDoc()
{
m_bmpDraw.LoadBitmap(IDB_BITMAP);
}
In function CGDIView::OnInitialUpdate(), we need to use bitmap dimension to set the total window
scroll sizes so that if the window is not big enough, we can scroll the image to see the covered portion:
void CGDIView::OnInitialUpdate()
{
CGDIDoc *pDoc;
270
Chapter 10. Bitmap
CBitmap *pBmp;
BITMAP bm;
pDoc=GetDocument();
ASSERT_VALID(pDoc);
pBmp=pDoc->GetBitmap();
pBmp->GetBitmap(&bm);
SetScrollSizes(MM_TEXT, CSize(bm.bmWidth, bm.bmHeight));
CScrollView::OnInitialUpdate();
}
The bitmap pointer is obtained from the document. By calling function CBitmap::GetBitmap(…), all
the bitmap information (including its dimension) is obtained and stored in variable bm. Then the scroll sizes
are set using bitmap dimension. By doing this, the scroll bars will pop up automatically if the dimension of
the client window becomes smaller than the dimension of the bitmap.
Function CGDIView::OnDraw(…) is implemented as follows:
CGDIDoc *pDoc=GetDocument();
ASSERT_VALID(pDoc);
dcMemory.CreateCompatibleDC(pDC);
pBmp=pDoc->GetBitmap();
pBmpOld=dcMemory.SelectObject(pBmp);
pBmp->GetBitmap(&bm);
pDC->BitBlt
(
0,
0,
bm.bmWidth,
bm.bmHeight,
&dcMemory,
0,
0,
SRCCOPY
);
dcMemory.SelectObject(pBmpOld);
}
First we call CDC::CreateCompatibleDC(…) to create a compatible memory DC, then use it to select
the bitmap obtained from the document. Like other GDI objects, after using a bitmap, we need to select it
out of the DC. For this purpose, a local variable pBmpOld is used to store the returned address when we call
CDC::SelectObject(…) to select the bitmap (pBmp) into memory DC. After the bitmap is drawn, we call
this function again to select pBmpOld, this will select bitmap stored by pBmp out of the DC.
In the next step function CBitmap::GetBitmap(…) is called to retrieve all the bitmap information into
variable bm, whose bmHeight and bmWidth members (represent the dimension of bitmap) will be used for
copying the bitmap. Then we call CDC::BitBlt(…) to copy the bitmap from the memory DC to the target
DC. The origin of the target bitmap is specified at (0, 0), also, the source bitmap and target bitmap have the
same size.
Sample 10.1-2\GDI
Sample 10.1-2\GDI demonstrates how to use function CDC::StretchBlt(…) to output the bitmap
image. It is based on sample 10.1-1\GDI. In this sample, the image is enlarged to twice of its original size
and output to the client window.
Because the target image has a bigger dimension now, we need to adjust the scroll sizes. First function
CGDIView::OnInitialUpdate() is modified as follows for this purpose:
void CGDIView::OnInitialUpdate()
{
……
SetScrollSizes(MM_TEXT, CSize(2*bm.bmWidth, 2*bm.bmHeight));
271
Chapter 10. Bitmap
CScrollView::OnInitialUpdate();
}
BOOL CDC::StretchBlt
(
int x, int y, int nWidth, int nHeight,
CDC *pSrcDC,
int xSrc, int ySrc, int nSrcWidth, int nSrcHeight,
DWORD dwRop
);
There are two extra parameters nSrcWidth and nSrcHeight here (compared to function
CDC::BitBlt()), which specify the extent of original bitmap that will be output to the target. Obviously,
nWidth and nSrcWidth determine the horizontal ratio of the output bitmap (relative to source bitmap).
Likewise, nHeight and nSrcHeight determine the vertical ratio.
In the sample, both horizontal and vertical ratios are set to 200%, and function CGDIView::OnDraw(…)
is modified as follows:
With the above modifications, we will have an enlarged bitmap image in the client window.
DIB Format
A DIB comprises three parts: bitmap information header, color table, and bitmap bit values. The
bitmap information header stores the information about the bitmap such as its width, height, bit count per
pixel, etc. The color table contains an array of RGB colors, it can be referenced by the color indices. The
272
Chapter 10. Bitmap
bitmap bit values represent bitmap pattern by specifying an index into the color table for every pixel. The
color table can also be empty, in which case the bitmap bit vlues must be actual R, G, B combinations.
There are several type of DIBs: monocrome, 16 colors, 256 colors and 24 bit. The first three formats
use color table and color indices to form a bitmap. The last format does not have a color table and all the
pixels are represented by R, G, B combinations.
The following is the format of bitmap information header:
It has 11 members, the most important ones are biSize, biWidth, biHeight, biBitCount and
biSizeImage.
Member biSize specifies the length of this structure, which can be specified by
sizeof(BITMAPINFORHEADER). Members biWidth and biHeight specify the dimension of the bitmap
image. Member biBitCount specifies how many bits are used to represent one pixel (Bit count per pixel).
This factor determines the total number of colors that can be used by the bitmap and also, the size of the
color table. For example, if this member is 1, the bitmap can use only two colors. In this case, the size of
color table is 2. If it is 4, the bitmap can use up to 16 colors and the size of the color table is 16. The
possible values of this member are 1, 4, 8, 16, 24, and 32. Member biSizeImage specifies the total number
of bytes that must be allocated for storing image data. This is a very important member, because we must
know its value before allocating memory.
An image is composed of multiple raster lines, each raster line is made up of an array of pixels. To
speed up image loading, each raster line must use a multiple of four-byte buffers (This means if we have a
2-color (monochrom) 1×1 bitmap, we need four bytes instead of one byte to store only one pixel). Because
of this, the value of biSizeImage can not be simply calculated by the following fomulae:
biHeight*biWidth*biBitCount/8
DIB Example
Following the bitmap header are bitmap bit values. Image data is stored in the memory one pixel after
another from left to right, and vertically from the bottom to the top. The following is an example of 3×4
image:
W B W
B W B
W B W
B W B
273
Chapter 10. Bitmap
This image has only two colors: black and white. If we store it using monochrome DIB format (2
colors), we need only 3 bits to store one raster line. For 16-color DIB format, we need 12 bits to store one
line. Since each raster line must use multiple of four-byte buffers (32 bits), if one raster line can not use up
all the bits, the rest will simply be left unused.
The following table compares four different types of DIB formats by listing the following information:
the necessary bits needed, the actual bits used, and number of bits wasted by one raster line:
DIB format Bits Needed for One Actual Bits Used by One Bits Wasted for One
Raster Line Raster Line Raster Line
2 color 3 32 29
4 color 12 32 20
8 color 24 32 8
24 bit 72 96 24
We can define a macro that allows us to calculate the number of bytes needed for each raster line for
different bitmap formats:
Here bits represents the number of bits that are needed for one raster line, it can be obtained from
BITMAPINFORHEADER by doing the following calculation:
biWidth×biBitCount
Now we know how to calculate the value of biSizeImage from other members of structure
BITMAPINFOHEADER:
WIDTHBYTES(biWidth*biBitCount)*biHeight
The following is the image data for the above DIB example, assume all unused bits are set to 0:
2 color bitmap (assuming in the color table, index to white color = 0, index to black color = 1):
C0 00 00 00
40 00 00 00
C0 00 00 00
40 00 00 00
4 color bitmap (assuming in the color table, index to white color = 0, index to black color = 15):
F0 00 00 00
0F 00 00 00
F0 F0 00 00
0F 00 00 00
8 color bitmap (assuming in the color table, index to white color = 0, index to black color = 255):
FF 00 FF 00
00 FF 00 00
FF 00 FF 00
00 FF 00 00
00 00 00 FF FF FF 00 00 00 00 00
FF FF FF 00 00 00 FF FF FF 00 00
00 00 00 FF FF FF 00 00 00 00 00
FF FF FF 00 00 00 FF FF FF 00 00
274
Chapter 10. Bitmap
The bitmap resource is stored exactly in the format mentioned above. To avoid color distortion, we
need to extract the color table contained in the DIB to create logic palette, and convert DIB to DDB before
drawing it.
HBITMAP ::CreateDIBitmap
(
HDC hdc,
CONST BITMAPINFOHEADER *lpbmih, DWORD fdwInit,
CONST VOID *lpbInit, CONST BITMAPINFO *lpbmi, UINT fuUsage
);
The first parameter of this function is a handle to target DC. Because DDB is device dependent, we
must know the DC information in order to create the bitmap. The second parameter is a pointer to
BITMAPINFORHEADER type object, it contains bitmap information. The third parameter is a flag, if we set it to
CBM_INIT, the bitmap will be initialized with the data pointed by lpbInit and lpbmi; if this flag is 0, a
blank bitmap will be created. The final parameter specifies how to use color table. If the color table is
contained in the bitmap header, we can set its value to DIB_RGB_COLORS.
Loading Resource
To access data stored in the resource, we need to call the following three funcitons:
The first function will find the specified resource and return a resource handle. When calling this
function, we need to provide module handle (which can be obtained by calling function
AfxGetResourceHandle()), the resource ID, and the resource type. The second function loads the resource
found by the first function. When calling this function, we need to provide the module handle, along with
the resource handle returned from the first function. The third function locks the resource. By doing this,
we can access the data contained in it. The input parameter to this function must be the global handle
obtained from the second function.
Sample
Sample 10.2\GDI demonstrates how to extract color table from DIB and convert it to DDB. It is based
on sample 10.1-2\GDI.
Because we need to implement logical palette, first a variable and a function are added to class
CGDIDoc:
Variable m_palDraw is added for creating logical palette and function GetPalette() is used to access it
outside class CGDIDoc.
In class CGDIView, some new variables are added for bitmap drawing:
275
Chapter 10. Bitmap
{
protected:
CDC m_dcMem;
CPalette *m_pPalMemOld;
CBitmap *m_pBmpMemOld;
BITMAP m_bmInfo;
CGDIView();
DECLARE_DYNCREATE(CGDIView)
……
}
Variable m_dcMem will be used to implement memory DC at the initialization stage of the client
window. It will be used later for drawing bitmap. By implementing memory DC this way, we don’t have to
create it every time. Also, we will select the bitmap and palette into the memory DC after they are avialabe,
and selet them out of the DC when the window is being destroyed. For this purpose, two pointers
m_pPalMemOld and m_pBmpMemOld are declared, they will be used to select the palette and bitmap out of DC.
Variable m_bmInfo is used to store the information of bitmap.
The best place to create bitmap and palette is in CGDIView::OnInitialUpdate(). First, we must locate
and load the bitmap resource:
void CGDIView::OnInitialUpdate()
{
CClientDC dc(this);
CGDIDoc *pDoc;
CBitmap *pBmp;
CPalette *pPalDraw;
LPBITMAPINFO lpBi;
LPLOGPALETTE lpLogPal;
CPalette *pPalOld;
int nSizeCT;
int i;
HBITMAP hBmp;
HRSRC hRes;
HGLOBAL hData;
CScrollView::OnInitialUpdate();
pDoc=GetDocument();
ASSERT_VALID(pDoc);
if
(
(
hRes=::FindResource
(
AfxGetResourceHandle(),
MAKEINTRESOURCE(IDB_BITMAP),
RT_BITMAP
)
) != NULL &&
(
hData=::LoadResource
(
AfxGetResourceHandle(),
hRes
)
) != NULL
)
{
lpBi=(LPBITMAPINFO)::LockResource(hData);
ASSERT(lpBi);
……
We call funcitons ::FindResource(…) and ::LoadResource(…) to load the bitmap resource. Function
::FindResource(…) will return a handle to the specified resource block, which can be passed to
::LoadResource(…) for loading the resource. This function returns a global memory handle. We can call
::LockResource(…) to lock the resource. This function will return an address that can be used to access the
bitmap data. In the sample, we use pointer lpBi to store this address.
Next, we must calculate the size of color table and allocate enough memory for creating logical palette.
The color table size can be calculated from “bit count per pixel” information of the bitmap as follows:
……
switch(lpBi->bmiHeader.biBitCount)
276
Chapter 10. Bitmap
{
case 1:
{
nSizeCT=2;
break;
}
case 4:
{
nSizeCT=16;
break;
}
case 8:
{
nSizeCT=256;
break;
}
default: nSizeCT=0;
}
……
The color table size is stored in variable nSizeCT. Next, the logical palette is created from the color
table stored in the DIB data:
……
lpLogPal=(LPLOGPALETTE) new BYTE
[
sizeof(LOGPALETTE)+(nSizeCT-1)*sizeof(PALETTEENTRY)
];
lpLogPal->palVersion=0x300;
lpLogPal->palNumEntries=nSizeCT;
for(i=0; i<nSizeCT; i++)
{
lpLogPal->palPalEntry[i].peRed=lpBi->bmiColors[i].rgbRed;
lpLogPal->palPalEntry[i].peGreen=lpBi->bmiColors[i].rgbGreen;
lpLogPal->palPalEntry[i].peBlue=lpBi->bmiColors[i].rgbBlue;
lpLogPal->palPalEntry[i].peFlags=NULL;
}
pPalDraw=pDoc->GetPalette();
pPalDraw->CreatePalette(lpLogPal);
delete [](BYTE *)lpLogPal;
pPalOld=dc.SelectPalette(pPalDraw, FALSE);
dc.RealizePalette();
……
The color table is obtained from member bmiColors of structure BITMAPINFO (pointed by lpBi). Since
the palette is stored in the document, we first call CGDIDoc::GetPalette() to obtain the address of the
palette (CGDIDoc::m_palDraw), then call CPalette::CreatePalette(…) to create the palette. After this we
select the palette into the client DC, and call CDC::RealizePalette() to let the logical palette be mapped
to the system palette.
Then, we create the DDB from DIB data:
……
hBmp=::CreateDIBitmap
(
dc.GetSafeHdc(),
&lpBi->bmiHeader,
CBM_INIT,
(LPSTR)lpBi+sizeof(BITMAPINFOHEADER)+sizeof(RGBQUAD)*nSizeCT,
lpBi,
DIB_RGB_COLORS
);
ASSERT(hBmp);
dc.SelectPalette(pPalOld, FALSE);
pBmp=pDoc->GetBitmap();
pBmp->Attach(hBmp);
……
Function ::CreateDIBitmap(…) returns an HBITMAP type handle, which must be associated with a
CBitmap type varible by calling function CBitmap::Attach(…).
277
Chapter 10. Bitmap
The rest part of this funciton fills variable CGDIView::m_bmInfo with bitmap information, sets the
scroll sizes, create the memory DC, select bitmap and palette into it, then free the bitmap resource loaded
before:
……
pBmp->GetBitmap(&m_bmInfo);
SetScrollSizes(MM_TEXT, CSize(m_bmInfo.bmWidth, m_bmInfo.bmHeight));
m_dcMem.CreateCompatibleDC(&dc);
m_pBmpMemOld=m_dcMem.SelectObject(pBmp);
m_pPalMemOld=m_dcMem.SelectPalette(pPalDraw, FALSE);
::FreeResource(hData);
}
}
Because the bitmap and the palette are selected into the memory DC here, we must select them out
before application exits. The best place to do this is in WM_DESTROY message handler. In the sample, a
WM_DESTROY message handler is added to class CGDIView through using Class Wizard, and the
corresponding function CGDIView::OnDestroy() is implemented as follows:
void CGDIView::OnDestroy()
{
m_dcMem.SelectPalette(m_pPalMemOld, FALSE);
m_dcMem.SelectObject(m_pBmpMemOld);
CScrollView::OnDestroy();
}
CGDIDoc *pDoc=GetDocument();
ASSERT_VALID(pDoc);
pPal=pDoc->GetPalette();
pPalOld=pDC->SelectPalette(pPal, FALSE);
pDC->RealizePalette();
pDC->StretchBlt
(
0,
0,
m_bmInfo.bmWidth,
m_bmInfo.bmHeight,
&m_dcMem,
0,
0,
m_bmInfo.bmWidth,
m_bmInfo.bmHeight,
SRCCOPY
);
pDC->SelectPalette(pPalOld, FALSE);
}
With the above implementations, the bitmap will become more vivid.
278
Chapter 10. Bitmap
File Format
All bitmap images stored on the hard disk are in the format of DIB, and therefore can be any of the
following formats: monochrome, 16 color, 256 color and 24-bit. The difference between DIB stored in a
file and DIB stored as a resource is that there is an extra bitmap file header for DIB stored in file. This
header has the following format:
This header specifies three factors: the file type, the bitmap file size, and the offset specifying where
the DIB data really starts. So a real DIB file has the following format:
BITMAPFILEHEADER
BITMAPINFOHEADER
Color Table
Bitmap Bits
Member bfType must be set to ‘BM’, which indicates that the file is a bitmap file. Member bfSize
specifies the whole length of the bitmap file, which is counted from the beginning of the file to its end.
Member bfOffBits specifies the offset from BITMAPFILEHEADER to bitmap bits, which should be the size of
BITMAPINFOHEADER plus the size of color table.
GDI\n\nGDI\n\n\nGDI.Document\nGDI Document
By default, no special file types are supported. If we want the file dialog box to contain certain filters,
we need to change the fourth and fifth items of the above string. In the sample, string resource
IDR_MAFRAME is changed to the following:
The fourth item (Bitmap Files(*.bmp)) is a descriptive name for bitmap files, and the fifth item
(.bmp) is the filter.
279
Chapter 10. Bitmap
CGDIDoc::CGDIDoc()
{
m_hDIB=NULL;
}
We will allocate global memory each time a new bitmap file is opened. So when the application exits,
we need to check variable m_hDIB to see if the memory has been allocated. If so, we need to release it:
CGDIDoc::~CGDIDoc()
{
if(m_hDIB != NULL)
{
::GlobalFree(m_hDIB);
m_hDIB=NULL;
}
}
We need to modify function CGDIDoc::Serialize(…) to load data from DIB file. First, when a file is
being opened, variable m_hDIB may be currently in use. In this case, we need to release the global memory:
if(m_hDIB != NULL)
{
::GlobalFree(m_hDIB);
m_hDIB=NULL;
}
……
280
Chapter 10. Bitmap
We can call CArchive::Read(…) to read bytes from the file into the memory. In the sample, first
bitmap file header (data contained in structure BITMAPFILEHEADER) is read:
……
if
(
ar.Read
(
(LPSTR)&bf,
sizeof(BITMAPFILEHEADER)
) != sizeof(BITMAPFILEHEADER)
)return;
……
Then the file type is checked. If it is DIB format, global memory is allocated, which will be used to
store DIB data:
……
if(bf.bfType != 'MB')return;
dwSize=bf.bfSize-sizeof(BITMAPFILEHEADER);
m_hDIB=::GlobalAlloc(GHND, dwSize);
ASSERT(m_hDIB);
lpDIB=(LPSTR)::GlobalLock(m_hDIB);
ASSERT(lpDIB);
……
This completes reading the bitmap file. The DIB data is stored in m_hDIB now.
Creating DDB
On the view side, we need to display the bitmap stored in memory (whose handle is m_hDIB) instead of
the bitmap stored as resource. So we need to modify function CGDIView::OnInitialUpdate(), which will
be called whenever a new file is opened successfully. First, instead of obtaining data from the resource,
function CGDIDoc::GetHDib() is called to get the handle of DIB data:
void CGDIView::OnInitialUpdate()
{
CClientDC dc(this);
CGDIDoc *pDoc;
CBitmap *pBmp;
CPalette *pPalDraw;
LPBITMAPINFO lpBi;
LPLOGPALETTE lpLogPal;
CPalette *pPalOld;
int nSizeCT;
int i;
HBITMAP hBmp;
HGLOBAL hData;
CScrollView::OnInitialUpdate();
pDoc=GetDocument();
ASSERT_VALID(pDoc);
hData=pDoc->GetHDib();
……
281
Chapter 10. Bitmap
Remember in the previous sample, after the DDB and palette are created, we will select them into the
memory DC so that the image can be drawn directly later. In this sample, when a new DIB file is opened,
there may be a bitmap (also a palette) that is being currently selected by the memory DC. If so, we need to
select it out of the memory DC, then lock the global memory and obtain its address that can be used to
access the DIB (In order to create DDB, we need the information contained in the DIB):
……
if(hData != NULL)
{
if(m_pPalMemOld != NULL)m_dcMem.SelectPalette(m_pPalMemOld, FALSE);
if(m_pBmpMemOld != NULL)m_dcMem.SelectObject(m_pBmpMemOld);
lpBi=(LPBITMAPINFO)::GlobalLock(hData);
ASSERT(lpBi);
……
The rest portion of this function calculates the size of the color table, creates the palette (If there is an
existing palette, delete it first), and creates DDB from DIB data. Then the newly created bitmap and palette
are selected into the memory DC, and m_bmInfo is updated with the new bitmap information. Finally, the
global memory is unlocked.
Other functions remain unchanged. With the above implementation, we can open any DIB file with our
application and display the image in the client window.
int ::GetDIBits
(
HDC hdc, HBITMAP hbmp,
UINT uStartScan, UINT cScanLines, LPVOID lpvBits, LPBITMAPINFO lpbi,
UINT uUsage
);
Its parameters are similar to that of funciton ::CreateDIBitmap(). Since DDB is device dependent, we
must know the attribute of device context in order to convert DDB to DIB. So we must pass the handle of
the target DC to the first parameter of this function. Parameter uStartScan and uScanLines specify the
starting raster line and total number of raster lines whose data is to be retrieved. Parameter lpBits specifies
the buffer address that can be used to recieve bitmap data. Pointer lpbi provides a BITMAPINFO structure
specifying the desired format of DIB data.
When calling this function, we can pass NULL to pointer lpvBits. This will cause the function to fill
the bitmap information into a BITMAPINFO object. By doing this, we can get the color table that is being
used by the DDB.
So the conversion takes three steps: 1) Call function CBitmap::GetBitmap(…) to obtain the information
of the bitmap, calculate the color table size, allocate enough buffers for storing bitmap information header
282
Chapter 10. Bitmap
and color table. 2) Call function ::GetDIBits(…) and pass NULL to parameter lpvBits to receive bitmap
information header and color table. 3) Reallocate buffers for storing bitmap data and call ::GetDIBits(…)
again to get the DIB data.
New Functions
Sample 10.4\GDI demonstrates how to convert DDB to DIB and save the data to hard disk.
First some functions are added to class CGDIDoc, they will be used for converting DDB to DIB:
Function CGDIDoc::ConvertDDBtoDIB(…) converts DDB to DIB, its input parameter is a CBitmap type
pointer and its return value is a global memory handle. Function CGDIDoc::GetColorTableSize(…) is used
to calculate the size of color table from bit count per pixel information (In the previouse samples, color
table size calculation is implemented within function CGDIView::OnInitialUpdate(). Since we need color
table size information more frequently now, this calculation is implemented as a single member function):
switch(wBitCount)
{
case 1:
{
dwSizeCT=2;
break;
}
case 4:
{
dwSizeCT=16;
break;
}
case 8:
{
dwSizeCT=256;
break;
}
case 24:
{
dwSizeCT=0;
}
}
return dwSizeCT;
}
In function CGDIDoc::ConvertDDBtoDIB(…), first we must obtain a handle to the client window that
can be used to create a DC:
pos=GetFirstViewPosition();
ptrView=(CGDIView *)GetNextView(pos);
ASSERT(ptrView->IsKindOf(RUNTIME_CLASS(CGDIView)));
283
Chapter 10. Bitmap
CClientDC dc(ptrView);
……
}
Then function CBitmap::GetBitmap(…) is called to retrieve the information of bitmap and allocate
enough buffers for storing structure BITMAPINFOHEADER and color table:
……
pBmp->GetBitmap(&bm);
bi.biSize=sizeof(BITMAPINFOHEADER);
bi.biWidth=bm.bmWidth;
bi.biHeight=bm.bmHeight;
bi.biPlanes=bm.bmPlanes;
bi.biBitCount=bm.bmPlanes*bm.bmBitsPixel;
bi.biCompression=BI_RGB;
bi.biSizeImage=0;
bi.biXPelsPerMeter=0;
bi.biYPelsPerMeter=0;
bi.biClrUsed=0;
bi.biClrImportant=0;
dwSizeCT=GetColorTableSize(bi.biBitCount);
dwDibLen=bi.biSize+dwSizeCT*sizeof(RGBQUAD);
pPalOld=dc.SelectPalette(&m_palDraw, FALSE);
dc.RealizePalette();
hDib=::GlobalAlloc(GHND, dwDibLen);
lpBi=(LPBITMAPINFO)::GlobalLock(hDib);
lpBi->bmiHeader=bi;
……
We first fill the information obtained previously into a BITMAPINFOHEADER object. This is necessary
because when calling function ::GetDIBits(…), we need to provide a BITMAPINFOHEADER type pointer
which contains useful information. Here, some unimportant members of BITMAPINFOHEADER are assigned 0s
(biSizeImage, biXPelsPerMeter…). Then the size of the color table is calculated and a global memory
that is big enough for holding bitmap information header and color table is allocated, and the bitmap
information header is stored into the buffers. We will use these buffers to receive color table.
Although the memory size for storing bitmap data can be calculated from the information already
known, usually it is not done at this point. Generally the color table and the bitmap data are retrieved
separately, in the first step, only the memory that is big enough for storing structure BITMAPINFOHEADER and
the color table is prepared. When color table is being retrieved, the bitmap information header will also be
updated at the same time. Since it is more desirable to calculate the bitmap data size using the updated
information, in the sample, the memory size is updated after the color table is obtained successfully, and
the global memory is reallocated for retrieving the bitmap data.
We also need to select logical palette into the DC and realize it so that the bitmap pixels will be
intepreted by its own color table.
Function ::GetDIBits(…) is called in the next step to recieve BITMAPINFOHEADER data and the color
table. Because some device drivers do not fill member biImageSize (This member carries redunant
information with members biWidth, biHeight, and biBitCount), we need to calculate it if necessary:
……
VERIFY
(
::GetDIBits
(
dc.GetSafeHdc(),
(HBITMAP)pBmp->GetSafeHandle(),
0,
(WORD)bi.biHeight,
NULL,
lpBi,
DIB_RGB_COLORS
)
);
bi=lpBi->bmiHeader;
::GlobalUnlock(hDib);
if(bi.biSizeImage == 0)
{
284
Chapter 10. Bitmap
bi.biSizeImage=WIDTHBYTES(bi.biBitCount*bi.biWidth)*bi.biHeight;
}
……
Now the size of DIB data is already known, we can reallocate the buffers, and call function
::GetDIBits(…) again to receive bitmap data. Finally we need to select the logical palette out of the DC,
and return the handle of the global memory before function exits:
……
dwDibLen+=bi.biSizeImage;
hDib=::GlobalReAlloc(hDib, dwDibLen, GHND);
ASSERT(hDib);
lpBi=(LPBITMAPINFO)::GlobalLock(hDib);
ASSERT(hDib);
VERIFY
(
::GetDIBits
(
dc.GetSafeHdc(),
(HBITMAP)pBmp->GetSafeHandle(),
0,
(WORD)bi.biHeight,
(LPSTR)lpBi+sizeof(BITMAPINFOHEADER)+dwSizeCT*sizeof(RGBQUAD),
lpBi,
DIB_RGB_COLORS
)
);
::GlobalUnlock(hDib);
dc.SelectPalette(pPalOld, FALSE);
return hDib;
}
hDib=ConvertDDBtoDIB(&m_bmpDraw);
ASSERT(hDib);
bmf.bfType='MB';
bmf.bfSize=sizeof(bmf)+::GlobalSize(hDib);
bmf.bfReserved1=0;
bmf.bfReserved2=0;
lpBi=(LPBITMAPINFOHEADER)::GlobalLock(hDib);
bi=*lpBi;
dwSizeCT=GetColorTableSize(bi.biBitCount);
bmf.bfOffBits=sizeof(bmf)+bi.biSize+dwSizeCT*sizeof(RGBQUAD);
ar.Write((char *)&bmf, sizeof(bmf));
ar.Write((LPSTR)lpBi, bi.biSize+dwSizeCT*sizeof(RGBQUAD)+bi.biSizeImage);
::GlobalUnlock(hDib);
::GlobalFree(hDib);
}
else
……
}
In the sample, command File | Save is disabled so that the user can only save the image through File |
Save As command by specifying a new file name. To implement this, UPDATE_COMMAND_UI message
handlers are added for both ID_FILE_SAVE and ID_FILE_SAVE_AS commands, and the corresponding
member functions are implemented as follows:
285
Chapter 10. Bitmap
{
pCmdUI->Enable(FALSE);
}
It seems unnecessary to conver the DDB to DIB before saving the image to a disk file because its
original format is DIB. However, if the DDB is changed after being loaded (This is possible for a graphic
editor application), the new DDB is inconsistent with the original DIB data.
The DDB to DIB converting procedure is a little complex. If we are programming for Windows 95 or
Windows NT 4.0, we can create DIB section (will be introduced in later sections) to let the format be
converted automatically. If we are writing Win32 programs that will be run on Windows 3.1, we must use
the method discussed in this section to implement the conversion.
New Functions
Sometimes it is easier to edit DDB directly instead of using DIB data. For example, if we want to
reverse every pixel of the image, we can just call one API funciton to let this be handled by lower level
driver instead of editting every single pixel by ourselves. This is why we need to handle both DIB and
DDB in the applications.
If our application is restricted on edittng only DIB data, we can call an API function directly to draw
DIB in the client window. By doing so, we eleminte the complexity of converting DIB to DDB back and
forth. This function is ::SetDIBitsToDevice(…), which has the following format:
int ::SetDIBitsToDevice
(
HDC hdc,
int XDest, int YDest, DWORD dwWidth, DWORD dwHeight,
int XSrc, int YSrc,
UINT uStartScan, UINT cScanLines,
CONST VOID *lpvBits, CONST BITMAPINFO *lpbmi, UINT fuColorUse
);
There are altogether 12 parameters, whose meanings are listed in the following table:
Parameter Meaning
hdc Handle of target DC.
Xdest, Ydest Position on the target DC where the image should be drawn.
dwWidth, dwHeight Image size on the target DC.
uStartScan Starting scan line of the source bitmap image.
UscanLines Number of scan lines in the source bitmap image that will be output to the target.
lpvBits Pointer to the DIB bits (image data).
Lpbmi Pointer to BITMAPINFO type object.
fuColorUse Specifies where to find the color table. If it is DIB_PALCOLORS, the bmiColors
member of structure BITMAPINFO contains indices into the currently selected
logical palette. If the flag is DIB_RGB_COLORS, bmiColors contains explicit red,
green, blue (RGB) values.
286
Chapter 10. Bitmap
This function can output image at 1:1 ratio with respect to the source image. Similar to
CDC::BitBlt(…) and CDC::StretchBlt(…), there is another function ::StretchDIBits(…), which allows
us to enlarge or reduce the original image and output it to the target device:
int ::StretchDIBits
(
HDC hdc,
int XDest, int YDest, int nDestWidth, int nDestHeight,
int XSrc, int YSrc, int nSrcWidth, int nSrcHeight,
CONST VOID *lpBits, CONST BITMAPINFO *lpBitsInfo, UINT iUsage, DWORD dwRop
);
The ratio between source and target image can be set through the following four parameters:
nDestWidth, nDestHeight, nSrcWidth, nSrcHeight.
public:
CPalette *GetPalette(){return &m_palDraw;}
HGLOBAL GetHDib(){return m_hDIB;}
DWORD GetColorTableSize(WORD);
//{{AFX_VIRTUAL(CGDIDoc)
public:
……
}
When saving image to file, we do not need to convert DDB to DIB any more. Instead,
CGDIDoc::m_hDIB can be used directly for storing data:
hDib=m_hDIB;
ASSERT(hDib);
bmf.bfType='MB';
bmf.bfSize=sizeof(bmf)+::GlobalSize(hDib);
bmf.bfReserved1=0;
bmf.bfReserved2=0;
lpBi=(LPBITMAPINFOHEADER)::GlobalLock(hDib);
bi=*lpBi;
if(bi.biSizeImage == 0)
{
bi.biSizeImage=WIDTHBYTES(bi.biBitCount*bi.biWidth)*bi.biHeight;
}
dwSizeCT=GetColorTableSize(bi.biBitCount);
bmf.bfOffBits=sizeof(bmf)+bi.biSize+dwSizeCT*sizeof(RGBQUAD);
287
Chapter 10. Bitmap
Note since biSizeImage member of BITMAPINFOHEADER structure may be zero, we need to calculate its
value before saving the image to file. Also, the original statement for releasing global memory is deleted
because CGDIDoc::m_hDIB is the only variable that is used for storing image in the application.
Function CGDIDoc::OnUpdateFileSaveAs(…) is changed to the following:
CGDIView();
DECLARE_DYNCREATE(CGDIView)
……
}
void CGDIView::OnInitialUpdate()
{
CGDIDoc *pDoc;
CPalette *pPalDraw;
LPBITMAPINFO lpBi;
LPLOGPALETTE lpLogPal;
int nSizeCT;
int i;
HGLOBAL hData;
CScrollView::OnInitialUpdate();
pDoc=GetDocument();
ASSERT_VALID(pDoc);
hData=pDoc->GetHDib();
if(hData != NULL)
{
lpBi=(LPBITMAPINFO)::GlobalLock(hData);
ASSERT(lpBi);
nSizeCT=pDoc->GetColorTableSize(lpBi->bmiHeader.biBitCount);
288
Chapter 10. Bitmap
pPalDraw=pDoc->GetPalette();
if(pPalDraw->GetSafeHandle() != NULL)pPalDraw->DeleteObject();
VERIFY(pPalDraw->CreatePalette(lpLogPal));
delete [](BYTE *)lpLogPal;
::GlobalUnlock(hData);
m_bBitmapLoaded=TRUE;
SetScrollSizes
(
MM_TEXT,
CSize(lpBi->bmiHeader.biWidth, lpBi->bmiHeader.biHeight)
);
Invalidate();
}
else SetScrollSizes(MM_TEXT, CSize(0, 0));
}
In this functon, DIB handle is obtained from the document and locked. From the global memory
buffers, the color table contained in the DIB is obtained and is used for creating the logical palette. The a
flag is set to indicate that the bitmap is loaded successfully.
In function CGDIView::OnDraw(…), the DIB is painted to the client window:
if(m_bBitmapLoaded == FALSE)return;
CGDIDoc *pDoc=GetDocument();
ASSERT_VALID(pDoc);
hDib=pDoc->GetHDib();
ASSERT(hDib);
lpBi=(LPBITMAPINFO)::GlobalLock(hDib);
ASSERT(lpBi);
pPal=pDoc->GetPalette();
pPalOld=pDC->SelectPalette(pPal, FALSE);
pDC->RealizePalette();
dwBitOffset=
(
sizeof(BITMAPINFOHEADER)+
pDoc->GetColorTableSize
(
lpBi->bmiHeader.biBitCount
)*sizeof(RGBQUAD)
);
::SetDIBitsToDevice
(
pDC->GetSafeHdc(),
0,
0,
lpBi->bmiHeader.biWidth,
lpBi->bmiHeader.biHeight,
0,
0,
0,
lpBi->bmiHeader.biHeight,
(LPSTR)lpBi+dwBitOffset,
lpBi,
DIB_RGB_COLORS
);
pDC->SelectPalette(pPalOld, FALSE);
::GlobalUnlock(hDib);
}
The procedure of selecting and realizing the logical palette is the same with the previous sample. The
difference between them is that function CDC::BitBlt(…) is replaced by function
::SetDIBitsToDevice(…) here.
Message handler CGDI::OnDestroy() is removed through using Class Wizard in the sample. The
reason for this is that we no longer need to select objects (palette, bitmap) out of memory DC any more.
Also, the constructor of CGDIView is changed as follows:
289
Chapter 10. Bitmap
CGDIView::CGDIView()
{
m_bBitmapLoaded=FALSE;
}
With the above modification, the application is able to display any DIB image without doing DIB to
DDB conversion.
Now that we understand different DIB formats, we can easily implement conversion from one format
to another. Sample 10.6\GDI demonstrates how to convert 256-color DIB format to 24-bit format, it is
based on sample 10.5\GDI.
Conversion
We need to delete the color table and expand the indices to explicit RGB combinations in order to
implement this conversoin. Also in the bitmap information header, we need to change the value of member
biBitCount to 24, and recalculate member biImageSize. There is also another difference in the bitmap
header bwteen 256-color and 24-bit formats: for DIB that does not contain the color table, member
biClrUsed is 0; for DIB that contains the color table, this member specifies the number of color indices in
the color table that are actually used by the bitmap.
Current Format
In the sample, a new command Convert | 256 to RGB is added to the mainframe menu
IDR_MAINFRAME, whose command ID is ID_CONVERT_256TORGB. Also, WM_COMMAND and UPDATE_COMMAND_UI
message handlers are added for this command through using Class Wizard. The corresponding functions
are CGDIDoc::OnConvert256toRGB() and CGDIDoc::OnUpdateConvert256toRGB(…) respectively. This
command will be used to convert the image from 256-color format to 24-bit format. We want to disable this
menu item if the current DIB is not 256 color format.
Before doing the conversion, we must know the current format of the image. So in the sample, a new
variable is declared in class CGDIDoc for this purpose:
The following macros are defined in the header file of class CGDIDoc:
#define BMP_FORMAT_NONE 0
#define BMP_FORMAT_MONO 1
#define BMP_FORMAT_16COLOR 2
#define BMP_FORMAT_256COLOR 3
#define BMP_FORMAT_24BIT 4
CGDIDoc::CGDIDoc()
{
……
m_nBmpFormat=BMP_FORMAT_NONE;
}
290
Chapter 10. Bitmap
the image format in this function. The following code fragment shows the modified function
CGDIDoc::GetColorTableSize():
switch(wBitCount)
{
case 1:
{
dwSizeCT=2;
m_nBmpFormat=BMP_FORMAT_MONO;
break;
}
case 4:
{
dwSizeCT=16;
m_nBmpFormat=BMP_FORMAT_16COLOR;
break;
}
case 8:
{
dwSizeCT=256;
m_nBmpFormat=BMP_FORMAT_256COLOR;
break;
}
case 24:
{
dwSizeCT=0;
m_nBmpFormat=BMP_FORMAT_24BIT;
}
}
return dwSizeCT;
}
Function Implementation
Function CGDIDoc::OnUpdateConvert256toRGB(…) is implemented as follows so that the menu
command will be enabled only when the current DIB format is 256-color:
In function CGDIDoc::OnConvert256toRGB(), first we need to lock the current DIB data, calculate the
size of new DIB data (after format conversion) and allocate enough buffers:
void CGDIDoc::OnConvert256toRGB()
{
LPBITMAPINFO lpBi;
LPBITMAPINFO lpBi24;
HGLOBAL hDIB24;
DWORD dwSize;
int nSizeCT;
int i, j;
LPSTR lpRowSrc;
LPSTR lpRowTgt;
BYTE byIndex;
RGBQUAD rgbQuad;
POSITION pos;
CGDIView *ptrView;
lpBi=(LPBITMAPINFO)::GlobalLock(m_hDIB);
ASSERT(lpBi);
nSizeCT=GetColorTableSize(lpBi->bmiHeader.biBitCount);
dwSize=
(
sizeof(BITMAPINFOHEADER)+
WIDTHBYTES(24*lpBi->bmiHeader.biWidth)*lpBi->bmiHeader.biHeight
);
hDIB24=::GlobalAlloc(GHND, dwSize);
ASSERT(hDIB24);
lpBi24=(LPBITMAPINFO)::GlobalLock(hDIB24);
291
Chapter 10. Bitmap
……
}
The new DIB size is stored in local variable dwSize. Here macro WIDTHBYTES is used to calculate the
actual bytes needed for one raster line (We use 24 instead of member biBitCount when using this macro to
implement calculation for the new format). The size of new DIB data is the size of BITMAPINFOHEADER
structure plus the size of bitmap data (Equal to bytes needed for one raster line multiplied by the height of
bitmap, there is no color table any more). Then we allocate buffers from global memory and lock them,
whose address is stored in pointer lpBi24.
Then we need to fill structure BITMAPINFOHEADER. Most of the members are the same for two different
formats, such as biHeight, biWidth. There are three members we need to change: biBitCount must be set
to 24, biImageSize should be recalculated, and biClrUsed needs to be 0:
……
*lpBi24=*lpBi;
lpBi24->bmiHeader.biBitCount=24;
lpBi24->bmiHeader.biSizeImage=WIDTHBYTES
(
24*lpBi->bmiHeader.biWidth
)*lpBi->bmiHeader.biHeight;
lpBi24->bmiHeader.biClrUsed=0;
……
Then we need to fill the DIB bit values. The image is converted pixel by pixel using two loops (one is
embedded within another): the outer loop converts one raster line, and the inner loop converts one single
pixel. As we move to a new raster line, we need to calculate the starting buffer address so that it can be
used as the origin of the pixels for the whole raster line (For each single pixel, we can obtain its address by
adding an offset to the origin address). The starting address of each raster line can be calculated through
multiplying the current line index (0 based) by total number of bytes needed for one raster line. As we
move from one pixel to the next of the same raster line, we can just move to the neighboring buffer (for
RGB format, next three buffers). However, the final pixel of one raster line and the first pixel of next raster
line may not use neighboring buffers, this is because there may exist some unused bits between them (Since
each raster line must use a multiple of 4-byte buffers). The following portion of function
CGDIDoc::OnConvert256toRGB() shows how to convert bitmap pixels from one format to another:
……
for(j=0; j<lpBi->bmiHeader.biHeight; j++)
{
lpRowSrc=
(
(LPSTR)lpBi+
sizeof(BITMAPINFOHEADER)+
nSizeCT*sizeof(RGBQUAD)+
WIDTHBYTES(lpBi->bmiHeader.biBitCount*lpBi->bmiHeader.biWidth)*j
);
lpRowTgt=
(
(LPSTR)lpBi24+
sizeof(BITMAPINFOHEADER)+
WIDTHBYTES(lpBi24->bmiHeader.biBitCount*lpBi24->bmiHeader.biWidth)*j
);
for(i=0; i<lpBi->bmiHeader.biWidth; i++)
{
byIndex=*lpRowSrc;
rgbQuad=lpBi->bmiColors[byIndex];
*lpRowTgt=rgbQuad.rgbBlue;
*(lpRowTgt+1)=rgbQuad.rgbGreen;
*(lpRowTgt+2)=rgbQuad.rgbRed;
lpRowSrc++;
lpRowTgt+=3;
}
}
……
Finally, we must unlock the global memory, release the previous DIB data and assign the new memory
handle to CGDIDoc::m_hDIB. We also need to inform the view to reload the image because the bitmap
format has changed. For this purpose, a new function CGDIView::LoadBitmap(…) is implemented, it will be
called from CGDIDoc::OnConvert256toRGB() and CGDIView::OnInitialUpdate() (The original portion of
292
Chapter 10. Bitmap
this funciton that loads the bitmap is replaced by calling the new function). The following is the portion of
funciton CGDIDoc::OnConvert256toRGB() which shows what should be done after the format is converted:
……
m_hDIB=hDIB24;
m_nBmpFormat=BMP_FORMAT_24BIT;
pos=GetFirstViewPosition();
ptrView=(CGDIView *)GetNextView(pos);
ASSERT(ptrView->IsKindOf(RUNTIME_CLASS(CGDIView)));
ptrView->LoadBitmap(m_hDIB);
}
Function CGDIView::LoadBitmap(…) should implement the following: delete the old palette, check if
the DIB contains color table. If so, create a new palette. The function is declared in class CGDIView as
follows:
The function is implemented as follows (Most part of this function is copied from function
CGDIView::OnInitialUpdate()):
pDoc=GetDocument();
ASSERT_VALID(pDoc);
lpBi=(LPBITMAPINFO)::GlobalLock(hData);
ASSERT(lpBi);
nSizeCT=pDoc->GetColorTableSize(lpBi->bmiHeader.biBitCount);
pPalDraw=pDoc->GetPalette();
if(pPalDraw->GetSafeHandle() != NULL)pPalDraw->DeleteObject();
if(nSizeCT != 0)
{
lpLogPal=(LPLOGPALETTE) new BYTE
[
sizeof(LOGPALETTE)+(nSizeCT-1)*sizeof(PALETTEENTRY)
];
lpLogPal->palVersion=0x300;
lpLogPal->palNumEntries=nSizeCT;
for(i=0; i<nSizeCT; i++)
{
lpLogPal->palPalEntry[i].peRed=lpBi->bmiColors[i].rgbRed;
lpLogPal->palPalEntry[i].peGreen=lpBi->bmiColors[i].rgbGreen;
lpLogPal->palPalEntry[i].peBlue=lpBi->bmiColors[i].rgbBlue;
lpLogPal->palPalEntry[i].peFlags=NULL;
}
VERIFY(pPalDraw->CreatePalette(lpLogPal));
delete [](BYTE *)lpLogPal;
}
::GlobalUnlock(hData);
m_bBitmapLoaded=TRUE;
SetScrollSizes
(
MM_TEXT,
CSize(lpBi->bmiHeader.biWidth, lpBi->bmiHeader.biHeight)
);
Invalidate();
}
293
Chapter 10. Bitmap
void CGDIView::OnInitialUpdate()
{
CGDIDoc *pDoc;
HGLOBAL hData;
CScrollView::OnInitialUpdate();
pDoc=GetDocument();
ASSERT_VALID(pDoc);
hData=pDoc->GetHDib();
if(hData != NULL)LoadBitmap(hData);
else SetScrollSizes(MM_TEXT, CSize(0, 0));
}
With the above implementation, the application is able to convert a bitmap from 256-color format
format to 24-bit format.
Two Cases
To convert a 24-bit format bitmap to 256-color format bitmap, we must extract a color table from the
explicit RGB values. There are two cases that must be handled differently: the bitmap uses less than 256
colors, and the bitmap uses more than 256 colors.
If the bitmap uses less than 256 colors, the conversion is relatively simple: we just examine every pixel
of the bitmap, and extract a color table from all the colors contained in the bitmap.
The following is the conversion procedure for this situation: At the beginning, the color table contains
no color. Then for each pixel in the bitmap, we examine if the color is contained in the color table. If so, we
move to the next pixel. If not, we add the color used by this pixel to the color table. After we go over all the
pixels contained in the bitmap, the color table should contain all the colors that are used by the bitmap
image.
If the bitmap uses more than 256 colors, we must find 256 colors that best represent all the colors used
by the image. There are many algorithms for doing this, a relatively simple one is to omit some lower bits
of RGB values so that maximum number of colors used by a bitmap does not exceed 256. For example, 24-
bit bitmap format uses 8 bit to represent a basic color, it can result in 256×256×256 different colors. If we
use only 3 bits to represent red and green color, and use 2 bits to represent blue color, the total number of
possible combinations are 8×8×4=256.
In this situation, when we examine a pixel, we use the 3 most significant bits of red and green colors,
along with 2 most significant bits of blue color to form a new color that will be used to create color table
(Other bits will be filled with 0s). By doing this, the colors contained in the color table will not exceed 256.
Although this algorithm may result in color distortion, it is relatively fast and less image dependent.
Sample
In the sample, a new command Convert | RGB to 256 is added to mainframe menu IDR_MAINFRAME,
whose command ID is ID_CONVERT_RGBTO256. Also, WM_COMMAND and UPDATE_COMMAND_UI message
handlers are added through using Class Wizard. The new corresponding functions are CGDIDoc::
OnConvertRGBto256() and CGDIDoc::OnUpdateConvertRGBto256(…) respectively.
Function CGDIDoc::OnUpdateConvertRGBto256(…) is implemented as follows:
294
Chapter 10. Bitmap
If the current bitmap format is 24-bit, command Convert | RGB to 256 will be enabled.
The implementation of function CGDIDoc::OnConvertRGBto256() is somehow similar to that of
CGDIDoc::OnConvert256toRGB(): we must first lock the global memory where the current 24-bit bitmap
image is stored, then calculate the size of new bitmap image (256-color format), allocate enough buffers
from global memory, and fill the new bitmap bit values one by one.
The first thing we need to do is creating the color table for the new bitmap image. The following
portion of function CGDIDoc::OnConvertRGBto256() shows how to extract the color table from explicit
RGB colors contained in a 24-bit bitmap image:
void CGDIDoc::OnConvertRGBto256()
{
LPBITMAPINFO lpBi;
LPBITMAPINFO lpBi24;
HGLOBAL hDIB;
DWORD dwSize;
int nSizeCT;
int i, j;
int n;
LPSTR lpRowSrc;
LPSTR lpRowTgt;
POSITION pos;
CGDIView *ptrView;
CPtrArray arRgbQuad;
LPRGBQUAD lpRgbQuad;
RGBQUAD rgbQuad;
BOOL bHit;
BOOL bStandardPal;
BYTE red, green, blue;
AfxGetApp()->DoWaitCursor(TRUE);
lpBi24=(LPBITMAPINFO)::GlobalLock(m_hDIB);
ASSERT(lpBi24);
for(j=0; j<lpBi24->bmiHeader.biHeight; j++)
{
lpRowSrc=
(
(LPSTR)lpBi24+
sizeof(BITMAPINFOHEADER)+
WIDTHBYTES(lpBi24->bmiHeader.biBitCount*lpBi24->bmiHeader.biWidth)*j
);
for(i=0; i<lpBi24->bmiHeader.biWidth; i++)
{
rgbQuad.rgbBlue=*lpRowSrc;
rgbQuad.rgbGreen=*(lpRowSrc+1);
rgbQuad.rgbRed=*(lpRowSrc+2);
rgbQuad.rgbReserved=0;
bHit=FALSE;
for(n=0; n<arRgbQuad.GetSize(); n++)
{
if
(
!memcmp
(
(LPSTR)&rgbQuad,
(LPSTR)arRgbQuad[n],
sizeof(RGBQUAD)
)
)
{
bHit=TRUE;
break;
}
}
if(bHit == FALSE)
{
lpRgbQuad=new RGBQUAD;
*lpRgbQuad=rgbQuad;
arRgbQuad.Add(lpRgbQuad);
}
lpRowSrc+=3;
}
}
……
295
Chapter 10. Bitmap
We examine from the first pixel. The color table will be stored in array arRgbQuad, which is empty at
the beginning. For each pixel, we compare the color with every color contained in the color table, if there is
a hit, we move on to next pixel, otherwise, we add this color to the color table.
The size of color table obtained this way may be less or greater than 256. In the first case, the
conversion is done after the above operation. If the color table size is greater than 256, we must create a
new color table using the alogrithsm discussed above:
……
if(arRgbQuad.GetSize() > 256)
{
while(arRgbQuad.GetSize())
{
delete (LPRGBQUAD)arRgbQuad.GetAt(0);
arRgbQuad.RemoveAt(0);
}
red=green=blue=0;
for(i=0; i<256; i++)
{
lpRgbQuad=new RGBQUAD;
lpRgbQuad->rgbBlue=blue;
lpRgbQuad->rgbGreen=green;
lpRgbQuad->rgbRed=red;
lpRgbQuad->rgbReserved=0;
if(!(red+=32))if(!(green+=32))blue+=64;
arRgbQuad.Add(lpRgbQuad);
}
bStandardPal=TRUE;
}
else bStandardPal=FALSE;
……
If the size of color table is greater than 256, we first delete the color table, then create a new color table
that contains only 256 colors. This color table comprises 256 colors that are evenly distributed in a 8×8×4
3-D space, which has the following contents:
For a 24-bit color, if we use only 3 most significant bits of red and green colors, and 2 most significant
bits of blue color, and set rest bits to 0. Every possible RGB combination (8 bits for each color) has a
corresponding entry in this table.
We use a flag bStandardPal to indicate which algorithm was used to generate the color table. This is
important because for the two situations the procedure of converting explicit RGB values to indices of
color table is different. If the color table is generated directly from the colors contained in the bitmap (first
case), each pixel can be mapped to an index in the color table by comparing it with every color in the color
table (there must be a hit). Otherwise, we must omit some bits before looking up the color table (second
case).
296
Chapter 10. Bitmap
……
nSizeCT=256;
dwSize=
(
sizeof(BITMAPINFOHEADER)+
nSizeCT*sizeof(RGBQUAD)+
WIDTHBYTES(8*lpBi24->bmiHeader.biWidth)*lpBi24->bmiHeader.biHeight
);
hDIB=::GlobalAlloc(GHND, dwSize);
ASSERT(hDIB);
lpBi=(LPBITMAPINFO)::GlobalLock(hDIB);
ASSERT(lpBi);
*lpBi=*lpBi24;
lpBi->bmiHeader.biBitCount=8;
lpBi->bmiHeader.biSizeImage=WIDTHBYTES
(
8*lpBi->bmiHeader.biWidth
)*lpBi->bmiHeader.biHeight;
lpBi->bmiHeader.biClrUsed=0;
The differences between the new and old bitmap information headers are member bitBitCount (8 for
256-color format), biSizeImage, and biClrUsed (Member biClrUsed can be used to indicate the color
usage. For simplicity, it is set to zero).
Next we need to convert explicit RGB values to color table indices. As mentioned before, there are two
situations. If the color table is extracted directly from the bitmap, we must compare each pixel with every
entry of the color table, find the index, and use it as the bitmap bit value. Otherwise the index can be
formed by omitting the lower 5 bits of red and green colors, the lower 6 bits of blue color then combining
them together. This eleminates the procedure of looking up the color table. It is possible for us to do so
because the color table is created in a way that if we implement the above operation on any color contained
in the table, the result will become the index of the corresponding entry.
For example, entry 1 contains color (32, 0, 0), which is (0x20, 0x00, 0x00). After bit omission, it
becomes (0x01, 0x00, 0x00). The followng calculation will result in the index of this entry:
……
for(j=0; j<lpBi->bmiHeader.biHeight; j++)
{
lpRowTgt=
(
(LPSTR)lpBi+
sizeof(BITMAPINFOHEADER)+
nSizeCT*sizeof(RGBQUAD)+
WIDTHBYTES(lpBi->bmiHeader.biBitCount*lpBi->bmiHeader.biWidth)*j
);
lpRowSrc=
(
(LPSTR)lpBi24+
sizeof(BITMAPINFOHEADER)+
WIDTHBYTES(lpBi24->bmiHeader.biBitCount*lpBi24->bmiHeader.biWidth)*j
);
for(i=0; i<lpBi->bmiHeader.biWidth; i++)
{
rgbQuad.rgbBlue=*lpRowSrc;
rgbQuad.rgbGreen=*(lpRowSrc+1);
rgbQuad.rgbRed=*(lpRowSrc+2);
rgbQuad.rgbReserved=0;
297
Chapter 10. Bitmap
if(bStandardPal == TRUE)
{
if(rgbQuad.rgbBlue <= 0xdf)rgbQuad.rgbBlue+=0x20;
if(rgbQuad.rgbGreen <= 0xef)rgbQuad.rgbGreen+=0x10;
if(rgbQuad.rgbRed <= 0xef)rgbQuad.rgbRed+=0x10;
*lpRowTgt=(BYTE)
(
(0x00c0&rgbQuad.rgbBlue) |
((0x00e0&rgbQuad.rgbGreen)>>2) |
((0x00e0&rgbQuad.rgbRed)>>5)
);
}
else
{
for(n=0; n<arRgbQuad.GetSize(); n++)
{
if
(
!memcmp
(
(LPSTR)&rgbQuad,
(LPSTR)arRgbQuad[n],
sizeof(RGBQUAD)
)
)break;
}
*lpRowTgt=(BYTE)n;
}
lpRowSrc+=3;
lpRowTgt++;
}
}
……
Finally, we must unlock the global memory, destroy the original 24 bit bitmap, and assign the new
handle to variable CGDIDoc::m_hDIB. We also need to delete the array that holds the temprory color table
and update the view to redraw the bitmap image:
……
::GlobalUnlock(m_hDIB);
::GlobalFree(m_hDIB);
::GlobalUnlock(hDIB);
m_hDIB=hDIB;
m_nBmpFormat=BMP_FORMAT_256COLOR;
while(arRgbQuad.GetSize())
{
delete (LPRGBQUAD)arRgbQuad.GetAt(0);
arRgbQuad.RemoveAt(0);
}
pos=GetFirstViewPosition();
ptrView=(CGDIView *)GetNextView(pos);
ASSERT(ptrView->IsKindOf(RUNTIME_CLASS(CGDIView)));
ptrView->LoadBitmap(m_hDIB);
AfxGetApp()->DoWaitCursor(FALSE);
}
With this application, we can convert between 256-color and 24-bit bitmap formats back and forth. If
the application is executed on a palette device with 256-color configuration, we may experience color
distortion after converting a 256-color format bitmap to 24-bit format bitmap. This is because for this kind
of bitmap, no logical palette is implemented in the application, so the color approximation method is
applied by the OS.
298
Chapter 10. Bitmap
Sample 10.8\GDI demonstrates how to edit DIB image pixel by pixel. It is based on sample 10.7\GDI.
The application will convert any color image to a black-and-white image. To make the conversion, we need
to examine every pixel and find its brightness, average it, and assign the averaged value to each of the R, G,
B factors. The brightness of a pixel can be calculated by adding up its R, G and B values. For 256-color
format, since all the colors are stored in the color table, we can just convert the color table to a black-and-
white one in order to make this change. No modification needs to be made to the pixels. For 24 bit format,
we need to edit every pixel.
In the sample, a new command Convert | Black White is added to the mainframe menu
IDR_MAINFRAME. Also, WM_COMMAND and UPDATE_COMMAND_UI message handlers are added to class CGDIDoc
through using Class Wizard. The corresponding two new functions are CGDIDoc::OnConvertBlackwhite()
and CGDIDoc::OnUpdateConvertBlackwhite(…) respectively.
Function CGDIDOC::OnUpdateConvertBlackwhite(…) is implemented as follows for supporting both
256-color and 24-bit formats:
For function CGDIDoc::OnConvertBlackwhite(), first we need to lock the global memory that is used
for storing the bitmap, and judge its format by examining biBitCount member of structure
BITMAPINFOHEADER:
void CGDIDoc::OnConvertBlackwhite()
{
LPBITMAPINFO lpBi;
int i, j;
LPSTR lpRow;
RGBQUAD rgbQuad;
POSITION pos;
CGDIView *ptrView;
AfxGetApp()->DoWaitCursor(TRUE);
lpBi=(LPBITMAPINFO)::GlobalLock(m_hDIB);
ASSERT(lpBi);
switch(lpBi->bmiHeader.biBitCount)
{
……
If its value is 8, the format of the bitmap is 256-color. We need to change each color contained in the
color table to either black or white color:
……
case 8:
{
For every color, we add up its R, G, B values and average the result. Then we assign this result to each
of the R, G, B factors.
299
Chapter 10. Bitmap
The 24 bit format is slightly different. We need to examine each pixel one by one and implement the
same conversion:
……
case 24:
{
for(j=0; j<lpBi->bmiHeader.biHeight; j++)
{
lpRow=
(
(LPSTR)lpBi+
sizeof(BITMAPINFOHEADER)+
WIDTHBYTES
(
lpBi->bmiHeader.biBitCount*
lpBi->bmiHeader.biWidth
)*j
);
for(i=0; i<lpBi->bmiHeader.biWidth; i++)
{
rgbQuad.rgbBlue=*lpRow;
rgbQuad.rgbGreen=*(lpRow+1);
rgbQuad.rgbRed=*(lpRow+2);
*(lpRow)=*(lpRow+1)=*(lpRow+2)=
(
rgbQuad.rgbBlue+rgbQuad.rgbGreen+rgbQuad.rgbBlue
)/3;
lpRow+=3;
}
}
break;
}
}
……
Finally, we need to unlock the global memory and update the view to reload the bitmap image.
Based on the knowledge we already have, it is not so difficult for us to enhance the quality of the
images using other image processing methods, such as contrast and brightness adjustment, color
manipulation, etc.
Importance of DDB
By now everything seems fine. We use DIB format to load, store image data. We also use it to draw
images. Withoug converting from DIB to DDB and vice versa, we can manage the image successfully.
However, sometimes it is very inconvenient without DDB. For example, it is almost impossible to
draw an image with transparency by solely using DIB (We will call the transparent part of an image
“background” and the rest part “foreground”). Although we can edit every pixel and change its color, there
is no way for us to prevent a pixel from being drawn, because functon ::SetDIBitsToDevice(…) will
simply copy every pixel contained in an image to the target device (It does not provide different drawing
mode such as bit-wise AND, OR, or XOR).
To draw image with transparency, we need to prepare two images, one is normal image and the other is
mask image. The mask image has the same dimension with the normal image and contains only two colors:
black and white, which indicate if a corresponding pixel contained in the normal image should be drawn or
not. If a pixel in the normal image has a corresponding black pixel in the mask image, it should be drawn.
If the corresponding pixel is white, it should not be drawn. By doing this, any image can be drawn with
transparency.
A DDB image can be painted with various drawing modes: bit-wise AND, OR, XOR, etc. Different
drawing modes will combine the pixels in the source image and the pixels in the target device differently.
Special effects can be made by applying different drawing modes consequently.
300
Chapter 10. Bitmap
When drawing a DDB image, we can use bit-wise XOR along with AND operation to achieve
transparency. First, the normal image can be output to the target device by using bit-wise XOR mode. After
this operaton, the output pattern on the target device is the XORing result of its original pattern and the
normal image. Then the mask bitmap is output to the same position using bit-wise AND mode, so the
background part (corresponding to white pixels in the mask image) of the device still remains unchanged (it
is still the XORing result of the original pattern and the normal image), however, the foreground part
(corresponding to black pixels in the mask image) becomes black. Now lets ouput the normal image to the
target device using bit-wise XOR mode again. For the background part, this is equivalent to XORing the
normal image twice with the original pattern on the target device, which will resume its original pattern
(A^B^A = B). For the foreground part, this operation is equivalent to XORing the normal image with 0s,
which will put the normal image to the device (0^B = B).
Although the result is an image with a transparent background, when we implement the above-
mentioned drawings, the target device will experience pattern changes (Between two XOR operations, the
pattern on the target device is neigher the original pattern nor the normal image, this will cause flickering).
So if we do all these things directly to the device, we will see a very short flickering every time the image is
drawn. To make everything perfect, we can prepare a bitmap in the memory and copy the pattern on the
target device to it, then perform XOR and AND drawing on the memory bitmap. After the the drawing is
complete, we can copy the memory bitmap back to the device. For the memory bitmap, since its
background portion has the same pattern with that of the target device, we will not see any flickering.
To paint a DDB image, we need to prepare a memory DC, select the bitmap into it, then call function
CDC::BitBlt(…) or CDC::StretchBlt(…) to copy the image from one device to another.
In order to draw an image with transparent background, we need to prepare three DDBs: the normal
image, the mask image, and the memory bitmap. For each DDB, we must prepare a memory DC to select
it. Also, because the DDB must be selected out of DC after drawing, we need to prepare a CBitmap type
pointer for each image.
Since the usr can load a new image when there is an image being displayed, we need to check the
states of variables that are used to implement DCs and bitmaps. Generally, before creating a new memory
DC, it would be safer to check if the DC has already been initialized. If so, we need to delete the current
DC and create a new one. Before deleting a DC, we further need to check if there are objects (such as
bitmap, palette) currently being selected. All the objects created by the user must be selected out before a
DC is delected. The DC can be deleted by calling function CDC::DeleteDC(). Also, before creating a
bitmap, we need to check if the bitmap has been initialized. If so, before creating a new bitmap, we need to
call function CGDIObject::DeleteObject() to destroy the current bitmap first.
Functions CBitmap::CreateBitmap(…) and CDC::CreateCompatibleDC(…) will fail if CBitmap and CDC
type variables have already been initialized.
If a logical palette is implemented, we must select it into every DC before performing drawing
operations. Before the application exits, all the objects selected by the DCs must be selected out, otherwise
it may cause the system to crash.
Although we can prepare normal image and mask image separately, it is not the most convenient way
to implement transparent background. The mask image can also be generated from the normal image so
long as all the background pixels of the normal image are set to the same color (For example, white). In this
situation, pixel in the mask image can be set by examing the corresponding pixel in the normal image: if it
is the background color, the pixel in the mask image should be set to white, otherwise it should be set to
black.
DIB Section
Both DIB and DDB are needed in order to implement transparent background drawing: we need DIB
format to generate mask image, and need DDB to draw the image. Of course we can call ::GetDIBits(…)
and ::SetDIBits(…) to convert between DIB and DDB format, however, there exists an easier way to let
us handle DIB and DDB simultaneously.
A DIB section can be created to manage the image so that we can have both the DIB and DDB features
without doing the conversion. A DIB section is a memory section that can be shared between the process
and the system. When a change is made within the process, it is automatically updated to the system. By
doing this, there is no need to update the data using functions ::GetDIBits(…) and ::SetDIBits(…).
301
Chapter 10. Bitmap
We can call function ::CreateDIBSection(…) to create a DIB section. This function will return an
HBITMAP handle, which can be attached to a CBitmap variable by calling function CBitmap::Attach(…).
Function ::CreateDIBSection(…) has six parameters:
HBITMAP ::CreateDIBSection
(
HDC hdc,
CONST BITMAPINFO *pbmi, UINT iUsage, VOID *ppvBits, HANDLE hSection,
DWORD dwOffset
);
Parameter Meaning
hdc The handle of the device context.
pbmi A BITMAPINFO type pointer that provides the bitmap information.
iUsage Specifies data type contained in member bmiColors of structure BITMAPINFO, which is
pointed by pbmi. If the value is DIB_PAL_COLORS, array bmiColors contains logical palette
indices; if the value is DIB_RGB_COLORS, array bmiColors contains literal RGB values.
ppvBits A pointer that can be used to receive device-independent bitmap’s bit values.
hSection Specifies the section handle, we can pass a file handle to it to allow the bitmap to be mapped
to the file. If we pass NULL, operating system will automatically allocate memory for
bitmap mapping.
dwOffset Specifies the offset from the beginning of the file to the bitmap data if the bitmap will be
mapped to a file. The parameter will be neglected if hSection is NULL.
After calling this function, we can access the buffers pointed by ppvBits and make change to the DIB
bits directly, there is no need for us to do any DIB to DDB conversion or vice versa. After the change is
made, we can draw the bitmap immediately by calling funciton CDC::BitBlt(…), this will draw the updated
image to the window.
New Variables
The following new variables are declared in class CGDIView for drawing bitmap with transparancy:
Altogether there are four CBitmap type variables, three CBitmap type pointers, three CDC type variables,
and three CPalette type pointers. Their meanings are explained in the following table:
Variable Meaning
m_bmpDraw Load the normal bitmap image.
m_bmpMask Store the mask bitmap image.
m_bmpBS Used for creating memory bitmap.
m_dcMem Selects m_bmpDraw.
302
Chapter 10. Bitmap
To make the application more interesting, we will also draw the background of client window using
bitmap. This bitmap will be loaded into m_bmpBkd variable.
The six pointers are initialized to NULL in the constructor:
CGDIView::CGDIView()
{
m_bBitmapLoaded=FALSE;
m_pBmpOld=NULL;
m_pBmpMaskOld=NULL;
m_pBmpBSOld=NULL;
m_pPalOld=NULL;
m_pPalMaskOld=NULL;
m_pPalBSOld=NULL;
}
Cleaning Up
A new function CGDIView::CleanUp() is added to the application for doing the clean up job. Within
this function, all the objects selected by the DCs are selected out, then DCs and bitmaps are deleted:
void CGDIView::CleanUp()
{
if(m_pPalOld != NULL)
{
m_dcMem.SelectPalette(m_pPalOld, FALSE);
m_pPalOld=NULL;
}
if(m_pPalMaskOld != NULL)
{
m_dcMemMask.SelectPalette(m_pPalMaskOld, FALSE);
m_pPalMaskOld=NULL;
}
if(m_pPalBSOld != NULL)
{
m_dcMemBS.SelectPalette(m_pPalBSOld, FALSE);
m_pPalBSOld=NULL;
}
if(m_pBmpOld != NULL)
{
m_dcMem.SelectObject(m_pBmpOld);
m_pBmpOld=NULL;
}
if(m_pBmpMaskOld != NULL)
{
m_dcMemMask.SelectObject(m_pBmpMaskOld);
m_pBmpMaskOld=NULL;
}
if(m_pBmpBSOld != NULL)
{
m_dcMemBS.SelectObject(m_pBmpBSOld);
m_pBmpBSOld=NULL;
}
if(m_dcMem.GetSafeHdc() != NULL)m_dcMem.DeleteDC();
if(m_dcMemMask.GetSafeHdc() != NULL)m_dcMemMask.DeleteDC();
if(m_dcMemBS.GetSafeHdc() != NULL)m_dcMemBS.DeleteDC();
if(m_bmpDraw.GetSafeHandle() != NULL)m_bmpDraw.DeleteObject();
if(m_bmpMask.GetSafeHandle() != NULL)m_bmpMask.DeleteObject();
if(m_bmpBS.GetSafeHandle() != NULL)m_bmpBS.DeleteObject();
}
303
Chapter 10. Bitmap
If a pointer is not NULL, it means that there is an object being currently selected by the DC, so
funciton CGDIObject::SelectObject(…) is called to select the object (palette or bitmap) out of the DC
before destroying it.
We need to call this function just before the application exits. In the sample, a WM_DESTROY message
handler is added to class CGDIView through using Class Wizard. The corresponding member function is
implemented as follows:
void CGDIView::OnDestroy()
{
CleanUp();
CScrollView::OnDestroy();
}
pDoc=GetDocument();
ASSERT_VALID(pDoc);
AfxGetApp()->DoWaitCursor(TRUE);
lpBi=(LPBITMAPINFO)::GlobalLock(hData);
ASSERT(lpBi);
nSizeCT=pDoc->GetColorTableSize(lpBi->bmiHeader.biBitCount);
pPalDraw=pDoc->GetPalette();
if(pPalDraw->GetSafeHandle() != NULL)pPalDraw->DeleteObject();
if(nSizeCT != 0)
{
lpLogPal=(LPLOGPALETTE) new BYTE
[
sizeof(LOGPALETTE)+(nSizeCT-1)*sizeof(PALETTEENTRY)
];
lpLogPal->palVersion=0x300;
lpLogPal->palNumEntries=nSizeCT;
for(i=0; i<nSizeCT; i++)
{
lpLogPal->palPalEntry[i].peRed=lpBi->bmiColors[i].rgbRed;
lpLogPal->palPalEntry[i].peGreen=lpBi->bmiColors[i].rgbGreen;
lpLogPal->palPalEntry[i].peBlue=lpBi->bmiColors[i].rgbBlue;
lpLogPal->palPalEntry[i].peFlags=NULL;
}
VERIFY(pPalDraw->CreatePalette(lpLogPal));
delete [](BYTE *)lpLogPal;
pPalOld=dc.SelectPalette(pPalDraw, FALSE);
dc.RealizePalette();
}
CleanUp();
hBmp=::CreateDIBSection
(
dc.GetSafeHdc(),
lpBi,
DIB_RGB_COLORS,
(void **)&pBits,
NULL,
0
);
304
Chapter 10. Bitmap
memcpy
(
(LPSTR)pBits,
(LPSTR)lpBi+sizeof(BITMAPINFOHEADER)+nSizeCT*sizeof(RGBQUAD),
lpBi->bmiHeader.biSizeImage
);
ASSERT(hBmp);
m_bmpDraw.Attach(hBmp);
……
Please note that function CGDIView::CleanUp() is called before the bitmap is created. After the DIB
section is created, we use the DIB data passed through hData parameter to initialize the image. The buffers
that store DIB bit values are pointed by pointer pBits. We can use it to edit the image pixels directly, there
is no need to convert between DDB and DIB foramts any more.
After the bitmap is loaded, we need to create the mask bitmap, memory bitmap, and theree memory
DCs. We also need to select the bitmaps and the logical palette into the DCs if necessary:
……
m_dcMem.CreateCompatibleDC(&dc);
m_dcMemMask.CreateCompatibleDC(&dc);
m_dcMemBS.CreateCompatibleDC(&dc);
ASSERT(m_dcMem.GetSafeHdc());
ASSERT(m_dcMemMask.GetSafeHdc());
ASSERT(m_dcMemBS.GetSafeHdc());
if(nSizeCT != 0)
{
m_pPalOld=m_dcMem.SelectPalette(pPalDraw, FALSE);
m_pPalMaskOld=m_dcMemMask.SelectPalette(pPalDraw, FALSE);
m_pPalBSOld=m_dcMemBS.SelectPalette(pPalDraw, FALSE);
}
m_bmpMask.CreateCompatibleBitmap
(
&dc,
lpBi->bmiHeader.biWidth,
lpBi->bmiHeader.biHeight
);
m_bmpBS.CreateCompatibleBitmap
(
&dc,
lpBi->bmiHeader.biWidth,
lpBi->bmiHeader.biHeight
);
m_pBmpOld=m_dcMem.SelectObject(&m_bmpDraw);
m_pBmpMaskOld=m_dcMemMask.SelectObject(&m_bmpMask);
m_pBmpBSOld=m_dcMemBS.SelectObject(&m_bmpBS);
……
The mask and memory bitmaps must be created by calling function CBitmap::
CreateCompatibleBitmap(…), this will allow the created bitmaps to be compatible with the device context.
Next, the mask bitmap is generated from the normal bitmap image:
……
for(j=0; j<lpBi->bmiHeader.biHeight; j++)
{
for(i=0; i<lpBi->bmiHeader.biWidth; i++)
{
if(m_dcMem.GetPixel(i, j) == RGB(255, 255, 255))
{
m_dcMemMask.SetPixel(i, j, RGB(255, 255, 255));
}
else m_dcMemMask.SetPixel(i, j, RGB(0, 0, 0));
}
}
m_bBitmapLoaded=TRUE;
SetScrollSizes
(
MM_TEXT,
CSize(lpBi->bmiHeader.biWidth, lpBi->bmiHeader.biHeight)
);
::GlobalUnlock(hData);
if(nSizeCT != 0)dc.SelectPalette(pPalOld, FALSE);
Invalidate();
305
Chapter 10. Bitmap
AfxGetApp()->DoWaitCursor(FALSE);
}
Every pixel of the normal image is examined to generate the mask image. Here functions
CDC::GetPixel(…) and CDC::SetPixel(…) are called for manipulating single pixels. Although the two
functions hide the details of device context and bitmap format, they are very slow, and should not be used
for fast bitmap drawing or image processing.
if(m_bBitmapLoaded == FALSE)return;
CGDIDoc *pDoc=GetDocument();
ASSERT_VALID(pDoc);
pPal=pDoc->GetPalette();
pPalOld=pDC->SelectPalette(pPal, FALSE);
pDC->RealizePalette();
m_bmpDraw.GetBitmap(&bm);
m_dcMemBS.BitBlt
(
0,
0,
bm.bmWidth,
bm.bmHeight,
pDC,
0,
0,
SRCCOPY
);
m_dcMemBS.BitBlt
(
0,
0,
bm.bmWidth,
bm.bmHeight,
&m_dcMem,
0,
0,
SRCINVERT
);
m_dcMemBS.BitBlt
(
0,
0,
bm.bmWidth,
bm.bmHeight,
&m_dcMemMask,
0,
0,
SRCAND
);
m_dcMemBS.BitBlt
(
0,
0,
bm.bmWidth,
bm.bmHeight,
&m_dcMem,
0,
0,
SRCINVERT
);
pDC->BitBlt
(
0,
0,
bm.bmWidth,
bm.bmHeight,
306
Chapter 10. Bitmap
&m_dcMemBS,
0,
0,
SRCCOPY
);
pDC->SelectPalette(pPalOld, FALSE);
}
In the above function, the pattern on the target device is first copied to the memory bitmap. Then
function CDC::BitBlt(…) is called three times to draw the normal image and mask image on the memory
bitmap, with two XOR drawings of the normal image (first and thrid operations) and one AND mode
drawing of the mask image (second operation). Finally, the new pattern in the memory bitmap is copied
back to the targert device.
Adding Background
If the window’s background is also white, it is difficult for us to see the transparency effect. To show
this effect, in the sample, the background of the client window is also painted with a bitmap image.
The image that is used to paint the background is prepared as a resource, whose ID is IDB_BITMAPBKD.
The bitmap is loaded to variable CGDIView::m_bmpBkd in function CGDIView:: OnInitialUpdate():
void CGDIView::OnInitialUpdate()
{
CGDIDoc *pDoc;
HGLOBAL hData;
BITMAP bm;
if(m_bmpBkd.GetSafeHandle() == NULL)
{
m_bmpBkd.LoadBitmap(IDB_BITMAPBKD);
m_bmpBkd.GetBitmap(&bm);
m_bmpBkd.SetBitmapDimension(bm.bmWidth, bm.bmHeight);
ASSERT(m_bmpBkd.GetSafeHandle());
}
CScrollView::OnInitialUpdate();
……
}
pt=GetScrollPosition();
dcMem.CreateCompatibleDC(pDC);
pBmpOld=dcMem.SelectObject(&m_bmpBkd);
size=m_bmpBkd.GetBitmapDimension();
GetClientRect(rect);
rect.right+=pt.x;
rect.bottom+=pt.y;
nRepX=(rect.Width()+size.cx-1)/size.cx;
nRepY=(rect.Height()+size.cy-1)/size.cy;
for(i=0; i<nRepX; i++)
{
for(j=0; j<nRepY; j++)
{
pDC->BitBlt
(
i*size.cx-pt.x,
j*size.cy-pt.y,
size.cx,
307
Chapter 10. Bitmap
size.cy,
&dcMem,
0,
0,
SRCCOPY
);
}
}
dcMem.SelectObject(pBmpOld);
return TRUE;
}
This function simply draw the bitmap image repeatedly so that the whole client area is covered by the
image.
To test this sample, we may use it to load any bitmap images with white background.
Figure 10-1 shows the result after 10.9\Rose.bmp is loaded into the application.
Algorithm
The chiseled effect can be implemented with the following algorithm: find out the object’s outline,
imagine some parallel lines with 135° angle are drawn from the upper left side to bottom right side (Figure
10-3). Think these lines as rays of light. If we draw the portion of the outline that first encounters these
parallel lines (the portion facing the light) with shadowed color, and draw the rest part of the outline (the
308
Chapter 10. Bitmap
portion facing away fromt he light) with highlighted color, the object will have a chiseled effect. If we
swap the shadowed and highlighted colors, it will result in an embossed effect.
If we have a 2-D binary image (an image that contains only two colors: white (255, 255, 255) and
black (0, 0, 0)), the outline can be generated by combining the inverse image with the original image at an
offset origin. For example, the outline that should be drawn using the shadowed color can be generated
with the following steps:
Draw this
portion with
shadowed color
Draw this
portion with
highlighted color
Figure 10-3. Implement chiseled effect
The highlighted outline can be obtained in the same way, but we need to combine the original bitmap
and the inverted bitmap differently here:
If we combine the two outlines and paint them with highlighted and shadowed colors respectively, then
fill the rest part with a normal color (A color between the highlighted and shadowed color), we will have a
3D effect. For example, we can use white as the highlighted color, dark gray as the shadowed color, and
light gray as the normal color.
Draw original
image at (0, 0) Resulted outline
Combine them
using bit-wise OR
operation
Draw inverted
image at (1, 1)
Figure 10-4. Generate the outline that should be drawn with shadowed color
309
Chapter 10. Bitmap
Draw original
image at (1, 1)
Combine them
using bit-wise OR
operation
Draw inverted
image at (0, 0)
Resulted outline
Figure 10-5. Generate the outline that should be drawn with highlighted color
(Brush Color) XOR (Destinaton Color) AND (Source Color) XOR (Brush Color)
The reason is simple: After the first operation (between the brush and destination pixels), the pixels in
the target device will become the XOR combination between the original pixel colors and the brush color.
Next, this XORed result will be ANDed with the source bitmap (Only the outlined part is black, rest part is
white), the outlined part on the target device will become black and the rest part remains unchanged (still
310
Chapter 10. Bitmap
XORed result from the first operation). Then we do XOR again between the target device and the brush, for
the outlined part, this operation will fill it with the brush color (A ^ 0 = A); for the rest part, this will
resume the original color for every pixel (A ^ B ^ A = B).
Chiselled Effect
With the following steps, we can create chiselled effect:
This will draw one of the outlines. Creating new brush and repeating the above steps using the other
mask bitmap can result in the chiselled effect.
The first and fourth steps can be implemented by calling function CDC::PatBlt(…), which allows us to
fill a bitmap with a brush using specified operation mode:
The last parameter allows us to specify how to combine the color of the brush with the destination
pixels. To do bitwise XOR, we need to specify PATINVERT mode.
So we can call CDC::PatBlt(…), CDC::BitBlt(…) and CDC::PatBlt(…) again to draw the outline using
the brush created by our own. However, there is a simpler way. When calling function CDC::BitBlt(…), we
can pass it a custom operation code and let it do the above-mentioned operations in one stroke.
To find out the custom operation code, we need to enumerate all the possible results from the
combinations among brush, destination and source bits for our raster operation:
(Brush Color) XOR (Destinaton Color) AND (Source Color) XOR (Brush Color)
The following table lists all the possible results from the above fomulae:
The sequence in the table must be arragned so that brush is in the first column, source pixel in the
second column and destination pixel in the third column. The code resulted from the combination of the
three bits must increment by one for adjacent rows (In the above sample, for the first row it is 000, second
row it is 001, third row it is 010…). If we read the output from the bit contained in the last row to the bit in
the first row (10111000), we will have 0xB8.
With this index, we can find a raster code that will implement this typical operation for either
CDC::BitBlt(…) or CDC::StretchBlt(…) calling. The table is documented in Win32 programming, we can
also find it in Appendix B.
By looking up the table, we know that the raster code needs to be use is 0x00B8074A.
311
Chapter 10. Bitmap
The following is a list of some values of nIndex parameter that could be used to retrieve some standard
colors in the system:
Parameter Meaning
COLOR_3DFACE Face color that should be used to draw three-dimensional
COLOR_BTNFACE
display elements such as a button.
COLOR_3DHILIGHT Highlight color of three-dimensional display elements, this
COLOR_3DHIGHLIGHT
COLOR_BTNHILIGHT color is used for drawing edges facing the light source.
COLOR_BTNHIGHLIGHT
COLOR_3DLIGHT Light color of three-dimensional display elements, this color is
used for drawing edges facing the light source.
COLOR_3DSHADOW Shadow color for three-dimensional display elements, this
COLOR_BTNSHADOW
color is used for drawing edges facing away from the light
source.
In the sample, we choose COLOR_BTNHIGHLIGHT as the highlighted color, and COLOR_BTNSHADOW as the
shadowed color.
New Function
In the sample, a new function CreateGrayedBitmap(…) is declared in class CGDIView to create grayed
image from a nomal bimap:
The only parameter to this function is a CBitmap type pointer. The function will return an HBITMAP
handle, which is the grayed bitmap. Within the function, we must prepare three bitmaps: the bitmap that
will be used to store the final grayed image, the mask image that stores the shadowed outline, and the mask
image that stores the highlighted outline. The function starts with creating these bitmaps:
dcMono.CreateCompatibleDC(&dc);
dcColor.CreateCompatibleDC(&dc);
ASSERT(dcMono.GetSafeHdc());
ASSERT(dcColor.GetSafeHdc());
pBmp->GetBitmap(&bm);
312
Chapter 10. Bitmap
The final grayed image will be stored in variable bmpGray. We will refer image created by this variable
as “grayed image”, although the image may not be grayed in the interim.
The other two CBitmap type variables, bmpHilight and bmpShadow will be used to store the outline
mask images. We need two memory DCs, one used to select the color bitmap (normal image passed
through pointer pBmp, and grayed image bmpGray) and one for binary bitmaps (the outline mask bitmaps).
Note that binary bitmaps are created with bit count per pixel set to 1 and the grayed bitmap (actually it is a
color bitmap, but we use only monochrom colors) is created by calling function CBitmap::
CreateCompatibleBitmap(…). Since the DC supports color bitmap (If the program is being run on a system
with a color monitor) , this will create a color bitmap compatible with the window DC.
Then we select the color bitmap and the binary bitmaps into the memory DCs and create the outline
mask images:
……
pBmpShadowOld=dcMono.SelectObject(&bmpShadow);
dcMono.FillSolidRect(0, 0, bm.bmWidth, bm.bmHeight, RGB(255, 255, 255));
dcMono.BitBlt
(
0, 0,
bm.bmWidth-1, bm.bmHeight-1,
&dcColor,
1, 1,
SRCCOPY
) ;
dcMono.BitBlt
(
0, 0,
bm.bmWidth, bm.bmHeight,
&dcColor,
0, 0,
MERGEPAINT
);
dcMono.SelectObject(pBmpShadowOld);
……
First we fill the mask bitmap with white color. Then we copy the patterns from the original image to
the mask bitmap image. When doing this copy, the first horizontal line (the upper-most line) and the first
vertical line (the left-most vertical line) are eleminated from the original image (Pixels with coordinates (0,
y) and (x, 0) are elemented, where x can be 0, 1, … , up to width of image -1; y can be 0, 1, …, up to height
of image -1). The colors contained in the souce bitmap will be automatically converted to black and white
colors when we call function CDC::BitBlt(…) because the target image is a binary bitmap. The souce
image is copied to the mask bitmap at the position of (0, 0). Then the original bitmap is inverted, and
merged with the mask image with bit-wise OR operation. Here flag MERGEPAINT allows the pixels in the
souce image and pixels in the target image to be combined in this way. After these operations, the binary
bitmap image will contain the outline that should be drawn with the shadowed color.
The following portion of the function generates the highlighted outline:
……
pBmpHilightOld=dcMono.SelectObject(&bmpHilight);
dcMono.BitBlt
(
0, 0,
bm.bmWidth, bm.bmHeight,
&dcColor,
0, 0,
SRCCOPY
);
dcMono.BitBlt
(
0, 0,
bm.bmWidth-1, bm.bmHeight-1,
&dcColor,
1, 1,
MERGEPAINT
);
313
Chapter 10. Bitmap
dcMono.SelectObject(pBmpHilightOld);
dcColor.SelectObject(pBmpOld);
pBmpOld=dcColor.SelectObject(&bmpGray);
……
Next we create a brush with standard button face color and used it to fill the grayed image (By default,
the standard button face color is light gray. It can also be customized to other colors):
……
brush.CreateSolidBrush(::GetSysColor(COLOR_BTNFACE));
pBrOld=dcColor.SelectObject(&brush);
dcColor.PatBlt
(
0, 0,
bm.bmWidth, bm.bmHeight,
PATCOPY
);
dcColor.SelectObject(pBrOld);
brush.DeleteObject();
dcColor.SetBkColor(RGB(255, 255, 255));
dcColor.SetTextColor(RGB(0, 0, 0));
……
The button face color is retrieved by calling ::GetSystColor(…) API function. Actually, all the
standard colors defined in the system can be retrieved by calling this function. Next, we draw the
highlighted outline of the grayed bitmap using the standard highlighted color:
……
brush.CreateSolidBrush(::GetSysColor(COLOR_BTNHIGHLIGHT));
pBrOld=dcColor.SelectObject(&brush);
pBmpHilightOld=dcMono.SelectObject(&bmpHilight);
dcColor.BitBlt
(
0, 0,
bm.bmWidth, bm.bmHeight,
&dcMono,
0, 0,
0x00B8074A
);
dcColor.SelectObject(pBrOld);
brush.DeleteObject();
dcMono.SelectObject(pBmpHilightOld);
……
Also, the shadowed outline is drawn on the grayed image in the same way:
……
brush.CreateSolidBrush(::GetSysColor(COLOR_BTNSHADOW));
pBrOld=dcColor.SelectObject(&brush);
pBmpShadowOld=dcMono.SelectObject(&bmpShadow);
dcColor.BitBlt
(
0, 0,
bm.bmWidth, bm.bmHeight,
&dcMono,
0, 0,
0x00B8074A
);
dcColor.SelectObject(pBrOld);
brush.DeleteObject();
dcMono.SelectObject(pBmpShadowOld);
dcColor.SelectObject(pBmpOld);
return (HBITMAP)bmpGray.Detach();
}
Finally, some clean up routines. Before this function exits, we call function CBitmap::Detach() to
detach HBITMAP type handle from CBitmap type variable. This is because we want to leave the HBITMAP
handle for further use. If we do not detach it, when CBitmap type variable goes out of scope, the destructor
314
Chapter 10. Bitmap
will destroy the bitmap automatically, and therefore, the bitmap handle will no longer be valid from then
on.
pDoc=GetDocument();
ASSERT_VALID(pDoc);
AfxGetApp()->DoWaitCursor(TRUE);
……
memcpy
(
(LPSTR)pBits,
(LPSTR)lpBi+sizeof(BITMAPINFOHEADER)+nSizeCT*sizeof(RGBQUAD),
lpBi->bmiHeader.biSizeImage
);
ASSERT(hBmp);
m_bmpDraw.Attach(hBmp);
hBmpGray=CreateGrayedBitmap(&m_bmpDraw);
ASSERT(hBmpGray);
m_bmpDraw.DeleteObject();
m_bmpDraw.Attach(hBmpGray);
m_dcMem.CreateCompatibleDC(&dc);
ASSERT(m_dcMem.GetSafeHdc());
……
}
The bitmap that was originally created by function ::CreateDIBSection(…) is destroyed. Finally,
function CGIDView::OnDraw(…) is changed to draw the grayed bitmap to the client window:
315
Chapter 10. Bitmap
BITMAP bm;
if(m_bBitmapLoaded == FALSE)return;
CGDIDoc *pDoc=GetDocument();
ASSERT_VALID(pDoc);
pPal=pDoc->GetPalette();
pPalOld=pDC->SelectPalette(pPal, FALSE);
pDC->RealizePalette();
m_bmpDraw.GetBitmap(&bm);
pDC->BitBlt
(
0,
0,
bm.bmWidth,
bm.bmHeight,
&m_dcMem,
0,
0,
SRCCOPY
);
pDC->SelectPalette(pPalOld, FALSE);
}
With the above implementations, the application is able to create grayed images with chiselled effect.
Summary
1) Usually an image is stored to disk using DIB format. To display it on a specific type of device, we
must first convert it to DDB format, which may be different from device to device.
1) To draw bitmap, we must prepare a memory DC, select the bitmap into it, and copy the image between
the memory DC and target DC.
1) Function CDC::BitBlt(…) can be used to draw the image with 1:1 ratio. To draw an image with an
enlarged or shrunk size, we need to use function CDC::StretchBlt(…).
1) A DIB contains three parts: 1) Bitmap information header. 2) Color Table. 3) DIB bit values. For the
DIB files stored on the disk, there is an extra bitmap file header ahead of DIB data.
1) We can call function ::GetDIBits(…) to get DIB bit values from a DDB selected by a DC, and call
function ::SetDIBits(…) to set DIB bit values to the DDB.
1) There are several DIB formats: monochrom format (2 colors), 16-color format, 256-color format, 24-
bit format. For each format, the total number of colors contained in the color table is different. The
pixels of 24-bit DIB contain explict RGB values, for the rest formats, they contain indices to a color
table which resides in the bitmap information header.
1) In order to accelarate bitmap image loading and saving, each raster line of the imge must use multiple
of 4 bytes for storing the image data. The extra buffers will simply be wasted if there are not enough
image data.
1) The following information contained in the bitmap information header is very important: 1) The
dimension of the image (width and height). 2) Bit count per pixel. 3) Image size, which can be
calculated from the image dimension and bit cout per pixel.
1) The size of a color table (in number of bytes) can be calculated from the following formula:
10) The total image size can be calculated from the following formula:
(size of bitmap information header) + (number of bytes for one raster line) × image height
316
Chapter 10. Bitmap
(size of bitmap information header) + size of color table + (number of bytes for one raster line) ×
image height
Before preparing DIB, we need the above information to allocate enough buffers for storing DIB data.
11) Function ::SetDIBitsToDevice(…) can be used to draw DIB directly to a device. We don’t need to
implement any DIB-to-DDB conversion. However, using this function, we also lose the control over
DDB.
1) DIB section can be created for managing the image in both DIB and DDB format.
1) Bitmap with transparency can be implemented by using a mask image, and drawing the normal and
mask images using bit-wise XOR and AND operation modes.
1) We can convert a color bitmap to a grayed image with chiselled or embossed effect by finding out the
outline of the object, then drawing the portion facing the light with highlighted (shadowed) color and
the portion facing away from the light with shadowed (highlighted) color.
317
Chapter 11. Sample: Simple Paint
Chapter 11
T
his chapter introduces a series of samples imitating standard “Paint” application, using the
knowledge from previous chapters. Also, some very useful concepts such as region, path are
discussed. By the end of this chapter, we will be able to build simple graphic editor applications.
Samples in this chapter are specially designed to work on 256-color palette device. To
customize them for non-palette devices, we can just eleminate logical palette creation and realization
procedure.
11.0 Preparation
With the knowledge we already have, it is possible for us to built a simple graphic editor now. So lets
start to build an application similar to “Paint”. Sample 11.0\GDI is a starting application whose structure is
similar to what we have implemented in previous chapters. The application has the following
functionalities: 1) Device independent bitmap loading and saving. 2) DIB to DDB conversion
(implemented through DIB section). 3) Displaying DDB using function CDC::BitBlt(…). Lets first take a
look at class CGDIDoc:
public:
CPalette *GetPalette(){return &m_palDraw;}
HBITMAP GetHDib(){return m_hDIB;}
DWORD GetColorTableSize(WORD);
//{{AFX_VIRTUAL(CGDIDoc)
public:
virtual BOOL OnNewDocument();
virtual void Serialize(CArchive& ar);
//}}AFX_VIRTUAL
……
};
We will support only 256 color device, so in the constructor, a logical palette with size of 256 is
created, the first 20 entries are filled with predefined colors. Later when we implement the application,
colors contained in the first 20 entries of this logical palette will be displayed on a color bar that could be
used by the user for interactive drawing. In the sample, variable CGDIDoc::m_palDraw implements a logical
palette, which will be used throughout the application’s lifetime. In the constructor, a default palette is
created and the current colors in the system palette are used to initialize the logical palette (Of course, we
can also initialize the logical palette with user-defined colors). When a new bitmap is loaded, colors
contained in the color table of the bitmap will be used to fill the logical palette.
After the DIB is loaded, its handle will be stored in variable CGDIDoc::m_hDIB, which is initialized to
NULL in the constructor. In function CGDIDoc::Serialize(…), the bitmap is loaded into memory and
stored to disk.
318
Chapter 11. Sample: Simple Paint
Function CGDIDoc::GetHDib() and CGDIDoc::GetPalette() let us access the DIB and logical palette
outside class CGDIDoc.
The following is a portion of class CGDIView:
public:
CGDIDoc* GetDocument();
void LoadBitmap(HGLOBAL);
void CleanUp();
……
}
Here variable m_bmpDraw is used to store the device dependent bitmap, variable m_dcMem is the memory
DC that will be used to select this bitmap. Other two pointers m_pBmpOld and m_pPalOld will be used to
resume m_dcMem’s original state.
Function CGDIView::LoadBitmap(…) will be called from function CGDIView::OnInitialUpdate(),
when a new bitmap is loaded by the application. In this function a DIB section will be created, and the
returned HBITMAP handle will be attached to variable CGDIView::m_bmpDraw. So any operation on the DDB
will be reflected to DIB bit values. Also if we modify DIB bits, the DDB will be affected automatically. In
the function, the color table contained in the DIB is extracted, and the entries of the logical palette
(implemented in the document) are updated with the colors contained in the bitmap file by calling function
CPalette::SetPaletteEntries(…).
After a bitmap is loaded, it will be painted to the client window by calling CDC::BitBlt(…) in function
CGDIView::OnDraw(…). Every time before the bitmap is painted, the logical palette contained in the
document is selected into the target DC and realized. By doing this, we can avoid color distortion.
Function CGDIView::CleanUp() selects the palette and bitmap out of the DC, then deletes the memory
DC and DIB. It is called from the following two functions: 1) In CGDIView::OnDestroy() when the
application is about to exit. 2) In CGDIView::LoadBitamp() before new DDB is created.
That’s all the features included in sample 11.0\GDI. The application can load a DIB file from the disk
and display it.
319
Chapter 11. Sample: Simple Paint
it doesn’t make much difference where we put this variable. But if an application has more than one
document or view, we should consider this more carefully. For example, suppose we have two documents
opened at the same time, if both documents need to share a same feature (for example, the ratio change will
affect both documents), the variable needs to be put in the frame window class. If we don’t want to affect
the other document when changing the feature of one document (for example, the ratio change for one
document should not affect the ratio of another document), we should let each document have its own
variable.
In the sample application, we use an integer type variable m_nRatio to record the current ratio. This
variable is initialized to 1 in the constructor. A member function CGDIDoc::GetRatio() is added to let this
value be accessible from other classes. To let the user be able to change the ratio of the image, two buttons
are added to toolbar IDR_MAINFRAME. The IDs of the two buttons are ID_ZOOM_IN and ID_ZOOM_OUT, one of
them lets the user zoom in and the other let the user zoom out the image.
Both of the two commands have WM_COMMAND and UPDATE_COMMAND_UI message handlers. The message
handlers allow the user to change the current ratio, which are relatively easy to implement. Within the
function, we need to judge if the current value of ratio will reach the upper or lower limit, if not, we should
increment or decrement the value, and update the client window. For example, function
CGDIDoc::OnZoomIn() is implemented as follows:
void CGDIDoc::OnZoomIn()
{
if(m_nRatio < 16)
{
m_nRatio*=2;
UpdateAllViews(NULL);
GetCGDIView()->UpdateScrollSizes();
}
}
The lower limit of the ratio value is 1 and the upper limit is 16. Another concern is that we must also
change the scroll sizes of the client window whenever the ratio has changed (In sample 11.0\GDI, since the
image does not change after it is displayed in the client window, it is enough to just set the scroll sizes
according to the image size after the bitmap is loaded).
To let the scroll sizes be set dynamically, a new member function CGDIView::UpdateScrollSizes() is
added to the application. In this function, the current ratio value is retrieved and the scroll sizes are set to
the zoomed bitmap size. In the sample, this newly added function is also called in function
CGDIView::OnInitialUpdate() to set the scroll sizes according to the image size whenever a new bitmap
is loaded (The old implementation is elemenated).
Functions CGDIDoc::OnUpdateZoomIn(…) and CGDIDoc::OnUpdateZoomOut(…) are used to set the state
of zoom in and zoom out buttons. We should disable both commands when there is no bitmap loaded.
Besides this, upper and lower limits are also factors to judge if we should disable either zoom in or zoom
out command. For example, CGDIDoc::OnUpdateZoomIn(…) is implemented as follows:
Grid
Grid implementation is similar. Since grid has only two states (it is either on or off), a Boolean type
variable is enough for representing its current state. In the sample, a Boolean type variable m_bGridOn is
added to class CGDIDoc, which is initialized to FALSE in the constructor. Besides this, an associate function
CGDIDoc::GetGridOn() is added to allow its value be retrieved outside the document. Also, a new button
(whose command ID is ID_GRID) is added to tool bar IDR_MAINFRAME, whose message handlers are also
added through using Class Wizard. The value of m_bGridOn is toggled between TRUE and FALSE in
function CGDIDoc::OnGrid(). Within function CGDIDoc::OnUpdateGrid(…), the button’s state (checked or
unchecked) is set to represent the current state of grid:
320
Chapter 11. Sample: Simple Paint
We must modify function CGDIView::OnDraw(…) to implement grid. First we need to check the current
value of CGDIDoc::m_bGridOn. If it is TRUE, we should draw both the image and the grid; if it is FALSE,
we need to draw only the image.
We can draw various types of grids, for example, the simplest way to implement grid would be just
drawing parallel horizontal and vertical lines. However, there is a disadvantage of implementing grid with
solid lines. If the image happens to have the same color with grid lines, the grid will become unable to be
seen. An alternate solution is to draw grid lines using image’s complement colors, this can be easily
implemented by calling function CDC::SetROP2(…) and passing R2_NOT to its parameter before the grid is
drawn. However, this type of grid does not have a uniform color, this makes the image looks a little
awkward.
If we write program for Windows 95, the size of the bitmap for making pattern brush must be 8×8. In
the sample, this image is included in the application as a bitmap resource, whose ID is IDB_BITMAP_GRID.
The variable used for creating pattern brush is CGDIView::m_brGrid, and the pattern brush will be created
in the constructor of class CGDIView.
In function CGDIView::OnDraw(…), after drawing the bitmap, we must obtain the value of
CGDIDoc::m_bGridOn. If it is true, we will use the pattern brush to draw the grid. When using pattern brush,
we must pay special attention to its origin. By default, the brush’s origin will always be set to (0, 0). This
will not cause problem so long as the client window is not scrolled. However, if scrolled position (either
horizontal or vertical, but not both) happens not to be an even number, we need to adjust the origin of the
pattern brush to let the pattern be drawn started from 1 (horizontal or vertical coordinate). This is because
our pattern repeats every other pixel.
If the coordinates are (2, 2), If the coordinates are (3, 2),
the brush origin should be the brush origin should be
set to (0, 0) set to (1, 1)
Figure 11-2. How to choose the origin of pattern brush when drawing grid
321
Chapter 11. Sample: Simple Paint
Figure 11-2 demonstrates the two situations. In the left picture, the logical coordinates of the upper-
left pixel of the visible client window are (2, 2). If we draw the grid starting from the pixel located at the
logical coordinates (0, 0), the grid pixel at (2, 2) should be drawn using dark color (See Figure 11-1). If the
client window is further scrolled one pixel leftward (the right picture of Figure 11-2), the logical
coordinates of the upper-left pixel of the visible client window become (3, 2). In this situation, it should be
drawn using the light color. However, if we do not adjust the origin of the pattern brush, the system will
treat the upper-left visible pixel in the client window as the origin and draw it using the dark color.
To set pattern brush’s origins, we need to call the following two functions before selecting brush into
the DC:
BOOL CGDIObject::UnrealizeObject();
CPoint CDC::SetBrushOrg(int x, int y);
In the second function, x and y specify the new origin of the pattern brush.
In the sample, the brush origin is set according to the current scrolled positions. The following code
fragment shows how the origin is adjusted in function CGDIView::OnDraw(…):
……
m_brGrid.UnrealizeObject();
pDC->SetBrushOrg(pt.x%2 ? 1:0, pt.y%2 ? 1:0);
pBrOld=pDC->SelectObject(&m_brGrid);
……
Here m_brGrid is a CBrush type variable that is used to implement the pattern brush, pt is a POINT type
variable whose value is retrieved by calling function CScrollView::GetScrollPosition().
Drawing horizontal grid lines and vertical grid lines are implemented separately. We use two loops to
draw different types of lines. Within each loop, function CDC::PatBlt(…) is called to draw one grid line.
The following code fragment shows how the horizontal grid lines are drawn in the sample application:
……
for(i=0; i<bm.bmHeight; i++)
{
pDC->PatBlt(0, i*nRatio, size.cx, 1, PATCOPY);
}
……
The height of line is set to 1, so the actual result will be a pattern line.
322
Chapter 11. Sample: Simple Paint
The current
foreground and
background colors
are indicated here
Color bar
When the user is editing the image, both foreground and background color need to be set. The
foreground color will be used to draw line, curve, arc, or the border of rectangle, ellipse, polygon, etc. The
background color will be used to fill the interior of rectangle, ellipse and polygon. In the sample, the user
can left click on any color contained in the color bar to select a foreground color, and right click on any
color to select a background color.
We know that this feature is similar to that of standard graphic editor “Paint”. In “Paint” application,
color bar is docked to the top or bottom border of the mainframe window. There are two rows of colors that
can be selected for drawing. The user can use left and right mouse buttons to select foreground and
background colors, double click on any color to customize it.
We need to recollect some old knowledge from chapter 1 through chapter 4 in order to implement the
color bar.
323
Chapter 11. Sample: Simple Paint
case default function CBitmapButton::DrawItem(…) will be called). If we do not override this function, we
will not be notified when buttons need to be updated.
Since this sample is supposed to be used for palette device (Of course, it can be run on a non-palette
device), we will let each button display a color contained in a different entry of the logical palette. In order
to do this, we should let different button have a different index that represents a different entry of the
logical palette. For this purpose, a variable m_nPalIndex and two functions (GetPaletteIndex() and
SetPaletteIndex(…)) are added to class CColorButton. In function CColorButton::DrawItem(…), this
value is used as the index to the application’s logical palette for button drawing:
……
else
{
pDC=CDC::FromHandle(lpDrawItemStruct->hDC);
pPalOld=pDC->SelectPalette(pPal, FALSE);
pDC->RealizePalette();
pDC->FillSolidRect
(
&lpDrawItemStruct->rcItem,
PALETTEINDEX(m_nPalIndex)
);
pDC->SelectPalette(pPalOld, FALSE);
}
……
We use macro PALETTEINDEX to retrieve the actual color contained in the palette entry. As usual, before
doing any drawing, we have to select the logical palette into the DC and realize it.
Class CFBButton is similar. Two variables m_BgdIndex and m_FgdIndex are added to class CGDIDoc
representing the currently selected foreground and background colors. Their values can be retrieved and set
through calling functions CGDIDoc::GetBgdIndex(), CGDIDoc::GetFgdIndex(), CGDIDoc::
SetBgdIndex(…), CGDIDoc::SetFgdIndex(…). Two variables are declared in the document class instead of
color bar class because their values may need to be accessed from the view. Since the document is the
center of the application, we should put the variables in the document so that they can be easily accessed
from other classes.
Function CFBButton::Drawitem(…) implements drawing a rectangle filled with current background
color overlapped by another rectangle filled with current foreground color. Like class CColorButton, the
color is retrieved from the logical palette contained in the document. The following code fragment shows
how the background rectangle is drawn:
……
brush.CreateSolidBrush(PALETTEINDEX(nBgdIndex));
rect=lpDrawItemStruct->rcItem;
rect.InflateRect(-2, -2);
rect.left+=rect.Width()/4;
rect.top+=rect.Height()/4;
pBrOld=pDC->SelectObject(&brush);
pDC->Rectangle(rect);
pDC->DrawEdge(rect, EDGE_ETCHED, BF_RECT);
pDC->SelectObject(pBrOld);
……
Variable nBgdIndex is an index to the logical palette, the whole area that needs to be painted is
specified by lpDrawItemStruct->rcItem (lpDrawItemStruct is the pointer passed to function
DrawItem(…)). When drawing the rectangle, we see that a margin of 2 is left first (This is done through
calling function CRect:: InflateRect(…)), then the width and height of the rectangle are set to ¾ of their
original values. The foreground rectangle has the same dimension, but overlaps the background rectangle.
To add more fluff to the application, the border of both rectangles has a 3D effect, which is implemented by
calling function CDC::DrawEdge(…).
Color Bar
To implement color bar, a new class derived from CDialogBar is added to the application. This class is
named CColorBar. To let the buttons act as color selection controls, we need to implement subclass for all
the owner-draw buttons. In the sample, function CColorBar::InitButtons() is added to initialize the
indices of all the buttons and implement subclass. Also, function CDialogBar::Create(…) is overridden,
324
Chapter 11. Sample: Simple Paint
within which CColorBar::InitButtons() is called to change the default properties of the buttons. The
following is the implementation of function CColorBar::InitButtons():
BOOL CColorBar::InitButtons()
{
int i;
Please note that in the sample, the first color button’s ID is IDC_BUTTON_COLOR1, and the IDs of all the
color buttons are consecutive. This may simplify message mapping.
Color Selection
Another feature implemented in the sample application is that the user may set foreground and
background colors by left/right mouse clicking on a color selection control. Also, the color of the color
selection control may be customized by double clicking on the button. Since the messages related to mouse
events will not be routed to the child window of dialog box (We can treat dialog bar as a dialog box), they
are handled in base class CColorBar. In the sample, functions CColorBar::OnLButtonDown(…),
CColorBar::OnRButtonDown(…) and CColorBar::OnLButtonDblClk(…) are implemented to handle mouse
clicking messages. In the first two functions, first the foreground or background palette index contained in
the document is set to a new value according to which button is clicked, then the button being clicked is
updated. In the third function, a color dialog box is implemented, if the user selects a new color, we will
use it to fill the corresponding entry of the logical palette, then update all the color buttons.
325
Chapter 11. Sample: Simple Paint
ID_BUTTON_LINE are handled in the following two functions respectively (New drawing tools added in the
following sections will also be handled here):
With this implementation, at any time, only one tool can be selected. The currently selected tool is
indicated by variable CGDIDoc::m_nCurrentTool.
New Functions
The implementation of drawing is complex. We must handle different mouse events, do the
coordinates conversion, update the bitmap image according to mouse activity and current drawing tool, and
update the client window. It is important to break this whole procedure down into small modules, so the
entire drawing task can be implemented by calling just several module functions.
It is obvious that both dot drawing and line drawing should be implemented by handling three mouse
related messages: WM_LBUTTONDOWN, WM_RBUTTONDOWN and WM_MOUSEMOVE. There is one thing that must be
done before doing any dot or line drawing: converting the current mouse position from the coordinate
system of the client window to the coordinate system of the bitmap image (The current ratio and scrolled
position must also be taken into consideration). For dot drawing, we need a function that can draw a dot on
the bitmap using current foreground color. This function will also be called for line drawing because after
the left button is pressed and the mouse has not been moved, we need to draw a dot first. Also, we need a
function that can draw a straight line on the bitmap image using the current foreground color if the starting
and ending points are known.
There are some concerns with the line drawing. When the user clicks the left button, we need to draw a
dot at this position and set the beginning point of the line to it. As the user moves the mouse (with left
button held down), we should draw temporary lines until the left button is released. Before the button is
released, every time the mouse is moved, we need to erase the previous line and draw a new one. Although
this can be easily implemented by using XOR drawing mode, it is not the only solution. An alternate way is
to back up the current bitmap image before drawing any temporary line. If we want to erase the temporary
drawings, we can just restore the bitmap image backed up before.
In the sample application of this section, several new functions are added to implement dot and line
drawings. These functions are listed as follows:
The parameter of this function is the mouse cursor position that is measured in the coordinate system
of the client window. It will be normalized to the coordinate system of the bitmap image. If the current ratio
is greater than 1, the position will be divided by the current ratio. If any of the scroll bars is scrolled, the
scrolled position will also be deducted.
This function draws a dot on the bitmap with current foreground color, which is stored in the
document. The input parameter must be a normalized point.
This function draws a line on the bitmap from point ptStart to ptend using the current foreground
color, which is stored in the document. The input parameters must be normalized points.
void CGDIView::BackupCurrentBmp();
326
Chapter 11. Sample: Simple Paint
For the purpose of backing up the current bitmap, a new CBitmap type variable m_bmpBackup is
declared in class CGDIView. When function CGDIView::BackupCurrentBmp() is called, we create a new
bitmap and attaches it to m_bmpBackup then initialize the bitmap with the current bitmap image
(CGDIView:: m_bmpDraw).
void CGDIView::ResumeBackupBmp();
This function does the opposite of the previous function, it copies the bitmap stored in CGDIView::
m_bmpBackup to CGDIView::m_bmpDraw.
With the above new functions, we are able to implement dot and line drawing. To implement
interactive line drawing, another new variable m_ptMouseDown is declared in class CGDIView. This variable
is used to record the position of mouse cursor when its left button is being pressed down. As the mouse
moves or the left button is released, we can use it along with the new mouse position to draw a straight line.
The following is the implementation of WM_LBUTTONDOWN message handler:
if(m_bmpDraw.GetSafeHandle() != NULL)
{
pDoc=(CGDIDoc *)GetDocument();
nCurrentTool=pDoc->GetCurrentTool();
SetCapture();
m_ptMouseDown=NormalizePtPosition(point);
switch(nCurrentTool)
{
case TOOL_PEN:
{
DrawPoint(m_ptMouseDown);
pDoc->SetModifiedFlag(TRUE);
break;
}
case TOOL_LINE:
{
BackupCurrentBmp();
DrawPoint(m_ptMouseDown);
pDoc->SetModifiedFlag(TRUE);
break;
}
}
}
CScrollView::OnLButtonDown(nFlags, point);
}
After left button of the mouse is pressed down, we must set window capture in order to receive mouse
messages even when the cursor is not within the client window. The window capture is released when the
left button is released. If the current drawing object is dot, we need to call function CGDIView::
DrawPoint(…) to draw the dot at the current mouse position; if the current drawing object is line, we need
to first backup the current bitmap then draw a dot at the current mouse position.
For WM_MOUSEMOVE message, first we must check if the left button is being held down. If so, we can
further proceed to implement drawing. For dot drawing, we need to draw a new dot at the current mouse
position by calling function CGDIView::DrawPoint(…); for line drawing, we need to first erase the old
drawings by copying the backup bitmap to CGDIView::m_bmpDraw, then draw a new line:
……
case TOOL_PEN:
{
if(nFlags & MK_LBUTTON)
{
if(pt != m_ptMouseDown)
{
DrawPoint(point);
pDoc->SetModifiedFlag(TRUE);
}
}
break;
327
Chapter 11. Sample: Simple Paint
}
case TOOL_LINE:
{
if(nFlags & MK_LBUTTON)
{
ResumeBackupBmp();
DrawLine(m_ptMouseDown, pt);
}
break;
}
……
The implementation of WM_LBUTTONUP message handler is almost the same with that of WM_MOUSEMOVE
message handler: for dot drawing, a new dot is drawn at the current mouse position by calling function
CGDIView::DrawPoint(…). For line drawing, the backup bitmap is first resumed to CGDIView::m_bmpdraw.
Then a new line is drawn between points represented by CGDIView::m_ptMouseDown and current mouse
position.
Mouse Cursor
It is desirable to change the shape of mouse cursor when it is within the bitmap image. We can either
choose a standard mouse cursor or design our own cursor. A standard cursor can be loaded by calling
function CWinApp::LoadStandardCursor(…). There are many standard cursors that can be used in the
application, which include beam cursor (IDC_IBEAM), cross cursor (IDC_CROSS), etc. The mouse cursor can
be changed by handling WM_SETCURSOR message. In this message handler, we can call ::SetCursor(…) to
change the current cursor shape if we do not want the default arrow cursor.
We need another function to judge if the current mouse cursor is within the bitmap image contained in
the client window. In the sample, function CGDIView::MouseWithinBitmap() is added for this purpose. The
current image ratio and scrolled positions are all taken into consideration when doing the calculation. The
following is the implementation of this function:
BOOL CGDIView::MouseWithinBitmap()
{
CPoint point;
CPoint ptScroll;
CRect rect;
CRect rectBmp;
BITMAP bm;
int nRatio;
CGDIDoc *pDoc;
pDoc=(CGDIDoc *)GetDocument();
nRatio=pDoc->GetRatio();
ptScroll=GetScrollPosition();
::GetCursorPos(&point);
GetWindowRect(rect);
point.Offset(-rect.left, -rect.top);
point.Offset(ptScroll);
ASSERT(m_bmpDraw.GetSafeHandle());
m_bmpDraw.GetBitmap(&bm);
rectBmp=CRect(0, 0, bm.bmWidth*nRatio, bm.bmHeight*nRatio);
return rectBmp.PtInRect(point);
}
First we retrieve the current image ratio, horizontal and vertical scrolled positions of the client
window. Then function ::GetCursorPos(…) is called to obtain the current position of mouse cursor.
Because the returned value of this function (a POINT type value) is measured in the coordinate system of the
desktop window (whole screen), we need to convert it to the coordinate system of the client window before
judging if the cursor is within the bitmap image. Next, the image rectangle is stored in variable rectBmp,
and function CRect::PtInRect(…) is called to make the judgment.
This function is called in WM_SETCURSOR message handler. The following is the implementation of the
corresponding function:
328
Chapter 11. Sample: Simple Paint
{
if
(
MouseWithinBitmap() == TRUE &&
HTVSCROLL != nHitTest && HTHSCROLL != nHitTest
)
{
::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_CROSS));
return TRUE;
}
}
return CScrollView::OnSetCursor(pWnd, nHitTest, message);
}
If the cursor is within the bitmap image and is over neither the horizontal scroll bar nor the vertical
scroll bar, we set the cursor to IDC_CROSS (a standard cursor). Otherwise by calling the default
implementation of function OnSetCursor(…), the cursor will be set to the default arrow cursor.
11.4 Tracker
Sample 11.4\GDI is based on sample 11.3\GDI.
Tracker can be implemented to let the user select a rectangular area very easily, and is widely used in
applications supporting OLE to provide a graphical interface that lets the user interact with OLE client
items. When implementing a tracker, we can select different styles. This can let the tracker be displayed
with a variety of visual effects such as hatched borders, resize handles, etc.
Tracker can also be applied to any normal application. In a graphic editor, tracker can be used to select
a rectangular region, move and drop it anywhere within the image. It can also be used to indicate the
selected rectangular area when we implement cut, copy and paste commands (Figure 11-4).
Implementing Tracker
Tracker is supported by MFC class CRectTracker. To enable a rectangular tracker, we need to first use
this class to declare a variable, then set its style. When the window owns the tracker is being painted, we
need to call a member function of CRectTracker to draw the tracker.
We can set the tracker to different styles. The style of tracker is specified by variable CRectTracker::
m_nStyle. The following values are defined in class CRectTracker and can be used to specify the border
styles of a tracker: CRectTracker::solidLine, CRectTracker::dottedLine, CRectTracker::
hatchedBorder. The following values can also be assigned to CRectTracker::m_nStyle to specify how the
tracker can be resized: CRectTracker::resizeInside, CRectTracker::resizeOutside. Finally,
CRectTracker::hatchInside can be assigned to CRectTracker::m_nStyle to specify if the hatched border
should be drawn outside or inside the rectangle. All the above styles can be combined together using bit-
wise OR operation.
Tracker
Resize button
Resize button
329
Chapter 11. Sample: Simple Paint
The following is a list of values that can be returned from this function along their meanings:
Valure Meaning
CRectTracker::hitNothing The mouse cursor is not over the tracker.
CRectTracker::hitTopLeft The mouse cursor is over the top-left resize button.
CRectTracker::hitTopRight The mouse cursor is over the top-right resize button
CRectTracker::hitBottomRight The mouse cursor is over the bottom-right resize button
CRectTracker:hitBottomLeft The mouse cursor is over the bottom-left resize button
CRectTracker:hitTop The mouse cursor is over the resize button on the top border.
CRectTracker:hitRight The mouse cursor is over the resize button on the right border.
CRectTracker:hitBottom The mouse cursor is over the resize button on the bottom border.
CRectTracker:hitLeft The mouse cursor is over the resize button on the left border.
CRectTracker:hitMiddle The mouse cursor is within the tracker rectangle.
If mouse cursor hits any of the resize buttons, we can call CRectTracker::Track() to track the mouse
moving activities from now on until the left button is released. With this function, there is no need for us to
handle other two messages WM_MOUSEMOVE and WM_LBUTTONUP, because once it is called, the function will
not return until the left button is released. Of course, we can also write code to implement right button
tracking. When we call function CRectTrack::Track(), the tracker’s owner window should not set window
capture, otherwise the mouse message will not be routed to the tracker.
New Tool
In the new sample application, a new tool “Rectangular Selection” is implemented in tool bar
IDR_DRAWTOOLBAR (Figure 11-5). If it is selected as the current tool, the user can drag the mouse to create a
tracker over the image, resize or move it to change the selection. The cursor will be automatically changed
if the mouse cursor is within the tracker’s region.
“Rectangular
Selection” button on
the tool bar
330
Chapter 11. Sample: Simple Paint
First, a new command ID_BUTTON_RECSEL is added to IDR_DRAWTOOL tool bar. Each time a new tool
command is added to the tool bar, we must make sure that the IDs of all the commands contained in the
tool bar are consecutive. Otherwise the macros ON_COMMAND_RANGE and ON_UPDATE_COMMAND_UI_RANGE will
not work correctly. In the sample, two macros TOOL_HEAD_ID and TOOL_TAIL_ID are defined, and they
represent the first and last IDs of the commands contained in the drawing tool bar. We use the above two
macros to do the message mapping. By doing this, if we add a new tool next time, all we need to do is
redefining the macros.
In class CGDIView, a CRectTracker type variable m_trackerSel is declared to implement the tracker.
The tracker’s styles are initialized in the constructor as follows:
……
m_trackerSel.m_nStyle=
(
CRectTracker::dottedLine | CRectTracker::resizeOutside
);
……
The tracker’s border is formed by dotted line and the resize buttons are located outside the rectangle.
The tracker is drawn in function CGDIView::OnDraw(…) if the tracker rectangle is not empty:
……
CPoint ptScroll;
ptScroll=GetScrollPosition();
m_trackerSel.m_rect.OffsetRect(-ptScroll);
m_trackerSel.Draw(pDC);
m_trackerSel.m_rect.OffsetRect(ptScroll);
……
As we will see, the tracker’s size and position will be recorded in the zoomed bitmap image’s
coordinate system. This is for the convenience of coordinate conversion. Since the DC will draw the tracker
in client window’s coordinate system, we must add some offset to the tracker rectangle before it is drawn.
Also we need to resume rectangle’s original state after the drawing is completed.
Function CGDIView::OnSetCursor(…) is modified as follows so that the cursor will be automatically
changed if it is over the tracker region:
……
if(m_bmpDraw.GetSafeHandle() != NULL)
{
if
(
!(
m_trackerSel.m_rect.IsRectEmpty() != TRUE &&
m_trackerSel.SetCursor(this, nHitTest) == TRUE
)
)
{
if
(
MouseWithinBitmap() == TRUE &&
HTVSCROLL != nHitTest && HTHSCROLL != nHitTest
)
{
::SetCursor(AfxGetApp()->LoadStandardCursor(IDC_CROSS));
return TRUE;
}
}
else return TRUE;
……
Here we first check if the cursor can be set automatically. If CRectTracker::SetCursor(…) returns
TRUE, we can exit and return a TRUE value (If this function returns TRUE, it means currently the mouse
cursor is within the tracker region, and the cursor is customized by the tracker). If not, we check if the
mouse is over the bitmap image, if so, the cursor’s shape is set to IDC_CROSS and the function exits (We
need to return TRUE every time the cursor has been customized). If all these fail, we need to call function
CWnd::SetCursor(…) to set the cursor to the default shape.
331
Chapter 11. Sample: Simple Paint
……
case TOOL_RECTSEL:
{
int nHitTest;
nHitTest=m_trackerSel.HitTest(point);
if(nHitTest == CRectTracker::hitNothing)
{
m_trackerSel.m_rect.left=
m_trackerSel.m_rect.right=
point.x+GetScrollPosition().x;
m_trackerSel.m_rect.top=
m_trackerSel.m_rect.bottom=
point.y+GetScrollPosition().y;
}
……
We record the starting position in the upper-left point of the tracker rectangle. As the mouse moves,
the rectangle’s bottom-right point is updated with the current mouse position, and temporary rectangles are
drawn and erased before the tracker is finally fixed.
Temporary rectangles are drawn by calling function CDC::DrawFocusRect(…). Because this function
uses XOR drawing mode, it is easy to erase the previous rectangle by simply calling the function twice.
When the left button is released, we erase the previous temporary rectangle if necessary, update the
tracker rectangle, and call function CWnd::InValidate() to let the tracker be updated (along with the client
window).
Because we must keep track of mouse cursor position after its left button is pressed down, the window
capture must be set when a new tracker is being created. Since we share the code implemented for dot and
line drawing here, there is no need to add extra code to set window capture for the client window here.
……
else
{
::ReleaseCapture();
m_trackerSel.Track(this, point);
Invalidate();
}
……
Because the window capture is set when a new tracker is being created (also, when a dot or a line is
being drawn), we must first release the capture before trackering mouse movement (Otherwise the tracker
will not be able to receive messages related to mouse moving events). There is no need for us to handle
332
Chapter 11. Sample: Simple Paint
WM_MOUSEMOVE and WM_LBUTTONUP messages here because after function CRectTracker::Track() is called,
mouse moving events will all be routed to the tracker. After this function exits, variable
CRectTracker::m_rect will be automatically updated to represent the new size and position of the tracker.
So after calling this function, we can update the client window directly to redraw the tracker.
Normalizing Tracker
Since the tracker rectangle is recorded in the zoomed image’s coordinate system, we must first convert
it back to the original bitmap’s own coordinate system (the image with 1:1 ratio) in order to find out which
part of the image is being selected. In the sample, function CGDIView::NormalizeTrackerRect(…) is added
for this purpose. In this function, the current image ratio is retrieved from the document, and the four points
of the tracker rectangle is divided by this ratio. The tracker can be created in two different ways. For
example, the user may click and hold the left mouse button and drag it right-and-downward; also, the
mouse may be dragged up-and-leftward. For the first situation, a normal rectangle will be formed, in which
case member CRectTracker.m_rect.left is always less than member CRectTracker.m_rect.right, and
CRectTracker.m_rect.top is less than CRectTracker::m_rect.bottom. However, in the second situation,
CRectTracker.m_rect.left and CRectTracker.m_rect.top are all grater than their corresponding
variables. So before using variable CRectTracker::m_rect, we must normalize the rectangle.
We can call function CRect::NormalizeRect() to normalize a rectangle implemented by class CRect.
In function CGDIView::NormailizeTrackerRect(…), before the four points of the tracker rectangle are
divided by the ratio, this function is called to first normalize the rectangle.
333
Chapter 11. Sample: Simple Paint
The selection should be copied back within WM_LBUTTONDOWN message handler after function
CRectTracker::Track() is called. By using this function, the tracker rectangle can be automatically
updated when the mouse button is released. In the sample, functions CGDIView::ResumeBackupBmp() and
CGDIView::StretchCopySelection() are called to copy the selected image back to the original bitmap:
With the above implementations, we are able to select the image using “Rectangular Selection” tool,
then move or resize it.
11.6 Region
Before implementing new drawing tools, we need to introduce some new concepts. In this and
following sections, we will discuss region and path, both of which are GDI objects. The implementation of
simple “Paint” will be resumed in section 11.8.
Basics
Region is another very useful GDI object. We can use region to confine DC drawings within a
specified area no matter where the DC actually outputs. After a specified region is specified, all DC’s
outputs (dot and line drawing, brush fill, bitmap copy) will be confined within the region area. By using the
region, it is very easy for us to draw objects with irregular shapes.
A region can have any type of shapes. It can be rectangular, elliptical, polygonal or any irregular
closed shape. Moreover, a region can be created by combining two existing regions, the result can be the
union, the intersection or difference of the two regions. With these operations, we can create regions with a
wide variety of shapes.
334
Chapter 11. Sample: Simple Paint
Region Creation
In MFC, region is implemented by class CRgn. Like other GID objects, this class is derived from
CGDIObject, which means a valid region must be associated with a valid handle. Standard regions can be
created by calling one of the following functions:
As we can see, a region may have different shapes: rectangular, elliptical, polygonal. We can even
create a region that is composed of a series of polygons by calling function CRgn::
CreatePolyPolygonRgn(…). As we will see later, a region can also have an irregular shape.
Existing regions can be combined together to form a new region. The combining operation mode can
be logical AND, OR, XOR, the union or the difference of the two regions. The function that can be used to
combine two existing regions is:
Please note that CRgn type pointers passed to this function must point to region objects that have been
initialized by one of the functions mentioned above, or by other indirect region creating functions.
Parameter nCombineMode can be set to any of RGN_AND, RGN_COPY, RGN_DIFF, RGN_OR and RGN_XOR, which
specify how to combine the two regions.
Using Region
Like any other GDI object, before using the region, we must first select it into the DC. The difference
between using region and other GDI objects is that we need to call function CDC::SelectClipRgn(…) to
select the region instead of calling function CDC::SelectObject(…).
Function CDC::SelectClipRgn(…) has two versions:
For the second version of this function, parameter nMode specifies how to combine the new region with
the region being currently selected by the DC. Again, it can be set to any of the following flags: RGN_AND,
RGN_COPY, RGN_DIFF, RGN_OR and RGN_XOR.
After using the region, we must select it out of the DC before deleting it. To select a region out of the
DC, we can call function CDC::SelectClipRgn(…) and pass a NULL pointer to it. For example, the
following statement selects the region out of DC (pointed by pDC):
pDC->SelectClipRgn(NULL);
Sample
Sample 11.6\GDI is a standard SDI application generated by Application Wizard. In the sample, two
variables are declared in class CGDIView: CGDIView::m_rgnRect and CGDIView::m_rgnEllipse. They will
be used to create two regions, one is rectangular and one is elliptical. The two regions will be combined
together to create a new region that is the difference of the two. We will select this region into the client
335
Chapter 11. Sample: Simple Paint
window’s DC, and output text to the whole window. As we will see, only the area that is within the region
will have the text output.
The regions are created in the constructor of class CGDIView:
CGDIView::CGDIView()
{
m_rgnRect.CreateRectRgn(0, 0, 400, 400);
m_rgnEllipse.CreateEllipticRgn(50, 50, 350, 350);
m_rgnRect.CombineRgn(&m_rgnRect, &m_rgnEllipse, RGN_DIFF);
}
The final region will look like the shaded area shown in Figure 11-6.
In function CGDIView::OnDraw(…), string “Clip Region” is output repeatedly until all the client
window is covered by this text:
……
size=pDC->GetTextExtent(szText);
uTextAlign=pDC->SetTextAlign(TA_UPDATECP);
GetClientRect(rect);
pt=CPoint(0, 0);
for(j=0; j<rect.Height()/size.cy+1; j++)
{
pDC->MoveTo(0, pt.y);
for(i=0; i<rect.Width()/size.cx+1; i++)
{
pDC->TextOut(0, pt.y, szText);
}
pt.y+=size.cy;
}
pDC->SetTextAlign(uTextAlign);
pDC->SelectClipRgn(NULL);
}
336
Chapter 11. Sample: Simple Paint
11.7 Path
Basics
Path is another type of powerful GDI object that can be used together with device context. A path can
be seen as a closed figure that is formed by drawing trails. For example, Figure 11-8 shows a path that is
made up of alternative lines and curves.
A path can record almost all types of outputs to the device context. Like other GDI objects, it must be
first selected into a DC before being used. However, there is no class such as CPath that lets us declare a
path type variable. Therefore, we can not select a path into DC by calling function CDC::
SelectObject(…). To use path, we must call function CDC::BeginPath() to start path recording and call
CDC::EndPath() to end it.
Between the above two functions, we can call any of the drawing functions such as CDC::LineTo(…),
CDC::Rectangle(…), and CDC::TextOut(…). The trace of the output will be recorded in the path and can be
rendered later. When rendering the recorded path, we can either draw only the outline of the path using the
selected pen or fill the interior with the selected brush, or we can do both.
The following functions can be used to implement these path drawing:
BOOL CDC::StrokePath();
BOOL CDC::FillPath();
BOOL CDC::StrokeAndFillPath();
Function CDC::StrokePath() will render a specific path using the currently selected pen. This will
draw outline of the closed figure. Function CDC::FillPath() will close any open figures in the path and fill
its interior using the currently selected brush. After the interior is filled, the path will be discarded from the
337
Chapter 11. Sample: Simple Paint
device context. Function CDC::StrokeAndFillPath() implements both: it will stroke the outline of the path
and fill the interior.
Please note that the last function can not be replaced by calling the first two functions consecutively.
After function CDC::StrokePath() is called, the path will be discarded, so further calling
CDC::FillPath() will not have any effect.
Sample 11.7-1\GDI
Sample 11.7-1\GDI demonstrates path implementation. It is a standard SDI application generated by
Application Wizard. No new variable is declared. In function CGDIView::OnDraw(…), we begin path
recording and output four characters ‘P’, ‘a’, ‘t’, ‘h’ to the client window. Then we stroke the outlines of
the four characters and fill the path with a hatched brush.
In the sample, the font used to output the text is “Times New Roman”, and its height is 400. The brush
used to fill the interior of the path is a hatched brush whose pattern is cross hatch at 45 degrees. Between
function CDC::BeginPath() and CDC::EndPath(), there is only one statement that calls function
CDC::TextOut(…) to output the four characters. Please note that while path recording is undergoing, no
output will be generated to the target device. So this will not output anything to the client window. Finally
function CDC::StrokeAndFillPath() is called to stroke the text outline and fill the path’s interior using
hatched brush. The following is the implementation of function CGDIView::OnDraw(…):
CGDIDoc* pDoc=GetDocument();
ASSERT_VALID(pDoc);
memset(&lf, 0, sizeof(LOGFONT));
lf.lfHeight=400;
lstrcpy(lf.lfFaceName, "Times New Roman");
font.CreateFontIndirect(&lf);
pFtOld=pDC->SelectObject(&font);
pDC->BeginPath();
pDC->TextOut(0, 0, "Path");
pDC->EndPath();
pDC->StrokeAndFillPath();
pDC->SelectObject(pBrOld);
pDC->SelectObject(pFtOld);
}
338
Chapter 11. Sample: Simple Paint
Obtaining Path
A path can be retrieved by calling function CDC::GetPath(…). This function has three parameters, first
two of which are pointers that will be used to receive path data, and the final parameter specifies how many
points are included in the path:
A path is formed by a series of points and different type of curves. The points are stored in the buffers
pointed by lpPoints, and curve types are stored in the buffers pointed by lpTypes, which can be any of the
following: PT_MOVETO, PT_LINETO, PT_BEZIERTO or PT_CLOSEFIGURE.
To receive path information, we must first allocate enough buffers for storing point and type
information. Since the buffer size depends on the number of points included in the path, when calling
function CDC::GetPath(…), we can first pass NULL pointer to lpPoints and lpTypes parameters and 0 to
nCount. This will cause the function to return the number of points included in the path. After enough
buffers are allocated for storing both point and type information, we can call CDC::GetPath(…) again to get
the path.
Since path stores drawing trace in the form of vectors, we can change the shape of a path by moving
the control points without losing quality of the image. We can change the positions of the points using
certain algorithm. For example, if we multiply the vertical coordinate of all points with a constant factor,
the result will be an enlarged image scaled from the original path.
Sample 11.7-2\GDI
Sample 11.7-2\GDI demonstrates how to obtain and modify a path. It is based on sample 11.7-1\GDI.
In the sample, after text “Path” is output to the device context (which is recorded into path), function
CDC::GetPath(…) is called to retrieve the points and curve types into the allocated buffers. Then, we
change the y coordinates of all points by linearly moving them upward. The following code fragment of
function CGDIView::OnDraw(…) shows how the buffers are allocated and path is obtained:
……
nNumPts=pDC->GetPath(NULL, NULL, 0);
ASSERT(nNumPts != -1);
lpPoints=(LPPOINT)::GlobalAlloc(GPTR, nNumPts*sizeof(POINT));
ASSERT(lpPoints);
lpTypes=(LPBYTE)::GlobalAlloc(GPTR, nNumPts);
ASSERT(lpTypes);
pDC->GetPath(lpPoints, lpTypes, nNumPts);
……
The total number of points is stored in variable nNumPts. Because we need to use a POINT type array to
receive the points, the buffer size for storing points is calculated as follows:
339
Chapter 11. Sample: Simple Paint
Since a curve type uses only one byte, the buffer size for storing curve types is nNumPts.
The following portion of function CGDIView::OnDraw(…) shows how the points are moved:
……
size=pDC->GetTextExtent("Path");
for(i=0; i<nNumPts; i++)
{
pt=lpPoints[i];
pt.y-=pt.y*pt.x/size.cx;
lpPoints[i]=pt;
}
……
……
pDC->BeginPath();
for(i=0; i<nNumPts; i++)
{
switch(lpTypes[i])
{
case PT_MOVETO :
{
pDC->MoveTo(lpPoints[i].x, lpPoints[i].y);
break;
}
case PT_LINETO | PT_CLOSEFIGURE:
case PT_LINETO:
{
pDC->LineTo(lpPoints[i].x, lpPoints[i].y);
break;
}
case PT_BEZIERTO | PT_CLOSEFIGURE:
case PT_BEZIERTO:
{
pDC->PolyBezierTo(&lpPoints[i], 3);
i+=2;
break;
}
}
}
pDC->EndPath();
……
For a different type of curves, we call the corresponding CDC member function. Please note that since
Bezier curve uses three points, after drawing the Bezier curve, we need to advance the loop index by 2.
After this, function CDC::StrokeAndFillPath(…) is called to stroke the path’s outline and fill the
interior. Figure 11-10 shows the effect.
340
Chapter 11. Sample: Simple Paint
Implementation
The freeform selection tool has a lot in common with the rectangular selection tool: both need to
respond to mouse events for specifying a selected region; both need to implement a tracker to allow the
user to move and resize the selected image; both need to back up the selected portion of the bitmap. The
only difference is that for the freeform selection, the selected region may not be rectangular.
However, we can still make use of functions CGDIView::BackupSelection() and CGDIView::
StretchCopySelection(). Since we can store the irregular selection in a CRgn type variable, it doesn’t
matter if the backup area is rectangular or not. If we select the region into the DC before copying the
rectangular backup image, only the pixels within the region will be copied.
The region can be created from path. As the user presses the left button, if the current drawing tool is
freeform selection, we will start path recording. After the left button is released, the path recording will be
stopped and the irregular region will be created from the path. To allow the user to resize or move this
region, we must enable tracker and backup the selected area at this point.
Although the selection can be an irregular area, the tracker must be rectangular. We can set the tracker
rectangle to the bounding rectangle of the region, which can be obtained by calling function CRgn::
GetRgnBox(…). If the user changes the tracker, we must copy this region to a new position and resize it to
let the region fit within the rectangle which is indicated by the new tracker.
Scaling Region
There is a problem here: the recorded region was created when the user first made the selection. After
the tracker is moved or resized, we must offset and scale the original region to let it fit within the new
tracker rectangle before selecting it into the target DC. Please note that in order to confine DC drawings
within a region, we must use the target DC to select the region. Thus the problem is how to offset and scale
the region to let it fit within another rectangle without losing its original shape.
Unlike path, region is not recorded using vectors. Instead, it is made up of a number of rectangles. We
need to resize every rectangle in order to resize the whole region (Figure 11-11).
341
Chapter 11. Sample: Simple Paint
Just like we can retrieve path data by calling function CDC::GetPath(…), for region, we can also
retrieve its data by calling function CRgn::GetRegionData(…). This function has two parameters:
Here lpRgnData is a pointer that will be used to receive the region data, and nCount specifies the size
of buffers that are pointed by lpRgnData. Of course it is not possible to know the size of region data before
we know its detail. To find out the necessary buffer size, we can first pass NULL to lpRgnData parameter
and 0 to nCount parameter, which will cause the function to return the size needed for storing all the region
data. Using this size, we can allocate enough buffers and call the function again to actually retrieve the
region data.
Region data is stored in a RGNDATA type structure:
It contains two members, rdh is of RGNDATAHEADER type, which is the region data header. Member
Buffer is the first element of a char type array that contains a series of rectangles. The information of the
region is stored in its header structure:
Member dwSize specifies the size of this header, and the iType specifies the region type, whose value
must be RDH_RECTANGLES, which means that the region is made up of a number of rectangles. Member
nCount specifies the number of rectangles contained in this region, and rcBound specifies the bounding
rectangle. In order to scale the region, we need to implement a loop, and scale the corresponding rectangle
whose data is contained in the data buffer within each loop.
If we simply want to offset the region, there is a member function of CRgn that can be used to offset the
whole region just like we can offset a rectangle:
The region must be selected into a DC in order to call the above member functions.
342
Chapter 11. Sample: Simple Paint
New Tool
Sample 11.8\GDI is based on sample 11.6\GDI that implements freeform selection. In the sample, first
a new command is added to toolbar IDR_DRAWTOOLBAR, whose ID is ID_BUTTON_FREESEL (Figure 11-12).
Macro TOOL_HEAD_ID is redefined (it represents ID_BUTTON_FREESEL now) to allow this new tool be
automatically considered for message mapping. In order to use old message handlers, the ID of the
freeform selection tool is set to have the following relationship with other IDs:
ID_BUTTON_FREESEL = ID_BUTTON_RECTSEL+1
……
m_penDot.CreatePen(PS_DOT, 1, RGB(0, 0, 0));
……
We will use dotted line to draw the outline of irregular selection while the mouse is moving.
To record the selected region, a CRgn type variable is declared in class CGDIView. The following
portion shows how freeform selection is handled after WM_LBUTTONDOWN message is received:
……
m_ptPrevious=point;
if(m_rgnFreeSel.GetSafeHandle() != NULL)
{
m_rgnFreeSel.DeleteObject();
}
m_dcMem.MoveTo(m_ptMouseDown);
m_dcMem.BeginPath();
……
For WM_MOUSEMOVE message, we need to draw the freeform outline in the client window and do the
same thing to CGDIView::m_bmpDraw. Note since the current image ratio may not be 1:1, we must use
normalized points when recording path:
……
pPenOld=dc.SelectObject(&m_penDot);
dc.MoveTo(m_ptPrevious);
dc.LineTo(point);
m_ptPrevious=point;
dc.SelectObject(pPenOld);
m_dcMem.LineTo(pt);
343
Chapter 11. Sample: Simple Paint
……
For WM_LBUTTONUP message, we need to draw the last segment of outline, close the figure, and end the
path. Then we need to create the region from path, and set the dimension of the tracker to the dimension of
the bounding box of the selected region. Again we must consider ratio conversion here: since the path is
recorded with a ratio of 1:1, we must scale its bounding box to the current image ratio. In the sample,
function CGDIView::UnnormalizeTrackerRect(…) is added to scale a normalized rectangle to the current
image ratio. The rest thing needs to be done is the same with that of rectangular selection: we need to back
up the whole image as well as the selected area:
……
pPenOld=dc.SelectObject(&m_penDot);
dc.MoveTo(m_ptPrevious);
dc.LineTo(point);
dc.SelectObject(pPenOld);
m_dcMem.LineTo(pt);
m_dcMem.CloseFigure();
m_dcMem.EndPath();
m_rgnFreeSel.CreateFromPath(&m_dcMem);
m_rgnFreeSel.GetRgnBox(rect);
m_trackerSel.m_rect=UnnormalizeTrackerRect(rect);
BackupCurrentBmp();
BackupSelection();
Invalidate(FALSE);
……
……
::ReleaseCapture();
m_trackerSel.Track(this, point);
ResumeBackupBmp();
m_rgnFreeSel.GetRgnBox(rectOrgTracker);
rectCurTracker=NormalizeTrackerRect(m_trackerSel.m_rect);
nCount=m_rgnFreeSel.GetRegionData(NULL, 0);
lpRgnData=(LPRGNDATA)new BYTE[nCount];
m_rgnFreeSel.GetRegionData(lpRgnData, nCount);
lpRect=(LPRECT)lpRgnData->Buffer;
for(i=0; i<lpRgnData->rdh.nCount; i++)
{
rect=*(lpRect+i);
rect.OffsetRect(-rectOrgTracker.left, -rectOrgTracker.top);
rect.left*=rectCurTracker.Width();
rect.left/=rectOrgTracker.Width();
rect.right*=rectCurTracker.Width();
rect.right/=rectOrgTracker.Width();
rect.top*=rectCurTracker.Height();
rect.top/=rectOrgTracker.Height();
rect.bottom*=rectCurTracker.Height();
rect.bottom/=rectOrgTracker.Height();
rect.OffsetRect(rectCurTracker.left, rectCurTracker.top);
*(lpRect+i)=rect;
}
lpRgnData->rdh.rcBound=rectCurTracker;
rgn.CreateFromData(NULL, nCount, lpRgnData);
delete []lpRgnData;
m_dcMem.SelectClipRgn(&rgn);
StretchCopySelection();
m_dcMem.SelectClipRgn(NULL);
Invalidate();
……
With the above implementation, the application will support freeform selection.
344
Chapter 11. Sample: Simple Paint
……
dwDIBSize=
(
sizeof(BITMAPINFOHEADER)+
GetColorTableSize(bi.biBitCount)*sizeof(RGBQUAD)+
WIDTHBYTES(rect.Width()*bi.biBitCount)*rect.Height()
);
hDIB=::GlobalAlloc(GHND | GMEM_SHARE, dwDIBSize);
ASSERT(hDIB != NULL);
lpBi=(LPBITMAPINFO)::GlobalLock(hDIB);
ASSERT(lpBi != NULL);
lpBi->bmiHeader=bi;
lpBi->bmiHeader.biWidth=rect.Width();
lpBi->bmiHeader.biHeight=rect.Height();
lpBi->bmiHeader.biSizeImage=WIDTHBYTES
(
bi.biBitCount*rect.Width()
)*rect.Height();
……
The buffer size of new DIB data is calculated and stored in variable dwDIBSize. Here rect stores the
normalized dimension of the current tracker. The current image’s bitmap information header is stored in
variable bi. After copying it into the new buffers, we change members biWidth, biHeight and
biImageSize to new values.
345
Chapter 11. Sample: Simple Paint
Then, we need to copy the current color table to the newly allocated buffers. Although we could
retrieve the palette from the original DIB data, it may not be up-to-date because the user may change any
entry of the palette by double clicking on the color bar. In the sample, function
CPalette::GetPaletteEntries(…) is called to retrieve the current palette, and is used to create the new
image:
……
nPalSize=GetColorTableSize(lpBi->bmiHeader.biBitCount);
lpPalEntry=(LPPALETTEENTRY)new BYTE[nPalSize*sizeof(PALETTEENTRY)];
m_palDraw.GetPaletteEntries(0, nPalSize, lpPalEntry);
for(i=0; i<nPalSize; i++)
{
lpBi->bmiColors[i].rgbRed=lpPalEntry[i].peRed;
lpBi->bmiColors[i].rgbGreen=lpPalEntry[i].peGreen;
lpBi->bmiColors[i].rgbBlue=lpPalEntry[i].peBlue;
lpBi->bmiColors[i].rgbReserved=NULL;
}
delete []lpPalEntry;
……
Because there is no restriction on the dimension of the tracker, the user can actually select an area that
some portion of it is outside the current image. In order to copy only the valid image, we need to adjust the
rectangle before doing the copy. In the sample, only the intersection between the tracker and the image is
copied, and the rest part of the target image will be filled with the current background color. In function
CGDIDoc::CreateCopyCutDIB(…), the actual rectangle that will be used to obtain the image bit values from
the source bitmap is stored in variable rectSrc, and sizeTgtOffset is used to specify the position where
the image will be copied to the target image. This variable is necessary because the tracker’s upper-left
corner may resides outside the image. The whole image is copied using a loop. Within each loop, one raster
line is copied to the target bitmap. In the function, two pointers are used to implement this copy: before
copying the actual pixels, lpRowSrc is pointed to the source image buffers and lpRowTgt is pointed to the
target image buffers. Their addresses are calculated by adding the bitmap information header size and the
color table size to starting addresses of the DIB buffer. Since one raster line must use multiple of 4 bytes,
we need to use WIDTHBYTES macro to calculate the actual bytes that are used by one raster line. The
following shows how the selected rectangular area of the bitmap is copied to the target bitmap:
……
for(i=0; i<rectSrc.Height(); i++)
{
lpRowSrc=
(
(LPSTR)m_lpBits+
WIDTHBYTES
(
bi.biBitCount*bi.biWidth
)*(bi.biHeight-rectSrc.top-i-1)+
rectSrc.left
);
lpRowTgt=
(
(LPSTR)lpBi+
sizeof(BITMAPINFOHEADER)+
sizeof(RGBQUAD)*nPalSize+
WIDTHBYTES
(
lpBi->bmiHeader.biBitCount*lpBi->bmiHeader.biWidth
)*(lpBi->bmiHeader.biHeight-sizeTgtOffset.cy-i-1)
);
memcpy
(
lpRowTgt,
lpRowSrc,
WIDTHBYTES(lpBi->bmiHeader.biBitCount*rectSrc.Width())
);
}
……
346
Chapter 11. Sample: Simple Paint
void CGDIDoc::OnEditCut()
{
if(GetCGDIView()->OpenClipboard() == TRUE)
{
HGLOBAL hDIB;
::EmptyClipboard();
hDIB=CreateCopyCutDIB();
ASSERT(hDIB != NULL);
::SetClipboardData(CF_DIB, hDIB);
::CloseClipboard();
GetCGDIView()->FillSelectionWithBgdColor();
UpdateAllViews(NULL);
}
}
Paste
Paste command is the reverse of cut or copy command: we need to obtain DIB data from the clipboard
and copy it back to the bitmap image that is being edited. To let the user place pasted image everywhere,
we need to implement tracker again to select the pasted image. With this implementation, the user can
move or resize the image surrounded by the tracker just like using the rectangular selection tool.
So instead of copying DIB data from the clipboard directly to the bitmap being edited, we can first
create and copy it to the backup bitmap image (CGDIView::m_bmpSelBackup), then change the current
drawing tool to rectangular selection (if it is not). By doing this, everything will go on as if we just selected
a portion of image using the rectangular selection tool.
When the user executes Edit | Paste command, function CGDIDoc::OnEditPaste()will be called. The
DIB data in the clipboard will be replicated and passed to function CGDIView::PasteDIB(…). Within the
function, a new bitmap will be created using variable m_bmpSelBackup, and the DIB data contained in the
clipboard will be copied to it. Please note we must replicate the clipboard data instead of using it directly
because the data may be used by other applications later. The following is the implementation of function
CGDIDoc::OnEditPaste():
void CGDIDoc::OnEditPaste()
{
if(GetCGDIView()->OpenClipboard() == TRUE)
{
HGLOBAL hDIB;
LPBYTE lpByte;
HGLOBAL hDIBPaste;
LPBYTE lpBytePaste;
ASSERT(hDIB != NULL);
hDIB=::GetClipboardData(CF_DIB);
ASSERT(hDIB);
hDIBPaste=::GlobalAlloc(GHND, ::GlobalSize(hDIB));
ASSERT(hDIBPaste);
lpByte=(LPBYTE)::GlobalLock(hDIB);
ASSERT(lpByte);
lpBytePaste=(LPBYTE)::GlobalLock(hDIBPaste);
ASSERT(lpBytePaste);
memcpy(lpBytePaste, lpByte, ::GlobalSize(hDIB));
::GlobalUnlock(hDIB);
::GlobalUnlock(hDIBPaste);
::CloseClipboard();
GetCGDIView()->PasteDIB(hDIB);
::GlobalFree(hDIBPaste);
}
}
347
Chapter 11. Sample: Simple Paint
The following portion of function CGDIView::PasteDIB(…) shows how to copy the clipboard DIB data
to the backup DIB image by calling function ::SetDIBits(…) (the clipboard data is passed through
parameter hData):
pDoc=GetDocument();
ASSERT_VALID(pDoc);
AfxGetApp()->DoWaitCursor(TRUE);
lpBi=(LPBITMAPINFO)::GlobalLock(hData);
ASSERT(lpBi);
if(lpBi->bmiHeader.biBitCount != 8)
{
::GlobalUnlock(hData);
return;
}
nRatio=pDoc->GetRatio();
if(m_bmpSelBackup.GetSafeHandle() != NULL)m_bmpSelBackup.DeleteObject();
m_bmpSelBackup.CreateCompatibleBitmap
(
&dc,
lpBi->bmiHeader.biWidth,
lpBi->bmiHeader.biHeight
);
::SetDIBits
(
dc.GetSafeHdc(),
(HBITMAP)m_bmpSelBackup.GetSafeHandle(),
0,
lpBi->bmiHeader.biHeight,
(LPVOID)
(
(LPSTR)lpBi+
sizeof(BITMAPINFOHEADER)+
pDoc->GetColorTableSize
(
lpBi->bmiHeader.biBitCount
)*sizeof(RGBQUAD)
),
lpBi,
DIB_RGB_COLORS
);
……
When calling function ::SetDIBits(…), we need to provide the handle of client window (to its first
parameter). This is because the image will finally be put to the client window. Also we need to provide the
handle of target bitmap image (to the second parameter). The third and fourth parameters of this function
specify the first raster line and total number of raster lines in the target bitmap respectively. The fifth
parameter is a pointer to the source DIB bit values (stored as an array of bytes). The sixth parameter is a
pointer to bitmap header, and final parameter specifis how to use the palette. Since we want the DIB bits to
indicate the color table contained in the DIB data, we should choose DIB_RGB_COLORS flag.
After copying the image, we need to enable the tracker, backup the current bitmap, copy the new
bitmap to the area specified by tracker rectangle, and set the current drawing tool to rectangular selection.
Then, we need to update the client window. The following portion of function CGDIView::PasteDIB(…)
shows how these are implemented:
……
m_trackerSel.m_rect=CRect
(
0,
0,
nRatio*lpBi->bmiHeader.biWidth,
nRatio*lpBi->bmiHeader.biHeight
);
BackupCurrentBmp();
348
Chapter 11. Sample: Simple Paint
StretchCopySelection();
Invalidate();
::GlobalUnlock(hData);
((CMainFrame *)(AfxGetApp()->m_pMainWnd))->SendMessage
(
WM_COMMAND, ID_BUTTON_RECTSEL, 0
);
……
Because the rectangular selection button is not actually pressed by the user, we need to generate a
WM_COMMAND message in the program and send it to the mainframe window. When doing this, WPARAM
parameter of the message is set to the ID of rectangle selection command. This will have the same effect
with clicking the rectangular selection button using the mouse.
With the above implementation, we can execute cut, copy and paste commands now. Please note that
these commands work correctly only if the currently loaded image is 256-color format. For the other
formats, error message may be generated.
Problems
Now we have a very basic graphic editor. Before the editor becomes perfect, we still have a lot of
things to do: we need to add new tools for drawing curves, rectangles, ellipses, and so on; also we need to
support more bitmap formats, for example: 16-color DIB format, 24-bit DIB format. We can even add
image processing commands to adjust color balance, brightness and contrast to make it like a commercial
graphic editor.
Besides these, there are still two problems remained to be solved. One is that after loading an image
with this simple editor, if we switch to another graphic editor and load a colorful image then switch back,
the color of the image contained in our editor may change. Another problem is the unpleasant flickering
effect when we draw lines with the grid on.
Message WM_PALETTECHANGED
The first problem is caused by the change on the system palette. Because each application has its own
logical palette, and the system has only one physical palette, obviously the system palette can not be
occupied by only one application all the time. Since the operating system always tries to first satisfy the
needs of the application that has the current focus, if we switch to another graphic editor and leave our
editor working in the background, most entries of the system palette will be occupied by that application
and very few entries are left for our application. In this case, the system palette represents the logical
palette of another application rather than ours, so if we still keep the original logical-to-system palette
mapping, most colors will not be implemented correctly. This situation remains unchanged until we realize
the logical palette again, which will cause the logical palette to be mapped to the system palette.
Under Windows, there is a message associated with system palette changing. When the system
palette is mapped to certain logical palette and this causes its contents to change, a WM_PALETTECHANGED
message will be sent out. All the applications that implement logical palette should handle this message and
re-map the logical palette to the system palette whenever necessary to avoid color distortion.
In the sample, whenever we draw the image in CGDIView::OnDraw(…), function CDC::
RealizePalette() is always called to update the logical palette. So when receiving message
WM_PALETTECHANGED, we can just update the client window to cause the palette to be mapped again.
Usually message WM_PALETTECHANGED is handled in the mainframe window. This is because an
application may have several views attached to a single document. By handing this message in the
mainframe window, it is relatively easy to update all views. The following is the message handler
CMainFarme::OnPaletteChanged(…) that is implemented in the sample for handling the above message:
349
Chapter 11. Sample: Simple Paint
CFrameWnd::OnPaletteChanged(pFocusWnd);
GetActiveDocument()->UpdateAllViews(NULL);
}
This function is quite simple. With the above implementation, the first problem is solved.
Flickering
The second problem will be present only if the grid is on. This is because the grid is drawn directly to
the client window: whenever the image needs to be updated, we must first draw the bitmap, and this
operation will erase the grid. The user will see a quick flickering effect when the grid appears again. If we
keep on updating the client window, this flickering will become very frequent and the user will experience
very unpleasant effect.
We already know that one way to get rid of flickering is to prepare everything in the memory and
output the final result in one stroke. This is somehow similar to that of drawing bitmap image with
transparency (See Chapter 10). To solve the flickering problem, we can prepare a memory bitmap whose
size is the same with the zoomed source image, before updating the client window, we can output
everything (image + grid) to the memory bitmap, then copy the image from the memory bitmap to the
client window.
However, the problem is not that simple. Because the image size can be different from time to time,
also the image could be displayed in various zoomed ratio, it is difficult to decide the dimension of the
memory bitmap. To avoid creating and destroying memory bitmaps whenever the size (or ratio) of the
output image changes, we can create a bitmap with fixed size for painting the client window. If the actual
output needs a larger area, we can use a loop to update the whole client area bit by bit: within any loop we
can copy a portion of the source image to the memory bitmap, add the grid, then output it to the client
window. Because CDC::BitBlt(…) is a relatively fast function, and the bitmap drawing will be
implemented directly by the hardware, calling this function several times will not cause obvious delay.
In the sample application, some new variables are added for this purpose. We know that in order to
prepare a memory bitmap, we also need to prepare a memory DC that will be used to select the bitmap. In
class CGDIView, a CBitmap type variable m_bmpBKStore and a CDC type variable m_dcBKMem are declared.
Also, since the DC will select the bitmap and logical palette, two other pointers CGDIView::m_pBmpBKOld
and CGDIView::m_pPalBKOld are also declared. The two pointers are initialized to NULL in the constructor,
and the memory bitmap CGDIView::m_bmpBKStore is created in function
CGDIView::OnInitialUpdate()when an image is first loaded. The reverse procedure is done in function
CDC::OnDestroy(), where the memory bitmap and the palette are selected out of the memory DC.
In function CGDIView::OnDraw(…), the drawing procedure is modified. First the zoomed image is
divided horizontally and vertically into small portions, all of which can fit into the memory bitmap. Then
each portion of the image is copied to the memory bitmap, and the grid is added if necessary. Next the
image contained in the memory bitmap is copied to the client window at the corresponding position. Here
we need to calculate the origin and dimension of the output image within each loop. The following portion
of function CGDIView::OnDraw(…) shows how the zoomed source image is divided into several portions
and copied to the memory bitmap:
……
size.cx=BMP_BKSTORE_SIZE_X/nRatio;
size.cy=BMP_BKSTORE_SIZE_Y/nRatio;
nRepX=(bm.bmWidth-1)/size.cx+1;
nRepY=(bm.bmHeight-1)/size.cy+1;
for(j=0; j<nRepY; j++)
{
for(i=0; i<nRepX; i++)
{
m_dcBKMem.StretchBlt
(
0, 0,
BMP_BKSTORE_SIZE_X, BMP_BKSTORE_SIZE_Y,
&m_dcMem,
i*size.cx, j*size.cy,
size.cx, size.cy,
SRCCOPY
);
……
350
Chapter 11. Sample: Simple Paint
Origin Dimension
Source Bitmap Memory Bitmap Source Bitmap Memory Bitmap
(i*size.cx, j*size.cy) (0, 0) (size.cx, size.cy) (
BMP_BKSTORE_SIZE_X,
BMP_BKSTORE_SIZE_Y
)
The actual dimension of the source image that we can display in one loop is stored in variable size
(with 1:1 ratio). For the memory bitmap, for each loop the image is copied to its upper-left origin (0, 0); for
the source bitmap, the origin depends on the values of i and j.
When we copy the image from the memory bitmap to the client window, we must calculate if the
whole bitmap contains valid image. If not, we should draw only the valid part:
……
pDC->BitBlt
(
i*size.cx*nRatio,
j*size.cy*nRatio,
(
(i+1)*size.cx*nRatio > bm.bmWidth*nRatio ?
(bm.bmWidth*nRatio-i*size.cx*nRatio):size.cx*nRatio
),
(
(j+1)*size.cy*nRatio > bm.bmHeight*nRatio ?
(bm.bmHeight*nRatio-j*size.cy*nRatio):size.cy*nRatio
),
&m_dcBKMem,
0,
0,
SRCCOPY
);
……
The grid drawing becomes very easy now. The brush origin can always be set to (0, 0) because both
the horizontal and vertical dimensions of the memory bitmap are even. Before the image contained in the
memory bitmap is copied to the client window, the grid is added if the current ratio is greater than 2 and the
value of CGDIDoc::m_bGridOn is TRUE:
……
m_brGrid.UnrealizeObject();
pDC->SetBrushOrg(0, 0);
pBrOld=m_dcBKMem.SelectObject(&m_brGrid);
for(i=0; i<size.cx; i++)
{
m_dcBKMem.PatBlt(i*nRatio, 0, 1, nRatio*size.cy, PATCOPY);
}
for(i=0; i<size.cy; i++)
{
m_dcBKMem.PatBlt(0, i*nRatio, nRatio*size.cx, 1, PATCOPY);
}
m_dcBKMem.SelectObject(pBrOld);
……
With the above implementation, there will be no more flickering when we draw lines with grid on.
Summary
1) Tracker can be implemented by using class CRectTracker. To add tracker to any window, first we
need to use CRectTracker to declare a variable, then set its style. To display the tracker, we need to
override the function derived from CWnd::OnDraw(…)and call CRectTracker::Draw(…) within it.
351
Chapter 11. Sample: Simple Paint
2) The style of a tracker can be specified by enabling or disabling flags for member
CRectTracker::m_nStyle. The following flags are predefined values that can be used:
CRectTracker::solidLine, CRectTracker::dottedLine, CRectTracker::hatchedBorder,
CRectTracker::resizeInside, CRectTracker::resizeOutside, CRectTracker::hatchInside.
1) When the mouse left button is pressed, we can call function CRectTracker::HitTest(…) to check if
the mouse cursor is over the tracker object. If so, we can call function CRectTracker::Track(…) to
track the activities of the mouse. By doing this, there is no need to handle WM_MOUSEMOVE and
WM_LBUTTONUP messages.
1) Region can be used to confine the DC output within a specified area. The shape of a region can be
rectangular, elliptical, polygonal, or irregular. A new region can be created from existing regions by
combining them using logical AND, OR, and other operations.
1) A region must be selected into DC before being used. The function that can be used to select a region
is CDC::SelectClipRgn(…). To select the region out of the DC, we can simply call this function again
and pass NULL to its parameter.
1) Path can be used to record outputs to the device context. To start path recording, we need to call
function CDC::BeginPath(). To end path recording, we can call function CDC::EndPath(). All the
drawings between the two function calls will be recorded in the path. The output will not appear on the
DC when the recording is undergoing.
1) Function CDC::StrokePath() can be called to stroke the outline of a path. Function CDC::FillPath(…)
can be used to fill the interior of the path. Function CDC::StrokeAndFillPath() can be used to
implement both.
1) Region can be created from an existing path by calling function CRgn::CreateFromPath(…).
1) A region is made up of a series of rectangles. By resizing all the rectangles, we can resize the region.
1) A path is made up of a series of vectors. To resize a path, we can scale all the control points. We can
also change the position of some control points to generate special effects.
1) The standard DIB format that can be used in the clipboard is CF_DIB. To put DIB data to the clipboard,
we need to prepare DIB data with standard DIB format, open the clipboard, empty the clipboard, call
function ::SetClipboardData(…) and pass CF_DIB flag along with the data handle to it. After all these
operations, we must close the clipboard.
1) To obtain DIB data from the clipboard, we can use CF_DIB flag to call function
::GetClipboardData(…).
13) Message WM_PALETTECHANGED is used to notify the applications that the system palette has changed.
Applications that implement logical palettes should realize the logical palette again after receiving this
message to achieve least color distortion.
14) Outputting directly to window may cause flickering sometimes. This is because usually the old
drawings must be erased before the window is updated. To avoid flickering, we can prepare everything
in a memory bitmap, then output the patterns contained in the memory bitmap to the window in one
stroke.
352
Chapter 12. Screen Capturing & Printing
Chapter 12
B
y now we already have a lot of experience of using both DIB and DDB along with logical palette.
But it is still not enough. All the samples we created in the previous chapters start from DIB, so we
know the color table before the image is displayed in the window, and therefore can implement
logical palette and realize it before the pixels are actually drawn. By doing this, we can always get
the best color result.
However, sometimes we need to create image starting from the DDB. If we use DDB to record image
data, it would be very simple because we don’t have to worry about the actual data format and palette
realization. However, there are two problems: 1) If we want to save the image data to a file, we still need to
convert it to DIB format. 2) If the hardware is a palette device, the entries of the system palette may
actually change (e.g., when another application realizes its own logical palette). In this case, the colors
contained in our DDB may not be correctly mapped if we do not implement logical palette for it.
A very typical application of this kind is the screen capturing application. Screen capture can be
implemented by copying images between the desktop window DC and our own memory DC. We can call
function CDC::BitBlt(…) to copy the images. In this case, the memory DC must select a DDB rather than
DIB. If the system uses palette device, this may cause potential problem. Think about the following
situation: we use screen capturing application to make a snapshot of the desktop screen, and currently there
is a graphic editor opened with a colorful image being displayed. As long as the system palette remains
unchanged, the captured image can be displayed in its original colors. Suppose the graphic editor is closed
and this causes the system palette to change, the captured image will also change accordingly.
To prevent this from happening, when capturing the screen, we need to copy not only the image bits,
but also the colors contained in the current system palette. Then we can use them to create a logical palette,
and convert the DDB to DIB data. In the client window, we can display the image using DIB data instead
of captured DDB data. By doing this, when the system palette changes, we can re-match the logical palette
to achieve the best effect.
Samples in this chapter are specially designed to work on 256-color palette device. To customize them
for non-palette devices, we can just eleminate logical palette creation and realization procedure.
Capture
Making capture is very simple. We already have a lot of experience of creating bitmap in memory,
selecting it into a memory DC, and copying it to the client window. We can make a screen capture by
reversing the above procedure. The following lists the necessary steps for making a screen capture: 1)
Create a blank bitmap in the memory. 2) Create a memory DC that is compatible with the window DC. 3)
Select the memory bitmap into the memory DC. 4) Obtain a valid window DC. 5) Copy the image from the
window DC to the memory DC. Here the window DC can be either the desktop window DC or a client
window DC. In the former case, the whole screen will be captured. In the later case, only the client window
will be captured.
353
Chapter 12. Screen Capturing & Printing
Sample 12.1\GDI demonstrates how to make screen capture. It is a standard SDI application generated
by Application Wizard, whose view class is based on CScrollView. In the sample, function CGDIView::
Capture() is added to capture the whole desktop screen and store the image in a CBitmap type variable.
Under Windows, desktop window is the parent of the all windows in the system, and its pointer can
be obtained by calling function CWnd::GetDesktopWindw(). With this pointer, we can create a DC and use
it to draw anything on the desktop (Be careful with this feature, generally an application should not draw
outside its own window). Of course, we can also copy a bitmap to the desktop window.
The following is the implementation of function CGDIView::Capture() that shows how to copy the
whole screen and store the image in a CBitmap type variable:
void CGDIView::Capture()
{
CRect rect;
CBitmap bmpCap;
CBitmap *pBmpOld;
CWindowDC dc(CWnd::GetDesktopWindow());
CDC dcMem;
(CWnd::GetDesktopWindow())->GetClientRect(rect);
dcMem.CreateCompatibleDC(&dc);
bmpCap.CreateCompatibleBitmap(&dc, rect.Width(), rect.Height());
pBmpOld=dcMem.SelectObject(&bmpCap);
dcMem.BitBlt
(
0, 0,
rect.Width(), rect.Height(),
&dc,
rect.left, rect.top,
SRCCOPY
);
((CGDIDoc *)GetDocument())->GetCaptureBitmap(&bmpCap);
SetScrollSizes(MM_TEXT, CSize(rect.Width(), rect.Height()));
dcMem.SelectObject(pBmpOld);
}
In the above function, first desktop window DC is created from the pointer of desktop window (By
calling function CWnd::GetDesktopWindow()), then the dimension of the desktop window is retrieved and
stored in variable rect; next the memory DC and blank memory bitmap are created and the bitmap is
selected into the memory DC; finally function CDC::BitBlt(…) is called to capture the whole screen.
Of course we can use the bitmap (bmpCap) and memory DC (dcMem) created here to display the
captured image. However, because there is no logical palette associated with the bitmap, if the colors
contained in the system palette change, the captured image may not be displayed correctly. So before
displaying the image, we need to first convert it to DIB and implement logical palette.
354
Chapter 12. Screen Capturing & Printing
index to the system palette). The logical palette (More accurately, the entries with flag PC_EXPLICT) created
this way will not be mapped to the system palette by looking up the same (or nearest) colors or filling
empty entries. Instead, each entry will always be mapped to a fixed entry contained in the system palette. If
the colors in the system palette change, the corresponding colors in the logical palette will also change.
We need to create this kind of logical palette right after the image is captured (Before the system
palette changes) so that the colors contained in the captured image will be represented by the system
palette. Although we can use this palette to obtain the correct color table we will use, it can not be used for
later DIB displaying. The reason is that the colors contained in these entries may change constantly. We
need to create a logical palette using the color table obtained this way (Member peFlags of the palette
entries should be set to NULL).
After the logical palette with PC_EXPLICIT flag entries is created, we can select it into DC and realize
it. Then we can allocate enough buffers to store BITMAPINFOHEADER type object and color table. When
calling function ::GetDIBits(…), we can pass NULL to its lpvBits parameter, this will cause the bitmap
header and the correct color table to be filled into the buffers allocated before.
Now that we have the correct color table, we can use it to create a logical palette with member peFlags
set to NULL. In the sample, since an uninitialized logical palette is created at the beginning, we can just fill
the palette entries after each capturing.
The rest part of DDB-to-DIB conversion is the same with that of sample 10.4\GDI: we just need to
calculate the image size, reallocate the buffers, and call ::GetDIBits(…) again to receive actual bitmap bit
values.
The following is the implementation of function CGDIDoc::ConvertDDBtoDIB(…), the input parameter
is a CBitmap type pointer, and the returned value is the handle of global memory that contains DIB data:
pos=GetFirstViewPosition();
ptrView=(CGDIView *)GetNextView(pos);
ASSERT(ptrView->IsKindOf(RUNTIME_CLASS(CGDIView)));
CClientDC dc(ptrView);
lpLogPal=(LPLOGPALETTE)::GlobalAlloc
(
GMEM_FIXED,
sizeof(LOGPALETTE)+sizeof(PALETTEENTRY)*(PALETTE_SIZE-1)
);
lpLogPal->palVersion=PALVERSION;
lpLogPal->palNumEntries=PALETTE_SIZE;
VERIFY(palSys.CreatePalette(lpLogPal));
pPalOld=dc.SelectPalette(&palSys, FALSE);
dc.RealizePalette();
pBmp->GetBitmap(&bm);
bi.biSize=sizeof(BITMAPINFOHEADER);
bi.biWidth=bm.bmWidth;
bi.biHeight=bm.bmHeight;
bi.biPlanes=bm.bmPlanes;
bi.biBitCount=bm.bmPlanes*bm.bmBitsPixel;
bi.biCompression=BI_RGB;
355
Chapter 12. Screen Capturing & Printing
bi.biSizeImage=0;
bi.biXPelsPerMeter=0;
bi.biYPelsPerMeter=0;
bi.biClrUsed=0;
bi.biClrImportant=0;
dwSizeCT=GetColorTableSize(bi.biBitCount);
dwDibLen=bi.biSize+dwSizeCT*sizeof(RGBQUAD);
hDib=::GlobalAlloc(GHND, dwDibLen);
lpBi=(LPBITMAPINFO)::GlobalLock(hDib);
lpBi->bmiHeader=bi;
VERIFY
(
::GetDIBits
(
dc.GetSafeHdc(),
(HBITMAP)pBmp->GetSafeHandle(),
0,
(WORD)bi.biHeight,
NULL,
lpBi,
DIB_RGB_COLORS
)
);
bi=lpBi->bmiHeader;
::GlobalUnlock(hDib);
if(bi.biSizeImage == 0)
{
bi.biSizeImage=WIDTHBYTES(bi.biBitCount*bi.biWidth)*bi.biHeight;
}
dwDibLen+=bi.biSizeImage;
hDib=::GlobalReAlloc(hDib, dwDibLen, GHND);
ASSERT(hDib);
lpBi=(LPBITMAPINFO)::GlobalLock(hDib);
ASSERT(hDib);
VERIFY
(
::GetDIBits
(
dc.GetSafeHdc(),
(HBITMAP)pBmp->GetSafeHandle(),
0,
(WORD)bi.biHeight,
(LPSTR)lpBi+sizeof(BITMAPINFOHEADER)+dwSizeCT*sizeof(RGBQUAD),
lpBi,
DIB_RGB_COLORS
)
);
::GlobalUnlock(hDib);
dc.SelectPalette(pPalOld, FALSE);
return hDib;
}
New Command
In sample 12.1\GDI, a new command Capture | Go! is added to mainframe menu IDR_MAINFRM. This
command is handled by function CGDIDoc::OnCaptureGo(). Within the function, we first minimize the
application by calling function CMainFrame::ShowWindow(…) using SW_SHOWMINIMIZED flag. Then we call
function CGDIView::PrepareCapture() to set the timer (This function contains only one statement).
356
Chapter 12. Screen Capturing & Printing
Because we will minimize our application window, the capture should be delayed for a few seconds
after the user executes Capture | Go! command. Function CGDIView::PrepareCapture() does nothing but
setting up a timer with time out period set to 2 seconds. Function CGDIView::Capture() is called when the
timer times out. Within function CGDIView::Capture(), after the capture is made, function CGDIDoc::
GetCaptureBitmap(…) will be called to convert DDB to DIB and update the client window (Within this
function, CGDIDoc::ConvertDDBtoDIB(…) will be called).
In CGDIView::OnDraw(…), we use function ::SetDIBitsToDevice(…) to display DIB data. This is the
same with sample 10.4\GDI.
Figure 12-1 shows the time sequence of function calling.
Execute Capture | Go !
command
Time out
Call CDocument::UpdateAllViews(…)
to update the client window
Call CGDIView::Capture() to make
the capture and store the image as DDB
Call
CGDIDoc::GetCaptureBitmap(…) to
convert DDB to DIB
Function
CGDIDoc::GetCaptureBitmap(…)
Picking Up a Window
Often there are many windows contained in the desktop window. Each application may have one
mainframe window and several child windows. To let the user pick up a window with mouse clicking, we
must find a way to detect if there is a window under the current mouse cursor.
We can call function CWnd::WindowFromPoint(…) to retrieve the window under current mouse cursor.
If there is such a window, its handle will be returned by this function. Otherwise the function will return
NULL.
We also need to monitor mouse movement and respond to its activities when the user is selecting a
window. We all know that this can be implemented by handling mouse related messages: WM_LBUTTONDOWN,
WM_MOUSEMOVE, and WM_LBUTTONUP. Also we must be able to receive these messages even if the mouse
cursor is outside our application window. To implement this, we need to set window capture. By doing so,
the user can hold the left button and move it anywhere to pick up window. As long as the left button is held
357
Chapter 12. Screen Capturing & Printing
down, we can receive WM_MOUSEMOVE messages (The capture will be released by the system if the left button
is released).
Another issue is how to indicate the selected window. To make the application easier to use, we need
to put some indication on the window that is being selected. From the previous section we know that the
DC of the desktop window can be obtained and used to draw anything anywhere on the screen. With the
desktop window DC, we can reverse the selected window when the mouse cursor is over it, and resume it
as the mouse cursor leaves the window.
The drawing mode can be set by calling function CDC::SetROP2(…) using R2_NOT flag. After this if we
draw a rectangle (By calling function CDC::Rectangle(…)) using the window’s position and size, the whole
window will be reversed. Calling this function twice using the same parameter will resume the original
window.
One thing we must pay attention to is that the application itself also has a mainframe window, and
therefore will be selected for capturing if the mouse cursor is over it. This is not a desirable feature. To
solve this problem, after function CWnd::WindowFromPoint(…) is called, we must check if the window
returned by this function belongs to the application itself.
Because the windows can overlap one another, when the mouse cursor is over one window, we should
not reverse the overlapped part (Figure 11-2). To solve this problem, we need to use region. We can create
a region that contains only the non-overlapped part of a window, which can be selected by the desktop
window DC. By doing this, the overlapped portion of the window will not be reversed when the function
CDC::Rectangle(…) is called.
Window A
Window B
Figure 11-2. Reverse a window to indicate that it can be selected for capturing
After the user has selected a specific window and executed Capture | Go! command, we can find out
the size and position of this window, start timer, call function CDC::BitBlt(…) to make a snapshot of the
window and store the data to the memory bitmap.
358
Chapter 12. Screen Capturing & Printing
Messages WM_LBUTTONDOWN, WM_LBUTTONUP and WM_MOUSEMOVE are handled to let the user select a
window. When the left button is pressed down, we check if it hits the icon contained in the dialog box. If
so, we set window capture for the dialog box, change the mouse cursor, and change icon IDI_ICON_CURSOR
to IDI_ICON_BLANK:
void CSelDlg::OnLButtonDown(UINT nFlags, CPoint point)
{
CRect rect;
CDialog::OnLButtonDown(nFlags, point);
GetDlgItem(IDC_STATIC_CURSOR)->GetWindowRect(rect);
ScreenToClient(rect);
if(rect.PtInRect(point))
{
SetCapture();
((CStatic *)GetDlgItem(IDC_STATIC_CURSOR))->SetIcon(m_iconBlank);
GetDlgItem(IDC_STATIC_CURSOR)->Invalidate();
m_curSave=::SetCursor(m_curSelect);
m_bCaptureOn=TRUE;
m_rectSelect=CRect(0, 0, 0, 0);
m_hWnd=NULL;
}
}
Variable CSelDlg::m_hWnd is used to store the handle of the selected window. Also, variable
CSelDlg::m_rectSelect is used to indicate the rectangle of the previously selected window. If this
rectangle is empty, no window is currently being reversed.
As the mouse moves, we will keep on receiving WM_MOUSEMOVE messages. In the sample, function
CSelDlg::DrawSelection(…) is implemented to handle this message. Within this function, first we create a
region that contains all of the desktop window excluding the area occupied by the application window. We
select this region into the desktop window DC before reversing any window. By doing this, if the selected
window is overlapped by the application window, the reversing effect does not apply to the application
window. The following portion of function CSelDlg::DrawSelection(…) shows how the region is created:
359
Chapter 12. Screen Capturing & Printing
{
CWnd *pWnd;
CWindowDC dc(CWnd::GetDesktopWindow());
int nRop2Mode;
CRgn rgn;
CRgn rgnDesk;
CRect rect;
GetWindowRect(rect);
rgn.CreateRectRgnIndirect(rect);
AfxGetApp()->m_pMainWnd->GetWindowRect(rect);
rgnDesk.CreateRectRgnIndirect(rect);
rgn.CombineRgn(&rgnDesk, &rgn, RGN_OR);
rgnDesk.DeleteObject();
CWnd::GetDesktopWindow()->GetWindowRect(rect);
rgnDesk.CreateRectRgnIndirect(rect);
rgn.CombineRgn(&rgnDesk, &rgn, RGN_DIFF);
dc.SelectClipRgn(&rgn);
……
In order to resume the reversed window, the drawing mode should be set to R2_NOT, which will reverse
all the pixels contained in the rectangle when function CDC::Rectangle(…) is called. The drawing mode
can be set by calling function CDC::SetROP2(…):
……
nRop2Mode=dc.SetROP2(R2_NOT);
……
Then we call function CWnd::WindowFromPoint(…) to see if the current mouse cursor is over any
window. We use the returned pointer to retrieve the handle of that window, and compare it with the handles
of our application windows (both mainframe window and dialog box window). If there is a match, we
should not draw the rectangle because the cursor is over the application window (In this case, if there exists
a window that has been reversed, we should resume it). Otherwise, we further compare it with the handle
stored in CSelDlg::m_hWnd, if they are the same, we don’t do anything because the window under the
cursor has been reversed. If not, this means a new window is being selected and we should resume the old
reversed window (If there exists such a window) then reverse the newly selected one:
……
if
(
pWnd->GetSafeHwnd() != GetSafeHwnd() &&
pWnd->GetSafeHwnd() != AfxGetApp()->m_pMainWnd->GetSafeHwnd()
)
{
if(pWnd->GetSafeHwnd() != m_hWnd)
{
if(m_rectSelect.IsRectEmpty() != TRUE)
{
dc.Rectangle(m_rectSelect);
}
pWnd->GetWindowRect(m_rectSelect);
dc.Rectangle(m_rectSelect);
m_hWnd=pWnd->GetSafeHwnd();
}
}
else
{
if(m_rectSelect.IsRectEmpty() != TRUE)
{
dc.Rectangle(m_rectSelect);
m_rectSelect=CRect(0, 0, 0, 0);
}
m_hWnd=NULL;
}
if(bErase == TRUE && m_rectSelect.IsRectEmpty() != TRUE)
{
dc.Rectangle(m_rectSelect);
m_rectSelect=CRect(0, 0, 0, 0);
}
dc.SetROP2(nRop2Mode);
dc.SelectClipRgn(NULL);
}
}
360
Chapter 12. Screen Capturing & Printing
New Command
After the user picks up a window, the dialog box will be closed. So we need to store the handle of
selected window (Contained in CSelDlg::m_hWnd) so that it can be used when making snapshot. In the
sample, variable CGDIDoc::m_hWnd is declared for this purpose. Also, function CGDIDoc::
GetSelectedWnd() is implemented to allow this handle be accessed from the view. In the sample, a new
command Capture | Settings is added to menu IDR_MAINFRAME, it is handled in function CGDIDoc::
OnCaptureSetting(). Within the function, window selection dialog box is implemented to let the user
select a window, if the user clicks OK button, the selected window’s handle will be saved to variable
CGDIDoc::m_hWnd:
void CGDIDoc::OnCaptureSetting()
{
CSelDlg dlg;
if(dlg.DoModal() == IDOK)
{
m_hWnd=dlg.GetSelectedWnd();
}
}
Mapping Mode
However, there are some differences between printing devices and display devices. One main
difference is that two devices may have different capabilities. Because all displays have similar sizes and
resolutions, it is relatively convenient to measure everything on the screen using pixel. For example, it
doesn’t make much difference if we display a 256×256 bitmap on an 800×600 display or a 1024×768
display. Since every window is able to display an object that is larger than the dimension of its client area
(using scroll bars), it is relatively easy to base every drawing on the minimum possible unit pixel.
For printers, this is completely different. There are many types of printers in the world, whose
resolutions are remarkably different from one another. For example, there are line printers, one pixel on this
kind of printers may be 0.1mm×0.1mm; also, there are many types of laser printers, whose resolution can
be 600dpi, 800dpi or even denser. If we display a 256×256 image on the two types of printers, their sizes
will vary dramatically.
Anther difference between printing devices and display devices is that when doing the printing, it is
desirable to make sure that all the outputs fit within the device. For a window, since we can customize the
total scroll size, it doesn’t matter what the actual output dimension is (The scroll size can always be set to
the dimension of output drawings). For the printer, we need to either scale the output to let it fit within the
device or print the output on separate papers.
In order to handle this complicated situation, under Windows, OS and devices have some common
agreements. When we draw a dot, copy a bitmap to device, everything is actually based on logical units
(pixels). By default, the size of one logical unit is mapped to one minimum physical pixel on the device,
however, this can be changed. Actually class CDC has a function that let us customize it:
361
Chapter 12. Screen Capturing & Printing
Parameter nMapMode specify how to map one logical unit to physical units of the target device. By
default it is set to MM_TEXT, which maps one logical unit to one physical unit. It can also be set to one of the
following parameters:
Parameter Meaning
MM_HIENGLISH One logical unit is mapped to 0.001 inch in the target device
MM_HIMETRIC One logical unit is mapped to 0.01 millimeter of the target device
MM_LOENGLISH One logical unit is mapped to 0.01 inch of the target device
MM_LOMETRIC One logical unit is mapped to 0.1 millimeter of the target device
MM_TWIPS One logical unit is mapped to 1/1440 inch
Instead of mapping one logical unit to a fixed number of pixels, it is mapped to a fixed size on the
target device. It is the device driver’s task to figure out the actual number of pixels that should be used for
drawing one logical pixel. By doing this type of mappings, the output will have the same dimension no
matter what type of target device we use.
There is one difference between MM_TEXT mapping mode and other modes: for MM_TEXT mode, the
positive y axis points downward. For other mapping modes, the positive y axis points upward. So if we
decide to use one of the mapping mode listed above, and the origin of the bitmap is still the same, we need
to use negative values to reference a pixel’s vertical position (Figure 12-5).
y y
(0, n) (m, n) (0, -n) (m, -n)
Figure 12-5. The direction of y axis is different for different mapping modes
Implementing Print
Actually it is easy to implement printing for applications generated from the Application Wizard.
When the user executes File | Print or File | Print Preview command, a series of printing messages will be
sent to the application. Upon receiving these messages, the frame window finds out the current active view,
and call that view’s CView::OnPrint(…) function to output drawings to the target device.
By default, CView::OnPrint(…) does nothing but calling function CView::OnDraw(…), so everything
contained in the client window (view) will also be output to the printer. We can experiment this with the
sample already implemented. For example, after executing sample 12.2\GDI, if we make a snapshot and
execute File | Print command, the captured image will be sent to the printer. The actual size of the output
image depends on the type of printer because in CGDIView::OnDraw(…), we didn’t set the mapping mode so
the default mode MM_TEXT is used.
We must know the resolution of the target device so that we can either scale the output to let it fit into
the device or we can manage to print one image on separate papers. One way of obtaining the target
362
Chapter 12. Screen Capturing & Printing
device’s resolution is to call function CDC::GetDeviceCap(…) using HORZRES and VERTRES parameters. The
returned value of this function will be the horizontal or vertical resolution of the target device, measured in
its device unit. Besides this, we can also use LOGPIXELSX and LOGPIXELSY to retrieve the number of pixels
per logical inch in the target device for both horizontal and vertical directions. Since the width and height
of a minimum pixel in the target device may not be the same, the above two parameters may be different.
px
py
ry
rx
In Figure 12-6, x=4, y=2, px=12, py=9, rx : ry = 1:2. First we choose px as the actual width of the
output image. In this case, we need to map 4 logical units to 12 physical units in the horizontal direction. So
the horizontal mapping ratio is selected as 1:3. If we do the same thing in the vertical direction, we will
have a 12×6 image on the target device. However, because a basic pixel on the target device is not square,
the proportion of the output image will change if we do not take it (rx : ry ratio) into consideration. To
compensate for this, we need to map one logical unit to (px/x) ×(rx/ry) pixels in the vertical direction. In
this sample, we will have a 12×3 image on the target device.
If we have a very tall image (For example, in Figure 12-6, if x=2, y=9), such mapping may cause some
portion of the image unable to fit into the target device. So after calculating the mapping using the above
method, we need to check the vertical physical size and see if it is greater than py (if y×(px/x) ×(rx/ry) >
py). If so, we need to calculate the mapping again by first setting the vertical size to py and then calculating
the horizontal size using the same method.
Displaying or Printing?
In function CView::OnDraw(…), this mapping is unnecessary if the target device is a display rather than
a printer. To find out if this function is being called for printer, we can call function CDC::IsPrinting(). If
the returned value is FALSE, the function is called to output drawings to display. Otherwise it is called to
output drawings to printer.
363
Chapter 12. Screen Capturing & Printing
Function CGDIView::OnDraw(…)
The following portion of function CGDIView::OnDraw(…) shows how to scale the image dimension so
that it will fit within the target device before being output to the printer. Since the image must be scaled
before being printed, we call function ::StretchDIBits(…) to implement printing and still use
::SetDIBitsToDevice(…) for painting the client window:
……
if(pDC->IsPrinting())
{
CRect rcDest;
int cxPage=pDC->GetDeviceCaps(HORZRES);
int cyPage=pDC->GetDeviceCaps(VERTRES);
int cxInch=pDC->GetDeviceCaps(LOGPIXELSX);
int cyInch=pDC->GetDeviceCaps(LOGPIXELSY);
rcDest.top=rcDest.left=0;
rcDest.bottom=(int)
(
(
(double)lpBi->bmiHeader.biHeight*cxPage*cyInch
)/((double)lpBi->bmiHeader.biWidth*cxInch)
);
if(rcDest.bottom > cyPage)
{
rcDest.right=(int)
(
(
(double)lpBi->bmiHeader.biWidth*cyPage*cxInch
)/((double)lpBi->bmiHeader.biHeight*cyInch)
);
rcDest.bottom=cyPage;
}
else rcDest.right=cxPage;
::StretchDIBits
(
pDC->GetSafeHdc(),
rcDest.left,
rcDest.top,
rcDest.right,
rcDest.bottom,
0,
0,
lpBi->bmiHeader.biWidth,
lpBi->bmiHeader.biHeight,
(LPSTR)lpBi+dwBitOffset,
lpBi,
DIB_RGB_COLORS,
SRCCOPY
);
}
……
The physical resolution of the target device is retrieved and stored in variables cxPage and cyPage, and
the number of pixels per logical inch are retrieved and stored in variables cxInch and cyInch, which can be
used to calculate the aspect ratio of a basic pixel on the target device. The logical dimension of the image is
stored in members lpBi->bmiHeader.biWidth and lpBi->bmiHeader.biHeight. With the above
parameters, it is easy to figure out the actual physical size of the output image. The output dimension is
stored in variable rcDest, and function ::StretchDIBts(…) is called to output the captured image to
printer.
364
Chapter 12. Screen Capturing & Printing
Sample 12.4\GDI
Sample 12.4\GDI is based on sample 12.3\GDI. In this sample the printing is handled in function
CGDIView::OnPrinting(…), and CGDIView::OnDraw(…) is only responsible for painting the client window.
Since we can use the DIB stored in the document for printing, we do not need to do any preparation in
function CGDIView::OnBeginPring(…). Within function CGDIView::OnPrinting(…), we first need to obtain
the DIB and palette from the document, and set the mapping mode to MM_LOMETRIC, which will map one
logical unit to 0.1 mm. Then we need to select the palette into the target DC, call function
::StretchDIBits(…) to copy the image to target device. Please note that after the mapping mode is set to
MM_LOMETRIC, the direction of y axis is upward. When calling function ::StretchDIBits(…), we must set
the output vertical dimension on the target device to a negative value if the origin is still located at (0, 0):
……
::StretchDIBits
(
pDC->GetSafeHdc(),
0,
0,
lpBi->bmiHeader.biWidth,
-lpBi->bmiHeader.biHeight,
0,
0,
lpBi->bmiHeader.biWidth,
lpBi->bmiHeader.biHeight,
(LPSTR)lpBi+dwBitOffset,
lpBi,
DIB_RGB_COLORS,
SRCCOPY
);
……
365
Chapter 12. Screen Capturing & Printing
Now no matter what type of printer we use, the output dimension will be the same. The only difference
between the output from two different types of printers may be the image quality: for printers with high
DPIs, we will see a smooth image; for printers with low DPIs, we will see undesirable image.
Although the printing ratio is fixed for this sample, the user can still modify it through File | Print
Setup… command. In the Print Setup dialog box, the user can also select printer, paper size and the
printing ratio. The maximum printing ratio that can be set by the user is 400%. This may cause the output
image unable to fit within the target device. In this case, we need to print one image on separate pages.
By doing this, if the user executes printing command, in the popped up dialog box, the total number of
pages will be set to 2. The user can choose to print any of the pages or both of them. In function
CGDIView::OnPrint(…), we need to check which page is being printed and call function
::StretchDIBits(…) using the corresponding ratio:
……
nRatio=pInfo->m_nCurPage;
……
::StretchDIBits
(
pDC->GetSafeHdc(),
0,
0,
nRatio*lpBi->bmiHeader.biWidth,
-nRatio*lpBi->bmiHeader.biHeight,
0,
……
366
Chapter 12. Screen Capturing & Printing
page now. So the actual number of pages needed depends on the printing ratio, which can range from 25%
to 400%.
One solution to this problem is to calculate the actual number of pages just before the printing starts.
At this time, the print setting will not be changed any more, so we can calculate the number of required
pages and call function CPrintInfo::SetMaxPage(…) to set the page range.
This can be done in either function CView::BeginPrinting(…) or in function CView::
OnPrepareDC(…). The first function will be called just before the print job begins, and the second function
will be called before CView::BeginPrinting(…) is called when the printing DC needs to be prepared.
Please note that CView::OnPrepareDC(…) will also be called for preparing display DC, to distinguish
between the two situations, we can check if parameter pInfo is NULL, if not, it is called for the printing
job.
The number of required pages can be calculated by retrieving the device resolution (need to be
converted to logical unit) and comparing it with the image size. If the image size is greater than the device
resolution, we can print one portion at a time until the whole image is output to the target device.
Sample 12.5-2\GDI is based on sample 12.4\GDI, it demonstrates how to print the captured image
using this method. In the sample, first function CGDIView::OnPrepareDC(…) is overridden, within which the
number of required pages is calculated as follows:
……
nMapMode=pDC->SetMapMode(MM_LOMETRIC);
CGDIDoc* pDoc=GetDocument();
ASSERT_VALID(pDoc);
hDib=pDoc->GetHDib();
if(hDib != NULL)
{
lpBi=(LPBITMAPINFO)::GlobalLock(hDib);
ASSERT(lpBi);
size.cx=lpBi->bmiHeader.biWidth;
size.cy=lpBi->bmiHeader.biHeight;
rect.left=rect.top=0;
rect.right=pDC->GetDeviceCaps(HORZRES);
rect.bottom=pDC->GetDeviceCaps(VERTRES);
pDC->DPtoLP(rect);
nNumPages=((size.cx-1)/abs(rect.Width())+1)*((size.cy-1)/abs(rect.Height())+1);
pInfo->SetMaxPage(nNumPages);
::GlobalUnlock(hDib);
}
……
Here, the device resolution is retrieved by calling function CDC::GetDeviceCaps(…). Because the
device mapping mode is MM_LOMETRIC, which may cause the values returned by this function to be negative
for vertical dimensions, we need to use absolute value when doing the calculation.
Within function CGDIView::OnPrint(…), we print the corresponding portion of the image according to
the current page number. This procedure can be illustrated in Figure 12-7.
The shaded area represents the image. It is divided into horizontal and vertical cells, each cell has a
dimension that is the same with target device resolution. To draw the image, we need nine pages, each page
print one cell that is labeled (v, u). First we need to calculate the cell label from the page number:
Here the page number starts from 1. The next step is to calculate the position and the dimension of the
cell. Obviously, the origin of a cell rectangle can be calculated as follows:
367
Chapter 12. Screen Capturing & Printing
Image
(0, 0) (0, 1) (0, 2)
Figure 12-7. Print a portion of the image according to the current page number
If the cell is not the one located right-most or bottom-most, the horizontal and vertical sizes of the cell
can be determined from the resolution of target device. If it is located right-most (i.e., cells (0, 2), (1, 2) and
(2, 2) in Figure 12-7), the horizontal size can be calculated using the following formulae:
Similarly, if the cell is located bottom-most, the cell’s height can be calculated using the following
formulae:
The following shows the procedure of calculating the dimension of a cell in function
CGDIView::OnPrint(…) (Sample 12.5-2\GDI):
……
nRepX=(lpBi->bmiHeader.biWidth-1)/abs(rectDC.Width())+1;
nRepY=(lpBi->bmiHeader.biHeight-1)/abs(rectDC.Height())+1;
u=(pInfo->m_nCurPage-1)%nRepX;
v=(pInfo->m_nCurPage-1)/nRepX;
rect.left=u*abs(rectDC.Width());
rect.top=v*abs(rectDC.Height());
if(u == nRepX-1)
{
rect.right=
(
rect.left+
abs(rectDC.Width())-
(nRepX*abs(rectDC.Width())-lpBi->bmiHeader.biWidth)
);
}
else rect.right=rect.left+abs(rectDC.Width());
if(v == nRepY-1)
{
rect.bottom=
(
rect.top+
abs(rectDC.Height())-
(nRepY*abs(rectDC.Height())-lpBi->bmiHeader.biHeight)
);
}
else rect.bottom=rect.top+abs(rectDC.Height());
……
Variables nRepX and nRepY are used to store the number of cells in horizontal and vertical directions,
and variable rectDC stores the device resolution. When a cell is copied, its dimension is calculated and
stored to variable rect. The following portion of function CGDIView::OnPrint(…) shows how a cell is
output to the device:
368
Chapter 12. Screen Capturing & Printing
……
::StretchDIBits
(
pDC->GetSafeHdc(),
0,
-rect.Height()+1,
rect.Width(),
rect.Height(),
rect.left,
lpBi->bmiHeader.biHeight-rect.top-1,
rect.Width(),
-rect.Height(),
(LPSTR)lpBi+dwBitOffset,
lpBi,
DIB_RGB_COLORS,
SRCCOPY
);
……
Please note that we must use 0xFFFFFFFE instead of 0xFFFFFFFF to set the maximum page range,
the latter will result in printing only the first page.
If we do not add further control, the printing will not stop until 0xFFFFFFFE pages have been printed.
To stop printing after all the image has been printed out, we need to calculate the total required number of
pages and check if the page currently being printed is the last page in function CGDIView::OnPrint(…). If
so, we set the page range again to stop printing:
……
uNumPages=
(
((lpBi->bmiHeader.biWidth-1)/abs(rectDC.Width())+1)*
((lpBi->bmiHeader.biHeight-1)/abs(rectDC.Height())+1)
);
if(uNumPages == pInfo->m_nCurPage)
{
pInfo->SetMaxPage(uNumPages);
}
……
In our case the number of pages can actually be decided before the printing begins, so it seems not
necessary to stop printing this way. However, for applications that the number of pages cannot be decided
beforehand, this is the only method to implement multiple-page printing.
369
Chapter 12. Screen Capturing & Printing
Disable these
controls
In standard SDI and MDI applications, we don’t need to add anything in order to include the print
dialog box. The print dialog box can be used to do either print setup or printer setup. The constructor of
CPrintDialog must have at least one input parameter, which indicates if the dialog box should be
implemented for print setup or printer setup:
CPrintDialog::CPrintDialog
(
BOOL bPrintSetupOnly,
DWORD dwFlags=PD_ALLPAGES | PD_USEDEVMODECOPIES | PD_NOPAGENUMS |
PD_HIDEPRINTTOFILE | PD_NOSELECTION,
CWnd* pParentWnd=NULL
);
Since the initialization procedure of the print dialog box is implemented within other MFC member
functions, as a programmer, we can only modify the dialog box after it is created. Remember in function
CGDIView::OnPreparePrinting(…), one of the input parameters is a CPrintInfo type pointer. The print
dialog box is embedded in this class. The member used to store the dialog box is CPrintDialog type
pointer CPrintInfo::m_pPD. We can modify any of its member to change the dialog box style before
function CView::DoPreparePrinting(…) is called.
Class CPrintDialog contains a PRINTDLG type object m_pd that allows us to customize the style of the
dialog box. Here structure PRINTDLG is similar to OPENFILENAME structure of class CFileDialog. It also has
a member Flags that allows us to specify the styles of the print dialog box. Two flags we will use in the
sample are listed in the following table:
Flag Meaning
PD_NOPAGENUMS Disables radio button labeled “Pages” and the edit boxes labeled “from:” and “to:”
PD_NOSELECTION Disables the radio button labeled “Selection”
Sample 12.6-1\GDI is based on sample 12.5-3\GDI, it demonstrates how to disable these controls. The
following is a portion of function CGDIView::OnPreparePrinting(…) of sample 12.6-1\GDI showing how
the styles of the print dialog box are customized:
370
Chapter 12. Screen Capturing & Printing
……
}
With the above implementation, the print dialog box will use the custom dialog template PRINTDLG.
Summary
1) To capture the screen, we need to obtain a DC of the desktop window, prepare blank memory bitmap,
and call function CDC::BitBlt(…) to make the copy.
1) For palette devices, we must obtain the system palette after a snapshot is taken. This can be
implemented by creating logical palette using PC_EXPLICIT flags.
1) One logical unit can be mapped to different size on the target device. If we use MM_TEXT mode, one
logical unit will be mapped to one physical unit. We can also use other mapping modes to map one
logical unit to an absolute length.
371
Chapter 12. Screen Capturing & Printing
1) To fit the output within the target device, we need to know the resolution of the device, along with the
number of pixels contained in one inch for both horizontal and vertical directions. These parameters
can be obtained by calling function CDC::GetDeviceCaps(…) and using flags HORZRES, VERTRES,
LOGPIXELSX and LOGPIXELSY.
1) If we want the output image to have the same size on any type of target devices, we need to use a
mapping mode other than MM_TEXT.
1) If the number of pages is fixed for printing output, we can call CPrintInfo::SetMaxPage(…) in
function CView::OnPreparePrinting(…) to set the page range. The page number will be passed to
function CView::OnPrint(…) to let us customize the print output.
1) If the number of pages can only be decided just before the printing starts, we can call
CPrintInfo::SetMaxPage(…) in function CView::BeginPrinting(…) or CView::OnPrepareDC(…).
1) If the number of pages must be decided when the printing is undergoing, we can set the page range to
its maximum value before the printing starts, and call CPrintInfo::SetMaxPage(…) to stop printing
dynamically in function CView::OnPrint(…).
372
Chapter 13. Adding Special Features to Application
Chapter 13
N
ormal applications created by Application Wizard cannot satisfy us all the time. For certain types
of applications, we need to add special features to our programs. Since Application Wizard or
Class Wizard does not directly support these features, we need to have in-depth knowledge on
Windows programming in order to customize standard applications. In this chapter, we will
discuss how to create applications with special features such as multiple documents, multiple views,
irregular-shaped window, customized non-client area. Also, we will discuss how to implement hook in the
applications.
Window Creation
To implement one instance application, we must understand how the applications are created under
Windows. This can be easily understood if we have the experience of writing Win32 Windows
applications. However, if we started everything from MFC, it is not very obvious how a window is created.
This is because MFC hides everything from the programmer. Although it is relatively easy to create an
application by deriving classes from MFC without thinking about the actual procedure of creating
windows, if we rely too much on MFC, we will lose the power of customizing it.
Every visual object that is created under Windows is a window. This includes the frame window,
tool bar, menu, view, button and other controls. Actually, MFC is not the only tool that can be used to
create windows. A window can be created by using any computer language such as C, Basic, Pascal so long
as it abides by the rules of creating windows.
Under Windows, a window can be described by structure WNDCLASS:
373
Chapter 13. Adding Special Features to Application
Member style specifies window styles, by setting different bits of this member we can create different
types of windows. There are many styles that can be combined together, two most often used styles are
CS_HREDRAW and CS_VREDRAW, which will cause the client area to be updated if user resizes the window in
either horizontal or vertical direction. Member lpfnWndProc points to a callback function that will be used
to process incoming messages. When a window is created, it should contain several default objects: 1) icon,
which will be used to draw the application when it is minimized; 2) cursor, which will be used to customize
the mouse cursor when it is located within the client window of the application; 3) default mainframe
menu; 4) brush, which will be used to erase the client area (This brush specifies the background pattern of
the window). The above objects are described by following members of structure WNDCLASS respectively:
hIcon, hCursor, hbrBackground and lpszMenuName.
Another very important member is lpszClassName, which describes the type of the window we will
create. Every window under Windows has a class name. Before creating a new type of window, we must
register its class name to the system. After that we can use this class name to implement an instance of
window. A class name is simply a string, which can be specified by the programmer.
If we write windows program in C, we must go through the following procedure in order to create a
new window: register the window class name, implement a message handling routine, use the registered
window class name to implement a new window instance. In MFC, this procedure is hidden behind the
classes, when we use a class derived from CWnd to declare a variable, the class registration is completed
sometime before the window is created. Also, we do not need to provide message handling routines
because there exist default message handlers in MFC. If we want to trap certain messages, we can add
member functions and use message mapping macros to associate the messages with functions.
It is relatively easy to implement one-instance application by programming in C: before registering a
window class, we can first find out if there exists any instance implemented by the same type of window
class (This class has nothing to do with class in C++) in the system. If so, we simply exit and do not go on
to create a new window. If not, we will implement the new window.
However, in MFC, we do not see how window class is registered, so it is difficult to manipulate it.
Also, in MFC, all the window class names are predefined, so we actually can not modify them. In order to
create one-instance application, we need to discard the default registered window class name, and use our
own class name to create new instance. By doing so, we are able to check if there already exists an instance
of this window type before creating a new one.
Function CWnd::PreCreateWindow(…)
The styles of a window (including the class name) can be modified just before it is created. In MFC,
function CWnd::PreCreateWindow(…) can be overridden for this purpose.
The input parameter of CWnd::PreCreateWindow(…) is a CREATESTRUCT type variable, which is passed
to the function by reference:
We can specify a new menu and use it as the mainframe menu. We can set the initial size and position
of the window. We can also specify window name, and customize many other styles. Within the structure,
the window class name is specified by member lpszClass. By default, this structure is stuffed with
374
Chapter 13. Adding Special Features to Application
standard values, however, we can change any of them to let the application have new styles. For example,
if we want to use "My class" as the class name of our application rather than using the default one, we
need to implement the overridden function as follows:
Sample 13.1\Once
Sample 13.1\Once is a standard MDI application generated by Application Wizard, it demonstrates
how to implement one-instance MDI application. The application has no functionality except that if we try
to activate more than one copy of this application, instead of creating a new instance, the existing one will
always be brought up and become active.
Instead of using default class name, we need to register a custom class name to the system. The first
thing we need to do before frame window, document and view are implemented is to look up if there exists
an application with the same class name in the system:
BOOL COnceApp::InitInstance()
{
CWnd *pWndFrame;
CWnd *pWndChild;
WNDCLASS wndcls;
if(pWndFrame=CWnd::FindWindow(ONCE_CLASSNAME, NULL))
{
pWndChild=pWndFrame->GetLastActivePopup();
pWndFrame->BringWindowToTop();
if(pWndFrame->IsIconic())pWndFrame->ShowWindow(SW_RESTORE);
if(pWndFrame != pWndChild)pWndChild->SetForegroundWindow();
return FALSE;
}
……
Function CWnd::FindWindow(…) is called to find the application with the same class name in the
system. This function allows us to search windows with specific class name and/or window name. It has the
following format:
We can pass NULL to window name parameter (lpszWindowName) to match only the class name. If the
pointer returned by this function is not NULL, we can activate that window, bring it to top, and activate all
its child windows. This procedure is implemented by calling the following functions: 1) CWnd::
GetLastActivePopup(), which will find out the most recently activated pop-up window. 2)
::ShowWindow(…), which will restore the original state of the window being minimized (parameter
SW_RESTORE can be used for this purpose). 3) CWnd::SetForegroundWindw(), which will bring the child
375
Chapter 13. Adding Special Features to Application
window to foreground if there is a such kind of window. Steps 1) and 3) are necessary here because a
mainframe window may own some pop up windows (For example, a dialog box implemented by an SDI or
MDI application). Once the mainframe window is brought to the top, we also need to bring its child pop up
window to foreground.
After this is done, we need to return a FALSE value, which indicates that the procedure of creating
mainframe window, document and view is not successful. This will cause the application to exit.
If no window with the same class name is found, we can proceed to register our own window class.
This can be done by stuffing a WNDCLASS type object and passing the object address to function
AfxRegisterClass(…), whose only parameter is a WNDCLASS type pointer:
In order to make sure that our application is the same with those implemented by default MFC window
classes, we must stuff the class with appropriate values. Here is how this is done in the sample:
……
memset(&wndcls, 0, sizeof(WNDCLASS));
wndcls.style=CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW;
wndcls.lpfnWndProc=::DefWindowProc;
wndcls.hInstance=AfxGetInstanceHandle();
wndcls.hIcon=LoadIcon(IDR_MAINFRAME);
wndcls.hCursor=LoadCursor(IDC_ARROW);
wndcls.hbrBackground=(HBRUSH)(COLOR_WINDOW+1);
wndcls.lpszMenuName=NULL;
wndcls.lpszClassName=ONCE_CLASSNAME;
if(!AfxRegisterClass(&wndcls))
{
TRACE("Class Registration Failed\n");
return FALSE;
}
……
The class name string is defined using ONCE_CLASSNAME macro. Of course, when we override function
CMainFrame::OnPreCreateWindow(…), we need to replace the default class name with it. The following
code fragment shows how this function is overridden in the sample:
Before the application exits, we must unregister the window class if it has been registered successfully.
For this purpose, a Boolean type variable m_bRegistered is declared in class COnceApp, which will be set to
TRUE after the class is registered successfully. When overriding function CWinApp::ExitInstance()
(which should be overridden if we want to do some cleanup job before the application exits), we need to
unregister the window class name if the value of m_bRegistered is TRUE. The following is the overridden
function implemented in the sample:
int COnceApp::ExitInstance()
{
if(m_bRegistered == TRUE)
{
::UnregisterClass(ONCE_CLASSNAME, AfxGetInstanceHandle());
}
return CWinApp::ExitInstance();
}
376
Chapter 13. Adding Special Features to Application
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CGDIDoc),
RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CGDIView));
AddDocTemplate(pDocTemplate);
Class CSingleDocTemplate binds together the mainframe window, view and document. It creates view
and makes it client of the frame window. Obviously, by eliminating these statements, we are able to create
our own client window without bothering to use document and view.
Creating Window
However, if we do not let the framework to create the mainframe window and a client window for us,
we have to do it by ourselves. Fortunately creating a window is not so difficult, we can call function
CFrameWnd::Create(…) at any time to dynamically create a window with both title bar and client window:
Here, we are asked to provide some information of the window that is about to be created. This
includes class name, window name, window styles, window position and size, etc. The class name must be
a registered one. Although we can register our own window class name as we did in the previous section,
we can also pass NULL to parameter lpszClassName to let the default registered class name be used. Also,
we can pass NULL to parameter dwStyle to use the default window style, and pass rectDefault to
parameter rect to let the window have default position and size.
Sample 13.2\Gen
Sample 13.2\Gen demonstrates how to create applications without using document/view structure.
Originally it is a standard SDI application generated by Application Wizard. Then it is modified to become
an application that does not use document/view implementation.
Rather than creating mainframe window then using view as its client window, in the sample, only a
mainframe window is created by calling function CFrameWnd::Create(…). This can be done in the
constructor of class CMainFrame. In sample 13.2\Gen, the constructor is modified as follows:
CMainFrame::CMainFrame()
{
CString szWinName;
szWinName.LoadString(IDR_MAINFRAME);
377
Chapter 13. Adding Special Features to Application
Create
(
NULL, szWinName, WS_OVERLAPPEDWINDOW, rectDefault,
NULL, MAKEINTRESOURCE(IDR_MAINFRAME)
);
}
We must provide a window name, which will be displayed in the caption bar of the window. In
standard SDI or MDI applications, this string can be obtained from string resource IDR_MAINFRAME. To
make the sample similar to a standard application, we can load this string and use it as the window name.
Since we will not support any file type, in the sample string resource IDR_MAINFRAME contains only one
simple string (This means it does not contain several sub-strings that are separated by character ‘\n’ as in
standard SDI or MDI applications).
For a simple application, there is no need to implement status bar and tool bar any more, so function
CMainFrame::OnCreate(…) is removed, and variables CMainFrame::m_wndStatusBar, CMainFrame::
m_wndToolBar along with another global variable indicators are also deleted.
For any application implemented by MFC, it is originated from class CWinApp. This class has a CWnd
type member pointer m_pMainWnd. When the mainframe window is created, its address is stored by this
pointer. If we want to create window by ourselves, we must do the same thing in order to let the rest part of
our application have MFC features.
With the above implementation, function CGenApp::InitInstance(…) can be greatly simplified, what
we need to do here is implementing a CMainFrame type object, assigning its address to CGenApp::
m_pMainWnd, then calling functions CWnd::ShowWindow(…) and CWnd::UpdateWindow(…) to display the
window. This last step is necessary, if we omit it, the window will not be displayed. The following is the
modified function in the sample:
BOOL CGenApp::InitInstance()
{
#ifdef _AFXDLL
Enable3dControls();
#else
Enable3dControlsStatic();
#endif
m_pMainWnd=new CMainFrame();
m_pMainWnd->ShowWindow(m_nCmdShow);
m_pMainWnd->UpdateWindow();
return TRUE;
}
Here variable m_nCmdShow indicates how the window should be displayed (minimized, maximized,
etc). It must be passed to function CWnd::ShowWindow(…) in order to initialize the window to a specified
state.
378
Chapter 13. Adding Special Features to Application
Check here to
exclude selected
file from build
Figure 13-2. Both “Bar Chart” and “Pie Chart” can interpret percentages
……
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_CHARTTYPE,
RUNTIME_CLASS(CChartDoc),
RUNTIME_CLASS(CChildFrame),
RUNTIME_CLASS(CChartView));
AddDocTemplate(pDocTemplate);
……
The constructor of class CMultiDocTemplate has four parameters, the first of which is a string resource
ID, which comprises several sub-strings for specifying the default child window title, document type, and
379
Chapter 13. Adding Special Features to Application
so on. The rest three parameters must use RUNTIME_CLASS macro, and we can use the appropriate class
name as its parameter. In the code listed above, the child window uses class CChildFrame to create the
frame window, and uses CChartView to create the client window. The child window is attached to the
document implemented by class CChartDoc.
Sample 13.3\Chart
Sample 13.3\Chart demonstrates how to attach multiple views to one document. It is a standard MDI
application generated by Application Wizard. The purpose of this application is to interpret data stored in
the document in different ways. The original classes generated by Application Wizard are CChartApp,
CChartDoc, CChartView, CMainFrame and CChildFrame. After the application skeleton is generated, a new
class CPieView (derived from CView) is add to the application through using Class Wizard.
Data stored in the document is very simple, there are three variables declared in class CChartDoc:
CChartDoc::m_nA, CChartDoc::m_nB and CChartDoc::m_nC. The variables are initialized in the constructor
as follows:
CChartDoc::CChartDoc()
{
m_nA=20;
m_nB=70;
m_nC=10;
}
Three variables each represents a percentage, so adding them up will result in 100. There are many
different types of charts that can be used to interpret them, two most common ones are “bar chart” and “pie
chart”.
In the sample application, two different types of views are attached to one document, so the user can
use either “Bar chart” or “Pie chart” to view the data. To obtain data from the document, function
CChartDoc::GetABC(…) is implemented to let these values be accessible in the attached views.
In function CChartView::OnDraw(…), three bars are drawn using different colors, their heights
represent the percentage of three member variables. For class CPieView, three pies are drawn in different
colors and they form a closed circle, whose angles represent different percentages.
Two views are attached to the document in function CChartApp::InitInstance(). Besides the
standard implementation, a new document template is created and added to the application. The following
portion of function CChartApp::InitInstance() demonstrates how the two views are attached to the same
document:
……
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_CHARTTYPE,
RUNTIME_CLASS(CChartDoc),
RUNTIME_CLASS(CChildFrame),
RUNTIME_CLASS(CChartView));
AddDocTemplate(pDocTemplate);
With the above implementation, when a new client is about to be created, the user will be prompted to
choose from one of the two types of views. Here strings that are included in the prompt dialog box should
380
Chapter 13. Adding Special Features to Application
be prepared as sub-strings contained in the string resources that are used to identify document type
(IDR_CHARTTYPE and IDR_PIETYPE in the sample).
The format of the document type string is the same with that of a normal MDI application, which
comprises several sub-strings. The most important ones are the first two sub-strings: one will be used as the
default title of the client window and the other will be used in the prompt dialog box to let the user select an
appropriate view when a new client window is about to be created. In the sample, the contents of string
IDR_CHARTYPE and IDR_PIETYPE are as follows:
\nBar\nBar\n\n\nChart.Document\nChart Document
\nPie\nPie\n\n\nChart.Document\nChart Document
Both the window and the string contained in the prompt dialog box for chart view are set to “Bar”, for
pie view, they are set to “Pie”.
To offset origin in either page-space or device-space, we can use the following functions:
381
Chapter 13. Adding Special Features to Application
non-equally scaling of them. So for MM_ANISOTROPIC mode, if we draw a circle in the page-space, the
output could be an ellipse in the device-space.
Function Usage
CDC::SetWindowExt(…) Specify a dimension in page-space (window extents)
CDC::ScaleWindowExt(…) Scale the window extents relative to the current values
CDC::SetViewportExt(…) Specify how the original dimension (set by function
CDC::SetWindowExt(…)) will be come after mapping (the mapped
dimension is called view port extents)
CDC::ScaleViewportExt(…) Scale the view port extents relative to the current values
It is the ratio, rather than their absolute values, of window extents and view port extents that specify
how a logical unit will be mapped in horizontal as well as vertical directions. It is important that if use
MM_ISOTROPIC mode, after calling function CDC::SetMapMode(…), CDC::SetWindowExt(…) needs to be
called before CDC::SetViewportExt(…).
pDC->SetMapMode(MM_ISOTROPIC);
GetClientRect(rect);
pDC->SetWindowExt(100, 100);
pDC->SetViewportExt
(
pDC->GetDeviceCaps(LOGPIXELSX),
-pDC->GetDeviceCaps(LOGPIXELSY)
);
pDC->SetViewportOrg(rect.right/2, rect.bottom/2);
rect=CRect(-100, 100, 100, -100);
ptStart.x=rect.right;
ptStart.y=0;
ptEnd.x=(long)(r*cos(nA*2*pi/100));
ptEnd.y=(long)(r*sin(nA*2*pi/100));
brush.CreateSolidBrush(RGB(255, 0, 0));
pBrOld=pDC->SelectObject(&brush);
pDC->Pie(rect, ptStart, ptEnd);
pDC->SelectObject(pBrOld);
brush.DeleteObject();
382
Chapter 13. Adding Special Features to Application
ptStart=ptEnd;
ptEnd.x=(long)(r*cos((nA+nB)*2*pi/100));
ptEnd.y=(long)(r*sin((nA+nB)*2*pi/100));
brush.CreateSolidBrush(RGB(0, 255, 0));
pBrOld=pDC->SelectObject(&brush);
pDC->Pie(rect, ptStart, ptEnd);
pDC->SelectObject(pBrOld);
brush.DeleteObject();
ptStart=ptEnd;
ptEnd.x=(long)(r*cos((nA+nB+nC)*2*pi/100));
ptEnd.y=(long)(r*sin((nA+nB+nC)*2*pi/100));
brush.CreateSolidBrush(RGB(255, 255, 0));
pBrOld=pDC->SelectObject(&brush);
pDC->Pie(rect, ptStart, ptEnd);
pDC->SelectObject(pBrOld);
brush.DeleteObject();
}
In the above code, first MM_ISOTROPIC mode is set. Then the window extents is set to (100, 100). To
map one logical unit to an absolute size, function CDC::GetDevice(…) is called using both LOGPIXELSX and
LOGPIXELSY parameters. This will cause the function to return the number of pixels per logical inch in both
horizontal and vertical directions. Then we use the returned values to set view port extents. This will cause
100 logical units to be mapped to 1 inch in both horizontal and vertical directions. By doing this, no matter
where we run the program, the output will be the same dimension.
When calling function CDC::SetViewportExt(…), we set the vertical extent to a negative value. This
will change the orientation of the y-axis so that the positive values locate at the upper part of the axis (See
Figure 13-3).
Next, function CDC::SetViewportOrg(…) is called to set the device origin to the center of the window.
This will simplify the calculation of starting and ending points when drawing pies.
383
Chapter 13. Adding Special Features to Application
In the sample, just for the purpose of demonstration, a new type of document is added without
implementing anything (It does not contain any data). Although it is a dummy document, by attaching a
CEditView type view to it we know that several documents can co-exist in one application.
The class name of the new document is CTextDoc, and is added through using Class Wizard. The view
that will be associated with it is CTextView, which is derived from class CEditView. No special change is
made to the two classes. In function CChartApp::InitInstance(), after two views are attached to class
CChartDoc, the new view is attached to the new document and they are bound together to the mainframe
window:
……
pDocTemplate = new CMultiDocTemplate(
IDR_TEXTTYPE,
RUNTIME_CLASS(CTextDoc),
RUNTIME_CLASS(CChildFrame),
RUNTIME_CLASS(CTextView));
AddDocTemplate(pDocTemplate);
……
Besides this, we also added following resources to the application: icons IDR_PIETYPE and
IDR_TEXTTYPE; menus IDR_PIETYPE and IDR_TEXTTYPE; string IDR_TEXTTYPE.
384
Chapter 13. Adding Special Features to Application
left position =
(
left position of the caption window +
border width +
system button horizontal size +
frame width
)
Caption Text
Minimize Button
Icon Frame
Maximize Button
Close Button
Figure 13-4. Caption bar
right position =
(
right position of the caption window -
border width -
frame width -
3×(system button horizontal size)
)
If we use window DC, the coordinates of the window’s top-left corner are (0, 0), this will simplify our
calculation.
Please note that we must use class CWindowDC to create DC for painting the non-client area rather than
using class CClientDC. Class CClientDC is designed to let us paint only within a window’s client area, so
its origin is located at left-top corner of the client window. Class CWindowDC can let us paint the whole
window, including both client and non-client area.
Sample 13.5\Cap
Sample application 13.5\Cap demonstrates this technique. It is a standard SDI application generated by
Application Wizard, and its caption window is painted yellow no matter what the corresponding system
color is (The default caption bar color can be customized by the user). The modifications made to the
application are all within class CMainFrame, and there are altogether four message handlers added to the
385
Chapter 13. Adding Special Features to Application
The caption text is obtained by combining the name of currently opened document with the string
stored in resource AFX_IDS_APP_TITLE. Here resource AFX_IDS_APP_TITLE stores application name, and
function CDocument::GetTitle() returns the name of currently opened document.
Then the area where we can put caption text is calculated and stored in a local variable rectDraw.
Before drawing the text, we need to fill it with the background color:
……
GetWindowRect(rect);
rectDraw.left=nCapButtonX+nBorderX+nFrameX;
rectDraw.top=nFrameY;
rectDraw.right=rect.Width()-3*nCapButtonX-nFrameX-nBorderX;
rectDraw.bottom=rectDraw.top+::GetSystemMetrics(SM_CYSIZE);
dc.FillSolidRect(rectDraw, color);
……
Since the DC is created using class CWindowDC, the coordinates of the window’s origin are (0, 0).
Before drawing the text, we need to set the text background mode to transparent. Also, in the sample, when
the caption text is being drawn, it is centered instead of being aligned left:
……
dc.FillSolidRect(rectDraw, color);
nBkMode=dc.SetBkMode(OPAQUE);
dc.DrawText(szCaption, rectDraw, DT_CENTER);
dc.SetBkMode(nBkMode);
……
386
Chapter 13. Adding Special Features to Application
Problem
To implement such type of window, we can let the application paint only within the elliptical area and
leave the rest area unchanged.
However, this will cause problem when the user moves or resizes the window. Although only the
elliptical area is painted, the window is essentially rectangular. By default, the portion not covered by the
ellipse will be treated as the background (It is not updated when message WM_PAINT is received). The
application itself can handle WM_ERASEBKGND message to update the background. To let the window have an
irregular shape, instead of painting this area with any pattern, we need to make it transparent. In order to
achieve this, we shouldn’t do anything after receiving message WM_ERASEBKGND. However, this still will
cause new problem when the window is moved or resized: since the application does not update its
background client area, original background pattern will remain unchanged after moving and resizing (This
will cause something doesn’t belong to the window background to move along with it).
Style WS_EX_TRANSPARENT
A window’s background could be made transparent by using style WS_EX_TRANSPARENT when we create
a window by calling function CWnd::CreateEx(…). Unfortunately, in MFC, the window creation procedure
is deeply hidden in the base classes. Although it is very easy to create special windows such as frame
windows, views, dialog boxes, buttons, we actually have very few controls over their styles.
387
Chapter 13. Adding Special Features to Application
Another difficult thing is that, if we want to create an irregular shape window, normally we do not
want it to have caption bar. If we want to create the window by ourselves instead of using MFC, we need to
choose appropriate window styles and take care everything by ourselves, which may be a very complex
issue.
Item Consideration
Caption bar We do not need caption bar for a “callout” window, so we need to uncheck “Title Bar”
check box in “Styles” page.
Resizing We want the “callout” window to be resizable, so in the “Border” combo box, we need to
select “Resizing” style.
Background We want the “callout” window’s background to be transparent, so in the “Extended Styles”
page, we need to check “Transparent” check box. This will cause the window to have
WS_EX_TRANSPARENT style.
All other styles remain unchanged, we need to use default settings for them.
Sample 13.6\Balloon is implemented this way. It is a dialog box based application generated by the
Application Wizard, and the two classes used to implement the application are CBalloonApp and
CBalloonDlg. After the skeleton is created we can open the default dialog box template (In the sample, the
template is IDD_BALLOON_DIALOG), remove the default buttons and controls, then customize the window
styles as mentioned above.
void CBalloonDlg::OnNcPaint()
{
}
388
Chapter 13. Adding Special Features to Application
We do need to override CBalloonDlg::OnPaint() to paint the elliptical area. Within this function, an
ellipse is drawn in the client area, also a pointer is drawn at the left bottom corner. Both ellipse and its
pointer are painted using yellow color.
CDialog::OnMouseMove(nFlags, point);
if((nFlags & MK_LBUTTON) && m_bCapture)
{
pt=point;
ClientToScreen(&pt);
GetWindowRect(rect);
rect.OffsetRect(CPoint(pt)-m_ptMouse);
m_ptMouse=pt;
MoveWindow(rect);
}
}
This implementation is slow, because when mouse moves a small distance, the whole window need to
be redrawn at the new position. An alternative solution is to draw only the rectangular outline when mouse
is moving (with the left button held down), and update the window only when the left button is released. To
implement this, instead of calling function CWnd::MoveWindow(…) in WM_MOUSEMOVE message handler, we
need to call function CDC::DrawDragRect(…) to erase the previous rectangle outline and draw a new one.
For this sample, as the left button is clicked on any portion of the rectangular window area, the
application will respond. If we want the window to be movable only when the clicking happens within the
elliptical area, we need to use region. To implement this, instead of calling function CRect::PtInRect(…),
we can call CRgn::PtInRegion(…) to respond to the left button clicking. Also, if we want to make resizing
more flexible, we can test if the mouse cursor is over the border of the ellipse rather let the resizing be
handled automatically (By default, window’s rectangular frame will be used as the resizing border). To
implement this, we need to change mouse cursor’s shape when it is over the border of the ellipse, set
capture when the left button is pressed down, release capture when the button is released, and resize the
window according to the mouse movement.
Although the application does not resemble a dialog box, we can still find some of its features: it can
be closed by pressing ENTER or ESC key. To modify this feature, we need to override function
CDialog::OnPreTranslateMsg(…). If we implement this, we must provide a way to close the window,
otherwise the user has to ask OS to end its life everytime.
389
Chapter 13. Adding Special Features to Application
This will set a registration key in the registry, all the information stored by the application will be
under this key. In the above statement the registration key is “Local AppWizard-Generated
Applications”. By default, all the applications generated by the Application Wizard will share this key. If
we want the application to use a different key, we can simply change this default string.
[Window Position]
[Splitter Window]
Vertical Size=100;
There are two sections here, the section keys are “Window Position” and “Splitter Window”
respectively. Under “Window Position” section, there are two entries, the first is “Window Position” and
the second is “Window State”, both of them store strings. The second section is “Splitter Window”, it has
390
Chapter 13. Adding Special Features to Application
only one entry “Vertical Size”, which stores an integer. When we store and retrieve a particular entry, we
need to provide the correct section key and entry key.
Sample 13.7\Ini
Now that we know how to save and load the information, we need to find out what kind of information
need to be saved. Sample 13.7\Ini demonstrates how to create an application that can resume its previous
states including the window state (minimized, maximized, or normal state), size, position and the states of
the tool bar. It is a standard SDI application generated by Application Wizard, which has a default tool bar.
Its client window is implemented by a two-pane splitter window.
The most appropriate place to save the state information is before the application is about to exit. This
corresponds to receiving message WM_CLOSE, which indicates that the application will terminate shortly.
Since most information concerns the top parent window of the application, it would be more convenient if
we handle this message in the mainframe window.
To retrieve a window’s position and size, we can call function CWnd::GetWindowRect(…). The values
obtained through calling this function will be in the coordinate system of the desktop window. When the
application is invoked next time, we need to call function CWnd::MoveWindow(…) to resume its previous
position and size. This should be handled in function CMainFrame::OnCreate(…). The following two
functions show how the frame window information is saved and loaded:
void CMainFrame::OnClose()
{
CRect rect;
CWinApp* pApp=AfxGetApp();
CString szStr;
int cySize;
int cyMinSize;
ASSERT(pApp);
GetWindowRect(rect);
szStr.Format("%d,%d,%d,%d", rect.left, rect.top, rect.right, rect.bottom);
if(IsIconic() == TRUE)
{
pApp->WriteProfileInt
(
FRAME_WINDOW,
WINDOW_STATUS,
WINDOW_STATUS_ICONIC
);
}
else
{
if(IsZoomed() == TRUE)
{
pApp->WriteProfileInt
(
FRAME_WINDOW,
WINDOW_STATUS,
WINDOW_STATUS_ZOOMED
);
}
else
{
pApp->WriteProfileInt
(
FRAME_WINDOW,
WINDOW_STATUS,
WINDOW_STATUS_NORMAL
);
pApp->WriteProfileString(FRAME_WINDOW, WINDOW_POSITION, szStr);
}
m_wndSplitter.GetRowInfo(0, cySize, cyMinSize);
pApp->WriteProfileInt(FRAME_WINDOW, SPLITTER_SIZE, cySize);
}
SaveBarState(TOOL_BAR);
CFrameWnd::OnClose();
}
391
Chapter 13. Adding Special Features to Application
{
……
szStr=pApp->GetProfileString(FRAME_WINDOW, WINDOW_POSITION);
if(!szStr.IsEmpty())
{
sscanf
(
szStr,
"%d,%d,%d,%d",
&rect.left,
&rect.top,
&rect.right,
&rect.bottom
);
MoveWindow(rect);
}
……
}
The window state information is retrieved by calling functions CWnd::IsIconic() and CWnd::
IsZoomed(). If both of them return FALSE, the window is in normal state. To set the window to zoomed or
iconic state, we need to set variable CIniApp::m_nCmdShow to either SW_SHOWMINIMIZED or
SW_SHOWMAXIMIZED in function CWinApp::InitInstance()(This variable is declared in base class CWinApp).
The following portion of function CIniApp::InitInstance() shows how the window state is set:
BOOL CIniApp::InitInstance()
{
……
switch
(
GetProfileInt
(
FRAME_WINDOW,
WINDOW_STATUS,
WINDOW_STATUS_NORMAL
)
)
{
case WINDOW_STATUS_ICONIC:
{
m_nCmdShow=SW_SHOWMINIMIZED;
break;
}
case WINDOW_STATUS_ZOOMED:
{
m_nCmdShow=SW_SHOWMAXIMIZED;
break;
}
}
……
}
Finally, saving and loading the states of tool bar is very simple, all we need to do is calling function
CFrameWnd::SaveBarState(…) to save the tool bar state after receiving message WM_CLOSE and calling
function CFrameWnd::LoadBarState(…) in CFrameWnd::OnCreate(…) to load them. No matter how many
tool bars are implemented in the application, all of their states will be saved and loaded automatically. The
following code fragment demonstrates this:
void CMainFrame::OnClose()
{
……
SaveBarState(TOOL_BAR);
CFrameWnd::OnClose();
}
return 0;
}
392
Chapter 13. Adding Special Features to Application
With the above implementation, the application is able to remember its previous states.
Sample
Sample 13.8\Sender and 13.8\MsgRcv demonstrate how to send user-defined messages between two
applications. Here, “Sender” is a dialog box based application, and “MsgRcv” is a list view based
application. Both of them register two messages: MSG_SENDSTRING and MSG_RECEIVED. The two macros are
defined in “Common.h” header file, which is included by both projects. The user can freely input any
number in one of the edit box contained in “Sender”, and press “Send” button to send the message to
“MsgRcv”. Before sending the message, “Sender” will search for “MsgRcv”, if the application exists, it
will send MSG_SENDSTRING message to it, with the number input by the user as the message parameter.
Upon receiving the message, “MsgRcv” will add the number to its list, then send back an MSG_RECEIVED
message. Upon receiving this message, “Sender” increments a counter indicating how many messages are
sent successfully, and its value will be displayed in the dialog box.
393
Chapter 13. Adding Special Features to Application
Two global variables are declared to store the registered message IDs in file “MainFrm.cpp” for both
applications:
UINT g_uMsgSendStr=0;
UINT g_uMsgReceived=0;
For both applications, in function CMainFrame::OnCreate(…), the messages are registered as follows:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
……
ON_REGISTERED_MESSAGE(g_uMsgSendStr, OnSendStr)
END_MESSAGE_MAP()
We use WPARAM and LPARAM parameters to pass the window handle of “Sender” and the number input
by the user to “MsgRcv”. By sending the handle with message MSG_SENDSTRING, the message receiver can
use it to make reply immediately, there is no need to find the window again.
Function CMainFrame::OnSendStr(…) is listed as follows, it demonstrates how message
MSG_SENDSTRING is handled in “MsgRcv”. Like a normal WM_MESSAGE type message handler, this function
has two parameters that are used to pass WPARAM and LPARAM. In the sample, WPARAM parameter is used to
pass the window handle of “Sender”, from which we can obtain a CWnd type pointer and use it to send
MSG_RECEIVED message back. After that the value obtained from LPARAM parameter is added to the list view.
The following is this function:
pWnd=CWnd::FromHandle((HWND)wParam);
ASSERT(pWnd != NULL);
szText.Format("%d", (int)lParam);
pWnd->PostMessage(g_uMsgReceived);
CListCtrl &lc=((CMsgRcvView *)GetActiveView())->GetListCtrl();
lc.InsertItem(lc.GetItemCount(), szText);
return (LONG)TRUE;
}
On the “Sender” side, if the user presses “Send” button, we call function CWnd::FindWindow(…) to find
the mainframe window of “MsgRcv”. If it exists, message MSG_SENDSTRING will be sent to it, with the
WPARAM parameter set to the window handle of the dialog box and LPARAM parameter set to the number
contained in the edit box:
void CSenderDlg::OnButtonSend()
{
CWnd *pWnd;
UpdateData(TRUE);
pWnd=CWnd::FindWindow(CLASS_NAME_RECIEVER, NULL);
if(pWnd != NULL && ::IsWindow(pWnd->m_hWnd))
{
394
Chapter 13. Adding Special Features to Application
return (LONG)TRUE;
}
It is fun to play with the two applications, because they implement the simplest communication
protocol: sending the message, replying with the acknowledgment. By using message communication, we
can send only simple information (like integers) to another application. If we want to send complex data
structure to other processes, we need to use other memory sharing techniques such as file mapping along
with message sending.
13.9 Z-Order
Z-order represents the third dimension of a window besides its x and y position. Under Windows, a
window can be placed before or after another window, and none of the two windows can have a same Z-
order (This means if the two windows share a common area, one of them must be overlapped by the other).
Under Windows, the Z-order of a top-most application window (the window that does not have
parent window) is managed by the OS. When the user clicks the mouse on an application, this window will
be brought to the top of the Z-order by default. If this happens, also, the orders of all other windows will be
changed accordingly.
A window’s Z-order can also be changed from within the application. The function that can be used to
implement this is CWnd::SetWindowPos(…). Besides Z-order, this function can also be used to change the x-
y position and the dimension of a window. It is more powerful than function CWnd::MoveWindow(…), which
could be used to move a window only in the x-y plane.
Function CWnd::SetWindowPos(…) has six parameters:
BOOL CWnd::SetWindowPos
(
const CWnd *pWndInsertAfter, int x, int y, int cx, int cy, UINT nFlags
);
The middle four parameters (x, y, cx and cy) can be used to change a window’s x-y position and
size. If we want to change only the Z-order of a window, we can set these variables to 0s and set nFlags to
SWP_NOMOVE | SWP_NOSIZE.
The first parameter is a CWnd type pointer, it indicates where the window should be placed. This gives
us the power to place a window before or after any existing window in the system. More over, we can
specify other four parameters: CWnd::wndBottom, CWnd::wndTop, CWnd::wndTopMost, CWnd::
wndNoTopMost. Among these parameters the most interesting parameter is CWnd::wndTopMost, if we use
this parameter, the window will always stay on top of other windows under any condition.
Sample 13.9\ZOrder demonstrates how to change a window’s Z-order. It is a dialog based application
generated by the Application Wizard. The only controls contained in the dialog template are four radio
buttons. If the user click on one of them, function CWnd::SetWindowPos(…) will be called using the
corresponding parameter:
void CZOrderDlg::OnRadioBottom()
{
SetWindowPos(&wndBottom, NULL, NULL, NULL, NULL, SWP_NOMOVE | SWP_NOSIZE);
}
void CZOrderDlg::OnRadioNotopmost()
{
395
Chapter 13. Adding Special Features to Application
void CZOrderDlg::OnRadioTop()
{
SetWindowPos(&wndTop, NULL, NULL, NULL, NULL, SWP_NOMOVE | SWP_NOSIZE);
}
void CZOrderDlg::OnRadioTopmost()
{
SetWindowPos(&wndTopMost, NULL, NULL, NULL, NULL, SWP_NOMOVE | SWP_NOSIZE);
}
By checking “wndTopMost” radio button, the dialog box will always stay on top of other windows.
13.10 Hook
Hook is a very powerful method in Windows programming. Remember when creating the snapshot
application in Chapter 12, when the application was made hidden to let the user make preparation, a timer
was started. The capture would be made just after the timer times out. This is a little inconvenient, because
the time that allows the user to make preparation is fixed. The ideal implementation would be like this:
instead of setting a timer, we can predefine a key stroke; the user can feel free to make any preparation as
the application is hidden; the capture will be made only after the user presses the predefined key.
There are two ways of implementing this feature: 1) Register the hot key by calling function
::RegisterHotKey(…). After doing this, whenever user presses the hot key, a WM_HOTKEY type message will
be sent to the application. 2) Install a keyboard hook in the system to monitor all the keystrokes. If the hot
key is pressed, we can activate the hidden application.
Obviously, the second method will slow down the system, because it tries trap every keystroke in the
system. Whenever possible, this method should be avoided.
However, in some situations, we can use this powerful feature to create some special effects. For
example, if we want to trap a special combination of key strokes, it is relatively difficult to implement it by
registering hot keys.
In this section we will demonstrate how to implement keyboard hook.
Hook Installation
Hooks can be installed either system wide or specific to a single thread. In the former case, we can
monitor the activities in the whole system. To install a hook, we need to provide a hook procedure, which
will be used to handle the intercepted message. For different kind of hooks, we need to provide different
procedures. For example, the mouse hook procedure and the keyboard hook procedure look like the
following:
Although they look the same, the meanings of their parameters are different. For keyboard hook
procedure, WPARAM parameter represents virtual-key code, and lParam parameter represents keystroke
information. For mouse hook procedure, WPARAM parameter represents message ID, and LPARAM represents
mouse coordinates. Different types of hooks have different hook procedures, they should be provided by
the programmer when one or more types of hooks are implemented.
To install a hook, we need to call function ::SetWindowsHookEx(…):
The first parameter indicates the type of hook, for a keyboard hook, it should be WH_KEYBOARD, for a
mouse hook, it should be WH_MOUSE. The second parameter is the address of the hook procedure described
above. The third and fourth parameters should be set differently for system wide hook and thread specific
hook.
396
Chapter 13. Adding Special Features to Application
Variables in DLL
Obviously in our case, we want the hook to be system wide, so we have to build a separate DLL. This
causes us some inconvenience. When the hook procedure receives the hot-key stroke, it needs to activate
the application. But since the DLL and the application are separate from one another, it is difficult to access
the application process from the DLL.
Suppose we want to implement the hot-key based screen capturing, as we execute Capture | Go!
command (see Chapter 12), we can hide the application by calling function CWnd::ShowWindow(…) using
SW_HIDE flag. From now on the application has no way to receive keystrokes, we have to process them in
the keyboard hook procedure residing in the DLL. As we get the predefined key stoke, we need to make the
capture and call function CWnd::ShowWindow(…) using flag SW_SHOW to activate the application.
We can implement this by sending message from DLL to the application. If the DLL knows the
window handle of the application’s mainframe window, this can be easily implemented. To pass the
window handle to the DLL, we can call a function exported from the DLL when the hook is installed, and
ask the DLL to keep this handle for later use.
However, data stored in the DLL is slightly different from data stored in a common process. For a
normal process, it has its own memory space, the static variables stored in this space will not change
throughout their lifetime. However, for a DLL, since it can be shared by several processes, its variables are
mapped to the application’s memory space separately. By doing this, for a variable contained in the DLL,
different applications may have different values. This will eliminate data conflicting among different
processes.
To prevent the value of a variable stored in DLL from being changed, we can define a custom data
segment to store the variable.
DLL Implementation
Sample 13.10\Hook demonstrates keyboard hook implementation. The hook procedure stays in a
separate DLL file: “HookLib.Dll”. Creating a DLL is slightly different from creating MFC based
applications, there is no skeleton generated at the beginning. After using the application wizard to generate
a Win32 based DLL project, we are not provided with a single line of source code.
Since our DLL is relatively small, we can use just one “.c” and “.h” file to implement it. We can create
these two files by opening new text files, then executing command Project | Add To Project | Files... to
add them into the project.
In the sample, the DLL is implemented by “HookLib.h” and “HookLib.c” files.
File “HookLib.h” declares all the functions that will be included in the DLL:
#if !defined(__HOOKLIB_H__)
#define __HOOKLIB_H__
#include "Windows.h"
#include "BaseTyps.h"
#if !defined(__DLLIMPORT__)
397
Chapter 13. Adding Special Features to Application
#else
#endif
#endif
Function LibMain(…) and WEP(…) are the entry point and the exit point of the DLL respectively. The
reason for using so many #if macros here is that it enables us to use the same header file for both the DLL
and the application that links the DLL. As we build the DLL, we want to export functions so that they can
be called outside the DLL; in the application, we need to import these functions from the DLL. Macro
__declspec(dllimport) declares an import function and __declspec(dllexport) declares an export
function. As we can see, if macro __DLLIMPORT__ is defined, function LibMain(…), WEP(…) and
KeyboardProc(…) will be declared (they will be used only in the DLL). In this case, two other functions
SetKeyboardHook(…) and UnsetKeyboardHook() will be declared as import function. If the macro is not
defined, the two functions will be declared as export functions.
The reason for using macro EXTERN_C is that the DLL is built with C convention and our application is
built with C++ convention. To make them compatible, we must explicitly specify how to build the
functions in the DLL. In the sample, two functions are exported from the DLL: SetKeyboardHook(…) will
be used by the application to install hook; UnsetKeyboardHook() will be used to remove the hook.
In file “HookLib.c”, first two static variables are declared:
#pragma data_seg("SHARDATA")
static HWND g_hWnd=NULL;
static HHOOK g_hHook=NULL;
#pragma data_seg()
We use #pragma data_seg("SHARDATA") to specify that g_hWnd and g_hHook will be stored in
"SHARDATA" segment instead of being mapped to calling processes.
Function SetKeyboardHook(…) installs system wide keyboard hook by calling function
SetWindowsHookEx(…). When using this function, we must provide the instance handle of the DLL library
and the handle of the application’s mainframe window:
return TRUE;
}
The handle of application’s mainframe window is stalled in variable g_hWnd for later use. The handle
of the hook is stored in variable g_hHook and will be used in function UnsetKeyboardHook() to remove the
keyboard hook:
STDENTRY_(BOOL) UnsetKeyboardHook()
{
return UnhookWindowsHookEx(g_hHook);
398
Chapter 13. Adding Special Features to Application
Function KeyboardProc(…) is the hook procedure. When there is a keystroke, this function will be
called, and the keystroke information will be processed within this function:
nKeyState=GetKeyState(VK_CONTROL);
if(lParam & 0x80000000 || lParam & 0x40000000)
{
return CallNextHookEx(g_hHookKeyboard, code, wParam, lParam);
}
switch(wParam)
{
case VK_F3:
{
if(IsWindow(g_hWnd) && HIBYTE(nKeyState))
{
if(IsWindowVisible(g_hWnd) == FALSE)
{
ShowWindow(g_hWnd, SW_SHOW);
UnsetKeyboardHook();
}
}
break;
}
}
}
The first parameter, code, indicates the type of keystroke activities. We need to respond only when
there is a keystroke action, in which case parameter code is HC_ACTION. If code is less than 0, we must pass
the message to other hooks without processing it (This is because there may be more than one hook
installed in the system). In order not to change the behavior of other applications, after processing the
keystroke message, we also need to call function ::CallNextHookEx(…) to let the message reach its
original destination.
If the keystroke is CTRL+F3, we will check if the application window is visible. If not, function
::ShowWindow(…) is called to activate it. In this case, the keyboard hook will be removed.
We need to use -SECTION link option in order to implement "SHARDATA" data segment. This can be
done through executing Project | Settings... command (Or pressing ALT+F7 hot key) then clicking “Link”
tab on the popped up property sheet. In the window labeled “Project Options”, we need to add “-
SECTION:SHARDATA,RWS” at the end of link option. This will make the data in this segment to be readable,
writable, and be shared by several processes (Figure 13-7).
399
Chapter 13. Adding Special Features to Application
Sample 13.6\Hook
Sample 13.6\Hook is a standard SDI application generated by the Application Wizard. In the sample,
header file “HookLib.h” is included in the implementation file “MainFrm.cpp”, also, macro __DLLIMPORT__
is defined there. This will import two functions contained in the DLL. The following portion of file
“MainFrm.cpp” shows how the header file is included and how the macro is defined:
#define __DLLIMPORT__
#include "stdafx.h"
#include "..\HookLib\HookLib.h"
#include "Hook.h"
To use the functions in DLL, we need to link file “HookLib.Lib” which is generated when the DLL is
being built. This can be done by executing command Project | Setting..., clicking tab “Link” from the
popped up property page, and entering the path of file “HookLib.Lib” in edit box labeled “Object/Library
Modules” (Figure 13-8).
The keyboard hook is installed in function CMainFrame::OnCreate(…). Also, within the function, DLL
is dynamically loaded by calling function ::LoadLibrary(…). The returned value of this function is the
DLL’s instance (if the DLL is loaded successfully), which will be used to install the keyboard hook. The
following code fragment shows how the DLL is loaded:
……
hInstance=LoadLibrary("HookLib.Dll");
ASSERT(hInstance);
……
The DLL library is released before the application is about to exit in function
CMainFrame::OnClose():
void CMainFrame::OnClose()
{
FreeLibrary(hInstance);
CFrameWnd::OnClose();
}
Figure 13-8. Before linking the library, we must provide its path
A command Hide | Go! is added to the mainframe menu IDR_MAINFRAME, this command installs
keyboard hook and hides the application. We can press CTRL+F4 to show the application after it becomes
hidden. The following is the implementation of this command:
void CMainFrame::OnHideGo()
{
SetKeyboardHook(GetSafeHwnd(), hInstance);
ShowWindow(SW_HIDE);
}
400
Chapter 13. Adding Special Features to Application
The application and the DLL should be in the same directory in order let the DLL be loaded
successfully. Or, the DLL may be stored under “Windows” or “Windows\System” directory.
For journal record procedure, we need to record event only when parameter code is HC_ACTION. At this
time, the event message is stored in an EVENTMSG type object, whose address can be obtained from
parameter lParam. Structure EVENTMSG stores hardware message sent to the system message queue, along
with time stamp indicating when the message was posted. We can use the information contained in this
structure to implement playback.
Analyzing Events
Another thing we need to pay attention to is that we need to provide a way of letting the user stop
recording and rendering playback at any time. By default, Windows allows journal hook to be stopped by
any of the following key stroking: CTRL+ESC, ALT+ESC and CTRL+ALT+DEL. Besides the default
feature, it is desirable to provide a build-in function for stopping the journal hook. We can predefine a key
for this purpose. The key pressing events can be trapped by analyzing EVENTMSG type object, which has the
following format:
Member message specifies event type. In order to trap keystroke, we need to respond to WM_KEYDOWN
activities. In this case, the virtual key code is stored in the lower byte of member paramL.
Sample 13.11\Hook demonstrates journal record and playback hook implementation. Like previous
section, here hook functions are also implemented in the DLL. For this sample, 13.11\Hook is based on
13.10\Hook, and 13.11\HookLib is based on 13.10\HookLib.
For events other than specified key stroke, we need to allocate enough buffers for storing an EVENTMSG
type object and bind them together using linked list. To implement this, we can define a new structure of
our own:
Pointer lpNextEvent will point to the next EVENTMSG structure. This will form a singly linked list.
401
Chapter 13. Adding Special Features to Application
The following code fragment shows how events are recorded in journal record hook procedure. Like
other types of hooks, we need to call CallNextHookEx() to pass the event to other hooks if parameter code
is less than 0:
if(code >= 0)
{
lpEvent=(LPEVENTMSG)lParam;
if
(
lpEvent->message == WM_KEYDOWN &&
LOBYTE(lpEvent->paramL) == VK_F3 &&
HIBYTE(GetKeyState(VK_CONTROL))
)
{
UnsetJournalRecordHook();
PostMessage(g_hWnd, uMsgFinishJournal, (WPARAM)FALSE, (LPARAM)NULL);
return FALSE;
}
……
In the above code fragment, we check if the event is CTRL+F3 key stroking. If so, we remove the
hook and send a message to the application’s mainframe window indicating that the recording is finished.
……
if((lpEventNode=(LPEVENTNODE)malloc(sizeof(EVENTNODE))) == NULL)
{
UnsetJournalRecordHook();
PostMessage(g_hWnd, uMsgFinishJournal, (WPARAM)FALSE, (LPARAM)NULL);
return FALSE;
}
……
In the above code fragment, we try to allocate buffers for recording the event. If this is not successful,
we also need to finish the recording.
……
if(lpEventTail == NULL)
{
dwStartRecordTime=(DWORD)GetTickCount();
lpEventHead=lpEventNode;
}
……
In the above code fragment, if lpeventTail is NULL, this is the first event we will record after the
journal record hook has been installed. We must record the current time so that we can play back the
recorded events at the rates they were generated.
……
else
{
lpEventTail->lpNextEvent=lpEventNode;
}
lpEventTail=lpEventNode;
lpEventTail->lpNextEvent=NULL;
lpEventTail->Event.message=lpEvent->message;
lpEventTail->Event.paramL=lpEvent->paramL;
lpEventTail->Event.paramH=lpEvent->paramH;
lpEventTail->Event.time=lpEvent->time;
return FALSE;
}
return CallNextHookEx(g_hHookRec, code, wParam, lParam);
}
402
Chapter 13. Adding Special Features to Application
……
if(code == HC_SKIP)
{
if(lpEventPlay->lpNextEvent == NULL)
{
free(lpEventHead);
lpEventHead=lpEventPlay=NULL;
UnsetJournalPlaybackHook();
PostMessage(g_hWnd, uMsgFinishJournal, (WPARAM)TRUE, (LPARAM)NULL);
}
……
Here, if there is no more event, we need to reset everything and send a message (the message is a user
defined message WM_FINISHJOURNAL, see below) to the mainframe window of the application indicating that
the playback is over. If there are still events left, we get the next recorded event and free the buffers holding
the event that is being played back. If parameter code is HC_GETNEXT, we need to obtain an EVENTMSG type
pointer from parameter lParam, and copy the event pointed by lpEventPlay to the object pointed by this
pointer. When doing this copy, we need to add an offset to the time stamp because originally it indicates the
time when the events were recorded:
……
else
{
lpEventPlay=lpEventPlay->lpNextEvent;
free(lpEventHead);
lpEventHead=lpEventPlay;
}
}
else if(code == HC_GETNEXT)
{
lpEvent=(LPEVENTMSG)lParam;
lpEvent->message=lpEventPlay->Event.message;
lpEvent->paramL=lpEventPlay->Event.paramL;
lpEvent->paramH=lpEventPlay->Event.paramH;
lpEvent->time=lpEventPlay->Event.time+dwTimeAdjust;
lReturnValue=lpEvent->time-GetTickCount();
if(lReturnValue < 0L)
{
lReturnValue=0L;
lpEvent->time=GetTickCount();
}
return lReturnValue;
}
403
Chapter 13. Adding Special Features to Application
void CMainFrame::OnMacroPalyback()
{
m_bEnableMenu=FALSE;
SetJournalPlaybackHook(GetSafeHwnd(), hInstance);
}
void CMainFrame::OnMacroRecord()
{
m_bEnableMenu=FALSE;
SetJournalRecordHook(GetSafeHwnd(), hInstance);
}
return TRUE;
}
To test the program, we can first execute Macro | Record command, then use mouse or keyboard to
generate a series of events. Next, we can press CTRL+F3 to stop recording. Finally we can execute Macro
| Playback command to see what has been recorded.
File Mapping
To solve this problem, in Win32 platform, there is a new technique that allows different processes to
share a same block of memory. This technique is called file mapping, and can be used to let different
processes share either a file or a block of memory.
404
Chapter 13. Adding Special Features to Application
The file or memory used for this purpose is called File Mapping Object and must be created using
special function. After it is created successfully, each process can open a view of the file or memory, which
will be mapped to the address space of the calling process.
HANDLE ::CreateFileMapping
(
HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName
);
HANDLE ::OpenFileMapping
(
DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName
);
LPVOID ::MapViewOfFile
(
HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow, DWORD dwNumberOfBytesToMap
);
File mapping object can be initiated by calling function ::CreateFileMapping(…). If we want to share
a file, we need to pass the file handle to the first parameter (hFile) of this function. If we want to share a
block of memory, we need to pass 0xFFFFFFFF to this parameter. The fourth and fifth parameters specify
the size of the object. For file sharing, they can be set to zero, in which case the whole file will be shared.
In the case of memory sharing, they must be greater than zero. Parameter lpFileMappingAttributes can
be used to specify the security attributes of the object, in most cases we can assign zero to it and use the
default attributes. Parameter flprotect specifies read and write permission. The most important parameter
is the last one, which must be the address of buffers that contain a name assigned to the file mapping
object. If any other process wants to access this object, it must also use the same name to create a view of
the file mapping object.
After the file mapping object is created successfully, the owner (the process that created the object) can
create a view of file to map the buffers to its own address space by calling function ::MapViewOfFile(…).
When doing this, we must pass the handle returned by function ::CreateFileMapping(…) to parameter
hFileMappingObject. If we pass 0 to parameters dwFileOffsetHigh, dwFileOffsetLow and
dwNumberOfBytesToMap, the whole file or memory will be mapped. Finally, parameter dwDesiredAccess
allows us to specify desired access right. This function will return a void type pointer, which could be cast
to any type of pointer.
If any other process wants to access the file mapping object, it must call functions
::OpenFileMapping(…) and ::MapViewOfFile(…) to first access it then create a view of file. When calling
function ::OpenFileMapping(…), it must pass the object name (specified by function
::CreateFileMapping(…) when the file mapping object was created) to parameter lpName. The buffers can
be mapped to the address space of the process by calling function ::MapViewOffile(…), which is exactly
the same with creating view of file for the owner of the object.
Samples
Samples 13.12\Send and 13.12\MsgRcv demonstrate how to share a block of memory between two
applications. They are based on samples 13.8\Send and 13.8\MsgRcv respectively. First, the “Sender”
application is modified so that its edit box will allow multiple line text input (When inputting the text,
CTRL+RETURN key stroke can be used to start a new line), and the original variable
CSenderDlg::m_nSent is replaced by CSenderDlg::m_szText, which is a CString type variable. The file
mapping object is created in function CSenderDlg::OnInitDialog() as follows:
BOOL CSenderDlg::OnInitDialog()
{
g_uMsgSendStr=::RegisterWindowMessage(MSG_SENDSTRING);
g_uMsgReceived=::RegisterWindowMessage(MSG_RECEIVED);
m_hMapFile=::CreateFileMapping
(
405
Chapter 13. Adding Special Features to Application
(HANDLE)0xFFFFFFFF,
NULL,
PAGE_READWRITE,
0,
BUFFER_SIZE,
MAPPING_PROJECT
);
……
}
UpdateData(TRUE);
lpMapAddress=(LPSTR)MapViewOfFile
(
m_hMapFile,
FILE_MAP_ALL_ACCESS,
0,
0,
0
);
if(lpMapAddress != NULL)
{
memcpy(lpMapAddress, m_szText, max(m_szText.GetLength(), BUFFER_SIZE));
}
pWnd=CWnd::FindWindow(CLASS_NAME_RECIEVER, NULL);
if(pWnd != NULL && ::IsWindow(pWnd->m_hWnd))
{
pWnd->PostMessage(g_uMsgSendStr, (WPARAM)GetSafeHwnd(), (LPARAM)NULL);
}
}
In project “MsgRcv”, the client window is implemented using edit view instead of original list view,
this makes it easier for us to display text. After receiving the message, we open the file mapping object,
create a view of file, then retrieve text from the buffers. Then we access the edit view, select all the text
contained in the view, and replace the selected text with the newly obtained text. Finally, the acknowledge
message is sent back:
pWnd=CWnd::FromHandle((HWND)wParam);
ASSERT(pWnd != NULL);
pWnd->PostMessage(g_uMsgReceived);
hMapFile=::OpenFileMapping
(
FILE_MAP_ALL_ACCESS,
FALSE,
MAPPING_PROJECT
);
if(hMapFile != NULL)
{
lpMapAddress=(LPSTR)MapViewOfFile
(
hMapFile,
FILE_MAP_ALL_ACCESS,
0,
0,
406
Chapter 13. Adding Special Features to Application
0
);
if(lpMapAddress != NULL)
{
szText=lpMapAddress;
}
CEdit &editCtrl=((CEditView *)GetActiveView())->GetEditCtrl();
editCtrl.SetSel(0, -1);
editCtrl.ReplaceSel(szText);
}
return (LONG)TRUE;
}
There are other methods for sharing memory among different processes, such as DDE and OLE.
Comparing to the two methods, file mapping method is relatively simple and easy to implement.
Summary
1) Before a window is created, we must stuff a WNDCLASS type object, register the window class name, and
use this name to create the window. Class WNDCLASS contains useful information about the window
such as mainframe menu, default icon, default cursor shape, brush that will be used to erase the
background, and the window class name.
1) To implement one-instance application, we need to register our own window class name, and override
function CWnd::PreCreateWindow(…). Before the window is created, we need to replace the default
window class name with the new one. By doing this, we can implement one-instance application by
searching for registered window class name: before registering the window class, we can check if there
already exists a window that has the same class name. If so, the application simply exits.
1) We can call function CWnd::FindWindow(…) to find out a window with a specific class name or
window name in the system.
1) The document/view structure is implemented by class CSingleDocTemplate or CMutiDocTemplate. If
we want to create an application that does not use document/view structure, we need to eliminate the
procedure of creating CSingleDocTemplate or CMutiDocTemplate type object and call function
CWnd::Create(…) to create the mainframe window by ourselves.
1) We can create several CMultiDocTemplate type objects in an application to let it support multiple
views or multiple documents.
1) Caption bar and window frame belong to non-client area. To paint non-client area, we need to handle
messages WM_NCPAINT and WM_NCACTIVATE.
1) To create a window with transparent background, we need to specify style WS_EX_TRANSPARENT while
creating the window.
1) An application can save its states in the system registry by calling function
CWinApp::SetRegistryKey(…). The information can be saved and loaded by calling the following
functions: CWinApp::WriteProfileInt(…), CWinApp::WriteProfileString(…), CWinApp::
GetProfileInt(…)., CWinApp::GetProfileString(…).
1) To exchange user defined messages between two different processes, we must use function
::RegisterWindowMessage(…) to register the messages.
1) Calling function CWnd::SetWindowPos(…) using parameter CWnd::wndTopMost will make a window
always stay on top of any other window.
1) Hook can be installed to let a process intercept and process Windows messages before they reach
destinations. There are several types of hooks, which include mouse hook, keyboard hook, journal
record hook, journal playback hook, etc.
1) A hook can be installed by calling function ::SetWindowsHookEx(…) and removed by calling function
::UnhookWindowsHookEx(…).
1) A DLL does not have its own memory space, instead, its variables are mapped to the memory spaces
of the calling processes. To declare static variables in the DLL, we need to specify a data segment by
using #pragma data_seg macro and -SECTION link option.
1) To share a file or a block of memory among different processes, we need to create file mapping object.
Any process that wants to access the memory must create a view of file, which will map the memory to
its own address space.
407
Chapter 13. Adding Special Features to Application
408
Chapter 14. Views
Chapter 14
Views
W
e are going to introduce various types of views in this chapter. In MFC, there are several types of
standard views that are supported by MFC classes. These views include edit view, rich edit
view, list view, form view and tree view. The classes that can be used to implement them are
CEditView, CRichEditView, CListView, CFormView and CTreeView respectively. They can be
used to display plain text, formatted text, tree, list, etc.
ID Command
ID_EDIT_UNDO undo
ID_EDIT_CUT cut
ID_EDIT_COPY copy
ID_EDIT_PASTE paste
409
Chapter 14. Views
The reason for this is that CEditView already maps commands with the above-mentioned IDs to its
built-in member functions that handle undo, cut, copy and paste commands. The name of these functions
are not documented in the current version of Visual C++, this means these function are not guaranteed to be
supported in the future.
If we want to use other command IDs instead of the recommended ones, we need to implement
command message mapping by ourselves. In order to do so, we need to look at the MFC source code that
contains member functions of class CEditView, find out the function names that support these commands,
and map the WM_COMMAND type messages to the appropriate functions.
Command ID Functionality
ID_SEARCH_FIND Find
ID_SEARCH_REPLACE Find and replace
ID_SEARCH_FINDNEXT Repeat
ON_COMMAND(ID_SEARCH_FIND, CEditView::OnEditFind)
ON_COMMAND(ID_SEARCH_REPLACE, CEditView::OnEditReplace)
ON_COMMAND(ID_SEARCH_FINDNEXT, CEditView::OnEditRepeat)
ON_UPDATE_COMMAND_UI(ID_SEARCH_FIND, CEditView::OnUpdateNeedText)
ON_UPDATE_COMMAND_UI(ID_SEARCH_REPLACE, CEditView::OnUpdateNeedText)
ON_UPDATE_COMMAND_UI(ID_SEARCH_FINDNEXT, CEditView::OnUpdateNeedFind)
With the above implementation, there is no need for us to declare and define new member functions to
handle the above commands, everything will be handled automatically.
Other Commands
In the sample, some other commands that are not supported by class CEditVew are also implemented.
These command include Edit | Delete, which can be used to delete the current selection; Edit | Select All,
which can be used to select all the text contained in the window, and Edit | Time/Date, which can be used
to insert a time/date stamp at the current caret position. For these commands, the message mapping and
message handlers need to be implemented by ourselves.
Edit view is implemented by an embedded edit control, which can be accessed by calling function
CEditView::GetEditCtrl(). Once this is done, we can call any member function of CEdit and make
change to the text contained in the window. For example, if we want to replace the selected text with a new
string or insert a string at the current caret position, we can call function CEdit::ReplaceSel(…) to do so.
If we want to select all the text, we can call function CEdit::SetSel(…) and pass 0 and -1 to its first two
parameters. If we want to delete the selected text, we just need to call function CEdit::Clear().
410
Chapter 14. Views
The following shows how command Edit | Time/Date is implemented in the sample:
void CNotePadView::OnEditTimedate()
{
CTime time;
CString szTime;
CEdit &edit=GetEditCtrl();
time=CTime::GetCurrentTime();
szTime=time.Format("%A, %B %d, %Y");
edit.ReplaceSel(szTime);
}
First the current time is obtained by calling function CTime::GetCurrentTime(), which is stored in a
CTime type variable. Then it is formatted and output to a CString type variable by calling function CTime::
Format(…). Finally, function CEdit::ReplaceSel(…) is called to insert the time stamp.
By now, our sample is almost the same with standard “Notepad” application. Obviously deriving class
from standard MFC class saves us a lot of work. If we implement the file I/O and formatted text display by
ourselves, we need to write a lot of source code.
411
Chapter 14. Views
in the clipboard, from which we can make the selection. For example, if we paste a bitmap, we have the
choice to paste it either as bitmap format, metafile format or DIB format.
The third feature of this editor is font selection. We may want to format different portion of the text
using a different font. This is the default feature of class CRichEditView. The command ID that can be used
to format the selected text using a specific font is ID_FORMAT_FONT (class CRichEditView supports this ID).
So all we need to do is adding a Format | Font… command to menu IDR_MAINFRAME. With this simple
implementation, we are able to format the text with any font that is available in the system.
Although it is very easy to build a fully functional application with a lot of enticing features, it is
relatively difficult to make modifications. For example, the standard Wordpad application under
Windows has a ruler and a format bar, if we want to add these features, we need to add them by
ourselves.
void CWordPadApp::OnFileOpen()
{
CFileDialog dlg(TRUE);
CString szName;
CRichEditDoc *pDoc;
dlg.m_ofn.lpstrFilter=STRING_FILTER;
dlg.m_ofn.nFilterIndex=1;
if(dlg.DoModal() == IDOK)szName=dlg.GetPathName();
pDoc=(CRichEditDoc *)(((CFrameWnd *)m_pMainWnd)->GetActiveDocument());
if(dlg.m_ofn.nFilterIndex == 1)
{
pDoc->m_bRTF=TRUE;
}
else pDoc->m_bRTF=FALSE;
OpenDocumentFile(szName);
}
We must map command ID_FILE_OPEN to this function in order to make it effective. In the sample,
WM_COMMANDmessage mapping for this command is customized as follows:
Original mapping:
ON_COMMAND(ID_FILE_OPEN, CWinApp::OnFileOpen)
New mapping:
412
Chapter 14. Views
ON_COMMAND(ID_FILE_OPEN, OnFileOpen)
The first parameter is a pointer to the buffers containing the file name, the second is a Boolean type
variable indicating if the file name should be changed. Actually, this parameter is always set to TRUE so
we can neglect its value.
Pointer lpszPathName gives us the file name that should be used for saving data. But this pointer can
also be NULL. If the file being edited is created through File | New command and the user has selected File
| Save or File | Save As command, lpszPathName will be NULL. The following table lists the values of
lpszPathName and bReplace under different situations:
Based on the above analysis, we can override function CDocument::DoSave(…) as follows: obtaining
the file name from lpszPathName, if it is NULL, we implement the “Save As” dialog box with multiple file
filters. After the user has selected a file name, we need to add extension to it according to the filter selected
by the user. Also, we need to set data format for file saving. Then we can call function CDocument::
OnSaveDocument(…) and pass the file name to it to implement file saving.
In the derived function CWordPadDoc::DoSave(…), we need to implement the customized “Save As”
dialog box and also, add file extension, change file format if necessary. The following is a portion of this
function:
……
dlg.m_ofn.lpstrFilter=STRING_FILTER;
dlg.m_ofn.nFilterIndex=1;
if(dlg.DoModal() == IDOK)newName=dlg.GetPathName();
else return FALSE;
if(dlg.m_ofn.nFilterIndex == 1)m_bRTF=TRUE;
else m_bRTF=FALSE;
if(bReplace)
{
CString szFind;
if(dlg.m_ofn.nFilterIndex == 1)
{
szFind=newName;
szFind.MakeLower();
iBad=szFind.Find(".txt");
if(iBad != -1)newName.ReleaseBuffer(iBad);
newName+=".rtf";
}
else
{
szFind=newName;
szFind.MakeLower();
413
Chapter 14. Views
iBad=szFind.Find(".rtf");
if(iBad != -1)newName.ReleaseBuffer(iBad);
newName+=".txt";
}
}
else
{
if(dlg.m_ofn.nFilterIndex == 1)
{
newName+=".rtf";
}
else newName+=".txt";
}
……
Formatting Text
Another feature we want to let this editor have is text formatting. For example, we may let the user
format the selected text using bolded, italic or underlined style. Or we may let the user change the
alignment of the selected paragraph (make the whole paragraph aligned left, centered or aligned right). The
two types of formatting are called Character Formatting and Paragraph Formatting respectively, they can
be implemented through calling functions CRichEditView::SetParaFormat(…) and CRichEditView::
SetCharFormat(…). The following shows the formats of the two functions:
Because there are many properties we can set, we need to use structures PARAFORMAT and CHARFORMAT
to specify which properties will be customized. The following is the format of structure PARAFORMAT:
We need to set the corresponding bits of member dwMask in order use other members of this structure.
For example, if we want to set paragraph alignment, we need to assign member wAllignment an
appropriate value, and set PFM_ALIGNMENT bit of member dwMask. If this bit is not set, member wAlignment
will have no effect when function CRichEditView::SetParaFormat(…) is called.
There are a lot of features we can set through using this function, which include text numbering (using
bullets at the beginning of each line), paragraph start indent, right indent, second line offset, paragraph
alignment and tabs.
The usage of function CRichEditView::SetCharFormat(…) is similar. Here we have another structure
CHARFORMAT that could be used to set appropriate properties for the selected text:
414
Chapter 14. Views
} CHARFORMAT;
Again, member dwMask should be used to specify which properties will be customized. We can make
change to character effects (make it bolded, italic, strikeout, underlined, or change its color), modify the
size of characters, customize character’s offset from the base line (this is useful for implementing
superscript or subsript), or select a different type of font.
Counterpart functions of CRichEditView::SetParaFormat(…) and CRichEditView::
SetCharFormat(…) are CRichEditView::GetParaFormat(…) and CRichEditView:: GetCharFormat(…)
respectively. They allow us to retrieve the properties of the current paragraph or the selected text (If no text
is selected, the properties indicate the text at the current caret position). Similarly, we need to specify
corresponding bits of member dwMask in order to retrieve certain properties: those members who have
corresponding zero bits in member dwMask will not be stuffed with the paragraph or character information.
It seems that by using the above four functions, we can build a very useful editor that supports rich edit
text format. However, in class CRichEditView, there exist more powerful functions that can be used to
format the selected text or paragraph. These functions are also undocumented, but using them can save us
much effort:
CRichEditView::OnParaCenter();
CRichEditView::OnParaRight();
CRichEditView::OnParaLeft();
CRichEditView::OnCharBold();
CRichEditView::OnCharUnderline();
CRichEditView::OnCharItalic();
CRichEditView::OnUpdateCharBold();
CRichEditView::OnUpdateCharUnderline();
CRichEditView::OnUpdateCharItalic();
CRichEditView::OnUpdateParaCenter();
CRichEditView::OnUpdateParaLeft();
CRichEditView::OnUpdateParaRight();
Instead of implementing our own message handlers, we can just add commands to the mainframe
menu or tool bar, then map the commands to these functions. In the sample, we add six buttons for
character and paragraphing formatting (Figure 14-1), and map the command messages to the above
functions as follows:
ON_COMMAND(ID_BUTTON_FORMATCENTER, CRichEditView::OnParaCenter)
ON_COMMAND(ID_BUTTON_FORMATRIGHT, CRichEditView::OnParaRight)
ON_COMMAND(ID_BUTTON_FORMATBOLD, CRichEditView::OnCharBold)
ON_COMMAND(ID_BUTTON_FORMATUNDER, CRichEditView::OnCharUnderline)
ON_COMMAND(ID_BUTTON_FORMATITALIC, CRichEditView::OnCharItalic)
ON_COMMAND(ID_BUTTON_FORMATLEFT, CRichEditView::OnParaLeft)
ON_UPDATE_COMMAND_UI(ID_BUTTON_FORMATBOLD, CRichEditView::OnUpdateCharBold)
ON_UPDATE_COMMAND_UI(ID_BUTTON_FORMATCENTER, CRichEditView::OnUpdateParaCenter)
ON_UPDATE_COMMAND_UI(ID_BUTTON_FORMATITALIC, CRichEditView::OnUpdateCharItalic)
ON_UPDATE_COMMAND_UI(ID_BUTTON_FORMATLEFT, CRichEditView::OnUpdateParaLeft)
ON_UPDATE_COMMAND_UI(ID_BUTTON_FORMATRIGHT, CRichEditView::OnUpdateParaRight)
ON_UPDATE_COMMAND_UI(ID_BUTTON_FORMATUNDER, CRichEditView::OnUpdateCharUnderline)
With the above implementation, the editor can let the user set the character and paragraph properties.
415
Chapter 14. Views
2-pane splitter window. We will use CTreeView to create the left pane, and use CListView to create the
right pane. One the left pane, the file system (directories) will be displayed in a tree form, the user can click
on any node to select a directory, or double click on it to expand the node (show all the sub-directories). On
the right pane, all files and sub-directories contained in the currently selected directory will be listed, they
can be displayed in one of the four styles supported by list view.
1. ID_BUTTON_FORMATCENTER
2. ID_BUTTON_FORMATRIGHT
1 3. ID_BUTTON_FORMATBOLD
6 4.
2 ID_BUTTON_FORMATUNDER
5. ID_BUTTON_FORMATITALIC
3 5 6. ID_BUTTON_FORMATLEFT
4
Figure 14-1. New commands
We have introduced how to create splitter window in Chapter 3. Obviously here we need to create
static splitter windows. Application Wizard does have a choice to let us create splitter window, however, it
can only help us with creating dynamic splitter window. We can modify the dynamic splitter window to
static splitter window after the application is generated. To let the Application Wizard generate code for
implementing dynamic splitter window, we can click “Advanced…” button (in step 4) and check “Use split
widow” check box in the popped up dialog box.
In order to create a splitter window with two panes implemented by different types of views, first we
must implement two view classes. Here, one of the views can be implemented as we go through the
Application Wizard’s project creation steps: in the final step, we can select CListView as the view’s base
class. The other class can be added after the project is generated by Class Wizard.
Sample 14.3\Explorer is created this way. It is a standard SDI application, with first view generated by
Application Wizard whose name is CExplorerView. The second view is added by Class Wizard, whose
base class is CTreeView and the class name is CDirView. In function CMainFrame::OnCreateClient(…), the
splitter window is created using the above two classes:
BOOL CMainFrame::OnCreateClient
(
LPCREATESTRUCT lpcs,
CCreateContext *pContext
)
{
if
(
!m_wndSplitter.CreateStatic
(
this, 1, 2, WS_CHILD | WS_VISIBLE, AFX_IDW_PANE_FIRST
)
)
{
TRACE0("Failed to CreateStaticSplitter\n");
return FALSE;
}
if
(
!m_wndSplitter.CreateView
(
0, 0, RUNTIME_CLASS(CDirFormView), CSize(100, 100), pContext
)
)
{
TRACE0("Failed to create first pane\n");
return FALSE;
}
if
(
!m_wndSplitter.CreateView
(
0, 1, RUNTIME_CLASS(CExplorerView), CSize(100, 100), pContext
)
)
416
Chapter 14. Views
{
TRACE0("Failed to create first pane\n");
return FALSE;
}
return TRUE;
}
ID Usage
IDB_BITMAP_DESKTOP Desktop node
IDB_BITMAP_COMPUTER Computer node
IDB_BITMAP_HARD Node for displaying a drive
IDB_BITMAP_OPENFOLDER Node for displaying an opened (expanded) file folder
IDB_BITMAP_CLOSEFOLDER Node for displaying a closed (collapsed) file folder
Like all other types of views, the best place to initialize the tree is in function CDirView::
OnInitialUpdate(). In order to do so, we need to create the image list, select the image list into the tree
control, and create the tree. Image list creation can be implemented by calling functions CImageList::
Create(…) and CImageList::Add(…). We can use the first function to create the image list, specify the
image size and number of images that will be included in the list. Then we can call the second function to
add each single image. In the sample, this procedure is implemented as follows:
……
pilCtrl=new CImageList();
pilCtrl->Create(BMP_SIZE_X, BMP_SIZE_Y, ILC_MASK, 6, 0);
bmp.LoadBitmap(IDB_BITMAP_DESKTOP);
pilCtrl->Add(&bmp, RGB(255, 255, 255));
bmp.DeleteObject();
bmp.LoadBitmap(IDB_BITMAP_COMPUTER);
pilCtrl->Add(&bmp, RGB(255, 255, 255));
bmp.DeleteObject();
……
417
Chapter 14. Views
The image list can also be created from two existing image lists by calling the following version of this
function:
BOOL CImageList::Create
(
CImageList& imagelist1, int nImage1, CImageList& imagelist2, int nImage2,
int dx, int dy
);
We set the background color to white so that all image’s portion with white color will be treated as
transparent region.
……
lStyleOld=::GetWindowLong(GetTreeCtrl().GetSafeHwnd(), GWL_STYLE);
lStyleOld|=TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS;
::SetWindowLong(GetTreeCtrl().GetSafeHwnd(), GWL_STYLE, lStyleOld);
……
Style TVS_HASLINES will let the nodes be connected by dotted lines, TVS_LINESATROOT will add a line
at the root node, and TVS_HASBUTTONS will add a rectangle button (displays either “+” or “-”) for each
expandable node. If we do not specify these styles, the tree control will look slightly different.
These styles can also be customized in function CView::PreCreateWindw(…). In order to do so, we
need to set the corresponding style flags for member dwExStyle of structure CREATESTRUCT. The difference
between two methods is that using ::SetWindowLong(…) can let us change the styles of a window
dynamically.
418
Chapter 14. Views
tvInsertStruct.hParent=NULL;
tvInsertStruct.hInsertAfter=TVI_LAST;
tvInsertStruct.item.iImage=0;
tvInsertStruct.item.iSelectedImage=0;
tvInsertStruct.item.pszText="Desktop";
tvInsertStruct.item.mask=TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_TEXT | TVIF_STATE;
tvInsertStruct.item.stateMask|=TVIS_EXPANDED;
tvInsertStruct.item.state|=TVIS_EXPANDED;
hTreeItem=GetTreeCtrl().InsertItem(&tvInsertStruct);
ASSERT(hTreeItem);
……
Parameter drive specifies target drive. It can be any number from 1 to 26, which represents drive A:,
B:, C:… and so on. The function will return 0 if the working drive is changed successfully, otherwise it
returns -1.
So we can call this function repeatedly by passing 1, 2, 3…26 to it and examining the returned value. If
the function returns 0, this means the drive is available, and we need to add it to the tree control. If the
function returns -1, we can just go on to check the next drive.
Because we do not want to change the default working drive, we need to save the current working
drive before calling function _chdrive(…), and resume it after the checking is over. The current working
drive can be retrieved by calling runtime function _getdrive(). The following portion of function
CDirView:: OnInitialUpdate() shows how the drives are added to the tree view window:
……
curdrive=_getdrive();
for(drive=1; drive <= 26; drive++)
{
if(!_chdrive(drive))
{
memset(buffer, 0, dwSize);
buffer[0]=drive+'A'-1;
buffer[1]=':';
tvInsertStruct.hParent=hTreeItem;
tvInsertStruct.hInsertAfter=TVI_LAST;
tvInsertStruct.item.mask=TVIF_IMAGE | TVIF_SELECTEDIMAGE |
TVIF_TEXT | TVIF_STATE;
tvInsertStruct.item.iImage=2;
tvInsertStruct.item.iSelectedImage=2;
tvInsertStruct.item.pszText=buffer;
tvInsertStruct.item.stateMask|=TVIS_EXPANDED;
tvInsertStruct.item.state|=TVIS_EXPANDED;
GetTreeCtrl().InsertItem(&tvInsertStruct);
}
}
_chdrive( curdrive );
……
419
Chapter 14. Views
When program exits, we must do some cleanup job, which includes removing all the nodes and
destroying the image list. In the sample, this is implemented in WM_DESTROY message handler:
void CDirView::OnDestroy()
{
CImageList *pilCtrl;
GetTreeCtrl().DeleteAllItems();
pilCtrl=GetTreeCtrl().GetImageList(TVSIL_NORMAL);
pilCtrl->DeleteImageList();
delete pilCtrl;
CTreeView::OnDestroy();
}
CFileFind ff;
BOOL bWorking;
bWorking=ff.FindFile();
while(bWorking)
{
bWorking=ff.FindNextFile();
AfxMessageBox(ff.GetFileName());
}
Note we can also use wildcard characters when calling function CFileFind::FindFile(…) to match
file name with specific patterns. If we do not pass any parameter to it, it will be equal to passing “*.*” to
the function. In this case all the files and directories will be enumerated. If function CFileFind::
FindNextFile() returns a non-zero value, we can call several other member functions of class CFileFind
to obtain the properties of the enumerated file such as file name, file path, file attributes, created time and
updated time.
420
Chapter 14. Views
Function CDirView::AddDirs(…) is implemented in the sample, it will be used to add directory items
to a specified node. It has two parameters, the first is the handle of the target tree item, and the second is a
Boolean type variable indicating if we should further add sub-directories for each added directory node.
The following is the format of this function:
Before calling this function, we need to change the current working directory to the directory we want
to examine. So in function CDirView::OnInitialUpdate(…), after one drive node is added to the tree view
window, we change the current working directory to root directory of that drive, and call function
CDirView::AddDirs(…) to add nodes for the directories. The following is the modified portion of function
CDirView::OnInitialUpdate(…):
……
for(drive=1; drive <= 26; drive++)
{
if(!_chdrive(drive))
{
……
hSubItem=GetTreeCtrl().InsertItem(&tvInsertStruct);
ASSERT(hSubItem);
For the root directory, we need to find out not only the directories under it, but also the sub-directories
of each first-level directory. So we pass a TRUE value to the second parameter of function CDirView::
AddDir(…). The function will recursively enumerate sub-directories for all the directories found within the
function if the parameter is TRUE.
At the beginning of function CDirView::AddDirs(…), we initialize a TV_INSERTSTRUCT type object and
call function CFileFind::FindFile(). If it returns TRUE, we can further call function CFileFind::
FindNextFile() and get all the attributes of the enumerated file (directory). Then we repeat file (directory)
enumerating until function CFileFind::FindNextFile() returns a FALSE value:
while(bWorking)
{
bWorking=ff.FindNextFile();
……
We can examine if the enumerated object is a directory or a file by calling function CFileFind::
IsDirectory(). This is necessary because only the directories will be added to the tree view window. A
directory node is added by first stuffing TV_INSERTSTRUCT type object then calling function CTreeCtrl::
InsertItem(…):
……
if(ff.IsDirectory())
{
szFilePath=ff.GetFileName();
tvInsertStruct.hParent=hTreeItem;
tvInsertStruct.hInsertAfter=TVI_LAST;
tvInsertStruct.item.mask=TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_TEXT;
tvInsertStruct.item.iImage=4;
tvInsertStruct.item.iSelectedImage=4;
421
Chapter 14. Views
hSubItem=GetTreeCtrl().InsertItem(&tvInsertStruct);
ASSERT(hSubItem);
……
If parameter bFindChild is TRUE, we need to enumerate sub-directories for each added directory
node. However, since “.” and “..” are also two types of directories (indicating the current and parent
directories respectively), if we apply this operation on them, it will cause infinite loop. To examine if a
directory is one the above two types of directories, we can call function CFileFind::IsDots(). If the
function returns FALSE, we can call function CDirView::AddDirs(…) again to add sub-directory nodes.
Before calling this function, we also need to change the current working directory. After the function is
called, we need to resume the original working directory:
……
if(!ff.IsDots())
{
if(bFindChild == TRUE)
{
szFilePath=ff.GetFilePath();
_getcwd(path, _MAX_PATH);
_chdir((LPCSTR)szFilePath);
AddDirs(hSubItem, FALSE);
_chdir(path);
}
}
}
}
ff.Close();
}
If we execute the sample, we need to wait for a while before the procedure of building directory tree is
completed. This waiting time is especially long for a system containing many drives and directories. This is
why we only add fist and second level directories to the tree view window at the beginning. If we build the
whole directory map before bringing up the window, the user will experience a very long waiting time. We
will add new nodes to the tree only when a node is expanded and its sub-level contents need to be revealed.
Image Lists
Before adding any file to the list view, we need to prepare image lists. This procedure is almost the
same with that of tree view. The only difference between the two is that for list view we have more choices.
This is because the items contained in a list view can be displayed in different styles, and for each style we
can use a different type of images.
A list view can display items in one of the four styles: big icon (default style), small icon, list, report.
We can prepare two image lists, one for big icon style, one for other three styles.
We can display different file types using different icons, this is how the files are displayed in real
“Explorer” application. Under Windows, each type of files can register both big and small icons in the
system, and “Explorer” will use the registered icons for file displaying. To get the registered icons, we need
to call some special functions. We will implement this method in later sections. Here, we will prepare our
own icons for displaying files. In the sample, two sets of image resources are included in the applications,
422
Chapter 14. Views
one of them will be used for displaying directories and the other for displaying files. Their IDs are
IDB_BITMAP_CLOSEFOLDERBIG, IDB_BITMAP_CLOSEFOLDER, IDB_BITMAP_FILEBIG and IDB_BITMAP_FILE.
In the sample, big icon image list is created from IDB_BITMAP_CLOSEFOLDERBIG and
IDB_BITMAP_FILEBIG. Small icon image list is created from IDB_BITMAP_CLOSEFOLDER and
IDB_BITMAP_FILE. The creation of image list is the same with what we did for the tree view. When an
image list is selected into the list control, we must specify the type of image list. The following portion of
function CExplorerView::ChangeDir() shows how the image lists are selected into the list control in the
sample:
……
GetListCtrl().SetImageList(pilSmall, LVSIL_SMALL);
GetListCtrl().SetImageList(pilNormal, LVSIL_NORMAL);
……
Here pointer pilSmall and pilNormal point to two different image lists. We use LVSIL_SMALL and
LVSIL_NORMAL to specify the type of the image list.
Adding Columns
First we need to add columns to the list control. The columns will appear in the list control window
when the items contained in it are displayed in “Report” style. For each item, usually the small icon
associated with the item and item label will be displayed at the left most column (column 0). For other
columns, we can display text to list other properties of the item.
The columns are added through stuffing LV_COLUMN type object and calling function CListCtrl::
InsertColumn(…). Like other structures such as TV_INSERTSTRUCT, LV_COLUMN also has a member mask that
lets us specify which of the other members of this structure will be used. For example, we can specify text
alignment format (is the text aligned left, right or is it centered?) by setting LVCF_FMT bit of member mask
and assigning appropriate value to member fmt; we can specify the width of each column by setting
LVCF_WIDTH bit and using cx member; we can set the column caption by setting LVCF_TEXT bit and using
pszText member. In the sample, text of each column is aligned left, the width of each column is set to 150,
and the column texts are: “Name”, “Size”, “Type”, and “Modified” respectively.
To make it convenient to add columns, the following global variables are declared in the sample:
#define NUM_COLUMNS 4
423
Chapter 14. Views
Listing Files
In the list view, each item represents a file under certain directory. When the items are displayed in
“big icon”, “small icon” and “list” styles, each file is represented by an icon contained in the list view
window. When they are displayed in the “report” style, the file is represented by both an icon and several
text strings. In this case, column 0 contains icon and the file name, and the rest columns contain other
information about the file (These items are called the sub-items).
The procedure of adding items to list control is similar to adding directory nodes to tree control, except
that we don’t need to worry about enumerating sub-directories here. Also, for each item, we need to set not
only the image number and item text (contained in column 0), but also the sub-item text (contained in the
rest of the columns). For this purpose, we can store the text of sub-items in a string array. After all the
items are added, we can set sub-item text for each item.
The file enumerating can be implemented by calling functions CFileFind::FindFile() and
CFileFind::FindNextFile() repeatedly. After a file is found, we stuff an LV_ITEM type object and call
CListCtrl::InsertItem(…) to add a new item to the list control. Here is how it is implemented in function
CExplorerView::ChangeDir():
……
bWorking=ff.FindFile();
i=0;
while(bWorking)
{
bWorking=ff.FindNextFile();
if(!ff.IsDots())
{
szFileName=ff.GetFileName();
lvi.mask=LVIF_TEXT | LVIF_IMAGE;
lvi.iItem=i;
lvi.iSubItem=0;
lvi.pszText=(LPSTR)(const char *)szFileName;
lvi.iImage=ff.IsDirectory() ? 0:1;
GetListCtrl().InsertItem(&lvi);
……
Unlike tree control, there is no handle here to identify a special item. All the items are identified by
their indices, this means if we display items in “list” or “report” style, the item located at the first row is
item 0, the next row is item 1… and so on. When inserting an item, we need to specify the item index by
using member iItem of LV_ITEM structure.
We store file size (for directory, display nothing), file type (“File” or “Folder”), the updated time in a
string array that will be used to add text for the sub-items. These attributes of file can be retrieved by
calling functions CFileFind::GetLength(), CFileFind::IsDirectory() and CFileFind::
GetLastWriteTime(…). When calling the third function to obtain the update time of a file, we get a CTime
type variable. To store the time in a CString type variable in ASCII format, we need to call function
CTime::Format(…). The following portion of function CExplorerView::ChangeDir() shows how the string
array is created:
……
if(!ff.IsDirectory())
{
szText.Empty();
szText.Format("%lu Bytes", ff.GetLength());
szArray.Add(szText);
szText.Empty();
szText="File";
}
else
{
szArray.Add("");
szText="Folder";
}
szArray.Add(szText);
ff.GetLastWriteTime(time);
szText.Empty();
szText=time.Format("%d/%m/%Y %I:%M%p");
szArray.Add(szText);
424
Chapter 14. Views
}
i++;
}
……
The text of sub-items is added by calling function CListCtrl::SetItemText(…). This can also be
implemented by stuffing LV_ITEM type object (specifying item and sub-item indices) and calling function
CListCtrl::SetItem(…). The following portion of function CExplorerView::ChangeDir() shows how
this is implemented in the sample:
……
for(i=0; i<GetListCtrl().GetItemCount(); i++)
{
for(j=1; j<NUM_COLUMNS; j++)
{
GetListCtrl().SetItemText(i, j, szArray[i*(NUM_COLUMNS-1)+j-1]);
}
}
szArray.RemoveAll();
ff.Close();
}
void CExplorerView::DestroyList()
{
CImageList *pilCtrl;
GetListCtrl().DeleteAllItems();
pilCtrl=GetListCtrl().GetImageList(LVSIL_NORMAL);
if(pilCtrl != NULL)
{
pilCtrl->DeleteImageList();
delete pilCtrl;
}
pilCtrl=GetListCtrl().GetImageList(LVSIL_SMALL);
if(pilCtrl != NULL)
{
pilCtrl->DeleteImageList();
delete pilCtrl;
}
}
Since we can retrieve the pointers of image list from the list control, there is no need for us to store
them as variables. This function is called in function CExplorerView::ChangeDir() and WM_DESTROY
message handler.
void CExplorerView::OnInitialUpdate()
{
int drive;
CListView::OnInitialUpdate();
425
Chapter 14. Views
}
}
ChangeDir();
}
Here parameter pszPath is a pointer to a string specifying the file path; dwFileAttributes specifies
the file attributes, and the file information can be retrieved into a SHFILEINFO type object which is pointed
by pointer psfi; cbFileInfo specifies the size of SHFILEINFO structure; uFlags specifies what information
is being retrieved. In our case, we can combine SHGFI_ICON with one of the following flags and pass the
result to parameter uFlags:
Flag Meaning
SHGFI_LARGEICON Retrieve large icon contained in the file
SHGFI_SMALLICON Retrieve small icon contained in the file
SHGFI_SHELLICONSIZE Retrieve shell large icon
SHGFI_SHELLICONSIZE | SHGFI_SMALLICON Retrieve shell small icon
To display each file with embedded or registered icons, before adding an item to the list control, we
need to first customize the image list. If any icon is found by calling function ::SHGetFileInfo(), we will
add it to the image list. If we could not find an icon using this method, the default icon will be associated
with the corresponding file.
Sample
Sample 14.7\Explorer is based on sample 14.6\Explorer. In this sample, the embedded and registered
icons are retrieved for displaying files in the list view.
In the sample, a new member function is added for retrieving icons for a file:
The returned value is an icon handle. Within this function, ::SHGetFileInfo(…) is called to get the
icon information of a file. The following is the implementation of this function:
426
Chapter 14. Views
return shfi.hIcon;
}
……
if(!ff.IsDirectory())
{
szFileName=ff.GetFilePath();
hIcon=GetIconFromFile(szFileName, SHGFI_LARGEICON);
if(hIcon != NULL)
{
pilNormal->Add(hIcon);
bUseDefaultNormalIcon=FALSE;
nNumImages++;
}
else
{
hIcon=GetIconFromFile(szFileName, SHGFI_SHELLICONSIZE);
if(hIcon != NULL)
{
pilNormal->Add(hIcon);
bUseDefaultNormalIcon=FALSE;
nNumImages++;
}
}
……
In rare cases, some files may have small embedded or registered icon but no corresponding big icon, or
vice versa. In any case, the embedded or registered icon has the highre priority to be used. The newly
obtained icon is added to the image list by calling function CImageList::Add(…).
427
Chapter 14. Views
implemented to obtain the full path represented by any item. Within this function, we keep on retrieving the
item’s parent node until root is reached, and combining the obtained directory names to form a full path.
::GetCursorPos(&pt);
ScreenToClient(&pt);
hItem=GetTreeCtrl().HitTest(pt, &nFlags);
if(hItem != NULL)
{
szPath=GetTreeCtrl().GetItemText(hItem);
if(szPath.Find('.') == -1)
{
szPath=GetDir(hItem);
if(szPath.GetLength())
{
if(_getcwd(path, _MAX_PATH) != NULL)
{
if(stricmp((LPCSTR)szPath, path) != 0)
{
if(_chdir((LPCSTR)szPath) == 0)
{
((CExplorerDoc *)GetDocument())->ChangePath();
}
}
}
}
}
}
*pResult=0;
}
428
Chapter 14. Views
CTreeCtrl::GetNextSiblingItem(…) repeatedly until it returns NULL value. Also, we can call function
CTreeCtrl::ItemHasChildren(…) to examine if a node already has child nodes. The following is the
implementation of function CDirView::AddChildrenChildren(…):
_getcwd(path, _MAX_PATH);
if(hItem != NULL)
{
szPath=GetDir(hItem);
hItemChild=GetTreeCtrl().GetChildItem(hItem);
while(hItemChild != NULL)
{
if(!GetTreeCtrl().ItemHasChildren(hItemChild))
{
szText=GetTreeCtrl().GetItemText(hItemChild);
if(szText.Find('.') == -1)
{
szDir=szPath+'\\'+szText;
_chdir((LPCSTR)szDir);
AddDirs(hItemChild, FALSE);
}
}
hItemChild=GetTreeCtrl().GetNextSiblingItem(hItemChild);
}
}
_chdir(path);
}
CPoint pt;
HTREEITEM hItem;
UINT nFlags;
CString szFile;
if(pNMTreeView->action == TVE_EXPAND)
{
::GetCursorPos(&pt);
ScreenToClient(&pt);
hItem=GetTreeCtrl().HitTest(pt, &nFlags);
if(hItem != NULL)
{
AddChildrenChildren(hItem);
}
}
……
To make the application more user friendly, the rest part of this function swaps the directory node
image from the image representing open directory (IDB_BITMAP_OPENFOLDER) to the one representing
closed directory (IDB_BITMAP_CLOSEFOLDER) when the node is collapsing and vice versa when it is
expanding.
429
Chapter 14. Views
all the files should be sorted by their extensions; if we click on “Updated” column, all the files should be
sorted by their updated dates and times.
The function’s first parameter is a little special, which is the pointer to a callback function provided by
the programmer. The callback function will be used to perform actual comparison. This is because when
comparing two items, class CListCtrl has no way of knowing which item should precede the other. In
order to provide our own rules of making comparison, we need to implement the callback function.
The callback function has the following format:
In order to compare two items, we need to provide each item with a parameter, which is an LPARAM
type value. When two items are compared, their parameters will be passed to the callback function, which
will return different values indicating which item should precede the other. If the first item (whose
parameter is lParam1) should precede the second item (whose parameter is lparam2), the function needs to
return -1; if the first item should follow the second item, the function needs to return 1; if the two items are
equal, the function needs to return 0.
When calling function CListView::SortItems(…), we can pass different pre-defined values to
parameter dwData, which will be further passed to parameter lParamSort of the callback function. This
provides us with a way of specifying different types of sorting methods.
……
lvi.mask=LVIF_TEXT | LVIF_IMAGE | LVIF_PARAM;
lvi.iItem=i;
lvi.iSubItem=0;
lvi.pszText=(LPSTR)(const char *)szFileName;
lvi.lParam=(LPARAM)i;
……
Actually, in the callback function, one of the above functions is called to perform the comparison
according to parameter lParamSort:
430
Chapter 14. Views
switch((int)lParamSort)
{
case 0:
{
return CompareByName(lParam1, lParam2, lc);
break;
}
case 1:
{
return CompareBySize(lParam1, lParam2, lc);
break;
}
case 2:
{
return CompareByType(lParam1, lParam2, lc);
break;
}
case 3:
{
return CompareByDate(lParam1, lParam2, lc);
break;
}
}
return 0;
}
Please note that the callback function must be either a global function or a static member function. So
within it we cannot call CListView::GetListCtrl() directly to obtain the list control. Instead, we must
first obtain the current instance of list view, then use it to call function CListView::GetListCtrl() and
obtain the list control. This is why at the beginning of the callback function the current active document is
first obtained, from which the current active list view (and the list control) is obtained.
memset(&lvfi, 0, sizeof(LV_FINDINFO));
lvfi.flags|=LVFI_PARAM;
lvfi.lParam=lParam;
return lc.FindItem(&lvfi);
}
Here a LV_FINDINFO type object is stuffed, with LVFI_PARAM bit of member flags set to “1” and the
item parameter assigned to member lParam. Then the object is passed to function CListCtrl::
431
Chapter 14. Views
FindItem(…) to search the item in the list control. Function CExplorerView::FindItem(…)’s second
parameter is a CListCtrl type reference, this is because within static member function, we must use the
instance of an object to call any of its non-static functions. This function returns the current index of the
corresponding item.
memset(&lvi1, 0, sizeof(LV_ITEM));
memset(&lvi2, 0, sizeof(LV_ITEM));
lvi1.mask|=LVIF_IMAGE;
lvi1.iItem=nItem1;
lvi2.mask|=LVIF_IMAGE;
lvi2.iItem=nItem2;
lc.GetItem(&lvi1);
lc.GetItem(&lvi2);
szName1=lc.GetItemText(nItem1, 0);
szName2=lc.GetItemText(nItem2, 0);
if(lvi1.iImage == 0 && lvi2.iImage != 0)return -1;
if(lvi1.iImage != 0 && lvi2.iImage == 0)return 1;
……
In case both items are directories or files, we need to further compare their names. Since file names
under Windows are case insensitive, we neglect character case when performing the comparison. The
comparison is done within a for loop, which starts from the first characters and ends under one of the
following situations: 1) The two compared characters are different, in which case the character that has the
greater value belongs to the item that should follow the other. 2) One of the strings reaches its end. In this
case the item with longer file name should follow the other. If two strings are exactly the same, the function
returns 0:
……
nSize=min(szName1.GetLength(), szName2.GetLength());
szName1.MakeLower();
szName2.MakeLower();
for(i=0; i<nSize; i++)
{
if(szName1[i] < szName2[i])return -1;
if(szName1[i] > szName2[i])return 1;
}
if(i == nSize)
{
if(szName1.GetLength() < szName2.GetLength())return -1;
if(szName1.GetLength() > szName2.GetLength())return 1;
}
return 0;
}
Notification LVN_COLUMNCLICK
When the user clicks on one of the columns, the list control sends a notification message
LVN_COLUMNCLICK to its parent window. If we want to handle this message within the list view, we need to
use macro ON_NOTIFY_REFLECT to map the message to one of its member functions. In the sample, this
message mapping is added through using Class Wizard:
432
Chapter 14. Views
BEGIN_MESSAGE_MAP(CExplorerView, CListView)
//{{AFX_MSG_MAP(CExplorerView)
……
ON_NOTIFY_REFLECT(LVN_COLUMNCLICK, OnColumnClick)
//}}AFX_MSG_MAP
……
END_MESSAGE_MAP()
GetListCtrl().SortItems(CompareFunc, pNMListView->iSubItem);
*pResult=0;
}
Both tree control and list control can be implemented in a form view. Sample 14.10\Explorer is based
on sample 14.9\Explorer whose left pane of the splitter window is implemented by a form view. Within the
form view, a tree control is implemented for displaying directories. We will see, it is almost the same to use
tree control in a form view with using a tree view directly.
433
Chapter 14. Views
char CDirFormView::m_szPath[_MAX_PATH];
CString CDirFormView GetDir(HTREEITEM);
void CDirFormView AddDirs(HTREEITEM, BOOL);
void CDirFormView::AddChildrenChildren(HTREEITEM);
void CDirFormView::OnInitialUpdate();
afx_msg void CDirFormView::OnClickTreeCtrl(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void CDirFormView::OnItemExpandingTreeCtrl(NMHDR* pNMHDR, LRESULT* pResult);
void CDirFormView::ResizeTreeView()
{
CRect rect;
GetClientRect(rect);
if(rect.Width() > 2*BORDER_WIDTH)
{
rect.left+=BORDER_WIDTH;
rect.right-=BORDER_WIDTH;
}
if(rect.Height() > 2*BORDER_WIDTH)
{
rect.top+=BORDER_WIDTH;
rect.bottom-=BORDER_WIDTH;
}
if(::IsWindow(m_tcDir.m_hWnd))m_tcDir.MoveWindow(rect);
}
BOOL CMainFrame::OnCreateClient
(
434
Chapter 14. Views
LPCREATESTRUCT lpcs,
CCreateContext *pContext
)
{
……
if
(
!m_wndSplitter.CreateView
(
0, 0, RUNTIME_CLASS(CDirFormView), CSize(100, 100), pContext
)
)
{
TRACE0("Failed to create first pane\n");
return FALSE;
}
……
This new version of Explorer behaves exactly the same with the previous one. However, with form
view, we can add other common controls such as buttons to the left pane. This will give us more flexibility
in improving our application.
Summary
1) A standard text editor can be implemented by class CEditView. This class contains some member
functions that can be used to implement a lot of useful commands:
2) Class CRichEditView and CRichEditDoc support formatted text editing. The classes support two file
formats: “rtf” format and plain text format. There is a member variable contained in class
CRichEditDoc: CRichEditDoc::m_bRTF. If we want to open “rtf” type files, we need to set this variable
to TRUE. If we want to edit plain ASCII text, we need to set this variable to FALSE.
1) To get or set the format of a paragraph, we can stuff structure PARAFORMAT and call function
CRichEditView::GetParaFormat(…) or CRichEditView::SetParaFormat(…); to get or set the format
of characters, we can stuff structure CHARFORMAT and call function
CRichEditView::SetCharFormat(…) or CRichEditView::SetCharFormat(…).
1) To customize “File Open” dialog box, we need to override function CWinApp::OnFileOpen(…). To
customize “Save As” dialog box, we need to override undocumented function CDocument::DoSave().
1) The following undocumented member functions can be used to implement commands for formatting
characters or paragraph in a rich edit view:
CRichEditView::OnParaCenter();
CRichEditView::OnParaRight();
CRichEditView::OnParaLeft();
CRichEditView::OnCharBold();
CRichEditView::OnCharUnderline();
CRichEditView::OnCharItalic();
6) The styles of tree view and list view can be set by calling function ::SetWindowLong(…). Any style
that can be used in function CTreeCtrl::Create(…) or CListCtrl::Create(…) can be changed
dynamically by using this function.
1) We can call function _chdrive(…) to test if a drive (from drive A to drive Z) exits in the system. If the
function returns -1, the drive does not exit. If it returns 0, the drive is available. We can also call this
function to change the current working drive.
435
Chapter 14. Views
1) Class CFileFind can be used to enumerate all the files and directories under certain directory. To
enumerate files and directories, we can call function CFileFind::FileFind() first then call function
CFileFind::FindNextFile() repeatedly until it returns FALSE.
1) Function CFileFind::IsDirectory() can be used to check if an enumerated object is a directory.
Function CFileFind::IsDot() can be used to check if a directory is “.” or “..”.
1) Function ::SHGetFileInfo(…) can be used to obtain the embedded or shell icons for a file.
1) Notification NM_CLICK can be used to trap mouse clicking events on the tree control. Notification
TVN_ITEMEXPANDING indicates that a node is about to expand.
1) When the user clicks mouse on the tree control, we can call function CTreeCtrl::HitTest(…) to find
out the handle of the item that was clicked.
1) We can call function CListCtrl::SortItems(…) to implement item sorting in the list control. In order
to do this, we must assign each item contained in the list control a parameter, which will be used as the
identification of the item. Then we need to prepare a callback function, within which rules of
comparison are implemented.
1) To search for a specific item by its parameter, we can stuff structure LV_FINDINFO and call function
CListCtrl::FIndItem(…).
1) To respond to the mouse clicking events on the columns of the list control, we need to trap notification
LVN_COLUMNCLICK.
436
Chapter 16. Context Sensitive Help
Chapter 15
DDE
D
ynamic Data Exchange (DDE) is one of the ways to exchange information and data among
different applications. From samples of previous chapters we already have some experience on
implementing registered message and file sharing, which allow one process to send simple
message or a block of data to another process. DDE is another way of implementing data
exchange, it can handle more complicated situation than the other two methods.
DDE is constructed under client-server model: a server application exposes its services to all the
applications in the system; any client application can ask the server for certain type of services, such as
getting data from server, sending data to the server, asking the server to execute a command. The best way
to implement DDE in an application is to use Dynamic Data Exchange Management Library (DDEML),
which is supported by the Windows. With this library, the implementation of DDE becomes easy. All the
samples in this chapter are based on this library.
MFC does not have a class that encapsulates DDE, so we have to call all the functions in the DDEML.
♦ DDE initialization:
UINT ::DdeInitialize
(
LPDWORD pidInst, PFNCALLBACK pfnCallback, DWORD afCmd, DWORD ulRes
);
An application must implement a DWORD type variable for storing its instance identifier, which is
somehow similar to application’s instance handle. When a server is making conversation with a client, they
both must use their instance identifiers to verify that the message is directed to them. This unique instance
ID is obtained through calling function ::DdeInitialize(…). When calling this function, we need to pass
the address of the DWORD type variable to its first parameter, and the variable will be filled with the instance
ID.
The second parameter is the pointer to a callback function, which will be discussed later.
The third parameter is the combination of different flags, which can be used to specify what kind of
DDE messages we want to receive. This is useful because there are a lot of services provided by the DDE
model. Sometimes we do not want certain types of messages to be sent to our applications. In this case, we
437
Chapter 16. Context Sensitive Help
can pass parameter afCmd a combination of filter flags, which will allow only certain type of messages to be
sent to the application. The following table shows some examples:
Parameter Meaning
APPCLASS_STANDARD Registers the application as a standard DDE application.
APPCMD_CLIENTONLY Registers the application as a DDE client, the server specific messages will not
be sent to this application.
APPCMD_FILTERINIT Prevent the application from receiving connection request messages before it has
S
registered the name service.
♦ DDE uninitialization:
The only parameter of this function is the value obtained from function ::DdeInitialize(…).
Again, idInst is the DDE instance ID which is obtained from function ::DdeInitialize(…). The
second parameter is the handle of the name service string, which is a new type of variable. Actually, it is
just a new type of handle that can be obtained by calling another function supported by DDEML.
In DDE, data is exchanged through sending string handles among different applications. For example,
if we have a string that is contained in a series of buffers, we cannot send the buffers’ starting address to
another process and let the string be accessed there. To let the string be shared by other DDE applications,
we need to create a string handle and let it be shared by other applications. With the handle, any application
can access the contents contained in the buffers. In DDEML, following two functions can be used to create
and free string handle:
For the first function, we can pass the string pointer to parameter psz, and the function will return a
string handle. When the string is no longer useful, we need to call function ::DdeFreeStringHandle(…) to
free the string handle.
When registering a service name, we must use a string handle rather than the service name itself.
A service name is the identifier of the server that lets the client applications find the server when
requesting a service. We can use any string as the service name so long as it is not identical to other DDE
service names present in the system. When a client requests for services from the server, it must obtain a
string handle for the service name and use it to communicate with the server.
438
Chapter 16. Context Sensitive Help
{
switch(wType)
{
case XTYP_REGISTER:
case XTYP_UNREGISTER:
case XTYP_ADVSTART:
case XTYP_ADVDATA:
case XTYP_ADVREQ:
case XTYP_ADVSTOP:
case XTYP_XACT_COMPLETE:
case XTYP_CONNECT:
case XTYP_DISCONNECT:
case XTYP_CONNECT_CONFIRM:
{
return (HDDEDATA)NULL;
}
default:
{
return (HDDEDATA)NULL;
}
}
}
The most important parameter here is uType, which indicates what kind of message has been sent to
this application. There are many types of DDE messages. In the callback function, we can return NULL if
we do not want to process the message. Standard DDE messages will be explained in the following
sections.
Server
Sample 15.1\DDE\Server is a standard SDI application generated by Application Wizard. The view of
this application is based on class CEditView, so that it can be used to display current server states. The
sample is a very basic DDE server, which implements DDE registration and name service registration but
actually provides no service. In the sample, the service name is “Server”. All the DDE functions are
implemented in the frame window. In the sample, function CMainFrame::InitializeDDE() is called to
initialize DDE. This function is called after the client view is created:
void CDDESrvView::OnInitialUpdate()
{
CEditView::OnInitialUpdate();
((CMainFrame *)(AfxGetApp()->m_pMainWnd))->InitializeDDE();
}
void CMainFrame::InitializeDDE()
{
UINT uDdeInit;
CString szRlt;
uDdeInit=::DdeInitialize
(
&m_dwInst,
(PFNCALLBACK)DdeCallback,
APPCMD_FILTERINITS,
0L
);
switch(uDdeInit)
{
case DMLERR_NO_ERROR:
{
Hszize();
if(::DdeNameService(m_dwInst, m_hszServiceName, NULL, DNS_REGISTER))
{
szRlt="DDE initialized successfully!";
}
else szRlt="Fail to initialize DDE: Fail to register name service!";
break;
}
……
439
Chapter 16. Context Sensitive Help
Printf((LPCSTR)szRlt);
Printf("\r\n");
}
void CMainFrame::OnClose()
{
::DdeNameService(m_dwInst, NULL, NULL, DNS_UNREGISTER);
UnHszize();
::DdeUninitialize(m_dwInst);
CFrameWnd::OnClose();
}
Also, the string handle is freed here. In the sample, functions CMainFrame::Hszize() and
CMainFrame:: UnHszize() are implemented for obtaining and freeing string handles respectively:
void CMainFrame::Hszize()
{
m_hszServiceName=::DdeCreateStringHandle(m_dwInst, m_szService, NULL);
Printf("Service name: %s\r\n", m_szService);
}
void CMainFrame::UnHszize()
{
::DdeFreeStringHandle(m_dwInst, m_hszServiceName);
}
return bPreCreated;
}
Because function CEditCtrl::ReplaceSel(…) is used to output text to the edit view in function
CMainFrame::Printf(…), we further need to prevent the cursor position from being changed by the user
(Function CEditCtrl::ReplaceSel(…) will always output text at the current caret position). For this
reason, the following messages are handled to bypass the default implementations in the sample:
WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOSUEMOVE and ON_WM_LBUTTONDBLCLK. The handlers of these
messages are empty. This will prevent the application from responding to the mouse events so that the
cursor position can not be changed under any condition.
If we execute the sample at this point, messages will appear on the client window indicating if the
DDE initialization is successful.
440
Chapter 16. Context Sensitive Help
We need to provide three parameters in order to make the connection: the client instance ID, the
server’s service name, and the topic name that is supported by the server.
While the service name is the identification that can be used by the client to locate the server in the
system, a topic name indicates the type of service which is provided by the server. A server can provide
more than one service, whose properties and features can be defined by the programmer. For example, we
can implement a DDE server managing images, and a client can request the server to send any image to it.
For this service, we can name the topic name “image” (or whatever).
Like the service name, a string handle must be obtained for the topic name before it is used. When
function ::DdeConnect(…) is called from the client side, this handle must be passed to its hszTopic
parameter.
The server does not need to register the topic name. When the client makes the connection, the server
will receive a XTYP_CONNECT message, and the topic name string handle will be passed as one of the
parameters to the DDE call back function. Upon receiving this message, the handle passed with the
message can be compared with the topic string handles stored on the server side (which represent all the
topics supported by the server). If there is a match, it means the server supports this topic, otherwise the
server should reject the connection.
Parameter Meaning
UFMT Not used
hconv Not used
hszTopic Handle of the topic name, must be obtained by calling ::DdeCreateStringHandle()
hszAppName Handle of service name, must be obtained by calling ::DdeCreateStringHandle()
hData Not used
lData1 Can be neglected
lData2 Can be neglected
In the sample of the previous section, a service name “Server” is registered on the server side. To let
the connection be set up between the server and the client, we also need to prepare a topic name that will be
441
Chapter 16. Context Sensitive Help
used by both sides. In the sample 15.2\DDE\Server, string “Topic” is used as the topic name, whose string
handle is obtained in function CMainFrame::Hszize() and freed in function CMainFrame::UnHszize().
The client should also obtain a string handle for the topic name and use it to make connection. The
server will receive the handles of service name and topic name together with message XTYP_CONNECT. Upon
receiving this message, the server needs to check if the service name and topic name requested by the client
are supported by itself.
To compare two strings by their handles, we can call function ::DdeCmpStringHandles(…), which will
compare two DDE strings. When calling this function, we need to pass the string handles to its two
parameters. The function will return -1, 0 or 1 indicating if the first string is less than, equal to, or greater
than the second string. The following is the format of this function:
Now it is clear what the server should do after receiving XTYP_CONNECT message: comparing
hszAppName with its registered service name, and comparing hszTopic with its supported topic names by
calling function ::DdeCmpStringHandles(…). If the comparisons are successful, the callback function
should return TRUE, this indicates the connection request is accepted. Otherwise it should return FALSE,
in which case the connection request is rejected. The following code fragment shows how this is
implemented in sample 15.2\DDE\Server:
Client Implementation
Sample 15.2\DDE\Client is implemented as a dialog based application using Application Wizard.
Similar to the server, here an edit control is included in the dialog template that will be used to display
DDE activities. The DDE initialization procedure is implemented in function
CDDEDialog::OnInitDialog(). There is also another edit box and a button labeled “Connect” in the dialog
box. When the user clicks this button, the application will retrieve the string from this edit box, use it as the
topic name and call function ::DdeConnect(…) to connect to the server:
void CDDECliDlg::OnButtonConnect()
{
if(m_hConvClient == 0)
{
442
Chapter 16. Context Sensitive Help
UpdateData();
if(m_szTopic.IsEmpty())
{
AfxMessageBox("Please input a topic name!");
return;
}
m_hszTopicName=ObtainHsz(m_szTopic);
m_hConvClient=::DdeConnect
(
m_dwInst, m_hszServiceName, m_hszTopicName, (PCONVCONTEXT)NULL
);
if(m_hConvClient == FALSE)
{
UnobtainHsz(m_hszTopicName);
Printf("Unable to connect server!\r\n");
}
else Printf("Connected to server!\r\n");
m_btnConnect.SetWindowText("Disconnect");
}
……
}
The value returned by function ::DdeConnect(…) is a handle used for conversation. Every time the
client want to make a transaction to the server, it must present this handle. By using the handle, the server
knows with whom it is talking with.
In case the connection is not successful, function ::DdeConnect(…) will return a FALSE value.
Confirm Connection
On the server side, if the connection is successful, it will further receive an XTYP_CONNECT_CONFIRM
message from the client. In this case, apart from hszTopic and hszAppName parameters, hconv is also used
to provide the conversation handle that should be used by the server to make further conversation with the
client. The following code segment shows how this message is processed on the server side:
……
case XTYP_CONNECT_CONFIRM:
{
m_hConvServer=hConv;
Printf("Handle saved for conversation!\r\n");
return (HDDEDATA)NULL;
}
……
Variable m_hConvServer is a static variable declared in class CMainFrame and is initialized to NULL in
the constructor.
DDE Disconnection
After the connection is set up, the client and server can proceed to initiate various types of transactions.
We will show this in later sections. After all the transactions are finished, or if one side wants to exit, the
conversation must be terminated. Either the server or the client can terminate the conversion. This can be
implemented by calling function ::DdeDisconnect(…) using the conversion handle. This function can be
called either from the server or from the client side. Once the message is sent, the other side will be notified
of the disconnection by receiving an XTYP_DISCONNECT message.
In the sample, once the connection is set up, it can be terminated from the client side by pressing
“Disconnect” button or from the server side through executing DDE | Disconnect command. This is
implemented as follows:
void CMainFrame::OnDdeDisconnect()
{
if(m_hConvServer != 0)
{
::DdeDisconnect(m_hConvServer);
m_hConvServer=0;
Printf("Disconnected by the server.\r\n");
}
443
Chapter 16. Context Sensitive Help
void CDDECliDlg::OnButtonConnect()
{
if(m_hConvClient == 0)
{
……
}
else
{
::DdeDisconnect(m_hConvClient);
m_hConvClient=0;
UnobtainHsz(m_hszTopicName);
m_hszTopicName=0;
m_btnConnect.SetWindowText("Connect");
Printf("Disconnected by the client!\r\n");
}
}
……
case XTYP_DISCONNECT:
{
if(m_hConvServer == hConv)
{
m_hConvServer=0;
Printf("Disconnected by the client.\r\n");
}
return (HDDEDATA)FALSE;
}
……
Test
We can test this version of DDE client and server by starting both applications, then inputting “Topic”
into the edit box contained in the client dialog box, and clicking “Connect” button (see Figure 15-1). After
this, we will see that the server will output some information indicating if the connection is successful.
Then, we can terminate the conversation either from the client or from the server side. We can also try to
use an incorrect topic name while making the connection, this will cause the connection to be unsuccessful.
To prevent the application from exiting without terminating the conversation, both client and server
need to check if the conversation is still undergoing while exiting. If necessary, function
::DdeDisconnect(…) will be called before the applications exit.
Figure 15-2 explains the procedure of DDE connection and disconnection.
444
Chapter 16. Context Sensitive Help
HDDEDATA DdeClientTransaction
(
LPBYTE pData,
DWORD cbData,
HCONV hConv,
HSZ hszItem,
UINT wFmt,
UINT wType,
DWORD dwTimeout,
LPDWORD pdwResult
);
We need to pass different types of data to parameters pData, cbData, wFmt and wType for different
types of transactions. If the transaction requires the client to pass data to the server (Besides data request
transaction, there exist other transactions that can be used by the client to send data to server), we need to
use parameters pData and cbData to pass the data, and use parameter wFmt to specify the data format. If the
client is requesting data from the server, it should specify the format of data by passing standard or user-
defined format to parameter wFmt. In case the client is not sending data to the server, we can neglect
parameters pData and cbData. Parameter wType can be used to specify the type of transaction the client is
requesting from the server. In the case of data request transaction, this parameter must be set to
XTYP_REQUEST. The meanings of the rest four parameters are the same for all types of transactions, which
are listed in the following table:
Parameter Meaning
hConv Conversation handle obtained from function ::DdeConnect()
445
Chapter 16. Context Sensitive Help
hszItem Handle of the item name which is obtained from function ::DdeCreateStringHandle(…)
dwTimeout Specifies how long the client will wait before the function returns, specific for synchronous
transmission
pdwResult A pointer that can be used to retrieve the result of the transaction
Server Client
DDE DDE
Initialization Initialization
Name service
registration
XTYP_CONNECT
Make
connection
Return TRUE,
Connection made
XTYP_CONNECT_ONFIRM
Transactions
XTYP_DISCONNECT
Terminate
conversation
Conversation over
For synchronous transmission mode (the asynchronous transmission mode will be introduced in a later
section), after the client calls this function, a timer will be set (the time out value is specified by parameter
dwTimeout). If there is no response from the server, the function does not return until the timer times out.
We can specify an appropriate value to prevent the program from getting into the deadlock.
446
Chapter 16. Context Sensitive Help
Parameter Meanings
uFmt Data format specified by the client
hconv The conversation handle, which is obtained at the conversation setup stage
hszTopic Handle of the topic name, must be obtained by calling function
::DdeCreateStringHandle()
hszAppName Handle of the item name, must be obtained by calling function
::DdeCreateStringHandle()
The server must check if it supports the topic and the specific item, as well as the data format. If it
supports all of them, the server must prepare data using the required format and send the data back to client.
Preparing Data
One way to send data to the client is to prepare data in server’s local buffers, then create a handle for
this data, and send the handle to the client. The data handle can be obtained by calling function
::DdeCreateDataHandle(…), which has the following format:
HDDEDATA DdeCreateDataHandle
(
DWORD idInst,
LPBYTE pSrc,
DWORD cb,
DWORD cbOff,
HSZ hszItem,
UINT wFmt,
UINT afCmd
);
The data can also be sent through a pointer. We will discuss this in a later section.
The following table explains the meanings of the above parameters:
Parameter Meaning
idInst Instance identification obtained from DDE initialization
pSrc Pointer to the buffers which hold the data
cb The length of the data in bytes
cbOff Specify the offset from the beginning of the buffer
hszItem String handle, indicating the item name
wFmt Indicates the data format
afCmd The creation flags which indicates who is responsible for freeing the data
We can use standard clipboard format such CF_TEXT, CF_DIB to pass data. If we define a special data
format, we must register it before passing the data.
Receiving Data
Because data is not sent directly, the client must obtain the required data from the handle first. For
synchronous transmission mode, the data handle will be returned directly from function
::DdeClientTransacton(). In case the transaction is not successful, a NULL value will be returned.
After receiving the handle, the client must first call function ::DdeAccessData(…) to access the data.
After the data is processed, function ::DdeUnaccessData(…) must be called to “unaccess” the data. If the
data is created with HDATA_APPOWNED flag, the client should not free the data. Otherwise, it can release the
data by calling function ::DdeFreeDataHandle(…).
Samples
In the sample application, an item “Time” is supported by the server under “Topic” topic name. When
the client request an XTYPE_REQUEST transaction on this item, the server get the current system time and
send it to the client. The client then displays the time in one of its edit box.
447
Chapter 16. Context Sensitive Help
Compared with the samples in the previous section, a new edit box and a button labeled “Request” are
added to the dialog box (Figure 15-3). The edit box is read-only, which will be used to display the time
obtained from the server. The button is used to let the user initiate the request transaction.
“Time” item
The following function shows how the client initiates the transaction after the user clicks “Request”
button, and how the client updates the time displayed in the edit box if the transaction is successful:
void CDDECliDlg::OnButtonRequest()
{
HSZ hszTimeItem;
HDDEDATA hData;
LPBYTE lpByte;
hszTimeItem=ObtainHsz(TEXT("Time"));
hData=::DdeClientTransaction
(
NULL,
0,
m_hConvClient,
hszTimeItem,
CF_TEXT,
XTYP_REQUEST,
5000,
NULL
);
if(hData != NULL)
{
lpByte=::DdeAccessData(hData, NULL);
m_szTimeItem=lpByte;
::DdeUnaccessData(hData);
::DdeFreeDataHandle(hData);
UpdateData(FALSE);
}
UnobtainHsz(hszTimeItem);
}
The following code fragment shows how the server responds to message XTYPE_REQUEST, prepares data
and sends it to the client:
……
case XTYP_REQUEST:
{
if
(
!::DdeCmpStringHandles(hszTopic, m_hszTopicName) &&
!::DdeCmpStringHandles(hszItemName, m_hszTimeItem) &&
wFormat == CF_TEXT
)
{
CTime time;
CString szTime;
HDDEDATA hData;
time=CTime::GetCurrentTime();
szTime=time.Format("%H:%M:%S");
448
Chapter 16. Context Sensitive Help
hData=::DdeCreateDataHandle
(
m_dwInst,
(LPBYTE)(LPCSTR)szTime,
szTime.GetLength(),
0,
m_hszTimeItem,
CF_TEXT,
0
);
Basics
Another interesting transaction is Advise, which provides a way to let the server inform the client after
an item stored on the server side has changed. The advise transaction can be initiated from the client side
by initiating XTYP_ADVSTART type transaction. As the server receives this message, it keeps an eye on the
item that is required by the client for advise service. If the data changes, the server will receive an
XTYP_ADVREQ message indicating that the item has changed (This message is posted by the server itself,
which could be triggered by any event indicating that the topic item has changed. For example, for “time”
item discussed in the previous section, it could be triggered by message WM_TIMER). After receiving
message XTYP_ADVREQ, the server sends an XTYP_ADVDATA message along with the handle of the updated
data to the client. After the client receives this message, it updates the advised topic item.
Now that we understand how XTYP_REQUEST type transaction is handled, it is easier for us to figure out
how the advise transaction should be implemented. First, the client initiates advise transaction by calling
function ::DdeClientTransaction(…) and passing XTYP_ADVSTART to parameter wType. Upon receiving
this message, the server must return TURE if it supports the specified topic and item; otherwise, it should
return FALSE. Once the data has changed, the server should call function ::DdePostAdvise(…) to let its
callback function receive an XTYP_ADVREQ message. Upon receiving this message, the server needs to
prepare the data and send the data handle to the client (There is no special function for doing this, all the
server needs to do is returning the data handle from the callback function). Once the server has provided
advise, the client will receive an XTYP_ADVDATA message along with the data handle. After receiving this
message, the client should update the advised item. The client can terminate the advise service at any time
by sending XTYP_ADVSTOP message through calling function ::DdeClientTransaction(…).
449
Chapter 16. Context Sensitive Help
“Advise” item
void CDDECliDlg::OnButtonAdvise()
{
HSZ hszTextItem;
HDDEDATA hData;
hszTextItem=ObtainHsz(TEXT("Text"));
if(m_bAdvise == FALSE)
{
hData=::DdeClientTransaction
(
NULL,
0,
m_hConvClient,
hszTextItem,
CF_TEXT,
XTYP_ADVSTART,
5000,
NULL
);
if(hData != FALSE)
{
m_btnAdvise.SetWindowText("Unadvise");
m_bAdvise=TRUE;
}
}
……
}
It is more or less the same with XTYP_REQUEST transaction. If the transaction is successful, we set a
Boolean type variable CDDECliDlg::m_bAdvise to TRUE and change the button’s text to “Unadvise”. By
doing this way, the button can be used for both advise starting and stopping.
450
Chapter 16. Context Sensitive Help
return (HDDEDATA)TRUE;
}
else
{
Printf("Do not support advise on this item!\r\n");
return (HDDEDATA)FALSE;
}
}
……
On the server side, a new command DDE | Advise is added to mainframe menu IDR_MAINFRAME. The
following function shows how this command is implemented. If the user changes the content contained in
variable CMainFrame::m_szText (if flag CMainFrame::m_bAdvise is TRUE at this time), message
XTYP_ADVREQ will be posted to the server:
void CMainFrame::OnDdeAdvise()
{
CAdviseDialog dlg;
Upon receiving message XTYP_ADVREQ in the DDE callback function, we must prepare a string handle
for CMainFrame::m_szText, and return it when the function exits:
……
case XTYP_ADVREQ:
{
if
(
!::DdeCmpStringHandles(hszTopic, m_hszTopicName) &&
!::DdeCmpStringHandles(hszItemName, m_hszTextItem) &&
wFormat == CF_TEXT
)
{
CString szText;
HDDEDATA hData;
szText=((CMainFrame *)(AfxGetApp()->m_pMainWnd))->m_szText;
hData=::DdeCreateDataHandle
(
m_dwInst,
(LPBYTE)(LPCSTR)szText,
szText.GetLength(),
0,
m_hszTextItem,
CF_TEXT,
0
);
Again, the data handle is created by calling function ::DdeCreateDataHandle(…). If the server doesn’t
support this service, it should return FALSE.
451
Chapter 16. Context Sensitive Help
if
(
hConv == m_hConvClient &&
hszTopic == m_hszTopicName &&
hszItem == hszText &&
wFormat == CF_TEXT &&
hData != NULL
)
{
lpByte=::DdeAccessData(hData, NULL);
((CDDECliDlg *)(AfxGetApp()->m_pMainWnd))->m_szText=lpByte;
::DdeUnaccessData(hData);
::DdeFreeDataHandle(hData);
((CDDECliDlg *)(AfxGetApp()->m_pMainWnd))->UpdateData(FALSE);
return (HDDEDATA)DDE_FACK;
}
return (HDDEDATA)DDE_FNOTPROCESSED;
}
……
void CDDECliDlg::OnButtonAdvise()
{
HSZ hszTextItem;
HDDEDATA hData;
hszTextItem=ObtainHsz(TEXT("Text"));
if(m_bAdvise == FALSE)
{
……
}
else
{
hData=::DdeClientTransaction
(
NULL,
0,
m_hConvClient,
hszTextItem,
CF_TEXT,
XTYP_ADVSTOP,
5000,
NULL
);
if(hData != FALSE)
{
m_btnAdvise.SetWindowText("Advise");
m_bAdvise=FALSE;
}
}
UnobtainHsz(hszTextItem);
}
452
Chapter 16. Context Sensitive Help
On the server side, after receiving this message, it simply turns off CMainFrame::m_bAdvise flag. After
this, if the user updates variable CMainFrame::m_szText, function ::DdePostAdvise(…) will not be called.
Figure 15-5. New controls are added for “poke” and “execute”
transactions
……
HDDEDATA hData;
HSZ hszPokeItem;
hszPokeItem=ObtainHsz(TEXT("Poke"));
m_szPoke+='\0';
hData=::DdeCreateDataHandle
(
m_dwInst, (LPBYTE)(LPCSTR)m_szPoke, m_szPoke.GetLength(), 0,
hszPokeItem, CF_TEXT, 0
);
453
Chapter 16. Context Sensitive Help
if
(
::DdeClientTransaction
(
(LPBYTE)hData,
0xFFFFFFFF,
m_hConvClient,
hszPokeItem,
CF_TEXT,
XTYP_POKE,
5000,
NULL
)
)
{
Printf("Poke executed!\r\n");
}
else Printf("Poke failed!\r\n");
UnobtainHsz(hszPokeItem);
……
Rather than using data handle, the address of the buffers will be used to transfer data. So when the
client calls ::DdeClientTransaction(…) to initiate transaction, the first parameter of this function is set to
the address rather that the handle of the buffers. Also, the second parameter is set to the size of buffers
instead of 0xFFFFFFFF. Since there is no need to specify item name and data format, the fourth and fifth
parameters are set to NULL.
454
Chapter 16. Context Sensitive Help
The address of the buffers will not be sent directly to the server. Instead, they will be used to create a
DDE object that contains the data. So as the server receives the corresponding message, it actually gets the
handle of this DDE object instead of the buffer address. In order to access the data, it must prepare some
buffers allocated locally and copy the data from the DDE object into these buffers by calling function
::DdeGetData(…), whose format is as follows:
Parameter hData is the handle received from the DDE callback function that identifies the DDE object.
Parameter pDst is a pointer to the buffers that will be used to receive data, whose size is specified by
parameter cbMax. Parameter cbOff specifies the offset within the DDE object.
Generally we do not know the size of data beforehand, so before allocating buffers, we can call this
function and pass NULL to its pDst parameter, which will cause the function to return the actual size of the
data. Then we can prepare enough buffers, pass the address of buffers and the buffer length to this function
to actually receive data.
In the samples, when the client initiates an execute transaction, the server does nothing but displaying
the command in its client window. Although it seems like poke transaction, there are some radical
differences between two types of transactions:
1) The poke transaction can be used to transmit any type of data, it requires a format specification. The
execute transaction only transmit a simple command.
1) The poke transaction must specify an item name. The execute transaction does not require this.
1) As we will see in section 15.7, the server will respond to execute transaction by executing a command.
The poke transaction just update data.
The following code fragment shows how the execute transaction is implemented on the server side:
……
case XTYP_EXECUTE:
{
if
(
!::DdeCmpStringHandles(hszTopic, m_hszTopicName) &&
hConv == m_hConvServer
)
{
LPBYTE lpByte;
int nSize;
455
Chapter 16. Context Sensitive Help
Synchronous transaction: after the client initialized the transaction, it will start a timer and wait till it
gets the response from the server. If there is no response when timer times out, the transaction will be
terminated.
Asynchronous transaction: after the client initialized the transaction, it does not wait. Instead, the
client will go on to do other job. After the server finished the transaction, the client will receive a
message indicating that the previous transaction has been finished.
Synchronous transaction is simple to implement. However, if the server responds very slowly, it will
cause severe overhead. In our sample, synchronous transaction is good enough because the server supports
only very limited types of services and each transaction won’t take much time. But generally a server may
need to serve multiple clients simultaneously, so when a client initialized a transaction, the server may be
busy with another transaction. If we use synchronous transaction, the client may need to wait until the
server comes to serve it. If we have several clients waiting concurrently, it will waste the system resource.
The asynchronous transaction is implemented more efficiently. As the client initialized the transaction,
it just moves on to do other job; when the server finishes this transaction, the client will receive a message
telling it the result of the transaction. In this case the client’s waiting time is eliminated.
Samples
Samples 15.6\DDE\Server and 15.6\DDE\Client are based on Samples 15.5\DDE\Server and
15.5\DDE\Client respectively. In the two samples, the poke transaction is implemented in asynchronous
mode. The following code fragment shows how the transaction is initiated:
……
HDDEDATA hData;
HSZ hszPokeItem;
hszPokeItem=ObtainHsz(TEXT("Poke"));
m_szPoke+='\0';
hData=::DdeCreateDataHandle
(
m_dwInst, (LPBYTE)(LPCSTR)m_szPoke, m_szPoke.GetLength(), 0, hszPokeItem,
CF_TEXT, 0
);
::DdeClientTransaction
(
(LPBYTE)hData,
0xFFFFFFFF,
m_hConvClient,
hszPokeItem,
CF_TEXT,
XTYP_POKE,
TIMEOUT_ASYNC,
NULL
);
UnobtainHsz(hszPokeItem);
……
456
Chapter 16. Context Sensitive Help
Initialize Initialize
transaction transaction
Yes No
Has the server
responded? Receive
XTYP_XACT_COMPLETE
message
Transaction
Go on finished
Yes No
Has timer
timed out?
Transaction
finished
The following code fragment shows how the transaction result is processed when the client receives an
XTYP_XACT_COMPLETE message:
……
case XTYP_XACT_COMPLETE:
{
HSZ hszPokeItem;
hszPokeItem=ObtainHsz(TEXT("Poke"));
if
(
hConv == m_hConvClient &&
hszTopic == m_hszTopicName &&
hszItem == hszPokeItem &&
wFormat == CF_TEXT
)
{
if(hData != NULL)
{
Printf("Poke executed!\r\n");
}
else Printf("Poke failed!\r\n");
}
UnobtainHsz(hszPokeItem);
}
……
457
Chapter 16. Context Sensitive Help
Program Manager
Under Windows, all types of applications are managed into groups. By clicking on Start | Programs
command on the task bar, we will see many groups, such as “Accessories”, “Startup”. Within each group,
there may exist some items that are linked directly to executable files, or there may exist some sub-groups.
Sometimes we may want to modify this structure by adding a new item or deleting an existing item.
Actually this structure is managed by Program Manager, which is a DDE server. We can interact with
this server to create group, delete group, add items to the group, delete items from a group through
initiating Execute transactions.
The client sample from the previous section is modified so that it can be used to communicate only to
Program Manager. Here, the service name of the Program Manager is “Progman”. As we can see, the
DDE initialization for this client is exactly the same as we did before.
The five commands we will ask the server to execute are: creating group; bringing up an existing
group; deleting a group; creating an item; deleting an item. All the DDE commands must start and end with
square braces (‘[’ and ‘]’). The following table lists the formats of five commands:
A combo box for storing the command types is added to sample’s dialog box. Besides this, the dialog
box also contains an edit box and a button labeled “Command”. The edit box will be used to let the user
input parameters for the command. For example, if we want to create a group with name “Test”, we can
select “Create group” from the combo box and input “Test” into the edit box then click “Command” button.
The application will initiate an execute transaction to the Program Manager and send
“[CreateGroup(Test)]” command to it.
We can use this program to create groups and add items to a group. We can also delete unwanted
groups or remove items from a group.
Summary
1) To support DDE in an application, function ::DdeInitialize(…) must be called to initialize it; also,
before the application exits, function ::DdeUninitialize(…) must be called to uninitialize the DDE.
1) The server must register DDE service by calling function ::DdeNameService(…). Before the server
exits, it must call the same function to unregister the service.
1) Before DDE client initiates any transaction, it must call function ::DdeConnect(…) to obtain a
conversation handle that can be used to identify the server in the following transactions.
1) A server can support several topics, under each topic there may be several items supported. When
initiating a transaction, the client need to specify both of them.
5) Data can not be exchanged directly between two processes. Instead, it must be sent either by data
handle or by DDE object. In the former case, the data can be accessed by calling functions
::DdeAccessData(…). Function ::DdeUnaccessData(…) can be used to unaccess data and
::DdeFreeDataHandle(…) can be used to free data. In the later case, the data can be retrieved by
calling function ::DdeGetData(…). To create a data handle, we can use function
::DdeCreateDataHandle(…).
458
Chapter 16. Context Sensitive Help
1) In DDE model, two strings can be compared by their handles. In order to do this, we need to call
function ::DdeCmpStringHandles(…).
1) Data request transaction can be used to request data from the server.
1) Advise transaction can be used to let the client monitor the changes on the data stored on the server
side.
1) Poke transaction can be used by the client to update the data stored on the server side.
1) Execute transaction can be used by the client to let commands be executed on the server side.
1) There are two types of transmission modes: synchronous and asynchronous. Synchronous transaction
is easy to implement, but asynchronous transaction is more efficient under certain conditions.
1) Program Manager is a DDE server that supports execute transaction.
459
Chapter 16. Context Sensitive Help
Chapter 16
H
elp is very essential for all types of applications. Since usually it is difficult to make the user
interface intuitive enough to eliminate guessing when user interacts with the program, we need to
include context sensitive help to tell user what exactly each command means. Although usually
help development is not a part of job of programmers, the persons who are in charge of application
development should cooperate closely with help developers to create high-quality applications.
Check here to
enable context
sensitive help
After we’ve enabled the context sensitive help, a “question mark” button will appear on the mainframe
tool bar (Figure 16-2). If we click on it, the mouse cursor will change to a question mark. If we use this
cursor to click any menu command (or any part of the application window), the help window will pop up
displaying the description of the item that was just clicked.
460
Chapter 16. Context Sensitive Help
Button used to
implement context
sensitive help, whose
ID is
ID_CONTEXT_HELP
By default, contents of help are stored in a rich text format file generated by the Application Wizard:
“AfxCore.rtf”. We can find many footnotes within this file, each footnote corresponds to one help page. To
implement the context sensitive help for a custom menu command, we need to add a new footnote to file
“AfxCore.rtf”, and link it to the command.
If the context sensitive help is enabled within the Application Wizard in step 4 (see Figure 1-1), the
help project will be generated automatically. All the files used to build the help will be contained in a “hlp”
directory under the project directory. For example, if we use the Application Wizard to generate an SDI
application named “Help”, the help project will be generated under “Help\hlp\” directory. There will be a
batch file “Makehelp.bat” under the “Help” directory. Also, there are some other important files under
“Help\hlp” directory that are used to build the help. The following table lists the usages of these files:
The help is compiled by a utility named “Microsoft Help Workshop”. The executable file “Hcw.exe”
can be found under Visual C directory “~DevStudio\Vc\Bin\”. This utility can compile “*.hpj” file to
generate a target help file.
By double clicking on the “*.hpj” file or “*.cnt” file (In “Explorer”, they are described as “Help
project file” and “Help contents file” respectively), we can compile the help project in the help workshop
environment. Also, when we compile the application in Developer Studio, the help project will also be
compiled. The help project file (“.hpj) is similar to a make file when we execute C compilers, it contains
information about how to generate the target help file. The help contents file (“.cnt”) contains the
information of “help topics”. If we execute Help | Help Topics command from the application, the help
contents will be displayed in a “Help Topics” property sheet (Figure 16-3). All the descriptions about the
commands and the application are included in “AfxCore.rtf” and “AfxPrint.rtf” files, we must edit them in
order to add custom help descriptions.
Sample
Because the default help project already has many help items, without the knowledge of the help
project, it is difficult for a programmer to add items for the newly added commands. Sample 16.1\Help is a
standard SDI application generated by Application Wizard, which demonstrates how to add new help items
and link them to the application commands to support context sensitive help.
461
Chapter 16. Context Sensitive Help
New Commands
First, after the standard project is generated, four new commands are added to the application. These
commands are implemented in both the mainframe menu and the tool bar, and their IDs are
ID_HELPTEST_TESTA, ID_HELPTEST_TESTB, ID_HELPTEST_TESTC and ID_HELPTEST_TESTD respectively. In
IDR_MAINFRAME menu, the new commands are Help Test | Test A, Help Test | Test B, Help Test | Test C,
and Help Test | Test D. In the IDR_MAINFRAME tool bar, we also have four buttons corresponding to the four
command IDs. The message handlers of these commands are all blank, because we just want to
demonstrate how to implement context sensitive help for them.
Editing “AfxCore.rtf”
We must add four items to the help file in order to link a command to the help. In order to do this, we
must modify file “AfxCore.rtf”. The rich text format file can be edited by a word processor like Microsoft
Word (WordPad is not powerful enough for this purpose, because it does not support footnote editing).
Within this file, each help item is managed as a footnote. If we want to add a new help item, we just need to
add a new footnote.
To show how to add a footnote to the “.rtf” file, lets assume that we want to add a footnote for
ID_HELPTEST_TESTA command.
Each footnote must be associated with a tag, so that it can be referenced from inside or outside the file.
In Microsoft Word, we can either add number-based tags or user-defined tags. To make the tags
meaningful, usually we define tags by ourselves. The tags can be any string, usually they will have some
relationship with the command IDs implemented in the application. For example, we can use “help_test_A”
as the tag for the footnote that will be used to implement help for the command whose ID is
ID_HELPTEST_TESTA.
To add a new tag within Microsoft Word, first we need to move the cursor to the bottom of the file,
then execute Insert | Break… command. From the popped up dialog box, we need to check the radio
button labeled “Page Break”. If we click “OK” button, a new page will be added to the file (Generally each
footnote needs to use one page, so we need to add page break whenever a new footnote is added). Now
execute Insert | Footnote command, and check the radio button labeled “Footnote” from the popped up
dialog box (in “Insert” section). Then, check radio button labeled “Custom Mark” (in “Numbering”
section) and input a ‘#’ into the edit box beside it (Figure 16-4). Finally, click “OK” button. Now the client
window of Word will split into two panes, the lower of which will show all footnote tags. The caret will be
placed right after the ‘#’ sign waiting for us to type in the footnote tag. We can type “help_test_A”, then
click on the upper pane. Then we can input any help description for command ID_HELPTEST_TESTA.
462
Chapter 16. Context Sensitive Help
Tag “help_test_A” can be referenced either from other footnote pages or from the application. If we
want to let the user jump from one help item to another (for example, when viewing help on “telp_test_A”,
the user may want to jump to footnote “help_test_B” by clicking on a link within the same page), we need
to implement links by editing the “.rtf” file.
Suppose we have added four footnotes for the newly implemented commands: “help_test_A”,
“help_test_B”, “help_test_C”, and “help_test_D”, and we want to add links to the rest of three commands
within each footnote page. For example, help item “help_test_A” may be implemented as illustrated in
Figure 16-5:
Under “See Also” statement, there are three links that will direct mouse clicking to footnotes
“help_test_B”, “help_test_C” and “help_test_D”.
To create this type of links, we need to use special font format. We need to use double underline style
to format the text that will be linked to a footnote (By doing this, the text formatted with double underline
can respond to mouse clicking and bringing up another help item). Following the underlined text, we need
to place the footnote tag using “Hidden” font style. To let the hidden text be displayed in the Word editor,
we can execute command Tools | Options… and click tab View on the popped up property sheet. Then
within “Nonprinting Characters” section, check “Hidden Text” check box.
Now we can make a link very easily. First we need to type in and format the text as illustrated in
Figure 16-6:
To format text using double underline, we can select the text, then execute command Format |
Font…. From the popped up property sheet, we can go to “Font” page and select “Double” form
“Underline” combo box. By doing this, the selected text will be double underlined. To format text using
“Hidden Text” style, we can first select the text, then execute command Format | Font…, go to “Font”
463
Chapter 16. Context Sensitive Help
page, make sure that “None” is selected from the “Underline” combo box, and check “Hidden” check box
within “Effects” section.
ID Mapping
In order to support context sensitive help, we must link the footnotes to their corresponding command
IDs. In our case, we need to link “help_test_A” to ID_HELPTEST_TESTA, “help_test_B” to
ID_HELPTEST_TESTB… and so on.
This ID mappings are implemented in “.hm” file, so by opening file “Help.hm” with a text editor, we
will see all the IDs of the help items supported in the sample. Generally help IDs are generated according to
certain rules: it bases the help ID of a control (or a window) on its resource ID. By default, for a command
whose ID starts with “ID_”, MFC generates symbolic help ID by prefixing a character ‘H’ to the resource
ID. For example, for command ID_HELPTEST_TESTA, the help ID generated by MFC is
HID_HELPTEST_TESTA. For the actual ID value, MFC generates it by adding a fixed number to the
corresponding resource ID value. For example, if the value of ID_HELPTEST_TESTA is 0x8004, the value of
HID_HELPTEST_TESTA would be 0x18004 here a number of 0x1000 is added to the command resource
ID.
By doing this, when the user executes a command in the help mode (When the mouse cursor changes
to a question mark after the user clicks command ID_CONTEXT_HELP), the application will first find out the
resource ID of the command being executed, add a fixed value, then pass the result to the help. By using
this rule to generate ID for a help item, it is easier for us to implement context-sensitive help.
Although the ID mapping is customizable, which means we can prefix different character(s) to a
resource ID for generating help ID (For example, the help ID of command ID_HELPTEST_TESTA could be
HLID_HELPTEST_TESTA), and we can add any value to the resource ID to generate a help ID, it is more
convenient if we stick to the rules of MFC. For example, if we add 0x20000 instead of 0x10000 to the
resource ID to make a help ID, we also need to add some code to the program to customize its default
behavior.
Another problem still remains: since we’ve already added footnotes and defined our own tags, the
command IDs are still not directly linked to the footnote tags. For example, the help ID of command
ID_HELPTEST_TESTA is HID_HELPTEST_TESTA, while the footnote tag implemented in the help file is
“help_test_A”. This can be solved by defining alias names in the help project file. By opening “*.HPJ” file,
we will see an [ALIAS] session. Under this session, a help ID can be linked directly to a footnote tag. In
our case, the footnote tags are “help_test_A”, “help_test_B”…, and the help IDs automatically generated
by MFC are HID_HELPTEST_TESTA, HID_HELPTEST_TESTB…. To link them together, we can add the following
to alias session:
[ALIAS]
……
HID_HELPTEST_TESTA=help_test_A
HID_HELPTEST_TESTB=help_test_B
HID_HELPTEST_TESTC=help_test_C
HID_HELPTEST_TESTD=help_test_D
Obviousely, if we use the default help ID strings to implement footnote tags (i.e., use
HID_HELPTEST_TESTA instead of help_test_A as the footnote tag), the ID mapping could be eleminated.
By doing this, when the user executes certain command in the help mode, the corresponding help page
will be automatically brought up.
464
Chapter 16. Context Sensitive Help
should be input into the edit box labeled “Title”, and the footnote tag should be input into the edit box
labeled “Topic ID” (Figure 16-7).
Heading
Tab Entries
After making any change to the help project, we need to recompile the project in order to get the up-to-
date help.
To make the help working, both “.hlp” and “.cnt” file must be copied to the directory that contains the
application executable file.
465
Chapter 16. Context Sensitive Help
Button Used to
Enter Help Mode
ID Naming Rules
Adding context sensitive help for common controls is more complicated than that of menu commands.
First we must add footnotes for each common control contained in the dialog box, then we need to do the
ID mappings.
By default, MFC will generate help IDs for the commands or controls whose resource IDs start from
“ID_”, “IDM_”, “IDP_”, “IDR_”, “IDD_” and “IDW_” by prefixing a single character ‘H’ to them. Also,
the values of these help IDs are generated by adding a fixed number to the corresponding resource IDs.
This fixed number is different for different types of IDs. For example, for “ID_XXX” and “IDM_XXX”
types of IDs, 0x10000 will be added to the resource ID; for “IDP_XXX” type IDs, 0x30000 will be added;
for “IDR_XXX” and “IDD_XXX” types of IDs, 0x20000 will be added; for “IDW_XXX” type IDs,
0x50000 will be added.
By default, the IDs of the common controls in a dialog box all start with “IDC_” prefix, which is not
included in the default mapping. This means we must generate help IDs by ourselves. Actually, this can be
easily achieved. If we open file “Makehelp.bat” (in our case, this file should be located in “16.2\Help\”
directory), we will see that the help IDs are generated through “Makehm” utility, which can be executed
under DOS prompt.
“Makehm” has the following syntax:
where
argument 1: prefix of resource ID
argument 2: prefix of help ID
argument 3: base number
argument 4: resource header file name
For example, if we want to generate symbolic help IDs for those IDs prefixed with “IDC_”, and add
0x10000 to the resource IDs to generate actual help IDs, we can execute this command as follows (Here,
the application resource header file name is “resource.h”, and the “.hm” file name is “Help.hm”, it is
located under the directory of “Hlp”):
If we include the above statement in file “Makehelp.bat”, after executing it, we will see that all the
common controls will have corresponding help IDs in file “Help.hm”.
466
Chapter 16. Context Sensitive Help
HIDC_RADIO 0x103EA
HIDC_COMBO 0x103EB
Please note that file “Makehm.exe” is located in directory “…DevStudio\Vc\Bin\”. We must set path
to this directory in order to execute it from DOS prompt. However, if we compile the help through
Developer Studio or Help Workshop, there is no need to set the path. An alternate solution that can let us
run the batch file from DOS prompt without setting path is to copy file “Makehm.exe” to the directory that
contains file “Makehelp.bat”.
In the sample application 16.2-1\Help, four new footnotes with tags “common_button”,
“common_combobox”, “common_edit” and “common_radio” are added to file “AfxCore.rtf”. Their alias
names are specified in the help project file:
[ALIAS]
……
HIDC_BUTTON=common_button
HIDC_COMBO=common_combobox
HIDC_EDIT=common_edit
HIDC_RADIO=common_radio
This still does not finish context sensitive help implementation for the common controls. When the
user uses question mark cursor to click a common control contained in the dialog box, by default, it is the
ID of the dialog box, not the ID of the control that will be used to activate the help. To enable context help
for each individual control, we need to call function CWnd::SetWindowContextHelpId(…) for it, which has
the following format:
Parameter dwContextHelpId is the help ID of the control. This function can be used to link a control’s
resource ID to its help ID.
To make things work, we must use the IDs generated by “Makehm” utility for each control. In the
sample, a function CHelpDlg::SetContextHelpId() (CHelpDlg is the class used to implement the dialog
box) is implemented to set help IDs for all the common controls contained in the dialog box:
void CHelpDlg::SetContextHelpId()
{
CWnd *ptrWnd;
ptrWnd=GetWindow(GW_CHILD);
while(TRUE)
{
ptrWnd->SetWindowContextHelpId(ptrWnd->GetDlgCtrlID()+0x10000);
if(ptrWnd == ptrWnd->GetWindow(GW_HWNDLAST))break;
ptrWnd=ptrWnd->GetWindow(GW_HWNDNEXT);
}
}
First we call function CWnd::GetWindow(…) and use flag GW_CHILD to find out the first child window
contained in the dialog box. Then we call this function repeatedly using GW_HWNDNEXT flag to find out all the
other child windows. The loop will be stopped after the last child window is enumerated. For each child
window (common control), we obtain its resource ID value by calling function CWnd::GetDlgCtrlID(),
adding 0x10000 to it, and using this value to set help ID.
We must make sure that both utility “Makehm” and function CWnd::SetWindowContextHelpID(…) add
the same value to the resource IDs to result in help IDs.
Function CWnd::OnHelpInfo(…)
We also need to override function CWnd::OnHelpInfo(…) in order to let the help jump to a special page
when being activated. The following code fragment shows how this function is overridden in the sample:
dwContextID=pHelpInfo->dwContextId-0x10000;
467
Chapter 16. Context Sensitive Help
switch(dwContextID)
{
case IDC_EDIT:
case IDC_RADIO:
case IDC_BUTTON:
case IDC_COMBO:
{
AfxGetApp()->WinHelp(pHelpInfo->dwContextId);
return TRUE;
}
default: break;
}
return CDialog::OnHelpInfo(pHelpInfo);
}
When the user uses question mark cursor to click a control, by default, function CWnd::OnHelpInfo(…)
will be called, this will not activate help for the common control being clicked. To customize this, we must
check if the help ID corresponds to one of the common controls contained in the dialog box. If so, we need
to activate the help by ourselves (and jump to the corresponding help page).
The information of the control (or window) will be passed through a HELPINFO type object. Especially,
the help ID will be stored in dwContextId member of this structure. If we have called function CWnd::
SetWindowContextHelpId(…) for a control, the value of this ID will be the one we set there. So in our case,
the resource ID can be obtained by subtracting 0x10000 from the help ID.
In the above function, we first check if the control is one of the four controls that support context
sensitive help. If so, function CWinApp::WinHelp(…) is called to activate the help (The help ID is passed to
the first parameter of this function). Otherwise, default implementation of this function will be called.
Function CWinApp::WinHelp(…) has two parameters:
The help will be activated in different styles according to the values of parameter nCmd. If we do not
specify this parameter, the help will be implemented in default style, and jump to the footnote
corresponding to the help ID specified by parameter dwData. If the help ID could not be found, the first
footnote contained in the help will be displayed and an error message will pop up.
dwContextID=pHelpInfo->dwContextId-0x10000;
switch(dwContextID)
{
case IDC_EDIT:
case IDC_RADIO:
{
AfxGetApp()->WinHelp(pHelpInfo->dwContextId);
return TRUE;
}
case IDC_BUTTON:
case IDC_COMBO:
{
AfxGetApp()->WinHelp(pHelpInfo->dwContextId, HELP_CONTEXTPOPUP);
return TRUE;
}
default: break;
}
return CDialog::OnHelpInfo(pHelpInfo);
}
The edit box and radio button still use the default style, whose help window contains standard menu
and buttons. The help windows of push button and combo box controls are implemented by pop up
window, which is a smaller window with a yellowish background, and does not contain other controls
(Figure 16-10). By passing different parameters to function CWinApp::WinHelp(…), we can activate “Help
Topic” dialog box. Also, we can adjust its position and size of the help window before it is displayed,.
468
Chapter 16. Context Sensitive Help
Display help in a
pop up window
Summary
1) In order to implement context sensitive help for a command, we need to add a footnote to the
“AfxCore.rtf” file, then link the command ID to the tag of the footnote.
2) Usually the help ID is generated by prefixing character(s) to the resource ID of the command, and the
value of the help ID is generated by adding a fixed number to the value of the resource ID.
3) The resource ID can be linked to a footnote tag by specifying an alias name in “.hpj” file.
4) A help page can be referenced either from other help pages or from the application.
5) To support context sensitive help in the dialog box, we need to call function CWnd::
SetWindowContextHelpId(…) for every common control that will support context sensitive help, and
override function CWnd::OnHelpInfo(…) to activate customized help window.
6) By default, function CWnd::OnHelpInfo(…) does not support context sensitive help for common
controls. In this case, we can call CWinApp::WinHelp(…) to customize the default help implementation.
7) When calling function CWinApp::WinHelp(…), we can use various parameters to activate help windows
in different styles. For example, using parameter HELP_CONTEXTPOPUP will invoke a pop up help
window.
469
Appendix A. Virtual-key Codes
Mouse Buttons:
Editing Keys (These keys are most commonly used in a text editor):
470
Appendix A. Virtual-key Codes
VK_8 38 8 key
VK_9 39 9 key
VK_A 41 A key
VK_B 42 B key
VK_C 43 C key
VK_D 44 D key
VK_E 45 E key
VK_F 46 F key
VK_G 47 G key
VK_H 48 H key
VK_I 49 I key
VK_J 4A J key
VK_K 4B K key
VK_L 4C L key
VK_M 4D M key
VK_N 4E N key
VK_O 4F O key
VK_P 50 P key
VK_Q 51 Q key
VK_R 52 R key
VK_S 53 S key
VK_T 54 T key
VK_U 55 U key
VK_V 56 V key
VK_W 57 W key
VK_X 58 X key
VK_Y 59 Y key
VK_Z 5A Z key
Arithmetic Keys:
471
Appendix A. Virtual-key Codes
Function Keys:
472
Appendix A. Virtual-key Codes
E6 OEM specific
E9-F5 OEM specific
VK_ATTN F6 Attn key
VK_CRSEL F7 CrSel key
VK_EXSEL F8 ExSel key
VK_EREOF F9 Erase EOF key
VK_PLAY FA Play key
VK_ZOOM FB Zoom key
VK_NONAME FC Reserved for future use
VK_PA1 FD PA1 key
VK_OEM_CLEAR FE Clear key
Currently undefined codes: 05-07, 0A-0B, 0E-0F, 1A, 3A-40, 5E-5F, 88-8F, 92-B9, C1-DA, E5, E7-E8.
473
Appendix B. Ternary Raster Operations
474
Appendix B. Ternary Raster Operations
28 00280369
29 002916CA
2A 002A0CC9
2B 002B1D58
2C 002C0784
2D 002D060A
2E 002E064A
2F 002F0E2A
30 0030032A
31 00310B28
32 00320688
33 00330008
34 003406C4
35 00351864
36 003601A8
37 00370388
38 0038078A
39 00390604
3A 003A0644
3B 003B0E24
3C 003C004A
3D 003D18A4
3E 003E1B24
3F 003F00EA
40 00400F0A
41 00410249
42 00420D5D
43 00431CC4
44 00440328
45 00450B29
46 004606C6
47 0047076A
48 00480368
49 004916C5
4A 004A0789
4B 004B0605
4C 004C0CC8
4D 004D1954
4E 004E0645
4F 004F0E25
50 00500325
51 00510B26
52 005206C9
53 00530764
54 005408A9
55 00550009
56 005601A9
57 00570389
58 00580785
59 00590609
5A 005A0049
5B 005B18A9
475
Appendix B. Ternary Raster Operations
5C 005C0649
5D 005D0E29
5E 005E1B29
5F 005F00E9
60 00600365
61 006116C6
62 00620786
63 00630608
64 00640788
65 00650606
66 00660046
67 006718A8
68 006858A6
69 00690145
6A 006A01E9
6B 006B178A
6C 006C01E8
6D 006D1785
6E 006E1E28
6F 006F0C65
70 00700CC5
71 00711D5C
72 00720648
73 00730E28
74 00740646
75 00750E26
76 00761B28
77 007700E6
78 007801E5
79 00791786
7A 007A1E29
7B 007B0C68
7C 007C1E24
7D 007D0C69
7E 007E0955
7F 007F03C9
80 008003E9
81 00810975
82 00820C49
83 00831E04
84 00840C48
85 00851E05
86 008617A6
87 008701C5
88 008800C6
89 00891B08
8A 008A0E06
8B 008B0666
8C 008C0E08
8D 008D0668
8E 008E1D7C
8F 008F0CE5
476
Appendix B. Ternary Raster Operations
90 00900C45
91 00911E08
92 009217A9
93 009301C4
94 009417AA
95 009501C9
96 00960169
97 0097588A
98 00981888
99 00990066
9A 009A0709
9B 009B07A8
9C 009C0704
9D 009D07A6
9E 009E16E6
9F 009F0345
A0 00A000C9
A1 00A11B05
A2 00A20E09
A3 00A30669
A4 00A41885
A5 00A50065
A6 00A60706
A7 00A707A5
A8 00A803A9
A9 00A90189
AA 00AA0029
AB 00AB0889
AC 00AC0744
AD 00AD06E9
AE 00AE0B06
AF 00AF0229
B0 00B00E05
B1 00B10665
B2 00B21974
B3 00B30CE8
B4 00B4070A
B5 00B507A9
B6 00B616E9
B7 00B70348
B8 00B8074A
B9 00B906E6
BA 00BA0B09
BB 00BB0226
BC 00BC1CE4
BD 00BD0D7D
BE 00BE0269
BF 00BF08C9
C0 00C000CA
C1 00C11B04
C2 00C21884
C3 00C3006A
477
Appendix B. Ternary Raster Operations
C4 00C40E04
C5 00C50664
C6 00C60708
C7 00C707AA
C8 00C803A8
C9 00C90184
CA 00CA0749
CB 00CB06E4
CC 00CC0020
CD 00CD0888
CE 00CE0B08
CF 00CF0224
D0 00D00E0A
D1 00D1066A
D2 00D20705
D3 00D307A4
D4 00D41D78
D5 00D50CE9
D6 00D616EA
D7 00D70349
D8 00D80745
D9 00D906E8
DA 00DA1CE9
DB 00DB0D75
DC 00DC0B04
DD 00DD0228
DE 00DE0268
DF 00DF08C8
E0 00E003A5
E1 00E10185
E2 00E20746
E3 00E306EA
E4 00E40748
E5 00E506E5
E6 00E61CE8
E7 00E70D79
E8 00E81D74
E9 00E95CE6
EA 00EA02E9
EB 00EB0849
EC 00EC02E8
ED 00ED0848
EE 00EE0086
EF 00EF0A08
F0 00F00021
F1 00F10885
F2 00F20B05
F3 00F3022A
F4 00F40B0A
F5 00F50225
F6 00F60265
F7 00F708C5
478
Appendix B. Ternary Raster Operations
F8 00F802E5
F9 00F90845
FA 00FA0089
FB 00FB0A09
FC 00FC008A
FD 00FD0A0A
FE 00FE02A9
FF 00FF0062
479
Index
Index
BEGIN_MESSAGE_MAP, 7
Bezier curve, 344
# BIF_BROWSEFORCOMPUTER, 171
BIF_BROWSEFORPRINTER, 171
#if, 402 BIF_RETURNFSANCESTORS, 172
#pragma, 401 BIF_RETURNONLYFSDIRS, 172
.cnt, 465 Binary bitmap image, 313
.hm, 468 Bit count per pixel, 276
.hpj, 465 BITMAP, 50, 195, 273, 285
__declspec, 402 Mask bitmap, 77
_AfxCommDlgProc, 188 Obtain handle, 72
_chdir, 101 Transparent background, 76
_chdrive, 423 Bitmap resource
_getdrive, 423 Add, 44
16 color, 215 Bitmap size, 51
16 color DIB, 276, 282 Bitmap with transparancy, 309
24 bit, 215 BITMAPFILEHEADER, 282
24-bit DIB, 276, 282 BITMAPINFO, 276, 305
24-bit to 256-color format conversion, 297 BITMAPINFORHEADER, 276
256 color, 215 Black-and-white image, 302
256 color DIB, 276, 282 BN_CLICKED, 131
256-color to 24-bit conversion, 293 BROWSEINFO, 171, 172
8.3 format, 168 Brush, 64, 65, 195
-SECTION, 403 Hatched, 64
-SEGMENT, 401 Hollow brush, 202
Solid, 64
Button
A Bitmap check box and radio button, 70, 74
Button states, 68
AFX_IDS_APP_TITLE, 390 Irregular shape bitmap button, 76
AFX_IDW_CNTROLBAR_FIRST, 157 Mouse sensitive button, 87
AFX_IDW_CONTROLBAR_LAST, 157 Owner-draw bitmap button, 68
AFX_IDW_PANE_FIRST, 57 Trap WM_LBUTTONUP message, 84
AfxCore.rtf, 465
AfxGetApp, 27
AfxGetInstanceHandle, 173, 181 C
AfxGetMainWnd, 43, 193
AfxGetResourceHandle, 154, 278 Callback function, 233
AfxRegisterClass, 380 CallNextHookEx, 403
AND, 304, 314, 339 CAnimateCtrl, 134
Animate control, 134–36 CAnimateCtrl::Open, 134
APPCLASS_STANDARD, 442 CAnimateCtrl::Play, 134
APPCMD_CLIENTONLY, 442 CAPS LOCK key, 252
APPCMD_FILTERINITS, 442 Caption bar, 388
ARROW key, 252 Caption text, 389
Attach menu, 53 Capture, 87
Auto tick, 96 Release capture, 87
AVI, 134 Set capture, 87
Capture screen, 357
Caret, 244
B CB_ERR, 104
CBitmap, 195
BACK SPACE key, 250 CBitmap::Attach, 281
480
Index
481
Index
482
Index
483
Index
484
Index
485
Index
486
Index
487
Index
488
Index
489
Index
490
Index
491
Index
492
Index
493