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
© Copyright 2024