Stingray Foundation Library Users Guide - Rogue Wave Software

Stingray Foundation Library Users Guide - Rogue Wave Software
StingrayВ® Foundation Library
User’s Guide
StingrayВ® Studio
Version 6.0.1
STINGRAY STUDIO FOUNDATION USER'S GUIDE
PRODUCT TEAM
Development: Terry Crook, Clayton Dean, Boris Meltreger, David Noi
Documentation: Marc Betz, Shelley Hoose
Development Manager: Clayton Dean
Product Manager: Ben Gomez
Support: Terry Crook, Boris Meltreger
THIS MANUAL
В© Copyright 1997-2012 Rogue Wave Software, Inc. All Rights Reserved.
Rogue Wave and Stingray are registered trademarks of Rogue Wave Software, Inc. in the United States and
other countries. All other trademarks are the property of their respective owners.
ACKNOWLEDGMENTS
This documentation, and the information contained herein (the "Documentation"), contains proprietary information of Rogue Wave Software,
Inc. Any reproduction, disclosure, modification, creation of derivative works from, license, sale, or other transfer of the Documentation without
the express written consent of Rogue Wave Software, Inc., is strictly prohibited. The Documentation may contain technical inaccuracies or typographical errors. Use of the Documentation and implementation of any of its processes or techniques are the sole responsibility of the client, and
Rogue Wave Software, Inc., assumes no responsibility and will not be liable for any errors, omissions, damage, or loss that might result from any
use or misuse of the Documentation
ROGUE WAVE SOFTWARE, INC., MAKES NO REPRESENTATION ABOUT THE SUITABILITY OF THE
DOCUMENTATION. THE DOCUMENTATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
KIND. ROGUE WAVE SOFTWARE, INC., HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS
WITH REGARD TO THE DOCUMENTATION, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, OR NONINFRINGEMENT. IN NO EVENT SHALL ROGUE
WAVE SOFTWARE, INC., BE LIABLE, WHETHER IN CONTRACT, TORT, OR OTHERWISE, FOR ANY SPECIAL, CONSEQUENTIAL, INDIRECT, PUNITIVE, OR EXEMPLARY DAMAGES IN CONNECTION WITH
THE USE OF THE DOCUMENTATION.
The Documentation is subject to change at any time without notice.
ROGUE WAVE SOFTWARE, INC.
Address: 5500 Flatiron Parkway, Boulder, CO 80301 USA
Product Information:
Fax:
Web:
(303) 473-9118 (800) 487-3217
(303) 473-9137
http://www.roguewave.com
CONTENTS
1Chapter 1 Introduction to Stingray Foundation Library
1.1 Welcome to Stingray Foundation Library 1
1.2 Product Features 2
1.3 Location of Samples 3
1.4 Supported Platforms 3
1.5 Getting Help 4
1.5.1 Documentation 4
1.5.2 Knowledge Base 4
1.5.3 Professional Services 4
1.5.4 Technical Support 5
1.6 Licensing Restrictions 5
2Chapter 2 Getting Started
2.1 Building the SFL Libraries 7
2.1.1 SFL Library Naming Conventions 7
2.2 SFL Build Configurations 9
2.3 Visual Studio Environment Setup for SFL 10
2.4 Building SFL Libraries with the Visual Studio Solution 10
2.4.1 Make Files and Building Directly with nmake 12
2.4.2 Cleaning SFL Build Targets 12
2.5 SFL Build Wizard 12
3Chapter 3 Interface-Based Programming
3.1 Introduction 13
3.2 IQueryGuid and guid_cast 14
Contents iii
3.3 GUID Maps 16
3.4 Reference Counting 17
4Chapter 4 Design Patterns
4.1 Introduction 19
4.2 The Subject-Observer Pattern 20
4.3 The Composite Pattern 22
4.4 The Object Factory Pattern 25
4.4.1 Example 26
4.5 Polymorphic Iteration 27
4.5.1 The Polymorphic Iterator Templates 28
4.5.2 The Traversable Interfaces 29
4.5.3 The Traversable Mix-in Templates 30
4.5.3.1 Lifetime Management 31
4.5.3.2 MFC and COM Collections 31
5Chapter 5 Properties Package
5.1 Introduction to SFL Properties 33
5.1.1 Property Objects 33
5.2 Property Containers 34
5.2.1 A Property Container Implementation 35
5.2.1.1 The Property Map 35
5.2.1.2 Property Accessors 35
5.2.2 Property Container Example 37
5.3 ActiveX Controls 39
5.3.1 ActiveX Property Containers 39
5.3.2 Using ActiveX Property Containers 40
6Chapter 6 Events Package
6.1 Introduction to SFL Events 43
6.2 Event Objects 44
6.2.1 Windows Messages 44
6.2.2 The Event Factory 45
6.2.3 Windows Message Cracking 46
6.3 Event Routers 47
iv Contents
6.3.1 Default Event Router Implementation 47
6.3.2 ATL Integration 48
6.3.3 MFC Integration 49
6.4 Event Listeners 52
6.4.1 Dispatching Events 52
6.4.2 Adapter Classes 53
6.4.3 Using Event Listeners 55
6.4.4 Efficiency of Event Listeners vs. Message Maps 56
6.5 Chaining Event Routers 57
6.6 Custom Event Types 58
7Chapter 7 Layout Manager
7.1 Layout Manager Framework 59
7.2 Issues with Resizable Windows 60
7.3 Layout Manager Architecture 61
7.3.1 Layout Nodes 61
7.3.2 Layout Recalculation Process 62
7.3.2.1 Recalculation 62
7.3.2.2 Realization 62
7.3.3 Node Creation 63
7.3.4 Node Initialization 63
7.4 Integration with ATL 65
7.4.1 Adding Layout Management to Your Applications 67
7.5 Layout Algorithms 68
7.5.1 Scale Layout 68
7.5.2 Relative Layout 68
7.5.3 Border-Client Layout 70
7.5.4 DC Layout Nodes 70
7.5.5 Splitter Layout 71
7.5.6 Borders and Edges 73
7.6 Examples 74
8Chapter 8 Model View Controller
8.1 What is MVC? 77
8.2 The MVC Design Pattern 78
8.2.1 Model-View-Controller Relationship 78
8.2.2 The Subject-Observer Pattern in MVC 79
Contents v
8.2.3 Additional Reading on MVC 79
8.3 Visual Components 80
8.3.1 Visual Component Interfaces 80
8.3.2 CMvcVisualComponent 81
8.3.3 CMvcVisualPart 81
8.3.4 Coordinate Mapping 82
8.3.5 CMvcLogicalPart 83
8.3.6 Wrappers (Decorators) 84
8.3.6.1 MvcWrapper_T 84
8.3.6.2 MvcBorderWrapper_T 84
8.3.6.3 MvcScrollWrapper_T 85
8.3.7 MFC Specifics 85
8.4 MVC Models 86
8.4.1 CMvcModel 86
8.4.2 Presentation Models 87
8.4.3 MFC Specifics 88
8.5 MVC Viewports 89
8.5.1 CMvcViewport 89
8.5.2 Associating Viewports with Windows 90
8.5.3 Getting a Device Context 91
8.5.4 Event Routing 92
8.5.5 Scrolling 93
8.5.6 Zooming 93
8.5.7 ATL Specifics 94
8.5.7.1 CMvcAtlWndViewport 94
8.5.7.2 CMvcClientViewport 94
8.5.8 MFC Specifics 94
8.5.8.1 MvcViewport 95
8.5.8.2 MvcScrollView_T 95
8.5.8.3 MvcBufferedWrapper_T 95
8.6 MVC Controllers 96
8.6.1 CMvcController 96
8.6.2 MFC Specifics 99
8.7 Connecting the Model, Viewport, and Controller 100
8.7.1 CMvcComponent 101
8.7.2 ATL Specifics 102
8.8 MVC Commands and Undo/Redo 103
8.8.1 CMvcCommand 103
8.8.2 Commands as Messages 103
8.8.3 IMvcUndoRedo 103
8.8.4 MvcTransactionModel 104
8.9 MVC Principles and Practice 106
vi Contents
8.9.1 Minimize Coupling 106
8.9.2 Avoid “Positional Awareness” in the Controller 106
8.9.3 Use Interface-Based Programming Techniques 106
8.9.4 Use Commands to Define the Model’s Services 107
8.9.5 Exploit Hierarchical Decomposition 107
8.9.6 Distinguish Between Architecture and Technology 107
8.9.7 Capture the System in the Model 108
8.9.8 Use MVC as a Widget Architecture 108
8.9.9 Distinguish Between Graphical and Non-Graphical Systems 108
8.10 Using MVC in MFC Applications 111
8.10.1 Define a Model Class 111
8.10.2 Define a Controller Class 112
8.10.3 Define a Viewport Class 114
9Chapter 9 Print Package
9.1 The Print Package 117
9.2 Printable Objects 118
9.3 Print Documents 119
9.4 Printer Configurations 120
9.5 Printers 121
9.6 Print Jobs 122
9.7 Print Preview 123
9.8 Using Print Preview with ATL 124
10Chapter 10 GDI Classes
10.1 SFL Graphics 125
10.2 GDI Objects 126
10.2.1 Creation and Destruction 126
10.2.2 Lifetime Management 127
10.2.3 Examples 128
10.3 Device Contexts 130
10.3.1 Device Context Creation and Destruction 130
10.3.2 MFC Compatibility 132
11Chapter 11 String and Collection Classes
Contents vii
11.1 SFL Utility Classes 133
11.2 Enhanced String 134
11.2.1 Character Set Conversion 134
11.2.2 Casting 135
11.2.3 Formatting and Buffering 135
11.2.4 Type Definitions 136
11.3 API Structure Wrappers 137
11.4 MFC Compatibility Classes 138
12Chapter 12 Developing Applications
12.1 Overview 141
12.2 Features and Benefits 142
12.3 Basic Architecture 143
12.3.1 HelloSFL 143
12.3.2 HelloSFL’s Application 144
12.3.3 HelloSFL’s Message Loop 144
12.3.4 HelloSFL’s Main Window 147
12.4 Application Classes 150
12.4.1 CApp 150
12.4.2 CMTIApp 151
12.5 Initializer Classes 152
12.6 Windowing Classes 153
12.6.1 Container Windows 153
12.6.1.1 CContainerImplBase 153
12.6.1.2 CContainerWindowImpl 154
12.6.1.3 CContainerDialogImpl 154
12.6.2 Frame Windows 154
12.6.3 Client Windows 156
12.6.4 MDI Support 157
12.6.4.1 CMDIChildImpl 157
12.6.4.2 CMDIClientWindow 158
12.6.4.3 CMDIFrame 158
12.6.4.4 CMDIFrameImpl 158
12.6.5 Common Dialogs 159
12.6.5.1 COpenFileDialog and CSaveAsFileDialog 159
12.6.5.2 CFontDialog 160
12.6.5.3 CColorDialog 161
12.6.5.4 CFindDialog and CReplaceDialog 161
12.7 User Interface Updating 163
12.7.1 User Interface Updating Essentials 163
viii Contents
13Chapter 13 The AppWizard
13.1 Overview 169
13.2 Conclusion 172
14Chapter 14 Persistence Framework
14.1 Persistence and Property Bags 173
14.1.1 COM Property Bags 173
14.1.2 Persistable Objects 174
14.2 SFL Property Bags 175
14.2.1 Data Types 175
14.2.2 IPersistenceStrategy Interface 176
14.2.3 Registry Property Bag 176
14.2.4 XML Property Bag 176
14.2.5 Examples 177
14.3 Using Property Bags in C++ Code 180
14.3.1 MVC Integration 181
15Chapter 15 XML Serialization Architecture
15.1 Overview 185
15.1.1 Usage Example 185
15.2 Architecture Classes 187
15.2.1 The XML Document Adapter class 187
15.2.2 SECXMLArchive 187
15.2.2.1 Attributes 188
15.2.2.2 Insertion Operations 188
15.2.2.3 Extraction Operations 188
15.2.2.4 Serialize Variant 189
15.2.2.5 Hierarchical nesting support 189
15.2.3 IXMLSerialize 189
15.3 XML Formatters 191
15.3.1 Built-in Formatters 191
15.3.1.1 The XML Formatter Factory 191
15.3.1.2 Collection Class Formatters 192
15.3.1.3 Other MFC types Formatters 193
15.3.2 Creating Custom Formatters 194
15.3.3 XML Serialization Support in Objective Grid and Objective Chart 194
15.4 Base64 and Quoted-Printable Encoding Classes 195
15.4.1 Content Transfer Encoding 195
Contents ix
15.4.2 Base64 195
15.4.3 Quoted-Printable 195
15.4.4 SFL Content-Transfer-Encoding Classes 195
15.4.5 Class Hierarchy 196
15.4.5.1 SECCTEBase 196
15.4.5.2 SECBase64Encoder 196
15.4.5.3 SECQPEncoder 196
15.4.6 Usage 196
15.4.6.1 Non-Streaming Mode 196
15.4.6.2 Streaming Mode 197
15.5 XML Framework Tutorial 198
15.5.1 The starter application 198
15.5.1.1 The starter application classes 198
15.5.2 Modifying application data classes 199
15.5.3 Adding SFL XML Support 199
15.5.3.1 stdafx.h 199
15.5.3.2 Resource includes 200
15.5.4 XML-enabling the document class 201
15.5.4.1 Using the SECXMLDocAdapter_T wrapper class 201
15.5.4.2 Modifying the base application 202
15.5.4.3 Adding menu commands 202
15.5.4.4 Menu command handlers 203
15.5.5 Creating XML formatters 203
15.5.5.1 The CXShape base class formatter 203
15.5.5.2 Implementing CXShape::XMLSerialize() 204
15.5.5.3 Creating formatters for derived CXShape classes 205
15.5.5.4 The CXDiagram formatter 206
15.5.5.5 Implementation of CXDiagramFMT::XMLSerialize() 206
15.5.6 Finishing up 207
Index
x Contents
209
Contents xi
xii Contents
Chapter 1
Introduction to Stingray Foundation
Library
1.1
Welcome to Stingray Foundation Library
Stingray Foundation Library (SFL) is a framework for developing Windows applications and components in Visual Studio. SFL provides a wide range of services that are useful for developing both
MFC and ATL programs—such as layout management, model-view-controller framework, printing and preview, OLE drag and drop, a property and event architecture, and application and
windowing classes. SFL consists of loosely coupled packages, many of which have no dependencies on either MFC or ATL, so you can use them in either framework. The modular nature makes it
an ideal foundation upon which to build components and applications. For example, the Stingray
Foundation Library products build upon the architectural framework provided by the SFL.
SFL is useful to developers of components and applications in Visual Studio. Individual packages
such as the Layout Manager, model-view-controller, property and event model, and design patterns can be used in conjunction with either MFC or ATL. SFL extends ATL with application and
windowing classes, so it can be used to develop entire applications. SFL provides an AppWizard so
you can quickly get started writing SFL applications. SFL provides a package that encapsulates the
Windows GDI; ATL does not provide an equivalent set of classes. The CString, CMap, CList, and
CArray classes emulate the equivalent MFC classes and are implemented using the Standard C++
Library. SFL provides an excellent foundation for Visual Studio developers who are using ATL,
MFC, or both. It helps to smooth out some of the differences between the two frameworks and provides an architectural foundation for building components and applications.
The implementation of SFL is based on these design principles:
пЃµ
Loose coupling and modularity. SFL comprises many loosely coupled packages.
An important design objective is to make each package as independent as possible
from other packages. This low coupling results in a modular design that makes
individual packages more reusable.
Chapter 1 Introduction to Stingray Foundation Library 1
пЃµ
Interface-based programming. Interface-based programming is also a key design
principle that is applied to SFL. Interfaces are more reusable than concrete classes,
so SFL separates interface from implementation whenever possible. Template
classes are used to provide generic implementations of those interfaces.
пЃµ
Interoperability between ATL and MFC. Isolation of framework dependencies
provides a clean separation between code that is framework-dependent and code
that is framework-neutral. Framework-dependent code generally extends the
framework-neutral code to provide MFC and ATL specific implementations. MFC
compatible classes and structures such as the GDI classes, API structure wrappers,
string classes, and collection classes allow the same code to be used with or without
MFC.
SFL leverages Rogue Wave’s expertise in developing C++|MFC class libraries and components for
Windows. It builds upon a solid architectural foundation based on design patterns and provides
services that are strategic for building components and applications. SFL’s application development package and AppWizard makes ATL into an outstanding application development
environment, which benefits from other SFL packages and components. SFL provides an excellent
foundation for Visual C++ development.
1.2
Product Features
The following table lists the major features in SFL and the environments in which they are supported. Features that overlap with MFC, such as the GDI, string, and collection classes, are
interchangeable with MFC and are not marked as MFC features even though they can be used in
conjunction with MFC. Remember that MFC can be used in ATL and vice versa, so the platform
distinctions in the table do not present any obstacle to using the features.
Table 1 – SFL features and their supported environments
2
Feature
ATL
MFC
Application and windowing classes
X
X
MDI, SDI, and multi-threaded SDI
X
X
Common dialog classes
X
X
OLE Drag-and-Drop
X
GDI classes
X
X
String and collection classes
X
X
Model-View-Controller framework
X
X
Layout Manager
X
X
X
Property and event architecture
X
X
X
Printing and print preview
X
X
X
XML persistence
X
X
X
X
Win32
X
Table 1 – SFL features and their supported environments (Continued)
Feature
ATL
MFC
Win32
Design patterns classes
X
X
X
Image classes (DIB, JPEG)
X
Owner draw and bitmap buttons
X
Color well controls
X
AppWizard
1.3
X
X
Location of Samples
If you want to examine or run any of the samples described in this book, you can download the
Stingray sample bundle from the Knowledge Base on the Rogue Wave Web site, as described in
Section 3.6.1, “Location of Sample Code,” in the Stingray Studio Getting Started Guide. This bundle
contains all of the samples for the Stingray Foundation Library, and additional samples for the
other Stingray products.
1.4
Supported Platforms
For a list of supported operating systems and compilers, see
http://www.roguewave.com/products/stingray.aspx, then click on the link “Supported Platforms” to download a PDF.
Chapter 1 Introduction to Stingray Foundation Library 3
1.5
Getting Help
Several avenues of help are available to you when working with Stingray Foundation Library.
1.5.1 Documentation
Documentation is located in the Docs subdirectory of your Stingray installation directory. The following documents are available:
пЃµ
User's Guide - This manual. The User's Guide is a how-to manual that provides an
introduction to Stingray Foundation Library and provides a foundation for using
Stingray Foundation Library “out-of-the-box.” There are several tutorials included
to help new Stingray Foundation Library users learn how to create Stingray
Foundation Library applications quickly. It assumes that you are familiar with
Visual C++ and the Microsoft Foundation Classes (MFC). This document is
available in two formats: HTML Help (sflug.chm) and Portable Document Format
(sflug.pdf).
пЃµ
Reference Guide - The reference document (sflref.chm) is a detailed description
of the classes and methods in Stingray Foundation Library.
пЃµ
ReadMe file - A basic description of the product and how to build it,
FoundationReadme.htm located in your Stingray installation directory.
Samples - Located in the Samples subdirectory of your Stingray Foundation Library installation
directory.
For more information on the documentation, including all Stingray documentation, an index to the
Help files, and document type conventions, see Section 1.4, “Product Documentation,” in the Stingray Studio Getting Started Guide.
1.5.2 Knowledge Base
The Rogue Wave Knowledge Base contains a large body of useful information created by the Support Services team. This information is available to any user of the Rogue Wave Web site, and no
login or registration is required.
http://www.roguewave.com/support/knowledge-base.aspx.
1.5.3 Professional Services
The Rogue Wave Professional Services offers training and mentoring for all levels of project development, from analysis and design to implementation. For more information, see Section 1.5,
“Professional Services,” in the Stingray Studio Getting Started Guide.
4
1.5.4 Technical Support
Technical support for Stingray Foundation Library products is provided through the Rogue Wave
Web site. For more information on registering with the support system, and the type of support
you may receive, see Section 1.6, “Technical Support,” in the Stingray Studio Getting Started Guide.
1.6
Licensing Restrictions
Please read the license agreement that was shipped with this package. You are bound by the licensing restrictions contained in that document. Do not use this product unless you can accept all the
terms of the license agreement.
You can use all the files accompanying this product for development of an application. You can distribute the Stingray Foundation Library Dynamic Link Libraries (DLLs) according to the terms of
the license agreement.
Your applications can also statically link to Stingray Foundation Library, in which case you do not
need to redistribute any Stingray Foundation Library files—except any required language configuration files.
Chapter 1 Introduction to Stingray Foundation Library 5
6
Chapter 2
Getting Started
2.1
Building the SFL Libraries
Before you begin using the Stingray Foundation Library, you must build one or more configurations of the library. You can build the SFL library as a static library or as a DLL. You can also build it
with or without support for the Microsoft Foundation Library. If you are developing ATL-based
components or applications, you may want to build the library without MFC support to reduce the
size of your .exe or .dll. You can build SFL to support either the Unicode or ASCII character sets.
Each configuration can be built with debug information or in release mode. The various configuration options result in twenty combinations for the build.
You can obtain prebuilt versions of the libraries by request to Rogue Wave technical support.
Libraries are available for Windows XP and Vista with the currently supported compilers. We recommend, however, that you build the libraries yourself. The prebuilt libraries are built with a
particular instance of Visual Studio and the Windows operating system. Building the libraries
yourself ensures that they are compatible with your version of the compiler and the operating system they are built on.
2.1.1 SFL Library Naming Conventions
A standard naming convention applies to each SFL build. Once you are familiar with the convention, you can determine the features of each SFL build by its name. This convention, shown in
Figure 1, is a factory-supplied default. You can change the library names with the Build Wizard,
which is described later in this section.
Chapter 2 Getting Started 7
Figure 1 – Naming convention for SFL library
S
F
L
#
#
w
a
s
u
d
Debug Build
Unicode Build
Stingray DLL
MFC DLL
(AFXDLL)
Win32 library (no
MFC
dependencies)
SFL Product
Version
Number
Library Name
8
2.2
SFL Build Configurations
Table 2 lists each library configuration and the default library name for each configuration, where
<ver> stands for the current product version number.
Table 2 – SFL build configurations
Win32 Configuration Name
Default Library Name
Win32 Lib Debug
sfl<ver>wd
Win32 Lib Release
sfl<ver>w
Win32 Dll Debug
sfl<ver>wsd
Win32 Dll Release
sfl<ver>ws
Win32 Lib Unicode Debug
sfl<ver>wud
Win32 Lib Unicode Release
sfl<ver>wu
Win32 Dll Unicode Debug
sfl<ver>wsud
Win32 Dll Unicode Release
sfl<ver>wsu
Win32 Lib MFC Lib Debug
sfl<ver>d
Win32 Lib MFC Lib Release
sfl<ver>
Win32 Lib MFC Dll Debug
sfl<ver>ad
Win32 Lib MFC Dll Release
sfl<ver>a
Win32 Dll MFC Dll Debug
sfl<ver>asd
Win32 Dll MFC Dll Release
sfl<ver>as
Win32 Lib MFC Lib Unicode Debug
sfl<ver>ud
Win32 Lib MFC Lib Unicode Release
sfl<ver>u
Win32 Lib MFC Dll Unicode Debug
sfl<ver>aud
Win32 Lib MFC Dll Unicode Release
sfl<ver>au
Win32 Dll MFC Dll Unicode Debug
sfl<ver>asud
Win32 Dll MFC Dll Unicode Release
sfl<ver>asu
Win32 All Ascii
N/A
Win32 All Unicode
N/A
Win32 All
N/A
Chapter 2 Getting Started 9
2.3
Visual Studio Environment Setup for SFL
For Visual Studio 2005 or 2008, check the Visual Studio VC++ Directories for both Win32 and x64
settings to ensure that the Include, Source, Library and Executable paths contain the appropriate
Stingray Studio include, source, library and executable directory paths. Please refer to Section 2.7.3,
“Check Visual Studio Paths,” of the Stingray Studio Getting Started Guide for details.
For Visual Studio 2010, you need to set paths in each project. Please see Section 2.7.4, "Microsoft
Visual Studio 2010 Changes," of the Stingray Studio Getting Started Guide for information on how to
add Stingray-specific property sheet(s) with Stingray Studio paths.
2.4
Building SFL Libraries with the Visual Studio
Solution
SFL includes a Visual Studio solution for building each configuration of the library. The solution is
located in the Install\SRC directory and is named Foundation<ver>.sln, where <ver> is the
Microsoft Visual Studio version. The solution contains a single project with every build configuration for SFL. Complete the following procedure to build one or more configurations of the library.
1. Start Microsoft Visual Studio.
2. For Visual Studio 2008, select Tools | Options | Project and Solutions | VC++ Directories. Verify that the include directories from your Stingray products installation location
appear in the list for include files. If not, add them now.
For Visual Studio 2010 and 2012, open the project and go to Project | Properties | Configuration Properties | VC++ Directories.
Figure 2 – SFL Include Options
10
3. Open the Foundation<ver>.sln solution in the Install\SRC directory.
4. From the Build menu in Visual Studio, select Set Active Configuration and then choose the
build configuration that suits your needs. By default, Win32 All is selected, which will
build every combination of the SFL library. The Unicode configuration builds every library
that support Unicode. All of the other configurations build one specific library. MFCLIB
indicates that MFC will be linked statically and MFCDLL indicates that MFC will be used
as a DLL. If neither MFCLIB nor MFCDLL is specified in the build configuration, it is a nonMFC configuration of the library.
Figure 3 – SFL Configuration Manager
5. From the Build menu in Visual Studio, select Build Foundation## to build the selected
library. Visual Studio builds the selected libraries and copies them into the Install\LIB
directory
Figure 4 – SFL Build Menu
Chapter 2 Getting Started 11
2.4.1 Make Files and Building Directly with nmake
When you build the Stingray libraries in Visual Studio, Visual Studio invokes make files that ship
with the product. For information on these underlying make files, and how to build the libraries by
invoking nmake on these files directly, see Section 2.3, “Building from the Command Line with
nmake,” in the Stingray Studio Getting Started Guide.
This section also discusses the issue of building the libraries with 1-byte structure alignment rather
than the default 8-byte structure alignment.
2.4.2 Cleaning SFL Build Targets
The intermediate object files that are produced when you build the SFL libraries can appropriate
significant disk space on your computer. After building the libraries, we recommend that you
delete these files to reclaim the space on your hard drive.
The location for all generated object files is
<StingrayInstallDir>\Src\objs\<compiler_version>\<architecture>\<product_abbrv>+
<product_version>+<build_configuration_abbrv>.
For example, C:\Program Files\Rogue Wave\Stingray Studio
10.4\Src\objs\vc10\x86\sfl504asd\*.obj.
2.5
SFL Build Wizard
The Foundation##.vcproj file in the Foundation<ver>.sln solution invokes NMAKE with a different target name for each configuration. The makefile passed to NMAKE is called Foundation.mak.
Foundation.mak is generated by the Stingray Build Wizard utility. The Build Wizard is a makefile
generator that allows you to select various library build options and to create a makefile. Running
the Build Wizard is optional, because a default makefile is installed with SFL. You need to run the
Build Wizard only to customize library names or exclude features from the library.
You can run the Build Wizard from the Windows Start menu by selecting All Programs | Rogue
Wave | Stingray Studio <Version> |Stingray Foundation Library | Foundation Build Wizard. It
can also be run by double-clicking the FoundationBuildWiz.exe program in your
<InstallDir>\Utils directory.
Please refer to Section 2.2, “Build Wizard,” in the Stingray Studio Getting Started Guide for more
information.
12
Chapter 3
Interface-Based Programming
3.1
Introduction
Interface-based programming is a popular and convenient technique frequently used in object-oriented software development. An interface is a collection of pure-virtual or abstract functions that
provide related functionality. An interface has no implementation and no data members. An interface defines a contract or protocol between the user of the interface and objects that implement the
interface. Interfaces make a design more flexible because they reduce coupling between client code
and an object's implementation. The same client code can manipulate objects that are completely
unrelated in the class hierarchy, as long as the objects provide the client with an interface it
understands.
Interface-based programming is the cornerstone of Microsoft's Component Object Model (COM).
Everything in COM is an interface. All COM interfaces are derived from the root interface
IUnknown, which provides the basic services required by all interfaces. Lifetime management is
the first service required by COM interfaces, and is provided through the IUnknown functions
AddRef() and Release(). Run-time discovery of interfaces is the other service required by COM
interfaces, and is provided through the IUnknown function QueryInterface(). The
QueryInterface() function is used to interrogate an object for another interface.
QueryInterface() takes a Globally Unique Identifier (GUID) and returns a pointer to an interface.
It is similar to the dynamic_cast operator in C++, although it is more flexible and efficient.
The same techniques that are used by COM are also useful in standard C++ programming. To do
interface-based programming in C++, the same basic services — reference counting and run-time
interface discovery — are required. It is possible to use IUnknown to provide these services for
C++ interfaces, but that results in some confusion if the C++ interfaces and the classes that implement them don’t follow COM conventions. One such convention is that all functions in a COM
interface must have a return type of HRESULT. This is an important convention to follow if your
application uses remote Distributed Component Object Model (DCOM) objects, but it is not a convenient notation for local C++ objects. It is also inconvenient and inefficient to create your C++
objects with CoCreateInstance(). Mixing C++ objects and COM objects in the same code can be
confusing and potentially dangerous. You need a mechanism similar to IUnknown for C++ objects
that doesn’t interfere with COM.
Chapter 3 Interface-Based Programming 13
3.2
IQueryGuid and guid_cast
One way to create functionality similar to QueryInterface() is to use the C++ dynamic_cast()
operator. This is an effective solution if your compiler supports it. Enabling RTTI also introduces
some extra overhead for every class compiled. Many developers prefer to avoid enabling RTTI, or
at least want a choice in the matter. The solution used by SFL avoids the use of C++ RTTI by introducing an interface that provides a function very similar to IUnknown’s QueryInterface(). The
IQueryGuid interface has a single method, QueryGuid() which allows the caller to pass in a GUID
and get back a pointer to an interface or class. It is exactly like QueryInterface(), except that
QueryGuid() is more generic. QueryInterface() is only meant to get back pointers to interfaces.
QueryGuid() acts as a substitute for dynamic_cast, so it is perfectly acceptable to associate a GUID
with a concrete class and then use QueryGuid() to cast pointers to that concrete class. Example 1
shows how IQueryGuid is defined.
Example 1 – Defining IQueryGuid
class IQueryGuid
{
public:
virtual bool QueryGuid(REFGUID guid,void **ppvObj)=0;
};
QueryGuid() is similar to QueryInterface() with a couple of notable exceptions. First,
QueryGuid() returns TRUE if the interface is supported by the object and FALSE if it fails. The most
important difference is that QueryGuid() does not make any assumptions about reference counting. Although QueryInterface() always increments the reference count on an interface before
returning it to the caller, QueryGuid() does not. This is because IQueryGuid does not have any ref-
erence counting methods. It is perfectly valid to use IQueryGuid for casting interfaces and classes
that do not support reference counting.
Example 2 shows a class that implements IQueryGuid.
Example 2 – Implementing IQueryGuid
class __declspec(uuid("81CEDD2C-B2F0-4702-AA2F-D912497F5F33"))
IAnimal : public IQueryGuid
{
public:
virtual void Eat() = 0;
virtual void Sleep() = 0;
virtual void Reproduce() = 0;
};
class CCow : public IAnimal
{
public:
virtual bool QueryGuid(REFGUID guid,void **ppvObj)
{
*ppvObj = NULL;
if (guid == __uuidof(IAnimal))
*ppvObj = static_cast<IAnimal*>(this);
else if (guid == __uuidof(IQueryGuid))
*ppvObj = static_cast<IQueryGuid*>(this);
return (*ppvObj != NULL);
}
14
virtual void Eat()
{
// chew some grass
}
virtual void Sleep()
{
// sleep standing up?
}
virtual void Reproduce()
{
// not a pretty sight
}
};
Notice that declspec uuid is used to associate a GUID with the IAnimal interface. The __uuidof operator can then
be used to return the GUID for IAnimal in the implementation of QueryGuid().
The guid_cast template function makes QueryGuid() type safe, so it is more like dynamic_cast
and easier to use. It acts like dynamic_cast and is implemented by calling QueryGuid().
Example 3 shows how guid_cast is used.
Example 3 – Using the guid_cast template function
void MakeAnimalEat(IQueryGuid* pObj)
{
IAnimal* pAnimal = guid_cast<IAnimal*>(pObj);
if (pAnimal)
pAnimal->Eat();
}
Chapter 3 Interface-Based Programming 15
3.3
GUID Maps
Implementing QueryGuid() is a tedious and repetitive task, so using macros to write most of the
code is convenient. A GUID map is a set of macros that collectively implement the QueryGuid()
function. They are nearly identical to ATL’s BEGIN_COM_MAP and END_COM_MAP macros, which
implement QueryInterface(). Example 4 shows how you can use GUID maps to implement
QueryGuid().
Example 4 – Using a GUID map
class CCow : public IAnimal
{
public:
BEGIN_GUID_MAP(CCow)
GUID_ENTRY(IAnimal)
GUID_ENTRY(IQueryGuid)
END_GUID_MAP
…
};
The macros expand out to the same implementation of CCow::QueryGuid() shown in the previous
sample.
In certain cases, a class may inherit an interface from more than one base class. That will produce
an ambiguous reference in the GUID map that you must resolve with the GUID_ENTRY2 macro.
Example 5 shows the GUID_ENTRY2 macro used to resolve an ambiguous reference to IQueryGuid.
Example 5 – Resolving an ambiguous reference in a GUID map
class IFood : public IQueryGuid
{
public:
virtual void BeConsumed() = 0;
};
class CCow : public IAnimal, public IFood
{
public:
BEGIN_GUID_MAP(CCow)
GUID_ENTRY(IAnimal)
GUID_ENTRY2(IQueryGuid, IAnimal)
END_GUID_MAP
…
};
16
3.4
Reference Counting
Reference counting is another service that is useful in interface-based programming. COM requires
that all interfaces are reference counted, but for our C++ interfaces reference counting is optional.
The interface IRefCount defines the AddRef() and Release() methods needed to perform reference counting. The signatures of AddRef() and Release() in IRefCount are identical to the
signatures in IUnknown, which makes it possible to mix IRefCount into classes that implement
IUnknown so they can share the same reference counting implementation. In other words,
IRefCount integrates seamlessly with IUnknown. Accordingly, smart pointer classes written to
work with IUnknown, such as ATL’s CComPtr, work for IRefCount-based classes.
SFL provides a default implementation of IRefCount that you can mix into a concrete class. The
CRefCountImpl is a template class that takes the base class as a template parameter and implements reference counting. Remember that the implementation of reference counting provided by
CRefCountImpl is not thread-safe — it does not use the InterlockedIncrement() and
InterlockedDecrement() functions.
If your classes need to be thread safe, do not use CRefCountImpl.
Example 6 modifies our cow so that it supports reference counting.
Example 6 – Adding support for reference counting
class IAnimal : public IQueryGuid, public IRefCount
{
public:
virtual void Eat() = 0;
virtual void Sleep() = 0;
virtual void Reproduce() = 0;
};
class IFood : public IQueryGuid, public IRefCount
{
public:
virtual void BeConsumed() = 0;
};
class CCow : public CRefCountImpl<IAnimal>, public IFood
{
public:
BEGIN_GUID_MAP(CCow)
GUID_ENTRY(IAnimal)
GUID_ENTRY2(IQueryGuid, IAnimal)
GUID_ENTRY2(IRefCount, IAnimal)
END_GUID_MAP
…
};
Chapter 3 Interface-Based Programming 17
18
Chapter 4
Design Patterns
4.1
Introduction
A design pattern is a solution to a problem or class of problems that can be reused over and over.
Problems that are similar in nature frequently exhibit recognizable patterns. Experienced software
designers learn to recognize these patterns and are able to draw on past experience to reuse old
designs to solve new problems. The book Design Patterns: Elements of Reusable Object-Oriented Software (by Gamma et al.) identifies and documents many common design patterns and has become a
classic software engineering textbook. The SFL Patterns package provides support for several commonly used design patterns.
Chapter 4 Design Patterns 19
4.2
The Subject-Observer Pattern
The subject-observer pattern defines a one-to-many dependency between objects, so that when one
object changes state, all its dependents are notified and updated automatically. In the subjectobserver relationship, a subject encapsulates related data and functionality that an observer monitors. If the state of the subject changes, the observer needs to know about it. To accomplish this, the
subject defines a notification dictionary that is the set of all notifications of change a subject may
broadcast. A notification is any class that implements the IMessage interface. It is the responsibility
of the subject to define a notification dictionary and to broadcast individual notifications of change
to its list of observers. It is the responsibility of the observer to subscribe to the notifications sent by
a subject and to understand and react to the notifications it receives.
A subject is any class that implements the ISubject interface, shown in Example 7.
Example 7 – The ISubject interface
class ISubject : public IQueryGuid, public IRefCount
{
public:
virtual void AddObserver(IObserver* pObserver) = 0;
virtual void RemoveObserver(IObserver* pObserver) = 0;
virtual void UpdateAllObservers(IObserver* pObserver,
IMessage* pMsg) = 0;
};
The AddObserver() method allows observers to subscribe to notifications sent by the subject. The
RemoveObserver() method removes a particular observer from the subject. The
UpdateAllObservers() method sends a notification message to all observers.
A notification message is any class that implements the IMessage interface, shown in Example 8.
Example 8 – The IMessage interface
class IMessage : public IQueryGuid, public IRefCount
{
public:
virtual unsigned int GetTypeID() const = 0;
virtual void Sprint(CString& strCmd) = 0;
};
Each message type is associated with an integer identifier, which can be used by observers to identify the message given an IMessage pointer. It is also possible to use QueryGuid() or the guid_cast
operator to cast an IMessage pointer to another type.
The IObserver interface, shown in Example 9, is implemented by classes that need to observe
subjects.
Example 9 – The IObserver interface
class IObserver : public IQueryGuid, public IRefCount
{
public:
virtual void OnUpdate(ISubject* pSubject, IMessage* pMsg) = 0;
};
20
The subject invokes the OnUpdate() method to send notification messages to observers. Subjects
typically implement the UpdateAllObservers() method by iterating over each observer and calling their OnUpdate() method.
An object can implement both the ISubject and IObserver interfaces, producing an object that
observes one object and acts as a subject for others. This makes it possible to chain together or nest
subjects and observers.
Chapter 4 Design Patterns 21
4.3
The Composite Pattern
The composite pattern composes objects into tree structures to represent part-whole hierarchies. The
composite pattern lets client code treat individual objects and compositions of objects uniformly.
For example, a composite shape is made up of several individual shapes such as rectangles and
ellipses. The composite pattern allows simple shapes and complex shapes to be handled the same
way.
The CComposite template class provides an implementation of the composite pattern. It maintains
a list of child objects that are accessed through methods such as AddChild(), RemoveChild(), and
GetChildrenCount(). In addition to having a list of children, each composite object maintains a
pointer to its parent. The declaration of this templated class is shown in Example 10
Example 10 – CComposite class declaration
template <typename _Component, const GUID* _guid>
class CComposite:
public IQueryGuid
{
<…>
};
The first parameter passed into the CComposite template is the component type, which determines
the type of parent and child objects in the composite. Objects in the tree are accessed using that
type. For example, the declaration of the GetParent() method returns a pointer to a _Component
object, not a CComposite<> object:
_Component* GetParent() const;
The second parameter in the template is a GUID that will identify the composite interface within
the set of interfaces implemented by the component classes. The composite implementation does
not assume an inheritance relationship between the CComposite<> class and the _Component
class. Rather than an implicit conversion, casting from one to the other is performed using the
guid_cast<> mechanism, standard in SFL. Whenever the CComposite<> interface is needed in
some operation, a guid_cast<> is performed using the GUID passed in the second template parameter. Derived classes are responsible for providing an adequate interface map that allows this
guid_cast<> call to succeed. All classes that mix in the CComposite<> template among their base
classes are indirectly deriving from IQueryGuid, as seen earlier in Example 10.
Example 11 uses the CComposite class to compose complex shapes from simple shapes. The sample defines an entire class hierarchy that mixes in the composite pattern for all of its classes.
Example 11 – Composing complex shapes
// Abstract base class for all shapes
class __declspec(uuid("ABDC16B0-5195-11d3-4D94-00C06F92F286")) Shape
{
public:
virtual Draw(CDC* pDC) = 0;
};
// Define a GUID to provide a way to downcast any shape to a
// composite shape
class __declspec(uuid("ABDC16B1-5195-11d3-4D94-00C06F92F286"))
CompositeShape;
22
// Default implementation for composite shapes
class CompositeShapeBase :
public Shape,
public CComposite<Shape, __uuidof(CompositeShape)>
{
public:
virtual Draw(CDC* pDC)
{
// Iterate over list of contained shapes
// using the CComposite<> interface facilities
. . .
// Draw each child shape
. . .
}
BEGIN_GUID_MAP(CompositeShapeBase)
GUID_ENTRY_IID(__uuidof(CompositeShape), _compositeBase)
GUID_ENTRY_IID(__uuidof(Shape), _compositeBase)
END_GUID_MAP()
};
// A normal composite shape
class CompositeShapeNormal : public CompositeShapeBase
{
public:
virtual Draw(CDC* pDC)
{
// Draw shapes front to back
}
};
// A reverse Z-order composite shape
class CompositeShapeRev : public CompositeShapeBase
{
public:
virtual Draw(CDC* pDC)
{
// Reverse order and draw shapes back to front
}
};
// Simple shapes
class Polygon : public Shape
{
public:
virtual Draw(CDC* pDC)
{
// Draw a polygon
. . .
}
BEGIN_GUID_MAP(ShapeImpl)
GUID_ENTRY_IID(__uuidof(Shape), _compositeBase)
END_GUID_MAP()
};
Chapter 4 Design Patterns 23
class Rectangle : public Shape
{
public:
virtual Draw(CDC* pDC)
{
// Draw a rectangle
}
BEGIN_GUID_MAP(ShapeImpl)
GUID_ENTRY_IID(__uuidof(Shape), _compositeBase)
END_GUID_MAP()
};
Example 12 sets up a composite tree with three descendents, one of which is, in turn, a composite.
Example 12 – Setting up a composite tree
typedef CComposite<Shape, __uuidof(CompositeShape)> CompositeShape;
Shape* pRootShape = new CompositeShapeNormal;
CompositeShape* pComposite = guid_cast<CompositeShape>(pRootShape);
pComposite->AddChild(new Rectangle);
pComposite->AddChild(new Polygon);
Shape* pSubShape = new CompositeShapeRev;
CompositeShape* pSubComposite =
guid_cast<CompositeShape>(pSubShape);
pSubComposite->AddChild(new Rectangle);
pComposite->AddChild(pSubComposite);
CComposite<> does not make any assumptions about the allocation of the children, and therefore does not deallocate them upon destruction of the object.
In Example 12, then, the objects allocated using the new() operator should be deallocated by some
external agent before the root of the composite gets destroyed. Otherwise, a memory leak occurs.
24
4.4
The Object Factory Pattern
In general, it is a good programming practice to decouple the object creation and destruction processes from the actual usage of the object. On one hand, it facilitates the application of interfaceoriented programming practices. Under this paradigm the objects are supposed to be manipulated
only through their interfaces. However, there is one place in the program where the real type of the
object needs to be known, and that is when a new instance needs to be created. Interface-based programming can greatly benefit if that unique place can be isolated from the rest of the program.
The other benefit of decoupling an object’s creation process from its usage is that it offers a greater
degree of freedom in the memory allocation of the object, that is, the location in memory where the
object will reside and the mechanism followed to release that memory upon destruction of the
object.
The object factory pattern offers a reusable mechanism to manage the creation and release of
objects. Particularly, it addresses the two main issues stated above. SFL offers an implementation of
the object factory in the <Patterns\Factory.h> header file, located in your Foundation include
directory. This implementation is centered around the class CObjectFactory. The declaration of this
class is presented in Example 13.
Example 13 – CObjectFactory class declaration
template <typename _Base, typename _Derived,
typename _A = std::allocator<_Derived> >
class CObjectFactory:
public CObjectFactoryBase<_Base>
The template parameter _Base is the type of the object being returned by the factory. The second
template parameter, _Derived, specifies the actual type of the object being created. _Base and
_Derived are not necessarily the same type (although it is possible), but they are assumed to be
related by inheritance: a pointer to _Derived must be implicitly convertible to a pointer to _Base.
The third template parameter specifies an allocation strategy. The default strategy used is the standard C++ allocator, which internally uses the standard C memory allocation routines. However,
other strategies with more complex allocation algorithms can be plugged into the factory using this
parameter.
CObjectFactoryBase<> declares the interface of the factory. The two methods declared on this
interface, and implemented by the object factory are:
virtual _Base* CreateObject() const = 0;
virtual void DestroyObject(_Base* pObject) const = 0;
This base class is only templated by _Base, the type of the interface being returned. This allows you
to give polymorphic treatment to a set of object factories that have only that element in common,
but differ in the type of the actual object being instantiated or in the allocation scheme.
Creation of a new instance is achieved by calling the CreateObject() method in a
CObjectFactory. The allocator functions are used to reserve the memory and initialize the object,
and the functions return a pointer to the desired interface on the object.
Always call DestroyObject() on the same factory class that created the object. Otherwise, the deallocation process
might not correspond to the allocation, and would cause unexpected errors in a program.
Chapter 4 Design Patterns 25
4.4.1 Example
Consider the following code.
Example 14 – Implementing factory interfaces
interface IElementBase {
<...>
};
class CElemImpl1: public IElementBase {
<...>
};
class CElemImpl2: public IElementBase {
<...>
};
typedef CObjectFactoryBase<IElementBase> ElementFactory;
typedef CObjectFactory<IElementBase, CElemImpl1> Impl1Factory;
typedef CObjectFactory<IElementBase, CElemImpl2> Impl2Factory;
IElementBase* CreateElement(ElementFactory& factory)
{
return factory.CreateObject();
}
In the sample above, an interface is declared and two implementations of that interface are provided. The creation of the objects is centralized on the CreateElement() routine, regardless of the
actual implementation.
IElementBase* p1 = CreateElement(Impl1Factory);
IElementBase* p2 = CreateElement(Impl2Factory);
The same creation mechanism could be employed if we declare a new factory with a different allocation strategy:
Example 15 – Creating objects with an object factory
IElementBase* pShared = CreateElement(Impl1OnSharedMemoryFactory);
To destroy the instances, use the same factory class used to create them:
Impl1Factory factory1;
factory1.DestroyObject(p1);
26
4.5
Polymorphic Iteration
The iterator is a well-known and useful design pattern for simplifying access to elements in a collection without exposing the collection’s underlying representation. If you’ve used STL, you’re
probably familiar with STL’s definition of iterators. Example 16 illustrates how STL-style iterators
are declared and used:
Example 16 – Declaring and using standard STL iterators
// Declare the vector and iterator
vector<int> v;
vector<int>::const_iterator i;
// Insert a few elements
v.push_back(1); v.push_back(2); v.push_back(3);
// Traverse and process the items
for (i = v.begin(); i != v.end(); i++) {
// process items
}
This code simply creates an array of integers and traverses the collection. Using iterators, your client code has less knowledge and dependence on the internal structure of the collection. All
collections are traversed with identical semantics, making it easier to substitute one collection type
for another. STL’s iterators are flexible and easy to use.
STL-style iterators are not polymorphic, however. To create an iterator, you must know the type of
collection. By contrast, a polymorphic iterator is a single class that can iterate over any type of collection which supports traversal. A polymorphic iterator can be created on any type of collection with
no knowledge of the collection’s type. The Stingray Foundation Library provides a powerful extension to STL’s iterator definition, which makes STL iterators polymorphic. Example 17 is equivalent
to Example 16, except that it uses SFL’s polymorphic iterators instead of STL-style iterators.
Example 17 – Declaring and using SFL polymorphic iterators
// Declare the vector and iterator
traversable < vector<int> > v;
const_iterator<int> i(&v);
// 1
// 2
// Insert a few elements
v.push_back(1); v.push_back(2); v.push_back(3);
// Traverse and process the items
for (i.begin(); !i.at_end(); i++) {
// process items
}
// 3
Polymorphic iterators are declared and used in a very similar fashion to STL standard iterators, but
with some important differences:
//1
The declaration of the vector v is wrapped with SFL’s traversable template. The traversable
template makes the collection accessible through SFL’s polymorphic iterators.
//2
The const_iterator declaration is not scoped by the collection class. Instead, the collection is passed in as a constructor argument. The const_iterator is a template, which is
parameterized only on the type of elements contained.
Chapter 4 Design Patterns 27
The members begin() and at_end() are semantically similar to their STL counterparts,
except they are defined on the iterator rather than the collection. This enables a separation
of collection type from the task of traversal.
//3
SFL’s polymorphic iteration solution can be divided into three parts:
пЃµ
The polymorphic iterator templates
пЃµ
The traversable interfaces
пЃµ
The traversable mix-in templates
4.5.1 The Polymorphic Iterator Templates
STL offers five categories of iterators:
пЃµ
Input
пЃµ
Output
пЃµ
Forward
пЃµ
Bidirectional
пЃµ
Random
The Stingray Foundation Library adds a sixth category: Polymorphic. Polymorphic iterators are
derivatives of bidirectional iterators and offer the same capabilities. They can traverse in forward
and reverse directions and allow retrieval and storage of elements. They add the ability to traverse
a collection without express knowledge of its type. Polymorphic iterators can be used with most of
the STL algorithms that accept bidirectional iterators. Note that polymorphic iterators are not randomly accessible and are not compatible with STL algorithms that require this property.
Like bidirectional iterators, polymorphic iterators come in four basic types:
пЃµ
const_iterator<>
пЃµ
iterator<>
пЃµ
const_reverse_iterator<>
пЃµ
reverse_iterator<>
These iterators are interface-compatible with their bidirectional counterparts and, for the most part,
can be used interchangeably. However, their declaration is somewhat different. All polymorphic
iterators are templatized classes whose parameter is the type of element contained in the collection
being traversed. And unlike standard STL iterators, polymorphic iterators are not nested classes
declared within a collection class. As a result, their declaration isn’t scoped by the collection class.
Finally, the collection the iterator should attach itself to is passed as a constructor argument. The
example below illustrates how a polymorphic iterator is declared:
Example 18 – Declaring a polymorphic iterator
const_iterator< element_type
> iter ( &aCollection );
or
iterator< element_type
28
> iter ( &aCollection );
STL also defines classes such as const_iterator and iterator. If you push STL and SFL into the global namespace, you
can end up with a collision.
A namespace conflict can be corrected in three independent ways:
пЃµ
Fully qualify iterator declarations. For example:
stingray::foundation::const_iterator<>)
пЃµ
Make sure your SFL using clause follows your STL using clause.
пЃµ
Add individual using clauses for SFL iterators. For example:
using stingray::foundation::const_iterator;
using stingray::foundation::iterator;
4.5.2 The Traversable Interfaces
Exchanging a collection between classes or functions usually requires an agreed-upon collection
type. For example, perhaps you need to write a display_employees() function that takes a collection of employees and writes their names to a console. With STL, you might write the function as in
Example 19.
Example 19 – Displaying a collection with standard STL iteration
void display_employees( const vector< employee >& vEmpl )
{
vector< employee >::const_iterator i;
// Traverse and process the items
for (i = vEmpl.begin(); i != vEmpl.end(); i++) {
cout << (*i).GetName();
}
}
But, what if the employees are sometimes stored in a map or a list? You would have to write this
function three or more times, only changing the collection type. You can use templated functions to
accomplish this, but they are still generating essentially the same function three or more times.
With polymorphic iteration, you can write one function to process an aggregate, regardless of the
collection type. The Stingray Foundation Library defines two interface templates for this purpose:
пЃµ
IConstTraversableT<>
пЃµ
ITraversableT<>
All polymorphic iterators take one of these interfaces as a constructor argument, and use it as a
bridge to the collection. The iterator calls the traversal interface members to move between and
access elements. Any collection that implements these interfaces becomes accessible through the
polymorphic iterators.
With IConstTraversableT<>, you can rewrite the display_employees() function once for all collection types, as shown in Example 20.
Chapter 4 Design Patterns 29
Example 20 – Displaying all collection types with SFL polymorphic iteration
void display_employees( IConstTraversableT< employee >& tEmpl )
{
const_iterator< employee > i(tEmpl);
/*** Next line doesn’t compile
iterator< employee > i(tEmpl);
**** only const iterators allowed
**** on an IConstTraversableT<> */
// Traverse and process the items
for (i.begin(); !i.at_end(); i++) {
cout << (*i).GetName();
}
}
Both IConstTraversableT<> and ITraversableT<> serve as abstract aggregate objects that can be
iterated over. The difference is that IConstTraversableT<> supports only const_iterator<> and
const_reverse_iterator<>, while ITraversableT<> supports all four polymorphic iterator types.
The power of traversable interfaces is that they allow you to exchange and use aggregates without
concern for collection type. Returning a collection is another place where this is useful, as shown in
Example 21, where a function searches for all employees contributing to a 401K plan.
Example 21 – Using aggregates without knowing collection type
ITraversableT< employee >& EmployeeServer::Get401kContributors()
{
// Perform SQL query
...
return m_tMatches;
}
Now, client code will be able to receive, traverse and process the returned set of employees, without knowing the collection type or impacting the code when the type changes.
4.5.3 The Traversable Mix-in Templates
As discussed in the previous section, any collection that implements one of the traversable interfaces is accessible through the polymorphic iterators. To ease the task of mixing in and
implementing these interfaces, SFL provides two helpers:
пЃµ
const_traversable<>
пЃµ
traversable<>
The purpose of these templates is to make any STL-compliant collection usable with the polymorphic iterators by implementing IConstTraversableT<> and ITraversableT<>, respectively. These
templates are an example of the decorator design pattern, discussed in Section 8.3.6, “Wrappers
(Decorators).” Their approach is to use multiple derivation from the collection type argument and
the appropriate traversable interface, implementing its members. Because derivation is used, the
original collection interface is preserved. So, collection declarations can be wrapped with one of the
templates without impacting existing code. Example 22 illustrates how these templates are used.
30
Example 22 – Adapting STL collections to polymorphic iteration
traversable < vector<int> > v;
or
const_traversable < deque<int> > dq;
You can manually derive and implement the traversable interfaces for your own collection classes.
Or, make your own collections STL-compliant so the traversable mix-in templates will work with
them. To be STL-compliant in this sense, your collection class must have the nested iterators
const_iterator, iterator, reverse_iterator, and const_reverse_iterator with STL’s calling
conventions.
4.5.3.1 Lifetime Management
Lifetime management is dealt with by the traversable interfaces. Both IConstTraversableT<> and
ITraversableT<> support reference counting through AddRef() and Release() members. However, STL collections (and many other collection classes) are not reference counted by default. You
can fix that by aggregating and dynamically allocating the collection, but that approach does not
preserve the collection’s interface.
The SFL solution is to declare AddRef() and Release() members in the traversable interface. If the
underlying collection supports reference counting, these members delegate. Otherwise, they do
nothing. To provide for reference-counted and non-reference-counted collections, SFL has two versions of the traversable mix-in templates:
пЃµ
const_traversable<>
пЃµ
traversable<>
пЃµ
refcounted_const_traversable<>
пЃµ
refcounted_traversable<>
The reference-counted prefixed versions handle lifetime management by delegating AddRef() and
Release() calls to the underlying collections. The non-prefixed versions define AddRef() and
Release() as no-ops; these versions should be used with STL collections.
4.5.3.2 MFC and COM Collections
The traversable interfaces can be mixed into any collection class, providing you with a uniform
method of access for all collections. Through traversable interfaces, you can more easily use collection classes from MFC, STL, COM and your own custom collections in a single project. Using the
traversable interfaces, you could even provide location transparency for a collection while still
achieving a friendly, STL-like interface.
Chapter 4 Design Patterns 31
32
Chapter 5
Properties Package
5.1
Introduction to SFL Properties
The ability to discover and access an object’s properties at run time is a feature with many applications. Writing a generic property browser is easier given a consistent interface to an object’s
properties. For example, Visual Basic is able to use the same property browser for any ActiveX control, because ActiveX controls implement ITypeInfo and IDispatch. Of course, implementing
ITypeInfo is a challenge, unless you are working with a COM object that has a type library. The
Properties package provides a simple set of interfaces and classes for doing the same type of thing
for any C++ object. It even provides an implementation of those interfaces for ActiveX controls, so
that properties for both C++ objects and ActiveX controls can be manipulated in a consistent
manner.
5.1.1 Property Objects
The IProperty interface provides a description of a given property. Each property has a LONG identifier associated with it. This is the same data type as a DISPID, which makes it easy to use the SFL
properties interface to access the properties of an ActiveX control. The symbol PropertyId is used
to declare property identifiers and is typedefed as LONG. Each property also has a name, description, data type, style flags, and an enumeration. Property values are stored and retrieved as
VARIANTs, because it is convenient and integrates with ActiveX control properties. Accordingly, the
data type for a property is described by a VARTYPE. Both MFC and ATL provide a CComVariant
class that makes it easy to work with VARIANTs. The CProperty class provides a straightforward
implementation of the IProperty interface and is sufficient for most applications.
Chapter 5 Properties Package 33
5.2
Property Containers
The IPropertyContainer interface provides an interface to an object’s properties. You can use it to
retrieve an IProperty pointer to each property supported by the object. The IProperty interface
only describes the property, it doesn’t give access to the value of the property. The
IPropertyContainer interface has methods for getting and setting the value of a given property.
пЃµ
PutPropertyValue() takes a VARIANT and sets the value of property identified by
the given property ID.
пЃµ
GetPropertyValue() returns a VARIANT given a property ID.
Example 23 demonstrates how to display each of the properties for a given property container.
Example 23 – Using a property container to display an object’s properties
void ShowProperties(IPropertyContainer* pContainer, CDC* pDC)
{
BSTR buf;
TCHAR tBuf[20];
COleVariant val;
USES_CONVERSION;
int i;
for (i=0; i < pContainer->GetPropertyCount(); i++)
{
// Get a pointer to the property at position i
// in the container
IProperty* pProp = pContainer->GetPropertyAt(i);
// Get the property ID
PropertyId propId = pProp->GetId();
pDC->TextOut(10, (i*90), _T("Property ID:"));
_itot(propId, tBuf, 10);
pDC->TextOut(150, (i*90), tBuf);
// Get the property name
pProp->GetName(buf);
pDC->TextOut(10,(i*90)+20,_T("Property Name:"));
pDC->TextOut(150,(i*90)+20,OLE2T(buf));
// Get the property description
pProp->GetDescription(buf);
pDC->TextOut(10,(i*90)+40,_T("Property Description:"));
pDC->TextOut(150, (i*90)+40, OLE2T(buf));
// Get the property value
pContainer->GetPropertyValue(propId, val);
val.ChangeType(VT_BSTR);
pDC->TextOut(10, (i*90)+60,
_T("Property Value:"));
pDC->TextOut(150, (i*90)+60, OLE2T(val.bstrVal));
}
}
34
5.2.1 A Property Container Implementation
Any C++ object can expose properties at run time by implementing the IPropertyContainer interface. However, the CPropertyContainer class provides an implementation of the
IPropertyContainer interface that is sufficient for most applications. Properties must be registered
with CPropertyContainer objects using one of several RegisterProperty() methods. The most
basic RegisterProperty() method takes an IProperty pointer. There are several variations of
RegisterProperty() that create a CProperty object using the parameters passed in and register it.
5.2.1.1 The Property Map
The CPropertyContainer class stores the properties in a map, which is keyed on the property ID.
The nested class CPropertyContainer::Map implements the property map. To avoid the overhead
of maintaining a separate map of properties for each instance of CPropertyContainer, the
CPropertyContainer class calls the virtual method GetPropertyMap() whenever it needs to access
the property map. This gives derived classes the opportunity to implement GetPropertyMap() in
an efficient manner. For example, they can return a statically declared CPropertyContainer::Map
object. The SFL_PROPERTY_MAP macro is provided to do this. The SFL_PROPERTY_MAP macro implements GetPropertyMap() as shown in Example 24.
Example 24 – Expansion of SFL_PROPERTY_MAP
virtual CPropertyContainer<_PropertyAccessor>::Map&
GetPropertyMap() const
{
static
CPropertyContainer<_PropertyAccessor>::Map propMap;
return propMap;
}
Example 25 shows a class that uses the SFL_PROPERTY_MAP macro.
Example 25 – Using the SFL_PROPERTY_MAP macro in a class definition
class CFoobar : public CPropertyContainer
{
public:
SFL_PROPERTY_MAP(CFoobar)
…
};
Classes that derive from CPropertyContainer can implement GetPropertyMap() any way they
like. The SFL_PROPERTY_MAP macro is a convenient way to do so.
5.2.1.2 Property Accessors
The CPropertyContainer class is a template that takes an accessor class as its parameter.
CPropertyContainer uses accessor objects to implement the GetPropertyValue() and
PutPropertyValue() methods it inherits from IPropertyContainer. It creates an accessor object for
each property and stores it in the property map along with the IProperty pointer. To store or
retrieve a property value, CPropertyContainer does a quick lookup in the property map to get the
Chapter 5 Properties Package 35
accessor object and then invokes either the GetValue() function or PutValue() function on the
accessor. A pointer to the derived class is passed to the accessor, so that it can invoke the appropriate get or put function. The accessor object is an essential get and put functor for the property.
The only requirement for an accessor class is that it define an embedded typedef called
_SourceClass and implement the following methods.
void GetValue(_SourceClass* pObj, VARIANT& propVal)
void PutValue(_SourceClass* pObj, const VARIANT& propVal)
Fortunately, there is a default accessor implementation called CPropertyAccessor, which is suitable
for most applications. The CPropertyAccessor uses function pointers to implement GetValue()
and PutValue(), and it uses a template parameter to define SourceClass. CPropertyAccessor
defines a set of function signatures that you must use for the get and put functions. The functions
assigned to the accessor must match one of those signatures. Example 26 shows how to create and
use an accessor for storing and retrieving a floating-point value in a class called CFoo.
Example 26 – Creating and using an accessor
class CFoo
{
protected:
float m_bar;
public:
float GetBar()
{
return m_bar;
}
void SetBar(const float bar)
{
m_bar = bar;
}
}
void UseFoo(CFoo& foo1, CFoo& foo2)
{
CPropertyAccessor<CFoo> barAccessor(&CFoo::GetBar,&CFoo::PutBar);
VARIANT val;
barAccessor.GetValue(&foo1, val);
barAccessor.PutValue(&foo2, val);
}
Property accessors allow containers to get and set values using simple accessor functions without
knowing the names of those functions and without knowing the memory address and type of the
data. The container simply needs to map the property ID to an accessor to store and retrieve values.
Class CFoo doesn’t need to perform any special functions.
36
5.2.2 Property Container Example
Example 27 shows a class that inherits CPropertyContainer and registers some properties.
Each property must have a numeric identifier associated with it. The only rule for assigning property identifiers is
that they must be unique within the container.
Example 27 – Implementing a property container
#define PROP_HORSEPOWER
#define PROP_COLOR
100
101
class CCar : public CPropertyContainer<CPropertyAccessor<CCar> >
{
public:
SFL_PROPERTY_MAP(CCar)
CCar()
{
RegisterProperty(
PROP_HORSEPOWER,
_T("HorsePower"),
_T("Horse power of the car"),
_PropertyAccessor(&CCar::GetHorsePower,
&CCar::PutHorsePower));
RegisterProperty(
PROP_COLOR,
_T("Color"),
_T("Color of the car"),
_PropertyAccessor(&CCar::GetColor,
&CCar::PutColor));
}
int GetHorsePower()
{
return nHorsePower;
}
void SetHorsePower(const int nHorsePower)
{
m_nHorsePower = nHorsePower;
}
LPCTSTR GetColor()
{
return m_color;
}
void PutColor(LPCTSTR lpszColor)
{
m_color = lpszColor;
}
Chapter 5 Properties Package 37
private:
int m_nHorsePower;
CString m_color;
}
With the exception of the SFL_PROPERTY_MAP macro and the calls to RegisterProperty() in the
constructor, this class looks and acts like any C++ class.
38
5.3
ActiveX Controls
ActiveX controls provide their own set of interfaces for accessing properties. Using those interfaces
to access the properties of an ActiveX control from C++ code is a daunting task, unless you’re a seasoned COM developer with knowledge of the IDispatch and ITypeInfo interfaces. The Properties
package simplifies access to ActiveX control properties by providing an implementation of the
IProperty and IPropertyContainer interfaces for ActiveX controls. In addition to simplifying access
to ActiveX control properties, it provides a uniform interface for accessing both C++ object properties and ActiveX control properties.
5.3.1 ActiveX Property Containers
The CAxPropertyContainer class extends a property container with support for ActiveX controls. It
is a template class that takes a base class as a parameter, where the base class is any concrete implementation of the IPropertyContainer interface. The CAxPropertyContainer class wraps an existing
property container class— CPropertyContainer by default— and extends it to support ActiveX
controls.
Each ActiveX property container is associated with a single ActiveX control. The
CAxPropertyContainer class accesses the ActiveX control through a pure virtual function called
GetAxControl(). In other words, CAxPropertyContainer is an abstract base class. Derived classes
must implement the GetAxControl() function and return the IUnknown pointer to the ActiveX
control managed by the container. The CAxPropertyContainer class contains a member function
called RegisterAxProperties(), which retrieves the properties from the control return by
GetAxControl() and registers them in the property map. Example 28 shows an ActiveX property
container based on CAxPropertyContainer.
Example 28 – ActiveX property container
class CCalendarControl : public CAxPropertyContainer
{
public:
CCalendarControl() : m_hWnd(NULL) {}
bool Create(HWND hParent)
{
bool bSuccess = false;
HRESULT hr;
AtlAxWinInit();
// Create MS calendar control
DWORD dwStyle = WS_CHILD;
CRect rcBounds(10,10,400,300);
m_hWnd = ::CreateWindow(_T("AtlAxWin"), NULL , dwStyle,
rcBounds.left, rcBounds.top,
rcBounds.Width(), rcBounds.Height(),
hParent, NULL,
_Module.GetModuleInstance(),
NULL);
if (m_hWnd)
{
IUnknown* pUnkHost;
Chapter 5 Properties Package 39
hr = AtlAxGetHost(m_hWnd, &pUnkHost);
_ASSERTE(SUCCEEDED(hr));
CComQIPtr<IAxWinHostWindow> spAxWin(pUnkHost);
_ASSERTE(spAxWin);
hr = spAxWin->CreateControl(OLESTR("MSCAL.Calendar.7"),
m_hWnd, NULL);
if (SUCCEEDED(hr))
{
// Register the properties of the ActiveX control
// in the property map
RegisterAxProperties();
::ShowWindow(m_hWnd, SW_SHOW);
bSuccess = true;
}
pUnkHost->Release();
}
return bSuccess;
}
IUnknown* GetAxControl()
{
IUnknown* pUnk = 0;
if (m_hWnd)
pUnk = (IUnknown*)::SendMessage(m_hWnd,WM_ATLGETCONTROL,0,0);
return pUnk;
}
protected:
HWND m_hWnd;
};
Notice that the Create() function calls RegisterAxProperties(), which retrieves the properties
from the ActiveX control and registers them by calling RegisterProperty() in the base class. The
base class must implement a RegisterProperty() function with the following signature.
bool RegisterProperty(IProperty* pProp);
The CPropertyContainer class implements RegisterProperty(), so you don’t need to implement
it yourself unless you use a base class other than CPropertyContainer. To create an ActiveX property container class, all you need to do is derive your class from CAxPropertyContainer,
implement the GetAxControl() method, and call RegisterAxProperties(). Most of the code in
Example 28 is devoted to creating the ActiveX control.
5.3.2 Using ActiveX Property Containers
Using an ActiveX property container is exactly like using any other property container, because
SFL hides the implementation details behind a uniform interface. The ShowProperties() sample
function shown earlier in Example 23 works against an ActiveX property container.
40
The function in Example 29 shows an example of setting the month and day properties of the calendar control. Notice that GetPropertyByName() is used to retrieve the property identifier before
calling PutPropertyValue().
Example 29 – Accessing the properties of an ActiveX control
void InitCalendar(CCalendarControl& cal, int nMonth, int nDay)
{
VARIANT val;
val.vt = VT_I4;
IProperty* pMonth = cal.GetPropertyByName(OLESTR("Month"));
if (pMonth != NULL)
{
val.intVal = nMonth;
cal.PutPropertyValue(pMonth->GetId(), val);
}
IProperty* pDay = cal.GetPropertyByName(OLESTR("Day"));
if (pDay != NULL)
{
val.intVal = nDay;
cal.PutPropertyValue(pDay->GetId(), val);
}
}
The function below passes a calendar control to the ShowProperties() sample function shown in
Example 30.
Example 30 – Passing an ActiveX property container to ShowProperties()
void ShowCalendarProperties(CCalendarControl& cal, CDC* pDC)
{
ShowProperties(&m_calendar, pDC);
}
Chapter 5 Properties Package 41
42
Chapter 6
Events Package
6.1
Introduction to SFL Events
Windows applications and components generate and handle numerous events. Because C++ is an
object-oriented language, you should treat events as objects within an application or component.
The Events package provides an object-oriented event model similar to the Java event model.
Events are treated as instances of C++ classes so you can invent new types of events through subclassing. Objects interested in receiving event notifications are called event listeners. Event listeners
can subscribe to event routers, which are objects that either generate or route events through the system. A publisher-subscriber relationship exists between event routers and event listeners. This
object-oriented approach to event handling is flexible and is ideal for handling Windows messages,
as well as custom events, in C++ applications and components.
Chapter 6 Events Package 43
6.2
Event Objects
Events are instances of event classes that implement the IEvent interface. The IEvent interface
defines the Dispatch() method, which takes a pointer to the object handling the event and invokes
the appropriate handler function. Events implement the Dispatch() method by querying the
event handler for the appropriate event listener interface and then invoking the handler method on
the interface. The relationship between events and event listeners is an example of the visitor
design pattern.
Event classes are simply C++ classes that implement the IEvent interface. The Events package
implements the most common Windows messages as event classes. Developers can create their
own custom event types by implementing the IEvent interface and one or more corresponding
event listener types.
6.2.1 Windows Messages
A Windows message is a type of event generated by the operating system and queued up for an
application. The Windows Platform SDK defines constants using the naming convention WM_XXX
for messages. The Events package defines the IWinEvent interface, which provides methods for
accessing the message ID, WPARAM, LPARAM, and result value of a Windows message. All Window
events implement the IWinEvent interface. The CWinEvent template class makes it easy to implement new Windows event classes by providing a default implementation of the IWinEvent
interface.
Example 31 shows how the WM_PAINT message is implemented using the CWinEvent class. First, an
interface is derived from IWinEvent that defines a method for cracking the WM_PAINT message.
Example 31 – Implementing the WM_PAINT message by using the CWinEvent class
class __declspec(uuid("8A6AF181-40A7-11d3-AF0D-006008AFE059"))
IWindowPaintEvent : public IWinEvent
{
public:
virtual HDC GetDC() const = 0;
};
Next, the class CWindowPaintEvent is derived from the CWinEventBase class, as shown in
Example 32. The template parameters for CWinEventBase are the interface for the event and the
GUID for that interface. The CWindowPaintEvent class implements the Dispatch() method
inherited from IEvent and the GetDC() method inherited from IWindowPaintEvent.
Example 32 – Deriving CWindowPaintEvent from the CWinEventBase class
class CWindowPaintEvent : public CWinEventBase<IWindowPaintEvent,
&IID_IWindowPaintEvent>
{
…
virtual bool Dispatch(IQueryGuid* pIListener);
virtual HDC GetDC() const;
…
};
44
bool CWindowPaintEvent::Dispatch(IQueryGuid* pIListener)
{
bool bHandled = false;
IWindowListener* pIWindowListener =
guid_cast<IWindowListener*>(pIListener);
if (pIWindowListener != NULL)
{
bHandled = pIWindowListener->OnPaint(GetDC());
pIWindowListener->Release();
}
return bHandled;
}
HDC CWindowPaintEvent::GetDC() const
{
return (HDC) GetWParam();
}
6.2.2 The Event Factory
The CEventFactory class translates Windows messages into event objects. It contains a method
named CreateWindowsEvent(), which takes a message ID, WPARAM, and LPARAM and creates the
appropriate type of Windows event object. The CEventFactory class defines a virtual
FilterWindowsEvent() method to provide derived classes the opportunity to filter events. Command messages are handled by the CreateCommandEvent() and FilterCommandEvent()
methods. Example 33 shows the definition of the CEventFactory class.
Example 33 – CEventFactory class definition
class CEventFactory
{
public:
virtual bool FilterWindowsEvent(UINT message, WPARAM wParam,
LPARAM lParam);
virtual IEvent* CreateWindowsEvent(UINT message, WPARAM wParam,
LPARAM lParam);
virtual bool FilterCommandEvent(UINT nID, int nCode);
virtual IEvent* CreateCommandEvent(UINT nID, int nCode);
virtual IEvent* CreateCommandQueryEvent(UINT nID);
};
Chapter 6 Events Package 45
6.2.3 Windows Message Cracking
Message cracking is a natural by-product of encapsulating the WPARAM and LPARAM of a Windows
event in an object. Calling member functions to crack a Windows message is more convenient and
type-safe than using macros. Example 34 illustrates a window procedure that cracks mouse events
and saves the point at which the mouse event occurred.
Example 34 – Windows procedure that cracks mouse events and saves the event point
static POINT g_ptLast;
LRESULT WndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
static CEventFactory factory;
IEvent* pEvent = factory.CreateWindowsEvent(nMsg, wParam, lParam);
IMouseEvent* pMouseEvent = guid_cast<IMouseEvent*>(pEvent);
if (pMouseEvent != NULL)
{
pMouseEvent->GetPoint(g_ptLast);
}
. . .
}
46
6.3
Event Routers
An event router is an object that generates events and routes them to event listeners. Event listeners
can be added and removed from an event router, and it is the event router’s responsibility to route
the events to interested listeners. Event routers implement the IEventRouter interface, which contains three methods. Example 35 shows the IEventRouter interface.
Example 35 – IEventRouter interface
class __declspec(uuid("47E1CE36-D500-11d2-8CAB-0010A4F36466"))
IEventRouter : public IRefCount, public IQueryGuid
{
public:
/* Routes event objects to event listeners. */
virtual bool RouteEvent(IEvent* pIEvent) = 0;
/* Add an event listener to the router. */
virtual bool AddListener(IEventListener* pIListener) = 0;
/* Remove an event listener from the router. */
virtual bool RemoveListener(IEventListener* pIListener) = 0;
};
6.3.1 Default Event Router Implementation
The IEventRouterImpl class provides a default implementation of the IEventRouter interface.
Example 36 shows the IEventRouterImpl implementation of RouteEvent().
Example 36 – IEventRouterImpl implementation of RouteEvent()
virtual bool RouteEvent(IEvent* pIEvent)
{
int nHandledCount = 0;
if (pIEvent != NULL)
{
ListenerVector::const_iterator itListener;
//
// Give each event listener a chance to handle the event.
//
for (itListener = m_listeners.begin();
itListener != m_listeners.end();
itListener++)
{
if ((*itListener)->HandleEvent(pIEvent))
{
nHandledCount++;
}
}
}
return (nHandledCount > 0);
}
Chapter 6 Events Package 47
The IEventRouterImpl class gives each event listener an opportunity to handle the event. An alternative implementation might stop as soon as a listener is found to handle the event.
6.3.2 ATL Integration
The Events package integrates seamlessly with the ATL message map architecture by generating
and routing events processed by ATL message maps. ATL defines the CMessageMap class, which
defines the ProcessWindowMessage() function for handling messages. The ATL message map
macros implement the ProcessWindowMessage() function. The CEventRouterMap class derives
from CMessageMap and implements the ProcessWindowMessage() function by creating an event
using an event factory and then passing it to RouteEvent(). The implementation of
CEventRouterMap is straightforward, as shown in Example 37.
Example 37 – CEventRouterMap implementation
template <typename T>
class CEventRouterMap : public CMessageMap
{
public:
virtual BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg,
WPARAM wParam, LPARAM lParam,
LRESULT& lResult,
DWORD dwMsgMapID = 0)
{
bool bHandled = FALSE;
T* pT = static_cast<T*>(this);
IEvent* pIEvent =
GetEventFactory()->CreateWindowsEvent(uMsg,
wParam,
lParam);
if (pIEvent != NULL)
{
bHandled = pT->RouteEvent(pIEvent);
pIEvent->Release();
}
return bHandled;
}
virtual CEventFactory* GetEventFactory()
{
static CEventFactory eventFactory;
return &eventFactory;
}
};
The CEventRouterMap class acts as a bridge between ATL message maps and the event-listener
architecture. The template parameter passed into CEventRouterMap is the derived class, which is
assumed to be an event router. The CEventRouterMap class defines a virtual GetEventFactory()
method to provide derived classes the opportunity to supply a different event factory, which is useful for filtering events.
48
To use CEventRouterMap, you need to insert one line of code in your ATL message map to chain to
the CEventRouterMap object. Example 38 shows an ATL window class that implements event
routing from an ATL message map.
Example 38 – Implementing event routing from an ATL message map
class CMyWnd : public CWindowImpl<CMyWnd>,
public IEventRouterImpl,
public CEventRouterMap<CMyWnd>
{
public:
BEGIN_MSG_MAP(CMyWnd)
CHAIN_MSG_MAP(CWindowImpl<CMyWnd>)
CHAIN_MSG_MAP(CEventRouterMap<CMyWnd>)
END_MSG_MAP()
};
To integrate event routing with the message map, you need to derive from the CEventRouterMap
class and then add one entry to the message map to chain to it. Alternatively, you can declare the
CEventRouterMap as a member variable of your class and then use ATL’s CHAIN_MSG_MAP_MEMBER
macro instead of CHAIN_MSG_MAP. Chaining to a CEventRouterMap does not interfere with the normal behavior of the ATL message map. In other words, the ATL message and event router logic live
together happily.
6.3.3 MFC Integration
The Events package integrates seamlessly with the MFC message map architecture by hooking the
OnWndMsg() and OnCmdMsg() functions and routing events for the Windows messages received by
those two functions. The template class CMFCEventRouter wraps any CWnd derived class and
overrides the OnWndMsg() and OnCmdMsg() functions to implement event creation and routing. The
CMFCEventRouter class takes two template arguments. The first template argument is the derived
class, which is assumed to be an event router. The CMFCEventRouter class does a static_cast to
the derived class to invoke the RouteEvent() method. The second template parameter is the base
class, which must be a CWnd derived class or any other class that defines OnWndMsg() and
OnCmdMsg() with the same signature as CWnd. Example 39 shows the implementation of
CMFCEventRouter.
Example 39 – CMFCEventRouter implementation
template <typename router, typename wndbase>
class CMFCEventRouter : public wndbase
{
public:
virtual BOOL OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
{
BOOL bHandled = FALSE;
// Create the event and route it to event listeners.
CEventFactory* pEventFactory = GetEventFactory();
IEvent* pIEvent = NULL;
Chapter 6 Events Package 49
if (pHandlerInfo != NULL)
{
// This message is a request for handler info.
pIEvent=pEventFactory->CreateCommandQueryEvent(nID);
}
else if (nCode == CN_UPDATE_COMMAND_UI)
{
// Create a command update UI event.
CCmdUI* pCmdUI = (CCmdUI*) pExtra;
pCmdUI;
}
else
{
// Regular command event.
pIEvent =
pEventFactory->CreateCommandEvent(nID, nCode);
}
if (pIEvent != NULL)
{
// Route event to event listeners.
router* pT = static_cast<router*>(this);
bHandled = pT->RouteEvent(pIEvent);
pIEvent->Release();
}
return bHandled;
}
virtual BOOL OnWndMsg(UINT message, WPARAM wParam,
LPARAM lParam, LRESULT* pResult)
{
BOOL bHandled = FALSE;
// Create an event, using the event factory and
// route it to the event listeners.
CEventFactory* pEventFactory = GetEventFactory();
IEvent* pIEvent =
pEventFactory->CreateWindowsEvent(message,
wParam, lParam);
if (pIEvent != NULL)
{
router* pT = static_cast<router*>(this);
bHandled = pT->RouteEvent(pIEvent);
pIEvent->Release();
}
return bHandled;
}
virtual CEventFactory* GetEventFactory()
{
static CEventFactory eventFactory;
return &eventFactory;
}
};
50
The CMFCEventRouter class acts as a bridge between MFC message maps and the event-listener
architecture. To use the CMFCEventRouter class, you need to mix it into your CWnd derived class.
The following code is of an MFC window class that implements event routing.
class CMyWnd : public CMFCEventRouter<CMyWnd, CWnd>,
public IEventRouterImpl
{
};
Using the CMFCEventRouter class does not interfere with MFC’s own message map processing.
Both techniques can co-exist without any conflicts and you can use MFC message maps in conjunction with event listeners to handle events.
Chapter 6 Events Package 51
6.4
Event Listeners
Event listeners subscribe to event routers to receive events. The IEventListener interface defines a
single method, HandleEvent(), as shown in Example 40.
Example 40 – HandleEvent() defined in the IEventListener interface
class __declspec(uuid("47E1CE38-D500-11d2-8CAB-0010A4F36466"))
IEventListener : public IRefCount, public IQueryGuid
{
public:
/* Receive an event and attempt to handle it. */
virtual bool HandleEvent(IEvent* pIEvent) = 0;
};
An event listener can implement HandleEvent() any way it chooses, but the typical implementation invokes the event’s Dispatch() method. Example 41 demonstrates a typical implementation
of HandleEvent().
Example 41 – Typical HandleEvent() implementation
virtual bool HandleEvent(IEvent* pIEvent)
{
bool bHandled = false;
if (pIEvent != NULL)
bHandled = pIEvent->Dispatch(this);
return bHandled;
}
6.4.1 Dispatching Events
Notice that the event listener passes a pointer to itself into the event object’s Dispatch() method.
The Dispatch() method queries the listener for an interface that it understands and then invokes
the callback method on that interface. Example 42 demonstrates how the paint event class invokes
the OnPaint() callback function.
Example 42 – Invoking the OnPaint() callback function
bool CWindowPaintEvent::Dispatch(IQueryGuid* pIListener)
{
bool bHandled = false;
IWindowListener* pIWindowListener =
guid_cast<IWindowListener*>(pIListener);
if (pIWindowListener != NULL)
{
bHandled = pIWindowListener->OnPaint(GetDC());
pIWindowListener->Release();
}
return bHandled;
}
52
The event’s Dispatch() method checks to see if it has the right type of listener. If it does have the
right listener, it invokes the appropriate callback method. In the preceding sample code, the event
listener is expected to implement the IWindowListener interface to receive the OnPaint() callback.
Although HandleEvent() method is sufficient for handling all the events that a listener is interested in receiving, doing so would be equivalent to writing a window procedure containing a big
switch() statement. The idea behind making event handling simpler is to map events onto individual member functions. For example, you could map a WM_PAINT message onto an OnPaint()
member function that receives a device context as a parameter. Event listener interfaces extend the
base IEventListener interface with callback functions that are invoked to handle events. An example of an event listener interface is IWindowListener, shown in Example 43.
Example 43 – An event listener interface, IWindowListener
class __declspec(uuid("A67C846D-0A0A-4b5e-8DC0-3DA18454F582"))
IWindowListener : public IEventListener
{
public:
virtual bool OnCreate(LPCREATESTRUCT lpCreateStruct) = 0;
virtual bool OnDestroy() = 0;
virtual bool OnMove(int x, int y) = 0;
virtual bool OnSize(UINT nFlag, int cx, int cy) = 0;
virtual bool OnEraseBkgnd(HDC hDC) = 0;
virtual bool OnPaint(HDC hDC) = 0;
virtual bool OnWindowPosChanging(LPWINDOWPOS lpWindowPos) = 0;
virtual bool OnWindowPosChanged(LPWINDOWPOS lpWindowPos) = 0;
virtual bool OnTimer(UINT nIDTimer) = 0;
};
The HandleEvent() method inherited from IEventListener is called to handle all events, but it delegates the work to the callback functions by calling the event object’s Dispatch() method. The
flow of control for handling events is as follows:
Event object->HandleEvent->Dispatch->Callback function
The event object is passed to the event listener’s HandleEvent() method. The HandleEvent()
method delegates the task of invoking the correct callback function to the event object. Be aware
that this is only the default behavior, and can easily be overridden. For example, you might handle
certain events directly in your event listener’s HandleEvent() method without invoking a callback
function. It is also possible to implement HandleEvent() in such a way that it bypasses the event
object’s Dispatch() method and invokes the callback methods directly. The architecture is flexible
enough to allow many different event listener implementations.
6.4.2 Adapter Classes
Implementing the HandleEvent() method along with each callback function every time you want
to write an event listener is time-consuming. Adapter classes provide default implementations of
event listener interfaces. In addition to implementing HandleEvent() and the event listener callback methods, adapters implement the QueryGuid(), AddRef(), and Release() methods
inherited from the IQueryGuid and IRefCount interfaces. The CEventListenerBase class is a
generic base class that you can use to implement adapters. It provides implementations of the
HandleEvent(), QueryGuid(), AddRef(), and Release() methods. The CEventListenerBase
implementation of HandleEvent() is the typical one described in Section 6.4, and is as follows:
Chapter 6 Events Package 53
virtual bool HandleEvent(IEvent* pIEvent)
{
bool bHandled = false;
if (pIEvent != NULL)
bHandled = pIEvent->Dispatch(this);
return bHandled;
}
The CEventListenerBase class is a template that takes the event listener interface as an argument,
and then derives itself from the given event listener interface. The CWindowAdapter class uses the
CEventListenerBase to provide a default implementation of the IWindowListener interface, as
shown in Example 44.
Example 44 – CWindowAdapter using CEventListenerBase to implement IWindowListener
by default
class CWindowAdapter : public CEventListenerBase<IWindowListener>
{
public:
virtual bool OnCreate(LPCREATESTRUCT lpCreateStruct)
{ return false; }
virtual bool OnDestroy()
{ return false; }
virtual bool OnMove(int x, int y)
{ return false; }
virtual bool OnSize(UINT nFlag, int cx, int cy)
{ return false; }
virtual bool OnEraseBkgnd(HDC hDC)
{ return false; }
virtual bool OnPaint(HDC hDC)
{ return false; }
virtual bool OnWindowPosChanging(LPWINDOWPOS lpWindowPos)
{ return false; }
virtual bool OnWindowPosChanged(LPWINDOWPOS lpWindowPos)
{ return false; }
virtual bool OnTimer(UINT nIDTimer)
{ return false; }
};
The adapter class provides basic stub implementations of each callback function, so that developers don’t have to implement every single callback function in their own event listener classes.
54
6.4.3 Using Event Listeners
Now let’s implement a class that listens for events. Our example will be a class that paints the text
“Hello World” in the center of a window. The first step is to mix in the CWindowAdapter class so
that our class can listen for paint and size events.
Example 45 – Adding CWindowAdapter to a class that will listen for events
class CHelloObject : public CWindowAdapter
{
POINT m_ptCenter;
public:
virtual bool OnSize(UINT nFlag, int cx, int cy)
{
m_ptCenter.x = cx / 2;
m_ptCenter.y = cy / 2;
}
virtual bool OnPaint(HDC hDC)
{
SIZE szText;
GetTextExtentPoint(hDC, _T(“Hello World”), 11, &szText);
int x = (m_ptCenter.x + szText.cx) / 2;
int y = (m_ptCenter.y + szText.cy) / 2;
TextOut(hDC, x, y, _T(“Hello World”), 11);
}
};
The CHelloObject class overrides the OnSize() and OnPaint() event handler functions it inherits
from IWindowEvent to paint the text “Hello World” in the center of a window. Now all we need is
a window capable of routing events, in which to plug an instance of the CHelloObject class. The
CHelloWnd class shown in Example 46 implements IEventRouter and generates events from the
ATL message map using the CEventRouterMap class.
Example 46 – CHelloWnd class implementing IEventRouter and generating events from the
ATL message map by using the CEventRouterMap class
class CHelloWnd : public CWindowImpl<CHelloWnd>,
public IEventRouterImpl,
public CEventRouterMap<CHelloWnd>
{
CHelloObject m_hello;
public:
// The CEventRouterMap is derived from CMessageMap and
// has an implementation of ProcessWindowMessage that uses
// an event factory to create events and then routes them
// by calling RouteEvent.
BEGIN_MSG_MAP(CHelloWnd)
CHAIN_MSG_MAP(CWindowImpl<CHelloWnd>)
CHAIN_MSG_MAP(CEventRouterMap<CHelloWnd>)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
END_MSG_MAP()
Chapter 6 Events Package 55
LRESULT OnCreate(UINT, WPARAM, LPARAM, BOOL&)
{
// Wire up the hello object to listen to window events
AddListener(&m_hello);
return 0;
}
};
6.4.4 Efficiency of Event Listeners vs. Message Maps
Both MFC and ATL use message maps to avoid the overhead of using virtual functions for handling messages. For example, if MFC’s CWnd class defined a virtual function for every possible
message a window might receive, the v-table would be large. It would also mean that the interface
to CWnd would have to change every time a new message type was added to the Windows API.
Event listeners define message handlers as virtual functions, but the monolithic v-table problem is
avoided by partitioning event listeners into small interfaces. Rather than putting every event handler method into a single monolithic base class, you can mix in event listeners that are small
interfaces as needed. Event listeners generally handle one event or a limited category of events. For
example, the IMouseListener interface has eight virtual functions to handle every type of mouse
event. If a class is only interested in mouse events, it does not have to pay the v-table overhead of
having virtual functions for every other type of Windows message. Partitioning event listeners into
categories and mixing them into classes as interfaces is ideal; it provides the convenience of using
virtual methods for event handlers and avoids the overhead of large, monolithic v-tables.
56
6.5
Chaining Event Routers
It is useful to route events through numerous objects in a system. In other words, an event may be
routed through multiple objects before reaching its final destination. MFC has hard-coded routing
logic to get messages to views and documents. The Events package takes advantage of the more
flexible publish-subscribe pattern to accomplish message routing. Event routers can also be event
listeners, and therefore they can listen to the events of other event routers. This flexible design
means that the event routing logic in a system can be very dynamic and easy to configure.
The CComboRouterListener template class can be used to mix the IEventListener interface with an
event router. The template parameter passed to CComboRouterListener is the base class, which
must be a class derived from both IEventListener and IEventRouter. The CComboRouterListener
class basically just implements the HandleEvent() method by forwarding it to the RouteEvent()
method. Example 47 shows the implementation of CComboRouterListener.
Example 47 – Implementation of CComboRouterListener
template <class base_t>
class CComboRouterListener : public base_t
{
public:
virtual bool HandleEvent(IEvent* pIEvent)
{
bool bHandled = false;
if (pIEvent != NULL)
{
bHandled =
pIEvent->Dispatch(guid_cast<IEventRouter*>(this));
}
if (!bHandled)
bHandled = base_t::RouteEvent(pIEvent);
return bHandled;
}
};
The following code shows how you can use CComboRouterListener to create an object that is both
an event router and an event listener. The CFoobarBase class inherits IEventRouterImpl and mixes
in the IEventListener interface. CFoobarBase is an abstract base class because it does not implement the HandleEvent() method inherited from IEventListener. Wrapping CFoobarBase with the
CComboRouterListener template class implements HandleEvent(). As a result, instances of
CFoobar can be added as listeners to other event routers. They can also route events to listeners.
class CFoobarBase : public IEventRouterImpl, public IEventListener
{
};
typedef CComboRouterListener<CFoobarBase> CFoobar;
Chapter 6 Events Package 57
6.6
Custom Event Types
The term “custom event types” is a misnomer. The Events package makes no distinction between
its own event types and other derived event types. In fact, events do not even need to correspond
to Windows messages. The first step in creating your own event type is to derive a class from the
base IEvent interface. If you are implementing a Windows message, then derive the class from
IWinEvent. The class can be either a pure virtual class (for example, an interface) or a concrete
class. Next, derive an event listener interface from the IEventListener base interface and add your
callback functions to it. Implement the Dispatch() function in your concrete event class by querying for the event listener interface and then invoking the appropriate callback function.
If you are implementing a Windows message (for example, WM_XXX) as an event class, the mouse
event and mouse listener classes in the Events package are a good model to follow. If your custom
event is not a Windows message, then do not derive your event from IWinEvent.
58
Chapter 7
Layout Manager
7.1
Layout Manager Framework
One of the biggest problems Windows developers face is window layout management and device
independent positioning. Before, developers had to write thousands of lines of custom code to handle the resizing of dialogs and forms. Stingray Foundation Library’s Layout Manager allows you to
circumvent this challenge by providing a framework for implementing plug-in layout algorithms.
The framework includes several sample layout algorithms such as relative layout, scaled layout,
and others. It also affords you the flexibility to design custom layout managers based on your
needs (for example, for low-resolution displays). The Layout Manager plugs seamlessly into your
existing dialog, frame window, property page, or any other window to allow for nested layouts.
You can integrate the Layout Manager into applications in a matter of minutes.
Chapter 7 Layout Manager 59
7.2
Issues with Resizable Windows
Whenever you create a dialog, property page, frame window or any other window that contains
child windows, you need to decide what to do when the user tries to resize it. You could forbid the
resize event, but this leads to an awkward user interface. You could ignore the event, but this leads
to an underutilized window with a disproportionate amount of empty space. Finally, you could
trap the size event and code your own custom layout logic. Unfortunately, this requires a large
amount of implementation-specific code that is time consuming to create. The code is also subject
to change whenever you want to modify the window’s layout. In addition, if you want to achieve
resolution-independent positioning, even more work is required.
The Stingray Foundation Library provides a powerful layout management framework that encapsulates all the details of laying out your child window controls, so that you can concentrate on
content rather than the mechanics of your user interface presentation.
With the layout management framework, you do not need to consider hard-coded pixel positions
that are difficult to write and even harder to maintain. You can establish your desired layout with a
simple series of layout “constraints” that are easy to remember and change. As an added benefit,
the use of a relative constraint-based layout algorithm guarantees that your window or dialog
appears the same way everywhere, be it a 640x480 laptop or a 1600x1200 workstation.
60
7.3
Layout Manager Architecture
The Layout Manager architecture is partly based in part on the Composite design pattern (see
Section 4.3, “The Composite Pattern.”) The Layout Manager consists of a collection of nodes
arranged in a tree-like hierarchy of responsibility.
7.3.1 Layout Nodes
All the nodes in the layout tree are expected to implement the interface
stingray::foundation::ILayoutNode, which defines the minimum set of functionality expected from
the members of the tree. A layout node is defined as any object that implements this interface, as
well as the composite pattern interface.
Every node is assigned a rectangle it is responsible for. Rectangles associated to child nodes should
not go outside the boundaries of the parent’s rectangle. The root node of the composite owns the
rectangle affected by all layout operations. Typically, this rectangle corresponds to some window’s
client area.
Conceptually, layout nodes are either proactive or reactive in nature. Proactive nodes, also known
as composites, hold the layout algorithms. Each proactive node encapsulates one layout algorithm.
Examples are CRelativeLayout and CScaleLayout. Proactive nodes are designed to have and
administrate child nodes.
Reactive nodes, also known as primitives, are home to the leaf objects controlled by the proactive
nodes. When you implement the appropriate functions in the ILayoutNode interface, a reactive
node can respond to events driven by its parent node and position, resize, and render itself as
appropriate. CWindowLayoutNode is an example of a reactive node, designed to link to a window.
Nodes derived from CDCLayoutBase are also reactive nodes.
The proactive versus reactive node distinction is only conceptual in nature. Syntactically, both
types of nodes realize ILayoutNode and both possess the same type-interface. Only the intended
use of the object defines its designation. Some objects can be both proactive and reactive; for example, CSplitterLayout can be considered of both types.
In general, proactive nodes are not visible entities. For example, an algorithmic layout node is a
“black box rectangle” that is responsible for administrating all its children within that rectangle. It
is entirely possible, however, that one of its children is also a proactive node that administers its
child nodes. This is the strength of a polymorphic layout node in a composite, tree-like hierarchy.
SFL provides a default base class for all layout nodes, CLayoutNode. CLayoutNode mixes a default
implementation of the ILayoutNode interface with the implementation of the composite pattern. It
also declares the creation and destruction methods required by the class factory, as discussed later
in this section. In addition to that, CLayoutNode derives from the IEventRouter and IEventListener
interfaces, so it can receive and process window messages and route them through the layout tree.
Deriving your custom layout node classes from CLayoutNode is not required, but it is recommended, to make these services available to every layout node.
Chapter 7 Layout Manager 61
7.3.2 Layout Recalculation Process
The Layout Manager framework is responsible for rearranging the contents of a window when
required by some external or internal condition, such as a resize operation. The actual procedure
involves two steps: recalculation and realization.
7.3.2.1 Recalculation
During the first step, the recalculation stage, proactive nodes, or composites, have the responsibility to act. The nodes follow their particular layout algorithm to logically rearrange the rectangles of
their child nodes.
The RecalcLayout() method is called on the root node of the Layout tree, giving it a new rectangle
on which to rearrange the contents. In the RecalcLayout() implementation, a node calculates the
rectangles that will be assigned to its child nodes, based on its own assigned rectangle.
RecalcLayout() is then called on each of those nodes, so they, in turn, can manage the positioning
of their child nodes, and so on. A child node can contest the rectangle being assigned to it, if its parent specifies that it is willing to negotiate. However, the parent node always has the last word. The
parent can deny the region requested by a child and assign another totally different one, if the
request doesn’t fit in the layout algorithm the parent implements.
The recalculation stage does not affect the visible objects in the screen. Recalculation deals with the
rectangles assigned to the nodes in a completely logical fashion.
7.3.2.2 Realization
The second step in the layout recalculation process is the realization stage. It is during this stage
that the new rectangles assigned to each node during the recalculation process are reflected by the
screen objects.
Reactive nodes, or primitives, are responsible for the execution of RealizeNode(). These nodes are
associated with visible objects on the screen, like child windows, images or decorations.
For example, on RealizeNode(), a CWindowLayoutNode instance should call some Win32 API
like SetWindowPos() to adjust its associated window to the new area.
A recalculation is usually triggered by resizing window messages (WM_SIZE). Sometimes, you want
to specify a maximum or minimum size for a node. This can be achieved by the SetMinMaxSize()
method in the ILayoutNode interface, as shown in Example 48.
Example 48 – Setting maximum and minimum window sizes
// the dialog will never get smaller than 475x450
// or larger than 900x600
pRootNode->SetMinMaxSize(CSize(475,450), CSize(900,600));
62
7.3.3 Node Creation
The creation of layout nodes is performed by an specialized class, the layout factory. The design of
this class is based on the Object Factory design pattern (see Section 4.4, “The Object Factory
Pattern.”)
Using the CLayoutFactory class requires the definition of a layout map, which specifies the layout
node classes that will be used in the application. The layout map gives the layout factory the information needed for it to be able to create new instances of those classes. The concept is very similar
to the object map mechanism used in ATL to define COM class factories for the COM objects
exported by a server.
For example, if your application uses the layout node classes CBorderClientLayout,
CWindowLayoutNode, CSplitterLayout and CBorderEdge, you should include somewhere in your
code the following lines:
Example 49 – Defining a layout map
BEGIN_LAYOUT_MAP()
LAYOUT_MAP_ENTRY(foundation::CBorderClientLayout)
LAYOUT_MAP_ENTRY(foundation::CSplitterLayout)
LAYOUT_MAP_ENTRY(foundation::CGripperWrapper)
LAYOUT_MAP_ENTRY(foundation::CBorderEdge)
LAYOUT_MAP_ENTRY(foundation::CWindowLayoutNode)
END_LAYOUT_MAP()
The advantage of using a layout map is that your application only pays the price of the layout node
classes it will actually use. The layout factory does not need to hard-code all the known layout
node classes, which would include them in your final executable file, even if they are not used in
the application.
A layout map also adds flexibility to the design. If you come up with additional layout node classes
that you wish to use in your applications, it is not necessary to modify the CLayoutFactory class or
derive a new class from it. Including additional entries in your layout map makes your new layout
node classes available to the layout factory.
Each node class is identified by a GUID, just like in COM. The class factory uses this information to
identify the class creator function in the layout map.
To create a new node instance, the layout class factory publishes a method called
CreateLayoutNode(). This method receives the GUID of the class you want to instantiate, and
returns a newly created instance of that class.
The layout factory also provides a DestroyLayoutNode() method, which destroys and deallocates
the node passed as a parameter, as well as its descendants.
7.3.4 Node Initialization
After it is created but before it is used, a layout node needs to be properly initialized. The
ILayoutNode interface publishes an Init() method, which takes two parameters. The first one is
the handle of the window associated with the root of the layout operation. All nodes need this
Chapter 7 Layout Manager 63
information in case they need to interact with the window. A second optional parameter identifies
a handle for a child window; this parameter is used only in the CWindowLayoutNode, which is
associated with that child window.
The final part of the initialization process is integrating the layout node into the layout tree, as the
child of another layout node. This is generally performed using the AddLayoutNode() method of
the ILayoutNode interface. Some specialized layout node classes may require you to use some
other mechanism for this, like the AddPane() procedure for splitters.
64
7.4
Integration with ATL
The composite-based layout tree described above is framework-independent. However, at some
point this functionality needs to be integrated into the behavior of the desired window. This part of
the package relies on the message handling mechanism of the framework being used. An ATL integration layer is included with SFL.
To add layout management to a window in ATL you need to:
1. Include the templated class CLayoutManager<> among the base classes of your window.
2. Delegate messages to this base class.
3. Override the InitLayout() method to initialize your layout logic.
The CLayoutManager class takes as its first template parameter the name of the most-derived
class. The second template parameter is the creation message that triggers the layout logic initialization. In general, two messages are used for this purpose: WM_INITDIALOG for dialog boxes and
WM_CREATE for all other classes of windows. Example 50 shows the use of WM_CREATE.
Example 50 – Initializing window layout logic
class CMyWindow:
public CWindowImpl<CMyWindow>,
public CLayoutManager<CMyWindow, WM_CREATE>
You must delegate the messages your window receives to the LayoutManager base, otherwise the
layout manager will not be able to trigger some of the processes necessary for the layout logic to
work. Your window can process the messages it is interested in first. But, if your window processes
the following messages:
пЃµ
WM_SIZE
пЃµ
WM_GETMINMAXINFO
пЃµ
WM_ERASEBKGND
пЃµ
WM_PAINT
you need to make sure that you do not stop their routing in your window’s message map, so they
eventually reach the CLayoutManager message map.
In addition to the previous messages, the individual layout node classes might need some other
messages to be passed to them.
Do not stop the routing of messages, except when you are absolutely sure you do not want further processing of a
particular message.
In ATL, the routing of a Windows message is stopped if the bHandled parameter is set to TRUE in
the message map. This is done by default by all the ATL macros similar to MESSAGE_HANDLER().
SFL offers an alternative macro for use in your message maps, MESSAGE_HANDLER_DELEGATE().
This message map entry differs from the traditional MESSAGE_HANDLER only in that it does not set
the bHandled parameter to TRUE, and therefore allows processing of a message without stopping
its routing to the base classes.
Chapter 7 Layout Manager 65
The message map in an ATL CWindowImpl-derived window class with layout management
should look like Example 51.
Example 51 – Message map in a window class with layout management
BEGIN_MSG_MAP(CMyWindow)
MESSAGE_HANDLER_DELEGATE(WM_CREATE, OnCreate)
MESSAGE_HANDLER_DELEGATE(WM_PAINT, OnPaint
CHAIN_MSG_MAP(CLayoutManager<CMyWindow, WM_CREATE>)
END_MSG_MAP()
The third step is to override the InitLayout() virtual method inherited from your
CLayoutManager base class. This method is called during the initialization process that takes place
in the creation message (WM_CREATE or WM_INITDIALOG), hence the importance of always delegating this message.
InitLayout() receives one single parameter, of type ILayoutNode*, which is the root node of your
layout tree. If this parameter is NULL, it is the responsibility of your window to create the node
before going any further. This will usually be the case, unless the root node is created in a base class
located in the inheritance chain between your final window and the CLayoutManager class. In that
case, you should use the root node handed to you and just create all the additional nodes your window needs.
Always call your base class’ version of InitLayout(), so the initialization process can be completed. Later in the examples section you will see some instances of how to override this method.
Besides integration with the ATL message routing mechanism, CLayoutManager offers some
shortcut functions for creation and initialization of layout nodes. As outlined previously, the normal process of creation and initialization of a node is as follows:
1. Create a node instance using the class factory.
2. Call Init().
3. Add it to the layout tree by calling the parent’s AddLayoutNode().
These three steps must be performed in that order, unless the specific interface of the parent node
requires you to add the nodes in some other fashion, as in the case of the splitter class.
CLayoutManager provides some routines that encapsulate those three steps in a single call. The
various overloads of the CreateLayoutNode() method serve this purpose. For example:
ILayoutNode* pNodeOKCANCEL =
CreateLayoutNode(__uuidof(CScaleLayout), pRootNode);
This call creates a new instance of CScaleLayout, initializes it to this window (remember, the
CLayoutManager<> is a base class), and assigns it as a child of the pRootNode node, using the
standard ILayoutManager::AddLayoutNode().
A very common special case is the CWindowLayoutNode class. As mentioned before, this class’
initialization process requires the handle of the child window in addition to the master window.
An additional overload takes care of this, as illustrated here:
ILayoutNode* pNodeSearchText =
CreateLayoutNode(__uuidof(CWindowLayoutNode),pRootNode,
IDC_SEARCH_STATIC);
66
Notice that here, a window id is passed as an extra parameter. This window id must correspond to
an actual child window, in which case its handle will be passed as a second parameter in the call to
Init().
Often, you want to create a layout node, but the default interface returned, ILayoutNode, is not the
one you need to perform the necessary configuration. You can cast the interface pointers by using
the guid_cast<> operator or calling QueryGuid directly. A more convenient alternative is to use
another overload of CreateLayoutNode() to return you the right interface pointer. This overload
requires passing a pointer to the interface you are requesting as a parameter. This pointer does not
need to be initialized, because its value is never accessed. Rather, it is used only to determine the
right interface type that must be returned:
IRelativeLayout* pRelative =
CreateLayoutNode(__uuidof(CRelativeLayout),pRelative);
7.4.1 Adding Layout Management to Your Applications
The process of merging the layout framework into your application is easy. The following procedure outlines the recommended steps:
1. Add layout management to one or more windows in your application, by following the
steps outlined in Section 7.4.
2. Add a layout map to your program to define the factory entries for the layout node classes
you are going to use.
Chapter 7 Layout Manager 67
7.5
Layout Algorithms
This section describes each of the main layout algorithms provided with the layout manager
component.
7.5.1 Scale Layout
The scale layout maintains all children with a constant aspect ratio to the parent scale node. In other
words, the child node’s top, left, right, and bottom coordinates are stored as percentages of the parent node’s size and are resolved to actual pixel values with each recalculation, as seen in Figure 5.
This guarantees a constant aspect ratio, regardless of the size of the parent node.
Figure 5 – Scale layout
7.5.2 Relative Layout
The relative layout allows a logical organization of layout nodes. The arrangement of child windows
is specified as a set of constraints, which are constructed using English-like semantics.
For example:
68
пЃµ
“Set the left side of node 1 equal to the right side of node 2 plus 10 pixels,” or
пЃµ
“Set the bottom of node 1 to 25 percent of the height of node 2,” or
пЃµ
“Move node 1 such that its bottom is equal to the top of node 2 – 10 pixels.”
The IRelativeLayout interface, directly derived from the ILayoutNode interface, provides an additional method SetConstraint(), which is used to specify the constraints to be used by a
determined instance of the CRelativeLayout class.
Thus, you can say in your program:
pRelative->SetConstraint(pSplitter, foundation::RelLeft, pNameNode,
foundation::RelRight, 20);
which can be translated to plain English as: “Set the left side of the node pSplitter to the right side
of the pNameNode node plus 20 units.”
The constraint:
pRelative->SetConstraint(pOkNode, foundation::RelMoveBottom,
pRootNode, foundation::RelBottom, -30);
can be interpreted as: “Move (without resizing it) the node pOkNode, such that its bottom is 30 pixels up from the Root node bottom.”
As an additional example, the constraint:
pRelative->SetConstraint(pOkNode, foundation::RelWidth, pRootNode,
foundation::RelWidth, 0, 0.5);
can be interpreted as: “Set the width of the pOkNode node to be 50% of the width of the Root
node.”
For a description of all the options available for specifying constraints, consult the Stingray Foundation Library Class Reference.
Chapter 7 Layout Manager 69
7.5.3 Border-Client Layout
The border-client layout implements the typical arrangement found in frame windows. Four designated areas are attached to each border of the window, where items like toolbars and status bars are
usually placed. The rectangular space between these borders is generally called the client area.
Figure 6 – Border-client layout
To provide the additional functionality, the node class CBorderClientLayout implements the specialized interface IBorderClientLayout, which in turn derives from the ILayoutNode interface. This
special interface allows the assignment of a child layout node to a specific area of the arrangement.
An overload of the AddLayoutNode method is used, which takes an extra parameter to specify the
area inside the window to which the child node should be assigned. For example:
pBorderClient->AddLayoutNode(pClientNode,
IBorderClientLayout::BorderPosition::Client);
assigns the pClientNode node to the Client area of the pBorderClient node.
The ILayoutNode::AddLayoutNode() method should not be used with a border-client node.
7.5.4 DC Layout Nodes
Rather than one concrete class, CDCLayoutBase is a templatized class. You can use
CDCLayoutBase as a base class for layout node classes that need to draw directly on the device
context of the window associated with their root node.
Classes derived from CDCLayoutBase should override the OnPaint() virtual method to process
the specific display logic. For example, border nodes are special classes of DC nodes that decorate
the surroundings of another node. They will be described later in this section.
If you want to display an image directly on your window, but you want that image to be laid out as
though it were an independent visual component, you can derive a class from CDCLayoutBase
and alter the OnPaint() method to display your image in the rectangle assigned to your node.
70
CDCLayoutBase also defines two virtual methods, PrepareDC() and RestoreDC(), that allow
derived classes to manipulate the device context before the actual drawing process takes place.
A node class that overrides PrepareDC() and changes parameters in the device context must also override
RestoreDC() and restore the device context to its original state.
The default version of PrepareDC() executes the following manipulations:
пЃµ
Sets the clipping region of the device context to the intersection of the current
clipping region and the rectangular region assigned to the node. The purpose of
this is to make sure the DC node instance does not draw outside its boundaries.
пЃµ
Offsets the viewport origin of the device context to the NonClientOffset attribute of
the node. The Get and Set operations for this attribute are declared in the
ILayoutNode interface, implemented by all layout nodes.
Your CDCLayoutBase-derived node class can perform different operations in these methods, but it
is important to remember always to undo in RestoreDC() all what is done in PrepareDC().
7.5.5 Splitter Layout
The splitter layout, unlike the rest of the layout algorithms, is a “dynamic” layout arrangement.
An application user can rearrange windows interactively, using the mouse. In the other layout
algorithms, the layout recalculation is triggered indirectly by operations such as resizing the container window.
CSplitterLayout implements the splitter functionality in SFL. This class derives from
CDCLayoutBase, which means that the splitter is not really a window, but it is drawn on the area
of the window associated with the layout manager’s root node.
To perform their function, the splitters need to process mouse messages, so the window must not
absorb those messages in its own message map. As explained in Section 7.4, the window must
allow messages to reach the layout manager, which can rout them within the layout tree.
There are several configuration options you can specify for a splitter node, all of which change the
way the user interface behaves. These options are defined in the enumerated type SplitterFlags,
and are manipulated with the SetSplitterFlags() and GetSplitterFlags() methods in the
ISplitter interface. SFL splitters support real-time dragging, in which the windows in the cells of
the splitters are resized during the drag operation, as well as the more traditional tracking rect drag,
in which a visual aid represents the result of the dragging operation but the actual windows are
resized only after the user releases the mouse button. Real time dragging is used if the
SplitterRealTimeDrag flag is specified.
You can disable dragging altogether in a splitter layout node, by specifying the SplitterNoSplitters
flag. The result is a simple rearrangement of the child nodes in a grid, with no interactive recalculation. You can get this effect with a splitter node because the splitter layout has no specific grid
layout algorithm.
Chapter 7 Layout Manager 71
The splitter layout supports three graphic representations:
пЃµ
The traditional 3-D display similar to the MFC splitter.
пЃµ
A flatter display similar to the splitters in the Visual Basic and Visual InterDev
development environments.
пЃµ
A 2-D display like the one found in Microsoft Outlook and other Microsoft Office
applications.
Which option is used by a specific instance of the CSplitterLayout class is specified using the
SetDrawingStyle() method in the ISplitter interface and the enumerated type
SplitterDrawingStyle. Figure 7 illustrates the difference in appearance of these three drawing
styles.
Figure 7 – Splitter drawing styles a) Traditional b) Flat c) Border
Example 52 illustrates how to set up a splitter in your window’s override of InitLayout().
Example 52 – Setting up splitter layout
pRootNode = CreateLayoutNode(__uuidof(CSplitterLayout));
ISplitter* pSplitter = guid_cast< ISplitter*>(pRootNode);
// Use the Flat splitter style, and real time drag
pSplitter->SetDrawingStyle(foundation::DrawFlat);
pSplitter->SetSplitterFlags(SplitterRealtimeDrag);
ILayoutNode* pNode;
pNode = CreateLayoutNode(__uuidof(CWindowLayoutNode));
pNode->Init(m_hWnd, GetDlgItem(IDC_LABEL));
pSplitter->AddPane(pNode, 0, 0);
// Span the list in one column, two rows
pNode = CreateLayoutNode(__uuidof(CWindowLayoutNode));
pNode->Init(m_hWnd, GetDlgItem(IDC_LIST));
pSplitter->AddPane(pNode, 0, 1, 2, 1);
pNode = CreateLayoutNode(__uuidof(CWindowLayoutNode));
pNode->Init(m_hWnd, GetDlgItem(IDC_NAME));
pSplitter->AddPane(pNode, 0, 2);
pNode = CreateLayoutNode(__uuidof(CWindowLayoutNode));
pNode->Init(m_hWnd, GetDlgItem(IDOK));
pSplitter->AddPane(pNode, 1, 0);
pNode = CreateLayoutNode(__uuidof(CWindowLayoutNode));
pNode->Init(m_hWnd, GetDlgItem(IDCANCEL));
pSplitter->AddPane(pNode, 1, 2);
72
7.5.6 Borders and Edges
Border nodes are layout nodes that embellish the node they contain with some kind of graphic decoration around the area assigned to the contained node. Two kinds of border nodes are provided
with SFL’s layout package: edges and grippers. All border nodes implement the specialized interface IBorderLayout.
An edge border is implemented in the CBorderEdge class. The border edge draws a 3-D border line
around the node. The node can be configured to draw the line on any combination of the four borders of the contained node area.
Figure 8 – Edge decoration
Gripper nodes display a gripper area either at the top (for vertical grippers) or at the left (for horizontal grippers) of the contained node. In addition, in the borders where a gripper is not drawn,
blank space can be left to give an appearance of separation between distinct elements.
Figure 9 – Gripper decoration
All border nodes implement IBorderLayout. This interface, which derives directly from
ILayoutNode, publishes methods for setting or getting the size of the borders or the border orientation, and for showing or hiding the border decoration. The default implementation of this interface
is provided in the CBorderLayoutBase template class.
Edges and grippers derive from a more specialized class, CBorderGraphic. CBorderGraphic
derives from CBorderLayoutBase, but it is designed to use the class CDCLayoutBase as its base
class. This allows the CBorderGraphic derivatives, like CBorderEdge and CGripperWrapper, to
paint directly in the device context of the window associated with the root node of the layout tree.
To add a border wrapper to some interface element of your application (such as a window or
image), you have to instantiate the appropriate border layout class and add the layout node associated with your interface element as the border node child. Example 53 shows how to do this.
Example 53 – Adding a border wrapper to an interface element
pWrapper = CreateLayoutNode(__uuidof(CGripperWrapper), pWrapper);
pWrapper->Init(*this);
ILayoutNode* pListNode =
CreateLayoutNode(__uuidof(CWindowLayoutNode));
pListNode->Init(*this, m_wndList);
pWrapper->AddLayoutNode(pListNode);
Chapter 7 Layout Manager 73
7.6
Examples
The following examples demonstrate integration of the layout manager into an application. For
more information, look at the Clouds, Scribble, DialogApp or LayoutControl samples in SFL (available
on request from [email protected]). The code in the examples is to be inserted in the
InitLayout override of the window you are adding layout management to.
Example 54 – Scale layout in a dialog
virtual void InitLayout(foundation::ILayoutNode* pRootNode)
{
// Scale is perhaps the simplest and easiest layout algorithm to
// merge into your code. This is all the code you need:
pRootNode = CreateLayoutNode(__uuidof(foundation::CScaleLayout));
// optional: set dialog box size limits
pRootNode->SetMinMaxSize(CSize(150, 255), CSize(900, 600), 0);
// set all child nodes to use optimized redraw
// (static controls require it)
pRootNode->ModifyNodeStyleEx(0, foundation::OptimizeRedraw,
true);
// Delegate to base class to autopopulate the root node
// and kick off the layout process
_LayoutManager::InitLayout(pRootNode);
}
Example 55 – Relative layout in a dialog
virtual void InitLayout(foundation::ILayoutNode* pRootNode)
{
// The "relative" layout is perhaps the most intuitive of the
// layout algorithms. Nodes can be moved or stretched relative to
// each other in a very logical and straightforward manner. Since
// each additional constraint specified results in a higher
// calculation overhead, it is up to the application writer to
// specify the minimum required set of constraints to achieve the
// desired layout.
IRelativeLayout* pRelative =
CreateLayoutNode(__uuidof(CRelativeLayout), pRelative);
pRootNode = guid_cast<foundation::ILayoutNode*>(pRelative);
// Create a Window node for each child window in the dialog
ILayoutNode* pNodeSearchText =
CreateLayoutNode(__uuidof(CWindowLayoutNode), pRootNode,
IDC_SEARCH_STATIC);
ILayoutNode* pNodeSearchEdit =
CreateLayoutNode(__uuidof(CWindowLayoutNode), pRootNode,
IDC_SEARCH_EDIT);
ILayoutNode* pNodeBrowse =
CreateLayoutNode(__uuidof(CWindowLayoutNode), pRootNode,
IDC_SEARCH_BROWSE);
ILayoutNode* pNodeGroup =
CreateLayoutNode(__uuidof(CWindowLayoutNode), pRootNode,
IDC_GROUPBOX);
74
ILayoutNode* pNodeGroupEdit =
CreateLayoutNode(__uuidof(CWindowLayoutNode),
IDC_GROUP_EDIT);
ILayoutNode* pNodeRadio1 =
CreateLayoutNode(__uuidof(CWindowLayoutNode),
IDC_GROUP_RADIO1);
ILayoutNode* pNodeRadio2 =
CreateLayoutNode(__uuidof(CWindowLayoutNode),
IDC_GROUP_RADIO2);
ILayoutNode* pNodeCheck1 =
CreateLayoutNode(__uuidof(CWindowLayoutNode),
IDC_GROUP_CHECK1);
ILayoutNode* pNodeCheck2 =
CreateLayoutNode(__uuidof(CWindowLayoutNode),
IDC_GROUP_CHECK2);
ILayoutNode* pNodeCheck3 =
CreateLayoutNode(__uuidof(CWindowLayoutNode),
IDC_GROUP_CHECK3);
ILayoutNode* pNodeEdit =
CreateLayoutNode(__uuidof(CWindowLayoutNode),
IDC_NONGROUP_EDIT);
pRootNode,
pRootNode,
pRootNode,
pRootNode,
pRootNode,
pRootNode,
pRootNode,
//
//
//
//
Since we want the ok, cancel, help, and "display again" checkbox
to float as 1 unit, configure as a nested alignment layout for
easiest configurability. We can then use this parent node easily
in the relative layout constraints below.
ILayoutNode* pNodeOKCANCEL =
CreateLayoutNode(__uuidof(foundation::CScaleLayout));
ILayoutNode* pOkButton =
CreateLayoutNode(__uuidof(foundation::CWindowLayoutNode),
pNodeOKCANCEL, IDOK);
ILayoutNode* pCancelButton =
CreateLayoutNode(__uuidof(foundation::CWindowLayoutNode),
pNodeOKCANCEL, IDCANCEL);
ILayoutNode* pHelpButton =
CreateLayoutNode(__uuidof(foundation::CWindowLayoutNode),
pNodeOKCANCEL, IDC_HELP_BUTTON);
ILayoutNode* pNoAgain =
CreateLayoutNode(__uuidof(foundation::CWindowLayoutNode),
pNodeOKCANCEL, IDC_NOAGAIN_CHECK);
pNodeOKCANCEL->Init(m_hWnd);
pRelative->AddLayoutNode(pNodeOKCANCEL);
// Move browse node such that right side is 20 pixels left of
// right side of the parent relative node (i.e. right border).
pRelative->SetConstraint(pNodeBrowse, RelMoveRight, pRelative,
RelRight, -20);
// Stretch search edit node such that right side is 10 pixels
// left of the left side of the browse node.
// (left-10=10 pels to left)
pRelative->SetConstraint(pNodeSearchEdit, RelRight, pNodeBrowse,
RelLeft, -10);
// Bottom justify/HCenter the ok, cancel, help,
// and "do not display" check
// Move the "ok/cancel/help" node such that its bottom is 20
// pixels above the bottom side of the parent relative node
pRelative->SetConstraint(pNodeOKCANCEL, RelMoveBottom, pRelative,
RelBottom, -20);
Chapter 7 Layout Manager 75
// Horz center the ok/cancel/help node relative to the parent
pRelative->SetConstraint(pNodeOKCANCEL, RelCenterHorizontal,
pRelative);
// Stretch the group and edit box as needed
pRelative->SetConstraint(pNodeGroup, RelTop, pNodeBrowse,
RelBottom, 20);
pRelative->SetConstraint(pNodeEdit, RelTop, pNodeGroup, RelTop);
pRelative->SetConstraint(pNodeGroup, RelBottom, pNodeOKCANCEL,
RelTop, -10);
pRelative->SetConstraint(pNodeEdit, RelBottom, pNodeGroup,
RelBottom);
// Set right side position of groupbox node equal to the value of
// the width of the parent relative node times 0.48.
// In other words, align right just a little left of the dialog
// midpoint.
pRelative->SetConstraint(pNodeGroup, RelRight, pRelative,
RelWidth, 0, (float)0.48);
pRelative->SetConstraint(pNodeEdit, RelLeft, pNodeGroup,
RelRight, 20);
pRelative->SetConstraint(pNodeEdit, RelRight, pNodeBrowse,
RelRight);
// Size/Position the elements within the group box
// Center the check1 node relative to the width of the groupbox
pRelative->SetConstraint(pNodeCheck1, RelCenterHorizontal,
pNodeGroup, RelWidth);
pRelative->SetConstraint(pNodeCheck2, RelMoveLeft, pNodeCheck1,
RelLeft);
pRelative->SetConstraint(pNodeCheck3, RelMoveLeft, pNodeCheck1,
RelLeft);
pRelative->SetConstraint(pNodeGroupEdit, RelRight, pNodeGroup,
RelRight, -15);
pRelative->SetMinMaxSize(CSize(475, 450), CSize(0, 0),
foundation::NoMaxSize);
_LayoutManager::InitLayout(pRootNode);
}
76
Chapter 8
Model View Controller
8.1
What is MVC?
MVC is a design pattern that provides a clear separation of responsibilities for graphical objects.
Data, control, and presentation are treated as separate and interchangeable parts. MVC provides a
concise definition for constructing reusable and extensible graphical user interface objects. Despite
its lack of wide spread use, the model-view-controller design pattern is not a new concept. It was
invented, along with the graphical user interface and the concept of object-oriented programming,
about twenty years ago by researchers at the Xerox Palo Alto Research Center (PARC). The culmination of that research was the Smalltalk language and its multi-windowed, highly interactive
Smalltalk-80 interface. Both of these inventions are revolutionary, even by today’s standards.
To some degree, nearly every user interface developed in the last two decades has been an adaptation of the work done at Xerox PARC. Indeed, MVC has been partially reproduced in many other
development environments. The MFC document/view architecture is an adaptation of the MVC
design pattern. However, the key purpose of MVC (reuse) was limited in the adaptation. Although
MFC’s Document/View is based on the sound design principle of separation of data from presentation, its implementation of this ideal compromises reuse, modularity, and scalability.
The Stingray Foundation Library’s implementation of MVC is a scalable architecture that supports
the development of lightweight graphical components, as well as providing a flexible supplement
or alternative to MFC’s document/view architecture. MVC is designed to complement and extend
existing frameworks, and it works seamlessly with both ATL and MFC. In addition to providing a
solid foundation on which to build graphical components and document management services,
SFL’s MVC adds support for scrolling, zooming, coordinate mapping, and command undo and
redo.
Chapter 8 Model View Controller 77
8.2
The MVC Design Pattern
The Model-View-Controller architecture is an object-oriented framework and well-known design
pattern for building applications and reusable GUI components. MVC prescribes a way of breaking
an application or component into three parts: the model, view, and controller. The original motivation for this separation was to map the traditional input, processing, output roles into the GUI
realm:
Input
--> Processing
--> Output
Controller
--> Model
--> View
The user input, system function and state, and visual feedback to the user are separated and handled by controller, model, and view respectively. Figure 10 represents the basic MVC triad and
lines of communication.
Figure 10 – The MVC Triad
Model
Subject-Observer
View
Controller
The model is really the cornerstone of the triad. As its name implies, its job is to model some realworld system by emulating its state and functionality. Models define queries for reporting state,
commands for altering state, and notifications to inform observers (views, for example) that a
change in state has occurred. The controller is responsible for defining the behavior of the triad. Its
job is to receive mouse and keyboard input and map this user stimulus into application response –
for example, by executing the model’s commands. The view manages a rectangular area of the display and is responsible for data presentation and hit testing. (Hit testing calculates the object at a
given position on screen.) And due to its observer relationship with the model, new views can be
defined and attached to a model while holding the model’s interface constant.
8.2.1 Model-View-Controller Relationship
Figure 10 shows the relationships between model, view and controller in a triad. The dashed lines
represent weakly typed aggregation and the solid lines represent strongly typed aggregation. The
model maintains a pointer to the viewport, which allows it to send the viewport weakly typed
change notifications. Since it is a weakly typed relationship, the model references the viewport only
through a base class that allows it to send notifications to the viewport.
78
In contrast, the viewport knows exactly what kind of model it observes. It has a strongly typed
pointer to the model that allows it to call any of the model’s functions. The viewport also has a
weakly typed relationship with the controller. The viewport is not tied to a specific type of controller, which means that different types of controllers can be used with the same viewport.
The controller has pointers to both the model and the viewport and knows the type of both.
Because the controller defines the behavior of the triad, it needs to know the type of both the model
and the viewport to translate user input into application response.
8.2.2 The Subject-Observer Pattern in MVC
The relationship between the model and viewport is actually defined by another design pattern.
The subject-observer pattern defines a one-to-many dependency between objects so that when one
object changes state, all its dependents are notified and updated automatically. In the case of MVC,
the model is a subject and viewports are observers. See Section 4.2, “The Subject-Observer Pattern,”
for an overview and examples of this design pattern.
8.2.3 Additional Reading on MVC
MVC is regarded as a classic example of a design pattern and has experienced resurgence in popularity as a result of recent publications on the subject.
The classic text Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et al.
ISBN 0-201-63361-2 discusses MVC and the Command design pattern. However, its coverage of
MVC is minimal.
A more recent text, A System of Patterns: Pattern-Oriented Software Architecture by Frank Buschmann
et al. ISBN 0 471 95869 7 offers more coverage of MVC and the Command Processor design pattern
within the context of C++.
Chapter 8 Model View Controller 79
8.3
Visual Components
Visual components are the cornerstone of visualization in MVC. Visual components provide a
structured approach to rendering objects. A visual component is an object with two-dimensional
bounds that can draw itself onto a device context. An MVC viewport is simply a visual component
that observes and renders a model. A viewport may also contain other visual components. Some
visual components support logical coordinate mapping, which forms the basis of zooming and
scrolling support. Having a concise definition for visual components makes it possible to write
generic code for manipulating and drawing visual objects.
8.3.1 Visual Component Interfaces
The IVisual interface shown in Example 56 defines methods for rendering objects to a device context. The OnPrepareDC() method gives visual components an opportunity to set up the device
context prior to drawing. OnPrepareDC() is typically used to set mapping modes, window and
viewport extents, select pens and brushes, etc. The OnRestoreDC() method returns the device context back to its original state. The Draw() method does the actual rendering of the visual
component to the device context.
Example 56 – IVisual interface
class __declspec(uuid("E7707E00-1E4F-4f4e-A525-290CFA9C1EF3"))
IVisual : public IQueryGuid, public IRefCount
{
public:
virtual void Draw(CDC* pDC) = 0;
virtual void OnPrepareDC(CDC* pDC) = 0;
virtual void OnRestoreDC(CDC* pDC) = 0;
};
As mentioned previously, a visual component has two-dimensional bounds. The IBounds2D interface shown in Example 57 provides methods for manipulating the bounds of a visual component.
Example 57 – IBounds2D interface
class __declspec(uuid("A332FE8E-B30D-47ee-AF1B-7E863FDEFFE5"))
IBounds2D : public ISize2D
{
public:
virtual CRect GetBounds() const = 0;
virtual CPoint GetOrigin() const = 0;
virtual CPoint SetOrigin(int x, int y) = 0;
virtual CPoint MoveOrigin(int xOff,int yOff) = 0;
};
The IBounds2D interface is derived from the ISize2D interface shown in Example 58. These two
interfaces are distinct because an object might have two-dimensional size, but no origin. The
IBounds2D extents the ISize2D interface by adding methods for accessing the origin.
80
Example 58 – ISize2D interface
class __declspec(uuid("A989AFCB-D665-4faf-93A6-34E378BF75E0")
ISize2D : public IQueryGuid, public IRefCount
{
public:
virtual CSize GetSize() const = 0;
virtual CSize SetSize(int cx, int cy) = 0;
};
8.3.2 CMvcVisualComponent
The CMvcVisualComponent class provides an implementation of the IVisual and IBounds2D interfaces. A CMvcVisualComponent object is basically just a rectangle with a draw method. The
CMvcVisualComponent class implements the IBounds2D interface by maintaining a CRect member variable. The implementations of the IVisual methods are just stubs. The declaration of
CMvcVisualComponent is shown in Example 59 below.
Example 59 – Declaration of CMvcVisualComponent
class CMvcVisualComponent : public IVisual, public IBounds2D
{
. . .
// Attributes
protected:
CRect m_rc;
// Operations
public:
// IVisual and IBounds2D methods
. . .
};
8.3.3 CMvcVisualPart
A visual part is a type of visual component that maintains a back pointer to its container. It is a template class that takes the base class and container class as template parameters. CMvcVisualPart is
typically instantiated with CMvcVisualComponent as the base class. The container class is
assumed to support the InvalidateRect() and ValidateRect() functions, and is usually derived
from the IVisualHost interface. A visual part is a visual component that supports nesting and
invalidation.
The CMvcVisualComponentImpl class is a typedef that instantiates the CMvcVisualPart template
class using CMvcVisualComponent and IVisualHost as the template parameters. It provides a convenient default for using the CMvcVisualPart class. The declaration of
CMvcVisualComponentImpl is shown below.
typedef CMvcVisualPart<CMvcVisualComponent, IVisualHost>
CMvcVisualPartImpl;
Chapter 8 Model View Controller 81
8.3.4 Coordinate Mapping
The bounds of a visual component are relative to the logical coordinates of its container. Logical
coordinates of the container are referred to as container points. The container might be a window
or another visual component. A visual component can also have its own logical coordinate system,
which maps logical coordinates onto container coordinates. Visual components that implement the
ILogCoordinates interface provide a mapping of logical coordinates to container coordinates. The
ILogCoordinates interface is shown in Example 60.
Example 60 – ILogCoordinates interface
class __declspec(uuid("9EBF6B30-E26A-4cea-BA7F-2C7E8220AA58"))
ILogCoordinates : public IQueryGuid
{
public:
virtual CPoint GetLogOrigin() const = 0;
virtual CSize GetLogSize() const = 0;
virtual CPoint GetVirtualOrigin() const = 0;
virtual CSize GetVirtualSize() const = 0;
virtual int GetMapMode() const = 0;
virtual CSize GetExtents() const = 0;
virtual CSize GetLogExtents() const = 0;
virtual YAxisDirection GetYAxisDirection() const = 0;
virtual
virtual
virtual
virtual
virtual
virtual
void
void
void
void
void
void
LPtoCP(LPPOINT lpPoints, int nCount) const = 0;
LPtoCP(LPRECT lpRect) const = 0;
LPtoCP(LPSIZE lpSize) const = 0;
CPtoLP(LPPOINT lpPoints, int nCount) const = 0;
CPtoLP(LPRECT lpRect) const = 0;
CPtoLP(LPSIZE lpSize) const = 0;
virtual
virtual
virtual
virtual
virtual
virtual
void
void
void
void
void
void
LPtoDP(LPPOINT lpPoints, int nCount) const = 0;
LPtoDP(LPRECT lpRect) const = 0;
LPtoDP(LPSIZE lpSize) const = 0;
DPtoLP(LPPOINT lpPoints, int nCount) const = 0;
DPtoLP(LPRECT lpRect) const = 0;
DPtoLP(LPSIZE lpSize) const = 0;
};
The ILogCoordinates interface provides methods for getting the mapping mode and extents,
which can be used to set the coordinate mapping for device context and to convert between logical
coordinates and container coordinates. The values returned by ILogCoordinates correspond
directly to the coordinate mapping functions defined by the Windows API for a device context. The
value returned by the GetExtents() function can be passed directly into the SetViewportExt()
API function. The value returned by the GetLogExtents() function can be passed directly into the
SetWindowExt() API function. The value returned by the GetLogOrigin() function corresponds
to the window origin set by the SetWindowOrg() function. The ILogCoordinates interface provides
a consistent way for retained mode graphical objects to expose coordinate mapping information.
The ILogCoordinates interface defines two sets of conversion functions. One set of conversion
functions translates between logical points and container points. The second of conversion functions translates between logical points and device points. If the visual component is windowless
and its container is a window, then there is no difference between container points and device
points. If the visual component is a window, then device points are not the same as container
points. The LPtoDP() and DPtoLP() functions translate between logical coordinates and pixels of
82
the window that contains the visual component. Visual components may be windowed or windowless, and the same is true of containers. Therefore, device units and container units are not
always the same.
The logical origin returned by GetLogOrigin() is the origin of the visual component in logical
coordinates. The logical size returned by GetLogSize() is the size of the visual component in logical coordinates. The result of passing the logical origin into the LPtoCP() function is the origin
returned by IBounds2D::GetOrigin(). Similarly, the result of passing the logical size into the
LPtoCP() function is the size returned by ISize2D::GetSize().
The ILogCoordinates interface also defines methods for getting the virtual origin and size. The virtual origin and size define the virtual bounds of the visual component, which is the entire logical
coordinate space that can be rendered by the visual component. Whereas the logical bounds
defined by GetLogOrigin() and GetLogSize() define the visible area, the virtual bounds defined
by GetVirtualOrigin() and GetVirtualSize() define the entire viewable area. Moving the logical origin scrolls the logical bounds within the virtual bounds.
8.3.5 CMvcLogicalPart
The CMvcLogicalPart class provides an implementation of the ILogCoordinates interface. It is a
template class that takes the base class as a parameter. In addition to ILogCoordinates, the
CMvcLogicalPart class implements the IZoom interface in order to support zooming. The declaration of CMvcLogicalPart is shown in Example 61.
Example 61 – Declaration of CMvcLogicalPart
template <class _Base>
class CMvcLogicalPart : public _Base,
public ILogCoordinatesImpl< CMvcLogicalPart<_Base> >,
public IZoom
{
. . .
};
The CMvcLogicalPart class actually inherits the implementation of ILogCoordinates from the
ILogCoordinatesImpl class. The ILogCoordinatesImpl class maintains the mapping mode, logical
origin, and extents and uses them to implement the ILogCoordinates interface.
The base class passed to CMvcLogicalPart as the template parameter is typically either
CMvcVisualComponent or CMvcVisualPart. CMvcLogicalPart extends the base visual component
class with a logical coordinate system and support for zooming and scrolling. MVC viewports are
frequently derived from CMvcLogicalPart in order to support zooming and scrolling.
The IZoom interface provides support for zooming and is shown in Example 62. It contains methods for getting and setting the magnification of the X and Y axes.
Example 62 – IZoom Interface
class __declspec(uuid("8407B2B4-4B5E-11d3-AF1B-006008AFE059"))
IZoom : public IQueryGuid
{
public:
virtual CSize SetMagnification(const int nPctX,
const int nPctY)=0;
virtual CSize GetMagnification() const = 0;
Chapter 8 Model View Controller 83
virtual CSize IncreaseMagnification(const
const
virtual CSize DecreaseMagnification(const
const
virtual void ZoomExtents(CSize& szWndExt,
const = 0;
int nPctX,
int nPctY) = 0;
int nPctX,
int nPctY) = 0;
CSize& szVpExt)
};
The CMvcLogicalPartImpl class provides a commonly used default usage of CMvcLogicalPart. It
instantiates CMvcLogicalPart with CMvcVisualPart as the first template parameter and
IVisualHost as the second parameter. CMvcLogicalPartImpl is shown in Example 63.
Example 63 – CMvcLogicalPartImpl class
typedef CMvcLogicalPart
< CMvcVisualPart<stingray::foundation::CMvcVisualComponent,
stingray::foundation::IVisualHost>
>
CMvcLogicalPartImpl;
8.3.6 Wrappers (Decorators)
The decorator design pattern is used to extend visual components with additional functionality. A
decorator wraps the borders of a visual component with margins and draws in the margins. For
example, a visual component may be wrapped with scroll bars or ruler guides. The term wrapper
and decorator are used interchangeably.
8.3.6.1 MvcWrapper_T
The MVCWrapper_T template class is the base class for visual component wrappers. It implements
the decorator design pattern by extending a base visual component class and adding margins
around its borders. The template parameter passed to the MVCWrapper_T class is the base visual
component class. Since the wrapper inherits from the visual component class, wrappers can be
added without affecting client code that uses the visual component class. In other words, the
wrapped visual component looks the same to client code as the plain visual component.
The MVCWrapper_T class overrides the SetOrigin() and SetSize() functions that it inherits
from the visual component. The origin and size are reduced by the size of the margins before being
passed to the base class. In other words, MVCWrapper_T subtracts the margins from the origin and
size. The margins for the wrapper are maintained by MVCWrapper_T in a CRect member variable
and are accessed through the SetMargin() and GetMargin() functions.
8.3.6.2 MvcBorderWrapper_T
This class decorates a visual component with a simple border. The size and color of the border are
passed in as template parameters. The following code segment declares a red, 10 pixel border
around a visual component.
MvcBorderWrapper_T<CMyVisualComp, RGB(255,0,0), 10> viscomp;
84
8.3.6.3 MvcScrollWrapper_T
This class decorates a visual component with scroll bars. It sets the wrapper margins to reflect the
width of the scroll bars. The MvcScrollWrapper_T class assumes that the visual component it decorates implements the ILogCoordinates interface. Classes derived from CMvcLogicalPart are
frequently used in conjunction with MvcScrollWrapper_T. The following code segment declares a
visual component with a scroll wrapper.
MvcScrollWrapper_T<CMyVisualComp> viscomp;
Scroll wrappers are frequently used to add scrolling capabilities to MVC viewports. The
MvcScrollWrapper_T class works equally well for plain visual components and viewports, since a
viewport is simply a type of visual component.
8.3.7 MFC Specifics
There are several MFC-specific visual component classes that existing primarily for historical reasons. Previous versions of the Stingray MVC library used a slightly different naming convention
and did not take full advantage of templates. In previous versions, the visual component inheritance hierarchy is hard-coded. It is a deep inheritance hierarchy that looks like this:
MvcVisualComponent<-MvcVisualPart<-MvcLogicalPart<-MvcViewport
This hierarchy has been replaced with framework neutral template classes, which provide a flatter
and more flexible hierarchy. The old classes are still supported, but are implemented in terms of the
new template classes. The definitions for MvcVisualComponent, MvcVisualPart, and
MvcLogicalPart are shown in Example 64. There is no difference in functionality. These definitions
simply provide aliases for the old names.
Example 64 – Definitions for MvcVisualComponent, MvcVisualPart and MvcLogicalPart
typedef CMvcVisualComponent MvcVisualComponent;
class MvcVisualPart : public CMvcVisualPart<MvcVisualComponent,
MvcVisualPart>
{
};
typedef CMvcLogicalPart< MvcVisualPart > MvcLogicalPart;
Chapter 8 Model View Controller 85
8.4
MVC Models
An MVC model encapsulates data that is rendered by viewports and manipulated by controllers. It
serves as a computational approximation or abstraction of some real-world process or system. It
captures not only the state of a process or system, but also how the system works. This makes it
easy to use real-world modeling techniques in defining your models. For example, you might
define a model that bridges a computational back-end with a GUI front-end. In this scenario, the
model encapsulates the functionality of a computation engine or hardware system and acts as a
liaison requesting the real services of the system it models.
All MVC models implement the ISubject interface so they can be observed by one or more viewports. Each type of model defines an interface for manipulating the data it encapsulates. Client
code that interacts with the model, such as the controller, can either use the model’s interface
directly or execute commands against the model. A command is an object that encapsulates an
action to be performed against the model. Commands are the key to supporting undo and redo,
which is a feature that can be easily added to MVC models.
8.4.1 CMvcModel
The CMvcModel class implements the ISubject interface and is the base class for all MVC models.
It maintains an array of IObserver pointers in order to implement the ISubject interface. In addition
to the ISubject methods it implements, the CMvcModel class defines some other methods. The
IsModified() method returns a Boolean value indicating if the model has been changed since it
was last saved. The Reset() method provides a way to clear a model and set it back to its default
state. The CMvcModel class is lightweight – its main purpose is to serve as a base class for domainspecific models. The declaration of the CMvcModel class is shown in Example 65.
Example 65 – Declaration of CMvcModel
class CMvcModel : public ISubject
{
// Constructors/destructor
public:
CMvcModel();
virtual ~CMvcModel();
// Attributes
protected:
ObserverVector m_observers;
// Operations
public:
virtual bool QueryGuid(REFGUID guid, void **ppvObj);
ULONG STDMETHODCALLTYPE AddRef();
ULONG STDMETHODCALLTYPE Release();
virtual void AddObserver(IObserver* pObserver);
virtual void RemoveObserver(IObserver* pObserver);
virtual void UpdateAllObservers(IObserver* pSender = NULL,
IMessage* pMsg = NULL);
86
virtual BOOL IsModified() const;
virtual void Reset();
};
8.4.2 Presentation Models
Frequently, a model contains graphical information that is directly rendered to the viewport. For
example, a model might contain visual components that draw themselves onto the viewport. Mixing graphical information with non-graphical information can blur the distinction between the
model and viewport. To provide a clear distinction between graphical and non-graphical data,
models are classified as either system models or presentation models. A system model is a
CMvcModel derived class that represents a non-graphical, real-world system or process. A presentation model is both a model and a visual component that can render itself to a device context. The
term visual model can also be used to describe this type of model. System and presentation models
can be used exclusively or in combination. Used in combination, a presentation model provides the
presentation for a system model, essentially mapping the real-world system into the graphical
realm. Figure 11 shows the relationship between the system model and presentation model.
Figure 11 – Relationship between the system model and the presentation model
MvcPresentationModel_T is a template class used to implement presentation models. The template parameter passed to MvcPresentationModel_T is a visual component type, which is declared
as a base class. MvcPresentationModel_T also mixes in the CMvcModel class, making the presentation model both a model and a visual component.
A good example of where this idea is useful is in the implementation of a diagramming application. It is natural to implement a diagram class as a presentation model. A diagram would be a
kind of presentation model, which manages the graphical symbol data, font choices, pen widths,
and so forth. Like a model, it manages data, albeit graphical data, and exports functionality. However, like a visual component, a diagram can draw itself, and it can even be nested as a symbol
inside of a parent diagram. Consequently, a diagram is both a model and a visual component.
Chapter 8 Model View Controller 87
Because a presentation model can draw itself, the role of the associated viewport changes to some
degree. The viewport provides a perspective onto another visual component. The presentation
model is essentially a graphics server, serving up whatever rectangular area of the canvas the viewport instructs it to paint.
8.4.3 MFC Specifics
There is an MFC-specific model class that exists primarily for historical reasons. Previous versions
of the Stingray MVC library used a slightly different naming convention. The old name is now just
an alias for CMvcModel.
typedef CMvcModel MvcModel;
88
8.5
MVC Viewports
In a nutshell, a viewport is a visual component that observes and renders a model. The term “viewport” is used to avoid confusion with the MFC CView class. At a minimum, a viewport implements
the IObserver, IVisual, IBounds2D, IEventRouter, and IVisualWindow interfaces. Viewports may
also implement other interfaces such as ILogCoordinates, IZoom, and IVisualHost. Viewports can
be lightweight, windowless objects, or they can be mixed in with window classes to create windowed objects. There is a great deal of flexibility in the way that viewport classes can be
implemented.
8.5.1 CMvcViewport
The CMvcViewport template class is the base class for all viewports. An excerpt from the
CMvcViewport declaration is shown in Example 66.
Example 66 – CMvcViewport declaration
template<typename _Visual, typename _Model, typename _Ctlr>
class CMvcViewport : public _Visual,
public IObserver,
public IEventRouterImpl
{
// Embedded types
public:
typedef CMvcViewport<_Visual, _Model, _Ctlr> ThisClass;
typedef _Visual VisualClass;
typedef _Model ModelClass;
typedef _Ctlr ControllerClass;
. . .
// Operations
public:
virtual BOOL Create(HWND hWndParent, LPRECT rc);
virtual ModelClass* GetModel() const;
virtual void SetModel(ModelClass* pModel);
virtual void SetController(ControllerClass* pController,
const bool bAutoDelCtlr = false)
virtual ControllerClass* GetController();
. . .
};
The first template parameter passed into the CMvcViewport template is the type of visual component the viewport will derive from, which can be any class that implements IVisual and
IBounds2D. This includes classes such as CMvcVisualComponent, CMvcVisualPart, and
CMvcLogicalPart, as well as any class derived from these classes. The CMvcViewport class takes a
visual component and extends it to be a viewport. The functionality of the visual component class
is inherited by the viewport. The code segment shown in Example 67 declares a viewport class
derived from CMvcLogicalPart.
Chapter 8 Model View Controller 89
Example 67 – A viewport class derived from CMvcLogicalPart
class CMyViewport : public CMvcViewport<CMvcLogicalPartImpl,
CMyModel,
CMyController>
{
. . .
};
The CMyViewport class shown above inherits the capabilities of CMvcLogicalPart, such as a logical coordinate system and support for zooming. In other words, CMyViewport supports the
ILogCoordinates and IZoom interfaces by virtue of the fact that it derives from CMvcLogicalPart.
Deriving from CMvcLogicalPart may be overkill if all you want is a simple, lightweight viewport
that doesn’t require zooming and scrolling. In that case, deriving from CMvcVisualComponent is
more appropriate. The code segment in Example 68 declares a viewport derived from
CMvcVisualComponent. The CMySimpleViewport class is leaner than CMyViewport and supports
only the basic interfaces required for a viewport.
Example 68 – Declaring a viewport derived from CMvcVisualComponent
class CMySimpleViewport : public CMvcViewport<CMvcVisualComponent,
CMyModel,
CMyController>
{
. . .
};
The CMvcViewport class also takes the type of model and type of controller as template parameters, which are used to declare type-safe functions for accessing the model and controller.
CMvcViewport also declares the following embedded data types: ThisClass, VisualClass,
ModelClass, and ControllerClass. The embedded typedefs are both a convenient short-hand and a
way for code outside of the scope of the template to have knowledge of the data types used by an
instance of the CMvcViewport template.
8.5.2 Associating Viewports with Windows
The CMvcViewport class contains several functions that require a window handle. The LPtoDP()
and DPtoLP() conversion functions and the InvalidateRect() and ValidateRect() functions
are the most notable. So the question is “how does a viewport get a window handle?” Some viewports are windowless and are contained within a window. Viewports can also be windowed, which
means that they are windows. In either case, the CMvcViewport class accesses the viewport’s window handle through the IVisualWindow interface. The IVisualWindow interface is shown in
Example 69 below.
Example 69 – IVisualWindow interface
struct __declspec(uuid("722E1FCB-034F-4030-A600-3140A9D23DB4"))
IVisualWindow : public IQueryGuid
{
virtual HWND GetWindowHandle() = 0;
};
90
Windowed viewport classes implement the GetWindowHandle() method by returning a handle to
their own window. Windowless viewports store the handle of their parent window and return that
handle in their implementation of GetWindowHandle(). The CMvcWindowlessViewport class provides an implementation of IVisualWindow for windowless viewports, shown in Example 70.
Example 70 – Implementation of IVisualWindow for windowless viewports
template <typename _Base>
class CMvcWindowlessViewport : public _Base, public IVisualWindow
{
public:
CMvcWindowlessViewport() :
m_hWnd(NULL)
{
}
protected:
HWND m_hWnd;
public:
BEGIN_GUID_MAP(CMvcWindowlessViewport<_Base>)
GUID_CHAIN_ENTRY(_Base)
GUID_ENTRY(IVisualWindow)
END_GUID_MAP
virtual BOOL Create(HWND hWndParent, LPRECT rc)
{
m_hWnd = hWndParent;
return _Base::Create(hWndParent, rc);
}
virtual HWND GetWindowHandle()
{
return m_hWnd;
}
};
MVC also provides several windowed viewport classes, which mix-in a window class with
CMvcViewport. The window viewport classes are framework-specific (either ATL or MFC) and are
discussed later in this section.
8.5.3 Getting a Device Context
CMvcViewport declares an embedded class called DC, which derived from the class CDC. The
CMvcViewport::DC class is a convenient way to get a device context for the window associated
with the viewport. The DC class gets the handle of the window that contains the viewport and
passes it to the Windows API GetDC() function, which returns a device context for the window.
The DC class can then optionally call the OnPrepareDC() function of the viewport to initialize the
device context with the appropriate settings for the viewport.
If you are using SFL with MFC support disabled, then the CDC class is defined by the SFL Graphics
package as CGraphicsContext. In other words, CDC is typedefed as CGraphicsContext. The
CGraphicsContext class is a compatible replacement for MFC’s CDC class. Refer to Chapter 10,
“GDI Classes,” for more information about the CGraphicsContext class. If MFC support is enabled,
then MFC’s CDC class is used.
Chapter 8 Model View Controller 91
The code segment in Example 71 creates a DC object and uses it to clear the viewport. The Boolean
flag passed to the constructor of the DC class indicates if the OnPrepareDC() function should be
called. The first parameter passed to the DC class constructor is a pointer to the viewport, which is
queried for the IVisualWindow interface in order to retrieve the window handle.
Example 71 – Clearing the viewport with a DC object
class CMyViewport : public CMvcWindowlessViewport<CMvcLogicalPartImpl,
CMyModel,
CMyController>
{
public:
void Clear()
{
CMyViewport::DC dc(this, TRUE);
CBrush brFill(RGB(255,255,255));
CRect rcFill(GetBounds());
dc.FillRect(&rcFill, &brFill)
}
};
8.5.4 Event Routing
A viewport routes events to its controller. The viewport is the point of contact with the window,
which receives messages or events. These messages are forwarded onto the controller to be handled. In order to receive window messages, the viewport must hook itself into the message
handling mechanism for the framework used (either ATL or MFC). For ATL, the CMessageMap
class is used to plug viewports into ATL message maps. For MFC, the OnWndMsg() and
OnCmdMsg() functions inherited from CWnd are overridden in order to capture the messages before
they are sent to the message map. In either case, hooking into the message handling mechanism is
fairly straightforward. A more detailed discussion of how the messages are intercepted by the
viewport in ATL and MFC is provided later in this section.
In addition to providing a framework-specific mechanism for handling events, MVC also uses the
SFL Events package in order to provide a framework neutral mechanism for handling events. The
Events package provides an object-oriented approach to generating and handling events. Events
are treated as objects that are handled by event listeners and routed by event routers. Encapsulating window messages in event objects provides a natural form of message cracking. The publishsubscribe relationship between event listeners and event routers is very flexible and provides a
very dynamic approach to event routing. Please refer to Chapter 6, “Events Package,” for a more
detailed discussion of the event-listener architecture.
Once the viewport receives a message from a window, it translates that message into an event
object using an event factory. The event objects are then routed to the event listeners by calling the
viewport’s RouteEvent() method. Recall that the CMvcViewport class mixes in the
IEventRouterImpl class, which defines the RouteEvent() method. The viewport’s implementation
of the RouteEvent() method passes the event to the controller, which gives its event listeners an
opportunity to handle the event.
As mentioned previously, a framework-specific bridge class takes care of translating the window
messages into events. Those classes usually take the form of a template wrapper or mix-in class
that declares a virtual GetEventFactory() method. The bridge class handles the window message
using the framework-specific mechanism and uses the event factory returned by the
92
GetEventFactory() method to create an event object. Viewport classes can override the
GetEventFactory() method and provide their own implementation of the event factory. This is
particularly useful for filtering the messages received by the viewport.
8.5.5 Scrolling
Scrolling capabilities can be added to a viewport by decorating the viewport using the
MvcScrollWrapper_T template class. In order to scroll a viewport, it must support a logical coordinate system by implementing the ILogCoordinates interface. The CMvcLogicalPart class
implements the ILogCoordinates interfaces, so passing a CMvcLogicalPart derived class as the
first parameter to CMvcViewport is an easy way to inherit an implementation of ILogCoordinates.
The code excerpt in Example 72 shows the declaration of a viewport that supports
ILogCoordinates and is capable of supporting scrolling.
Example 72 – Declaration of a viewport that supports scrolling
class CMyViewport : public CMvcViewport<CMvcLogicalPartImpl,
CMyModel,
CMyController>
{
. . .
};
Now, add scrolling by wrapping the viewport in the MvcScrollWrapper_T template as shown
below.
typedef MvcScrollWrapper_T<CMyViewport> CmyScrollingViewport;
Refer to Visual Components section for more information about MvcScrollWrapper_T.
8.5.6 Zooming
The CMvcLogicalPart class implements the IZoom interface, so instantiating the CMvcViewport
template with a CMvcLogicalPart derived class creates a viewport with zooming capabilities.
Example 73 shows the declaration of a viewport that supports zooming.
Example 73 – Declaration of a viewport that supports zooming
class CMyViewport : public CMvcViewport<CMvcLogicalPartImpl,
CMyModel,
CMyController>
{
. . .
};
The viewport can be zoomed in and out using the SetMagnification(),
IncreaseMagnification(), and DecreaseMagnification() methods inherited from IZoom.
Refer to Visual Components section for more information about CMvcLogicalPart.
Chapter 8 Model View Controller 93
8.5.7 ATL Specifics
To provide seamless integration with the ATL windowing classes, several ATL-specific viewport
classes are provided.
8.5.7.1 CMvcAtlWndViewport
This template class mixes any CWindow derived class with a viewport. The template parameters
are the viewport class and window class. The GetWindowHandle() method inherited from
IVisualWindow is implemented by returning the m_hWnd member of CWindow. The
CEventRouterMapWrapper class is mixed-in to provide an implementation of
ProcessWindowMessage() method that translates messages into event objects and passes them to
the RouteEvent() method. Refer to the Events section for more information about
CEventRouterMapWrapper.
The following code segment, Example 74, declares a class that derives from
CMvcAtlWndViewport.
Example 74 – Declaration of a class derived from CMvcAtlWndViewport
class CBullseyeViewport :
public CMvcAtlWndViewport<CBullseyeViewportBase,
CWindowImpl< CBullseyeViewport > >
{
. . .
};
8.5.7.2 CMvcClientViewport
This template class mixes SFL’s CClientWindowImpl class with a viewport. It is less generic than
the CMvcAtlWndViewport class since it mixes in a specific type of window. Like the
CMvcAtlWndViewport class, it implements the IVisualWindow interface and takes care of routing
events to the controller. The first template parameter is the derived class and the second template
parameter is the type of viewport. The code segment in Example 75 shows the declaration of an
MVC client window.
Example 75 – Declaration of an MVC client window
class CMyViewClientWnd : public CMvcClientViewport
<CMyViewClientWnd,
CMyViewport> >
{
. . .
}
8.5.8 MFC Specifics
To provide seamless integration with the MFC windowing classes, several MFC-specific viewport
classes are provided.
94
8.5.8.1 MvcViewport
The MvcViewport class is an MFC-specific implementation of a windowless viewport. This name
of this class is inconsistent with the SFL naming conventions for historical reasons. In previous versions of the Stingray MVC library, the MvcViewport class was the base class for all viewports, and
it was part of a deep inheritance hierarchy which consisted of the following classes:
MvcVisualComponent<-MvcVisualPart<-MvcLogicalPart<-MvcViewport
The CMvcViewport template class flattens the hierarchy so that viewports can derive from any
visual component class. The MvcViewport class is derived from CMvcViewport and passes in
MvcLogicalPart, MvcModel, and MvcController so that it is compatible with previous versions of
MvcViewport. An excerpt from the declaration of MvcViewport is shown in Example 76.
Example 76 – MvcViewport class declaration
class MvcViewport :
public MvcViewport_T<MvcLogicalPart,MvcModel,MvcController>
{
. . .
};
The MvcViewport class implements a windowless viewport. It maintains a pointer to a CWnd
object, which it uses to implement the IVisualWindow interface.
8.5.8.2 MvcScrollView_T
This is a template class that mixes the MFC CScrollView class with a viewport. The type of viewport is passed in as the template parameter. MvcScrollView_T takes care of synchronizing the
logical origin and size of the viewport with the scroll bars provided by CScrollView. Example 77
shows an excerpt from the declaration of MvcScrollView_T.
Example 77 – MvcScrollView_T class declaration
template<class base_t>
class MvcScrollView_T : public CScrollView, public MvcWrapper_T<base_t>
8.5.8.3 MvcBufferedWrapper_T
The MvcBufferedWrapper_T template class provides back buffering for MVC viewports. It is a template class that takes the base viewport class as a template parameter. Using this wrapper class
eliminates flicker when the viewport is rendered.
Chapter 8 Model View Controller 95
8.6
MVC Controllers
An MVC controller is an object that receives events and translates them into actions on the model
and viewport. A controller determines the behavior of an MVC component. The controller has a
strongly typed relationship with the model so that it can call methods exposed by the model’s
interface and execute commands against the model. The controller can also call methods on the
viewport. One of the attractive features of the MVC architecture is that different controllers can be
used with the same viewport and model. The behavior of an MVC component can be modified by
swapping one controller for another.
8.6.1 CMvcController
The CMvcController template class provides a base class for controllers. It takes two template
parameters: the type of model and the type of viewport. An excerpt from the declaration of
CMvcController is shown in Example 78.
Example 78 – Declaration of CMvcController
template<typename _Model, typename _Viewport>
class CMvcController : public IEventRouter
{
public:
typedef _Model ModelClass;
typedef _Viewport ViewportClass;
. . .
};
Notice that CMvcController declares embedded types for the model and viewport, which provides
a way for code outside of the scope of the class to have knowledge of the model and viewport
types.
The CMvcController class implements the IEventRouter interface. Event listeners can be added to
the controller using the AddListener() function. Event listeners can either be mixed into the controller class or aggregated into the controller.
The sample code shown in Example 79 below demonstrates an implementation for an SFL Scribble
controller. This sample code also implements the drawing canvas as an MVC component.
Example 79 – Sample Code for an SFL Scribble controller
class CCanvasController : public CMvcController<CCanvasModel,
IVisual>,
public CCommandAdapter,
public CMouseAdapter,
public ISubjectImpl
{
public:
CCanvasController() :
m_pStroke(NULL),
m_nLineWidth(1),
m_crLineColor(RGB(0,0,0))
96
{
m_ptCur.x = m_ptCur.y = 0;
// Add the controller as an event listener, since it mixes
// in event listener interfaces.
AddListener(static_cast<CCommandAdapter*>(this));
// Add the aggregrated keyboard listener
AddListener(&m_kbdListener);
}
virtual bool OnLButtonDown(UINT nFlags, POINT pt)
{
OutputDebugString("Left button down\n");
m_pStroke = new CStroke(m_nLineWidth, m_crLineColor);
GetModel()->m_strokes.push_back(m_pStroke);
m_pStroke->m_pts.push_back(pt);
GetViewport()->Draw(NULL);
return true;
}
virtual bool OnLButtonUp(UINT nFlags, POINT pt)
{
m_pStroke = NULL;
GetViewport()->Draw(NULL);
return true;
}
virtual bool OnMouseMove(UINT nFlags, POINT pt)
{
m_ptCur = pt;
if (m_pStroke != NULL)
{
m_pStroke->m_pts.push_back(pt);
GetViewport()->Draw(NULL);
}
CMouseUpdateMsg* pMouseUpd = new CMouseUpdateMsg(pt);
pMouseUpd->AddRef();
UpdateAllObservers(NULL, pMouseUpd);
pMouseUpd->Release();
return true;
}
virtual bool OnLineWidth(UINT nID, int nNotifyCode)
{
CLineWidthDlg dlg(m_nLineWidth);
if (dlg.DoModal() == IDOK)
{
m_nLineWidth = dlg.m_nLineWidth;
}
return true;
}
Chapter 8 Model View Controller 97
virtual bool OnLineColor(UINT nID, int nNotifyCode)
{
CColorDialog dlg(m_crLineColor);
if (dlg.DoModal() == IDOK)
{
m_crLineColor = dlg.GetColor();
}
return true;
}
ULONG STDMETHODCALLTYPE AddRef()
{
return 1;
}
ULONG STDMETHODCALLTYPE Release()
{
return 1;
}
BEGIN_GUID_MAP(CCanvasController)
GUID_CHAIN_ENTRY(CCommandAdapter)
GUID_CHAIN_ENTRY(CMouseAdapter)
GUID_CHAIN_ENTRY(ISubjectImpl)
END_GUID_MAP
BEGIN_COMMAND_MAP(CCanvasController)
COMMAND_ENTRY(ID_LINEWIDTH, OnLineWidth)
COMMAND_ENTRY(ID_LINECOLOR, OnLineColor)
END_COMMAND_MAP
protected:
CStroke* m_pStroke;
int m_nLineWidth;
COLORREF m_crLineColor;
POINT m_ptCur;
// Aggregate the keyboard listener instead of mixing
// it into the controller
class CKeyboardTestListener : public CKeyboardAdapter
{
public:
virtual bool OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
TCHAR msg[40];
_stprintf(msg, _T("%c pressed"), nChar);
::MessageBox(NULL, msg, _T("Key pressed"), MB_OK);
return true;
}
};
CKeyboardTestListener m_kbdListener;
};
98
8.6.2 MFC Specifics
The MvcController class provides an MFC-specific implementation of a controller that is tightly
integrated with MFC message maps. It is derived from the more generic CMvcController class and
mixes in the MFC-specific SECWndPlugIn class. The SECWndPlugIn is derived from CWnd and
provides a mechanism for receiving messages from a window. SECWndPlugIn does not actually
create a window. Instead, it is assigned the handle to the window it is plugged into. The advantage
to this approach is that the MFC Class Wizard can be used to add message handlers directly to the
controller, since it is derived from CWnd and contains an MFC message map.
This class does not conform to the SFL naming conventions for historical reasons. In previous versions of the MVC library, the MvcController class was the base class for all controllers.
Chapter 8 Model View Controller 99
8.7
Connecting the Model, Viewport, and
Controller
The model, viewport, and controller objects must be created and connected in order to function as
a unit. The connections that must be established are listed below.
пЃµ
The viewport must be given pointers to the model and controller.
пЃµ
The viewport must be added as an observer of the model.
пЃµ
The controller must be given pointers to the model and viewport.
Example 80 shows how to establish the model, viewport, and controller connections. In the sample
code, the OnCreate() function is a member of a frame window class that contains the viewport as
a member variable. The model is assumed to be global. The viewport is created by calling the
Create() member function. The handle of the parent window and the bounding rectangle of the
new viewport are passed to Create(). Next, the controller is created and passed to the viewport
using the SetController() function. The model is then passed to the viewport using the
SetModel() function, which simultaneously stores a pointer in the viewport to the model and adds
the viewport as an observer to the model. Finally, the controller is initialized by passing a pointer to
the viewport and a pointer to the model to it.
Example 80 – Establishing model, viewport, and controller connections
LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL&)
{
CRect rcClient;
GetClientRect(rcClient);
// Get a pointer to the global model object
CCanvasModel* pModel = guid_cast<CCanvasModel*>(&_CanvasModel);
// Create the viewport
m_View.Create(m_hWnd, &rcClient);
// Create the controller
CCanvasController* pCtlr = new CCanvasController();
// Initialize the viewport
m_View.SetController(pCtlr, true);
m_View.SetModel(pModel);
// Initialize the controller
pCtlr->SetViewport(&m_View);
pCtlr->SetModel(pModel);
return 0L;
}
An alternative to the previous scenario is to have the viewport create and initialize the controller.
The CMvcViewport class defines a virtual CreateController() function that can be overridden to
create and initialize a default controller for the viewport. The CreateController() function is
called during the creation of the viewport, which makes it unnecessary to explicitly initialize the
connections for the controller. Example 81 shows how to create and connect the model, viewport,
and controller using the CreateController() function in the viewport.
100
Example 81 – Creating and connecting the model, viewport and controller
class CCanvasViewport : public CMvcViewport<CMvcLogicalPartImpl,
CCanvasModel,
CCanvasController>
{
public:
. . .
virtual BOOL CreateController()
{
CCanvasController* pCtlr = new CCanvasController();
pCtlr->SetViewport(this);
pCtlr->SetModel(GetModel());
SetController(pCtlr);
return TRUE;
}
. . .
};
LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& bHandled)
{
CRect rcClient;
GetClientRect(rcClient);
// Create the viewport
m_View.Create(m_hWnd, &rcClient);
m_View.SetModel(guid_cast<CCanvasModel*>(&_CanvasModel));
return 0L;
}
8.7.1 CMvcComponent
The CMvcComponent class combines a model, viewport, and controller into a single object. It takes
care of making the connections between the model, viewport, and controller objects and provides a
single abstraction for the user to deal with. The template parameters passed into CMvcComponent
are the type of model, type of viewport, and type of controller. The CMvcComponent class aggregates the model, viewport, and controller objects. The model, viewport, and controller objects are
either be passed into the component or they are created automatically by CMvcComponent.
CMvcComponent provides a Create() method identical to the viewport’s Create() method,
which will create the model, viewport, and controller objects, if they have not already been
assigned by the time Create() is called. CMvcComponent also provides an implementation of
QueryGuid() which delegates to the aggregated model, viewport, and controller objects. This
means that any interfaces support by the model, viewport, or controller objects are also supported
by the component. The code below shows the declaration of an MVC component.
Example 82 – MVC component declaration
typedef CMvcComponent<CBullseyeModel,
CBullseyeViewport,
CBullseyeController>
CBullseyeComponent;
Chapter 8 Model View Controller 101
8.7.2 ATL Specifics
The CMvcAtlComponent class is extends the CMvcComponent class by mixing in CMessageMap. It
overrides the ProcessWindowMessage() function it inherits from CMessageMap and translates
messages into event objects and routes them to the viewport. The advantage of using
CMvcAtlComponent to create MVC components is that you can add the component to an ATL
message map using the CHAIN_MSG_MAP_MEMBER macro. Example 83 forwards messages to an MVC
component using the CHAIN_MSG_MAP_MEMBER macro.
Example 83 – Forwarding messages to an MVC component
typedef CAtlMvcComponent<CBullseyeModel,
CBullseyeViewport,
CBullseyeController>
CBullseyeComponent;
. . .
class CMainFrame : public CFrameWindowImpl<CMainFrame>
{
. . .
CBullseyeComponent m_bullseye;
. . .
BEGIN_MSG_MAP(CMainFrame)
CHAIN_MSG_MAP_MEMBER(m_bullseye)
END_MSG_MAP()
. . .
};
102
8.8
MVC Commands and Undo/Redo
A command is an object that encapsulates an action to be performed on another object or command
receiver. Commands invoke one or more methods on the command receiver and store the data
required as parameters for those methods. It is also possible to reverse or undo the action performed by a command. Since commands are objects, they can be stored, logged, and used to
support undoable operations. Commands are used in MVC to perform actions on models and to
support undo and redo capabilities. Although the command design pattern and the undo and redo
capabilities are not part of the basic MVC design pattern, they complement MVC very nicely.
8.8.1 CMvcCommand
The CMvcCommand class provides a base class for all commands. It defines virtual Execute() and
Unexecute() functions, which are overridden by derived classes. Classes derived from
CMvcCommand must store the data required to execute the command, which usually includes a
pointer to the object that is acted upon by the command. The object acted upon by a command is
referred to as the command receiver, and is usually a model in the case of MVC. Implementing the
Unexecute() command is optional, so the CMvcCommand class defines the IsUndoable() function. Command classes that implement the Unexecute() function must be capable of restoring the
command receiver to the state it was in prior to the call to Execute(). Since commands store the
parameters needed to execute and undo an action, they can be thought of as persistent function
calls.
8.8.2 Commands as Messages
The CMvcCommand class implements the IMessage interface, which is part of the notification
mechanism of the subject-observer design pattern (see Section 4.2, “The Subject-Observer Pattern.”) Messages are sent from the subject to observers via the OnUpdate() function. An IMessage
pointer is passed as a parameter to the OnUpdate() function and used to determine the nature of
the change made to the subject. Since commands are messages, they can be used to notify observers
of the changes made to the model they execute against. After a command is executed, it can be
passed as the message parameter to the model’s UpdateAllObservers() function.
8.8.3 IMvcUndoRedo
The IMvcUndoRedo interface defines methods for executing, undoing, and redoing commands.
The IMvcUndoRedo interface is shown in Example 84. The Do() method executes the given command and logs it. The other methods in this interface are fairly self-explanatory.
Example 84 – The IMvcUndoRedo interface
class IMvcUndoRedo
{
public:
/* Execute and log a command*/
virtual BOOL Do(MvcCommand* pCmd) = 0;
/* Undo a command*/
virtual MvcCommand* Undo() = 0;
Chapter 8 Model View Controller 103
/* Redo
virtual
/* What
virtual
/* What
virtual
a command*/
MvcCommand*
is the next
MvcCommand*
is the next
MvcCommand*
Redo() = 0;
command on the undo stack*/
PeekUndo() = 0;
command on the redo stack*/
PeekRedo() = 0;
};
8.8.4 MvcTransactionModel
The MvcTransactionModel class is an MVC model that implements command undo and redo
capabilities. An excerpt from the declaration of MvcTransactionModel is shown in Example 85.
Example 85 – Declaration of MvcTransactionModel
class MvcTransactionModel : public MvcModel
{
. . .
public:
/* Reset the state of the transaction model to its initial
state*/
virtual void Reset();
/* Tests whether the transaction model has stored new commands
since last save*/
virtual BOOL IsModified() const;
/* Records the specified command for later undo or event
logging*/
virtual BOOL Log(MvcCommand* pCmd);
/* Execute and log a command*/
virtual BOOL Do(MvcCommand* pCmd);
/* Undo a command*/
virtual MvcCommand* Undo();
/* Redo a command*/
virtual MvcCommand* Redo();
/* Get the command that will be reversed next time Undo is
called*/
MvcCommand* PeekUndo();
/* Get the command that will be execute next time Redo is
called*/
MvcCommand* PeekRedo();
/* Set the number of commands that can be stored by the
transaction model*/
void SetHistorySize(int m_nHistorySize);
. . .
};
104
The MvcTransactionModel maintains two separate stacks of commands: an undo stack and a redo
stack. As commands are executed, they are pushed onto the undo stack. If the transaction model is
instructed to undo the most recent command, it pops the command off the top of the undo stack
and invokes the Unexecute() function. Then, it pushes the command onto the redo stack. If a redo
is requested, the exact opposite occurs.
Chapter 8 Model View Controller 105
8.9
MVC Principles and Practice
MVC means different things to different people. While the architecture has been implemented
many times by many frameworks, no two implementations of MVC are alike. This stems from the
fact that MVC is conceptually rich, but leaves much open to interpretation in order to preserve
generality.
SFL has produced another implementation of MVC and with it, another interpretation of its concepts. Our objective has been to produce a modern, highly flexible implementation of MVC with
minimal framework and platform dependencies. Below is a set of principles and best practices that
we’ve upheld in the design of the core MVC classes and that you should continue to uphold in
your application of them.
8.9.1 Minimize Coupling
The MVC triad contains both strong (derived type) and weak (base type) references. It’s important
that you know which are which and adhere to them. Obviously, the view and controller classes
must have strong references to the model – they exist to exercise the model’s domain-specific queries and commands. However, the model should never strongly reference its views or controllers.
A model should export its services through queries, commands and notifications to any interested
party. Therefore, the view and controller should have one-sided knowledge of their model’s
capabilities.
8.9.2 Avoid “Positional Awareness” in the Controller
It may seem logical to assume that the class that receives mouse events, the controller, should also
perform hit testing. In most cases, this is a mistake. Misplacing hit testing logic in the controller
mandates tight coupling with the view, which counteracts the value of their separation.
The view is the one that computes and renders the display, so it already knows where things are on
screen. So logically, it should perform hit testing. When the controller gets a mouse event, its first
order of business is to request a hit test from the view. The view returns the hit object, and the controller decides what to do with it. This approach frees the controller to focus on behavior only,
resulting in simpler logic and less coupling to the view. Moreover, it affords the flexibility to swap
out one view for another without impacting the controller.
8.9.3 Use Interface-Based Programming Techniques
MVC and interface-based programming techniques are a powerful combination. By using interface-based programming techniques to realize the MVC triad, coupling and complexity are
reduced. For example, rather than have the view and controller depend directly on a particular
implementation of a model, they can depend on an interface which the model implements. So, any
subject that implements this interface can serve as a model to this view and controller pair.
This does not refer to IDL interfaces that can be made remote. Within the triad, native language
interfaces are easier and more flexible to declare and use. Refer to Chapter 3, “Interface-Based Programming,” for more information.
106
8.9.4 Use Commands to Define the Model’s Services
Again, the model defines queries, commands and notifications. Command, in this case, means the
command design pattern, also known as functors. You could choose to implement the model’s services in terms of public member functions. However, that approach doesn’t provide for record
keeping. For example, you might want to create an audit trail of services rendered by the model for
undo and redo or analysis purposes. Commands facilitate this, because they essentially transform
functions into objects, which can store parameters and execution results. Commands can be executed, unexecuted, printed and stored. What’s more, because commands are objects, they can
double as the notification. In other words, the command is both the executor of change within the
model and the messenger of change to all views.
So, a model should define and export a command dictionary, which is a set of classes that operate on
the model’s state. The controller imports this dictionary and triggers execution of one or more commands in response to some event. After the command completes its execution, the model forwards
it as the notification of change to all views. Finally, the views can inspect the type and state of the
command to determine how they should respond.
8.9.5 Exploit Hierarchical Decomposition
MVC scales from components to systems. For example, you can base a single control on MVC or an
entire 3-tier application. This is because MVC is inherently hierarchical, allowing small systems to
be composed into larger ones. Other MVC adaptations have failed to recognize this, and precluded
any ability to nest – MFC’s CDocument, for example. A model should be capable of composing and
observing multiple submittals. A view can be composed of many sublevels, which are attached to
submittals. Moreover, a controller can nest subcontrollers, sometimes called tasks, delegating control as appropriate.
8.9.6 Distinguish Between Architecture and Technology
Why should you use MVC when there are already so many frameworks that take full advantage of
the latest technologies? The answer is, MVC is not meant to be yet another framework, rather it is
designed to complement and extend existing ones. MVC is about architecture, whereas most
frameworks are concerned less with architecture and more with technology and platform-leverage
– ATL, for example.
Of course, both are necessary, but it is certainly advantageous to keep the architecture and platform-dependencies separate and distinct. Very often, the technologies used to implement a system
become an integral part of its architecture, which can limit flexibility. However, to the extent possible, your architecture should abstract and encapsulate the technical and platform details so team
members can effectively specialize. Moreover, this focus on architecture and encapsulation helps to
reduce complexity, while minimizing and localizing the impact of shifting technologies.
Although achieving a separation of architecture and technology is hard, MVC can help. Think of
each triad as a completely self-contained entity. The triad needs only a host to occupy and it can do
the rest, because it is container-independent. This container-independence requirement is key,
because that is where many technology and platform dependencies live. Conversely, it places several constraints on design; any tight coupling to the host must be eliminated. For example, you can
implement the view as a derived window or as a rectangle that is aggregated by a window. The
Chapter 8 Model View Controller 107
first approach is typically used (MFC’s CView, for example, derives from CWnd), but the latter
approach is much better. A rectangle that simply draws itself knows nothing of window types or
platform specifics, yielding platform independence without the usual compromises in appearance,
power and flexibility. In addition, a view of this form is lighter and can be hosted anywhere.
The same can be said of the controller and model. The controller requires events, but doesn’t care
how they are delivered. Since the model begins as a platform independent abstraction, we need
only to keep such dependencies out of its interface. In general, the platform-centric framework
should aggregate and host an MVC triad. The hosting involves delivering events to the triad and
giving it space to render to. This design makes your code more architecture-centric and less platform-dependent. Even if that’s not a goal, you’ll certainly appreciate how this encapsulation of
implementation details tends to simplify your code.
8.9.7 Capture the System in the Model
Because the MVC triad is composed of three collaborating parts, it may seem natural to think of the
entire triad as representing the “system” or component. And therefore, that the implementation of
the system’s capabilities can be hosted in any class in the triad. However, this is not a good design
choice. The model should be the one class that represents the system and exclusively responsible
for encapsulating its state and functionality. The viewport and controller classes are simply clients
to the model that render its state and request its services. The model should be defined as a software-IC that can be embedded within an MVC triad or exercised programmatically by a batchoriented client.
Before designing the interface to your model, view and controller class, consider what the system is
that you are modeling. Identify the system in terms of what capabilities are part of its services and
what capabilities are not. Then, design the model class so that it captures all of the system’s capabilities. But, of equal importance, design the view and controller classes so that they capture none
of them.
8.9.8 Use MVC as a Widget Architecture
What if you are developing a tree control, or a list control. Is MVC useful for such small-scale widgets? The answer is yes. MVC applies equally to application architecture and widget architecture
concerns. Used as a widget architecture, the MVC model encapsulates the state and functionality of
the widget. A tree control, for example, can add nodes, remove nodes, expand and rename nodes,
and so on. These are examples of functions that would exist in the model of a tree control. And
since the functionality of the tree control will be completely and exclusively contained in its model,
the model’s interface becomes the primary programmatic interface to the tree control.
8.9.9 Distinguish Between Graphical and Non-Graphical
Systems
A model is a software analog for a real system with state and function. But, what types of systems
should a model – well, model? Actually, any system that has a well-defined and self-contained
function can be modeled, no matter how large or small. One might model a nuclear reactor or a
108
clock. But these are obvious. Less obvious are the graphical systems, such as diagramming applications or spreadsheets. In these cases, most of the function and state is graphical. So, how do we
delineate, without ambiguity, between model and view responsibilities?
The reality is that there are two independent systems in these scenarios, one graphical and one not.
Often, this fact isn’t recognized and they end up combined into one model or triad, which can lead
to ambiguities in the design. But, graphical systems are systems too and should be modeled separately, through a presentation model.
A presentation model is a model whose purpose is to abstract and serve the functionality of a
graphical system. Like other types of systems, graphical systems come in all sizes. Consider a user
interface system such as a tree control. A tree control has a well-defined, self-contained purpose
and can be embedded within larger systems. Its model possesses state (hierarchy data, for example) and function (such as add, remove, and expand nodes). And the tree control’s MVC triad
constitutes a system that can be embedded within larger MVC triads.
Figure 12 – The MVC triad with a presentation model
System
Model
...
System
Model
Presentation
Model
Subject-Observer
View
Controller
Figure 12 depicts an MVC triad with a presentation model. The presentation model represents the
model component of the MVC triad, serving view and controller requests. However, it isn’t the
only model in this scenario. There may also be one or more system models. A system model
abstracts some external (perhaps physical) system. The presentation model’s job is to implement a
separate visual system, which serves to present the underlying system’s state and capability.
Now, consider a more sophisticated graphical system such as a schematic editor. A schematic editor enables you to visualize and design electronic circuitry. Both the schematic editor and the
system under design are independent systems and should be modeled independently. The circuitry’s model would contain information such as the physical chips, connections, and timing
properties. The schematic editor’s model will store the visual counterparts to each chip with the
queries and commands to add, remove, stretch, connect them, and so on. Commands on the sche-
Chapter 8 Model View Controller 109
matic editor’s model may implicitly invoke commands on the system model (adding a
component). Lastly, upon receipt of a change notification, the schematic editor presentation model
will perform reactive processing and potentially broadcast its own notification.
110
8.10 Using MVC in MFC Applications
The MVC classes are designed to be general-purpose and can be incorporated into your MFC
application in any number of ways. You could, for example, use MVC alone, as an alternative to the
document/view architecture. You could also use parts of MVC to take advantage of its reuse
potential. You can use as much or as little MVC in your application as you find appropriate and
even mix it with document/view based code.
In an MVC triad, you have three primary parts that you need to integrate into your new or existing
application. The model is usually integrated via containment into the document class. The viewport is usually integrated via containment into the window or CView-derived class. Lastly, the
controller is normally instantiated by the viewport it controls. The remainder of this section
describes the steps required to incorporate an MVC triad into your application.
8.10.1 Define a Model Class
To define a model class, complete the following steps:
1. Create your CMvcModel derived class. At this point, you can either derive your model
from CMvcModel or MvcPresentationModel_T. For the purposes of this tutorial, we’ll use
the presentation model as a base. If you require serialization support in your model, you
need to multiply inherit your model from CObject and MvcPresentationModel_T.
class CloudDiagram : public CObject, public
MvcPresentationModel_T<CMvcVisualComponent>
{
public:
~CloudDiagram();
2. Add your model class as a member variable inside your document.
class CMyDoc : public CDocument
{
// Attributes
protected:
CloudDiagram m_CloudDiagram;
3. Create an accessor member inside your document that simply returns the model.
CloudDiagram* GetCloudDiagram() {
return &m_CloudDiagram;
};
4. Override CDocument::IsModified() so that it tests the modified flag of the contained
model also.
BOOL CMyDoc::IsModified()
{
return CDocument::IsModified() ||
GetCloudDiagram()->IsModified();
}
Chapter 8 Model View Controller 111
5. Override your document’s serialize member so that the contained model is serialized.
void CMyDoc::Serialize(CArchive& ar)
{
GetCloudDiagram()->Serialize(ar);
}
6. If your model creates and destroys objects for which you want to support undoable deletion, multiply derive those objects from IRefCount.
class CloudComponent : public CObject,
public CMvcVisualComponent,
public IRefCount
{
. . .
7. Override the cloud diagram’s Draw() member and implement its data presentation.
void CloudDiagram::Draw(CDC* pDC)
{
Iterator_T<CloudComponentPtr> i(GetClouds());
for (CloudComponent* pCloud = i.GetFirst();
pCloud; pCloud = i.GetNext())
pCloud->Draw(pDC);
}
8.10.2 Define a Controller Class
To define a controller class, complete the following steps:
1. Create a controller class that understands how to translate events into actions on the model
and viewport classes. The MvcController class is used instead of the more generic
CMvcController, because it contains an MFC message map and can have message handlers
added to it using the MFC Class Wizard. When deriving from the MvcController class, it is
convenient to define a type-safe access function for the model.
class CloudController : public MvcController
{
// Constructors
public:
CloudController();
virtual ~CloudController();
// Overrides
public:
CloudDiagram* GetDiagram () {
return (CloudDiagram*)m_pModel;
};
};
112
2. Add a message map to your controller class so that Class Wizard can be used to manage
your message handlers.
// Generated message map functions
protected:
//{{AFX_MSG(CloudController)
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
//}}AFX_MSG
FOUNDATION_DECLARE_MESSAGE_MAP()
3. Generate a new Class Wizard file, which incorporates your new controller class. You can do
this by deleting the *.clw files and then running the Class Wizard. Class Wizard prompts
you to rebuild the .clw file.
4. To incorporate your controller into your application, the last step is to include it in the standard message routing. This step allows your controller to listen and handle the messages
being sent to the containing window. You must override two functions - OnWndMsg() and
OnCmdMsg() as follows:
BOOL CMvcCloudView::OnCmdMsg(UINT nID, int nCode,
void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
{
// First pump through normal channels.
// This allows you to override the components
// default handling inside the view class.
if (m_component.OnCmdMsg(nID, nCode, pExtra,
pHandlerInfo))
return TRUE;
else
return CView::OnCmdMsg(nID, nCode, pExtra,
pHandlerInfo);
}
BOOL CMvcCloudView::OnWndMsg( UINT message, WPARAM wParam,
LPARAM lParam,
LRESULT* pResult )
{
// First pump through normal channels.
// This allows you to override the components
// default handling inside the view class.
if (m_component1.OnWndMsg(message, wParam,
lParam, pResult))
return TRUE;
else
return CView::OnWndMsg(message, wParam,
lParam, pResult);
}
Chapter 8 Model View Controller 113
8.10.3 Define a Viewport Class
To define a viewport class, complete the following steps:
1. Create your CMvcViewport derived class. Pass a visual component class, such as
CMvcLogicalPartImpl, in as the first template parameter. Pass in the type of model and
type of controller as the other two template parameters. Override the Draw(),
CreateController(), OnInitalUpdate(), SetVirtualSize(), and GetVirtualSize()
members.
class CloudViewport : public CMvcViewport<CMvcLogicalPartImpl,
CloudDiagram,
CloudController>
{
public:
virtual void Draw(CDC* pDC);
virtual BOOL CreateController();
virtual void OnInitialUpdate();
void SetVirtualSize(int cx, int cy);
CSize GetVirtualSize() const;
2. Add your viewport as a member of your CWnd or CView derived class.
class CMyView : public CView
{
protected: // create from serialization only
CMyView ();
FOUNDATION_DECLARE_DYNCREATE(CMvcCloudView)
// Attributes
protected:
MyViewport m_component;
3. Create your viewport and attach it to the model that is contained in the document. This is
accomplished via a call to the viewport’s Create() and SetModel() members respectively.
This initialization is typically done from the OnCreate() member.
int CMyView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
CMvcCloudDoc* pDoc = GetDocument();
CloudDiagram* pModel = pDoc->GetCloudDiagram();
m_component.Create(this->m_hWnd, NULL);
m_component.SetModel(pModel);
return 0;
}
114
4. Delegate all calls to OnInitialUpdate() and OnDraw() to your viewport from the CView or
CWnd-derived class that contains it. This gives your viewport the opportunity to initialize
and render itself on the drawing surface of its container.
void CMyView::OnInitialUpdate()
{
m_component.OnInitialUpdate();
}
void CMyView::OnDraw(CDC* pDC)
{
CMyDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
m_component.Draw(pDC);
}
5. Next, size and position your viewport to occupy the entire client area of its container.
void MyView::OnSize(UINT nType, int cx, int cy)
{
m_component.SetOrigin(0, 0); // Position the viewport
m_component.SetSize(cx, cy); // Size the viewport
CView::OnSize(nType, cx, cy);
}
6. Override the CreateController() method in your viewport and create and initialize the
controller.
BOOL CloudViewport::CreateController()
{
m_pCtlr = new CloudController;
m_bAutoDelCtlr = true;
return TRUE;
}
Setting the m_bAutoDelCtlr flag instructs the viewport to destroy the controller in its
destructor. In other words, it ties the lifetime of the controller to the viewport. If you don’t
want to tie the lifetime of the controller to the viewport, then set the bAutoDelCtlr flag to
FALSE, but make sure you take care of deleting the controller at the appropriate time.
Having the viewport create the controller is optional. It is only provided as a convenient
mechanism for creating a default controller for the viewport. In many cases, it is not desirable to have the viewport have knowledge of the type of controller. In fact, you may want to
use several types of controllers with your viewport class. If that is the case, then do not
override CreateController(). Instead, create the controller outside of the scope of your
viewport class and assign it to the viewport using the SetController() method.
7. Next, override the Draw() method and supply code to render the model onto viewport.
Since the model in this sample is a presentation model, the viewport can simply instruct the
model to draw itself.
void CloudViewport::Draw(CDC* pDC)
{
OnPrepareDC(pDC);
GetCloudDiagram()->Draw(pDC);
}
Chapter 8 Model View Controller 115
8. Override OnInitialUpdate in your viewport class. This is a good place to initialize the logical and container extents of the viewport’s client area. Basically, all this statement indicates
is that for every 1000 units along the X-axis in the container’s client area, there are 4000 logical units in this viewport. We didn't use 1 and 4 because small values like these don’t leave
much room for zooming in and out. Extents can never go below 1 because
CDC::SetWindowExt() expects an integer.
void CloudViewport::OnInitialUpdate()
{
SetAxisExtents(X, 4000, 1000);
SetAxisExtents(Y, 4000, 1000);
}
9. Define the Get() and Set() functions for the virtual size of the viewport. The virtual size of
the viewport is equated to the size of the diagram because the diagram is rendered through
the viewport and may be larger than the viewport. Consequently, the size of the diagram is
the virtual size of the viewport.
void CloudViewport::SetVirtualSize(int cx, int cy)
{
GetDiagram()->SetSize(cx, cy);
}
CSize CloudViewport::GetVirtualSize() const
{
return GetDiagram()->GetSize();
}
You are done. At this point, you have a completely reusable component integrated into your document/view application that defines its control, its data, and its rendering. It can be moved to any
other MFC application using the same steps outlined above.
116
Chapter 9
Print Package
9.1
The Print Package
The Print package provides a layer of abstraction on top of the Windows printing API, in addition
to providing the structure for adding print and print preview support to applications. The primary
abstractions involved in printing include:
пЃµ
Print jobs. Control the rendering of printable objects to documents.
пЃµ
Printable objects. Implement the IPrintable interface, which contains methods for
printing the object one page at a time to a document.
пЃµ
Document objects. Provide an interface for rendering output to a printer device or
file.
пЃµ
Printer configuration. Assigned by the document; used to open the printer and get
a device context for printing.
Chapter 9 Print Package 117
9.2
Printable Objects
Objects that implement the IPrintable interface can participate in printing and print preview. The
IPrintable interface is shown in Example 86 below.
Example 86 – The IPrintable interface
class __declspec(uuid("9B35CA0F-7A12-401e-8BD5-4330074FF35B"))
IPrintable
{
public:
/* Get number of pages in document. */
virtual int GetPageCount(CPrintDoc* pPrintDoc) = 0;
/* Prepare the next page for printing. */
virtual bool BeginPage(CPrintDoc* pPrintDoc) = 0;
/* Print the current page to the print document. */
virtual bool PrintPage(CPrintDoc* pPrintDoc) = 0;
/* Cleanup after printing a page. */
virtual bool EndPage(CPrintDoc* pPrintDoc) = 0;
};
Printable objects are expected to output one page at time to a document object. They implement the
BeginPage(), PrintPage(), and EndPage() functions in order to print a single page. The
BeginPage() and EndPage() functions provide the printable object with an opportunity to separate the process of printing a page into three steps. The document object passed into these functions
provides a device context on which to render the pages. Printable objects must also provide a count
of the total number of printed pages in a given job by implementing the GetPageCount() function.
A printable object can implement GetPageCount() either by returning the number of pages it contains or by returning –1 to indicate that the printing should continue until PrintPage() returns
false. Printable objects do not drive the printing process – they simply respond to requests for
pages of printed output.
118
9.3
Print Documents
Document objects provide an interface for rendering output to a printer device or file. The
CPrintDoc class encapsulates the Windows DOCINFO structure, printer configuration data, and a
printer device context (DC). The printer configuration data stored by CPrintDoc is used to create a
device context for the printer on which the document will be printed. The printer device context is
passed, along with the DOCINFO structure, to the Windows API function StartDoc(). The
StartDoc() method returns a job identifier and ensures that output from multiple print jobs is not
mixed together by the printer. In other words, the StartDoc() method ensures that documents are
queued to the printer.
The CPrintDoc class actually maintains two DCs in order to support print preview: a print DC and
an output DC. The print DC always represents a printer even if the output is sent to a window, as in
the case with print preview. The output DC can represent either a window or a printer. In the case
of print preview, the output DC represents a window. In the case of normal printing, the output DC
and print DC are identical. Two DCs are needed because print preview emulates printing in a window, which means that two devices are involved, each with different characteristics—such as
device resolution. The CPrintDoc class defines the GetOutputDC() and GetPrintDC() methods for
accessing the two device contexts.
Printable objects use the device contexts maintained by the CPrintDoc class in order to render
pages of output to the document. Notice that the BeginPage(), PrintPage(), and EndPage()
methods of the IPrintable interface all take a CPrintDoc as a parameter. Printable objects also
query the document for other information relevant to printing, such as the current page number.
Chapter 9 Print Package 119
9.4
Printer Configurations
The Windows API defines two structures for identifying and configuring printers, or more specifically, printer device drivers. The DEVNAMES structure identifies a particular printer device driver,
and the information it contains can be used to create a device context for the specified printer. The
DEVMODE structure is used in conjunction with the DEVNAMES structure in order to create a device
context for the specified printer. The DEVNAMES structure contains information such as page orientation, page size, and margins that is used to configure the printer device context. Anyone who has
used the DEVNAMES and DEVMODE structures and the Windows printing API directly will tell you
how difficult and time consuming it can be.
The CPrinterConfig class encapsulates the DEVNAMES and DEVMODE structures and hides the messy
details of the Windows API functions that use these structures. The CPrinterConfig class takes care
of allocating and manipulating the DEVNAMES and DEVMODE structures, which is particularly nice
since these are variable length structures whose size is determined by the printer device driver.
CPrinterConfig provides methods such as GetOrientation(), SetOrientation(),
GetPaperSize(), SetPaperSize(), GetNumCopies(), and SetNumCopies() for accessing the data
in these structures. The PRINTDLG and PAGESETUPDLG structures can be used to store and retrieve
the printer configuration using the following methods: LoadPrintDlg(), StorePrintDlg(),
LoadPageSetupDlg(), and StorePageSetupDlg().
This makes it incredibly simple to use the CPrinterConfig class in conjunction with the common
Windows print dialogs. The default system printer can be easily loaded into the printer configuration using the SetDefaultPrinter() method. The CPrinterConfig class also provides direct access
to the DEVNAMES and DEVMODE structures through the GetDevNames() and GetDevMode() methods,
so that you can still get to them if you need to.
120
9.5
Printers
The CPrinterConfig class provides a way to identify a printer and create a device context for it
using a particular configuration. The Windows API also provides functions for directly accessing
printers and the print spooler. The Windows API function OpenPrinter() returns a handle for a
printer that can be used in conjunction with several other functions such as ReadPrinter(),
WritePrinter(), GetJob(), PrinterProperties(), and DeletePrinter(). The CPrinter class
encapsulates a printer handle and the Windows API functions for accessing printers and the
printer spooler.
The CPrinter class implements an Open() method, which takes the name of a printer and calls the
OpenPrinter() Windows API function in order to get back a valid printer handle. The CPrinter
class also has the Attach() and Detach() methods for assigning an existing printer handle to a
printer object. The remaining methods in this class are simple wrappers for Windows API functions that operate on a printer handle.
Chapter 9 Print Package 121
9.6
Print Jobs
The CPrintJob class encapsulates the task of sending a document to a printer. A print job takes a
printable object and a document and coordinates the task of printing to the document. When a
print job is started, it uses the IPrintable interface implemented by the printable object to print
pages to the document.
The CPrintJob class uses the IPrintable interface and CPrintDoc classes. At the beginning of a job,
CPrintJob calls the StartDoc() function on the CPrintDoc object and is returned an integer value
that identifies the job on that printer. Once CPrintJob has started a job on the printer by calling
StartDoc(), it invokes the virtual OnPrintDocument() method, which iterates over the pages contained by the printable object and prints them. The CPrintJob class also implements functions for
controlling the print job such as Cancel(), Pause(), Resume(), Restart(), Delete() and
SetPriority().
122
9.7
Print Preview
The print preview feature provides a way for users to view a printed document on the screen
before it is actually sent to a printer. The print preview window allows the user to page up and
down through the output to see every page. The output shown in the print preview window
should be identical to the output generated by the printer.
The print preview feature is implemented with the help of the Model-View-Controller (MVC)
package. The implementation treats print preview as an MVC component that can be mixed into or
aggregated with any type of window. Using MVC to implement print preview also makes it easy to
swap in and out different types of viewports and controllers in order to modify or enhance the
appearance and behavior of the print preview window. It also makes it possible to use wrapper
classes for decorating the print preview viewport with scroll bars or ruler guides. Most of the print
preview functionality is implemented in the following three classes: CPrtPreviewModel,
CPrtPreviewViewport, and CPrtPreviewController.
Using the print preview MVC classes is just like using any other MVC component. The viewport is
either aggregated into a window or mixed into a window class. Paint messages are delegated to the
viewport’s Draw() method in order to render the data in the model. The model, viewport and controller are connected together and cooperate to provide a service.
Once the CPrtPreviewModel, CPrtPreviewViewport and CPrtPreviewController have been created and connected together, a printable object and document must be supplied to the model. The
CPrtPreviewModel class implements a Start() method, which takes an IPrintable and CPrintDoc
as parameters. The Start() method stores pointers to the IPrintable and CPrintDoc objects in the
model, creates and initializes a printer DC, and calls UpdateAllObservers(). The call to
UpdateAllObservers() informs the viewports to render the model. The class CPrtPreviewModel
also keeps track of the current page and the number of pages to display in the viewports.
Chapter 9 Print Package 123
9.8
Using Print Preview with ATL
The CPrintPreviewFrameImpl class makes it easy to use print preview in ATL. It can be used to
add print preview capabilities to ATL windows. It is a template class that takes the base window
class as a template parameter. The CPrintPreviewFrameImpl class contains a print preview model,
viewport, and controller. It takes care of creating and initializing the print preview model, viewport, and controller objects.
To use the CPrintPreviewFrameImpl class, just instantiate it with an ATL-based window class as
the first template parameter. The CPrintPreviewFrameImpl class handles both the print and print
preview commands. The command identifiers for both commands can optionally be passed in as
template parameters. The default command identifiers are IDC_SFL_FILE_PRINT and
IDC_SFL_FILE_PRINT_PREVIEW. The CPrintPreviewFrameImpl class defines the virtual method
BeginPrintPreview(), which is called when a print preview command is received. The
BeginPrintPreview() method takes care of creating and showing the print preview window. A
corresponding EndPrintPreview() method closes the print preview window. The
OnBeginPreview() and OnEndPreview methods give derived classes an opportunity to perform
custom tasks before the print preview window is shown and after it is closed.
The CPrintPreviewFrameImpl class is actually an abstract base class. It defines the pure virtual
method GetCurrentPrintable(), which must be implemented by derived classes. Rather than
hardwiring into the framework knowledge of which object to print, as MFC does with Document/View, the GetCurrentPrintable() method allows the CPrintPreviewFrameImpl class to
avoid making any assumptions about what to print.
124
Chapter 10
GDI Classes
10.1 SFL Graphics
The Stingray Foundation Library includes a set of wrapper classes for the objects in the Win32
Graphic Device Interface (GDI) API.
These wrappers allow an application to use a more object-oriented approach when dealing with
GDI objects. They also provide some graphics primitives not available as direct API calls.
Chapter 10 GDI Classes 125
10.2 GDI Objects
The Win32 API defines the following types of GDI objects:
пЃµ
Pen
пЃµ
Brush
пЃµ
Bitmap
пЃµ
Font
пЃµ
Region
пЃµ
Palette
Similarly, SFL defines a correspondent wrapper class for each of those GDI object types:
пЃµ
CGDIPen
пЃµ
CGDIBrush
пЃµ
CGDIBitmap
пЃµ
CGDIFont
пЃµ
CGDIRgn
пЃµ
CGDIPalette
All GDI Object wrappers in SFL derive from the CGDIObject<> class. This class is templatized by
the specific handle type of the object it is wrapping. So for example, CGDIPen specializes
CGDIObject<HPEN>, whereas CGDIRgn derives from CGDIObject<HRGN>.
CGDIObject<> encapsulates the common functionality applicable to all types of GDI objects.
10.2.1 Creation and Destruction
Each GDI object type has its own set of API calls used for creation of a new object. Each one of them
takes its own set of creation parameters: the parameters necessary to create a new pen are different
than the parameters required for a new font. For this reason, the declaration of the creation methods in the SFL GDI object wrappers differs from one class to another.
For example, the code in Example 87 creates a new font based on the font information given by the
user using the font common dialog:
Example 87 – Creating a new font based on font information from font common dialog
LOGFONT m_lf;
CFontDialog dlg(&m_lf);
if (dlg.DoModal() != IDCANCEL) {
CGDIFont font;
font.CreateFontIndirect(&m_lf);
// Use font here to display some text
}
126
An effort has been made to make the name of the functions equivalent to their counterparts in the
Win32 API. This allows you to use the Win32 API reference as a reference for the GDI wrapper
classes.
10.2.2 Lifetime Management
CGDIObject<> derives from the class CHandleWrapper<>. This class controls the lifetime of the
underlying handle.
SFL follows a simple ownership model for the relationship between instances of classes derived
from CHandleWrapper and the handles they encapsulate. Under this model, multiple instances can
encapsulate the same handle value, but only one of them should be considered the “owner” of the
handle. It is the owner’s responsibility to destroy the handle appropriately when it is destroyed
itself. When they are destroyed, objects that encapsulate a handle without ownership on it should
not take any action at all with respect to the handle.
CHandleWrapper<> offers methods for attaching and detaching a handle from the wrapper
instance. The attachment methods take an optional “ownership” boolean parameter, which determines whether this instance should take ownership of the handle being attached.
Consider the following code snippet:
CGDIPen myPen(hSomePen, true);
CGDIPen anotherPen(hSomePen, false);
Here, the variable myPen() takes ownership of the pen handle. When the instance referenced by
that variable is destroyed, so is the GDI object. On the other hand, anotherPen() does not take
ownership. It will only serve as a wrapper to invoke functions on the handle without affecting its
lifetime.
It is the responsibility of the programmer to make sure that only one wrapper object has ownership
of a handle at a given time. Under some circumstances, the SFL code has no way of knowing
whether you have assigned ownership of a handle to more than one instance. This behavior is by
design: keeping track of ownership outside of the instances would require having some kind of
global map, an option we discarded in order to keep SFL lean. However, this means that the programmer must be aware of this possibility and take the necessary steps to avoid its occurrence. For
example, if in the previous code snippet, the ownership parameter in the second line is true, it will
cause a conflict of ownership between both instances of CGDIPen.
In general, the default for the ownership parameter is true. This is useful for those cases when an
implicit attachment occurs, as in the following example.
CGDIPen AttachPen(HPEN hpen)
{
CGDIPen myPen(hpen);
return myPen;
}
When you return the CGDIPen object by value, a new temporary instance of the object is created
and its copy constructor is invoked. Since the default parameter of the copy constructor instructs
the temporary object to take ownership of the handle, the handle is not destroyed when the myPen
variable goes out of scope at the end of the routine.
Chapter 10 GDI Classes 127
It is important to notice that the assignment operator (operator=) does not take an ownership
parameter, since its signature is predetermined by the C++ language. Therefore, the convention has
been adopted that direct assignment always transfers ownership of the handle.
10.2.3 Examples
The usage of GDI objects is very simple. Use these wrapper objects wherever you would traditionally use a plain handle such as HPEN or HBRUSH. You usually follow this process:
1. Create the object by passing the adequate parameters to one of its creation methods.
2. Select the object in a device context.
3. Call some GDI functions to generate some output.
4. Restore the old objects to the device context.
If your instance has ownership of the GDI objects you are using, you don’t have to worry
about releasing the handle. This will occur automatically when the object goes out of scope.
For example, the code in Example 88 paints a line in the Highlight color designated by the user:
Example 88 – Painting a line in a user-designated color
void DrawHilite (
CGraphicsContext& dc,
CRect rcDraw
)
{
CGDIPen m_penHilite;
m_penHilite.CreatePen(PS_SOLID, 1, ::GetSysColor(COLOR_BTNHIGHLIGHT));
CGDIPen penOld = dc.SelectObject(m_penHilite);
dc.MoveTo(rcDraw.left, rcDraw.top + 1);
dc.LineTo(rcDraw.right, rcDraw.top + 1);
dc.SelectObject(penOld);
}
Alternatively, you can create and initialize some objects as data members of some other object (for
example, a window), and cache them there for use in the painting operations. This technique is
helpful when you want to speed up the painting operations by creating all GDI objects at once at
the beginning, or when your application has painting code spread over multiple routines, as in
Example 89.
Example 89 – Initializing and caching objects as data members of another object
class CHighlighter
{
<...>
CGDIPen m_penHilite;
<...>
};
CHighlighter::CHighlighter ()
{
m_penHilite.CreatePen(PS_SOLID, 1,
::GetSysColor(COLOR_BTNHIGHLIGHT));
}
128
CHighlighter::Draw (
CGraphicsContext& dc,
CRect rcDraw
)
{
CGDIPen penOld = dc.SelectObject(m_penHilite);
dc.MoveTo(rcDraw.left, rcDraw.top + 1);
dc.LineTo(rcDraw.right, rcDraw.top + 1);
dc.SelectObject(penOld);
}
Chapter 10 GDI Classes 129
10.3 Device Contexts
The Device Context (DC) is the main abstraction used in GDI programming. The use of a Device
Context allows the programmer to write output code that is device-independent. The same calls
can be used to send output to the display and the printer, the specific details of each device are
taken care of by a device driver with no intervention from the programmer.
The class that encapsulates a DC in the SFL Graphics package is CGraphicsContext. This class provides an inline wrapper method for every API function in the Win32 GDI. It also has methods for
attaching and detaching a plain DC handle to a CGraphicsContext.
To use the CGraphicsContext class, you first have to provide a DC handle obtained somehow, as
shown in Example 90. After that, you can call all the GDI functions you want.
Example 90 – Using the CGraphicsContext class
void DrawTracker (
HDC hdcTracker,
CRect rcTracker
)
{
CSize szTracker(0, 0);
CGraphicsContext dc(hdcTracker);
<...>
dc.DrawDragRect(rcTracker, szTracker, rcLastTracker, szTracker);
}
10.3.1 Device Context Creation and Destruction
Noticeably absent from the CGraphicsContext public interface are methods for creation of a device
context. There is a ReleaseHandle() method, but its implementation is a no-op.
The reason for this is that there is no single way to create and release device contexts in the Win32
API. The GDI API distinguishes multiple types of device contexts; each one of them has a different
creation process, accompanied by the corresponding destruction process.
SFL provides several specializations of CGraphicsContext, each one corresponding to one of the
DC types distinguished by the GDI. These are:
130
пЃµ
CPaintGraphicsContext: Used in the WM_PAINT handler of a window. Allows
output on the invalidated area of the window.
пЃµ
CClientGraphicsContext: Allows output on the entire client area of a window.
пЃµ
CWindowGraphicsContext: Permits output on any point of the window area,
including client and non-client space.
пЃµ
CDeviceGraphicsContext: Associated with a specific device driver. Usually used
for printing.
пЃµ
CMemoryGraphicsContext: Device context not directly associated with any
physical output device.
пЃµ
CMetafileGraphicsContext: Output to a metafile, a file that contains GDI
instructions that can be reproduced later on an actual output device.
Each one of these classes publishes a Create() method that creates and initializes a new DC of the
type corresponding to that class. The set of parameters taken by this method varies from class to
class, since different types of DCs require a different set of initialization data.
SFL’s plain CGraphicsContext class does not follow an ownership scheme like the GDI objects. An
instance of CGraphicsContext never owns the DC handle it contains; therefore it never destroys it
when the object instance gets destroyed.
An instance of any specialized Graphics Context class always owns the DC handles that it contains.
This is because it was that instance which originally created the contained handle; and, therefore, it
is the only one who has the knowledge of how to deallocate it.
As shown in Example 91, let’s consider a handler for the WM_PAINT message:
Example 91 – A handler for the WM_PAINT message
LRESULT OnPaint(UINT , WPARAM , LPARAM , BOOL& )
{
CPaintGraphicsContext dcPaint(*this);
dcPaint.Rectangle(CRect(0, 0, 100, 100));
return 0;
}
The code above paints a rectangle on the upper left corner of the window. Notice that no explicit
destruction call is necessary; the device context is released appropriately when the dcPaint variable goes out of scope.
Below is Example 92, in which a Client DC is created for a window, and a compatible Memory DC
is created to display a bitmap on the painting area of that window.
Example 92 – Creating a Client DC and a Memory DC
void DisplayBitmap (
GDIBitmap& bmpToDisplay
)
{
CClientGraphicsContext dcClient(*this);
CMemoryGraphicsContext dcBitmap(dcClient);
CGDIBitmap bmpOld = dcBitmap.SelectObject(bmpToDisplay);
CSize szToDisplay = bmpToDisplay.GetBitmapSize();
dcClient.BitBlt(CRect(CPoint(0, 0), szToDisplay), dcBitmap,
CPoint(0, 0), SRCCOPY);
dcBitmap.SelectObject(bmpOld);
return 0;
}
Chapter 10 GDI Classes 131
10.3.2 MFC Compatibility
It is very likely that you already have some complex painting code in some MFC applications, and
you would like to be able to migrate that code to your new SFL-based applications with the least
amount of work possible.
In order to maintain source code level compatibility with legacy MFC code, SFL’s Graphics package has a compatibility layer that enables you to do precisely that.
If you are not using MFC in conjunction with SFL’s Graphics package (concretely, if the preprocessor flag _SFL_MFC_SUPPORT is not defined), the MFC names for GDI objects (i.e. CPen, CBrush, CDC,
etc.) are defined as synonyms of the native SFL names described in the previous sections.
When you are using MFC in conjunction with SFL, the MFC names belong to MFC. If you wish to
use the SFL classes, you will have to address them by their native names.
Most of the method names and signatures in MFC have been respected. However, there are some
aspects where the behavior differs in both libraries. For example, calls to CDC::FromHandle() will
not compile in SFL. SFL does not manage a global map of temporary objects like MFC does, therefore it is not capable of returning an instance that you have created elsewhere in the program. You
have to keep track of your own objects in SFL, and manage temporary objects accordingly. Remember, however, that the plain CGraphicsContext class can be attached to any DC handle with no
harm, since it does not release the handle when destroyed; this technique can be a substitute for the
FromHandle() function.
132
Chapter 11
String and Collection Classes
11.1 SFL Utility Classes
In addition to all the integrated packages—such as GDI classes, application support, layout management, and MVC—SFL also offers a set of highly independent, small utility classes. These classes
are shared by several packages.
Of interest among this group, because they can be used in multiple situations independently of the
rest of SFL, are:
пЃµ
Enhanced string
пЃµ
API Structure wrappers: CRect, CPoint, CSize
пЃµ
MFC compatible string and collections
Chapter 11 String and Collection Classes 133
11.2 Enhanced String
SFL’s enhanced string is based on the basic_string<> class that is part of the Standard C++ Library.
In comparison to the standard versions, SFL’s string offers the following advantages:
пЃµ
Conversion between different character sets
пЃµ
Implicit cast operator to C string (array of characters)
пЃµ
Formatting capabilities
пЃµ
Buffer allocation capabilities
пЃµ
Unicode compliance
The code for SFL’s enhanced string can be found in the <String\StringEx.h> header file under
your SFL include directory.
The core of the implementation is in a new class called basic_string_ex<>. This class derives
directly from std::basic_string. The signature of the class is:
Example 93 – Signature for class basic_string_ex<>
template <
typename _CharType,
typename _ConversionCharType,
typename _Traits = char_traits_ex<_CharType,
_ConversionCharType>,
typename _A = std::allocator<_Traits>
>
class basic_string_ex:
public std::basic_string<_CharType, _Traits, _A>
As you can see, the first difference in the template signature is that it takes not one but two character types. These will usually be the pair <char, wchar_t> or <wchar_t, char>. The first character
type corresponds to actual elements of the string. For instance, a basic_string_ex<char, wchar_t>
derives from basic_string<char>; therefore it is implemented as a sequence of elements of type
char. The second character type enables conversions to be done from sequences of this character
type to the native character type.
Another difference is that the _Traits template parameter defaults to a new class
char_traits_ex<>, as opposed to the standard char_traits<>. Class char_traits_ex<> is also an SFL
specialization of its Standard C++ Library counterpart. It adds to the traits class conversion and
formatting routines, which will be used to implement the additional features of our extended
string. As you can see, the char_traits_ex<> template also takes two character types as parameters.
11.2.1 Character Set Conversion
In addition to the constructors and assignment operators present in std::basic_string<>,
basic_string_ex<> declares a set of conversion constructors and operators. These routines take a
sequence of characters of the conversion char type and construct a string from there using the normal API calls for conversion from and to Unicode. For example, the following code copies and
converts the contents of the BSTR variable to the appropriate string type.
134
BSTR bstrSomeString = GetBstr();
foundation::string s(bstrSomeString);
This is not a supported feature of the standard string.
11.2.2 Casting
The standard definition of the basic_string type purposely left out any casting operator, based on
the theory that implicit castings can cause problems in multiple situations. The standard defines
the c_str() function in order to provide access to the internal character sequence managed by the
string object.
It is often convenient, however, to have a casting operator so that the string object can be used naturally in calls to routines that take a constant C string, like many of the Win32 API functions. For
this reason, the SFL string does publish the casting operator. Obviously, casting is permitted only to
a const character sequence.
11.2.3 Formatting and Buffering
SFL’s basic_string_ex<> offers formatting capabilities similar to the classic printf() in C. This is
something notably absent from the standard string. The recommended method to achieve this
using the Standard C++ Library is with string streams. Sometimes, however, it is more convenient
to go back to the old-fashioned way, particularly if format strings need to be stored externally, such
as in a resource file.
The signature of the format() routine is:
void format(const _CharType* lpFormat, ...);
The format() string supports the same formatting codes as the printf() function. The result of
the formatting operation is assigned to the string instance on which this method is called.
Many Win32 API routines require that you pass a previously allocated character array of a determined size as an output parameter. This often involves having to allocate a character array on the
stack just as a temporary buffer, and assigning the contents of that array to a string variable afterwards. There is a particular feature of MFC’s CString that comes handy in such cases: the
GetBuffer() and ReleaseBuffer() set of functions.
Our basic_string_ex implements a similar functionality. The signatures of the methods involved
are:
_CharType* get_buffer(unsigned int _N = 0);
_CharType* get_buffer_set_length(unsigned int _N = 0);
void release_buffer(unsigned int _N = 0);
The usage of these functions is the same as in MFC. Whenever a pre-allocated character sequence is
required, a call to get_buffer() is performed, specifying the size of the buffer. get_buffer()
returns a non-const pointer to the internal character sequence, guaranteed to be at least of the size
specified. Alternatively, get_buffer_set_length() returns a buffer of exactly the size specified.
Chapter 11 String and Collection Classes 135
After the call to the external function, you can optionally call release_buffer() to release the
space allocated in the buffer but not used by the actual contents. release_buffer() assumes that
your string ends with the first null character. If your string has embedded null chars, another
mechanism will have to be used to deallocate that space. For example:
string sItem;
int nres = ::LoadString(hResInst, stringId,
sItem.get_buffer(256), 256);
sItem.release_buffer();
11.2.4 Type Definitions
The Standard C++ Library defines a type string, which is no more than a typedef for a
basic_string<char>; however, it is much more convenient and natural to use just the name string
than the entire templatized symbol. In a similar fashion, SFL defines some short names for the most
commonly used string types. However, the Standard C++ Library doesn’t take into account the
possibility of applications using the Unicode character set, string is always defined to use 1 byte
characters. SFL goes one step beyond, taking into account the standard way for a Windows application to define the character set it will use. The definition of string varies depending on whether
the _UNICODE preprocessor macro is defined or not. For applications that need string processing for
char or wchar_t types independently of the _UNICODE symbol, two permanent definitions are also
included: cstring and wstring. The definition of each of these types is as follows:
пЃµ
cstring: String of ANSI characters. Always defined as basic_string_ex<char,
wchar_t>.
пЃµ
wstring: String of wide (Unicode) characters. Always defined as
basic_string_ex<wchar_t, char_t>
пЃµ
string: Defined as synonym of cstring if the _UNICODE preprocessor flag is not
defined; otherwise is defined to wstring.
Remember that the string symbol we refer to here should not conflict with the string type in the
Standard C++ Library: the former is within the stingray::foundation:: namespace, whereas the
latter is in the std:: namespace. If you flatten those namespaces using the using statement, a
name ambiguity will occur.
A similar naming trick is included for string streams. SFL does not provide an enhanced string
stream; however, it does define some convenient names depending on the _UNICODE symbol, just
as explained before. Thus, we have:
пЃµ
cstringstream: Stream of ANSI characters. Always defined as
basic_stringstream<char>.
пЃµ
wstringstream: String of wide (Unicode) characters. Always defined as
basic_stringstream<wchar_t>
пЃµ
stringstream: Defined as synonym of cstringstream if the _UNICODE
preprocessor flag is not defined; otherwise is defined to wstringstream.
These streams use the char_traits_ex classes as their traits parameters, so they are compatible with
SFL’s string types.
136
11.3 API Structure Wrappers
SFL includes wrappers for some commonly used Win32 API structures, in particular:
пЃµ
RECT
пЃµ
POINT
пЃµ
SIZE
A major design directive driving the development of SFL has been maintaining MFC source code
level compatibility. The objective of this is to enable you to write code that can be used in MFC
applications as well as in SFL with as few changes as possible.
Not coincidentally, the names and public interfaces of these classes in SFL is the same as their MFC
counterparts. So we have:
пЃµ
CRect
пЃµ
CPoint
пЃµ
CSize
We will not describe the interface or the usage of these classes, since they are identical to MFC’s.
Please refer to the MFC documentation for an overview of their operations and data members.
It is important to notice that, unlike most of SFL, these wrappers are not within the
stingray::foundation namespace. That means that you should not use the SFL version of them
when your project uses MFC in conjunction with SFL. SFL’s own header files correctly strip out
these definitions when MFC is present, and adequately use the MFC structures instead. But you
must be careful not to include this class explicitly in your program, under such circumstances,
since it will cause a ambiguous symbol name error.
Chapter 11 String and Collection Classes 137
11.4 MFC Compatibility Classes
Toward the same goal of MFC source-code compatibility, SFL also offers a CString-compatible class
and a set of collection classes that also share the public interfaces of MFC collections.
SFL’s CString offers the possibility of reusing with virtually no change chunks of code that make
use of MFC’s CString, but eliminating the MFC linkage. This is attractive for ATL projects where
up until now no string-processing capabilities were easily available.
SFL’s CString class can be distinguished from its MFC counterpart because it is contained in the
stingray::foundation namespace. However, in a project that uses MFC it is recommended to
avoid the inclusion of SFL’s version since it would lead to duplication of functionality and larger
executable code.
SFL’s CString is implemented in terms of the basic_string_ex<> class described in a previous section. This class, in turn, is derived from the Standard C++ Library string. What CString contributes
is to change the programming interface in order to provide source code compatibility with MFC.
Every routine is translated to its equivalent in the basic_string<> interface. The definition of SFL’s
CString is located in the header file <string\SflString.h>.
SFL also offers a set of collection classes that follow this same pattern: they are implemented on top
of the corresponding containers in the Standard C++ Library portion commonly known as STL, but
offer the same interface as the familiar MFC collections.
The following MFC collections are included in the header <string\sflcoll.h>:
пЃµ
CMap
пЃµ
CTypedPtrMap
пЃµ
CArray
пЃµ
CTypedPtrArray
пЃµ
CList
пЃµ
CTypedPtrList
In addition, there are explicit type definitions as instantiations of the templatized classes for the following MFC collections:
138
пЃµ
CMapPtrToPtr
пЃµ
CMapPtrToWord
пЃµ
CMapWordToPtr
пЃµ
CMapStringToPtr
пЃµ
CMapPtrToString
пЃµ
CWordArray
пЃµ
CDWordArray
пЃµ
CUIntArray
пЃµ
CStringArray
пЃµ
CPtrArray
пЃµ
CPtrList
пЃµ
CStringList
Chapter 11 String and Collection Classes 139
140
Chapter 12
Developing Applications
12.1 Overview
In the beginning, there was MFC. For developers wanting to write robust, fast, and flexible doubleclickable Windows applications, MFC was the framework of choice. As a class-based framework,
MFC made it easier to create applications. Using MFC is far easier than using the raw API.
Over the years, MFC has matured into a popular framework. However, like a snowball growing as
it careens down a mountainside, MFC has managed to gain a lot of weight over the years. In addition, MFC is tightly coupled to itself. For example, buying into one part of the framework
architecture often means investing one’s development attention towards other parts of the framework that might cloud the issues involving the task at hand. For instance, electing to use MFC’s
Object Linking and Embedding support means using MFC’s Document/View architecture.
Then while MFC was maturing, the Component Object Model became a prominent fixture within
the Windows software development community, spurring the development of a framework named
the Active Template Library (ATL). While ATL is mostly useful for writing COM servers very
quickly, the addition of ActiveX Control Support to ATL in 1997 introduced a windowing framework within ATL.
For developers wishing to build single, double-clickable applications, MFC provides a complete
framework. On the other hand, ATL is poised as a potential application development framework
for lighter-weight applications. The only problem is that ATL’s windowing support is oriented
toward controls and not toward whole applications. Developers wishing a smaller, more modern,
templatized approach to Windows development can use ATL combined with the Stingray Foundation Library (SFL) classes.
Chapter 12 Developing Applications 141
12.2 Features and Benefits
SFL represents a framework for building thin applications using C++. SFL leverages ATL’s windowing support while adding many features that Windows applications developers will find
useful. The following is a list of SFL’s application features and benefits.
пЃµ
Wraps application boilerplate code
пЃµ
Provides an event-listener architecture
пЃµ
Is templatized so it’s more flexible than straight inheritance
пЃµ
Provides a Model-View-Controller subsystem
пЃµ
Includes a Layout Manager
пЃµ
Provides OLE Drag and Drop support
пЃµ
Wraps the common Windows dialog boxes
пЃµ
Provides an AppWizard, making it easy to generate applications
пЃµ
Provides wrapper for Win32 GDI
This User’s Guide covers the essentials of application development using SFL, focusing on the architecture and the classes fundamental to application development.
142
12.3 Basic Architecture
This section describes SFL’s overall architecture, explaining how the application, message management, and windowing classes work together. To help illustrate how SFL works, you’ll go through a
simple SFL-based application named HelloSFL.
12.3.1 HelloSFL
The HelloSFL application is a bare-bones application that simply shows a window, runs a message
loop, and processes window messages. HelloSFL illustrates SFL’s basic facilities and how the
framework maps to fundamental SDK-style programming.
As with regular SDK-style programming, in which applications conceptually include both an
application portion and a window message handling portion, so does SFL. SFL’s basic architecture
consists mainly of an application class and a collection of one or more window classes. The application class manages the message loop and window creation while the window class (or classes)
handles the events. Figure 13 shows a high-level view of SFL’s architecture.
Figure 13 – Architecture of the Stingray Foundation Library
The main components of an SFL-based application include a WinMain() function and a single
instance of an application class derived from CComModule. The application class holds a message
loop class and an initializer class that are passed in as template parameters. You’ll start with SFL’s
application class, named CApp.
Chapter 12 Developing Applications 143
12.3.2 HelloSFL’s Application
Every Windows application needs a place to store global information such as the main window
handle and the instance handle. In addition, COM servers need a place to store global reference
counts for the server. ATL provides a class named CComModule for managing details global to the
server. SFL’s architecture provides a class named CApp that inherits from CComModule. CApp’s
job is to manage the global details for an application. Example 94 shows how HelloSFL declares
the CApp class.
Example 94 – HelloSFL.H
#pragma once
class CMainFrame;
////////////////////////////////////////////////////// CHelloSFLApp
typedef CApp < CComModule,
CMessageLoop < CMainFrame>,
CNoopInitializer > CHelloSFLApp;
The declaration of an application class usually occurs in the main header file of the application, as
shown in Example 94. Notice that CApp takes three template parameters: a base class, a message
loop class, and an initializer class. Deriving from CComModule is an ATL requirement; ATL
expects applications to have a single instance of CComModule. To that end, CApp expects as its first
template parameter a class derived from CComModule. CApp uses CComModule as the default
base class. Second, because CApp is expected to manage the message loop, CApp takes a message
loop class as a second parameter. CApp uses the class passed in as a second template parameter to
the application’s message loop. The final template parameter is a class that implements initialization steps. Now take a closer look at SFL’s message loop class.
12.3.3 HelloSFL’s Message Loop
Many application architectures hard code the message loop in the framework as part of the base
application class. Instead of hard coding the message loop into the framework, SFL’s message loop
is componentized and added to the base application class as a template parameter. In most cases,
the job of the message loop component is to create the window on the screen and pump messages.
The base class for SFL’s message loop classes is named CMessageLoopBase. -CMessageLoopBase is
an abstract class—it has two pure virtual functions CreateMainWindow() and
DestroyMainWindow() that SFL expects to be implemented by classes derived from
CMessageLoopBase. You’ll see how that’s done in a minute.
Example 95 shows the pseudo-code for CMessageLoopBase:
Example 95 – Pseudo-code for SFL’s message loop
class CMessageLoopBase
{
public:
int Run()
{
CreateMainWindow()
RunMessageLoop()
DestroyMainWindow()
}
144
protected:
virtual void CreateMainWindow() = 0;
virtual void DestroyMainWindow() = 0;
int RunMessageLoop()
{
while (not quit message) {
while(PeekMessage() {
if (OnIdle()) {
}
}
GetMessage()
PreTranslateMessage()
DispatchMessage()
}
}
virtual bool PreTranslateMessage ()
{
::TranslateMessage()
}
// override to change idle processing
virtual bool OnIdle ()
{
}
};
SFL has two classes filling in the implementation of the message loops—CCreateWindowMessageLoop and CCreateDialogMessageLoop. Both template classes accept a
window type and a message loop type. The window type parameter names the kind of window to
create and the message loop type (which defaults to CMessageLoopBase) determines how the message loop is to run. Example 96 illustrates the declaration of CCreateWindowMessageLoop.
Example 96 – SFL’s CCreateWindowMessageLoop class
template <typename _WindowClass,
typename _Base = CMessageLoopBase>
class CCreateWindowMessageLoop:
public _Base
{
typedef _Base _baseClass;
public:
typedef _WindowClass WindowClass;
void CreateMainWindow();
void DestroyMainWindow();
protected:
_WindowClass* m_pwndMain;
};
For convenience, SFL defines a generic window message creation loop named CMessageLoop.
Notice that CMessageLoop is the second template parameter passed into HelloSFL’s CApp class.
Example 97 shows the CMessageLoop class.
Chapter 12 Developing Applications 145
Example 97 – SFL’s generic message loop
template <typename WindowClass>
class CMessageLoop :
public CCreateWindowMessageLoop<WindowClass,
CMessageLoopDefaultImpl<> >
{
…
};
Once an SFL-based application declares a class derived from CApp, a global instance of the class
must appear once in the application. Furthermore, the single application class must be named
“_Module” and be derived from ATL’s -CComModule class. (These are ATL’s requirements.)
The _Module class is usually declared externally in the STDAFX.H file. The declaration needs to
appear globally because ATL makes several references to the name _Module. Example 98 shows
how the _Module class is defined within HelloSFL’s stdafx.h file.
Example 98 – HelloSFL’s STDAFX.H file
----------------------------------// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
//
are changed infrequently
//
#pragma once
#define WIN32_LEAN_AND_MEAN
#define _WIN32_WINNT
0x0400
#include <atlbase.h>
#include <Foundation\apps\Application.h>
using namespace stingray;
using namespace foundation;
#include "HelloSFL.h"
// CHelloSFLApp class definition
extern CHelloSFLApp _Module;
#include <atlwin.h>
If you go back and look at the definition of HelloSFL’s CApp class, you’ll notice that the second
template parameter, the message loop, requires its own template parameters. SFL’s message loop
class is responsible for actually creating the application’s main window. The message loop class
needs to know what kind of window to create, so that’s passed in as a template parameter. Next
you’ll take a look at HelloSFL’s main window.
146
12.3.4 HelloSFL’s Main Window
HelloSFL’s main window class named CMainFrame is derived from -CFrameWindowImpl.
Example 99 shows how HelloSFL uses SFL’s CFrameWindowImpl.
Example 99 – MAINFRAME.H
// MainFrame.h
#pragma once
#include <Foundation\Apps\Application.h>
#include <Foundation\Apps\FrameWnd.h>
class CMainFrame : public CFrameWindowImpl<CMainFrame, IDR_HelloSFL>
{
public:
typedef CFrameWindowImpl<CMainFrame, IDR_HelloSFL> _BaseClass;
BEGIN_MSG_MAP(CMainFrame)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
COMMAND_ID_HANDLER(ID_APP_EXIT, OnAppExit)
CHAIN_MSG_MAP(_BaseClass)
END_MSG_MAP()
LRESULT OnAppExit(WORD, WORD, HWND, BOOL& rb)
{
DestroyWindow();
rb = TRUE;
return 0;
}
LRESULT OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& bHandled)
{
HDC hDC = NULL;
PAINTSTRUCT ps;
hDC = ::BeginPaint(m_hWnd, &ps);
int x = strlen("Salutations, world");
BOOL b = TextOut(hDC, 1, 30, "Salutations, world", x);
::EndPaint(m_hWnd, &ps);
bHandled = TRUE;
return 0L;
}
};
CMainFrame’s job is simply to show itself and process window messages. -CMainFrame derives
from SFL’s class named CFrameWindowImpl, which in turn derives from ATL’s CWindow class,
giving CMainFrame the capability to manage a message map. The two messages the main window
cares about include the WM_PAINT message and the WM_COMMAND message (with the command ID
ID_APP_EXIT). Notice that CMainFrame’s message map includes these two entries.
Just as the CApp class is a template class, so is CFrameWindowImpl. -CFrameWindowImpl actually
takes four template parameters, though only two are shown in the listing above. The first parameter to CMainFrame is the type of derived class and the second parameter is the number identifying
Chapter 12 Developing Applications 147
the menu resource, the icon for the window, an accelerator table for the window, and a caption
string. The other two parameters for CFrameWindowImpl include the window creation flags and
the base class. The window creation flags default to the ATL-defined WinTraits class for frame
windows, which includes the following creation flags: WS_OVERLAPPEDWINDOW, WS_CLIPSIBLINGS,
WS_CLIPCHILDREN, WS_EX_APPWINDOW, and WS_EX_WINDOWEDGE. The base class for CFrameWindowImpl defaults to ATL’s CWindow, which gives CMainFrame ATL’s basic message
handling capabilities through the message maps.
CMainFrame responds to the ID_APP_EXIT command by destroying the window. CMainFrame
responds to the WM_PAINT message by creating a paint device context and drawing on it using regular Win32 GDI calls.
If you’re accustomed to SDK-style programming, you’ll notice the basic bones of a Windows application within the window class and the various constituents of the application class. The final link
in the chain is WinMain(). Example 100 shows how HelloSFL implements WinMain().
Example 100 – HELLOSFL.CPP
// HelloSFL.cpp : Defines the entry point for the application.
//
#include
#include
#include
#include
#include
"stdafx.h"
"resource.h"
<Foundation\Apps\AppImpl.h>
<Foundation\Layout\LayoutFactory.h>
"MainFrame.h"
CHelloSFLApp _Module;
BEGIN_LAYOUT_MAP()
END_LAYOUT_MAP()
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR
lpCmdLine,
int
nCmdShow)
{
_Module.Init(nCmdShow, 0, hInstance);
_Module.Run();
_Module.Term();
return 0;
}
The main job of nearly every version of WinMain() is to create a window, run the message loop
until it’s time to quit, and then clean up after everything is finished. HelloSFL’s main C++ source
file declares an instance of CHelloSFLApp and names the instance “_Module”. The naming convention is an ATL requirement; ATL expects to see a CComModule-derived object named _Module.
The WinMain() function calls the module’s Init() function. HelloSFL’s version of Init() simply
calls CComModule’s version of Init() and then the Initializer’s version of Init(). (Remember,
the initializer was passed in as a template class.) HelloSFL uses the NoopInitializer, which does
nothing. SFL’s other initializers perform operations like initializing the common controls library
and initializing COM.
Next, WinMain() calls the module’s Run() function. Run() is actually implemented by the message
loop object. Run() calls the virtual function named CreateMainWindow(), which creates a window
type passed in as a template parameter. Run then calls RunMessageLoop(), which picks messages
148
off the queue and dispatches them, allowing for idle processing in between. When the application
falls out of the message loop, WinMain() calls the module’s Term() function, which calls the initializer’s and ATL’s termination code.
HelloSFL shows how SFL’s basic application architecture works. Next you’ll take a more detailed
look at SFL’s architecture. You’ll start by examining the various mutations of SFL’s application
classes.
Chapter 12 Developing Applications 149
12.4 Application Classes
SFL contains two application-level classes, CApp and CMTIApp. Both classes work fundamentally
the same. CApp mixes together window creation and application initialization. CMTIApp supplies
additional functionality for managing the multiple top-level windows on different threads.
12.4.1 CApp
CApp is the basic class that is responsible for creating a window, running the message loop, and
handling termination issues (such as destroying the window). Example 101 shows the SFL’s declaration of CApp.
Example 101 – SFL’s CApp class
template<typename _Base = ATL::CComModule,
typename _MessageLoop,
typename _Initializer = CNoopInitializer>
CApp : public _Base {
public:
HRESULT Init(int nShowCmd,
_ATL_OBJMAP_ENTRY* p,
HINSTANCE h,
const GUID* plibid = NULL);
void Term();
int Run();
};
CApp is a template class taking three parameters: a CComModule-derived class, a class for handling the message loop, and a class for handling application initialization. CApp derives itself from
whatever type is passed in as the _Base type. Notice the default is ATL’s CComModule class. SFL
(and ATL) expects to see a single instance of a class of type CComModule within the application. To
this end, SFL assumes CApp to derive ultimately from CComModule (which is why -CComModule
is named as the default base class).
Also notice that CApp’s declaration has two other parameters in addition to the base class: the message loop and the initializer. The class passed in as the _MessageLoop class is responsible for
providing the message pumping machinery. CApp declares a member variable of the type passed
in through the _MessageLoop template parameter and uses that member variable to drive the message loop.
CApp’s final parameter is the initializer class. SFL expects the initializer class to implement static
functions named Init() and Term() for initializing the application and terminating the class. SFL
declares four initializer classes: CNoopInitializer, CComInitializer, and COleInitializer, and CCommonControlsInitializer. You’ll cover each of the initializers shortly. Notice that CApp
defaults to the CNoopInitializer.
CApp has three methods: Init(), Term(), and Run(). These methods are for initializing the application, running the message loop, and terminating. They are usually called within the application’s
WinMain() function.
150
12.4.2 CMTIApp
In addition to the basic CApp type, SFL also includes a class named CMTIApp, which manages a
user interface consisting of multiple top-level windows. CMTIApp is useful for writing applications that have multiple top-level windows. Example 102 shows SFL’s CMTIApp class.
Example 102 – SFL’s CMTIApp class
template <typename _Base = ATL::CComModule,
typename _MessageLoop,
typename _Initializer = CNoopInitializer >
class CMTIApp : public _Base
{
public:
HRESULT Init(int nShowCmd, _ATL_OBJMAP_ENTRY* p, HINSTANCE h,
const GUID* plibid = NULL)
void Term()
int Run()
bool RunTopLevelWindow(void* lpParam = NULL);
protected:
// methods for managing the multiple message queue threads…
};
The difference between the CApp class and the CMTIApp class is that the CMTIApp class supports
multiple top-level windows. CMTIApp manages a collection of message loops, each living on a
separate thread. CMTIApp has a function for opening a new top-level window. Applications based
on CMTIApp normally respond to the File | New menu option by running a new top level window. Other than that, the programmatic interface is more or less the same. You declare an instance
of CMTIApp providing the same template parameters as you would with CApp. That is, name the
instance _Module and call the Init(), Run(), and Term() functions within WinMain().
Example 103 shows how to call RunTopLevelWindowInit() in response to the File | New menu
option.
Example 103 – Calling the application’s RunTopLevelWindow() function
LRESULT OnFileNew(WORD, WORD, HWND, BOOL&)
{
_Module.RunTopLevelWindow();
return 0;
}
SFL’s application classes also have hooks for integrating the SFL Layout Manager. See Chapter 7,
“Layout Manager,” for more information.
As part of examining how the application classes work, take a look at how SFL’s initializer classes
work.
Chapter 12 Developing Applications 151
12.5 Initializer Classes
SFL includes four initializer classes to use when instantiating instances of the CApp and CMTIApp
classes. These classes are named CNoopInitializer, -CComInitializer, COleInitializer, and
CCommonControlsInitializer. All four classes follow the same form; they expose two static functions: Init() and Term().
пЃµ
CNoopInitializer: Init() and Term() are essentially place holders, doing nothing
within their implementations.
пЃµ
CComInitializer: CComInitializer takes two parameters when being instantiated—
a base class (defaulting to CNoopInitializer) and a DWORD representing the COM
threading model to use. CComInitializer::Init() calls CoInitializeEx() for
the application, while CComInitializer::Term() calls CoUninitialize() for the
application.
пЃµ
COleInitializer: COleInitializer::Init() calls OleInitialize() for the
application, and CComInitializer::Term() calls OleUninitialize() for the
application.
пЃµ
CCommonControlsInitializer: CCommonControlsInitializer takes two parameters
when being instantiated—a base class (defaulting to CNoopInitializer) and a DWORD
instructing the initializer how to call InitCommonControlsEx().
CComInitializer::Init() calls InitCommonControlsEx() for the application.
CCommonControlsInitializer::Term() simply calls the base initializer’s Term()
function.
With the application classes out of the way, take a look at how SFL’s window classes work.
152
12.6 Windowing Classes
After the application class, the second component within an SFL-based application is the window.
SFL contains several classes that encapsulate various types of windows. These types of windows
include container windows, frame windows, client windows and MDI windows. Following is a
brief overview of each.
12.6.1 Container Windows
One of SFL’s more useful features is a Layout Manager. Sometimes you want to have the presentation of your application behave in certain ways as the application’s window is sized. SFL has
classes to manage various layout algorithms. For example, when you program a toolbar into your
application’s main window, sometimes you want the toolbar to stick close to the border of the
application. Or perhaps you’d like the controls on a dialog box to scale as the window is sized.
These layout algorithms are already implemented within SFL. The advantage of the Layout Manager is that they’re much more flexible than MFC’s hard-wired docking window management
architecture.
To support the layout algorithms, SFL introduces several window classes that can contain layout
plug-ins. Layout plug-ins are layout manager components that have hooks into the ATL messaging
architecture to receive messages like WM_SIZE and WM_MOVE.
SFL’s container window classes include CContainerImplBase, CContainerWindowImpl,
CContainerDialogImpl.
12.6.1.1CContainerImplBase
CContainerImplBase is the main class, mixing a window class with one of SFL’s layout plug-ins.
Example 104 shows CContainerImplBase.
Example 104 – CContainerImplBase
template <typename _Derived,
typename _Traits,
typename _BaseImpl,
typename _LayoutPlugin >
class CContainerImplBase:
public _BaseImpl,
public _LayoutPlugin
{
…
};
CContainerImplBase is a template class, taking four parameters: the bottom-most derived class,
the window creation flags, the base implementation class, and the layout plug-in to be used.
CContainerImpl is almost never used by itself, serving instead as a base class for the other layout
container windows.
Chapter 12 Developing Applications 153
12.6.1.2CContainerWindowImpl
CContainerImplBase makes its first appearance in the declaration of -CContainerWindowImpl.
Example 105 shows the definition of CContainerWindowImpl.
Example 105 – CContainerWindowImpl
template <typename _Derived,
typename _Traits,
typename _Base = CWindow>
class CContainerWindowImpl :
public CContainerImplBase<_Derived, _Traits,
CWindowImpl<_Derived, _Base, _Traits>,
foundation::CLayoutManager<_Derived> >
{
…
};
CContainerWindowImpl defines a CContainerImplBase-derived class using ATL’s CWindow class
and SFL’s CLayoutManager class.
12.6.1.3CContainerDialogImpl
CContainerDialogImpl is useful for adding layout management to dialogs. Example 106 shows
CContainerDialogImpl.
Example 106 – CContainerDialogImpl
template <typename _Derived>
class CContainerDialogImpl :
public CContainerImplBase<_Derived,
CNullTraits, CAxDialogImpl<_Derived>,
foundation::CLayoutManager<_Derived, WM_INITDIALOG> >
{
…
};
CContainerDialogImpl adds layout management to ATL’s CAxDialogImpl class.
The container window classes aren’t intended to be used by themselves but instead are intended to
add the Layout Manager layer to SFL by mixing with real window classes—that is, frame windows, client windows, dialog windows, and MDI windows.
12.6.2 Frame Windows
Frame windows are usually intended to be the main window in an SDI application. SFL’s
CFrameWindowImpl serves this purpose. CFrameWindowImpl derives from SFL’s container window and so obtains the benefit of the Layout Manager automatically. Example 107 shows the
definition of SFL’s CFrameWindowImpl class.
Example 107 – SFL’s CFrameWindowImpl class
template <typename _Derived,
unsigned int _nResource = 0,
typename _Traits = CFrameWinTraits,
154
typename _Base = CWindow>
class CFrameWindowImpl:
public CContainerWindowImpl<_Derived,
_Traits,
_Base >
{
typedef CFrameWindowImpl<_Derived, _nResource, _Traits, _Base>
thisClass;
typedef CContainerWindowImpl<_Derived, _Traits, _Base >
_windowBase;
};
SFL’s CFrameWindowImpl class handles the WM_DESTROY and WM_INITMENUPOPUP messages.
CFrameWindowImpl handles the WM_DESTROY by posting the quit message if the frame window is
not a child window or a pop up window. CFrameWindowImpl handles the WM_INITMENUPOPUP
message by issuing a user interface update notification.
CFrameWindowImpl includes everything necessary to create and show a frame window on the
screen. Notice that the definition takes four parameters. The first template parameter, _Derived, is
the only one that you must provide. The other templates specify the resource identifier (for the
menu, the accelerator table, the caption string, and icon), the creation flags, and the base window
class. CFrameWindowImpl defaults to the number zero for the resource id, to ATL’s
CFrameWinTraits as the window creation flags, and to ATL’s CWindow class as the base window
class. This combination generates the infrastructure for creating a plain vanilla frame window.
Example 108 shows how to define a frame window for your application.
Example 108 – Defining a derived frame window class
class CMyFrame :
public foundation::CFrameWindowImpl<CMyFrame, IDR_HelloSFL>
{
…
};
CFrameWindowImpl also has a create function which maps to the Win32 -CreateWindowEx API.
Example 109 shows CFrameWindowImpl’s Create() function.
Example 109 – CFrameWindowImpl::Create()
HWND CFrameWindowImpl::Create (HWND hWndParent,
RECT& rcPos,
LPCTSTR lpszWindowName = 0,
DWORD dwStyle = 0, DWORD dwExStyle = 0,
HMENU hMenu = 0, LPVOID lpCreateParam = 0)
You don’t usually need to override CFrameWindowImp::Create(). As long as your derived window class includes this signature for the Create() function, SFL’s message loop class will create
the window automatically.
Chapter 12 Developing Applications 155
12.6.3 Client Windows
Sometimes your application architecture calls for non-frame windows. For example, many applications require the rendering and drawing code to be separate from the frame. SFL supports this
requirement through its client windows. SFL defines its client windows through a class named
CClientWindowImpl. Client windows are usually children of frame windows. Example 110 shows
SFL’s client window class.
Example 110 – SFL’s CClientWindowImpl class
template <typename _Derived,
typename _Traits = CClientWindowTraits,
typename _Base = ATL::CWindowImpl<_Derived,
CWindow,
_Traits> >
class CClientWindowImpl:
public _Base
{
public:
typedef CClientWindowImpl<_Derived, _Base, _Traits > _thisClass;
typedef _Base _windowBase;
};
Also a template class, CClientWindowImpl takes three template parameters: the ultimately
derived class, the window creation flags (the window traits), and a base class. Notice that the
default base class for CClientWindowImpl is ATL’s -CWindowImpl class and that the default creation flags are SFL’s client window creation flags. Example 111 shows SFL’s CClientWindow traits
flags.
Example 111 – SLF’s CClientWindow traits flags
typedef CWinTraits<WS_CLIPCHILDREN |
WS_CLIPSIBLINGS |
WS_CHILD |
WS_VISIBLE,
WS_EX_STATICEDGE> CClientWindowTraits;
SFL’s client window class is usually mixed in with other classes. For example, SFL’s Model-ViewController architecture uses CClientWindow as one of -CMvcClientViewport’s base classes, as
illustrated in Example 112.
Example 112 – SFL’s CMvcClientViewport
class CMvcClientViewport : public
public
public
public
{
…
};
CClientWindowImpl<T>,
_Viewport,
CEventRouterMap<T>,
IvisualWindow
When developing applications, this class is often useful when you need to embed one window into
another window.
Next you’ll examine SFL’s Multiple Document Interface support.
156
12.6.4 MDI Support
SFL provides support for developing Multiple Document Interface (MDI) applications. MDI applications are more complex than SDI applications because MDI applications have to be built to
handle multiple open windows and documents at once. The MDI specification stipulates several
kinds of windows including the main MDI Frame window, the MDI Client window, and the MDI
Child window. Figure 14 shows the MDI Architecture.
Figure 14 – MDI Architecture
SFL hides the complexities behind MDI applications using a set of classes. SFL’s MDI classes
include CMDIChildImpl, CMDIClientWindow, CMDIFrame, and CMDIFrameImpl. Following is
an explanation of each class.
12.6.4.1CMDIChildImpl
SFL’s CMDIChildImpl class manages the MDI child’s menu, the WM_MDIACTIVATE message to set
up that menu within the MDI frame, and the special steps involved in creating an MDI child window. CMDIChildImpl is usually used as a base class for MDI child windows within your
application. CMDIChildImpl is a template class taking the derived class as the first parameter and
the menu resource as the second parameter. Example 113 shows an example of using
CMDIChildImpl as a base class.
Chapter 12 Developing Applications 157
Example 113 – SFL’s CMDIChildImpl
class CMyMDIChild : public CMDIChildImpl<CMyMDIChild,
IDR_SFLMDISimpleApp>
{
…
};
12.6.4.2CMDIClientWindow
An MDI application needs to contain an MDI client window within the main frame window. The
job of the MDI client window is to manage SFL’s MDI child window creation. You usually don’t
need to use this class explicitly—the class is usually mixed into the frame window.
12.6.4.3CMDIFrame
The job of SFL’s CMDIFrame class is to frame an MDI application and manage the application’s
MDI-ness. This means being able to perform such functions as finding the currently active MDI
child window, toggling through the application’s windows, cascading the windows, and tiling the
windows. CMDIFrame is a template class taking a single template parameter, the MDI client window to use. Example 114 shows the declaration of SFL’s CMDIFrame class.
Example 114 – SFL’s CMDIFrame class
template <typename _MDIClient = CMDIClientWindow<> >
class CMDIFrame:
public CWindow
{
protected:
typedef _MDIClient CMDIClientWindow;
public:
CMDIClientWindow m_wndClient;
};
Notice that CMDIFrame uses CMDIClientWindow as the default MDI client window. CMDIFrame
is usually mixed into SFL’s MDI architecture via CMDIFrameImpl.
12.6.4.4CMDIFrameImpl
The CMDIFrame class is still an intermediate layer in the MDI-ness of an application. The final
layer in an MDI application is usually CMDIFrameImpl. Example 115 shows SFL’s
CMDIFrameImpl class.
Example 115 – SFL’s CMDIFrameImpl class
template <typename
unsigned
typename
typename
typename
158
_Derived,
int _nResource,
_Traits = CFrameWinTraits,
_MDIClient = CMDIClientWindow<>,
_Base = CMDIFrame<_MDIClient> >
class CMDIFrameImpl:
public CFrameWindowImpl<_Derived,
_nResource,
_Traits,
_Base>
{
…
};
The first template parameter is the ultimately-derived class. The second parameter represents the
resource id. The window creation flags are passed in as the third template parameter, defaulting to
the ATL’s CFrameWindowTraits class. The MDI client window is passed as the fourth parameter,
defaulting to SFL’s -CMDIClientWindow. The final parameter is the base class, which defaults to
SFL’s CMDIFrame class.
CMDIFrameImpl is meant to be the base class for the main frame window in an MDI application.
Example 116 shows how to declare a main MDI frame window using CMDIFrameImpl.
Example 116 – Using SFL’s CMDIFrameImpl class
class CMainFrame :
public foundation::CMDIFrameImpl<CMainFrame, IDR_MAINFRAME>,
public foundation::CMDIMessagePreTranslator<CMainFrame>,
{
public:
typedef foundation::CMDIFrameImpl<CMainFrame, IDR_MAINFRAME>
_WindowBase;
{
…
};
There is one final note concerning the MDI frame window—its message preprocessing capabilities
(the PreTranslation facilities). Every MDI Frame must derive from CMDIMessagePreTranslator if it
wants the MDI accelerators to work.
12.6.5 Common Dialogs
In the early days of Windows programming, developers had to write their own versions of dialog
boxes for opening files, saving files, finding and replacing strings in a document, and choosing
fonts and colors. Windows 3.1 introduced API functions for creating these common dialog boxes.
SFL wraps the Windows common dialog box API to make common dialogs much easier to manage.
Here you’ll survey SFL’s common dialog box classes, including the COpenFileDialog and
CSaveAsFileDialog classes, the CFontDialog class, the CColorDialog class, and the CFindDialog
and CReplaceDialog classes.
12.6.5.1COpenFileDialog and CSaveAsFileDialog
SFL contains implementations of the common file management dialog boxes. Both
COpenFileDialog and CSaveAsFileDialog inherit from CFileDialogImpl. -CFileDialogImpl serves
as the base class for SFL’s common file dialog boxes. CFileDialogImpl inherits from ATL’s
CDialogImplBase and so has the normal dialog box functionality. CFileDialogImpl has a member
variable of type OPENFILENAME named m_ofn, which the file open and file save dialog boxes use to
pass to the file dialog API functions. In addition, CFileDialogImpl has a boolean member variable
Chapter 12 Developing Applications 159
named m_bOpenFileDialog. Both COpenFileDialog and CSaveAsFileDialog override the
DoModal() function. DoModal() simply checks this flag to decide whether to call
GetOpenFileName() or GetSaveFileName() API functions.
Example 117 shows how to use COpenFileDialog to get a file name and path.
Example 117 – Using COpenFileDialog
void DoFileOpen()
{
COpenFileDialog dlg("txt",
"Text files (*.txt)\0*.txt\0All files (*.*)\0*.*\0");
if (dlg.DoModal() != IDCANCEL) {
ATLTRACE("Complete file name is %s\r\n", dlg.GetFileName());
ATLTRACE("File name is %s\r\n", dlg.GetFileTitle());
}
}
Example 118 shows how to use CSaveAsFileDialog to get a file name and path.
Example 118 – Using CSaveAsFileDialog
void DofileSave()
{
CSaveFileDialog dlg("txt",
"Text files (*.txt)\0*.txt\0All files (*.*)\0*.*\0");
if (dlg.DoModal() != IDCANCEL) {
ATLTRACE("Complete file name is %s\r\n", dlg.GetFileName());
ATLTRACE("File name is %s\r\n", dlg.GetFileTitle());
}
}
12.6.5.2CFontDialog
The CFontDialog class wraps the standard Windows font-selection dialog box into your application. A CFontDialog object is a dialog box with a list of fonts currently installed in the system. The
user can select a particular font from the list.
To construct a CFontDialog object, use the provided constructor, or derive a new subclass and use
your own custom constructor.
The CFontDialog contains a data member of type CHOOSEFONT named m_cf. Once a CFontDialog
object has been constructed, you can use the m_cf structure to initialize the values or states of controls in the dialog box. For more information on this structure, see the Win32 SDK documentation.
After initializing the dialog object’s controls, call the DoModal() member function to display the
dialog box and allow the user to select a font. DoModal() calls the standard Windows
ChooseFont() API function. DoModal() returns whether the user selected the OK (IDOK) or Cancel
(IDCANCEL) button.
If DoModal() returns IDOK, you can use one of CFontDialog’s member functions to retrieve the
information input by the user. You can also use m_cf directly.
Example 119 shows how to use the font dialog.
160
Example 119 – Using CFontDialog
void DoFontSel() {
LOGFONT lf;
CFontDialog dlg(&lf);
if (dlg.DoModal() != IDCANCEL) {
LPCTSTR szFaceName;
szFaceName = GetFaceName();
LPCTSTR szStyleName;
szStyleName = GetStyleName();
}
}
12.6.5.3CColorDialog
The CColorDialog class wraps the standard Windows color-selection dialog box. A CColorDialog
object is a dialog box with a list of colors that are defined for the display system. The user can select
or create a particular color from the list, which is then reported back to the application when the
dialog box exits.
To construct a CColorDialog object, use the provided constructor or derive a new class and use
your own custom constructor.
The CColorDialog has a member variable of type CHOOSECOLOR named m_cc. Once the dialog box
has been constructed, you can set or modify any values in the m_cc structure to initialize the values
of the dialog box’s controls.
After initializing the dialog box’s controls, call the DoModal() member function to display the dialog box and allow the user to select a color. DoModal() simply calls the ChooseColor() Windows
API to show the standard color dialog box.
If DoModal() returns IDOK, CColorDialog’s m_cc member contains the information input by the
user.
Example 120 shows how to use CColorDialog.
Example 120 – Using CColorDialog
void DoColorSel() {
CColorDialog coldlg(RGB(0xCC, 0x0c, 0x0c), CC_FULLOPEN |
CC_RGBINIT);
if (coldlg.DoModal() != IDCANCEL) {
COLORREF color;
color = coldlg.GetColor();
}
}
12.6.5.4CFindDialog and CReplaceDialog
CFindDialog and CReplaceDialog wrap the standard replace dialog boxes. Both these dialog boxes
are modeless (as opposed to a normal modal dialog box). These dialog boxes are usually created on
the heap instead of stack. Both operate in a similar fashion. The CFindDialog lets you type in a
string for which to search. The dialog box then calls back to the parent window every time the user
clicks on the Find button. The application then takes the string typed in by the user and tries to find
Chapter 12 Developing Applications 161
it within the document. The CReplaceDialog works in the same fashion, except the replace dialog
also includes a field for typing a replacement string. Example 121 shows a frame class that uses the
find and replace dialog boxes.
Example 121 – Using the CFindDialog and the CReplaceDialog common dialog boxes
class CMainFrame :
public CFrameWindowImpl<CMainFrame, IDR_UseCommonDlgs>
{
public:
typedef CFrameWindowImpl<CMainFrame, IDR_UseCommonDlgs>
_BaseClass;
BEGIN_MSG_MAP(CMainFrame)
COMMAND_ID_HANDLER(ID_EDIT_FONTDIALOG, OnFontDialog)
COMMAND_ID_HANDLER(ID_EDIT_FINDDIALOG, OnFindDialog)
COMMAND_ID_HANDLER(ID_EDIT_FINDREPLACEDIALOG, OnReplaceDialog)
CHAIN_MSG_MAP(_BaseClass)
END_MSG_MAP()
LRESULT OnFindDialog(WORD, WORD, HWND, BOOL&)
{
CFindDialog *dlg = new CFindDialog;
dlg->Create("What's up Doc?");
return 0;
}
LRESULT OnReplaceDialog(WORD, WORD, HWND, BOOL&)
{
CReplaceDialog *dlg = new CReplaceDialog;
dlg->Create("What's up Doc?", "We really mean it");
return 0;
}
LRESULT OnFindReplace(UINT , WPARAM , LPARAM lParam, BOOL& )
{
OutputDebugString("On Find Replace Notification\n");
CReplaceDialog* pDlg = CReplaceDialog::GetNotifier(lParam);
string sString;
sString = pDlg->m_szFindWhat;
// or do this...
sString = pDlg->GetFindString();
return 0;
}
};
This frame window class responds to the Find and Replace menu commands by creating and
showing the appropriate dialog box. Each time the user hits the Find or Replace button, the common dialog box calls back to the frame window. Then the frame window can do whatever it needs
to do with the find and replace strings.
162
12.7 User Interface Updating
The Stingray Foundation Library classes include facilities for keeping an application’s user interface consistent with the state of the application. For example, an application may have a toolbar
and provide a menu option for showing and hiding the menu. While the toolbar is showing, the
menu should be checked. While the toolbar is hidden, the menu should be unchecked. This section
describes how the SFL User Interface Updating mechanism works, and how to use the mechanism
to create a consistent user interface.
12.7.1 User Interface Updating Essentials
SFL’s User Interface Updating mechanism has several components, including
CUIUpdateGenerator, the IEventRouter interface, CUIUpdateAdapter, and an interface named
IIdleHandler.
The UIUpdating mechanism works like this. Any class wishing to implement User Interface updating handles idle-time processing by plugging in its own idle-time processing interface while
processing the WM_CREATE message. You’ll see the specific calls for doing that shortly. The class performing User Interface updating also inherits from a class named CUIUpdateGenerator, which has
a member function named GenerateUIUpdates(). The class performing User Interface updating
handles idle-time processing by calling GenerateUIUpdates(). GenerateUIUpdates() goes
through the menu, toolbars, and status bars that have been registered in to the User Interface
Updating mechanism, creating a wrapper class for each User Interface item (menu, tool bar button,
and status pane). The User Interface mechanism generates a WM_UIUPDATE message and routes the
event, where the class performing User Interface updating may modify the element that handles it.
Classes wanting to receive UIUpdating events inherit from CUIUpdateAdapter, which redirects
the UI updating logic to a handler function. The handler function takes each incoming User Interface element and massages it in a way appropriate for the current state of the application. For
example, if the toolbar is already showing, you may want to put a check mark on the Toolbar toggling menu option. When the toolbar is hidden, you may wish to remove that check mark.
Figure 15 shows the User Interface Updating mechanism in action.
Figure 15 – The SFL User Interface Updating mechanism in action
Chapter 12 Developing Applications 163
Figure 16 illustrates the architecture of SFL’s User Interface Updating mechanism.
Figure 16 – SFL’s User Interface Updating architecture
The best way to examine SFL’s User Interface Updating mechanism is to examine a class that uses
the User Interface Mechanism. Example 122 shows an entire class with User Interface Updating
applied.
Example 122 – A C++ class with User Interface Updating applied
template <class _Base>
class CMainFrameBase : public
public
public
public
public
public
public
{
// Attributes
protected:
bool m_bMenuCheckA;
bool m_bMenuCheckB;
bool m_bEnable;
IEventRouterImpl,
_Base,
foundation::IIdleHandler,
CEventRouterMap< CMainFrame >,
CCommandAdapter,
CUIUpdateAdapter,
CUIUpdateGenerator
// Embedded types
public:
typedef CMainFrameBase<_Base> _ThisClass;
typedef _Base _BaseClass;
164
// Constructors/destructor
public:
CMainFrameBase() {
m_bMenuCheckA = true;
m_bMenuCheckB = false;
m_bEnable = true;
AddListener(static_cast<ICommandListener*>(this));
}
// Operations
public:
virtual void InitLayout(ILayoutNode* pRootNode) {
_BaseClass::InitLayout(pRootNode);
}
// GUID map implements QueryGuid()
public:
BEGIN_GUID_MAP(_ThisClass)
GUID_CHAIN_ENTRY(IEventRouterImpl)
GUID_CHAIN_ENTRY(CCommandAdapter)
GUID_CHAIN_ENTRY(CUIUpdateAdapter)
END_GUID_MAP
// Implement AddRef() and Release() to resolve ambiguity
public:
virtual ULONG STDMETHODCALLTYPE AddRef()
{ return 1L;}
virtual ULONG STDMETHODCALLTYPE Release()
{ return 1L; }
// Message map
public:
BEGIN_MSG_MAP(_ThisClass)
MESSAGE_HANDLER_DELEGATE(WM_CREATE, OnCreate)
CHAIN_MSG_MAP(CEventRouterMap<CMainFrame>)
CHAIN_MSG_MAP(_BaseClass)
END_MSG_MAP()
BEGIN_COMMAND_MAP(_ThisClass)
COMMAND_ENTRY(ID_UIUPDATETEST_MENUCHECKA, OnMenuCheckA)
COMMAND_ENTRY(ID_UIUPDATETEST_MENUCHECKB, OnMenuCheckB)
COMMAND_ENTRY(ID_UIUPDATETEST_DISABLE, OnDisable)
COMMAND_ENTRY(ID_APP_EXIT, OnAppExit)
END_COMMAND_MAP
// Event Handlers
public:
LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam,
BOOL& bHandled) {
// Register objects for participation in command UI updating.
SetUIUpdateRouter(guid_cast<IEventRouter*>(this));
_Module.GetMessageLoop()->AddIdleHandler(this);
bHandled = FALSE;
return 0L;
}
Chapter 12 Developing Applications 165
virtual void OnFinalMessage(HWND hWnd) {
_BaseClass::OnFinalMessage(hWnd);
_Module.GetMessageLoop()->RemoveIdleHandler(this);
}
virtual bool OnIdle() {
GenerateUIUpdates();
HandleMenu(GetMenu());
// do this if there’s a toolbar…
// HandleToolBar(&m_wndToolBar);
return true;
}
virtual bool OnMenuCheckA(UINT nID, int nNotifyCode) {
if(m_bMenuCheckA) {
m_bMenuCheckA = false;
} else {
m_bMenuCheckA = true;
}
return true;
}
virtual bool OnDisable(UINT nID, int nNotifyCode) {
if(m_bEnable) {
m_bEnable = false;
} else {
m_bEnable = true;
}
return true;
}
virtual bool OnMenuCheckB(UINT nID, int nNotifyCode) {
if(m_bMenuCheckB) {
m_bMenuCheckB = false;
} else {
m_bMenuCheckB = true;
}
return true;
}
virtual bool OnAppExit(UINT nID, int nNotifyCode) {
DestroyWindow();
return true;
}
virtual bool OnUIUpdate(IUIUpdateElement* pUIUpdateElement,
UINT nCommandID) {
switch(nCommandID) {
case ID_UIUPDATETEST_DISABLE:
pUIUpdateElement->Enable(nCommandID, m_bEnable);
break;
case ID_UIUPDATETEST_MENUCHECKA:
pUIUpdateElement->SetCheck(nCommandID, m_bMenuCheckA);
break;
166
case ID_UIUPDATETEST_MENUCHECKB:
pUIUpdateElement->SetCheck(nCommandID, m_bMenuCheckB);
break;
default:
return 0;
};
return true;
}
};
This listing illustrates a main window frame with User Interface Updating applied. Notice how the
class derives from IEventRouterImpl, IIdleHandler, -CEventRouterMap, CCommandAdapter,
CUIUpdateAdapter, and CUIUpdateGenerator.
Deriving from IIdleHandler adds idle processing capability to the class. The class registers itself as
an idle-time processor during window creation by getting the module’s message loop and calling
AddIdleHandler(). AddIdleHandler() comes in through inheritance from the message loop.
Notice the class also disconnects the idle handler during OnFinalMessage().
Deriving from CUIUpdateAdapter brings in the virtual function OnUIUpdate(), which the class
overrides by checking the user element ID and handling the menu item appropriately (either by
setting the text, enabling the control, setting or a check box.
The idle time processing generates WM_UIUPDATE messages for every toolbar item, every status bar
pane, and every menu item. These are handled in the -OnUIUpdate() handler.
Chapter 12 Developing Applications 167
168
Chapter 13
The AppWizard
13.1 Overview
To make it easier to write SFL-based applications, SFL provides an AppWizard. Like the MFC
AppWizard, the SFL AppWizard generates several kinds of applications:
пЃµ
Multiple Top-Level Window Interface applications
пЃµ
Multiple Document Interface applications
пЃµ
Dialog-based applications
пЃµ
A bare-bones “Hello World” application
To generate an application using the SFL AppWizard, just choose File|New from the Visual Studio
main menu. Then select SFLWiz70 from the available projects, shown in Figure 17.
Chapter 13 The AppWizard 169
Figure 17 – The Stingray AppWizard within the File|New project dialog box
After giving your application a name, advance the AppWizard by pressing the OK button. You’ll
see the next dialog box, which lets you select the type of application to generate. Figure 18 shows
the SFL AppWizard dialog box.
170
Figure 18 – Selecting the kind of application to generate through the SFL AppWizard
Following is a description of each kind of application:
пЃµ
Hello World Application. This is the simplest kind of application you can create.
This option generates a small, single-window application, very much like the
HelloSFL application mentioned above.
пЃµ
SDI Application. This option generates an application featuring a single frame.
The default for this type of application is for Multiple Top Level Interface. You can
easily change the behavior, however, by changing the application base class from
CMTIApp to CApp and overriding the File | New menu handler. The SDI
Application option allows you to generate applications containing toolbars and
status bars, the SFL Layout Manager, the Model-View-Controller architecture, and
Print Preview.
пЃµ
MDI Application. This option generates an application that features a single frame
showing multiple document windows. The MDI Application option allows you to
generate applications containing toolbars and status bars, the SFL Layout Manager,
the Model-View-Controller architecture, and Print Preview.
пЃµ
Dialog-based Application—A Dialog-based application is one whose window is
simply a dialog box.
When creating SDI Applications or MDI Applications, the SFL AppWizard provides the ability to
add Model/View/Controller support, a toolbar and a status bar, an about box, and even print preview. The SFL AppWizard responds to the Finish button by generating a set of C++ classes, based
on the Stingray Foundation Library, which will compile into a full-fledged, living and breathing
Windows application.
Chapter 13 The AppWizard 171
13.2 Conclusion
Developers wishing to create applications using C++ have traditionally had three choices. Developers could go with the basic Win32 API, the Microsoft Foundation Classes, or the minimal
windowing support within the Active Template Library.
Developing applications using Win32 at the native level is akin to using assembly language. While
you get complete control over all aspects of an application, you also have complete responsibility
for getting everything right—including managing the window creation, the switch statements,
device contexts, GDI objects, and other myriad other issues involved in Win32-based development.
MFC removes much of that drudgery, letting you concentrate on the application itself. However,
MFC is both fairly substantial and coupled to itself. ATL provides a minimal amount of windowing
support, but is missing some of the features like the Document View Architecture, User Interface
Updating, and Multiple Document Interface support.
SFL answers this need by providing a set of template-based classes and an AppWizard. SFL makes
creating applications easier than using either Win32 alone or ATL. Moreover, SFL isn’t tightly coupled to itself like MFC is, making it easier to mix application features independently of each other.
In addition, SFL is substantially smaller than MFC, meaning your clients won’t need a huge DLL
providing run-time support.
172
Chapter 14
Persistence Framework
14.1 Persistence and Property Bags
Persisting application data so that it can be reused between different runs of the application is a
problem developers continually face. Most of the services provided by the operating system for
this effect consist of APIs for reading and writing byte streams to files on disk. The application
developer has to take care of issues like file formats, object construction and recovery, object reference resolution, and so on.
SFL offers a solution for this problem in the form of its Persistence package, a set of classes based
on the standard COM property bag concept. The property bags implementation provided in SFL is
usable in two ways:
пЃµ
As a binary library that publishes its services in the form of COM objects
пЃµ
At the source code level from a C++ application
14.1.1 COM Property Bags
COM defines several standard mechanisms for object persistence, including:
пЃµ
COM storages
пЃµ
COM streams
пЃµ
Property bags
The first two methods represent binary, non-structured storage mechanisms. The application is
responsible not only for the contents that are stored there, but also for the physical format in which
the data is stored.
Property bags are a more structured persistence mechanism. Conceptually, a property bag is a set
of name-value pairs, each one of which is a simple property. A property can also contain multiple
subproperties, making it a composite property. This means that property bags are hierarchical, with
a tree-like logical structure.
Property bags are used extensively by technologies like ActiveX documents and ActiveX controls.
Many other COM objects are also capable of persisting their internal state in property bags.
Chapter 14 Persistence Framework 173
COM originally defined the interface IPropertyBag as the standard mechanism to manipulate a
property bag object. A more flexible interface, IPropertyBag2, was introduced in later versions of
COM to accommodate some functionality the original IPropertyBag interface was not able to perform, such as providing metainformation about the actual properties existing in a bag, or loading
the state of an object already instantiated.
The COM property bags definition standardizes only the way in which a COM component interacts with a property bag object. It says nothing about the media where the persisted data will be
stored for later retrieval. The definition provides a logical format for the data, in the form of namevalue pairs; but the actual shape of this data in the persisted media is implementation-dependent.
Unlike streams and storages, COM does not offer a single property bag implementation. Applications that make use of this technology, like Microsoft Visual Basic or Microsoft Internet Explorer,
have their own private implementations of these interfaces, not available to be used by other
applications.
14.1.2 Persistable Objects
To be loaded from or saved to persistent media using the property bag mechanism, a COM object
must implement either the IPersistPropertyBag or IPersistPropertyBag2 standard COM interfaces, depending on the property bag interface being used. Both interfaces provide Load() and
Save() methods to retrieve or store an object to a property bag. For more documentation of these
interfaces, consult the COM Reference.
174
14.2 SFL Property Bags
SFL introduces two property bag implementations to be used generically by custom applications.
One uses the Windows registry as data storage; the other uses the Microsoft XML Document Object
Model. These two media are prime candidates for property bag storage, due to their flexible API
and their hierarchical nature.
The SFLPropBags sample, which is in the COMServers\Persistence folder in your Stingray Foundation Library installation, is an ATL-based COM DLL that publishes the two SFL property bag
implementations as COM objects. The property bags can be used from any COM-enabled programming language or development tool, like Visual Basic, Visual J++, or Visual C++. The property bag
classes can also be used in a C++ application at a source code level.
Which approach to use depends on the specific characteristics of your project. The benefits and
drawbacks of each approach are the same that you face when deciding whether to use source codebased components or binary COM objects. If the objects you want to persist in your application are
already COM-enabled, or if you are using a language other than C++, you should use the property
bags as independent COM objects. If you want less dependence on external libraries, or if you want
to maintain the COM requirements on your objects as an implementation detail of your code, you
are better off using the implementations at the source code level.
14.2.1 Data Types
The property bag specification supports every VARIANT-compatible type to be stored and retrieved
in a property bag.
SFL’s implementation supports the following types. The specific storage characteristics depend on
what property bag class is being used.
пЃµ
Basic types (integer, boolean, long, and so on)
пЃµ
Strings
пЃµ
Objects. If the object is persistable, it is asked to save itself as a sublevel of the
property bag tree. Objects are saved as composite properties, where the entire set of
the object properties is contained under the name-value pair that identifies the
object within its parent. If the object is not persistable, it cannot be manipulated by
the property bag, so an error occurs.
пЃµ
Safe arrays. Elements are saved and retrieved one by one, recursively applying the
rules described above depending on the type of the element, with one exception: if
the safe array element type is INT1 (byte), the safearray is treated as a binary
stream and manipulated as such.
SFL’s property bag implementations do not support user-defined types (structures). If you want to
store a structure, you have to decompose it explicitly into its constituent data fields and store these
individually. Then at read time, you must restore them explicitly.
Chapter 14 Persistence Framework 175
14.2.2 IPersistenceStrategy Interface
The IPropertyBag and IPropertyBag2 interfaces offer methods to read and write data to and from
the persistent media. This is enough functionality for the object being persisted, which needs only
those methods to load or save its information.
However, an application that uses the property bag to serialize its objects needs more functionality.
The application must control where the data is going to be persisted, for example, and needs to
launch the serialization process. This functionality is not standardized by any of the COM persistence interfaces.
The SFL implementation defines an additional interface that is implemented by all the property
bag concrete classes. It is the IPersistenceStrategy interface.
The IPersistenceStrategy::Init() method initializes the property bag. It takes a VARIANT
parameter, whose semantics are dependent on the actual implementation. For example, for a property bag whose media is a file in the file system, the required initialization parameter could be the
file name. For an implementation that uses a relational database, it could be the connection information for the database.
The Save() and Load() methods start the corresponding operation on a given persistable object.
This object becomes the root of the property bag. Given the hierarchical nature of property bags,
multiple persistable objects can be stored as subobjects of this root object.
The Commit() method is used to commit to persistent media a save operation. The data is not guaranteed to be persistent until the Commit() method has been invoked.
14.2.3 Registry Property Bag
The registry property bag implementation stores the information in a given key in the registry. The
implementation assumes that the user executing the application has read/write rights in the
HKEY_CURRENT_USER key in the registry.
The Init() method in this IPersistenceStrategy implementation expects the name of the registry
key where the data will be stored. The string passed must be the relative path of the key within the
windows registry database, assuming HKEY_CURRENT_USER as the starting point. For example:
pPropBagInit->Init (“Software\\MyCompany\\MyApp\\Data”)
This directs all input and output queries to the property bag to the registry key
HKEY_CURRENT_USER\Software\MyCompany\MyApp\Data.
The Commit() method does not have any effect in the case of Registry property bags, but it is a
good idea to use it always after Load() and Save() operations to enhance the transparency of the
media.
14.2.4 XML Property Bag
The XML property bag implementation makes use of the Microsoft XML document model (DOM)
provided as part of Internet Explorer 5.0 or later. Previous versions of the XML DOM are incompatible with this one, and therefore this implementation will not work in such systems.
176
Microsoft’s DOM always manages the underlying XML document in memory until it is told to save
it to permanent media. Therefore, you always need to call Commit() after a saving operation so the
SFL implementation can appropriately save the XML document to disk.
The IPersistenceStrategy::Init() method can take two possible parameters in the XML property bag:
пЃµ
A string, that represents the name of a file on disk that contains the XML document.
пЃµ
A COM stream instance (object that implements IStream). The XML document will
be written to this stream, regardless of what its media is, adding one more level of
indirection to the persistence operation. This option is useful for memory-only
persistence operations like clipboard interaction or drag-and-drop.
A proprietary XML document format is used to represent the property bag. This is an example of
the resulting XML file:
Example 123 – XML document representing a property bag
<SflPropBag>
<PersistableObject PropName="BooksCollection"
CLSID="{395FF86C-0AD5-4B1D-A317-4AB3A8FD3370}">
<BasicType PropName="BooksCount" ValueType="2">2</BasicType>
<PersistableObject PropName="Book1"
CLSID="{445E7451-7261-11D2-9D33-00C04F91E286}">
<BasicType PropName="Title"
ValueType="8">The C++ Programming Language</BasicType>
<BasicType PropName="AuthorsCount" ValueType="3">1</BasicType>
<PersistableObject PropName="Author1"
CLSID="{445E744F-7261-11D2-9D33-00C04F91E286}">
<BasicType PropName="FirstName" ValueType="8">Bjarne</BasicType>
<BasicType PropName="LastName" ValueType="8">Stroustrup</BasicType>
</PersistableObject>
</PersistableObject>
</PersistableObject>
</SflPropBag>
14.2.5 Examples
The following is some code in Visual Basic that illustrates the usage of the SFL property bag
implementations.
Example 124 – Initializing and loading an object from an XML property bag
Set bag = New XMLPropertyBag
bag.Init "books.xml"
bag.Load "BooksCollection", Books
bag.Commit
Example 125 – Saving an existing object to a registry property bag
Set bag = New RegistryPropertyBag
bag.Init "Software\Stingray\SFL\Persistence"
bag.Save "BooksCollection", Books
bag.Commit
Chapter 14 Persistence Framework 177
The implementation of the persistable object in Visual Basic requires the class to be marked as Persistable, by assigning the corresponding value to the class properties. This adds two methods to the
class, ReadProperties() and WriteProperties(), which correspond to the Load() and Save()
methods of the IPropertyBag interface. Example 126 shows the implementation of these methods
for a book collection class.
Example 126 – Implementing Load() and Save() methods for a collection class
Private Sub Class_ReadProperties(PropBag As PropertyBag)
Dim count As Integer
count = PropBag.ReadProperty("BooksCount")
ReDim Books(1 To count)
Dim i As Integer
For i = 1 To count
Set Books(i) = PropBag.ReadProperty("Book" & i)
Next i
End Sub
Private Sub Class_WriteProperties(PropBag As PropertyBag)
Dim count As Integer
count = UBound(Books)
PropBag.WriteProperty "BooksCount", count
Dim i As Integer
For i = 1 To count
PropBag.WriteProperty "Book" & i, Books(i)
Next i
End Sub
To implement a persistable object in C++, you need to implement one of the COM interfaces
IPersistPropertyBag or IPersistPropertyBag2. How to do this depends on the framework you are
using to develop your components. Example 127 shows what it might look like if you used ATL.
Example 127 – Implementing a persistable C++ object using ATL
class CPersistableComponent:
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CPersistableComponent, &__uuidof(CPersistableComponent)>,
public IMyComponent,
public IPersistPropertyBag
{
public:
BEGIN_COM_MAP(CPersistableComponent)
COM_INTERFACE_ENTRY(IPersistPropertyBag)
COM_INTERFACE_ENTRY2(IPersist, IPersistPropertyBag)
COM_INTERFACE_ENTRY(IMyComponent)
END_COM_MAP()
Вє
};
It is important to notice that persistable COM objects need to be creatable, since the property bag
needs to create a new instance using the standard COM mechanisms whenever it is retrieving an
object of that class. To be created, an object requires a CLSID and a registered class factory.
Example 128 shows the implementation of the Save() and Load() methods of a persistable object.
The implementation makes use of the Load() and Save() methods in the IPropertyBag (or
IPropertyBag2) interface pointer it receives as a parameter to retrieve or store individual pieces of
data the object considers to be part of its persistent internal state.
178
Example 128 – Implementing Save() and Load() methods for a persistable C++ object
STDMETHOD(Load)(IPropertyBag* pPropBag, IErrorLog* pErrorLog)
{
HRESULT hr = S_OK;
_variant_t vaProp;
hr = pPropBag->Read(OLESTR("Number"), &vaProp, pErrorLog);
if (FAILED(hr)) return E_FAIL;
m_nANumber = static_cast<long>(vaProp);
vaProp.Clear();
CPoint pt;
hr = pPropBag->Read(OLESTR("PositionX"), &vaProp, pErrorLog);
if (FAILED(hr)) return E_FAIL;
pt.x = static_cast<long>(vaProp);
hr = pPropBag->Read(OLESTR("PositionY"), &vaProp, pErrorLog);
if (FAILED(hr)) return E_FAIL;
pt.y = static_cast<long>(vaProp);
return S_OK;
}
STDMETHOD(Save)(IPropertyBag* pPropBag, BOOL fClearDirty,
BOOL fSaveAllProperties)
{
HRESULT hr = S_OK;
_variant_t vaProp;
vaProp = static_cast<long>(m_nANumber);
hr = pPropBag->Write(OLESTR("Number"), &vaProp);
if (FAILED(hr)) return E_FAIL;
vaProp = static_cast<long>(m_rc.left);
hr = pPropBag->Write(OLESTR("PositionX"), &vaProp);
if (FAILED(hr)) return E_FAIL;
vaProp = static_cast<long>(m_rc.top);
hr = pPropBag->Write(OLESTR("PositionY"), &vaProp);
if (FAILED(hr)) return E_FAIL;
return S_OK;
}
Chapter 14 Persistence Framework 179
14.3 Using Property Bags in C++ Code
Although the SFL property bags implementation are intended to be used as an independent COM
library, it is also possible to use the individual classes directly from source code in a C++ application when a flexible persistence mechanism needs to be put in place.
First, it is important to note that, even if used at the source code level, SFL’s property bag implementations require some degree of COM support in order to work correctly. For example, the
persistable objects need to implement the IUnknown interface and the corresponding IPersistXXX
interface. Also, the data that will be stored in a property bag needs to be representable as a set of
name-value pairs, where the values are of VARIANT-compatible types.
You can adapt your C++ objects by partially enabling just the COM needed to work with the property bags at a source code level. For example, Example 129 illustrates a C++ class CHybrid that
combines the C++ interface mechanism based on the IQueryGuid interface, with an
IPersistPropertyBag implementation put together using ATL as the COM framework.
Example 129 – Enabling COM support for property bags within a C++ class
class __declspec(uuid("B348A7BB-8573-4979-8E9E-40387CC80D29")) CHybrid:
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass< CHybrid, &__uuidof(CHybrid)>,
public IPersistPropertyBag,
// IHybrid derives from both IUnknown and IQueryGuid
public IHybrid
// Access to C++ class functionality from COM
{
public:
DECLARE_NO_REGISTRY()
DECLARE_PROTECT_FINAL_CONSTRUCT()
// Internal object
// COM interface map
BEGIN_COM_MAP(CHybrid)
COM_INTERFACE_ENTRY(IPersistPropertyBag)
COM_INTERFACE_ENTRY2(IPersist, IPersistPropertyBag)
COM_INTERFACE_ENTRY(IHybrid)
END_COM_MAP_NO_PURE()
ATL_IUNKNOWN_IMPL()
// SFL interface map
BEGIN_GUID_MAP(CHybrid)
GUID_ENTRY(IHybrid)
END_GUID_MAP
<...>
};
The IQueryGuid interface is used widely in SFL. See Chapter 3, “Interface-Based Programming,”
for more information. The class CHybrid utilizes the QueryGuid mechanism for C++ interfacebased programming. However, when persistence was added, an IUnknown implementation was
needed, as well as the interface querying based on the QueryInterface function used in COM. Since
this is not a true COM class, it is not available for external instantiation and it doesn’t have to deal
with issues like apartments or contexts, thread models, or other COM-isms. Its lifetime does not
even need to be managed by the reference counting mechanism. It needs only enough functionality
to satisfy the expectations of the property bag:
180
пЃµ
An IPersistPropertyBagX implementation
пЃµ
An IUnknown implementation that knows how to respond to requests for that
interface
пЃµ
Optionally, a registered class factory that allows the property bag to create new
instances of the class
Persistable objects do not need to have their COM class factory registered in the system. The property bag implementation is adjusted to always look at the local _Module variable first for “internal”
class factories corresponding to C++ objects disguised as COM objects for persistence purposes
only.
If you want to use an altogether different creation mechanism for your persistable objects, the
IPropertyBag2 interface offers a method LoadObject(). It allows you to load a persisted state in an
instance already created, as in Example 130.
Example 130 – Loading a persisted state in an existing instance
CHybrid* pHybrid = GetNewHybridInstanceBySomeOtherMechanism();
HRESULT hr = pBag2->LoadObject(OLESTR("ExistingInstance"), 0,
static_cast<IUnknown*>(pHybrid), NULL);
14.3.1 MVC Integration
Usually, when your application uses the Model-View-Controller architecture, the model is the
place from where the data you want to persist is accessible. You should save a model instance to a
disk file as a response to the Save or Save as command, and you should retrieve an existing model
from a disk file when your application receives the Open command.
In order to facilitate this typical scenario, SFL includes a class that merges the Model concept from
the MVC architecture with the persistence capabilities of the property bags implementation.
The CMvcPersistableModel class is defined in the header file MvcPersist.h, located in the
include\foundation\mvc folder of your SFL installation. This class defines only two virtual methods: Load() and Save(), both of which receive, as their only parameter, a string that represents the
name of the file to which the disk operation will be directed. Both methods are declared as abstract,
and no implementation is given.
Model objects derived from CMvcPersistableModel can Load() and Save() themselves to disk,
but there is no assumption on how this process is actually going to be accomplished. The
CMvcPropertyBagPersistableModel translates that abstract defined behavior to an actual implementation that uses SFL’s XML property bag as the output of the Save() operation and input of the
Load() process.
The CMvcPropertyBagPersistableModel offers the following services:
пЃµ
Derives from the basic ATL classes in order to provide an IUnknown
implementation for your model.
пЃµ
Implements the IPersistPropertyBag interface.
пЃµ
Responds to the Load() and Save() methods inherited from
CMvcPersistableModel, initializing a property bag associated to the file name
given.
Chapter 14 Persistence Framework 181
Model classes incorporating persistence in this fashion must include
CMvcPropertyBagPersistableModel among their base classes and override two methods:
WriteToPropertyBag() and ReadFromPropertyBag(). Both of these methods receive a pointer to
an IPropertyBag interface, which will be used for all the input/output operations of the model.
Both methods return a boolean value, which should be set to FALSE to indicate the occurrence of
some error condition (TRUE otherwise).
Example 131 shows how to override these methods.
Example 131 – Overriding read and write methods to enable persistence
class __declspec(uuid("7C540CD2-3B5C-46be-885E-0B82E13A28A6")) CMyModel:
public CMvcPropertyBagPersistableModel<CMyModel>
{
<...>
virtual bool WriteToPropertyBag (
IPropertyBag* pPropBag
)
{
_variant_t vaProp = static_cast<long>(m_size);
hr = pPropBag->Write(OLESTR("Size"), &vaProp);
if (FAILED(hr)) return false;
vaProp(static_cast<IUnknown*>(&m_NestedObject));
HRESULT hr = pPropBag->Write(OLESTR("NestedObject"), &vaProp);
if (FAILED(hr)) return false;
return true;
}
virtual bool ReadFromPropertyBag (
IPropertyBag* pPropBag
)
{
// All our property bags are guaranteed
// to implement this interface
IPropertyBag2Ptr spBag2 = pPropBag;
if (spBag2 == 0) return false;
hr = pPropBag->Read(OLESTR("Size"), &vaProp, pErrorLog);
if (FAILED(hr)) return false;
long m_size = static_cast<long>(vaProp);
// LoadObject does not exist in IPropertyBag
HRESULT hr = spBag2->LoadObject(OLESTR("NestedObject"), 0,
static_cast<IUnknown*>(&m_NestedObject), NULL);
if (FAILED(hr)) return false;
return true;
}
<...>
};
It is important to notice that the IUnknown implementation provided in
CMvcPropertyBagPersistableModel makes no assumption about the allocation of the model. The
instance is not destroyed when the reference counter is decremented past zero, since there is no
assurance the model is allocated on the heap. You can manage the lifetime of your model indepen182
dently of this IUnknown implementation, or you can take advantage of reference counting for
lifetime management by overriding Release() and performing the necessary deallocation process
there.
Chapter 14 Persistence Framework 183
184
Chapter 15
XML Serialization Architecture
15.1 Overview
MFC-application data can be serialized in XML format— using the high-level object model provided by the Stingray XML Serialization architecture. Programmers can easily insert and retrieve
their application data structures (as elements) into and from the XML document. There is full support for multi-level nesting.
Because our architecture closely resembles MFC's serialization architecture, to plug any object into
it all you need to do is implement an XMLSerialize() function in an IXMLSerialize() interface.
We provide default implementations for XML serializing MFC collection classes and GDI objects.
We also provide a CDocument adapter class with built-in functionality for opening and saving
XML documents. You start by overriding XMLSerialize() in your document class, just as you
would with the MFC equivalent.
15.1.1 Usage Example
A sample implementation would look like this:
void CXMLSerArchiveDoc::XMLSerialize(SECXMLArchive& ar)
{
if(ar.IsStoring())
{
ar.Write("Intvalue", m_nIntMemb);
ar.OpenElement("ELEMENT11");
ar.Read("LONGVALUE", m_nLongMemb);
ar.OpenElement("ELEMENT12");
ar.Write("UNSIGNEDVALUE", (WORD)USHRT_MAX);
// Built-in support for collection classes in action:
// In this case m_myPtrArray is a CPtrArray containing
// reference to objects of type CMyObject.
ar.Write(NULL, CPtrArrayFTR<CMyObject, CMyObjectFTR>(m_myPtrArray));
ar.CloseElement("ELEMENT12");
Chapter 15 XML Serialization Architecture 185
ar.CloseElement("ELEMENT11");
}
else if(ar.IsLoading())
{
WORD w;
//Unwanted elements (eg. from obsolete versions can be ignored)
//eg. in this case m_nIntMemb written out is never retrieved.
ar.OpenElement("ELEMENT11");
ar.Read("LONGVALUE", m_nLongMemb);
ar.OpenElement(_T("ELEMENT12"));
ar.Read(_T("UNSIGNEDVALUE"), w);
ar.Read(NULL, CPtrArrayFTR<CMyObject, CMyObjectFTR>(m_myObject));
ar.CloseElement(_T("ELEMENT12"));
ar.CloseElement("ELEMENT11");
}
}
186
15.2 Architecture Classes
15.2.1 The XML Document Adapter class
The Stingray XML serialization architecture has been designed to emulate, as closely as possible,
MFC's serialization mechanism (based on CArchive and CDocument). Enabling the seamless transitioning of a standard document-view type application into the Stingray XML framework was
among the architecture designers’ chief priorities.
The document adapter, SECXMLDocAdapter_T, is a template class that multiply inherits from
your document's base class and IXMLSerialize. The document adapter is a core component of this
architecture; it serves as a bridge between the standard MFC serialization mechanism and our XML
archiving architecture. SECXMLDocAdapter_T overrides certain CDocument virtuals and creates
the plumbing required for the XML serialization—such as creating the XML archive, and saving and
opening .xml documents. All of this is completely transparent to the application. The developer is
expected to provide only the implementation for the IXMLSerialize::XMLSerialize() override
in the document.
The application's document class derives from the SECXMLDocAdapter_T class and provides the
base document, either CDocument or its derivative, as a template parameter. As with ATL, because
SECXMLDocAdapter_T is a template class and the CDocument-based template parameter is its
base, users can conveniently retain existing custom document hierarchies—without the need for
complex workarounds such as multiple-inheritance, abstraction models, etc. The application's document is hooked into the XML serialization framework. Attempting to open or save an .xml file
will automatically invoke the XML serialization routine, the IXMLSerialize::XMLSerialize()
override, in the document class.
If necessary, separate menu entries can be provided for the XML file open/save commands. These
can be incorporated into the framework using existing command handlers in the document
adapter base class. The message map entries needed for hooking the menu commands into the
XML framework are shown below:
BEGIN_MESSAGE_MAP(CChartAppDoc, CGraphDoc)
//{{AFX_MSG_MAP(CChartAppDoc)
ON_COMMAND(ID_FILE_OPENXML, OnSECFileOpenXML)
ON_COMMAND(ID_FILE_SAVEXML, OnSECFileSaveXML)
ON_COMMAND(ID_FILE_SAVEXMLAS, OnSECFileSaveXMLAs)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
15.2.2 SECXMLArchive
SECXMLArchive is the CArchive equivalent of MFC's binary serialization architecture. However,
unlike CArchive, SECXMLArchive is just an interface; the actual implementation is provided in the
SECXMLDOMArchive-derived class. SECXMLDOMArchive uses the XML DOM interfaces specified in the Microsoft XML SDK to interact with XML documents.
Chapter 15 XML Serialization Architecture 187
An SECXMLDOMArchive instance gets created and gets associated with an .xml file by the
SECXMLDocAdapter_T class. However, you would only deal with the base SECXMLArchive interface in your application. You can also create and initialize SECXMLDOMArchive yourself, if you
choose not to use the document adapter class in your application. Important interfaces in
SECXMLArchive are discussed in the following sections. These include:
пЃµ
Attributes
пЃµ
Insertion operations
пЃµ
Extraction operations
пЃµ
Serialize variant
пЃµ
Hierarchical nesting support
15.2.2.1Attributes
BOOL IsLoading();
BOOL IsStoring();
These public functions let you know whether the archive is in Storing or Loading mode.
15.2.2.2Insertion Operations
SECXMLArchive& Write(LPCTSTR tagName, long lVal);
This is one of the Write() overrides that allow you to insert primitive data types in your application as a child element node at the current node with the specified tagName in the XML document
hierarchy. There are similar overrides for other primitive data types and CString.
It is almost always the case that you will have other non-primitive types (either custom classes or
MFC classes) in your application. If you do, then you can associate such classes with an implementation of the IXMLSerialize interface—this implementation we call a formatter—and pass it on to
the archive class using the following override:
SECXMLArchive& Write(LPCTSTR contextTagName, IXMLSerialize* pFormatter);
Take a look at the IXMLSerialize interface and formatters discussion in Section 15.3 for more
information.
15.2.2.3Extraction Operations
The following Read() overrides closely reflect the Write() operations shown in Section 15.2.2.2
above.
// A Read override to read a string
BOOL Read(LPCTSTR tagName, LPTSTR& lpBuff, UINT& nLen,
BOOL bAssertOnFailure = FALSE, BOOL bTruncateOnOverflow = FALSE);
// Special Read override for reading child elements via their own formatters.
BOOL Read(LPCTSTR tagName, IXMLSerialize* pFormatter,
BOOL bAssertOnFailure = FALSE);
188
The return value indicates whether the specified tagName was found. If an element node with
tagName was not found as a child of the current node, then the supplied data type is left uninitialized and a FALSE is returned. However, if you call the function with bAssertOnFailure set to TRUE
(usually for critical elements) then Read() will ASSERT if the specified tagName is not found.
15.2.2.4Serialize Variant
If you can make a single call into the SECXMLArchive— regardless of the serialization context
(either loading or storing)— it makes your serialization code look simpler. That is just what the
Serialize() overloads provide.
// A Serialize override to serialize a string
BOOL Serialize(LPCTSTR tagName, LPTSTR& lpBuff, UINT& nLen,
BOOL bAssertOnFailure = FALSE,
BOOL bTruncateOnOverflow = FALSE);
The Serialize() function will in turn call Read() or Write()— based on the archive's current
context.
15.2.2.5Hierarchical nesting support
While serializing, at any layer you could create parent element nodes and insert your data structures as child elements to that parent node, using the following functions:
BOOL OpenElement(LPCTSTR tagName, BOOL bAssertOnFailure = FALSE);
void CloseElement(LPCTSTR tagName);
For example, consider the following code:
ar.OpenElement("Brush");
ar.Write("Color", m_lColor);
ar.Write("Thickness", m_nThickness);
ar.CloseElement("Brush");
The above code will result in an XML segment as shown below:
<Brush>
<Color>
255
</Color>
<Thickness>
10
</Thickness>
</Brush>
15.2.3 IXMLSerialize
You should have at least one implementation of the IXMLSerialize interface in your application, if
you want to participate in the XML serialization framework. This one implementation should be
associated with the class (usually CDocument) that contains the data in your application. This asso-
Chapter 15 XML Serialization Architecture 189
ciation is automatically set up for you when you derive your document class from the
SECXMLDocAdapter_T base, which in turn multiply inherits IXMLSerialize. You should then provide an implementation of the XMLSerialize virtual in your document class.
You could serialize all your application data from this single XMLSerialize override of your single
IXMLSerialize implementation or you could provide other IXMLSerialize implementations for
other non-primitive data types in your application and use the corresponding
Read()/Write()/Serialize() override to serialize those objects from within any other
XMLSerialize override.
Every IXMLSerialize implementation should also be associated with a tagName, which will be the
name for the corresponding element node in the resultant XML document.
For example, a Write() call with the following syntax will produce an XML document segment as
shown below.
ar.Write("AchildElement", MyObjectFTR(MyObject));
MyObjectFTR() is an implementation of IXMLSerialize with a tagName of say "MyObject." This
will result in the following XML segment under the current node:
<AchildElement>
<MyObject>
...
<!-- Other element nodes serialized from inside MyObjectFTR's
<!-- XMLSerialize override -->
</MyObject>
</AchildElement>
-->
We will be referring to an implementation of IXMLSerialize associated to an object as a formatter
for that object. Section 15.3 will discuss formatters.
190
15.3 XML Formatters
A formatter of an object is any implementation of IXMLSerialize that is meant to XML serialize
that object. Most of our formatters also can (optionally) dynamically create an instance of the associated class type while loading an XML document.
15.3.1 Built-in Formatters
You can use our built-in formatters for MFC collection classes and GDI objects in your own
applications.
15.3.1.1The XML Formatter Factory
The XML Formatter Factory is a behind-the-scenes global singleton used by the Stingray XML persistence framework to mimic the CObject-based polymorphic serialization provided by MFC. The
factory object is used by the formatters, provided with the SEC XML framework, for the MFC collection classes such as CObArray and CObList that have native support for serializing CObjectbased classes. The Formatter Factory is implemented by the SECXMLFormatterFactory class;
building the Stingray Foundation Library with the XML Serialization option selected in the SFL
Build Wizard will define a global instance of the factory object.
Formatters for the MFC CObject-derived classes that are a part of the serialization routine have to
be first registered with the factory before any of the XML persistence functions can be invoked.
During an XML file save operation, if a collection of CObject-derived classes is encountered, the
collection formatter will:
1. query the global factory instance for element type information— identifying the class along
with the associated formatter,
2. write the retrieved type information under the TYPE tag of the element node for the particular instance,
and then...
3. invoke the serialization routine on the class formatter.
This sequence is reversed during a file open operation. The collection formatter first reads the tag
descriptor for an element and then queries the factory for the formatter associated with this type.
Failing to register formatters with the factory will preclude this type look-up, resulting in a breakdown of the serialization attempt.
Formatter registration is performed using the set of XMLFORMATTERMAP() macros; the macros can be
placed within the formatters themselves or can be used within a higher-level class, such as the document, that possesses knowledge of the persisted types and performs a collective registration of all
the formatters.
Chapter 15 XML Serialization Architecture 191
Usage of the formatter map macros for registering formatter classes is shown below:
// Declaration of the XML formatter class for the SRGraphStyle
// CObject based class
class SRGraphStyleFTR : public IXMLSerialize
{
BEGIN_SEC_XMLFORMATTERMAP(SRGraphStyleFTR)
XMLFORMATTERMAP_ADDENTRY(SRGraphStyle, SRGraphStyleFTR)
END_SEC_XMLFORMATTERMAP()
…
…
virtual void XMLSerialize(SECXMLArchive& ar);
};
// Implementation of SRGraphStyleFTR
DEFINE_SEC_XMLFORMATTERMAP(SRGraphStyleFTR)
void SRGraphStyleFTR::XMLSerialize(SECXMLArchive& ar)
{
if(ar.IsStoring())
{
…
}
else
{
…
}
}
The XMLFORMATTERMAP series of macros aids only in the serialization of MFC collection classes—such as CObList and CObArray—that natively support persistence of CObject pointers. XMLFORMATTERMAP is not a
replacement for the SEC_XML_DYNCREATE_OBJECT macro, which provides a more generic creation mechanism.
Once the formatters and the accompanying registration macros are in place, the Formatter Factory
has to be initialized. This can be done as part of the application's start-up code in the
CWinApp::InitInstance() override.
// Initialize the XML formatter factory
SECXMLInitFTRFactory();
15.3.1.2Collection Class Formatters
The Stingray XML framework includes formatters for the commonly used MFC collection classes
(arrays, lists, maps, etc.). The collection class formatters observe all the semantics of any other XML
formatter and, therefore, their usage is virtually identical. These formatters implement the
IXMLSerialize interface and accept a reference to a pointer as a constructor argument. While serializing a collection class, within the XMLSerialize() override, create an equivalent formatter for the
collection and pass this to the SECXMLArchive::Read() or Write() function. The following code
snippet demonstrates using some of the simpler collection classes,
// m_dwArray is a CDWordArray
CDWordArray* pDWArray = &m_dwArray;
ar.Write("MyDWords", CDWordArrayFTR(pDWArray));
192
// m_strArray is a CStringArray
CStringArray* pStrArray = &m_strArray;
ar.Write("MyStrings", CStringArrayFTR(&pStrArray));
// m_strList is a CStringList
CStringList* pStringList = &m_strList;
ar.Read("MyStrings", CStringListFTR(pStringList));
Providing the formatters for collections of native types is straightforward. More complex collections— such as CPtrArray, CObArray, CPtrList, CObList, CList (template) and typed
collections— require formatters to be provided for the serialized objects as well.
You will have noticed, undoubtedly, that the Stingray XML framework’s built-in serialization support provided for the pointer collection classes is over and above what is provided in MFC. A
restriction here, however, is that the serialization support for pointers (as well as for the simple
template-based collections) is limited to one specific type. The polymorphic serialization behavior
exhibited by the CObject collections and their equivalent formatters is not available. The collection
formatter is updated directly by the underlying class formatter during template instantiation.
Usage of the pointer and simple template-based collections is shown in the code below,
// m_list is of type CList<CPoint, CPoint&>
CPointList* pList = &m_list;
ar.Read("MyCList", CListFTR<CPoint, CPoint&, CPointFTR>(pList));
// m_ptrList is of type CPtrList
CPtrList* ptrList = &m_ptrList;
ar.Read("MyCPtrList", CPtrListFTR<CSimpleObj, CSimpleObjFTR>(ptrList));
The XML formatter specification for serializable CObject-based classes is done through the Formatter Factory. As mentioned in Section 15.3.1.1, it is important that the registration macros be an
integral part of either the formatter or an encompassing class. Once all formatters have been registered with the Formatter Factory, using the CObject-based collection classes is straightforward and
much like using the native type collections. The following excerpt demonstrates this:
// m_obList is a CObList
CObList* pObList = &m_obList;
ar.Read("MyObList", CObListFTR(pObList));
// m_typedList is a CTypedPtrList<CObList, CMyObject*>
CMyTypedList* pTypedList = &m_typedList;
ar.Read("MyTypedList", CTypedPtrListFTR<CObList, CMyObject*>(pTypedList));
15.3.1.3Other MFC types Formatters
CBrushFTR, CPenFTR, CFontFTR, CRectFTR and CPointFTR are other built-in formatters for
some MFC classes. Their names imply their functions.
Chapter 15 XML Serialization Architecture 193
15.3.2 Creating Custom Formatters
To create a custom formatter, derive a class from IXMLSerialize or CXMLSerializeImp and override XMLSerialize. Our additional recommendations include:
пЃµ
Take a pointer reference to the associated object type in the constructor.
пЃµ
Provide a way for the user to specify the element tag name in the second argument.
пЃµ
Enable your formatter to dynamically create the underlying object while loading
using a single macro— SEC_XML_DYNCREATE_OBJECT()—as shown below:
class CMyObjFormatter : public CXMLSerializeImp
{
public:
CMyObjFormatter(CMyObject*& pObj, LPCTSTR strElementType =
_T("MyObject"))
: CXMLSerializeImp(strElementType), m_ptrObj(pObj){};
virtual void XMLSerialize(SECXMLArchive& ar);
SEC_XML_DYNCREATE_OBJECT(CMyObject)
};
15.3.3 XML Serialization Support in Objective Grid and
Objective Chart
Objective Grid and Objective Chart use this XML Serialization architecture to optionally serialize
their data in XML format. For more information, take a look at the User’s Guide for each of these
products.
194
15.4 Base64 and Quoted-Printable Encoding
Classes
15.4.1 Content Transfer Encoding
Internet User Agents, especially those subscribing to the Simple Mail Transfer Protocol (SMTP)—
such as mail programs and message transfer agents (MTAs), impose a restriction of 7-bit US-ASCII
characters and short line lengths on the transferred content. However, to be able to send a rich
body of content, such as multipart attachments, non-English messages, and binary data including
bitmaps and executables, it is necessary to have a standard that specifies an encoding scheme that
allows client programs to work with rich content.
The Multipurpose Internet Mail Extensions (MIME) specification defines such a format for encoding and decoding complex message bodies. Base64 and Quoted-Printable are two of the encoding
schemes defined by MIME that ensure that binary as well as non US-ASCII content will properly
pass through all MTAs and can be interpreted by all MIME compliant user agents. Base64 is a compact and efficient encoding scheme for binary data, while quoted-printable is a generally humanreadable scheme used for mostly text data. Line length in both cases is restricted to 76.
15.4.2 Base64
Base64 is a fully mapped encoding scheme for transmitting binary data. In base64 encoding, the
input data is sequenced into 24-bit groups— with each 6 bits of this group constituting an index
into a base64 alphabet table. This is referred to as 3-to-4 encoding. The characters used in the base64
mapping table are only those deemed safe for MIME.
15.4.3 Quoted-Printable
The quoted-printable scheme is used mostly for data composed of printable US-ASCII text. Userreadable input data composed of US-ASCII characters is, for the most part, retained as such in the
encoded format. Non-printable and other 8-bit characters will be represented as an equal sign followed by their hexadecimal values.
15.4.4 SFL Content-Transfer-Encoding Classes
The Stingray Foundation Library XML (SFLXML) update includes two components,
SECBase64Encoder and SECQPEncoder, that allow data to be encoded and decoded in the base64
and quoted-printable formats, respectively. These classes are built on tried and tested algorithms
and provide a convenient object-oriented wrapper that makes it very simple to work with data
streams as well as fixed-size buffers.
Chapter 15 XML Serialization Architecture 195
15.4.5 Class Hierarchy
15.4.5.1SECCTEBase
SECCTEBase serves as the base class from which both SECBase64Encoder and SECQPEncoder are
derived. This class defines the API for working with the encoding routines and also provides some
of the common functionality for the two schemes. Certain attributes of the encoded data—such as
line lengths, carriage-return line-feed sequences, and new-line terminator— can be set using the
accessor functions implemented by SECCTEBase. SECCTEBase is an abstract class and cannot be
instantiated.
15.4.5.2SECBase64Encoder
SECBase64Encoder derives from SECCTEBase and implements the encoding/decoding logic for
base64.
15.4.5.3SECQPEncoder
Similar to SECBase64Encoder, the SECQPEncoder class also derives from SECCTEBase but it
implements the quoted-printable encoding scheme.
To encode data in the base64 or quoted-printable formats or to decode existing encoded data, create
an instance of SECBase64Encoder or SECQPEncoder, as appropriate, and invoke their Encode() or
Decode() methods.
15.4.6 Usage
The encoder classes can be used in either streaming or non-streaming modes.
15.4.6.1Non-Streaming Mode
The non-streaming mode is the default and usage is straightforward. An instance of the encoder is
created and the Encode()/Decode() routine is called. If an output buffer is provided to the
Encode()/Decode() routine, the encoded/decoded data will be written to this buffer. Passing a
NULL output parameter, however, will instruct the encoder to maintain an internal buffer—in which
case the encoded data can be obtained by a subsequent call to EndEncode()/EndDecode().
The following code demonstrates a simple case:
// Default constructor creates a non-streaming encoder
SECBase64Encoder encoder;
char srcInput[] = "Try to encode me!";
encoder.Encode((BYTE*)srcInput, strlen(srcInput));
int nLen = encoder.GetOutBufferSize();
char* pEncode = new char[nLen];
encoder.EndEncode((BYTE*)pEncode, nLen);
196
// The output buffer can also be directly provided
char* pDecode = new char[25];
nLen = encoder.Decode((BYTE*)pEncode,nOutLen,(BYTE*)pDecode,25);
pDecode[nLen] = '\0';
// Terminate with a NULL
delete pEncode;
delete pDecode;
15.4.6.2Streaming Mode
In the streaming mode, an input stream is provided to the encoder by sequential calls to the
SECCTEBase::Encode()/Decode() routine. The encoded output is stored in the internal buffer
and can be obtained by a call to SECCTEBase::EndEncode()/EndDecode(). Passing a TRUE parameter to the constructor while creating an instance of the encoder will initialize the encoder for the
streaming mode.
Chapter 15 XML Serialization Architecture 197
15.5 XML Framework Tutorial
This tutorial will walk you through the steps required to add XML serialization support to an existing MFC Document-View application. (A starter application XMLTutorial is available by request
from [email protected]) All tutorial steps will be based on adding to and changing the
code in the XMLSerTut_Base application. You can follow along and make the changes to the code
as you go, or refer to the completed application provided in the same directory.
15.5.1 The starter application
XMLSerTut_Base was generated using the MFC application wizard. It is a very simple drawing
program. The user can select shapes from the toolbar and drop them onto the view window
Figure 19 – The XML Starter Application.
15.5.1.1The starter application classes
CXShape—The base class for the drawing shapes. This class contains an STL vector of POINT
structures.
CXTriangle, CXCircle, and CXRectangle— CXShape-derived classes used to render their respective shapes.
CXDiagram— Custom class that contains an array of CXShape objects.
CDesignDoc — The application's CDocument class. The document data consists of a title and a diagram (CXDiagram object).
198
15.5.2 Modifying application data classes
In our starter application the CDesignDoc, CXDiagram, and CXShape classes are all serializable
classes. Each object is responsible for serializing its internal data. For XML serialization, we will be
using helper classes called formatters to write each object's data as XML tags. For this to succeed
we need to ensure that the internal data is publicly accessible.
For example, the CXShape class member m_vecPoints is protected. To allow us to read the shape's
data, we can add two accessor methods to the class.
We can also add similar logic to the CXDiagram object to give us access to the title and array of
shape objects.
We do not need to add public accessors to our document class as we will not be using a formatter
class for the document. However, the internal data should be either protected or have protected
accessors since we will be creating a new class derived from the existing document class. Read
more on this in Section 15.5.4, “XML-enabling the document class.”
class CXShape : public CObject
{
...
public:
// Added public accessor to determine how many points in the vector
int GetNumPoints() const;
// Added public accessor to fetch each point from the vector (zero-based)
const POINT& GetPoint(int idx) const;
protected:
std::vector<POINT> m_vecPoints;
};
class CXDiagram : public CObject
{
...
public:
// Added public accessors for title and shape object array
CString GetTitle() const;
CTypedPtrArray<CObArray,CXShape*>* GetShapesArray();
protected:
CTypedPtrArray<CObArray,CXShape*> m_arrShapes;
CString m_strTitle;
};
15.5.3 Adding SFL XML Support
15.5.3.1stdafx.h
To link in the SFL libraries, we need to make some modifications to our project's stdafx.h (precompiled header file).
// SFL-XML
// XML framework requires ATL support
#include <atlbase.h>
CComModule _Module;
#include <atlcom.h>
Chapter 15 XML Serialization Architecture 199
// If you want to link statically to the SFL library
// remove the following line
#define _SFLDLL
// The main header for XML serialization support
// We can alternately use sflall.h
#include <foundation/xmlserialize.h>
15.5.3.2Resource includes
1. Open up the resource includes dialog via the View | Resource includes... menu option.
2. Add sflres.h to the read-only symbol directives.
3. Add sfl.rc at the bottom of the compile-time directives.
Figure 20 – Resource Includes Dialog
200
15.5.4 XML-enabling the document class
15.5.4.1Using the SECXMLDocAdapter_T wrapper class
The SFL library provides a very handy wrapper class, SECXMLDocAdapter_T, for adding XML
serialization to your existing CDocument class. To use this template, simply create a new class that
publicly inherits from the template. The template argument is your existing document class.
class CDesignDocXML : public sfl::SECXMLDocAdapter_T<CDesignDoc>
{
...
// Our required override of XMLSerialize
void XMLSerialize(sfl::SECXMLArchive &ar);
// This method is invoked by the framework to determine
// what the XML tag name will be for our document
virtual void GetElementType(LPTSTR str)
{
_tcscpy(str, _T("DiagramDocument"));
}
};
The sfl:: scope declaration is a typedef for the stingray::foundation namespace. You can just as easily add the directive
using namespace stingray::foundation; to your header files (or stdafx.h). However, we have chosen to use the sfl::
notation to clearly document where SFL framework classes are being used.
We must provide an implementation of the pure virtual SECXMLDocAdapter_T::XMLSerialize()
method. For now, we'll simply provide the boilerplate code.
void CDesignDocXML::XMLSerialize(sfl::SECXMLArchive &ar)
{
if(ar.IsStoring())
{
// Write out XML
}
else
{
// Read in XML
}
}
We provide an override of the GetElementType() method so that the XML framework will know
what to call the top-level tag in the XML document. Our resulting XML will look like this:
<?xml version="1.0" standalone="yes"?>
<DiagramDocument>
<!-- document data will be child nodes of this top-level node -->
</DiagramDocument>
Chapter 15 XML Serialization Architecture 201
15.5.4.2Modifying the base application
Now that we have a document class capable of participating in the SFL XML framework, we need
to make some small modifications to the application.
In the application object's ::InitInstance() we'll add logic to initialize the OLE libraries and the
SFL framework:
BOOL CXMLSerTutApp::InitInstance()
{
// SFL-XML
// Required SFL framework initialization
AfxOleInit();
sfl::SECXMLInitFTRFactory();
...
Change the application's CDocTemplate to use the new XML-enabled document class:
CMultiDocTemplate* pDocTemplate;
pDocTemplate = new CMultiDocTemplate(
IDR_XMLSERTYPE,
RUNTIME_CLASS(CDesignDocXML), // new XML-enabled doc class
RUNTIME_CLASS(CChildFrame),
RUNTIME_CLASS(CDesignView));
AddDocTemplate(pDocTemplate);
...
}
15.5.4.3Adding menu commands
We'll edit the menu resource to add some entries for loading and saving XML files. This is not absolutely required since the framework will actually parse the file extension when you load or save
your document. If the .xml extension is found, the XML framework will call your document's
XMLSerialize() override. Otherwise, your base class Serialize(CArchive& ar) method will be
invoked.
Figure 21 – Menu Commands
202
15.5.4.4Menu command handlers
To handle the menu commands, we make some MESSAGE_MAP entries in our new document
class (the one we created from the template and the original document class). You can choose any
id value you want for the menu commands, as they aren't predefined in the SFL headers. You do
not need to write any message handlers, because they have been provided by the template base
class.
BEGIN_MESSAGE_MAP(CDesignDocXML, CDocument)
//{{AFX_MSG_MAP(CDesignDocXML)
// Our custom menu commands mapped to the template
// base class handlers
ON_COMMAND(ID_FILE_OPENXML, OnSECFileOpenXML)
ON_COMMAND(ID_FILE_SAVEXML, OnSECFileSaveXML)
ON_COMMAND(ID_FILE_SAVEXMLAS, OnSECFileSaveXMLAs)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
15.5.5 Creating XML formatters
We added public accessors for serializable data to our application classes so that we can create
XML formatters to save our objects as XML. A formatter is a simple class that implements the
IXMLSerialize interface. The framework contains a base class, CXMLSerializeImp, which can be
used as the base class for our formatter classes.
Our formatter has to do three things:
пЃµ
Write class data as XML.
пЃµ
Read class data from XML.
пЃµ
Create a new instance of the class when reading XML.
We'll first concentrate on the first two requirements.
15.5.5.1The CXShape base class formatter
We will create a class hierarchy of formatters that parallels our CXShape class hierarchy. We derive
a class from CXMLSerializeImp and provide an override of the XMLSerialize() method.
class CXShapeFMT : public sfl::CXMLSerializeImp
{
public:
// All our CXShape derived classes do their serialization in
// the base class, so we only need one implmentation of
// XMLSerialize, and we can take a base class pointer
CXShapeFMT(CXShape* pShape, LPCTSTR strElementType = _T("Shape"))
: sfl::CXMLSerializeImp(strElementType),m_pShape(pShape)
{
}
virtual ~CXShapeFMT(){}
virtual void XMLSerialize(sfl::SECXMLArchive& ar);
Chapter 15 XML Serialization Architecture 203
protected:
// Pointer to the shape we're serializing
CXShape* m_pShape;
};
15.5.5.2Implementing CXShape::XMLSerialize()
To write out the shape data we need to do the following:
1. Write out the number of points.
2. Loop through our POINT vector and write each point's x and y value.
This is the same logic that exists in the standard CXShape::Serialize() method. We use the
SECXMLArchive::Read() and ::Write() methods to read and write XML tags. Our first call is to
read or write the point count, which will be read from and written to the <PointCount> XML tag.
This is the first method parameter.
To ensure that our points can be read back into the object in the correct order, we separate each
point as a unique XML child element. Here we have used the format PTxxxxxx to name the XML
tags, so that the first point is <PT000001>. Each PTxxxxxx will have two child elements, <XValue>
and <YValue>. Even though an STL collection is zero-based, we're using a 1-based naming convention for demonstration purposes.
void CXShapeFMT::XMLSerialize(sfl::SECXMLArchive &ar)
{
// Shared XML serialization routine for all CXShape classes
int nPointCount = 0;
CString strPointTag;
if (ar.IsLoading()) // Read from XML
{
// Read in the point count
ar.Read(_T("PointCount"),nPointCount);
// Read in each point from the XML document
for(int idx = 0; idx < nPointCount; idx++)
{
// Format the string to create our unique PTxxxxxx tag name
strPointTag.Format(_T("%s%06d"),_T("PT"),(idx+1));
// Open the point tag and read its X and Y values
ar.OpenElement(strPointTag);
POINT ptTemp;
//Add the point to the shape object's vector if successful read
if ((ar.Read(_T("XValue"),ptTemp.x)) && (ar.Read(_T("YValue"),ptTemp.y)))
m_pShape->AddPoint(ptTemp);
// Close the tag
ar.CloseElement(strPointTag);
}
}
else // Write to XML
{
// Write out the point count to the <PointCount> tag
nPointCount = m_pShape->GetNumPoints();
ar.Write(_T("PointCount"),nPointCount);
204
// Write out each point in the vector
for(int idx = 0; idx < nPointCount; idx++)
{
// Format the string to create our unique tag name
strPointTag.Format(_T("%s%06d"),_T("PT"),(idx+1));
// Open/Create the tag
ar.OpenElement(strPointTag);
// Write the X and Y values
const POINT& ptTemp = m_pShape->GetPoint(idx);
ar.Write(_T("XValue"),ptTemp.x);
ar.Write(_T("YValue"),ptTemp.y);
// Close the tag
ar.CloseElement(strPointTag);
}
}
}
15.5.5.3Creating formatters for derived CXShape classes
We've met our first two requirements for reading and writing our shape class data as XML. So how
do we satisfy the third? How can we create an instance of our object from simple XML text?
The SFL framework uses a set of macros that are similar to the MFC FOUNDATION_DECLARE_SERIAL
/ IMPLEMENT_SERIAL macros. These SFL macros define a lookup map that determines what XML
tag corresponds to your domain object classes.
To make our concrete shape classes creatable from XML, we derive three new classes that inherit
from the CXShapeFMT class we just created. We're only showing one class, but the procedure is the
same for all three.
Our concrete formatter class is doing two things:
1. Mapping a specific formatter to a specific class.
2. Describing what the class XML tag will be (via the constructor).
class CXCircleFMT : public CXShapeFMT
{
// Add our class to the XML serialization map
BEGIN_SEC_XMLFORMATTERMAP(CXCircleFMT)
// First parameter is our domain class,
// second is this formatter class
XMLFORMATTERMAP_ADDENTRY(CXCircle, CXCircleFMT)
END_SEC_XMLFORMATTERMAP()
public:
// The second parameter to the constructor determines what
// the name of the XML tag will be when writing this object.
CXCircleFMT(CXCircle* pShape, LPCTSTR strElementType = _T("Circle"))
: CXShapeFMT((CXShape*)pShape, strElementType)
{
}
virtual ~CXCircleFMT() {}
};
In our implementation (.cpp) file, we use another SFL macro to create an instance of the XML initilization information. The macros we put in the class header declare a static nested class, and we
initialize this static instance.
Chapter 15 XML Serialization Architecture 205
// Everywhere we have declared a BEGIN_SEC_XMLFORMATTERMAP in a
// formatter class requires a matching DEFINE_SEC_XMLFORMATTERMAP
DEFINE_SEC_XMLFORMATTERMAP(CXCircleFMT)
15.5.5.4The CXDiagram formatter
The final class for which we need a formatter is our CXDiagram class. The process of declaring the
class and initializing the lookup map is the same as it is for the shape classes.
class CXDiagramFMT : public sfl::CXMLSerializeImp
{
// MACROS for initializing the XML formatter map
BEGIN_SEC_XMLFORMATTERMAP(CXDiagramFMT)
XMLFORMATTERMAP_ADDENTRY(CXDiagram, CXDiagramFMT)
END_SEC_XMLFORMATTERMAP()
public:
CXDiagramFMT(CXDiagram* pDiagram, LPCTSTR strElementType = _T("Diagram"))
: sfl::CXMLSerializeImp(strElementType), m_pDiagram(pDiagram)
{
}
virtual ~CXDiagramFMT(){}
virtual void XMLSerialize(sfl::SECXMLArchive& ar);
protected:
// Pointer to the diagram we are serializing.
CXDiagram* m_pDiagram;
};
15.5.5.5Implementation of CXDiagramFMT::XMLSerialize()
Our diagram class needs to write out its title and list of shape objects. In the standard MFC serialization we can write the list of objects with one line of code since we are using a CTypedPtrArray,
which provides a Serialize() method.
The SFL framework provides a set of prebuilt formatter classes that can accomplish the same oneline serialization for MFC collection classes. This makes our implementation of XMLSerialize()
quite simple.
Here we will use the SFL-provided CTypedPtrArrayFTR formatter class. This is a template class.
The two template parameters are the same as the template parameters for the CTypedPtrArray
declared in the CXDiagram class.
When we call the SECXMLArchive::Read() method, we pass NULL as the first argument. For the
second parameter, we create an instance of the formatter inline. The first parameter to the constructor is a pointer to the diagram's array. The second parameter determines the name of the XML tag
representing the collection of objects.
The resulting XML will have this structure:
<Title>Untitled</Title>
<Shapes>
<!-- all the shape nodes here -->
</Shapes>
206
void CXDiagramFMT::XMLSerialize(sfl::SECXMLArchive &ar)
{
// We will serialize our title and then the list of child objects
CString strTitle;
CTypedPtrArray<CObArray,CXShape*>* pShapes =
m_pDiagram->GetShapesArray();
if (ar.IsLoading()) // Reading in from XML
{
ar.Read(_T("Title"),strTitle);
m_pDiagram->SetTitle(strTitle);
// Use the SFL- provided formatter to read the collection
ar.Read(NULL,sfl::CTypedPtrArrayFTR<CObArray,
CXShape*>(pShapes,_T("Shapes")));
}
else // Storing to XML
{
strTitle = m_pDiagram->GetTitle();
ar.Write(_T("Title"),strTitle);
// Use the SFL- provided formatter to write the collection
ar.Write(NULL,sfl::CTypedPtrArrayFTR<CObArray,
CXShape*>(pShapes,_T("Shapes")));
}
}
15.5.6 Finishing up
Now that we have all our formatter classes written, the only item left is to add serialization code to
our new document class.
void CDesignDocXML::XMLSerialize(sfl::SECXMLArchive &ar)
{
// Use our diagram formatter object to handle the XML
// serialization. This parrallels the logic in the standard
// DocView serialization
if(ar.IsStoring())
{
// Write out XML
ar.Write(NULL,CXDiagramFMT(&m_Diagram));
}
else
{
// Read in XML
ar.Read(NULL,CXDiagramFMT(&m_Diagram));
}
}
Chapter 15 XML Serialization Architecture 207
Our results: We can now run our application, create a new drawing, and save it as XML. If we've
done everything correctly, our XML will look like this:
<?xml version="1.0" standalone="yes"?>
<DiagramDocument>
<Diagram>
<Title>Untitled</Title>
<Shapes>
<Size>1</Size>
<Element0>
<Type>Circle</Type>
<Circle>
<PointCount>2</PointCount>
<PT000001>
<XValue>4</XValue>
<YValue>2</YValue>
</PT000001>
<PT000002>
<XValue>104</XValue>
<YValue>102</YValue>
</PT000002>
</Circle>
</Element0>
</Shapes>
</Diagram>
</DiagramDocument>
208
INDEX
Symbols
_Module 146
Numerics
3-to-4 encoding 195
A
Active Template Library 141
ActiveX controls 39
ActiveX property containers 39
adapter classes 53
AddChild()
CComposite 22
AddLayoutNode()
ILayoutNode 70
AddListener()
IEventRouter 47
AddObserver())
ISubject 20
AddRef()
IRefCount 17
IUnknown 13
AddRef())
CEventListenerBase 53
algorithms
layout 68
Application Classes 150
application, types of 171
AppWizard
Dialog-based
Application 171
Hello World Application 171
kinds of generated
applications 169
MDI Application 171
SDI Application 171
architecture 143
HelloSFL 143
ATL integration 48, 65–67
B
Base64 195
BeginPage()
IPrintable 118
border nodes 73
border-client layout 70
buffering 135
Build 9
Build Wizard 12
C
Cancel()
CPrintJob 122
CApp 144, 146, 150, 171
Init() 150
Run() 150
Term() 150
CArray 138
casting operator 135
CAxPropertyContainer 39
RegisterAxProperties() 39
CBorderEdge 73
CBorderGraphic 73
CBorderLayoutBase 73
CBrushFTR 193
CClientGraphicsContext 130
CClientWindow
flags 156
CClientWindowImpl 156
CColorDialog 161
m_cc 161
CComboRouterListener 57
CComInitializer 150, 152
CComModule 144, 146
Init() 148
Run() 148
Term() 148
CCommonControlsInitializer 150
, 152
CComposite
AddChild() 22
GetChildrenCount() 22
RemoveChild() 22
template class 22
CContainerDialogImpl 154
CContainerImplBase 153
CContainerWindowImpl 154
CCreateDialogMessageLoop 145
CCreateWindowMessageLoop 1
45
CDCLayoutBase 70
PrepareDC() 71
RestoreDC() 71
CDeviceGraphicsContext 130
CDWordArray 138
CEventFactory 45
CreateCommandEvent() 45
CreateWindowsEvent() 45
FilterCommandEvent() 45
FilterWindowsEvent() 45
CEventListenerBase 53, 54
AddRef() 53
HandleEvent() 53
QueryGuid() 53
Release() 53
CEventRouterMap 48
CFileDialogImpl 159
m_bOpenFileDialog 160
m_ofn 159
CFindDialog 161
CFontDialog 160
m_cf 160
CFontFTR 193
CFrameWindowImpl 147, 154
Create() 155
CGDIObject 126
CGraphicsContext 130, 131
chaining event routers 57
CHandleWrapper 127
character set conversion 134
classes
application 150
initializer 152
windowing 153
CLayoutManager 65
CLayoutNode 61
client windows 156
CList 138
CMap 138
CMapPtrToPtr 138
Index 209
CMapPtrToString 138
CMapPtrToWord 138
CMapStringToPtr 138
CMapWordToPtr 138
CMDIChildImpl 157
CMDIClientWindow 158
CMDIFrame 158
CMDIFrameImpl 158
usage 159
CMDIMessagePreTranslator 159
CMemoryGraphicsContext 131
CMessageLoop 145
CMessageLoopBase 144
CreateMainWindow() 144
DestroyMainWindow 144
CMessageMap 48
CMetafileGraphicsContext 131
CMFCEventRouter 49
CMTIApp 151, 171
RunTopLevelWindowInit() 1
51
CMvcAtlWndViewport 94
CMvcClientViewport 94, 156
CMvcCommand 103
CMvcController 96
CMvcModel 86
CMvcPersistableModel 181
CMvcPropertyBagPersistableMo
del 182
CMvcViewport 89
CMvcVisualComponent 81
CMvcVisualPart 81
CNoopInitializer 150, 152
CObjectFactory 25
CObjectFactoryBase 25
COleInitializer 150, 152
COM. See Component Object
Model, Microsoft
command dictionary, defined 107
common dialogs 159
compatibility
with MFC 132
compatibility classes 138
Component Object Model,
Microsoft 13
const_iterator, polymorphic
iterator 28
const_reverse_iterator
210 Index
polymorphic iterator 28
container windows 153
controller class, how to define 112
controller, MVC, defined 78
conversion constructors 134
conversion operators 134
coordinate mapping 82
COpenFileDialog 159
CPaintGraphicsContext 130
CPenFTR 193
CPoint 137
CPointFTR 193
CPrintDoc 119
GetOutputDC() 119
GetPrintDC() 119
CPrinterConfig 120
GetNumCopies() 120
GetOrientation() 120
GetPaperSize() 120
LoadPageSetupDlg( 120
LoadPrintDlg() 120
SetNumCopies() 120
SetOrientation() 120
SetPaperSize() 120
StorePageSetupDlg() 120
StorePrintDlg() 120
CPrintJob 122
Cancel() 122
Delete() 122
OnPrintDocument() 122
Pause() 122
Restart() 122
Resume() 122
SetPriority() 122
StartDoc() 122
CPrintPreviewFrameImpl 124
GetCurrentPrintable() 124
CPropertyContainer 35
accessor class 35
map 35
CPrtPreviewController 123
CPrtPreviewModel 123
CPrtPreviewViewport 123
CPtrArray 139
CPtrList 139
CreateCommandEvent()
CEventFactory 45
CreateWindowsEvent()
CEventFactory 45
CRect 137
CRectFTR 193
CRefCountImpl 17
CReplaceDialog 161
CSaveAsFileDialog 159
CSize 137
cstring typedef 136
CStringArray 138
CStringList 139
cstringstream typedef 136
CTypedPtrArray 138
CTypedPtrList 138
CTypedPtrMap 138
CUIntArray 138
CUIUpdateAdapter 163, 167
CUIUpdateGenerator 163
GenerateUIUpdates() 163
custom event types 58
custom formatters, creating 194
CWindow 147
CWindowAdapter 54
CWindowGraphicsContext 130
CWindowPaintEvent 44
CWinEvent 44
CWinEventBase 44
CWordArray 138
D
decorator design pattern 84
Delete()
CPrintJob 122
design patterns 19–31
composite pattern 22
defined 19
MVC 77–79
object factory pattern 25
polymorphic iteration 27
subject-observer pattern 20
visitor 44
device context 91, 130
creation and destruction 130
defined 130
MFC compatibility 132
DEVMODE structure 120
DEVNAMES structure 120
differences between CApp and
CMTIApp 151
Dispatch() 52
IEvent 44
dispatching events 52
distributing Objective Edit
applications 5
document objects 119
documentation
content 4
formats 4
DoModal() 160, 161
DPtoLP()
ILogCoordinates 82
Draw()
IVisual 80
dynamic_cast operator 13
dynamic_cast() operator 14
E
EndPage()
IPrintable 118
enhanced string 133
event classes, defined 44
event factory 45
event listeners 52
defined 43
efficiency 56
event routers
chaining 57
defined 43, 47
event routing 92
event types, creating your own 58
events
defined 44
Events package 43–58
F
FilterCommandEvent()
CEventFactory 45
FilterWindowsEvent()
CEventFactory 45
format string 135
formatter 188
frame windows 154
functors 107
G
GDI classes 125–132
GDI objects 126
creation and destruction 126
examples 128
examples of use 128
lifetime management 127
wrappers for 126
GetAxControl()
CAxPropertyContainer 39
GetChildrenCount()
CComposite 22
GetCurrentPrintable()
CPrintPreviewFrameImpl 12
4
GetDevMode()
CPrinterConfig 120
GetDevNames()
CPrinterConfig 120
GetExtents()
ILogCoordinates 82
GetLogExtents()
ILogCoordinates 82
GetLogOrigin()
ILogCoordinates 82
GetLogSize()
ILogCoordinates 83
GetNumCopies()
CPrinterConfig 120
GetOrientation()
CPrinterConfig 120
GetOrigin()
IBounds2D 83
GetOutputDC()
CPrintDoc 119
GetPageCount()
IPrintable 118
GetPaperSize()
CPrinterConfig 120
GetPrintDC()
CPrintDoc 119
GetPropertyByName()
with ActiveX containers 41
GetSize()
ISize2D 83
getting file name and path 160
graphics 125
gripper nodes 73
GUID maps 16
guid_cast template function 15
H
HandleEvent()
CEventListenerBase 53
IEventListener 52
HelloSFL 143
application 144
CMainFrame 147
main window 147
message loop 144
STDAFX.H 146
WinMain() 148
hierarchical nesting support 189
I
IBorderClientLayout 70
IBorderLayout 73
IBounds2D 80
IConstTraversableT 29
IEvent
Dispatch() 44
IEventListener
HandleEvent() 52
IEventRouter 47, 163
AddListener() 47
RemoveListener() 47
RouteEvent() 47
IEventRouterImpl 47
IIdleHandler 163, 167
ILayoutNode
AddLayoutNode() 70
ILogCoordinates 82
GetExtents() 82
GetLogExtents() 82
GetLogOrigin() 82
SetViewportExt() 82
SetWindowExt() 82
SetWindowOrg() 82
IMessage 20
defined 20
IMvcUndoRedo 103
Init()
CApp 150
initializer classes 152
integration
ATL 65
of layout manager 74
interface templates
IConstTraversableT 29
ITraversableT 29
interface-based programming 13
interfaces
CObjectFactoryBase 25
defined 13
IBorderClientLayout 70
IBorderLayout 73
Index 211
IEvent 44
IEventListener 52
IEventRouter 47
ILogCoordinates 82
IMessage 20
IMvcUndoRedo 103
IObserver 20
IPrintable 118
IProperty 33
IPropertyBag 176
IPropertyBag2 176
IPropertyContainer 34
IQueryGuid 14
IRelativeLayout 69
ISplitter 71
ISubject 20, 86
IUnknown 13
IVisual 80
IWindowListener 53, 54
IWinEvent 44
IZoom 93
traversable 29
Internet User Agents 195
InvalidateRect() 81
IObserver 20
OnUpdate() 21
IPersistenceStrategy
init() 176
IPrintable
BeginPage() 118
EndPage() 118
PrintPage() 118
IProperty 33
IPropertyBag 174
IPropertyBag2 174
IPropertyContainer 34
IQueryGuid
QueryGuid() 14
IRefCount
AddRef() 17
Release() 17
IRelativeLayout 69
ISize2D 80
ISplitter 71
SetDrawingStyle() 72
ISubject
AddObserver() 20
RemoveObserver() 20
UpdateAllObservers() 20
ISubject, defined 20
iterator
212 Index
polymorphic iterator 28
iterators
polymorphic 27
ITraversableT 29
IUnknown
AddRef() 13
QueryInterface() 13
Release() 13
IUnknown, defined 13
IVisual 80
IVisualHost 81
IXMLSerialize 189
L
layout algorithms
border nodes 73
border-client layout 70
DC layout nodes 70
gripper nodes 73
relative layout 68
scale layout 68
splitter layout 71
layout manager 153
architecture 61
nodes. See layout nodes 61
overview 59
realization 62
recalculation 62
layout map 63
layout nodes 61
classes 63
composites 61
creation of 63
initialization 63
primitives 61
reactive 61
license agreement 5
LoadPageSetupDlg()
CPrinterConfig 120
LoadPrintDlg()
CPrinterConfig 120
LPtoDP()
ILogCoordinates 82
M
MDI support 157
message cracking 46
message maps, efficiency 56
MFC
and ATL, SFL 141
MFC compatibility 132
MFC integration 49
MIME 195
model class, how to define 111
model, MVC, defined 78
Model-View-Controller architecture. See also MVC 78
Multiple Document Interface
applications 157
Multipurpose Internet Mail Extensions (MIME) 195
MVC 103
MVC commands
as messages 103
CMvcCommand 103
defined 103
IMvcUndoRedo 103
MvcTransactionModel 104
MVC controllers
CMvcController 96
defined 96
MvcController 99
MVC design pattern 78
bibliography 79
defined 77
relationships in triad 78
subject-observer pattern 79
MVC integration 181
MVC models
CMvsModel 86
defined 86
MFC specifics 88
presentation models 87
MVC package 123
MVC viewports
associating viewports 90
ATL specifics 94
CMvcViewport 89
defined 80, 89
device context 91
event routing 92
MFC specifics 94
scrolling 93
zooming 93
MVC, SFL implementation
principles 106–110
MVC, using in MFC applications
defining a controller class 112
defining a model class 111
defining a viewport class 114
MVC, visual components 80
CMvcLogicalPart 83
CMvcVisualComponent 81
CMvcVisualPart 81
coordinate mapping 82
interfaces 80
MFC specifics 85
wrappers 84
MvcBorderWrapper_T 84
MvcBufferedWrapper_T 95
MvcScrollView_T 95
MvcScrollWrapper_T 85
MvcTransactionModel 104
MvcViewport 95
MVCWrapper_T 84
N
non-streaming mode 196
notification message, defined 20
notification, defined 20
O
Objective Edit
license agreement 5
OnCmdMsg() 49
OnPaint() 52
OnPrepareDC()
IVisual 80
OnRestoreDC()
IVisual 80
OnUpdate()
IObserver 21
OnWndMsg() 49
output DC 119
P
PAGESETUPDLG 120
Pause()
CPrintJob 122
Persistence package 173–183
based on property bags 173
polymorphic iteration
polymorphic iterator
templates 28
traversable interfaces 29
traversable mix-in
templates 30
polymorphic iterator
templates 28
polymorphic iterators 27
kinds of 28
PrepareDC()
CDCLayoutBase 71
print DC 119
print jobs 122
Print package 117–124
defined 117
print preview 123
in ATL 124
printable objects 118
PRINTDLG 120
printer configurations 120
PrinterConfig
GetDevMode() 120
GetDevNames() 120
printers 121
PrintPage()
IPrintable 118
ProcessWindowMessage() 48
Properties package 33–41
ActiveX controls 39
containers 34
property bags, COM 173
property bags, SFL
examples 177
in C++ code 180
IPersistenceStrategy
interface 176
registry property bag 176
supported data types 175
two implementations 175
XML property bag 176
property containers
implementation 35
property map 35
PutPropertyValue()
with ActiveX containers 41
Q
QueryGuid()
CEventListenerBase 53
differs from
QueryInterface() 14
IQueryGuid 14
substitute for
dynamic_cast 14
QueryInterface()
IUnknown 13
Quoted-Printable 195
R
RecalcLayout() 62
recalculation process
realization 62
recalculation 62
reference counting 17
RegisterAxProperties()
CAxPropertyContainer 39
relative layout 68
Release()
CEventListenerBase 53
IRefCount 17
IUnknown 13
RemoveChild()
CComposite 22
RemoveListener
IEventRouter 47
RemoveObserver()
ISubject 20
resizing windows 60
Restart()
CPrintJob 122
RestoreDC()
CDCLayoutBase 71
Resume()
CPrintJob 122
reverse_iterator
polymorphic iterator 28
RouteEvent()
IEventRouter 47
Run()
CApp 150
S
samples
on Rogue Wave Web site 3
scale layout 68
scrolling 93
SEC_XML_DYNCREATE_OBJEC
T 192
SEC_XML_DYNCREATE_OBJEC
T() 194
SECBase64Encoder 195, 196
SECCTEBase 196
Decode() 197
Encode() 197
EndDecode() 197
EndEncode() 197
SECQPEncoder 195, 196
SECXMLArchive 187
Index 213
SECXMLDocAdapter_T 187
SECXMLFormatterFactory 191
Serialize() 189
SetDefaultPrinter() 120
SetMinMaxSize() 62
SetNumCopies()
CPrinterConfig 120
SetOrientation()
CPrinterConfig 120
SetPaperSize()
CPrinterConfig 120
SetPriority()
CPrintJob 122
SetViewportExt()
ILogCoordinates 82
SetWindowExt()
ILogCoordinates 82
SetWindowOrg()
ILogCoordinates 82
SFL
application classes 150
architecture 143
client windows 156
common dialogs 159
container windows 153
features and benefits 142
frame windows 154
MDI support 157
UI updating 163
windowing classes 153
SFL_PROPERTY_MAP 35
SFL. See Stingray Foundation Library
SFLXML 195
ShowProperties()
with ActiveX containers 40
splitter layout 71
StartDoc() 119
CPrintJob 122
Stingray Foundation Library
build configurations 9
building with nmake 10
building with Visual
Studio 10
Buld Wizard 12
cleaning after building 12
how to build 7
naming conventions 7
path configuration for Visual
Studio 9, 10
214 Index
Stingray Foundation Library
(SFL)
defined 1
design principles 1
major features 2
who should use 1
StorePageSetupDlg()
CPrinterConfig 120
StorePrintDlg()
CPrinterConfig 120
string class, enhanced 133
string typedef 136
stringstream typedef 136
structure wrappers 137
subject, defined 20
subject-observer pattern
in MVC 79
T
tagName 188
Term()
CApp 150
traversable 30
traversable interfaces
lifetime management 31
with MFC and COM
collections 31
traversable mix-in templates
const_traversable 30
traversable 30
tutorial, XML 198
typedef
cstring 136
cstringstream 136
string 136
stringstream 136
wstring 136
wstringstream 136
U
UIUpdating 163
UpdateAllObservers())
ISubject 20
user interface 163
mechanism 164
utility classes 133
V
ValidateRect() 81
view, MVC, defined 78
viewport class, how to define 114
viewports
associating with windows 90
defined 89
device context 91
visitor design pattern 44
W
windowing classes 153
Windows messages 44
windows, resizing 60
WinTraits 148
WM_CREATE 65
wrappers
decorator design pattern 84
for Win32 API structures 137
WS_CLIPCHILDREN 148
WS_CLIPSIBLINGS 148
WS_EX_APPWINDOW 148
WS_EX_WINDOWEDGE 148
WS_OVERLAPPEDWINDOW 1
48
wstring typedef 136
wstringstream typedef 136
X
XML Formatter Factory 191
XML formatters 191–194
XML Serialization
architecture 185
multi-level nesting, support
for 185
XMLFORMATTERMAP() 191
XMLSerialize() 185
Z
zooming 93
Index 215
216 Index
Index 217
218 Index
Index 219
220 Index
Index 221
222 Index
Was this manual useful for you? yes no
Thank you for your participation!

* Your assessment is very important for improving the work of artificial intelligence, which forms the content of this project

Download PDF

advertisement