Visual C++ Tutorials

Visual C++ provides eight tutorials to help you begin learning new areas of the development system. Each tutorial is broken into steps, and complete code is supplied for each step. You can work through the tutorials step by step or simply build the code for a given step to see the results.
Tutorials Orientation Learn about the tutorials and how to work through them using the documentation and sample files.
Scribble: MDI Drawing Application Learn how to design and develop a multiple-document interface drawing program with the aid of AppWizard, WizardBar, the views, and the editors. Implement a dialog box, printing, and context-sensitive help.
Scribble: OLE Server Application Convert your Scribble drawing application to an OLE server by implementing in-place editing.
Container Application Learn about the MFC OLE classes and design an OLE container.
Autoclik: Automation Create and test an automation server application.
Circle: ActiveX Control Design an ActiveX control.
Enroll: Database Application Create a database application and learn to manipulate data using a form's controls and recordsets.
DAOEnrol: Database Application Learn to read a data source directly using MFC's DAO database classes.


Introduction

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

These tutorials introduce the experienced C/C++ programmer to the Microsoft® Visual C++® development system and the Microsoft Foundation Class (MFC) Library (including the MFC OLE, ActiveX, and database classes). The MFC Library is a set of C++ classes that encapsulate the functionality of applications written for the Microsoft Windows® family of operating systems.

The tutorials show you how to build sample applications, while demonstrating key concepts you’ll need to understand when developing your own Visual C++ applications. If you're new to Visual C++ and the development environment, the tutorials are a great hands-on way to become more familiar with it.

The tutorials demonstrate practical application of the MFC classes, including the ActiveX, OLE, and both Open Database Connectivity (ODBC) and Data Access Objects (DAO) database classes. See Overview of the Visual C++ Tutorials later in this introduction for a description of each tutorial.


Navigating Through the Visual C++ Tutorials

The tutorial lessons include two kinds of links: jumps to other parts of the online documentation set and jumps to topics in the tutorial lessons. The jumps to other parts of the online documentation are grouped into suggested reading lists in the lessons. These topics will introduce you to concepts that are necessary to perform the lesson. The jumps will take you to topics in one of the following information titles:

  • Visual C++ Programmer’s Guide Information on using Visual C++ and the MFC classes.

  • Microsoft Foundation Class Reference Reference information on the MFC classes and their member functions and data members.

  • Visual C++ User’s Guide Information about using the development environment, including the resource editors and wizards.

  • Win32 SDK Reference and conceptual information on the various application programming interfaces (APIs) included in the Win32 Software Development Kit.

If you click a jump going to one of these information titles and it isn't installed, you will be prompted to install it. Note that the jump identifies the name of the information title the topic is in. Jumps within the tutorial information title appear in the text of the lessons and do not list the name of the information title.

Sample Suggested Reading

  • Overview: MSDN Library Help, MSDN Library Help

  • Overview: User Interface Basics, Visual C++ User’s Guide


Overview of the Visual C++ Tutorials

Visual C++ includes the following tutorials:

Scribble

The Scribble Tutorial introduces the basics of creating MFC applications and demonstrates how to use the class library’s main features. Among the features covered in the Scribble tutorial are the following:

  • Using AppWizard to Create the Starter Application upon which to build your program.

  • Implementing a document class to manage your application’s data and to write to and read from files.

  • Implementing a “view” class to display your document and manage all user interaction with it.

  • Using the resource editors to create and edit your application’s resources.

  • Using WizardBar to connect user-interface objects, such as menu items, buttons, and accelerator keys, to handler functions in your source code.

  • Using the dialog editor to create a dialog resource, and ClassWizard to encapsulate the dialog resource in a dialog class. Also, using WizardBar to declare handler functions for a dialog box control, map the dialog box controls to member variables of the dialog class, and define validation rules.

  • Implementing scrolling and splitter windows; enhancing the default printing capabilities, including Print Preview; and adding context-sensitive Help to your application.

Scribble OLE Server

The OLE Server Tutorial is Scribble Step 7, which demonstrates adding OLE in-place editing server support to an existing application.

Container

Container, the OLE Container Tutorial, introduces the MFC OLE classes and teaches you to use their main features. Among the features this tutorial describes are the following:

  • Creating a starter OLE container by using AppWizard

  • Implementing the client item rectangle

  • Implementing hit testing and selection

  • Implementing activation with the mouse click

  • Implementing special cursors for resizing and moving objects

  • Drawing embedded objects

  • Deleting embedded objects

Autoclik

Autoclik, the Automation Server Tutorial, describes how to create and test a simple Automation server application, starting with AppWizard’s built-in support for Automation. Among the tutorial topics are:

  • Analyzing the dispatch interface name

  • Analyzing AppWizard-provided code

  • ODL file for creating a type library

  • Implementing properties of a dispatch interface

  • Implementing methods of a dispatch interface

  • Creating a new CCmdTarget class with a dispatch interface

  • Referring to one dispatch interface from another

Circle Control

The Circle Control Tutorial introduces the ActiveX control classes. Among the topics discussed are:

  • Building an ActiveX control

  • Implementing background painting behavior

  • Adding a custom property

  • Implementing Get/Set methods for a custom property

  • Adding special effects

  • Adding custom events

  • Handling text and fonts

  • Working with property pages

  • Simple data binding

  • Versions and serialization of ActiveX controls

Enroll and DAOEnrol

The database tutorials introduce the database classes and describe how to use their main features, demonstrating both ODBC and DAO support. Among the features these tutorials describe are:

  • Selecting and registering a database with ODBC, or using the DAO database classes to read the data source directly (if applicable).

  • Examining the classes AppWizard generates for your database application.

  • Using ClassWizard to bind controls on your form to the member variables of a “recordset” object. A recordset represents a set of records selected from a data source. The framework takes care of moving data between the controls and the data source.

  • Using a database form based on two or more recordsets. Each recordset represents a different table or query.

  • Controlling the data selected by a recordset. You can filter a recordset, parameterize it at run time, and sort it.

  • Adding, editing, and deleting records and implementing commands for these actions.


Developing an Application Using Visual C++

Tutorials Orientation, Lesson 1

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

This lesson describes how you’ll use the development environment to develop an application, and suggests topics in the Visual C++ documentation set for additional reading about the development environment and Visual C++.

Suggested Reading

  • Class Library Overview, Microsoft Foundation Class Reference

  • Creating an MFC program with AppWizard, Visual C++ Programmer’s Guide

  • Dialog Box Topics, Visual C++ Programmer’s Guide

  • Using ClassWizard, Visual C++ Programmer’s Guide

  • Using WizardBar, Visual C++ Programmer’s Guide


Creating a New Project

The stages in the development process are outlined here and briefly discussed within the context of Visual C++ and the development environment. They include:

  • Creating the Project

  • Selecting and Inserting Pre-Built Components

  • Adding and Editing User-Interface Objects

  • Connecting User-Interface Objects with Code

  • Editing Code

  • Compiling and Building Your Project

  • Debugging the Application

Each programmer may approach any given stage at a different time or in a different order; no preferred order is implied.

Usually, you can use AppWizard to generate a robust set of application “starter” files. Within AppWizard, you specify the structure you’d like your application to take, and the options and features you’d like AppWizard to provide.

Choosing a Project Type

You can use AppWizard to generate the following types of MFC projects:

  • Executable

  • DLL

  • ActiveX control

    The Circle Tutorial demonstrates building an ActiveX control.

  • Custom AppWizards

  • ISAPI extension

You can create many non-MFC project types:

  • Console applications

  • Static libraries (not available in the Standard Edition)

  • Projects based on external makefiles

Suggested Reading

  • Building Projects Not Created in the Development Environment, Visual C++ Programmer’s Guide

  • Working with Projects, Visual C++ User’s Guide

Choosing an Application Type

Once you’ve chosen your project type, you’ll choose the application type. AppWizard provides the following types to choose from, with MDI being selected as the default.

  • Single Document Interface (SDI)

    The Enroll Tutorial is an SDI-based application.

  • Multiple Document Interface (MDI)

    The Scribble Tutorial demonstrates creating a simple MDI application.

  • Dialog-based

Specifying Initial Application Features

After choosing a basic project and application type, your choices become more specific. AppWizard provides branching options you can use in various combinations to create the basis for sophisticated applications. For example, based on your specifications, AppWizard will generate MFC code for any of the following features:

  • Standard menus and dialog boxes for opening, editing, arranging and saving files, previewing print jobs, and printing.

  • ODBC and DAO support.

    The Enroll Tutorial demonstrates building a simple database application with ODBC support. The DaoEnrol Tutorial demonstrates the Enroll application using the DAO classes.

  • Support for OLE, including support for:
    • OLE compound documents, both server and container.

      Scribble Step 7 demonstrates adding OLE server support to Scribble. The OLE Container Tutorial demonstrates building an OLE container application.

    • OLE compound files.

    • Automation.

      The AutoClik Tutorial demonstrates building an Automation server.

    • ActiveX controls.

      The Circle Tutorial demonstrates building an ActiveX control.

  • A dockable toolbar, status bar, and 3D controls.

  • Windows Open System Architecture (WOSA) support, including the Messaging API (MAPI) and Windows sockets.

  • Support for context-sensitive Help, including a menu and toolbar button. In addition, AppWizard provides initial .RTF (help project source) files.

    Scribble Step 6 is a complete tour of providing context-sensitive Help for your Visual C++ applications.

Custom AppWizards

If the options presented by AppWizard aren’t as tailored to your needs as you’d like, you can design a custom AppWizard that generates a set of starter project files. With a custom AppWizard, you can specify the basis for the application according to your needs, your company’s particular data sources, and so forth. Every time you run this custom AppWizard, it generates the starter files according to those specifications.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Creating Custom AppWizards


Selecting and Inserting Pre-Built Components

The Component Gallery makes building powerful applications more straightforward than ever. By simply selecting from the many pre-built components and inserting them into your project, you can add fully developed features to your application, such as owner-draw controls, pop-up menus, ActiveX controls, and more. The Component Gallery also significantly increases code reuse in your application development process.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Gallery Topics


Adding and Editing User-Interface Objects

A large part of developing and refining your interface involves customizing user-interface objects, or Windows resources. The development environment provides a rich set of resources you’ll use to enhance the initial interface that AppWizard creates for you. By adding various resources to your project, and editing them in the resource editors, you can quickly implement your user-interface design.

The resource editors enable you to move easily between design mode and a run-time view of interface objects, such as menus, toolbars, and dialog boxes.

Suggested Reading in the Visual C++ User’s Guide

  • Overview: Resource Editors


Connecting User-Interface Objects with Code

The MFC code that AppWizard generates may be sufficient for a small number of your application’s user-interface objects, such as the splash screen, but for the most part you’ll have application-specific routines that you want called when an interface object receives input from the user. To do that, you need to make a connection between the interface object, the event, and the code you want called.

The visual tools provided by the development environment simplify the task of connecting user-interface objects to your application-specific code. If you’ve used Visual C++ before, you’re probably familiar with the ways that ClassWizard automates such tasks as generating message-handler functions and message maps. Visual C++ implements two shortcuts to the ClassWizard dialog box: ClassView and WizardBar. In addition to providing a seamless link between your project classes and files, these tools make navigating through your source code as easy as pointing and clicking.

  • ClassView

    ClassView displays your classes and their members in a hierarchical view, using visually intuitive icons. For instance, it displays private member functions with a “key” next to the function name. By simply clicking a class or any of its displayed members, you can jump directly to the associated declaration or definition in your project source file.

    ClassView provides a pop-up menu that you can use to directly add member functions and variables to a given class. (You’ll still use ClassWizard, or WizardBar, to add message-handling functions.)

    You can also use ClassView to:

    • Browse symbols

    • Display graphs of symbol relationships

    • Set breakpoints
  • WizardBar

    Like ClassView, WizardBar enables you to jump directly to a member function. Additionally, you can use WizardBar to override virtual functions and to create a handler for the Windows messages or commands a particular class can respond to.

The tutorials demonstrate how these features simplify and speed the process of specifying how your user-interface objects respond to messages.

Suggested Reading

  • ClassView: Overview, Visual C++ User’s Guide

  • ClassView Topics, Visual C++ Programmer’s Guide


Editing Code

You’ll continue to use the navigational support provided by ClassView and WizardBar as you edit your project source code. The integrated text editor provides full-featured editor support, including the following editor features:

  • Emulate another editor

  • Incremental searching

  • Automatic indentation and separate tabs and indent sizes

  • Full-screen editing mode

  • File navigation

Suggested Reading in the Visual C++ User’s Guide

  • Overview: Text editor

  • Navigating the Documentation System

  • Overview: User Interface Basics


Compiling and Building Your Project

You’ll compile and build at every step of project development to check that your code is correct and that your application behaves as you expect it to. The initial active project configuration builds your application with debugging information, so that you can easily debug any errors you discover. You can specify a different project configuration to be built by default.

The development environment provides several enhancements to the build process, including:

  • Building subprojects, such as DLLs

  • Specifying custom build settings for alternate file types, such as Help project (.HPJ) files

In addition, there are many ways to optimize the build process, including:

  • Minimal rebuild

  • Incremental compilation

  • Improved incremental linking

Suggested Reading

  • Working with Projects, Visual C++ User’s Guide

  • Build Commands in Visual C++, Visual C++ Programmer’s Guide

  • Compiling and Linking Home Page, Visual C++ Programmer’s Guide


Debugging the Application

Compiling and building leads to debugging. You may want to debug your application when a build failure occurs, or when you’ve modified your source code. You can run a program in the debugger, if you’ve built the debug project configuration. Or, you can enter Debug mode if your application fails in execution, without closing the application.

The integrated debugger adds features that make debugging a less troublesome process, including:

  • DataTips

  • Variables window

  • Stepping into a specific function

  • QuickWatch

  • New Watch window

  • TCP/IP remote debugging

Suggested Reading

  • Subtopics under Viewing and Modifying SQL Variables, Visual C++ Programmer’s Guide

  • Debugging Remote Applications, Visual C++ Programmer’s Guide

  • Home Page: The Debugger, Visual C++ User’s Guide


Scribble Tutorial

Scribble: MDI Drawing Application, Lesson 1

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

In this lesson, you’ll:

  • Install and preview the Scribble sample files

  • Preview the completed application

  • Preview the general steps for creating the Scribble application

  • Familiarize yourself with AppWizard-generated files

  • Set the project's build options

Suggested Reading

  • Overview: MSDN Library Help, MSDN Library Help

  • Overview: Project Workspaces, Visual C++ User’s Guide

Scribble, the application you build in this tutorial, is a small drawing program that poses a realistic trial run in the Windows programming environment of the traditional “Hello, World!” Scribble lets the user draw “Hello, World!” (or any free-hand drawing) by using the mouse and then save the image in a file.

By the end of the tutorial, Scribble has custom menus, Microsoft Messaging Application Programming Interface (MAPI) support, a dialog box with automatic initialization and validation, printing and print preview, scrolling, splitter windows, application Help support, and more: a fitting list of features for a Graphical User Interface (GUI) “Hello, World!” The development environment provides many of these features “for free,” and makes implementing the rest easy.

The following figure shows what Scribble looks like on the screen.

Scribble in Action


Installing the Sample Files

The completed sample source code for each tutorial step is included with Visual C++ so you can examine and compare this code to your own tutorial code. You do not need these files to complete the tutorial; they are provided for your convenience and information. To examine source code for Scribble, or any other tutorial sample, install the sample files that are included on your product CD.

Note There are also many samples included on the CD that are not covered in the tutorials. You can easily install the files needed to build these sample programs as well. See the Microsoft Foundation Class Library Samples Index for a list of these samples.

To install sample files

  1. From the MSDN Library browser, expand the following nodes:

    Visual C++ 6.0 \ Samples \ MFC Samples \ Tutorial Samples

    (Nontutorial samples are also located under the Samples \ MFC Samples category, organized according to topic.)

  2. Double-click the page icon for the application you are interested in, for example, Scribble.

  3. Click the tutorial step you want installed locally.

  4. In the Sample Application dialog box, click the Copy All button to install all files needed to build and run the sample application or tutorial step.

    You can also select and copy one or more individual files, or use the View button to examine a file before copying it.

  5. Specify the directory for the sample files, and click OK.

    When the sample files have been copied, you are returned to the Sample Application dialog box.

  6. Click Close.

A Note About Long Filenames

The names of the source code files on the distribution CD (and therefore in the online documentation) comply with ISO standards, which specify an 8.3 filename configuration. When you follow the instructions in any of the Visual C++ tutorials to generate your tutorial project files, AppWizard generates long filenames by default. If you use the project files from the distribution CD or from the online documentation (the sample source files) as your starting point to develop a particular tutorial step, watch for discrepancies between the filenames.

Scribble Step 7

For Scribble Step 7 (Creating an OLE Server), do not start from the sample source files; use your own Step 6 files as the starting point for Scribble Step 7, so your project filenames will remain compatible across these two steps.

If you do choose to use the sample source files, when the tutorial steps instruct you to change a filename, substitute the short filename in order to avoid naming conflicts. For instance, #include statements may refer to the short filename rather than the long filename AppWizard generates. For more information, refer to Using Short Filenames in the OLE Server tutorial.


Previewing the Sample Applications

You can use the online documentation to preview sample applications, including tutorial samples, in their executable state. For example, if you’re simply reading through the Scribble tutorial without adding code, you can still run Scribble at each step to see what it looks like and how it behaves. You do not need to install any sample files to preview sample applications.

To run a sample application from the source files, follow the steps described in Installing the Sample Files earlier in this lesson. At step 4, follow the instructions to run the sample application.


Scribble Tutorial Steps

The tutorial develops the Scribble application in seven steps. By installing the sample files, you can make a local copy of all project files in their representative state for each step. By default, these files are copied to subdirectories (represented as folder icons in your Project Workspace window) named STEP1 through STEP7. You can change the name and location of the subdirectories.

Before beginning the tutorial’s steps, you must create the starter AppWizard files for Scribble as explained in Lesson 2. Steps 1 through 6 describe how to develop the basic Scribble application. Step 7 describes how to convert Scribble to an OLE server.

Note A single tutorial step might be covered in more than one lesson. The following table correlates steps and lessons. Each lesson begins where you left off in the previous step.

Tutorial Steps

Step | Lesson
 | Creating a New Application with AppWizard. Create a starter AppWizard application, including MAPI support. (No corresponding tutorial step.)
1 | Creating Scribble’s Document. Create the Scribble document. (Start Step 1.)
 | Creating Scribble’s View. Create the Scribble view. (Complete Step 1.)
2 | Constructing Scribble’s User Interface. Create Scribble’s menus and toolbar; learn about graphics editors. (Start Step 2.)
 | Binding Scribble’s Visual Objects to Code Using WizardBar. Implement handlers for commands using ClassWizard, WizardBar, and ClassView. (Complete Step 2.)
3 | Adding a Dialog Box. Implement a dialog box using resource editors, ClassWizard, WizardBar, and ClassView. Add a spin control.
4 | Enhancing Views. Implement scrolling and splitting.
5 | Enhancing Printing. Implement printing and print preview.
6 | Adding Context-Sensitive Help. Learn how to add context-sensitive help for Scribble’s menus and dialog boxes.
7 | Creating an OLE Server. Convert your Scribble application to an OLE server.


The Files You Work With

Follow the steps described in Creating a New Application with AppWizard in Lesson 2 to create the starter Scribble application files. Even if you aren’t planning to add the tutorial code yourself, you can easily follow this procedure. It’s a good way to learn to use this tool.

Suggested Reading

  • ClassView: Overview, Visual C++ User’s Guide

  • ClassView Topics, Visual C++ Programmer’s Guide

  • Resource Editors, Visual C++ User’s Guide

  • Viewing Resources, Visual C++ User’s Guide

  • AppWizard, ClassWizard, and the Resource Editors, Visual C++ Programmer’s Guide

For most of the procedures that follow, you will need to deal with only a few of the files generated by AppWizard:

  • Document class files: ScribbleDoc.h (the header file) and ScribbleDoc.cpp (the implementation file)

  • View class files: ScribbleView.h and ScribbleView.cpp

You’ll also occasionally refer to or edit Scribble.h and Scribble.cpp, the application class files.

Tip Use ClassView to work with your code without explicitly opening the file. See Navigating Through Code in Lesson 2 for detailed instructions.

For tutorial lessons covering the resource editors, you will work in the ResourceView pane, which displays the contents of Scribble.rc, the application’s resource file (or files). Usually, you do not need to open the file Scribble.rc directly; rather, just manipulate the resources from the ResourceView pane.

You can examine the other files created by AppWizard and ClassWizard, but you probably won’t need to alter them. For more information about the starter application files that AppWizard creates, see the ReadMe.txt file, created by AppWizard, in your Scribble project root directory.


Scribble Build Information

This topic explains concepts you’ll need to know to build Scribble. General procedures for compiling and linking MFC programs in Visual C++ and running the executable program under Windows are introduced in Compiling and Building Your Project in the Tutorial Introduction.

As you progress though the tutorial, adding code as you read, you should occasionally build the version of the project that you’ve been developing. When you build your project, you should be in your Scribble project directory and have the project open. Setting up the project directory is described in the procedure, To create starter files for Scribble in Lesson 2.


Setting Build Options

When you create a new project, both Release and Debug build configurations are automatically created, although only one project configuration (the current default) is displayed in the Project Workspace window. Debug is set as the active project configuration. For Scribble, you shouldn’t need to change this default setting. However, for your own projects you’ll want to be able to build a Release version.

To change the default, display the Build toolbar. From the Default Configuration drop-down list on the Build toolbar, select the type of project you want to build. The project you select remains the active project until you change it again.


Lesson Review

In this lesson, you learned how to install the Scribble sample files, previewed the Scribble application, previewed Scribble’s steps, learned about AppWizard-generated files, learned about ClassView and ResourceView, and learned to select a build configuration.


Creating a New Application with AppWizard

Scribble: MDI Drawing Application, Lesson 2

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

In this lesson, you’ll learn about the steps and tools used in developing and building an application, starting with AppWizard-generated files.

Suggested Reading

  • Create an MFC EXE Program with AppWizard, Visual C++ Programmer’s Guide

  • Visual C++ Wizards that Help You Begin Your Program, Visual C++ Programmer’s Guide

  • Overview: Resource Editors, Visual C++ User’s Guide

  • Overview: Text Editor, Visual C++ User’s Guide

  • Overview: Project Workspaces, Visual C++ User’s Guide

  • Using WizardBar, Visual C++ Programmer’s Guide

  • Debugging Your Program, Visual C++ Programmer’s Guide

  • Using the Classes to Write Applications for Windows, Visual C++ Programmer’s Guide

  • The Frame-Window Classes Created by AppWizard, Visual C++ Programmer’s Guide

  • Document and View Classes Created by AppWizard, Visual C++ Programmer’s Guide

Once you’ve completed your initial application design, you’ll typically perform the following tasks to develop the application with the development environment and the Microsoft Foundation Class Library (MFC):

  • Use AppWizard to create a set of C++ starter files and associated Windows resources — a starter application that you can build and run immediately.

  • Use the resource editors to construct the objects that make up the user interface, such as menus and dialog boxes.

  • Use elements of the development environment to generate and edit application-specific code:
    • The text editor

    • ClassView

    • ClassWizard

    • WizardBar
  • Build, browse, test, and debug your project files — then add more code.

The steps tend to be iterative: You’ll go back and forth between editing the user interface and writing code all through the development process. You can also do the steps in a different order, depending on your working style.


Starting with Scribble

The starter files AppWizard creates contain starter code for several C++ classes:

  • An application class

  • A document class

  • A view class

  • A frame window class

Details about these files are available in a text file (ReadMe.txt) that is created along with the starter files.

Without adding a line of code, you can build the starter application you created with AppWizard and run the resulting program, which exhibits much of the standard functionality you expect from a program written for Windows. See Building the Starter Application and Running the Starter Application, later in this lesson.

If you’re working along, adding code as you read, follow all directions in this lesson. When you finish, you’ll have a full set of starter files in your own project directory. If you’re reading along without adding any code, work through this lesson to familiarize yourself with AppWizard.


Using AppWizard to Create the Starter Application for Scribble

In this topic, you will:

  • Use AppWizard to create the starter application that forms the beginnings of Scribble

  • View the starter files and classes using the Project Workspace

  • Compile and run your starter application

Suggested Reading in the Visual C++ User’s Guide

  • Overview: MSDN Library Help

  • Project Workspace Views

  • Reading the FileView and ClassView Panes

  • Overview: Resource Editors

Note The following procedure describes how to enter the correct values for Scribble. Many of the AppWizard dialog boxes contain choices that you won’t use to create the starter files for Scribble.

New Dialog Box

To create starter files for Scribble

  1. On the File menu, click New.

    The New dialog box appears.

  2. In the New dialog box, click the Projects tab. Select MFC AppWizard (exe) in the list box.

  3. Fill in or select the remaining boxes as follows:
    • Create new workspace option button: activated.

    • Project Name box: Scribble.

      AppWizard creates a project directory with this name under the main (root) directory specified in the Location box. The workspace configuration file and project makefile are based on this name, in this case, Scribble.dsw and Scribble.mak, respectively.

    • Location box: (Optional: the directory in which you want your project files stored. AppWizard creates this directory if it doesn’t exist).

    • Platforms check box: Win32.

      Clear any other entries. If your installation includes other language packages, they are represented by check boxes as potential platforms. Choosing check boxes other than Win32 might mean that options you need for Scribble are not available.

  4. Click OK.

    AppWizard creates the project directory, and the MFC AppWizard – Step 1 dialog box appears.

  5. Accept the default options in the dialog boxes for AppWizard Steps 1, 2, and 3 by clicking Next.

    By default, AppWizard generates source code that supports the multiple document interface (MDI), creating Scribble as an MDI application. Also by default, AppWizard enables ActiveX Controls support.

  6. In the MFC AppWizard – Step 4 dialog box, select the following:
    • Check the MAPI (Messaging API) support check box. MAPI support is available to users only if they have an electronic mail system.

    • Click Advanced.

      The Advanced Options dialog box appears with the Document Template Strings tab selected.

  7. Fill in Advanced Options dialog box as follows:
    • File extension box: scb

      This step is optional. AppWizard appends the extension you specify here to the names of files that the user saves with Scribble. If you do not specify an extension, users must specify their own file extensions when saving files.

    • Filter name box: Scribble Files (*.scb)

      This step is optional. In the compiled Scribble application, this string appears in the List Files of Type box in the Open and Save As dialog boxes of the File menu.

  8. Click Close.

    Default options in the Step 4 dialog box create code that supports the following features:

    • A dockable toolbar

    • A status bar

    • Printing and print preview

    • 3D controls

    The default options in the Step 5 dialog box supply comments throughout the files to indicate where you need to add your own code.

    The default settings in the Step 6 dialog box specify program file and class names. For the purposes of Scribble, you won’t modify class or filenames that AppWizard automatically generates.

  9. Click Finish in the dialog box for MFC AppWizard – Step 4.

    Note When you follow these steps, AppWizard generates filenames that differ slightly from the sample source code files. This difference will help you distinguish between your files and the samples.

    The New Project Information dialog box appears, summarizing the settings and features AppWizard will generate when it creates your project. You might want to take a moment to examine the application type, classes, and features that AppWizard automatically provides.

  10. Click OK in the New Project Information dialog box.

    AppWizard creates all necessary files and displays the project in ClassView in the Project Workspace window. See Viewing the Starter Classes later in this lesson.

To learn more about viewing the code generated for the project, see Navigating Through Code, later in this lesson.

Project Build Information

AppWizard creates both Debug and Release versions of each project it creates, selecting the Debug version as the default. The current active project configuration is displayed in the Default Configuration drop-list on the Build toolbar. You can switch the active project to specify which configuration you want to build. See Building the Starter Application later in this lesson.


Viewing the Starter Classes

In the Project Workspace, switch to ClassView and examine the classes that AppWizard defined in the starter application files:

  • CAboutDlg: the class used to create the “About Scribble” dialog box

  • CChildFrame: the class used to create the MDI child frame window (which contains the Scribble documents)

  • CMainFrame: the class used to create the MDI main frame window (which contains the child frame windows)

  • CScribbleApp: the main application object

  • CScribbleDoc: the document class

  • CScribbleView: the view class

By expanding each class icon, you can view the default member functions and variables AppWizard declared for you. In most cases, these are starter functions that you’ll fill in later.

To learn more about viewing the code generated for the project, see the next topic, Navigating Through Code. Take time to examine the source code files. To orient you, AppWizard creates a text file, ReadMe.txt, in your new project root directory. This file explains the contents and uses of the other new files created by AppWizard.

The topics Building the Starter Application and Running the Starter Application, later in this lesson, guide you through the process of compiling the starter application and running the resulting program to examine its capabilities.


Navigating Through Code

The Project Workspace has three views with which to examine and manipulate the contents of your project files:

  • Use ClassView to jump quickly into existing code. Without having to think about which file to open, you can jump to existing class definitions, member function declarations and definitions, and member variable definitions.

  • Use ResourceView to create, open, examine, and manipulate your project’s resources, including the accelerator table, cursor, dialog boxes, icons, menus, toolbars, and the string table.

  • Use FileView to open a file that you can’t jump to directly. FileView lets you edit a file without going through a menu.


Opening a Class with ClassView

To use ClassView to locate class definitions

  1. In ClassView, right-click the class name.

  2. In the pop-up menu, click Go To Definition.

    The associated header (.h) file for the class opens in the text editor, with the cursor placed at the beginning of the class definition.

To use ClassView to locate member function declarations

  1. In ClassView, expand the folder containing the member function declaration you want to view or edit.

  2. Right-click the member function name.

  3. In the pop-up menu, click Go To Declaration.

    The associated header (.h) file opens in the text editor with the cursor at the declaration for the function you chose.

To use ClassView to locate member function definitions

  1. In ClassView, expand the folder of the class containing the member function definition you want to view or edit.

  2. Right-click the member function name.

  3. In the pop-up menu, click Go To Definition.

    The associated implementation (.cpp) file opens in the text editor with the cursor at the definition for the function you chose.

To use ClassView to locate member variable definitions

  1. In ClassView, expand the folder for the class that contains the member variable you want to view or edit.

  2. Right-click the member variable name.

  3. In the pop-up menu, click Go To Definition.

    The associated implementation (.cpp) file opens in the text editor with the cursor placed at the appropriate line in your class definition.


Opening a File for Editing

Sometimes you will need to locate a place in one of Scribble’s files that you can’t automatically jump to by using ClassView or WizardBar. The FileView pane of the Project Workspace window provides a way to easily open your project files without using a menu command.

FileView displays the main project folder. By expanding this folder, you can view your project files. You can double-click any of these files to open them inside the text editor.

To open a file for editing

  1. In FileView, expand the top-level project folder, if necessary, to display the project folders.

    Implementation files (.cpp and .rc) can be found under the Source Files folder. Header files (.h) files can be found under the Header Files folder. Bitmaps and icons associated with your project can be found in the Resource Files folder.

  2. Double-click the file in which you want to work.

    The file opens in an editor window.


Using WizardBar

To view WizardBar, right-click the unoccupied area next to the toolbars to display the toolbar menu. Check WizardBar on the list of toolbars.

WizardBar

When the class associated with the implementation file is selected in the Class drop-down list, the Filter drop-down list displays Windows messages, and the Members drop-down list displays all of the member functions and data members for the selected class.

When you select a function without a handler, ClassWizard displays a message box that asks whether you want to create a handler for the function. If you respond Yes, ClassWizard creates a starter function with the correct parameters and syntax, highlighting the // TODO comment so you can replace it with your code.


Building the Starter Application

The starter application you created provides the beginnings of a working application for the Windows operating system. You can build either a Release or a Debug version, because the development environment generates project files and settings for both versions.

Suggested Reading in the Visual C++ Programmer’s Guide

  • File Types Created for Visual C++ Projects

Before compiling, select the type of project you want to build.

To select Debug or Release build options

  • From the Default Configuration drop-down list on the Build toolbar, select the type of project — either Win32 Debug or Win32 Release — you want to build.

    The project type you specify will remain the default until you change it again.

    The Build Toolbar

To build the starter application

  • From the Build menu, click Build scribble.exe.

    This builds the default version of the starter application, producing the file Scribble.exe in your project directory.

When you build the starter Scribble application from the files that AppWizard creates — without adding a single line of code — the result is an application that runs, opens and closes windows, and lets you perform other generic operations on the windows. Try running the application to see this basic functionality.


Running the Starter Application

After you build the starter application, you can run either a Debug or a Release version, so long as you have first built the version of the project you want to run. See the previous topic, Building the Starter Application.

To run Scribble

  • From the Build menu, click Execute Scribble.exe.

When the starter application runs, an MDI application window appears with a default toolbar and a menu bar that contains File, Edit, View, Window, and Help menus. The application window contains one open document window, as shown below.

The Starter Application

The document window is empty because you’ve added no application-specific code yet. But there is already a great deal of functionality built into Scribble. Try out your new application to see what works and what still needs to be implemented.

In this lesson, you’ve created, built, and run a starter application that lays the foundation for Scribble and displays much of the standard behavior you expect in an MDI application written for Windows.


Creating the Document

Scribble: MDI Drawing Application, Lesson 3

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

This lesson and the next, Creating the View, cover Step 1 of Scribble. Begin with the files you created with AppWizard in Using AppWizard to Create the Starter Application for Scribble in Lesson 2. You’ll be working in the following files: StdAfx.h, ScribbleDoc.h, and ScribbleDoc.cpp. As you read this lesson, add or change all lines of code as instructed in the procedures. At the end of Lesson 4, your files should essentially resemble Scribble Step1 source files.

Suggested Reading

  • CDocument Overview, Microsoft Foundation Class Reference

  • CPen Overview, Microsoft Foundation Class Reference

  • CObject::Serialize, Microsoft Foundation Class Reference

  • Document/View: Overview, Visual C++ Programmer’s Guide

  • Document/View Architecture Topics, Visual C++ Programmer’s Guide

  • Using the Classes to Write Applications for Windows, Visual C++ Programmer’s Guide

  • Overview: Text Editor, Visual C++ User’s Guide

  • Exception Handling Topics (MFC), Visual C++ Programmer’s Guide

In this lesson, you’ll develop:

  • Scribble’s data — CStroke, a class that defines one “stroke” of a drawing.

  • Scribble’s document — CScribbleDoc, an application-specific class derived from class CDocument, to contain and manage a list of strokes.

  • Scribble’s serialization code — code that implements writing and reading documents.

Lesson 4, Creating the View, introduces views and develops the view class. The two lessons together introduce many of the fundamental concepts of the application framework: documents, views, drawing, messages, and serialization (file loading and saving). Because documents and views are intimately related, you need to implement both before Scribble is fully functional.

In later lessons, you’ll add features to Scribble incrementally: more menus, a working toolbar, a dialog box with automatic initialization and validation of its controls, scrolling, splitter windows, enhanced printing, full application Help support, and OLE server support.

Note You can find a finished example of this lesson's code in the Scribble Step1 sample source code directory.


You and the Document

Scribble demonstrates the MFC programming model, which separates storage of the program's data (the document) from the display of that data (the view), and from most user interaction with the data. This model supports multiple views, multiple document types, splitter windows, and other valuable user-interface features. At the heart of document/view are four key classes:

  • CDocument (or COleDocument): an object used to store or control your program's data.

  • CView (or one of its many derived classes): an object used to display a document's data and manage user interaction with the data.

  • CFrameWnd (or one of its variations): an object that provides the frame around one or more views of a document.

  • CDocTemplate (or CSingleDocTemplate or CMultiDocTemplate): an object that coordinates one or more existing documents of a given type and manages creating the correct document, view, and frame window objects for that type.

To gain a better understanding of how the document/view model works for your application, study the following tables:

This table shows how the document and other objects are created and managed in a framework application.

Key Objects in an Application

Object | Primary purpose | Relationships to other objects
Application | Manages all other framework objects. | Keeps a list of document templates.
Document template | Creates and manages documents. | Manages a list of open documents of a given type. Creates frame windows and views to provide a user interface for a document’s data.
Document | Stores data. | Manages a list of views on its data.
View | Manages user interaction with a document. | Attached to a document. Owned by a frame window.
Frame window | Frames a view. | Owns a view that is attached to a document.

This table shows your responsibilities and those of the framework in implementing a document.

Document Implementation Responsibilities

Your job | The framework’s job
Derive a document class from class CDocument. | Provide many document services through class CDocument.
Add data members to your class. |
Implement application-specific initialization and cleanup of your document’s data. | Call the appropriate initialization and cleanup functions at the right times.
Override CDocument’s Serialize member function to specify how your data is read and written. | Provide implementations of File Open, Save, and Save As that call your Serialize override to read and write your data.

Typically, you also add member functions to your derived document class through which other objects — mainly the view — can access the document’s data.


Scribble's Document: Class CScribbleDoc

Scribble’s Document: Class CScribbleDoc

Documents in Scribble store the lines, or “strokes,” that make up a drawing. Because a drawing is typically made up of many strokes, the document stores a list of all the strokes the user has drawn. The following figure shows a single stroke drawn in a Scribble document’s view.

One Stroke in Scribble

Documents in Scribble are objects of class CScribbleDoc, which is derived from CDocument. CScribbleDoc has a member variable named m_strokeList for storing a list of strokes and member functions to manage the stroke list. You can view this member variable in the ClassView pane by expanding the icon for class CScribbleDoc.

Note By convention, class names begin with an uppercase “C” and member variable names begin with a lowercase “m_”.


Customizing CScribbleDoc

AppWizard writes a starter CScribbleDoc class for you, which you’ll customize in the following procedures. First you’ll add member variables and functions to CScribbleDoc, which provide some typical document functionality, such as:

  • Defining and manipulating the document’s data

  • Serializing the data to and from files

Then you’ll add class CStroke, a class used to define the document’s data structure.

Jumping Directly to Code

You’ll use the text editor to add code to the files AppWizard created for you. To display the code to edit, use either ClassView or WizardBar, which jumps directly to the code. See Navigating Through Code, in Lesson 2.


Adding Code to CScribbleDoc

In procedures that follow, you’ll add new member variables (m_nPenWidth and m_penCur) and functions (InitDocument and NewStroke) to CScribbleDoc.

Suggested Reading

  • ClassView: Overview, Visual C++ User’s Guide

  • ClassView Topics, Visual C++ Programmer’s Guide

  • Project Workspace Views, Visual C++ User’s Guide

  • Precompiled Header Compiler Options, Visual C++ Programmer’s Guide

  • Using WizardBar, Visual C++ Programmer’s Guide

  • CTypedPtrList Overview, Microsoft Foundation Class Reference

  • CArray Overview, Microsoft Foundation Class Reference

  • Diagnostic Features, Visual C++ Programmer’s Guide

Note The source files for tutorials such as Scribble contain comments that clarify the purpose of the code. Including such comments is not always mentioned as part of a tutorial step, both for brevity’s sake, and because the purpose of code you add is explained in the procedures you’ll follow. You can modify or add to comments in your own files.

To add member variables to Scribble’s document class

  1. In ClassView, expand the project folder, if necessary, and right-click CScribbleDoc. In the menu, click Add Member Variable.

    The Add Member Variable dialog box appears.

  2. In the Variable Type drop-box, type UINT.

  3. In the Variable Declaration box, type m_nPenWidth.

  4. In the Access area, click Protected, and click OK.

    ClassWizard adds the variable declaration to file ScribbleDoc.h.

  5. Repeat steps 1 through 4 to declare the protected m_penCur variable of type CPen.

    ClassView displays the new member declarations and variables you’ve added with your code.

  6. Expand the icon for CScribbleDoc.

    Note that m_nPenWidth and m_penCur are now displayed as variables belonging to the class. Once member functions and variables are declared, ClassView displays them, even if their definition isn’t written yet.

  7. Double-click m_nPenWidth.

    ScribbleDoc.h places the cursor at the definition for m_nPenWidth.

  8. Add the following lines of code in the Public attributes section of ScribbleDoc.h:
    CTypedPtrList<CObList, CStroke*> m_strokeList;

CPen*   GetCurrentPen( ) { return &m_penCur; }

    The first line is an instance of a template. The second is the definition of an inline function, so its definition appears here instead of in ScribbleDoc.cpp.

To add member functions to Scribble’s document class

  1. In ClassView, right-click CScribbleDoc.

  2. In the menu, click Add Member Function.

  3. Fill in the Add Member Function dialog box as follows:
    • In the Function Type box, type CStroke*.

    • In the Function Declaration box, type NewStroke().

    • In the Access area, click Public.

    • Click OK.

    A declaration is added to ScribbleDoc.h, while a starter definition is added to ScribbleDoc.cpp. You’ll fill in the definition later in this lesson.

  4. Repeat steps 1 through 3 to declare the function InitDocument. In the Function Type box, type void, and in the Access area, click Protected.

  5. Save the file. (Optional.)

    Note The development environment saves all your work for you automatically when you build your project, close the workspace, or exit the program. Any prompts within these procedures to save your files are provided only as extra safeguards of your work.

You can view the new functions using ClassView.


Adding the Template Collection Classes to Scribble

Scribble uses two of several C++ collection template classes provided by MFC: CTypedPtrList and CArray. You’ve just added the code that uses CTypedPtrList; you’ll add the code that uses CArray when you declare the CStroke class.

All the template collection classes are defined in the header file, AfxTempl.h. This MFC-provided header file will not change over the course of your development of the Scribble application, so add it to Scribble’s precompiled header, StdAfx.h.

StdAfx.h was created by AppWizard to keep the list of header files to be precompiled. It consists of a list of #include statements, followed by the names of the header files.

Note Precompiled header (PCH) files speed up build times because they don’t require recompiling.

To add AfxTempl.h to the precompiled header

  1. Switch to FileView and, if necessary, expand the Scribble project folder.

  2. Expand the Header Files folder, and open file StdAfx.h.

  3. At the end of the file, type:
    #include <afxtempl.h>    // MFC templates
  4. Save StdAfx.h.


Using WizardBar to Override Base-Class Functions

In this task, you will use WizardBar to override OnOpenDocument and DeleteContents, which are needed for Scribble-specific initialization and cleanup.

WizardBar

To display WizardBar

  1. Right-click the unoccupied area next to the toolbars.

  2. From the menu, click WizardBar.

To use WizardBar to override functions

  1. From the WizardBar Class combo box, select CScribbleDoc.

  2. Click the arrow on the action button, located on the right side of WizardBar.

  3. From the action menu, click Add Virtual Function.

    The New Virtual Override dialog box appears.

  4. From the New Virtual Functions box, select OnOpenDocument. Click Add Handler.

    ClassWizard creates a starter definition for the function, highlighting the code so you can simply begin typing to override it. (You won’t type anything at this point.)

  5. Repeat step 4 for DeleteContents.

  6. Click OK and save ScribbleDoc.h and ScribbleDoc.cpp.

ClassWizard writes the overridden functions to ScribbleDoc.h. Practice using ClassView to jump to and examine the declaration in ScribbleDoc.h.

When you created the starter application, AppWizard added to ScribbleDoc.h the declaration for OnNewDocument and for Serialize. Here is the starter function handler code that ClassWizard added to ScribbleDoc.cpp (do not add this code):

BOOL CScribbleDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
if (!CDocument::OnOpenDocument(lpszPathName))
   return FALSE;

// TODO: Add your specialized creation code here

return TRUE;
}

void CScribbleDoc::DeleteContents()
{
// TODO: Add your specialized code here and/or call the base class

CDocument::DeleteContents();
}

You will fill in the handler definitions for OnOpenDocument, OnNewDocument, and DeleteContents later, in Initializing and Cleaning Up.


Summary of CScribbleDoc Member Functions and Variables

The following table describes the current member variables of class CScribbleDoc and the purpose they will serve in Scribble.

CScribbleDoc Data Members

Member | Description
m_strokeList | A list of strokes. Each item in the list is an object of class CStroke. The list itself is a C++ class template based on the MFC template class, CTypedPtrArray.
m_penCur | A CPen object used to do the drawing. Its main attribute is its width. The pen is created when the document is constructed and is used during the creation of new strokes.
m_nPenWidth | The current width of the lines drawn by the pen.

The following table describes the member functions.

CScribbleDoc Member Functions

Member | Description
CScribbleDoc, ~CScribbleDoc | A default constructor and a virtual destructor. AppWizard creates placeholders for these functions. In Scribble, they remain empty.
DeleteContents | Deletes the contents of a document — the strokes that make up the drawing.
GetCurrentPen | Retrieves a pointer to the current pen object any time it’s needed by the drawing code.
InitDocument, OnNewDocument, OnOpenDocument | Called when a new document is created or an existing document is opened. The overridden versions of the CDocument member functions OnNewDocument and OnOpenDocument call InitDocument to initialize the new document.
NewStroke | Creates a new stroke object and adds it to the list of strokes in m_strokeList.
Serialize | Overrides the Serialize member function of class CDocument. The override specifies how to serialize a list of stroke objects to and from a disk file. AppWizard creates a starter version of this function for you.
AssertValid | Tests the validity of an object’s internal state.
Dump | Dumps the contents of an object’s members for examination during debugging.

You’ll add code for most of these member functions later in this lesson. You’ll learn more about Serialize under Serializing the Data, later in this lesson. You won’t add code to the functions AssertValid and Dump for Scribble.


The Document's Data: Class CStroke

The Document’s Data: Class CStroke

In Scribble, a stroke consists of an array of points. As the user drags the mouse to draw, Scribble collects points and stores them as part of the current stroke. Points collected from the time the left mouse button is pressed to the time it’s released form one stroke of a Scribble drawing. The figure below shows Scribble’s data structure schematically. Scribble uses an object of class CPen for drawing.

Scribble’s m_strokeList Data Structure

Each stroke is stored in an object of class CStroke, Scribble’s primary data structure, which, in the next topic, you’ll add as a new class in Scribble’s files (ScribbleDoc.h and ScribbleDoc.cpp). The whole drawing is a list of CStroke objects.


Adding the CStroke Class

In this topic, you will add the CStroke class declaration.

To add the CStroke class

  • Add the code shown below to ScribbleDoc.h and then save the file. The declaration for class CStroke must precede that for class CScribbleDoc, so it comes at the beginning of the file.
    /////////////////////////////////////////////////////
// class CStroke
// A stroke is a series of connected points in the Scribble drawing.
// A Scribble document may have multiple strokes.
class CStroke : public CObject
{
public:
CStroke( UINT nPenWidth );

protected:
CStroke( );
DECLARE_SERIAL( CStroke )

// Attributes
protected:
UINT   m_nPenWidth;  // One width applies to entire stroke
public:
CArray<CPoint, CPoint>   m_pointArray;  // Series of connected                                                    // points

// Operations
public:
BOOL DrawStroke( CDC* pDC );

public:
virtual void Serialize( CArchive& ar );
};

This code declares a C++ class of stroke objects. You can examine the new CStroke class and its current list of member functions and variables in ClassView. The next topic, CStroke Member Functions and Variables, describes their purpose.


CStroke Member Functions and Variables

The CStroke class member variables and functions will be used to define and manipulate the data of a stroke and serialize it when the document is serialized. You’ll add the member function definitions in the next topic, Building and Storing Strokes.

The following table lists CStroke’s member variables.

CStroke Data Members

Member | Description
m_nPenWidth | Stores the width of the pen in effect at the time this stroke was drawn.
m_pointArray | Stores an array containing the points that define this stroke. The points are used to redraw the stroke as needed.

The following table lists CStroke’s member functions.

CStroke Member Functions

Member | Description
CStroke | The class defines two constructors: one protected and one public.
DrawStroke | Draws each stroke. When the view object redraws the document’s data, it calls upon each stroke object in the stroke list to draw itself by calling its DrawStroke member function.
Serialize | Assists the document in making its data persistent, typically on disk. CStroke overrides the Serialize member function of CObject to define how a single stroke serializes its points and other data. For more information about point serialization, see Serializing the Data later in this lesson.


Building and Storing Strokes

To add implementation code for the CStroke members

  • Add the following definitions for CStroke’s two empty constructors to ScribbleDoc.cpp, after the last line of CScribbleDoc code:

    Tip Jump to ScribbleDoc.cpp by selecting CScribbleDoc from the Class combo box in WizardBar. Click the action button on the right side of WizardBar. The action button’s default opens the .cpp file and highlights the line for the displayed member function’s definition.

    ////////////////////////////
// CStroke
CStroke::CStroke()
{
// This empty constructor should be used by
// the application framework for serialization only
}

CStroke::CStroke(UINT nPenWidth)
{
m_nPenWidth = nPenWidth;
}

    The first constructor, declared protected in ScribbleDoc.h, is used only by the application framework during serialization of CStroke objects. Its parameter list and function body are empty. The second constructor is for public use, to construct new stroke objects directly. When it constructs a new stroke object, the public constructor initializes the pen width. CStroke doesn’t declare its own destructor — it relies on CObject to provide one by default.

At this point, class CStroke is not quite complete. You’ll add code for the remaining member function, DrawStroke, in Redrawing the View, later in Lesson 4.


Managing the Document

Typically, you must write code to:

  • Initialize a document’s data members

  • Deallocate memory allocated for the data, release system resources, and perform other cleanup chores.

When a new Scribble document is created, CScribbleDoc must create a pen for drawing new strokes. When a document is closed, the document must delete the stroke objects it has stored up. The next topic, Initializing and Cleaning Up, describes how to do this for Scribble.

Suggested Reading in the Microsoft Foundation Class Reference

  • CTypedPtrList::RemoveHead


Initializing and Cleaning Up

Because a document can be created with either the New command or the Open command on the File menu, CScribbleDoc overrides both the OnNewDocument and OnOpenDocument member functions of CDocument to perform necessary document initialization. However, for Scribble, both initializations are the same, so both overrides call the member function InitDocument.

The framework automatically calls OnNewDocument when a new document is created or OnOpenDocument when a document is opened. AppWizard creates a starter version of OnNewDocument for you, and ClassWizard created a starter version for OnOpenDocument.

In the following procedure, you’ll define these functions for Scribble.

To implement initialization for Scribble’s documents

  1. Use WizardBar to jump to the starter definition for InitDocument (in class CScribbleDoc) and fill it in with the following code:
    m_nPenWidth = 2;  // Default 2-pixel pen width
// Solid black pen
m_penCur.CreatePen( PS_SOLID, m_nPenWidth, RGB(0,0,0) );

    InitDocument sets a default pen width and creates a pen object for drawing. Pen creation is done through the CPen object, m_penCur, by calling its CreatePen member function. The arguments specify a solid black pen 2 pixels wide.

  2. Use WizardBar to jump to the override of OnNewDocument created by AppWizard and replace the //TODO comments with the call to InitDocument. The completed handler looks like this:
    BOOL CScribbleDoc::OnNewDocument()
{
if(!CDocument::OnNewDocument())
return FALSE;
InitDocument();
return TRUE;
}
  3. Finally, use WizardBar jump to the starter version of OnOpenDocument and replace the //TODO comments with another call to InitDocument. The completed handler looks like this:
    BOOL CScribbleDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
if( !CDocument::OnOpenDocument( lpszPathName ) )
return FALSE;
InitDocument( );
return TRUE;
}

    The overrides of OnNewDocument and OnOpenDocument call the base-class version of the function before performing application-specific initialization of the document.

The following procedure describes how to add the code that implements document cleanup for Scribble. You’ll replace the ClassWizard starter version of the DeleteContents member function of CDocument. Note that you are still working in ScribbleDoc.cpp.

To implement document cleanup

  • Use WizardBar to jump to the DeleteContents member function, and replace the //TODO comment with the following code:
    while( !m_strokeList.IsEmpty( ) )
{
delete m_strokeList.RemoveHead( );
}

DeleteContents provides the best place to destroy a document’s data when you want to keep the document object around. The function is called automatically by the framework any time it’s necessary to delete only the document’s contents. It’s called in response to the Close command on the File menu, when the user closes the document’s last open window, and before creating or opening a document with the New and Open commands. This is all part of the base-class functionality of DeleteContents.

Scribble’s override of DeleteContents iterates through the stroke list. DeleteContents calls the CTypedPtrList member function RemoveHead, which removes the list object and returns a pointer to the removed object. DeleteContents then calls the delete operator on this pointer, destroying the stroke object.

Alternatively, this cleanup code could be placed in the destructor, but DeleteContents is reused later in other functions.


Managing the Data

In the following procedure you’ll implement the NewStroke function that you declared earlier. NewStroke manages the stroke data in Scribble’s drawings.

To implement document members for managing Scribble’s data

  • Use WizardBar to jump to the starter definition of NewStroke (in class CScribbleDoc), and fill it in with the following code:
    CStroke* pStrokeItem = new CStroke(m_nPenWidth);
m_strokeList.AddTail( pStrokeItem );
SetModifiedFlag( );    // Mark document as modified
// to confirm File Close.
return pStrokeItem;

Note The new operator never returns NULL. Instead, an exception is thrown if memory could not be allocated. This would be a good place to implement an exception handler with the TRY and CATCH macros.

In the above code, NewStroke:

  1. Constructs a new CStroke object dynamically using the C++ new operator

  2. Initializes the CStroke object with the current pen width

  3. Adds the new stroke to the list using the CTypedPtrList member function AddTail

  4. Flags the document as having been modified since the last save by calling the CDocument member function SetModifiedFlag

  5. Returns a pointer to the stroke


Serializing the Data

The next three topics describe how to add the code that defines file input/output for Scribble documents. The default I/O implementation in MFC is called “serialization” (see the figure below). It provides a mechanism for making a document’s data persistent between work sessions with the program. By adding the code described in these topics, you enable the Scribble application to handle file serialization when the user clicks the Open, Save, or Save As commands from the File menu.

Suggested Reading

  • CArchive::GetObjectSchema, Microsoft Foundation Class Reference

  • /VERSION (Version Information), Visual C++ Programmer’s Guide

  • Overview: Version Information Editor, Visual C++ User’s Guide

Note Other than filling in the serialization function, you don’t have to write any code — for example, to display the dialog boxes — to process the Open, Save, and Save As commands on the File menu. The framework supplies this code.

Serialization in Scribble

The CScribbleDoc class declaration in ScribbleDoc.h begins with the following AppWizard-generated code, which contains an important macro invocation (DECLARE_DYNCREATE) needed for serialization (don’t add this code):

class CScribbleDoc : public CDocument
{
protected: // Create from serialization only.
   CScribbleDoc( );
   DECLARE_DYNCREATE( CScribbleDoc )
   // Other declarations ...
};

The DECLARE_DYNCREATE macro prepares the class so that document objects can be dynamically created by the framework.


Serializing the Document

Serializing a document occurs in two stages:

  1. The framework calls the document’s Serialize member function.

  2. That Serialize function calls the Serialize function of the stroke list.

In the following procedure, you’ll add code that implements serialization for Scribble’s documents.

To implement serialization for Scribble documents

  • Use WizardBar to jump to the Serialize member function in CScribbleDoc and add the following line just before the closing brace:
    m_strokeList.Serialize( ar );

    Later, you’ll add code to both branches of the if statement.

Serialization uses an object of class CArchive to manage the connection to a disk file or other storage. A CArchive object, ar, is passed in as an argument.

A call to the archive object’s IsStoring member function determines whether this is a store or a load operation. If the archive is for storing (saving), the stroke-list object’s own Serialize member function is called to store the stroke’s data to disk. If the archive is for loading, its Serialize member function is called to load data from the disk file. This constructs new CStroke objects to fill the list. The stroke list for a document being read in from disk must already be empty.

Note that the stroke list already exists when Serialize reads data in. That’s because you declared it as an embedded object, like this:

CTypedPtrList <CObList, CStroke*> m_strokeList;

For an embedded object, as in Scribble, you call Serialize directly because you don’t want to create a second CTypedPtrList object (as you would if you used a pointer), and because you know the exact type of the object.


Serializing Strokes

When the document responds to an Open, Save, or Save As command, it delegates the real serialization work to the strokes themselves. That is, the document tells the stroke list to serialize itself, and the stroke list, in turn, tells the individual strokes to serialize themselves. As a result, all strokes in the document are read from or written to a file.

Note Throughout the tutorial, Scribble is presented as a series of incremental versions. When you build successive versions that modify the structure of CStroke, they are incompatible with earlier versions. Attempts to read CStroke data stored by a previous version may fail because the serialization process expects a different structure. Each time you make such a modification of CStroke, tag the new version with a version number, as described in the procedure below. The version or “schema” number is checked automatically during serialization. You can check the schema number in the serialization code to support backward compatibility, allowing you to read files created with earlier versions of your application.

You’ve added serialization for Scribble documents; now you’ll implement serialization for the strokes.

To implement serialization for stroke objects

  1. Use WizardBar to jump to the CStroke constructor and, just before it, add the IMPLEMENT_SERIAL macro as shown:
    IMPLEMENT_SERIAL( CStroke, CObject, 1 )

    The third argument is the schema number, discussed earlier. It’s set to 1 for Scribble Step 1.

    The IMPLEMENT_SERIAL macro complements the DECLARE_SERIAL macro, which you declared in ScribbleDoc.h when you were adding the code for the CStroke class. The two macros prepare a class for serialization.

    Like CScribbleDoc, CStroke also overrides the Serialize member function of its base class. When the stroke-list object is called to serialize itself, it calls each stroke object in turn to serialize itself.

  2. Just after the second CStroke constructor (the one that initializes the pen), add the following Serialize override:
    void CStroke::Serialize( CArchive& ar )
{
if( ar.IsStoring( ) )
{
ar << (WORD)m_nPenWidth;
m_pointArray.Serialize( ar );
}
else
{
WORD w;
ar >> w;
m_nPenWidth = w;
m_pointArray.Serialize( ar );
}
}

    If the archive object is used for storing, the stroke’s pen-width value is stored in the archive and then its array of points is stored. Notice that the m_pointArray object as a CArray object can serialize itself.

    If the archive object is used for loading, the stroke’s data must be read in the same order it was written: first the pen width, then the array of points. The else branch of the if statement declares a local variable to receive the width, then copies that value to m_nPenWidth. It then calls upon the point array to load its data (see the figure Serialization in Scribble in Serializing the Data earlier in this lesson).

    Note that the m_nPenWidth variable is cast to a WORD before it’s inserted in the archive, ar:

    ar << (WORD)m_nPenWidth;

    The cast is necessary because m_nPenWidth is declared as type UINT (unsigned integer). The archive mechanism only supports saving types of fixed size. UINT, for example, is 16 bits in Windows 3.1 and 32 bits in Windows NT and Windows 95. Using the WORD cast makes the data files created by your application portable. To promote machine independence, class CArchive doesn’t have an extraction operator for type int but does have one for type WORD.

Once this code is in place, serialization of Scribble’s data is automatic.


Creating the Document: Summary

In this lesson you filled in the details of Scribble’s document class by defining its data, providing useful functions through which to manipulate the data, and specifying how the data objects are written to and read from files. So far, the data can be initialized and cleaned up but not displayed or worked on by the user.

At this point, Scribble is about half ready to build.


Creating the View

Scribble: MDI Drawing Application, Lesson 4

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

This lesson and the previous lesson, Creating the Document, cover Step 1 of Scribble.

Suggested Reading

  • ClassView: Overview, Visual C++ User’s Guide

  • ClassView Topics, Visual C++ Programmer’s Guide

  • Using WizardBar, Visual C++ Programmer’s Guide

Note You can find a finished example of this lesson's code in the Scribble Step1 sample source code directory.

In this lesson, you will add a view class that provides a “view on the document.” You will add code to Scribble’s view files, ScribbleView.h and ScribbleView.cpp. You will also add two more member functions to class CStroke in ScribbleDoc.cpp. Begin with the files from your Scribble root project directory.

At this point, your files should consist of the starter files you created with AppWizard in Creating a New Application with AppWizard and modified in Creating the Document. As you read this lesson, add all lines of code as instructed in the procedures. At the end of this lesson, your files should closely resemble the Step1 source code files, and your Scribble program should compile and run.

Scribble’s view class displays the strokes of a drawing and accepts user input from the mouse. Among the things you will develop in this lesson are:

  • Code to display Scribble’s strokes — in class CScribbleView.

  • Code to handle Windows messages as the user draws with the mouse — also in class CScribbleView.

While you’re adding code, you will continue to use ClassView and WizardBar to jump directly to your code.

You will also use WizardBar to:

  • Map Windows messages to message-handler member functions in your classes.

  • Map the commands generated by user-interface objects such as menu items, toolbar buttons, and accelerator keys to message-handler functions.


Views

The document object defines, stores, and manages the application’s data. All user interaction with the document is managed through a view object attached to the document object. Scribble uses a view object to display a document on the screen or on a printer. This topic introduces the role of the view and Scribble’s view class and members.

Suggested Reading

  • CView Overview, Microsoft Foundation Class Reference

  • CView::OnInitialUpdate, Microsoft Foundation Class Reference

  • CView::OnUpdate, Microsoft Foundation Class Reference

  • CView::OnDraw, Microsoft Foundation Class Reference

  • CView::GetDocument, Microsoft Foundation Class Reference

  • Document/View Overview, Visual C++ Programmer’s Guide

When a new document is created in response to a New or Open command from the File menu, the framework also creates a document frame window and creates a view inside the frame window’s client area as a child window. The view displays the document’s data and responds to mouse actions, keystrokes, menu commands, and other actions as the user works on the document. It is your task to specify how the view draws your application-specific data and what it does in response to user actions.

The figure below illustrates the view’s role in relation to the document.

The View and the Document

In the framework, the document manages data, but the view displays it and acts as intermediary between the user and the document for all input, selection, and editing in the document. A given view is always associated with only one document. However, it is possible for a document to have multiple views associated with it. In MDI applications such as Scribble, for example, the Window menu contains commands to open a new view onto a document, and to arrange open documents. In Scribble, you’ll also add splitter window functionality, enabling the user to create a second view onto a document without opening a new window.

The following table shows your responsibilities and those of the framework in implementing a view on a document.

View Implementation Responsibilities

Your job | The framework’s job
Derive a view class from class CView. For scrolling, use CScrollView instead. Other view classes are available as well. | AppWizard provides a starter view class for you. Class CView and its derived classes provide view services.
Implement your view’s OnDraw member function. | The framework calls OnDraw at the appropriate times, passing it a device-context object into which it can draw.
Map Windows messages and commands to member functions of your view. | The framework calls your message-handler member functions in response to the corresponding Windows messages.


Scribble's View: Class CScribbleView

Scribble’s View: Class CScribbleView

The job of the view in Scribble is to redraw the view as needed — for example, when the window is covered by another window and then uncovered, or as the user draws strokes with the mouse.

Suggested Reading

  • CObject Class Topics, Visual C++ Programmer’s Guide

Views in Scribble are objects of class CScribbleView, which is derived from class CView. CScribbleView knows how to access the document’s stroke list and can tell the strokes stored there to draw themselves in the view.

You can view a graphical representation of CScribbleView and its member functions in the ClassView pane of the Project Workspace window.

The following table describes the member functions of class CScribbleView that AppWizard created for you.

CScribbleView Member Functions

Member | Description
CScribbleView, ~CScribbleView | With nothing to initialize and no data to destroy, the view’s constructor and destructor are empty.
OnDraw | OnDraw updates the view by redrawing its contents. (It’s used to draw both on the screen and on a printer.)
GetDocument | Defined inline in file ScribbleView.h, GetDocument retrieves a type-safe pointer to the document attached to this view. The view uses the pointer to call document member functions, which it must do to access the data it displays.
AssertValid, Dump | These diagnostic functions simply call the base-class functions they override.
OnPreparePrinting, OnBeginPrinting, OnEndPrinting | These virtual functions override the versions in CView to specify the application’s printing behavior. See Lesson 9, Enhancing Printing, for more information about how Scribble prints.

You won’t need to alter any of the following AppWizard-created functions in this lesson: GetDocument, AssertValid, Dump, OnPreparePrinting, OnBeginPrinting, and OnEndPrinting.

Notice the inline definition of GetDocument in the above table. The debug version of this member function calls the IsKindOf member function defined in class CObject and uses the RUNTIME_CLASS macro to retrieve the run-time class name of the document.


Defining the Working Data Used by the View

In the following procedure, you’ll add two member variables to class CScribbleView that store information about a stroke in progress.

Suggested Reading

  • ClassView: Overview, Visual C++ User’s Guide

  • ClassView Topics, Visual C++ Programmer’s Guide

  • Using WizardBar, Visual C++ User’s Guide

  • CPoint Overview, Microsoft Foundation Class Reference

  • CDC Overview, Microsoft Foundation Class Reference

  • CPen Overview, Microsoft Foundation Class Reference

To declare the new member variables

  1. In ClassView, double-click the icon for the CScribbleView class.

    This jumps you directly to the class definition (generated for you by AppWizard) in file ScribbleView.h.

  2. Add the following code right after the public Attributes section:
    protected:
CStroke*   m_pStrokeCur;    // The stroke in progress
CPoint     m_ptPrev;    // The last mouse pt in the stroke
// in progress

The New CScribbleView Member Variables

The code you just added declares two new protected member variables inside class CScribbleView — m_pStrokeCur and m_ptPrev.

You can view the new member variables in the ClassView pane of the Project Workspace window by saving the file and expanding the CScribbleView class icon.

The following table describes the new member variables.

CScribbleView Data Members

Member | Description
m_pStrokeCur | A pointer to the stroke currently being drawn.
m_ptPrev | A CPoint object containing the previous mouse coordinates, from which a line will be drawn to the current coordinates.

The view uses these members to store the information it needs in order to record the points of a stroke in progress.


Redrawing the View

When the view, or some part of it, must be redrawn, the framework calls the override of the OnDraw member function that AppWizard generated. In this topic you’ll add Scribble-specific code for OnDraw to the ScribbleView.cpp file.

To add implementation code for the view’s OnDraw member function

  1. Use WizardBar to jump to the starter OnDraw member function of class CScribbleView.

  2. Replace the //TODO comments, after the ASSERT_VALID(pDoc) line, with the following code:
    // The view delegates the drawing of individual strokes to
// CStroke::DrawStroke( ).
CTypedPtrList<CObList, CStroke*>& strokeList =
pDoc->m_strokeList;
POSITION pos = strokeList.GetHeadPosition( );
while (pos != NULL)
{
CStroke* pStroke = strokeList.GetNext(pos);
pStroke->DrawStroke( pDC );
}

The view uses the pointer to iterate through the stroke list, telling each stroke to draw itself. When OnDraw calls DrawStroke for a given stroke object, it passes along the device-context object it received as a parameter. (Having the data draw itself is only one possible strategy.)

To complete Scribble’s drawing, you must also add the DrawStroke member function definition to class CStroke.

To add drawing code for strokes

  • Add the DrawStroke member function definition to ScribbleDoc.cpp as shown, right after the Serialize function. (Recall that you added its declaration when you added the CStroke class to ScribbleDoc.h.)
    BOOL CStroke::DrawStroke( CDC* pDC )
{
CPen penStroke;
if( !penStroke.CreatePen(PS_SOLID, m_nPenWidth, RGB(0,0,0)))
return FALSE;
CPen* pOldPen = pDC->SelectObject( &penStroke );
pDC->MoveTo( m_pointArray[0] );
for( int i=1; i < m_pointArray.GetSize(); i++ )
{
pDC->LineTo( m_pointArray[i] );
}
pDC->SelectObject( pOldPen );
return TRUE;
}

This code passes DrawStroke a pointer to an object of class CDC, which encapsulates a Windows device context (DC).

Note In programs written with MFC, all graphics calls are made through a device-context object of class CDC or one of its derived classes.

DrawStroke:

  1. Calls CDC member functions — SelectObject, MoveTo, LineTo — through the pointer to select a graphic device interface (GDI) pen into the device context and to move the pen and draw.

  2. Constructs a new CPen object and initializes it with the current properties by calling the pen’s CreatePen member function.

    Note This two-stage construction is typical of framework objects.

  3. Calls SelectObject to select the pen into the device context (saving the existing pen as pOldPen).

  4. Calls MoveTo to position the pen to the first point.

  5. Iterates through the array of points.

  6. Calls the device context’s LineTo member function to connect the previous point with the next point.

  7. Restores the device context to its previous condition by reinstalling its old pen.

Important Always restore the device context to its original state before releasing it to Windows. To do so, save the state before you change it. Storing the old pen in DrawStroke is an example of how to do this.

The addition of DrawStroke completes Scribble’s code for drawing in response to update requests from the framework. However, Scribble also draws in response to mouse actions, as discussed in the next topic, Handling Windows Messages in the View.


Handling Windows Messages in the View

To implement mouse-driven drawing in Scribble, you must add code that handles several Windows messages related to mouse activity. You will use WizardBar to help add this message-handling code.

Suggested Reading

  • WM_LBUTTONDOWN, Win32 SDK Reference

  • WM_LBUTTONUP, Win32 SDK Reference

  • WM_MOUSEMOVE, Win32 SDK Reference

  • GetCapture, Win32 SDK Reference

  • CWnd::OnLButtonDown, Microsoft Foundation Class Reference

  • CWnd::OnLButtonUp, Microsoft Foundation Class Reference

  • CWnd::OnMouseMove, Microsoft Foundation Class Reference

  • CView Overview, Microsoft Foundation Class Reference

  • CClientDC Overview, Microsoft Foundation Class Reference

  • Declaring Message Handler Functions, Visual C++ Programmer’s Guide

  • Overview: Text Editor, Visual C++ User’s Guide

In Scribble, Windows sends a user action message to a window, in this case the currently active view. The view uses its message map to determine whether it has a member function that can handle the message. If it does, the view calls the handler.

The following table correlates the user action to the message sent by Windows to Scribble’s view and the corresponding member function that handles the message:

User | Message sent from Windows to the window | Handler
Press left mouse button | WM_LBUTTONDOWN | CScribbleView::OnLButtonDown
Move mouse (whether or not mouse button is pressed) | WM_MOUSEMOVE | CScribbleView::OnMouseMove
Release left mouse button | WM_LBUTTONUP | CScribbleView::OnLButtonUp

The view handles mouse-drawing messages because it’s in the view that Scribble’s drawing takes place. The view represents that part of the document that can be seen at any one time.

The message handlers track mouse activity and draw in the view accordingly. They also call member functions of the document to update its data. As the user draws a stroke, the points that make up the stroke are stored in the document’s stroke list.


Connecting Messages to Code

In this topic, instead of directly adding lines of code to a file in the text editor, you’ll first use WizardBar to make connections between Windows messages and their handler functions. ClassWizard adds an entry to the message map in ScribbleView.cpp for class CScribbleView and writes a default member function definition to the same file for the handler function. Then, as described in the next topic, Adding the Message-Handler Functions, you use WizardBar to jump directly to the starter member function, and you fill in the function’s code.

To connect the messages to Scribble’s code

  1. Using WizardBar, from the Class combo box, select CScribbleView. Click the arrow on the action button on the right end of WizardBar. Click Add Windows Message Handler.

  2. In the New Windows Message Handler dialog box, select WM_LBUTTONDOWN from the New Windows Messages to Handle list. Click Add Handler.

    You won’t fill in the handler just yet.

  3. Repeat steps 2 and 3 for the additional mouse messages: WM_LBUTTONUP and WM_MOUSEMOVE.

  4. Click OK.

After you click Add Handler, ClassWizard does the following tasks to associate each of the three messages with its handler and to greatly simplify your work:

  • Adds the following function declarations for the handlers to the CScribbleView class declaration in file ScribbleView.h:
       afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
   afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
   afx_msg void OnMouseMove(UINT nFlags, CPoint point);
  • Adds the following message-map entries for the message-to-handler connections in CScribbleView’s message map in file ScribbleView.cpp:
    ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONUP()
ON_WM_MOUSEMOVE()
  • Adds the appropriate function definitions (with a default body) to file ScribbleView.cpp. For example, for OnLButtonDown:
    void CScribbleView::OnLButtonDown( UINT nFlags, CPoint point )
{
    // TODO: Add your message handler code here
    // and/or call default
    CView::OnLButtonDown( nFlags, point );
}

    Notice that ClassWizard embeds a comment reminding you what to do and adds a call to the CView::OnLButtonDown.


Adding the Message-Handler Functions

With the connections made, you can fill in the bodies of the handler functions.

The following figure shows OnLButtonDown in the editor.

Starter definition of OnLButtonDown in the Text Editor

In the next few topics, you’ll fill in member functions to:

  • Initiate stroke drawing

  • Terminate stroke drawing

  • Draw while the mouse button is down


Initiate Stroke Drawing

The OnLButtonDown member function is called via the message map when Windows sends a WM_LBUTTONDOWN message to the view object. The function begins a new stroke, adding the current location of the mouse to the stroke and adding the stroke to the document’s stroke list. Then OnLButtonDown captures the mouse — until the left mouse button is released to end the stroke.

To add code for OnLButtonDown

  1. Using WizardBar, jump to CScribbleView’s member function OnLButtonDown in ScribbleView.cpp.

  2. Replace the //TODO comments and code with the code shown here:
    // Pressing the mouse button in the view window
// starts a new stroke.

m_pStrokeCur = GetDocument( )->NewStroke( );
// Add first point to the new stroke
m_pStrokeCur->m_pointArray.Add(point);

SetCapture( );  // Capture the mouse until button up
m_ptPrev = point;  // Serves as the MoveTo( ) anchor point
// for the LineTo() the next point, as
// the user drags the mouse
return;

    Note This version of OnLButtonDown doesn’t include a call to the base class version. It completely replaces the inherited behavior.


Terminate Stroke Drawing

The OnLButtonUp member function ends the current stroke when the user releases the left mouse button. The function draws a line to connect the last stroke, then releases the mouse for use by other windows. The test at the beginning calls the Windows GetCapture function to determine whether the current window has control of the mouse. If not, the user is not currently drawing in this view.

To add code for OnLButtonUp

  1. Using WizardBar, jump to CScribbleView’s member function OnLButtonUp in ScribbleView.cpp.

  2. Replace the //TODO comments and code with the code shown here:
    // Mouse button up is interesting in the Scribble
// application only if the user is currently drawing a new
// stroke by dragging the captured mouse.

if( GetCapture( ) != this )
return;    // If this window (view) didn't capture the
// mouse, the user isn't drawing in this window.

CScribbleDoc* pDoc = GetDocument();
CClientDC dc( this );
CPen* pOldPen = dc.SelectObject( pDoc->GetCurrentPen( ) );
dc.MoveTo( m_ptPrev );
dc.LineTo( point );
dc.SelectObject( pOldPen );
m_pStrokeCur->m_pointArray.Add(point);

ReleaseCapture( );    // Release the mouse capture established
// at the beginning of the mouse drag.
return;

In the code above, the view obtains a pointer to its document, using GetDocument. Then the view makes sure it has the current pen. It draws the line, adding the points of the current stroke, m_pStrokeCur, to the array, m_pointArray, which contains the points that define the stroke. Finally it releases the mouse capture established at the beginning of the mouse drag.


Draw While the Mouse Button Is Down

Between the time that the mouse button goes down and the time that it’s released, Scribble tracks the mouse and draws a trace of its movements in the view. OnMouseMove is called as the user moves the mouse while drawing the current stroke. The function connects the latest mouse location with its previous location and saves the new location as the previous point for the next time the function is called. To do the drawing, OnMouseMove constructs a local CClientDC object used to draw in the window’s client area.

To add code for OnMouseMove

  1. Using WizardBar, jump to CScribbleView’s member function OnMouseMove in ScribbleView.cpp.

  2. Replace the //TODO comments and code with the code shown here:
    // Mouse movement is interesting in the Scribble application
// only if the user is currently drawing a new stroke by
// dragging the captured mouse.

if( GetCapture( ) != this )
return;        // If this window (view) didn't capture the
// mouse, the user isn't drawing in this window.

CClientDC dc( this );

m_pStrokeCur->m_pointArray.Add(point);

// Draw a line from the previous detected point in the mouse
// drag to the current point.
CPen* pOldPen =
dc.SelectObject( GetDocument( )->GetCurrentPen( ) );
   dc.MoveTo( m_ptPrev );
   dc.LineTo( point );
   dc.SelectObject( pOldPen );
   m_ptPrev = point;
   return;

In the Scribble application, OnLButtonDown, OnMouseMove, and OnLButtonUp handle the three phases of mouse drawing: beginning to track the mouse, tracking the mouse and connecting points, and ending mouse tracking.


Build Scribble, Step 1

Compile your completed code and try out the program.

Suggested Reading

  • Debugging Your Program, Visual C++ Programmer’s Guide

  • Overview: The Debugging Interface, Visual C++ User’s Guide

To build and try Scribble Step 1

  1. Open your Scribble project (if it’s not already open) by choosing Scribble.dsw from your project directory.

  2. If necessary, specify the Debug build:
    • From the Default Configuration drop-down list on the Build toolbar, select the Win32 Debug version of the project.
  3. From the Build menu, click Execute Scribble.exe.

  4. When prompted with the message that one or more files are out of date or do not exist, click Yes to build the file.

    When Scribble runs, an MDI application window appears with a menu bar containing File, Edit, View, Window, and Help menus and a toolbar and status bar. It has one document window open as shown in the following figure.

Note Scribble may not build if there are errors, such as typos, in your code. Errors are reported in the Output window. By double-clicking an error in the Output window, you display the line where the error occurs in the text editor. Compare your code to the code in the sample directory or the tutorial text to discover where the error has occurred.

Scribble Step 1

  5. Move, resize, minimize, and maximize the document window.

  6. Draw “Hello, World!” (or anything) in the window. Then save the file as Hello.scb.

  7. Try the Print Preview and Print commands on the File menu.

    Note Drawing on the printer device will show the strokes at a reduced size. This is because the current version of Scribble uses MM_TEXT mapping mode. Later in this tutorial, you’ll use MM_LOENGLISH mapping mode to improve the printer output.

  8. If you have an electronic mail system, try the Send command on the File menu.

  9. Close Hello.scb and reopen it with the Open button on the toolbar, or by choosing it from the MRU list.

  10. Create a new document with the New button on the toolbar and draw in the new document. (Save the new document if you like.)

  11. Exit Scribble.

This concludes Step 1 of Scribble. You’ve seen how to implement the document with serialization code, and the view with message handling functions.


Constructing the User Interface

Scribble: MDI Drawing Application, Lesson 5

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

Suggested Reading

  • Working with Resource Files, Visual C++ Programmer’s Guide

  • Overview: Resource Editors, Visual C++ User’s Guide

The next three lessons show how to use some of the tools supplied with the Visual C++ environment and the Microsoft Foundation Class Library (MFC).

  • This lesson explains how to use resource editors to customize Scribble’s default menus and toolbar.

  • Lesson 6, Binding Visual Objects to Code Using WizardBar, describes how to use WizardBar and the text editor to bind Scribble menu items and toolbar buttons to commands, and define message-handler functions to process the commands.

  • Lesson 7, Adding a Dialog Box, shows how to use the dialog resource editor and WizardBar to create a dialog box and connect it to a menu command.

This lesson and the next cover Step 2 of Scribble. You can build the new version of Scribble at the end of Binding Visual Objects to Code Using WizardBar.

Begin with the files in your Scribble project directory. At this point, your files should closely resemble the Step1 source code files.

As you read this lesson, perform all steps that use the resource editors. At the end, your Scribble ResourceView pane should closely resemble the same ResourceView pane in the Scribble\Step2 source files.


Edit Scribble's Menus

Edit Scribble’s Menus

The first task in this lesson is to edit Scribble’s default menus by using the menu editor.

Suggested Reading

  • Overview: Menu Editor, Visual C++ User’s Guide

  • Overview: Resource Editors, Visual C++ User’s Guide

  • Viewing Resources, Visual C++ User’s Guide

  • Icons and Cursors, Visual C++ Programmer’s Guide

Scribble starts out with a starter ResourceView pane, which contains the default menus described in the next topic, as well as other AppWizard-generated resources, such as a toolbar, an icon to depict the application in its minimized state, and so on. Open the ResourceView pane and examine the default menus and other resources.

To examine the contents of the Scribble ResourceView pane

  1. From ResourceView, click the Scribble Resources folder.

    Scribble’s resource types appear as folder icons in ResourceView.

  2. Expand any of the folders to open them and view the resources they contain; then double-click a resource to open it inside a resource editor window, where you can both view and edit it.


Default Menus

The menu resources AppWizard generated by default for Scribble include:

  • A menu bar to display when Scribble, a multiple document interface (MDI) application, has no documents open.

    The menus include a basic File menu, a View menu for toggling the visibility of Scribble’s status bar and toolbar, and a basic Help menu.

  • A menu bar to display when a Scribble document is open.

    The menu resources include, besides those above, more File menu commands, an Edit menu with standard commands, and a Window menu with standard commands (supplied only for MDI applications, like Scribble).

Note Single document interface (SDI) applications have only one menu bar. The correct menu bars are generated when you choose between single-document and multiple-document options in AppWizard.


Scribble's New Menu Commands

Scribble’s New Menu Commands

In the following topics, you will add a Clear All command to Scribble’s default Edit menu, as well as a Pen menu with two commands, Thick Line and Pen Widths. The Clear All command clears the current drawing and deletes its stroke data. The Thick Line command toggles the thickness of the lines (either thin or thick) used to draw subsequent strokes. The Pen Widths command displays a dialog box that lets the user define, in pixels, the width of the thick and thin lines for subsequent drawing.


Adding the Menus

The next two topics describe how to add the new Scribble menu items, demonstrating some fundamental techniques for using the menu editor to edit menu resources.

Suggested Reading in the Visual C++ User’s Guide

  • Overview: Menu Editor

  • Viewing Resources

  • Using Property Pages

  • Moving and Copying Menus and Menu Items


Add the Clear All Command to Scribble's Edit Menu

Add the Clear All Command to Scribble’s Edit Menu

To add Scribble’s Clear All menu command

  1. If you don’t already have Scribble’s resource file open, switch to ResourceView and expand the Scribble folder.

  2. Expand the Menu folder.

    Two menu IDs appear: IDR_MAINFRAME and IDR_SCRIBBTYPE. These menus correspond to the default menus, described earlier in this lesson. Their IDs are defined for you by AppWizard, based on the document type you chose (MDI or SDI).

  3. Double-click IDR_SCRIBBTYPE.

    The menu editor opens, with the menu displayed much as it appears in the running application.

  4. Click the Edit menu item.

    The Edit menu opens as it would in the application. An empty cell sits below the last menu item, as shown in the following figure. This cell defines where you add the next menu item.

  5. Click the cell at the bottom of the Edit menu to select it.

    Menu Editor for IDR_SCRIBBTYPE

  6. Type the caption Clear &All. (Typing the ampersand character (&) in front of a letter creates an access key combination.)

    When you start typing, the Menu Item Properties page opens, with your cursor in the Caption edit box. As you continue to type, the caption appears both in the Caption edit box and on the Edit menu.

    Note To ensure that the Menu Item Properties page remains open while you are editing Scribble’s menus, click the pushpin in the upper-left corner of the Properties window.

    Note To specify an accelerator, or shortcut key, for the menu item, append its specifier after the caption. For example, to specify CTRL+O as the accelerator for an Open command, the caption string would read “Open...\tCTRL+O” where “\t” signifies a tab to align the column.

  7. Open the ID list in the Properties window and begin typing the ID for the Clear All command: ID_EDIT_C.

    If you type with the drop-down list open, you’ll see the list box scroll to the first ID that matches the letters you’ve typed so far. The Visual C++ environment always ensures that the ID you enter is unique. ID_EDIT_CLEAR_ALL is a command ID predefined by the class library. If what you type matches a predefined command, Visual C++ assigns the predefined command ID to your menu item rather than creating a duplicate.

    Several IDs that begin with “ID_EDIT_C” appear in the list box.

  8. Select the ID_EDIT_CLEAR_ALL entry.

    As soon as you select the predefined ID, the following string appears in the Prompt box: “Erase everything\nErase All”. This prompt string is displayed in the status bar, if the application has one, when the user navigates up and down the menu using the keyboard; the text after the newline character (\n) appears as a tool tip if you create a toolbar button for this menu command. (For more information, see Add the Thick Line Button to Scribble’s Toolbar Bitmap, later in this lesson.)

    AppWizard predefines the prompt text for the ID_EDIT_CLEAR_ALL symbol. For any ID that isn’t predefined, you should enter a descriptive prompt string. This context-sensitive menu information is essentially free, so take advantage of it.

    The following figure shows the property page after you’ve selected the ID.

    Property Page with ID

    You can define your own command IDs. You’ll see an example under the next topic, Add Scribble’s Pen Menu.

  9. Change the Prompt wording to Clears the drawing.

    Because this menu command won’t have an equivalent toolbar button, you remove the tool tip.

  10. Save the resource file.

You’ve added the Clear All command to the Edit menu.

Note You don’t have to press ENTER or click any buttons to accept the values you’ve entered in the Menu Item Properties page. Your entries are stored immediately by the program.

The following figure shows how the menu looks at this stage.

The Clear All Menu Item

The most important task in defining the menu command is assigning its ID. To the framework, the ID is the command. You must specify which code is executed when the user clicks the Clear All menu command. The code is linked to the menu by the ID.


Add Scribble's Pen Menu

Add Scribble’s Pen Menu

Adding a new menu is similar to adding new commands to existing menus.

Note This procedure assumes you have “pinned” the Menu Properties window so that it remains open.

To add Scribble’s Pen menu

  1. With the menu-editor window still showing, click in the empty cell at the right-hand end of the Scribble menu bar (after the Help menu).

  2. To position the menu entry, drag the selected cell to the left and drop it between the Edit and View menus.

  3. Type the new menu’s caption: &Pen.

    The caption appears simultaneously in the cell and in the Caption edit box of the Menu Item Properties window.

  4. Press ENTER to advance to the first menu item on the Pen menu (or click the empty cell that descends beneath the word “Pen”).

  5. Type this menu item’s caption: Thick &Line.

    Move your cursor to the ID box in the Menu Item Properties window. You’ll notice that Visual C++ created an ID for you, based on the menu name and the menu item name: ID_PEN_THICKLINE.

  6. Modify this ID slightly, to ID_PEN_THICK_OR_THIN.

  7. In the Prompt box, type the following command prompt string:
    Toggles the line thickness between thin and thick

    No default prompt string appeared because ID_PEN_THICK_OR_THIN is not a predefined command ID.

  8. Select the empty cell at the bottom of the Pen menu, below “Thick Line.” (You can also toggle between the Menu Item Properties window and the menu editor by pressing ENTER.)

  9. Type this menu item’s caption: Pen &Widths....

    An ellipsis (. . .) included in a menu item’s caption lets the user know that selecting the item opens a dialog box or a cascading menu.

  10. In the ID box, modify the Visual C++-generated ID slightly, to ID_PEN_WIDTHS.

  11. Type the following command prompt string:
    Sets the size of the thin and thick pen
  12. Close the menu editor and save your work.

The following figure shows the completed menu as it appears in the menu editor.

The Completed Pen Menu

Typically, at this point you would use ClassWizard or WizardBar to bind the menu commands to message-handler functions. That step is postponed until the next lesson in order to keep this lesson focused on constructing the user interface. If you like, you can skip ahead, perform the command-binding steps in Binding Visual Objects to Code Using WizardBar, and then return to this lesson to edit Scribble’s toolbar.


Edit Scribble's Toolbar

Edit Scribble’s Toolbar

The resources that AppWizard creates for Scribble, which you can view in ResourceView, also include a toolbar resource, shown in the following figure. When you build Scribble, the framework uses the toolbar resource to create a dockable toolbar. In this topic you’ll use the toolbar editor to add a new button and, optionally, delete some unnecessary buttons from Scribble’s toolbar.

Suggested Reading

  • Overview: Toolbar Editor, Visual C++ User’s Guide

  • Toolbars: Docking and Floating, Visual C++ Programmer’s Guide

  • Viewing Resources, Visual C++ User’s Guide

The Default Scribble Toolbar

Earlier in the lesson in Add Scribble’s Pen Menu, you added the Pen menu. One of its menu items is the Thick Line command. The present task is to add a corresponding Thick Line button to Scribble’s toolbar. Then, in Lesson 6, Binding Visual Objects to Code Using WizardBar, you’ll use WizardBar to connect both the Thick Line menu item and the Thick Line toolbar button to the same handler member function. Thus, both user-interface objects will have the same command ID so they generate the same command message, which calls the same handler function. When the user clicks either the menu item or the toolbar button, the handler function toggles Scribble’s drawing pen between thin and thick lines.


About the Toolbar

Some of the buttons on Scribble’s toolbar already work, as you saw when you built Scribble following Step 1, in the previous lesson. The buttons for opening and saving files are already connected to handlers defined by the framework. To make the file operations functional, you wrote the serialize functions for the document and the stroke data structure. Also, the print button is supported by default, and the About button automatically displays the About box for Scribble (the About box was created for you by AppWizard).

The Cut, Copy, and Paste buttons on the toolbar appear grayed, because they have no handlers defined for them. These buttons will not be implemented for Scribble, and so you will (optionally) remove them from the Scribble toolbar.


Add the Thick Line Button to Scribble's Toolbar Bitmap

Add the Thick Line Button to Scribble’s Toolbar Bitmap

You’ll add the Thick Line button to Scribble’s toolbar by performing a few simple steps:

  • Open Scribble’s toolbar resource

  • Delete toolbar buttons

  • Add a new button

  • Assign a resource ID to the new button

  • Add a tool tip


Opening Scribble's Toolbar Resource

Opening Scribble’s Toolbar Resource

As with any of the resource editors, when you open a specific application resource, the corresponding resource editor opens automatically.

To open Scribble’s default toolbar resource

  1. If you don’t already have Scribble’s resource file open, switch to ResourceView and expand the Scribble folder.

  2. Expand the Toolbar folder and double-click IDR_MAINFRAME.

    The toolbar editor, shown in the figure below, opens, displaying the default toolbar resource that AppWizard created for Scribble. The first button on the toolbar, selected by default, appears in the bottom pane, or magnified view, of the editor window.

    The Toolbar Editor

The graphics and color tools, shown in the figure below, also open as part of the toolbar editor. If these graphics tools don’t appear:

  1. From the Tools menu, click Customize.

  2. Click the Toolbars tab.

  3. In the Customize dialog box, select Graphics and Colors on the Toolbars list.

  4. Click Close.

    You can drag the graphics tools to either side of the screen and dock them to get a better view of the editor window.

    The Graphics Toolbar


Deleting and Adding Buttons to Scribble's Toolbar

Deleting and Adding Buttons to Scribble’s Toolbar

In this procedure, you will delete and add toolbar buttons in Scribble.

To delete a toolbar button

  • Drag the button off the toolbar (in the top, or normal view pane).

    In this case, drag the Cut, Copy, and Paste buttons off the Scribble toolbar.

    The above procedure is optional; if you don’t remove these buttons, they will remain grayed in the running application but otherwise will not interfere with Scribble operations.

To add the new toolbar button

  1. Select the blank button at the right-hand end of the toolbar resource.

    It receives focus in the two split panes of the editing window.

    Tip If you want the button to appear larger in the editor, click the Magnify tool and select the magnification factor you want.

  2. Click the pencil tool from the graphics toolbar.

  3. Using the magnified view of the button, draw the image shown in the following figure.

    It doesn’t have to be exact. If you make a mistake, change the pencil color to gray and color over your mistake.

  4. Save your edits.

    Thick Line Toolbar Button Resource

Note The blank button that appears by default in the toolbar editor window does not appear in the running application.


Associating the Toolbar Button with a Command ID

In this task, you will associate the new Thick Line button with the command ID ID_PEN_THICK_OR_THIN, which you defined earlier for the Thick Line menu command. This step is identical to the one you performed to associate a menu item with a command ID.

Note You don’t need to delete the command IDs for the buttons you deleted. The Visual C++ environment does that for you automatically.

To associate the button with a command ID

  1. Click Properties from the View menu to display the Toolbar Button Properties page.

    You’ll notice that Visual C++ has already assigned a command ID to the button. You will replace this ID with an existing ID — the one defined for the Thick Line menu command.

  2. From the ID drop-down list, select ID_PEN_THICK_OR_THIN.

    You can also type this directly into the ID box.

  3. Save your work.

Note By associating the command ID with the toolbar button, the string resources become active for the button as well: when the mouse passes over the button, the prompt string displays in the status bar, and the tool tip displays by the button.

The following figure shows the modified toolbar resource as it appears in Scribble.

The Edited Toolbar Resource


Adding a Tool Tip

In this procedure, you will add a tool tip, the yellow hint that appears when the mouse rests over an interface element, to your new toolbar button.

Note AppWizard appends the tool tip string automatically to the prompt strings it generates.

To add a tool tip

  1. If necessary, select the new toolbar button in the editor window, and click Properties from the View menu to display the Toolbar Button Properties page.

    In the Prompt box, you’ll see the text string “Toggles the line thickness between thin and thick”. Because you entered this text for the Thick Line menu item on Scribble’s Pen menu; it appears in the status line when the mouse passes over the menu command.

  2. To add a tool tip, at the end of the Prompt text type a newline character (\n) plus the text you want displayed in the tip (there should be no space between the newline character and the tip text). If you want a tool tip without a prompt string, simply start with the newline character.

    Keep this text short. For Scribble, type \nToggle Pen after the existing Prompt string.

  3. Save your work.


Summary: Constructing the User Interface

Scribble’s resource needs are simple, so this lesson introduced only a few of the things you can do with the resource editors.

After editing your application’s menus and toolbar, the next task is to connect them to code using WizardBar and ClassView. That task is explained in the next lesson, Binding Visual Objects to Code Using WizardBar.


Binding Visual Objects to Code Using WizardBar

Scribble: MDI Drawing Application, Lesson 6

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Message Handling and Mapping Topics

  • How to Update User-Interface Objects

  • Using WizardBar

  • Overview: The Universal WizardBar

  • Associating a Menu Item with an Accelerator Key

Like all applications written for Windows, Scribble is message driven. A keystroke, mouse click, or other event causes messages to be sent to some part of the application that can respond to the event. In Adding the Message-Handler Functions, in Lesson 4, for example, you saw that Scribble implements mouse drawing by detecting and responding to messages generated by mouse clicks and drags.

This lesson discusses a category of messages called “commands,” which are messages to your application from menu items, toolbar buttons, and accelerator keys.

The expanded version of Scribble developed in this lesson adds two menu items:

  • One generates commands to toggle the line thickness for drawing.

  • One clears all strokes from the current document.

The command that toggles line thickness is also duplicated by a button on Scribble’s toolbar.

You created the resources for Scribble’s new menu items and its new toolbar button in Edit Scribble’s Menus and Edit Scribble’s Toolbar in Lesson 5. Now you can use WizardBar to assign a user-interface object, such as a menu item, to a command and map the command to a function that handles it.

In this lesson, you will:

  • Extend your knowledge of WizardBar, begun in Lesson 4, Creating the View.

  • Add new command-handling code for Scribble.

  • Connect a toolbar button and a menu item to the same command.

  • Learn how to keep your user-interface objects (menus and toolbar buttons) updated in response to changing program conditions by, for example, enabling or disabling a menu item and toggling a button.

At the end of this lesson, your files should closely resemble the Step2 source code files, and your Scribble program should compile and run.

Note You can find a finished example of this lesson's code in the Scribble Step2 sample source code directory.


Creating Message-Handler Functions with WizardBar

In this lesson, you’ll learn to use WizardBar to bind commands to message-handler functions.

When you use WizardBar to create a message-handler function, ClassWizard writes an entry for the command in the chosen class’s message map and adds a function declaration to the class. Also, ClassWizard writes a function template — a complete member function definition with an empty function body — in the source file that contains the class. WizardBar jumps you directly to the text editor to fill in the function template.

Important If you delete a command binding with ClassWizard, its message-map entry is deleted, but the message-handler function, and any references to it in your other code, are not deleted. You must delete those items by hand. This is for your safety; the message-handler function code, which you probably wrote, is preserved until you delete it.


Binding Scribble's Commands

Binding Scribble’s Commands

This topic explains the issues and procedures involved in binding Scribble’s Clear All and Thick Line commands to their handlers using WizardBar. (You’ll bind the Pen Widths command in Adding a Dialog Box, Lesson 7.)

Which Command-Target Class Gets the Handler?

Before you can bind Scribble’s Clear All command to a message-handler function in the document class, you must solve some problems. Where should you put the handler for a command? Where should you put attributes, such as a line thickness value? In the document class? In the view class? Somewhere else?

Scribble has one document class (some applications might have several kinds of documents — such as text documents and graphics documents) and one view class (some documents might have more than one way to view their data — for example, as text or as an outline).

Scribble’s Clear All command has two effects: It deletes data in the document and it causes the view to be redrawn with no strokes. Should the handler for Clear All be located in the document or the view? Scribble’s CScribbleDoc class houses the application’s data structure, the stroke list. Clear All’s primary effect is to delete the data. Redrawing the view afterward is secondary. Hence, it makes sense to locate the OnEditClearAll handler in the document.

Scribble’s Thick Line command toggles the current value of a line thickness variable between thick and thin. Should the handler for Thick Line be located in the view because it affects how Scribble’s data is drawn? This seems reasonable, but consider what happens when, in Adding Splitter Windows, in Lesson 8, Scribble gets splitter window functionality. In that case, each pane of the splitter window is really a new view on the same data. Should each of these views house its own line thickness information (and its own pen)? A better solution is to store that information in the document instead, where all of the views can access it.

This decision is specific to Scribble’s user interface, where the pen width commands should apply to all views, not just the one with the current focus. You could organize things differently in another application. This type of program flow is up to you.

Consider a hypothetical application with more than one view on a document and perhaps even more than one frame window for the same document. Should handlers and attributes be part of the document, part of one of the frame windows, or part of one of the views? Should an attribute be duplicated in more than one view or frame window?

Here are some guidelines:

  • In general, put handlers in the command-target class where they have the greatest effect.

  • When attributes are shared by multiple views or frame windows, put them in the common document.

  • If attributes are not shared, put them in the view(s) or window(s) that use them.


Bind Scribble's Clear All Command to Its Handler Code

Bind Scribble’s Clear All Command to Its Handler Code

As discussed in the previous topic, Which Command-Target Class Gets the Handler?, Scribble’s Clear All command is bound to the document class.

To create the starter handler for Scribble’s Clear All command

  1. In FileView, expand the Source Files folder, if necessary, and double-click the icon for ScribbleDoc.cpp to open the file.

    Recall the decision to handle the command from the document rather than the view. That’s why the handler for Clear All will be placed in CScribbleDoc.

  2. Click the action button arrow located on the right end of WizardBar.

  3. Click Add Windows Message Handler.

    The New Windows Message Handler dialog box appears.

  4. In the Class or object to handle box, select ID_EDIT_CLEAR_ALL.

  5. In the New Windows messages to handle list box, select COMMAND.

    You can see both COMMAND and UPDATE_COMMAND_UI in the New Windows messages to handle list box. These are the two events for which the framework provides ClassWizard support in creating your command handler code. That’s why, for commands, these are always the choices you see in this list box. In other cases, you might see other things listed — a list of Windows messages, for example, when the selected item is the name of a window or view class.

    Later in the lesson, in Update Scribble’s Clear All Menu Item, you’ll see how UPDATE_COMMAND_UI is used for this menu command.

  6. Click Add and Edit.

  7. In the Add Member Function dialog box, click OK to accept the name OnEditClearAll.

    The following figure shows the selections from steps 3 through 7.

    Clear All in WizardBar

    ClassWizard creates the starter handler function at the end of ScribbleDoc.cpp and highlights the //TODO comments where you will add the code, which is described in the next procedure, To complete the OnEditClearAll handler function.


Completing the OnEditClearAll Function

ClassWizard adds several things to your source files when you use WizardBar to create a member function to handle a command:

  • A message-map entry to the class’s message map (in the .cpp file for the class).

  • A member function declaration to the class declaration (in the .h file for the class).

  • An empty member function definition to the .cpp file.

The following figure shows the starter code for OnEditClearAll.

The OnEditClearAll Function Template

To complete the OnEditClearAll handler function

  • Add the following code to fill in the OnEditClearAll message-handler function. (Replace the highlighted //TODO comments.)
    DeleteContents( );
SetModifiedFlag();
UpdateAllViews( NULL );

    SetModified Flag is a member function of class CDocument. It marks the document as changed so the framework will prompt the user to save the document when it closes.

The OnEditClearAll message handler:

  • Calls DeleteContents to destroy the document’s stroke data.

    Scribble’s version of DeleteContents, from Lesson 8, Enhancing Views, overrides CDocument’s DeleteContents member function. (See To implement document cleanup.) The DeleteContents member function iterates through the list of strokes. For each stroke, it gets the next stroke and calls the delete operator on it.

  • Calls the UpdateAllViews member function inherited from CDocument to cause all views of the data to be updated.

    The document’s view is redrawn, this time with no data. UpdateAllViews takes a NULL argument because the document is modifying itself. The parameter is normally used to pass a pointer to the view that modified the document, but that doesn’t apply here.


Binding the Thick Line Command

When you finish adding OnEditClearAll, you will continue binding commands, using WizardBar.

Like the Clear All command, the Thick Line command will be handled by the document. Recall the discussion earlier in this lesson under Which Command-Target Class Gets the Handler?.

To bind Scribble’s Thick Line command

  1. Make sure that ScribbleDoc.cpp is active in the text editor.

  2. Repeat steps 2 through 7 of the procedure To create the starter handler for Scribble’s Clear All command, only this time for ID_PEN_THICK_OR_THIN, accepting the default member function name OnPenThickOrThin.

    ClassWizard creates the starter handler function at the end of ScribbleDoc.cpp.

  3. Replace the //TODO comment text with the following code:
    // Toggle the state of the pen between thin and thick.
m_bThickPen = !m_bThickPen;

// Change the current pen to reflect the new width.
ReplacePen( );

The OnPenThickOrThin message handler first toggles the state of a Boolean variable, m_bThickPen. If the variable is now TRUE, the pen will be thick. Otherwise, it will be thin. The handler then calls a helper function, ReplacePen, to reset the current pen to the new width. (You will declare the m_bThickPen variable later in the lesson.)


Adding the ReplacePen Helper Function

In this topic, you will use WizardBar to add the member function ReplacePen.

To create the ReplacePen helper function

  1. Click the action button arrow, located on the right end of WizardBar.

  2. From the action menu, click Add Member Function.

    The Add Member Function dialog box appears.

  3. In the Function Type edit box, type the function’s return type (in this case, void).

  4. In the Function Declaration edit box, type the declaration (function name and parameters, if any) of the new function.

    In this case, type the following:

    ReplacePen ()

    You don’t need to type a semicolon. Also, for functions such as this that take no parameters, the parentheses are optional.

  5. In the Access area, select Protected.

  6. Click OK.

    ClassWizard adds the declaration to the beginning of the first Protected section of the header file it finds (creating the section if it does not exist); creates a starter definition in the implementation file; and jumps you to the declaration.

  7. Using WizardBar, select CScribbleDoc from the Class combo box and select ReplacePen from the Members combo box.

  8. Type the following code to fill in the function definition for ReplacePen:
    m_nPenWidth = m_bThickPen ? m_nThickWidth : m_nThinWidth;
// Change the current pen to reflect the new width.
m_penCur.DeleteObject( );
m_penCur.CreatePen( PS_SOLID, m_nPenWidth, RGB(0,0,0) );

The ReplacePen member function uses the C conditional operator (?:) to determine the pen width and return its value. Then it calls the DeleteObject member function of the current pen object and creates a new solid black pen with CreatePen, setting its width and other attributes.

Now that you’ve created ReplacePen, you want to call it from the Scribble code that initializes the pen width. This happens in the InitDocument member function code.

To incorporate ReplacePen into Scribble

  1. Using WizardBar, select CScribbleDoc in the Class combo box and InitDocument in the Members combo box. Click the action button to jump to InitDocument.

  2. In the text editor, replace the current code with the single line that calls ReplacePen:
    ReplacePen();   // Initialize pen according to current width

    Note This code replaces the code you originally added in Lesson 4, Creating the View:

       m_nPenWidth = 2; // Default 2 pixel pen width
   // Solid, black pen
   m_penCur.CreatePen( PS_SOLID, m_nPenWidth, RGB( 0,0,0 ) );

    because this code was incorporated into the ReplacePen function.

  3. Save your work.


Add New Member Variables to Scribble

In addition to storing the current pen width in m_nPenWidth, class CScribbleDoc needs to keep track of whether the pen is currently thick or thin and how “thick” and “thin” are defined (in pixels). You do this by adding new data members for the thick and thin pen values. In Connecting a Class to a Dialog Box, in Scribble’s next lesson, you will add code to allow the user to define these values with a dialog box. For now, the default values will be hard coded.

To add the new data members

  1. Use ClassView to jump to the class definition for CScribbleDoc.

    Locate the section labeled “// Attributes:”.

  2. Right above this label, and after the protected keyword and the existing m_nPenWidth declaration, add the following code:
    BOOL    m_bThickPen;    // Thick currently selected or not
UINT    m_nThinWidth;    // Current definition of thin
UINT    m_nThickWidth;    // Current definition of thick

Tip You could also right-click CScribbleDoc in ClassView and click Add Member Variable to add these three variables.

To hard code the values

  1. Use ClassView to jump to InitDocument (in ScribbleDoc.cpp), and add the following code just before the call to ReplacePen:
    m_bThickPen = FALSE;
m_nThinWidth = 2;     // Default thin pen is 2 pixels wide
m_nThickWidth = 5;    // Default thick pen is 5 pixels wide

    The added code specifies that the pen is initially thin and defines the meanings of “thin” and “thick.”

  2. Save your work in ScribbleDoc.h and ScribbleDoc.cpp.

ClassView displays icons for the new member variables in class CScribbleDoc.


Updating User-Interface Objects

You’ll often change the condition of menu items in your running application to provide the user with information about current program conditions. For example, you might want certain menu items to appear grayed to show they’re unavailable. You might want other menu items to have a check mark toggle. The same general idea holds true for toolbar buttons: You can specify that they appear enabled, disabled, or perhaps pushed in, as conditions in the program change. The framework provides a direct, command-based way to set the state of the menus and toolbar buttons.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Messages and Commands in the Framework

Follow the procedures in the topics listed below to learn how to change menu items to match their corresponding commands' states.

  • Update Scribble’s Clear All Menu Item

  • Update Scribble’s Thick Line Menu Item


Update Scribble's Clear All Menu Item

Update Scribble’s Clear All Menu Item

This topic presents the steps you will take to update the Clear All menu item on Scribble’s Edit menu. The update command is handled by the document object, which has the necessary information on whether there are any strokes in the current drawing to clear.

Recall how you used WizardBar to create a starter handler for the Edit menu’s Clear All command earlier in this lesson, in the topic Bind Scribble’s Clear All Command to Its Handler Code; then you filled in the handler code. In this topic you’ll use WizardBar to create a starter handler for the update command, which you’ll then fill in with Scribble-specific code.

To add an update handler for Scribble’s Clear All menu

  1. Using WizardBar, open ScribbleDoc.cpp in the text editor.

  2. In the WizardBar Filter combo box, select ID_EDIT_CLEAR_ALL.

  3. Click the action button arrow on the right end of WizardBar and click Add Windows Message Handler.

  4. In the New Windows messages to handle box, select UPDATE_COMMAND_UI. Click Add and Edit.

    The Add Member Function dialog box appears, displaying a suggested name for the handler.

  5. Click OK to accept the name OnUpdateEditClearAll.

    WizardBar creates the function template for OnUpdateEditClearAll at the end of the file.

  6. Replace the //TODO comment text with the following code:
    // Enable the user-interface object (menu item or tool-
// bar button) if the document is non-empty, i.e., has
// at least one stroke.
pCmdUI->Enable( !m_strokeList.IsEmpty( ) );
  7. Save your work.

The OnUpdateEditClearAll handler takes one argument, a pointer to a CCmdUI object that contains information about the Clear All menu item on Scribble’s Edit menu.

The pointer to a CCmdUI object, pCmdUI, is used to access a CCmdUI member function, Enable. Enable takes one Boolean argument. In this code, the expression !m_strokeList.IsEmpty( ) evaluates to nonzero if the document has at least one stroke to clear. If the expression evaluates to zero (no strokes), the menu item is disabled (and dimmed or grayed).

Note When the user opens a menu, the update handlers for all items on the menu are called before the user sees the menu displayed. Thus it’s important not to perform a lot of processing in your update handlers.

When you added the update command handler for the Clear All menu item, ClassWizard wrote the following message-map entry in the document’s message map in ScribbleDoc.cpp:

   ON_UPDATE_COMMAND_UI( ID_EDIT_CLEAR_ALL, OnUpdateEditClearAll )

The ON_UPDATE_COMMAND_UI macro resembles the ON_COMMAND macro for the OnEditClearAll message handler.

In addition, ClassWizard added a new member function declaration for OnUpdateEditClearAll to the CScribbleDoc class definition in ScribbleDoc.h. The function declaration looks like this:

afx_msg void OnUpdateEditClearAll( CCmdUI* pCmdUI );


Update Scribble's Thick Line Menu Item

Update Scribble’s Thick Line Menu Item

Updating the Thick Line menu item is similar to updating the Clear All menu. In this case, however, rather than enabling or disabling the menu item, the handler puts a check mark beside the item or removes an existing check mark.

To add an update handler for the Thick Line menu item

  1. In the WizardBar Filter combo box, select ID_PEN_THICK_OR_THIN.

  2. Click the action button arrow on the right end of WizardBar and click Add Windows Message Handler.

  3. In the New Windows messages to handle box, select UPDATE_COMMAND_UI. Click Add and Edit.

    The Add Member Function dialog box appears, displaying a suggested name for the handler.

  4. Click OK in the Add Member Function dialog box to accept the name OnUpdatePenThickOrThin, and to create the handler.

  5. Replace the //TODO comments with the following code:
    // Add check mark to Pen Thick Line menu item if the current
// pen width is "thick."
pCmdUI->SetCheck( m_bThickPen );
  6. Save your work.

Rather than enabling or disabling the menu command, this handler uses the pointer to a CCmdUI object to call the SetCheck member function. SetCheck puts a check mark in front of a menu item if its argument evaluates to TRUE and removes the check if the argument evaluates to FALSE. In this case, the expression m_bThickPen is a member variable of CScribbleDoc. It evaluates TRUE if the line thickness is currently set to thick. Since the value of m_bThickPen is passed to SetCheck, the effect is to toggle the menu item’s check mark on or off as the line thickness changes.

The ON_UPDATE_COMMAND_UI message-map entry and the OnUpdatePenThickOrThin message handler serve to update the state of the Thick Line button on the toolbar as well as the Thick Line menu item. The code line

pCmdUI->SetCheck( m_bThickPen );

adjusts the state of the toolbar button as well as updates the checked state of the menu item. For a toolbar button, “checked” means depressed.

In this example, the user would previously have reset the line thickness. The next time the user clicks the Pen menu item (or the toolbar button), the user-interface update mechanism takes care of updating the check mark to match the current thickness. Similarly, the toolbar button’s state toggles between a “pressed down” appearance and a normal appearance.

As with the update handler for Clear All, ClassWizard adds a message-map entry for OnUpdatePenThickOrThin to the document’s message map in ScribbleDoc.cpp:

BEGIN_MESSAGE_MAP( CScribbleDoc, CDocument )
   //{{AFX_MSG_MAP(CScribbleDoc)
   // Other message-map entries
   ON_UPDATE_COMMAND_UI(ID_PEN_THICK_OR_THIN,OnUpdatePenThickOrThin)
   //}}AFX_MSG_MAP
END_MESSAGE_MAP( )

ClassWizard also adds a member function declaration to the document class definition in ScribbleDoc.h:

afx_msg void OnUpdatePenThickOrThin( CCmdUI* pCmdUI );


Build Scribble, Step 2

How does Scribble behave with these new commands in place? Build, and then run, the new Step 2 version of Scribble to find out.

To build and try Scribble Step 2

  1. From the Build menu, click Execute Scribble.exe.

  2. When prompted with the message that one or more files are out of date or do not exist, click Yes to build the file.

    Once the file is built, the executable will run.

The following figure shows this version of Scribble.

Scribble Step 2

Draw some strokes with the default thin pen. Then change the line thickness by clicking the Thick Line toolbar button you added in Lesson 5, Constructing the User Interface, and draw some new strokes. Try the same task by using the Thick Line command on the Pen menu. (The Pen Widths command is grayed because you have not hooked it up to the dialog box yet. You’ll do that in Lesson 7, Adding a Dialog Box.) Clear all strokes from the drawing with the Clear All command on the Edit menu. Move the toolbar around and see how it docks when you drag it over to the frame. The framework provides this functionality for you.

Exit Scribble.

This completes Step 2 in the tutorial. You should now have a basic understanding of commands. In later lessons you’ll build on that foundation.


Adding a Dialog Box

Scribble: MDI Drawing Application, Lesson 7

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

Note You can find a finished example of this lesson's code in the Scribble Step3 sample source code directory.

Suggested Reading

  • Overview: Dialog Editor, Visual C++ User’s Guide

  • Using Property Pages, Visual C++ User’s Guide

  • Property Sheets and Property Pages, Visual C++ Programmer’s Guide

In Lesson 5, Constructing the User Interface, and Lesson 6, Binding Visual Objects to Code Using WizardBar, you added new commands to Scribble in two steps:

  • Using the menu editor to add new menu items

  • Using WizardBar to define message handlers and bind them to the menu commands.

You added menu items for three new commands: Edit Clear All, Thick Pen, and Pen Widths. You practiced binding only the first two of these commands.

The Pen Widths command is somewhat different from the other two commands. Both the Edit Clear All and Thick Pen commands execute to completion as soon as the user clicks them. By contrast, the Pen Widths command requires more information from the user. This command opens a dialog box, one that lets the user specify the widths of the Thin Pen and the Thick Pen.

Before you can write a message handler for the Pen Widths command, you must design the dialog box that it displays and define a new class to manage the dialog box. That’s what you’ll do in this lesson.

This lesson develops a modal dialog box using the same general procedure that was used for adding menu commands in Constructing the User Interface and Binding Visual Objects to Code Using Wizard Bar. You’ll use the dialog editor to design the dialog box’s appearance, and then use ClassWizard to declare a dialog class, and WizardBar to define message handlers and bind them to the dialog box.

This lesson includes the following topics:

  • Designing a dialog box

  • Connecting a class to a dialog box

  • Opening the dialog box from your application


Designing a Dialog Box

The following figure shows the Pen Widths dialog box that you will create.

Scribble’s Pen Widths Dialog Box

Suggested Reading

  • Adding and Editing Controls in a Dialog Box (all topics), Visual C++ User’s Guide

  • Changing the Tab Order, Visual C++ User’s Guide

  • Testing a dialog box, Visual C++ User’s Guide

  • Dialog Box Topics, Visual C++ Programmer’s Guide

  • Dialog Editor Topics, Visual C++ Programmer’s Guide

The Pen Widths dialog box will have the following characteristics:

  • Thin and Thick Pen width edit boxes where the user can enter any number (representing pixels) between 1 and 20. If the user enters a value outside this range, Scribble displays a message box stating the legal range; after dismissing the message box, the user can enter new values.

  • A Default button that enables the user to reset the pen widths to their default values.

  • An OK button that enables the user to use the specified values for any subsequent drawing.

  • A Cancel button that enables the user to cancel any values entered in the dialog box.

Visual C++ Environment's Dialog Resource Editor

The Visual C++ environment provides a dialog resource editor for designing dialog boxes. This editor displays the Controls toolbar, which shows the available controls (such as radio buttons, check boxes, and pushbuttons). You select controls from the Controls toolbar and position them on your dialog box. You can move and resize the controls directly by using the mouse.

You use the property page for each control to specify its caption and ID.

Designing a dialog box requires three steps:

  1. Creating a new dialog box and editing its caption and ID.

  2. Adding the controls and editing their captions and IDs.

  3. Arranging and testing the controls within the dialog box.


Create the Dialog Box

Visual C++ provides many predesigned resources that you can incorporate into your projects. The default dialog box is one such resource. In this procedure, you’ll create a simple dialog box by starting with the default dialog box that the development environment provides.

To create the Pen Widths dialog box

  1. With your Scribble project open, from the Insert menu, click Resource.

  2. In the Insert Resource dialog box, select Dialog from the list of resource types and click OK.

    Scribble’s ResourceView pane opens and the dialog editor window appears, displaying a default dialog box that contains two buttons labeled OK and Cancel. The Controls toolbar also appears.

  3. If the property page is not displayed, right-click the dialog box, then click Properties on the menu. Click the pushpin on the property page to keep it open.

  4. In the ID box, type IDD_PEN_WIDTHS.

    This is not a predefined ID, so you can’t select it from the drop-down list.

  5. In the caption box, change the caption to Pen Widths.

    Notice that the title bar of the dialog box reflects the new caption.

  6. Save your work.

Note The OK and Cancel buttons have predefined properties, including their command IDs — IDOK and IDCANCEL, respectively. If you click the OK button and switch to the Styles tab of the property page, you will see that the Default Button check box is checked.

In the next topic, you’ll add several controls to the default dialog box resource.


Add the Controls

This procedure assumes you have the property page open, the pushpin "clicked," and the IDD_PEN_WIDTHS dialog resource open in the dialog editor.

To add controls to the Pen Widths dialog box

  1. From the Controls toolbar, add two edit box controls to the Pen Widths dialog box.
    • Select the first edit box to display its property page. Change its ID to IDC_THIN_PEN_WIDTH.

    • Select the second edit box, and in the property page change its ID to IDC_THICK_PEN_WIDTH.
  2. From the Controls toolbar, add two static text controls to contain the descriptions for the two edit controls.
    • Select the first text box to display its property page. Change the caption to Thin Pen Width:.

    • Select the second text box, and in the property page change its caption to Thick Pen Width:.

    For the purposes of Scribble, you won’t have to refer programmatically to the IDs of the text boxes, so you can leave them with their default values (both have the value IDC_STATIC).

  3. From the Controls toolbar, add a third button to the two already present.

  4. Select the third button to display its property page. Change its ID to IDC_DEFAULT_PEN_WIDTHS and its caption to Default.

    The handler for this button will reset the thick and thin pens to their default widths.

The following figure illustrates designing the Pen Widths dialog box. In this illustration, the central window is the dialog editor window. The property page is below the dialog editor, and the Controls toolbar is to the right of the dialog editor.

Designing the Pen Widths Dialog Box


Arrange and Test Controls

Once you’ve added all the controls to the dialog box, you can also:

  • Resize the dialog box for a balanced layout.

  • Align the controls and make them the same size, using the commands on the Layout menu.

  • Define the tab order for the controls.

    You can see the tab order by clicking Tab Order from the Layout menu. To change the tab order, click each control in the order you want as the tab order.

  • Test the dialog box.

    If you want to see how the dialog box will look when it’s displayed, click the Test command from the Layout menu. This displays the dialog box as it will appear in Scribble, enabling you to test aspects such as the tab order, default button, and so on. Exit Test mode by clicking either the OK or Cancel button on the dialog box or by pressing the ESC key.


Connecting a Class to a Dialog Box

Once you’ve specified the appearance of your dialog box, you can specify its run-time behavior. This requires deriving a class from CDialog that implements your dialog box, and connecting that class to the dialog resource you created in the previous topic.

To connect a class to a dialog box:

  1. Declare a class to represent the dialog box.

  2. Declare handler functions for the messages you want to handle.

  3. Map the dialog box controls to member variables of the dialog class and define what (if any) validation rules should be applied to each.

You could do all of this manually, but WizardBar provides a graphical user interface to do it quickly and easily. ClassWizard generates a header (.h) and an implementation (.cpp) file for your dialog class, complete with function prototypes, skeletal function definitions, a message map, and a data map.

The following topics show how these steps are accomplished for Scribble’s Pen Widths dialog box.


Declare the Dialog Class

The following procedure shows you how to use WizardBar to declare a class for the dialog box you just created.

Note The following procedure assumes you have the Pen Widths dialog resource open, and that you have not previously declared a class for it.

To declare the new dialog class

  1. Click the arrow on the action button, located at the right end of WizardBar.

  2. Click New Class.

    The New Class dialog box appears.

    The New Class Dialog Box

  3. Fill in the selection as follows:
    • In the Class type combo box, select MFC.

    • In the Name box, type CPenWidthsDlg.

      In the File name box, the name is assigned for the implementation (.cpp) file based on the characters you type. If you wanted to change the filename, you would click the Change button.

    • In the Base class combo box, click CDialog.

      When you click CDialog, the Dialog ID box is activated with the default IDD_PEN_WIDTHS.

  4. Click OK to create the dialog class.

    This creates the class and closes the New Class dialog box.


Viewing the Newly Created Class

When you use WizardBar to create a new class, WizardBar creates and adds the associated .h and .cpp files to the project. You can view them in the FileView pane of the Project Workspace window; and in the ClassView pane, you can view the iconic representation of the new CPenWidthsDlg class and its default member functions.

In the next topic, Declare a Message-Handling Function for a Dialog Box Control, you’ll use WizardBar to add a message-handling function to the PenWidthsDlg.h and PenWidthsDlg.cpp files. But first, examine the files as they appear as first created.

Header File

Take a minute to examine the initial version of PenWidthsDlg.h. This file contains a declaration for CPenWidthsDlg, the class that implements the Pen Widths dialog box. At this point, the class contains two member functions: a constructor and the DoDataExchange function, which is described later on.

The file contains comment lines that begin //{{AFX_ and //}}AFX_. ClassWizard uses those comment lines to find the sections of code that it maintains. There are three such sections in the header file, each delimited by slightly different comments:

  • The AFX_DATA section, containing the declarations of the dialog data members

  • The AFX_VIRTUAL section, containing the declarations of the override functions

  • The AFX_MSG section, containing the declarations of the message handlers

Note In general, you shouldn’t manually edit any declarations that appear in these sections, or add code here. It is good style, and safe practice, to put any custom declarations in the appropriate group, but below the //}}AFX_ line.

Implementation File

Now examine the initial version of PenWidthsDlg.cpp. This file contains an empty message map and empty function definitions for the constructor and the DoDataExchange member function. For more information on the DoDataExchange function, see Map the Controls to Member Variables later in this lesson.

The CPenWidthsDlg.cpp constructor has a base initializer for CDialog. The CDialog constructor that it invokes creates a modal dialog box, and it takes two parameters: the ID of the dialog resource and a pointer to the parent window. For the first parameter ClassWizard has specified CPenWidthsDlg::IDD. This is an enumerated value that is defined in the AFX_DATA section in the class declaration. This enumerated value is equal to IDD_PEN_WIDTHS, the ID you specified earlier in the topic Create the Dialog Box. Thus the dialog class is associated with the dialog resource you created.

Also notice that the implementation file, like the header file, contains sections delimited by //{{AFX_ and //}}AFX_, into which ClassWizard will insert code later.


Declare a Message-Handling Function for a Dialog Box Control

The CDialog class, from which CPenWidthsDlg is derived, defines default handlers for the OK and Cancel buttons. The Pen Widths dialog box contains a third button, the Default button. For CPenWidthsDlg to respond when the user clicks this button, you must define a new message handler and bind it to the Default button.

Binding a message handler to a control in a dialog box is similar to binding a message handler to a menu command, which was described in Bind Scribble’s Clear All Command to Its Handler in Lesson 6; both can be accomplished using the New Windows Message Handler dialog box.

Adding the Message Handler for the Default Button

The following procedure assumes you have PenWidthsDlg.cpp open in the text editor.

To add a message handler for the Default button

  1. Using WizardBar, click CPenWidthsDlg in the Class combo box.

  2. Click the action button arrow, located on the right end of WizardBar.

  3. On the action menu, click Add Windows Message Handler.

    Note For a dialog class, the Class or object to handle box lists the IDs of all the controls in the dialog box, not the commands in a menu. Dialog classes also handle messages differently: the message being handled is a Windows control notification message, not an application-specific command. As a result, the New Windows messages to handle box displays more than just COMMAND and UPDATE_COMMAND_UI. It displays all the messages that can be sent by the object that’s highlighted in the Class or object to handle box.

  4. In the Class or object to handle box, select IDC_DEFAULT_PEN_WIDTHS. This is the ID of the Default button you created.

    The New Windows messages to handle box now shows all the notification messages that a pushbutton can send; namely, BN_CLICKED and BN_DOUBLECLICKED.

  5. In the New Windows messages to handle box, select the BN_CLICKED message.

  6. Click Add and Edit to create the function.

    The Add Member Function dialog box appears, displaying the candidate name, OnDefaultPenWidths. ClassWizard has synthesized this name from the object’s ID and the message name.

  7. Click OK to accept the function name offered by ClassWizard.

ClassWizard generates a starter function definition in PenWidthsDlg.cpp for the OnDefaultPenWidths message handler. Class Wizard also inserts the member function declaration into PenWidthsDlg.h, and an entry in the message map in PenWidthsDlg.cpp indicating that the member function OnDefaultPenWidths is the message handler called whenever the control IDC_DEFAULT_PEN_WIDTHS sends a BN_CLICKED message.

Right now the CPenWidthsDlg class doesn’t have any member variables defined; you will define those members in the next topic, Map the Controls to Member Variables. You will implement OnDefaultPenWidths later, in Implementing the Message Handler, after you’ve added the member variables.


Map the Controls to Member Variables

Suggested Reading

  • Using ClassWizard, Visual C++ User’s Guide

  • CWnd::DoDataExchange, Microsoft Foundation Class Reference

  • Defining Member Variables for DDX, Visual C++ Programmer’s Guide

  • ClassWizard and Dialog Data Validation, Visual C++ Programmer’s Guide

Scribble must be able to retrieve the values that the user enters in the Thin Pen Width and Thick Pen Width boxes. MFC defines a mechanism that automates the process of gathering values from a dialog box; this mechanism is called a “data map.” In the same way that a message map binds a user-interface element with a member function, a data map binds a dialog-box control with a member variable. The value of the member variable reflects the status or the contents of the control. By adding entries to CPenWidthsDlg’s data map, you can retrieve the values entered in the Thin Pen Width and Thick Pen Width boxes.

For Scribble, the widths of the thin and thick pens must be between 1 and 20. You can enforce these conditions by using the automated data validation that data maps provide. If the user enters values that fall outside this range, the application displays a message box stating the legal range and allows the user to enter new values.

To map the controls of the Pen Widths dialog box to member variables

  1. From the View menu, click ClassWizard and click the Member Variables tab.

    This tab, shown in the figure below, contains a list box displaying the mapping between controls and member variables.

  2. In the Class name list, select CPenWidthsDlg.

    At the moment the box displays only the IDs for the controls because you haven’t yet specified which member variables the controls correspond to.

  3. Select IDC_THIN_PEN_WIDTH and then click the Add Variable button.

    The Add Member Variable dialog box appears.

  4. In the Member variable name box, specify m_nThinWidth as the variable name.

  5. From the Variable type list box, click int.

  6. Click OK to add the member variable to the class.

    Notice the changes in the MFC ClassWizard dialog box:

    • The member name and type you specified now appear in the Control IDs list.

    • Description reads “int with validation”.

    • Two new edit boxes (Minimum Value and Maximum Value) appear to receive the validation parameters appropriate for an integer. These correspond to the edit boxes you added to the dialog box resource.
  7. In the Minimum Value and Maximum Value boxes, enter 1 and 20, respectively.

  8. Repeat steps 3 through 7 for the control IDC_THICK_PEN_WIDTH. Specify m_nThickWidth as the member name, click int, and enter lower and upper limits of 1 and 20.

  9. Click OK.

You’ve now completed the data map connecting the Pen Widths dialog box to the CPenWidthsDlg class. You can view the m_nThickWidth and m_nThinWidth member variables in ClassView under the CPenWidthsDlg class.

The Member Variables Tab

ClassWizard inserts declarations into the data map of PenWidthsDlg.h for the member variables you specified in the Add Member Variable dialog box.

ClassWizard also makes changes to PenWidthsDlg.cpp after you’ve mapped the controls to member variables. Examine these changes by opening PenWidthsDlg.cpp.

Notice that ClassWizard has initialized the member variables in the constructor and provided an implementation for DoDataExchange, which is a member function defined by CWnd (the base class of CDialog). The framework calls DoDataExchange whenever values have to be moved between the member variables in the class and the controls in the dialog box on screen (for example, when first displaying the dialog box on the screen or when the user closes the dialog box by clicking OK).

The DoDataExchange function is implemented using DDX and DDV function calls. A DDX (for Dialog Data eXchange) function specifies which control in the dialog box corresponds to a particular member variable and transfers the data between the two. A DDV (for Dialog Data Validation) function specifies the validation parameters for a particular member variable, ensuring that its value is legal. The DDX and DDV function calls shown in PenWidthsDlg.cpp reflect the mapping and validation parameters you specified with ClassWizard.

Notice that the DDV function call for a given member variable immediately follows the DDX function call for that variable. This is a rule you must follow if you choose to manually edit the contents of the data map.


Implementing the Message Handler

Recall that ClassWizard provided an empty function definition for the OnDefaultPenWidths message handler, which is called when the user clicks the Default button. Now that the CPenWidthsDlg class contains the necessary member variables, m_nThickWidth and m_nThinWidth, it’s time to fill in that function definition.

Suggested Reading in the Microsoft Foundation Class Reference

  • CWnd::UpdateData

The OnDefaultPenWidths function sets the contents of the edit boxes to the default widths of the thin and thick pens.

To implement the message handler for the Default button

  1. Use WizardBar to jump to the OnDefaultPenWidths function definition in PenWidthsDlg.cpp, and add the following code (delete the //TODO comments):
    m_nThinWidth = 2;
m_nThickWidth = 5;
UpdateData(FALSE);  // causes DoDataExchange()
// bSave=FALSE means don't save from screen, rather, write
// to screen
  2. Save your changes to PenWidthsDlg.cpp.

OnDefaultPenWidths sets m_nThinWidth and m_nThickWidth to their default values and then calls UpdateData, a member function defined by CWnd (the base class of CDialog).

The UpdateData member function calls the DoDataExchange function to move values between the member variables and the controls displayed on the screen. The direction in which the data values are moved is specified by the argument to UpdateData. The default value of this argument is TRUE, which moves data from the controls to the member variables. A value of FALSE moves data from the member variables to the controls. The OnDefaultPenWidths member function passes FALSE, causing the default values to be displayed in the edit boxes on the screen.


Open the Dialog Box

You’ve specified almost everything about the Pen Widths dialog box: its appearance, the data map for its edit controls, and the message handlers for its pushbuttons. Only one thing remains to be specified: when the dialog box should be opened.

Suggested Reading in the Microsoft Foundation Class Reference

  • CDialog::DoModal

  • CDialog::Create

Currently no programmatic connection exists between the Pen Widths menu item and the Pen Widths dialog box. That is, the menu item and the dialog box are not bound together. You must explicitly bind them by calling the Pen Widths dialog box from within the message handler for the Pen Widths command.

How do you open a dialog box? With the following two steps:

  1. Declare a CPenWidthsDlg object. This doesn’t display the dialog box on the screen; it just constructs the C++ object that manages the dialog box.

  2. Complete the OnPenWidths member function handler for the Pen Widths menu command.

To specify that the Pen Widths command displays the dialog box modally, you call the DoModal member function defined by the CDialog class. (To display a modeless dialog box, you would call the Create member function of CDialog.)

The DoModal function continues executing as long as the dialog box is displayed on the screen. When the user clicks the OK or Cancel button, the DoModal function returns IDOK or IDCANCEL, respectively, and the application can continue.

Before you write the message handler for the Pen Widths command, you need to decide which class should get the handler. In Lesson 6, in Add New Member Variables to Scribble, you added declarations for the m_nThickWidth and m_nThinWidth member variables to the CScribbleDoc class, because the document must keep track of the widths of the thick and thin pens (this allows multiple views to share the same pen widths). Since the document class must maintain those values, it should get the handler for the Pen Widths command.


Declaring the CPenWidthsDlg Object

In the following procedure you’ll use WizardBar to add a function handler for the OnPenWidths message and bind the function to its handler code, which is executed whenever the user clicks the Pen Widths command.

To declare the CPenWidthsDlg object

  1. Open ScribbleDoc.cpp in the text editor.

  2. Click the arrow on the action button on the right end of WizardBar.

  3. Click Add Windows Message Handler.

    The New Windows Message Handler dialog box appears.

  4. In the Class or object to handle list box, select ID_PEN_WIDTHS.

  5. In the New Windows messages to handle list box, select COMMAND.

  6. Click Add and Edit.

  7. In the Add Member Function dialog box, click OK to accept the candidate name “OnPenWidths.”

  8. In place of the highlighted //TODO comment, add the following code:
    CPenWidthsDlg dlg;
// Initialize dialog data
dlg.m_nThinWidth = m_nThinWidth;
dlg.m_nThickWidth = m_nThickWidth;

// Invoke the dialog box
if (dlg.DoModal() == IDOK)
{
// retrieve the dialog data
m_nThinWidth = dlg.m_nThinWidth;
m_nThickWidth = dlg.m_nThickWidth;

// Update the pen used by views when drawing new strokes
// to reflect the new pen widths for "thick" and "thin".
ReplacePen();
}
  9. Scroll to the top of ScribbleDoc.cpp and add the following #include statement:
    #include "PenWidthsDlg.h"
  10. Save ScribbleDoc.cpp.

    When you modify ScribbleDoc.cpp, you must include PenWidthsDlg.h so the message handler has access to the dialog class you’ve created.

The OnPenWidths function:

  11. Declares a CPenWidthsDlg object and sets the values of the m_nThickWidth and m_nThinWidth member variables to the current widths of the thick and thin pens.

  12. Calls the DoModal function, which displays the dialog box on the screen and takes control of the application until the user exits the dialog box. If the user exits the dialog box by clicking the OK button, the function changes the current thick and thin pen widths to the new values; if the user clicks the Cancel button, the old values are retained.

  13. Calls the ReplacePen member function to make the document’s pen use the current widths.

When does the application perform the data exchange and validation defined in the DoDataExchange function? Recall that DoDataExchange is called by the UpdateData member function. Just before the dialog box is first displayed on the screen, the framework calls the UpdateData function with an argument of FALSE, which sets the contents of the edit boxes to the values of the member variables. If the user exits the dialog box by clicking the OK button, the framework calls UpdateData with an argument of TRUE, which retrieves the contents of the edit boxes and sets the values of the member variables accordingly. (If the user exits by clicking the Cancel button, the framework doesn’t call UpdateData.)

You don’t have to handle the UPDATE_COMMAND_UI message for the Pen Widths menu item because the menu item doesn’t need to be updated. The command is never disabled since it’s always legal to change the widths of the pens, and there’s no need to add or remove a check mark because the command isn’t a toggle.


Build Scribble, Step 3

Build the new version of Scribble and examine how Scribble behaves with a dialog box.

To build and test Scribble Step 3 Version

  1. From the Build menu, click Build Scribble.exe.

    When prompted to build the files, click Yes.

  2. Run the new version of Scribble. Draw some strokes with the default thick pen and the default thin pen. Then use the Pen Widths dialog box to change the thickness of the pens and draw some new strokes. The following figure illustrates the third version of Scribble with a variety of strokes drawn.

    Scribble Version 3

  3. Exit Scribble.

This completes Step 3 of the tutorial.


Enhancing Views

Scribble: MDI Drawing Application, Lesson 8

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

Suggested Reading in the Microsoft Foundation Class Reference

  • CDocument::UpdateAllViews

  • CDocument Overview

  • CView::OnUpdate

  • CView Overview

In the previous lessons, you’ve seen how a view acts as an intermediary between a document and the user: The view displays a document on the screen and interprets mouse actions as operations on the document. You’ve also seen how a view cooperates with a frame window so that the frame window implements the generic window behavior while the view provides the application-specific functionality.

However, there are additional benefits to having a view class that is separate from the document and the frame window. This lesson describes how to take advantage of the division of labor between these classes to add special features to your application’s user interface by:

  • Updating multiple views on the same document.

  • Scrolling a view.

  • Splitting a window.

Note You can find a finished example of this lesson's code in the Scribble Step4 sample source code directory.


Updating Multiple Views

To illustrate some issues about updating multiple views, try the following:

  1. Run Scribble (your Step 3 version) and draw a few strokes in the open document.

  2. From the Window menu, click the New Window command.

  3. This opens a new document window displaying the same drawing. The document object now has two view objects connected to it.

  4. Click the Tile command so you can see both views at the same time.

  5. Add some more new strokes in the first window.

Do the new strokes appear in the other window simultaneously? No.

Why is this the case? Right now, Scribble has no way of telling each open document window what is happening in any other open document window. (This is illustrated in the figure below.) You could force a repaint — for instance, by minimizing and then restoring the window. Then its OnDraw function would display the drawing again, including the new strokes. But how can you ensure that all the views attached to a document reflect changes to the document as soon as they are made?

Multiple Views on a Document Without Updating

Each view must notify the other views whenever it has modified the document. MFC provides a standard mechanism for notifying views of modifications to a document through the UpdateAllViews member function of the CDocument class.

The UpdateAllViews function traverses the list of views attached to the document. For each view in the list, the function calls the OnUpdate member function of the CView class. The OnUpdate function is where the view responds to changes in the document; the default implementation of the function invalidates the client area of the view, causing it to be repainted. The simplest way for you to use this updating mechanism in your application is to call the document’s UpdateAllViews function whenever a view modifies a document in response to a user action.

You can also perform more efficient repainting with this updating mechanism if you use the parameters of the UpdateAllViews function. Here is the declaration of UpdateAllViews:

void UpdateAllViews(CView* pSender, LPARAM lHint = 0L,
                            CObject* pHint = NULL);

The first argument identifies the view that made the modifications to the document. This is specified to keep the UpdateAllViews function from performing a redundant notification. The view that made the modifications doesn’t need to be told about them. The second two arguments are “hints.” You can use these hints to describe the modifications that the view made.

The UpdateAllViews function gives the hints to every view attached to the document by passing them as parameters to the OnUpdate member function. You can override OnUpdate to interpret those hints and update only the area of the display that corresponds to the modified portion of the document. Thus, if another view is displaying a completely different portion of the document, it doesn’t have to perform any repainting at all.


Basic Tasks to Update Multiple Views

These are the basic tasks you'll do to inform other views of modifications:

  1. Define a type of hint that describes a modification to a document.

  2. When a view modifies the document, create a hint describing the modification made and pass it to UpdateAllViews.

  3. Override OnUpdate to use the hint so that only the portion of the screen corresponding to the modification gets updated.

The following topics describe these tasks in more detail.


Define a Hint for Scribble

When a stroke is added to a drawing in Scribble, the rectangular region that contains the new stroke is the only area that needs to be updated; the remainder of the drawing can be left alone. Therefore, a logical choice for a hint in Scribble is the bounding rectangle of the new stroke.

Instead of creating a separate class to represent the hint, it’s more convenient to pass a CStroke pointer as a hint. Store the bounding rectangle for each stroke in the CStroke object itself, so that it can be quickly referred to by OnUpdate to determine which area of the window needs to be repainted.

The following procedure assumes you have your Scribble project file (Scribble.dsw) open in the workspace.

To define bounding rectangles for strokes

  1. From ClassView, jump to the definition for class CStroke.

  2. In the Attributes section, add the following code, just after the m_pointArray declaration:
    protected:
CRect m_rectBounding;  // smallest rect that surrounds all
  // of the points in the stroke
public:
CRect& GetBoundingRect() { return m_rectBounding; }

    The protected member variable m_rectBounding is a CRect object storing the bounding rectangle, and the public member function GetBoundingRect allows the rectangle to be retrieved by the view.

  3. Now search ScribbleDoc.cpp for the IMPLEMENT_SERIAL macro and change the schema number parameter to 2.
    IMPLEMENT_SERIAL( CStroke, CObject, 2 )

    This version of Scribble changes what’s stored in a CStroke object by adding a new member variable. Changing the schema number distinguishes strokes saved by this version of Scribble from those of other versions.

  4. Go to the second CStroke constructor (the one that initializes the pen width) and add the following line:
    m_rectBounding.SetRectEmpty();

    This initializes the bounding rectangle to an empty rectangle in the constructor.

  5. Jump to the CStroke Serialize function and add the following line just after the first if condition:
    ar << m_rectBounding;

    This stores the m_rectBounding member variable in the archive.

  6. Add its code pair just after the else branch:
    ar >> m_rectBounding;

    This reads the m_rectBounding member variable from the archive.

In the next procedure, you’ll add a helper function, FinishStroke. This function calculates the bounding rectangle, which is needed for smart repainting.

To add the FinishStroke helper function

  1. From ClassView, point at the CStroke class icon and click the right mouse button.

  2. From the pop-up menu, click Add Function.

    The Add Member Function dialog box appears.

  3. In the Function Type box, type the return type (in this case, void).

  4. In the Function Declaration box, type the following:
    FinishStroke()
  5. In the Access area, select Public.

  6. Click OK.

    ClassWizard adds the declaration to the header file, creates a starter definition in the implementation file, and jumps you to the body of the definition so you can begin typing your application-specific code.

  7. Type the following code to fill in the function definition for FinishStroke:
    if( m_pointArray.GetSize() == 0 )
{
m_rectBounding.SetRectEmpty();
return;
}
CPoint pt = m_pointArray[0];
m_rectBounding = CRect( pt.x, pt.y, pt.x, pt.y );

for (int i=1; i < m_pointArray.GetSize(); i++)
{
// If the point lies outside of the accumulated bounding
// rectangle, then inflate the bounding rect to include it.
pt = m_pointArray[i];
m_rectBounding.left   = min(m_rectBounding.left, pt.x);
m_rectBounding.right  = max(m_rectBounding.right, pt.x);
m_rectBounding.top    = min(m_rectBounding.top, pt.y);
m_rectBounding.bottom = max(m_rectBounding.bottom, pt.y);
}

// Add the pen width to the bounding rectangle.  This is needed
// to account for the width of the stroke when invalidating
// the screen.
m_rectBounding.InflateRect(CSize(m_nPenWidth, m_nPenWidth));
return;

The FinishStroke member function calculates the bounding rectangle for a stroke. In this function, the stroke object iterates through its array of points, testing the location of each; if a point falls outside the current bounding rectangle, the stroke object enlarges the bounding rectangle just enough to contain it. Then the bounding rectangle is expanded on each side by the width of the pen.


Pass the Hint After Modifying the Document

The next task is to pass the hint to the document’s UpdateAllViews member function. You must pass a hint each time a stroke is completed.

To pass the hint after modifying the document

  • From ClassView, jump to the OnLButtonUp member function of class CScribbleView and add the following code, just before the ReleaseCapture function call:
    // Tell the stroke item that we're done adding points to it.
// This is so it can finish computing its bounding rectangle.
m_pStrokeCur->FinishStroke();

// Tell the other views that this stroke has been added
// so that they can invalidate this stroke's area in their
// client area.
pDoc->UpdateAllViews(this, 0L, m_pStrokeCur);

The OnLButtonUp member function is called when a stroke is finished, so you call UpdateAllViews from there. In this function, the view gets the hint information that it will send to the document. It does this by calling the FinishStroke member function for m_pStrokeCur; FinishStroke computes the bounding rectangle for the current stroke. Then the view calls UpdateAllViews, passing two arguments: the this pointer, which identifies this view as the one that performed the modification to the document, and m_pStrokeCur, whose bounding rectangle is the hint. (The function sends a pointer to the entire CStroke object rather than just the bounding rectangle because the hint must be a CObject pointer, and CRect isn’t derived from CObject.) The view doesn’t need to send any more hint information, so it passes nothing (0) in the LPARAM parameter.

The UpdateAllViews function iterates through the list of views attached to the document; for each view (except the one that performed the modification), the function calls its OnUpdate function and passes the hint as a parameter.


Use the Hint for Efficient Repainting

The last task is to take advantage of the hint so the other views can repaint themselves more efficiently. This involves modifying the CScribbleView class by overriding the OnUpdate function to respond to any hint it receives.

In Scribble Lesson 6, Binding Visual Objects to Code Using WizardBar, you saw how to use WizardBar to connect user-interface objects to their message-handler functions. You can also use WizardBar to override functions inherited from the base class, which are not attached to user-interface objects. The following procedure illustrates this point.

Note To use WizardBar to override OnUpdate, file ScribbleView.cpp must be open in the text editor.

To add the OnUpdate function to Scribble

  1. Use WizardBar to open ScribbleView.cpp in the text editor.

  2. Click the arrow on the action button, located on the right end of WizardBar.

  3. On the menu, click Add Virtual Function.

    The New Virtual Override dialog box appears.

  4. From the New Virtual Functions list, select OnUpdate.

  5. Click Add and Edit.

  6. Replace the //TODO comments with the following code:
    // The document has informed this view that some data has changed.

if (pHint != NULL)
{
if (pHint->IsKindOf(RUNTIME_CLASS(CStroke)))
{
// The hint is that a stroke has been added (or changed).
// So, invalidate its rectangle.
CStroke* pStroke = (CStroke*)pHint;
CRect rectInvalid = pStroke->GetBoundingRect();
InvalidateRect(&rectInvalid);
return;
}
}
// We can't interpret the hint, so assume that anything might
// have been updated.
Invalidate();
return;

    Remember, this function is called by the UpdateAllViews function of CScribbleDoc, which passes it a hint. In this function, the view checks if the hint is a CStroke object. If so, the view gets the bounding rectangle for the stroke and marks it as invalid. This rectangle marks the area that must be redrawn. If the hint isn’t a CStroke object, the view doesn’t know what area was modified, so it invalidates the entire client area as a precaution.

    After a region has been invalidated, Windows sends a WM_PAINT message. The OnPaint member function defined by CView handles this message by calling the virtual OnDraw member function. Consequently, you must modify the OnDraw function to take advantage of the invalidated rectangle when redrawing.

  7. From ClassView, jump to the OnDraw member function in class CScribbleView, and add the following code just after the ASSERT_VALID(pDoc) line:
    // Get the invalidated rectangle of the view, or in the case
// of printing, the clipping region of the printer DC.
CRect rectClip;
CRect rectStroke;
pDC->GetClipBox(&rectClip);

//Note: CScrollView::OnPaint() will have already adjusted the
//viewpoint origin before calling OnDraw(), to reflect the
//currently scrolled position.
  8. Then add the following code immediately following the CStroke* pStroke = strokeList.GetNext(pos) line:
    rectStroke = pStroke->GetBoundingRect();
if (!rectStroke.IntersectRect(&rectStroke, &rectClip))
continue;

    In the OnDraw function, the view first calls the GetClipBox member function of CDC to get the invalidated portion of the client area. Then the view iterates through the list of strokes in the document, calling IntersectRect for each to determine if any part of the stroke lies in the invalidated region. If so, the view asks the stroke to draw itself. Any strokes that don’t intersect the invalidated region don’t have to be redrawn.

Note This is a good point to compile your changes and test the window updating.

To test your update code

  1. Build and execute Scribble.

  2. Add some lines to the document window.

  3. From the Window menu, click New Window, then click Tile.

  4. Draw in either window and note that the application now correctly tracks the results in both windows.


Adding Scrolling

In the current version of Scribble, you cannot work on a drawing that is larger than the window. To allow the user to work on a large drawing, no matter how small the window is, Scribble must support scrolling.

The addition of scrolling expands the conceptual role played by a view. Not only does a view produce a visual representation of a document’s data, it also acts as a peephole to a document that may be too large to display all at once. This peephole can be moved across the document to reveal different portions of it. This is illustrated in the figure below.

A Scrollable View on a Document

Implementing scrolling from scratch is fairly complicated. However, since a lot of the scrolling code is the same for all applications, MFC implements the common scrolling logic in a class called CScrollView.


Basic Tasks for Adding Scrolling

The basic tasks for adding scrolling to your application are as follows:

  1. Define a size for your documents. This can be a constant, a member stored in each document object, or a value calculated at run time, for example.

  2. Derive your view class from CScrollView instead of CView.

    Note In the AppWizard – Step 6 dialog box, you have the option of changing your base class. You could have, for example, chosen CScrollView instead of CView at that point, thereby eliminating some of the steps in this procedure.

  3. Pass the document’s size to the SetScrollSizes member function of CScrollView whenever the size may change.

  4. Convert between logical coordinates and device coordinates if passing points between graphic device interface (GDI) and non-GDI functions.

The framework’s responsibilities are as follows:

  • Handle all WM_HSCROLL and WM_VSCROLL messages, scroll the document in response, and move the scroll box accordingly.

    The positions of the scroll boxes reflect where the currently displayed portion of the document resides relative to the rest of the document:

    • If the user clicks a scroll arrow at either end of the scroll bar, the document scrolls one “line” (whose meaning depends on the document type).

    • If the user clicks either side of the scroll box, the document scrolls one page.

    • If the user drags the scroll box itself, the document scrolls accordingly.
  • Calculate a mapping between the lengths of the scroll bars and the height and width of the document, adjust this scaling factor when the window is resized or when the size of the document changes, and in turn remove or add scroll bars as needed.

The figure below shows Scribble with scroll bars added.

Scribble with Scrolling Support


Add Scrolling to Scribble

The following procedures describe how to perform the first three tasks involved in adding scrolling to Scribble, as described in the previous topic, Basic Tasks for Adding Scrolling. In the topic, Working with GDI Coordinates, you’ll see how to perform the fourth task.

Suggested Reading in the Microsoft Foundation Class Reference

  • CDC::SetMapMode

  • CScrollView Overview

To add scrolling support to Scribble

  1. In ClassView, double-click CScribbleDoc.

    ScribbleDoc.h is displayed in the text editor.

  2. In the text editor, scroll to the //Attributes section and add the following code after public:
    protected:
CSize m_sizeDoc;
public:
CSize GetDocSize() { return m_sizeDoc; }

    This defines the size of Scribble documents by having each document store its dimensions. The member variable m_sizeDoc stores the size of the document in a CSize object. This member is protected, so it cannot be accessed directly by the views attached to the document. To let the views retrieve the size of the document, you provide a public helper function named GetDocSize. The views base their scrolling limits on the document size.

    Jump to the Serialize member function in CScribbleDoc and add the following line of code after the if branch, in place of the //TODO comments for storing:

    ar << m_sizeDoc;
  3. Add the matching line just after the else branch:
    ar >> m_sizeDoc;

    The changes to the Serialize member function store and read the m_sizeDoc member variable.

  4. In ClassView, double-click the member function InitDocument.

    ScribbleDoc.cpp is displayed in the text editor.

  5. In the text editor, add the following code after the call to ReplacePen:
    // Default document size is 800 x 900 screen pixels.
m_sizeDoc = CSize(800,900);

    The new code in the InitDocument member function initializes the m_sizeDoc member variable. Recall that you use this function whenever a new document is created or an existing document is opened. All Scribble documents are the same size: 800 logical units in width and 900 logical units in height. For simplicity’s sake, Scribble doesn’t support documents of varying size.

  6. In ClassView, double-click CScribbleView to return to the CScribbleView class definition and, in the text editor, specify that it be derived from class CScrollView (instead of class CView):
    class CScribbleView : public CScrollView

    Recall that MFC uses message maps as well as C++ inheritance. As a result, modifying the class definition in the header file isn’t enough to give CScribbleView all of CScrollView’s functionality. You also have to modify the message-map macros in the implementation file.

  7. In ScribbleView.cpp, change the reference to CView in the following lines to refer to CScrollView instead:
    IMPLEMENT_DYNCREATE( CScribbleView, CScrollView )

BEGIN_MESSAGE_MAP( CScribbleView, CScrollView )

    In the message-map macro, referencing CScrollView instead of CView instructs the framework to search CScrollView’s message map if it can’t find the message handler it needs in CScribbleView’s message map.

  8. If you want to use the diagnostic features provided by MFC, change the implementations of the Dump and AssertValid member functions of CScribbleView. These functions simply call their base class versions; change them to call the CScrollView versions rather than the CView versions.

These changes set the document’s scrolling limits according to the size of the document. By changing the base class of CScribbleView from CView to CScrollView, you give CScribbleView scrolling functionality without having to implement scrolling yourself.

Since Scribble documents are fixed in size, there is no need to make any subsequent calls to SetScrollSizes. If your application supports documents of varying size, you should call SetScrollSizes immediately after the document’s size changes. (You can do this from the OnUpdate member function of your view class.)

In addition to the changes just made, the CScribbleView class will override the OnInitialUpdate member function, which is called when the view is first attached to the document. By overriding this function, you can inform the view of the document’s size as soon as possible.

The following procedure describes how to do this.

To override OnInitialUpdate

  1. Use WizardBar to open ScribbleView.cpp in the text editor.

  2. Click the arrow on the action button, located on the right end of WizardBar.

  3. On the menu, click Add Virtual Function.

    The New Virtual Override dialog box appears.

  4. From the New Virtual Functions list, select OnInitialUpdate.

  5. Click Add and Edit.

  6. Add the following line of code just before the call to the OnInitialUpdate function in the base class:
    SetScrollSizes( MM_TEXT, GetDocument()->GetDocSize() );

The SetScrollSizes member function is defined by CScrollView. Its first parameter is the mapping mode used to display the document. The current version of Scribble uses MM_TEXT as the mapping mode; in Lesson 9, Enhancing Printing, Scribble will use the MM_LOENGLISH mapping mode for better printing. (For more information on mapping modes, see Enlarge the Printed Image in Lesson 9.)

The second parameter is the total size of the document, which is needed to determine the scrolling limits. The view uses the value returned by the document’s GetDocSize member function for this parameter.

SetScrollSizes also has two other parameters for which Scribble uses the default values. These are CSize values that represent the size of one “page” and one “line,” the distances to be scrolled if the user clicks the scroll bar or a scroll arrow. The default values are 1/10th and 1/100th of the document size, respectively.


Working with GDI Coordinates

Notice that the addition of scrolling didn’t require you to modify the OnDraw member function of CScribbleView. If the drawing function is unchanged, why does the window display different portions of the document depending on where the user has scrolled to? Because the document is displayed using coordinates relative to an origin used by GDI. When this origin was fixed at the upper-left corner of the client area, the part of the document that was visible was always the same. By moving the origin used by GDI, CScrollView can adjust which portion of the document is shown in the client area of the window and which portions are hidden.

The origin used by GDI is a characteristic of a device context; it is used by the member functions of the CDC class. If you want to make adjustments to the CDC object used by your view, you can override the OnPrepareDC member function defined by CView. CScrollView overrides OnPrepareDC to move the device context’s origin to reflect the currently scrolled position. OnPrepareDC is always called by the framework before it calls OnDraw. As a result, you don’t have to make any changes to the OnDraw function to draw a properly scrolled document; all the work needed to do scrolling is done to the device context before OnDraw receives it.

Note that changing the device context’s origin doesn’t affect the coordinates you receive with Windows messages such as WM_LBUTTONDOWN or WM_MOUSEMOVE; the points accompanying those messages are still specified in coordinates relative to the upper-left corner of the client area. This is because Windows messages are not part of a device context, so they are unaffected by changes to the GDI origin. Thus, CScribbleView must now deal with two types of coordinates:

  • The coordinates used for describing the points received with a mouse message. Those points are returned in “device coordinates.”

  • The coordinates used for drawing with GDI. These are known as “logical coordinates.”


Converting from Device Coordinates to Logical Coordinates

When storing the coordinates of strokes, Scribble needs to know where the strokes are relative to the document, not relative to the client area. Consequently, CScribbleView must convert points from device coordinates (relative to the window origin) to logical coordinates (relative to the document origin) before storing them in CStroke objects.

To store the strokes using logical coordinates

  1. Use ClassView to jump to the OnLButtonDown member function of class CScribbleView in ScribbleView.cpp and add the following code to the beginning of the function definition:
    // CScrollView changes the viewport origin and mapping mode.
// It's necessary to convert the point from device coordinates
// to logical coordinates, such as are stored in the document.
CClientDC dc(this);
OnPrepareDC(&dc); // set up mapping mode and viewport origin
dc.DPtoLP(&point);

    In this function, the view receives a point specified in device coordinates. A device context is needed to find the GDI origin, so the function declares a CClientDC object, which is a CDC object for the client area of the view, and calls OnPrepareDC to adjust its origin. Then the function passes the point to the DPtoLP (Device Point to Logical Point) member function of CDC to perform the actual conversion. The point added to m_pStrokeCur is thus described in logical coordinates (that is, relative to the document origin).

  2. Jump to OnMouseMove and add similar code just after the line CClientDC dc(this):
    // CScrollView changes the viewport origin and mapping mode.
// It's necessary to convert the point from device coordinates
// to logical coordinates, such as are stored in the document.
OnPrepareDC(&dc); // set up mapping mode and viewport origin
dc.DPtoLP(&point);

    This function already has a device context for drawing the stroke in progress, so the only modifications needed are to call OnPrepareDC to move the viewport origin and then DPtoLP to convert the point before adding it.

  3. Make the same modification to the OnLButtonUp member function, again, just after the line CClientDC dc(this):
    // CScrollView changes the viewport origin and mapping mode.
// It's necessary to convert the point from device coordinates
// to logical coordinates, such as are stored in the document.
OnPrepareDC(&dc);  // set up mapping mode and viewport origin
dc.DPtoLP(&point);

    Like OnMouseMove, this function already has a device context to complete drawing the stroke, so the only modifications needed are to call OnPrepareDC and then DPtoLP.

  4. Jump to OnUpdate and add the following lines of code just after the line CStroke* pStroke = (CStroke*)pHint:
    CClientDC dc(this);
OnPrepareDC(&dc);
  5. Skip a line (the CRect rectInvalid ... line) and add the following line:
    dc.LPtoDP(&rectInvalid);

    Unlike the previous three functions, OnUpdate requires a conversion in the opposite direction; that is, from logical coordinates to device coordinates. Recall that OnUpdate retrieves the bounding rectangle of a stroke and invalidates that rectangle. The stroke’s bounding rectangle is stored in logical coordinates. However, the rectangle passed to InvalidateRect must be specified in device coordinates (since InvalidateRect is not a GDI function). Accordingly, a stroke’s bounding rectangle must have its coordinates converted into device coordinates before it can be invalidated.

    The function declares a CClientDC object and then calls the OnPrepareDC member function to move the viewport origin of the device context to reflect the currently scrolled position. The rectangle is then passed to the LPtoDP (Logical Point to Device Point) function of CDC to convert its points into device coordinates. (Both DPtoLP and LPtoDP are overloaded to accept rectangles as well as points.) Once it is converted, the rectangle can be invalidated.

Note This is a good point to compile and test Scribble’s scrolling.


Adding Splitter Windows

Scrolling lets you work on a document that is larger than the window, but by the same token it means that much of the document is hidden at any one time. Suppose the user needs to refer to two widely separated portions of a document at the same time. The user could open another window on the same document and scroll both windows to different locations; however, windows must be resized individually so that they don’t overlap. A more convenient solution is to divide a window into separate “panes,” as shown in the figure below, each of which can display a different portion of the document.

A Window with Two Views on a Document

A window that can be divided into multiple panes is called a "splitter window." A splitter window contains split boxes at the top of the vertical scroll bar and at the left of the horizontal scroll bar. By double-clicking a split box, the user can divide a window vertically or horizontally into panes. The panes are separated by a "split bar"; each pane can be scrolled independently to display a different portion of the document. The user can also drag the split bar to resize both panes at once.

The following figure shows a Scribble window split into two panes.

Scribble Document Window Split into Two Panes

Each pane in a splitter window represents a separate view object. In the figure above, each pane is an instance of the CScribbleView class, but it’s not necessary for the panes to use the same view class. You can use different classes for different panes. This technique is useful when, for example, you want one pane to display an outline of a document while the other pane displays the full text.


How the Framework Supports Splitting

For a multiple-document interface (MDI) application such as Scribble to support splitting, objects of three classes must cooperate to display a document:

  • A CMDIChildWnd object, which manages the document window’s frame

  • A CSplitterWnd object, which manages the document window’s client area

  • One or more CScribbleView objects, each of which manages a pane in the window

The CSplitterWnd object is not visible as a distinct entity, but it is responsible for handling the CScribbleView objects as panes, managing their scroll bars, and drawing the split boxes and split bars.

This technique for managing splitter windows is similar to the implementation of MDI in general. A client window manages the entire client area, or workspace, of an MDI application’s frame window. It is this client window that owns the child windows that display documents.

Because you specified Scribble as an MDI application, AppWizard creates the CChildFrame class, derived from CMDIChildWnd. You’ll add the code to support splitter windows in Scribble to this class. If Scribble were a single-document interface (SDI) application, you would add the splitter window functionality directly to the CMainFrame class.

You can add splitter window functionality to your application in one of two ways:

  • You can click the Use Splitter Window option in AppWizard when you first create the application’s starter files. This method performs the tasks outlined below for you.

  • You can add this functionality manually, using ClassWizard to automate the process somewhat.

You’ll use the manual method for Scribble to learn how simply the framework implements this feature. The method for using AppWizard is described later in this lesson, in the topic Adding Splitter Window Functionality by Using AppWizard.


Basic Tasks to Add Splitter Windows

Study the following technique for adding splitter windows to an application:

  1. Derive a frame window class from CMDIChildWnd if you are writing an MDI application.

    Because you specified Scribble as an MDI application, AppWizard generated a CChildFrame class for you, derived from CMDIChildWnd.

  2. Give this class a member variable of type CSplitterWnd. (For SDI applications, add the member variable to your CMainFrame class.)

    This is the window that covers the frame window’s client area.

  3. Override the OnCreateClient member function of your frame window class to create a CSplitterWnd. (For SDI applications, the frame window class is CMainFrame.)

    The framework calls OnCreateClient when it first creates the frame window. For Scribble, you will override the OnCreateClient member function of CChildFrame.


Add Splitter Windows to Scribble

Adding splitter windows to Scribble requires two simple procedures:

  1. Add the member variable m_wndSplitter to class CChildFrame.

  2. Add the member function OnCreateClient to class CChildFrame.

After these procedures, you can build and run Scribble to test the splitter window functionality.

Suggested Reading in the Microsoft Foundation Class Reference

  • CSplitterWnd Overview

Optional Exercise When you select the splitter window option from AppWizard, AppWizard also generates a menu item, which the user can use instead of directly selecting the split box. To duplicate this feature, create a Split item on Scribble’s Window menu, and assign it an ID of ID_WINDOW_SPLIT. There’s no need to create a handler for it—the framework handles it automatically.

To declare the CSplitterWnd member variable

  1. In ClassView, point to the CChildFrame class icon and click the right mouse button.

  2. From the pop-up menu, click Add Member Variable.

    The Add Member Variable dialog box appears.

  3. In the Variable Type edit box, type: CSplitterWnd.

  4. In the Variable Declaration edit box, type: m_wndSplitter.

  5. In the Access area, click Protected.

  6. Click OK.

    You can view the new variable in ClassView under the CChildFrame class.

To add the OnCreateClient member function

  1. Use WizardBar to open ChildFrm.cpp in the text editor.

  2. Click the arrow on the action button, located on the right end of WizardBar.

  3. On the menu, click Add Virtual Function.

    The New Virtual Override dialog box appears.

  4. From the New Virtual Functions list, select OnCreateClient.

  5. Click Add and Edit.

  6. Replace the //TODO comments with the following code:
    return m_wndSplitter.Create(this,
2, 2,       // TODO: adjust the number of rows, columns
CSize(10, 10),  // TODO: adjust the minimum pane size
pContext);
  7. Save your work.

    You can view the new function in ClassView under the CChildFrame class.

In OnCreateClient, the frame window creates the window that will cover its client area by calling the Create function of its CSplitterWnd member variable. The following parameters passed to the Create function describe the panes that the splitter window will manage:

  • The first parameter passed to Create specifies the parent window for the client window. The function passes the this pointer, making the CScribbleFrame window the parent of the CSplitterWnd object (the member variable, m_wndSplitter).

  • The second and third parameters specify the maximum number of rows and columns that the splitter window can have. A value of two is used for each, so Scribble’s splitter windows can have up to four panes.

  • The fourth parameter specifies the minimum size of a pane: a square 10 logical units on a side.

  • The fifth parameter is the CCreateContext structure that is passed to OnCreateClient. This structure is used to determine which view class should be used for each pane in the splitter window.

The Create function accepts two additional arguments:

  • The sixth argument specifies the styles to be used for the splitter window. The default value specifies a visible child window with vertical and horizontal scroll bars that supports dynamic splitting.

  • The seventh argument specifies the ID to be assigned to the splitter window. Its default value is AFX_IDW_PANE_FIRST, which is the ID of the first pane.

Because Scribble doesn't pass any values for these, the default values are used.


Adding Splitter Window Functionality by Using AppWizard

The previous topic described how to add splitter window functionality to an MDI application that you had already developed. By performing the following procedure, AppWizard adds the splitter window functionality for you when you first create a project.

Note This procedure isn’t relevant to the version of Scribble you’ve developed to this point, as it requires starting over with AppWizard. It is included here for information only.

To add splitter windows by using AppWizard

  1. Start AppWizard and specify your preferences in the first three dialog boxes.

  2. In the AppWizard – Step 4 dialog box, click the Advanced button.

  3. Click the Window Styles tab.

  4. Click the Use Splitter Window option, and click Close.

  5. Finish choosing your options in AppWizard, and click OK in the New Project Information dialog box.


Build Scribble, Step 4

How does Scribble behave with these new enhancements? Build the new version and find out.

To build Scribble

  1. From the Build menu, click Execute Scribble.exe.

  2. Click Yes when prompted to build new versions of files.

Draw some strokes, scroll to a new portion of the drawing, and draw some more strokes. Resize the window and scroll back and forth. Double-click the split box, or drag it to split the window into two panes. With both panes displaying the same portion of the document, draw some strokes in one pane and see them reflected in the other one. The following figure shows this version of Scribble.

Scribble Version 4

Exit Scribble.

This completes Step 4 of the tutorial. You now have a basic understanding of the view architecture provided by the Microsoft Foundation Class Library.


Enhancing Printing

Scribble: MDI Drawing Application, Lesson 9

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Printing

Scribble has supported printing and print preview since Lesson 4, Creating the View, when you first added application-specific code to the starter files created by AppWizard. None of the code you added dealt specifically with printing.

In this lesson, you will improve Scribble’s current printing support. As it is now, the printed image is smaller than you might like, and the printed image is very plain. It doesn’t include a header or footer. This lesson describes how to enlarge the printed image and implement printing enhancements in your application.

This lesson covers the following topics:

  • Enhance Scribble’s printing

  • Enhance Scribble’s print preview

This lesson, Step 5 of Scribble, begins with the files you generated in Step 4. These files should closely resemble those in the Scribble/Step4 subdirectory.

Note You can find a finished example of this lesson's code in the Scribble Step5 sample source code directory.


Enhance Scribble's Printing

Enhance Scribble’s Printing

Step 5 of Scribble adds the following printing capabilities to the program:

  • Enlarges the printed image by using the MM_LOENGLISH mapping mode

  • Paginates a Scribble document

  • Adds a page header

These enhancements will invalidate previous Scribble file formats. To alleviate this issue, you’ll increment Scribble’s serialization.

This lesson’s topics describe these enhancements in detail.


Enlarge the Printed Image

Recall from Add Scrolling to Scribble, in Lesson 8, that when you specify a position for a GDI drawing function, you use logical coordinates. You learned how CScrollView moves the origin of this coordinate system. You can also control the scale of this coordinate system, that is, the physical size of a logical unit. By default, GDI considers logical units to be equal to device units, meaning that 1 logical unit equals 1 pixel on the screen. This interpretation of logical units is called the MM_TEXT mapping mode.

Since Scribble uses MM_TEXT mapping mode, it considers a stroke that is 100 units long to be 100 pixels long. The physical size of the stroke depends on the device that displays it. For example, a device unit on a typical laser printer is 1/300 of an inch, which is considerably smaller than a pixel on a typical screen. As a result, the images that Scribble produces on a printer are much smaller than those it displays on the screen.

To keep Scribble from producing tiny images on the printer, you need a mapping mode that ensures that a drawing remains the same size no matter what device displays it. Windows provides several such mapping modes, known as metric mapping modes. In these modes, GDI considers logical units to be equal to real-world units (or metrics), such as millimeters or inches.


Using the MM_LOENGLISH Mapping Mode

In Step 5, Scribble changes to MM_LOENGLISH mapping mode, which treats each logical unit as 0.01 inches. In this mode, a stroke that is 100 logical units long is drawn as 1 inch long, no matter which device is used; each device driver determines how many device units are needed to draw a 1-inch stroke.

Once Scribble uses MM_LOENGLISH mode, all coordinates used for GDI drawing are in hundredths of an inch, not pixels. As a result, the images that Scribble displays on the printer are the same size as the ones it displays on the screen. Recall that in Add Scrolling to Scribble, in Lesson 8, Scribble drawing was defined to be 800 logical units across and 900 logical units high; once you change the mapping mode, a drawing is 8 inches across and 9 inches high.

Another feature of MM_LOENGLISH mode (as well as the other metric mapping modes) is that its y-axis runs in the opposite direction to that in MM_TEXT mode. In MM_TEXT mode, y-coordinates increase when you move down, but in all the metric mapping modes, y-coordinates increase when you move up.

After you change the mapping mode, you need to make several adjustments to the existing Scribble code. These changes are described in the procedure To compensate for the reversal of the y-axis. For an explanation of the modifications, see Compensating for the Reversal of the Y-Axis later in this lesson.


Serializing Scribble's New Stroke Information

Serializing Scribble’s New Stroke Information

All of these code changes, including the change to the mapping mode, invalidate previous Scribble file formats. For example, the x and y-coordinates of earlier Scribble drawings were based on pixel locations. Now they are interpreted according to 1/100ths of an inch. Also, because of the metric mapping mode, the sign of the y-coordinate reverses, and what used to be a positive value is now negative. To compensate for this reversal, you’ll modify the m_rectBounding variable, which is a member of CStroke, the class that stores Scribble’s stroke data.

What does this mean? If the new version of Scribble tries to read in stroke data from an older version of Scribble, it will still try to draw the stroke, but the old stroke data might map to an area, for example, outside the current window.

Recall that, earlier in the tutorial, when you defined a hint for Scribble in Lesson 8, you modified the way Scribble stored strokes by adding the member variable, m_rectBounding. In that tutorial step, you incremented the schema number of the IMPLEMENT_SERIAL macro to distinguish between stroke data from different versions of Scribble. Because of the code changes you’ll make in following topics, you need to increment this schema number again.

Note After marking the version of Scribble data in this way, you’ll receive an error message “Unrecognized file format” if you try to load drawings from previous versions of Scribble. This is preferable to opening a drawing and seeing nothing on the screen.

To increment Scribble’s serialization

  • Open ScribbleDoc.cpp, search for the IMPLEMENT_SERIAL macro, and change the schema number parameter to 3.
    IMPLEMENT_SERIAL( CStroke, CObject, 3 )


Specify the Mapping Mode

This topic describes how to specify MM_LOENGLISH as the new mapping mode.

You specify the mapping mode in Scribble when you call the SetScrollSizes member function defined by CScrollView. Recall from Add Scrolling to Scribble, in Lesson 8, that this function sets the view’s scrolling limits. SetScrollSizes is called from the OnInitialUpdate member function of CScribbleView.

To specify the mapping mode

  1. Use WizardBar to jump to the OnInitialUpdate function in class CScribbleView.

  2. Replace MM_TEXT with MM_LOENGLISH as illustrated below.
    SetScrollSizes( MM_LOENGLISH, GetDocument()->GetDocSize() );

    Recall that OnInitialUpdate is called immediately after the view is attached to the document. This lets the view set its mapping mode before OnDraw is called.


Compensating for the Reversal of the Y-Axis

Even though MM_LOENGLISH mapping has changed the direction of the y-axis for drawing, most of the current Scribble code doesn’t require any modifications. This is because the DPtoLP function performs the conversion for you. (DPtoLP is called by the mouse event handler functions: OnLButtonDown, OnMouseMove, and OnLButtonUp.)

When a point is received with a mouse message, its coordinates are converted by the DPtoLP function before being stored in a CStroke object. This means its y-coordinates are converted from a positive number of pixels to a negative number of inches (1 pixel = .01 inch). Those coordinates are then passed to the LineTo drawing function, and then it’s up to the device driver for the screen to determine how many pixels are equivalent to the value that was passed in inches. You never have to directly examine the value of the coordinates.

However, there are some places where the reversal of the y-axis does have an impact. The mapping mode used by GDI is a characteristic of a device context. Functions that don’t use a device context are unaffected by the mapping mode. The member functions of the CRect class don’t use the mapping mode; consequently, you must make some adjustments wherever Scribble uses CRect functions.

To compensate for the reversal of the y-axis

  1. From ClassView, jump to the FinishStroke member function definition of class CStroke.

  2. Modify the code, as shown below, by reversing the min and max functions for the top and bottom of the bounding rectangle, and adding a cast to the y-axis m_nPenWidth parameter. These modifications take into account the negative sign of the y-coordinates.

    Note that the lines of code shown already exist ? you are just modifying them slightly, as described.

    void CStroke::FinishStroke()
{
// ...

m_rectBounding.top        = max(m_rectBounding.top, pt.y);
m_rectBounding.bottom    = min(m_rectBounding.bottom, pt.y);
}

// ...
m_rectBounding.InflateRect
(CSize(m_nPenWidth,-(int)m_nPenWidth));
return;
}

    You must also make a correction when using the invalid rectangle. Recall that the OnDraw member function checks whether the invalid rectangle intersects the bounding rectangle for each stroke. The IntersectRect member function of CRect assumes that the bottom of a rectangle must have a larger y-coordinate than that of the top; it cannot find the intersection of two rectangles whose bottoms have smaller y-coordinates than their tops.

  3. Use ClassView to jump to the OnDraw member function definition of CScribbleView, and add the following lines of code after the line pDC->GetClipBox(&rectClip):
    pDC->LPtoDP(&rectClip);
rectClip.InflateRect(1, 1); // avoid rounding to nothing
  4. Add the following lines of code after the line rectStroke = pStroke->GetBoundingRect():
    pDC->LPtoDP(&rectStroke);
rectStroke.InflateRect(1, 1);

    Both the invalidated rectangle and the bounding rectangle are converted to device coordinates (changing the signs of the coordinates to positive) before being tested for intersection. They are also inflated by one pixel in case they were rounded to nothing during the conversion.


Adding Pagination

Scribble is a fairly simple application, so it supports drawings of only one size, and each one fits on a single page. To illustrate pagination, Step 5 of Scribble prints each drawing as a two-page document: a title page, and the drawing itself.

To add pagination, you’ll:

  • Modify the OnPreparePrinting member function.

  • Override the default OnPrint member function.

  • Add two new helper functions: PrintTitlePage, which prints the title page, and PrintPageHeader, which prints a header on the drawing page.

To modify OnPreparePrinting

  • Use WizardBar to jump to the OnPreparePrinting function definition of class CScribbleView, and replace the existing comment and code with the following code:
    pInfo->SetMaxPage(2);  // the document is two pages long:
// the first page is the title page
// the second page is the drawing

This function specifies the length of the document by calling SetMaxPage for the pInfo parameter. Since all Scribble documents are two pages long, the function uses a numeric constant rather than a variable to represent the number of the last page of the document. The title page and the drawing page are numbered 1 and 2, respectively. Later, you’ll add a Scribble-specific version of the call to DoPreparePrinting that AppWizard generated (which you just replaced), when you enhance Scribble’s Print Preview. This function displays the Print dialog box and creates a device context for the printer.

To override OnPrint

  1. Use WizardBar to open ScribbleView.cpp in the text editor.

  2. Click the arrow on the action button, located on the right end of WizardBar.

  3. On the menu, Click Add Virtual Function.

    The New Virtual Override dialog box appears.

  4. From the New Virtual Functions list, select OnPrint.

  5. Click Add and Edit.

  6. Replace the //TODO comments and existing code with the following code:
    if (pInfo->m_nCurPage == 1)     // page no. 1 is the title page
{
PrintTitlePage(pDC, pInfo);
}
else
{
CString strHeader = GetDocument()->GetTitle();

PrintPageHeader(pDC, pInfo, strHeader);
// PrintPageHeader() subtracts out from the pInfo->m_rectDraw the
// amount of the page used for the header.
pDC->SetWindowOrg(pInfo->m_rectDraw.left,-pInfo->m_rectDraw.top);

// Now print the rest of the page
OnDraw(pDC);
}
return;

The behavior of the OnPrint member function depends on which of the two pages is being printed. If the title page is being printed, OnPrint simply calls the PrintTitlePage function and then returns. If it’s the drawing page, OnPrint calls PrintPageHeader to print the header and then calls OnDraw to do the actual drawing. Before calling OnDraw, OnPrint sets the window origin at the upper-left corner of the rectangle defined by m_rectDraw; this rectangle was reduced by PrintPageHeader to account for the size of the header. This keeps the drawing from overlapping the header.

Notice that the drawing itself isn’t divided into multiple pages. Consequently, OnDraw never has to display just a portion of the drawing (for example, it never has to display the section that fits on a particular page without displaying the surrounding sections). Either the title page is being printed and OnDraw isn’t called at all, or else the drawing page is being printed and OnDraw displays the entire drawing at once.

This also explains why CScribbleView doesn’t override the OnPrepareDC member function: there’s no need to adjust the viewport origin or clipping region depending on which page is being printed.


Adding the Helper Functions

The next step is to add the new helper functions. By using the Add Function pop-up menu command in ClassView, you can declare and define them in one step.

As mentioned, the PrintTitlePage function prints a title page, and PrintPageHeader prints a header on the drawing page.

To add the PrintTitlePage helper function

  1. In the ClassView, point to CScribbleView and click the right mouse button.

  2. From the pop-up menu, click the Add Function command.

  3. In the Function Type box, type the return type of the function (in this case, void).

  4. In the Function Declaration box, type the following:
    PrintTitlePage(CDC* pDC, CPrintInfo* pInfo)
  5. In the Access area, select Public, and click OK.

    ClassWizard adds the declaration to the Public section of the header file and creates a starter definition in the implementation file.

  6. Expand CScribbleView and double-click PrintTitlePage to jump to the body of the definition so you can begin typing your application-specific code.

  7. Fill in the starter PrintTitlePage function with the following code:
    // Prepare a font size for displaying the file name
LOGFONT logFont;
memset(&logFont, 0, sizeof(LOGFONT));
logFont.lfHeight = 75; //  3/4th inch high in MM_LOENGLISH
CFont font;
CFont* pOldFont = NULL;
if (font.CreateFontIndirect(&logFont))
pOldFont = pDC->SelectObject(&font);

// Get the file name, to be displayed on title page
CString strPageTitle = GetDocument()->GetTitle();

// Display the file name 1 inch below top of the page,
// centered horizontally
pDC->SetTextAlign(TA_CENTER);
pDC->TextOut(pInfo->m_rectDraw.right/2, -100, strPageTitle);

if (pOldFont != NULL)
pDC->SelectObject(pOldFont);

    The PrintTitlePage function uses m_rectDraw, which stores the usable drawing area of the page, as the rectangle in which the title should be centered.

    Notice that PrintTitlePage declares a local CFont object to use when printing the title page. If you needed the font for the entire printing process, you could declare a CFont member variable in your view class, create the font in the OnBeginPrinting member function, and destroy it in EndPrinting. However, since Scribble uses the font for just the title page, the font doesn’t have to exist beyond the PrintTitlePage function. When the function ends, the destructor is automatically called for the local CFont object.

To add the PrintPageHeader helper function

  1. In ClassView, point to CScribbleView and click the right mouse button.

  2. From the pop-up menu, click the Add Function command.

  3. In the Function Type box, type the return type of the function (in this case, void).

  4. In the Function Declaration box, type the following:
    PrintPageHeader(CDC* pDC, CPrintInfo* pInfo,
CString& strHeader)
  5. In the Access area, select Public, and click OK.

  6. Fill in the starter PrintPageHeader function with the following code:
    // Specify left text alignment
pDC->SetTextAlign(TA_LEFT);

// Print a page header consisting of the name of
// the document and a horizontal line
pDC->TextOut(0, -25, strHeader);  // 1/4 inch down

// Draw a line across the page, below the header
TEXTMETRIC textMetric;
pDC->GetTextMetrics(&textMetric);
int y = -35 - textMetric.tmHeight;        // line 1/10th in.
// below text
pDC->MoveTo(0, y);                        // from left margin
pDC->LineTo(pInfo->m_rectDraw.right, y); //  to right margin

// Subtract from the drawing rectangle the space used by header.
y -= 25;     // space 1/4 inch below (top of) line
pInfo->m_rectDraw.top += y;

    The PrintPageHeader member function prints the name of the document at the top of the page, and then draws a horizontal line separating the header from the drawing. It adjusts the m_rectDraw member of the pInfo parameter to account for the height of the header; recall that OnPrint uses this value to adjust the window origin before it calls OnDraw.


Enhance Scribble's Print Preview

Enhance Scribble’s Print Preview

The default print preview capabilities are almost sufficient for Scribble’s needs. To some extent, Scribble’s print preview has already been enhanced when the printing capabilities were enhanced. Recall that in the override of OnPreparePrinting you called the SetMaxPages function to specify the length of Scribble documents. This allows the framework to add a scroll bar to the preview window.

Another enhancement you can make is to change the number of pages displayed when preview mode is invoked.

To set the number of pages displayed in preview mode

  • Use WizardBar to jump to the OnPreparePrinting member function in ScribbleView.cpp. Then add the following code to the end of the function:
    BOOL bRet = DoPreparePrinting (pInfo);      // default preparation
pInfo->m_nNumPreviewPages = 2;        //Preview 2 pages at a time
// Set this value after calling DoPreparePrinting to override
// value read from registry
return bRet;

    The lines added here assign the value 2 to m_nNumPreviewPages. This causes Scribble to preview both pages of the document at once: the title page (page 1) and the drawing page (page 2). Note the value for m_nNumPreviewPages must be assigned after calling DoPreparePrinting, because DoPreparePrinting sets m_nNumPreviewPages to the number of preview pages used the last time the program was executed; this value is stored in the application’s registry entry.


Build Scribble, Step 5

What does Scribble’s printing look like now? Build the new version of Scribble and find out.

To build Scribble

  1. From the Build menu, click Execute Scribble.exe.

  2. When prompted to build the files, click Yes.

Run the new version of Scribble. Draw some strokes, and then click Print Preview from the File menu. Switch back and forth between one-page and two-page display mode, or move to the previous or next page. The following figure shows this version of Scribble.

Scribble Version 5

Exit Scribble.

This completes Step 5 of the Scribble tutorial.


Adding Context-Sensitive Help

Scribble: MDI Drawing Application, Lesson 10

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

Suggested Reading

  • Toolbars: Tool Tips, Visual C++ Programmer’s Guide

  • Control Bar Topics, Visual C++ Programmer’s Guide

  • Help: Authoring Help Topics, Visual C++ Programmer’s Guide

  • Technical Note 28, Context-Sensitive Help Support, Microsoft Foundation Class Reference

So far, thanks to the Microsoft Foundation Class Library (MFC), Scribble implements a number of common user-interface features, such as print preview and splitter windows. This lesson adds another such feature to Scribble: context-sensitive Windows Help.

Scribble already offers the user some help in the form of prompt strings and tool tips. When the user navigates through a menu by using the UP ARROW and DOWN ARROW keys, or uses the mouse to point to a toolbar button, Scribble displays a brief description of the command’s purpose in the status bar (if the status bar is visible). In addition, if the user holds the mouse cursor over a toolbar button, a small pop-up window (called a “tool tip”) appears with a brief description of the button.

Because prompts are attached to command IDs, Scribble’s toolbar buttons, which duplicate commands on the menus, automatically invoke the appropriate prompts.

The framework supplies this level of information for commands predefined by the class library. And, as you did in Lesson 5, in Add the Clear All Command to Scribble’s Edit Menu, you can add prompt strings and tool tips to the menu items you create by filling in a field in the menu’s property page.

The level of help described in this lesson, however, goes much further. By the end of this lesson, which covers Step 6 of Scribble, Scribble provides access to Windows Help from the Help menu, and to context-sensitive Help by pressing the F1 key or SHIFT+F1. When the user chooses the Help menu item, the Help file for Scribble opens, displaying the Help contents screen. Context-sensitive help invokes a Help topic specific to the area of the user interface with focus when F1 or SHIFT+F1 is pressed.


What This Lesson Covers

This lesson explains how to implement:

  • F1 help

  • SHIFT+F1 help mode

  • Help menu support

The next topic, What Does Context-Sensitive Help Consist of?, describes the three kinds of help listed here.

AppWizard provides this level of help support for free when you choose the Context-Sensitive Help option when initially creating your application starter files. For a quick preview of the results, follow the instructions described in See Context-Sensitive Help in Action later in this lesson.

Similarly to Lesson 8, Enhancing Views, this lesson shows how to add functionality supported by AppWizard to your application, if you didn’t originally choose it in AppWizard. In Adding Help to Scribble After the Fact later in this lesson, you’ll learn how to add Help support to Scribble, since you didn’t select the help option in AppWizard when you originally created your Scribble starter files. This will demonstrate in detail all the work AppWizard does for you to provide a robust Help system for your application.

Note You can find a finished example of this lesson's code in the Scribble Step6 sample source code directory.


What Does Context-Sensitive Help Consist of?

This topic provides more detail about the different levels of application help supported by the framework.

  • F1 help

    This level of help support enables the user to press the F1 key from an active window, dialog box, or message box, or with a menu item or toolbar button selected, to invoke a Help topic relevant to the selected item.

    For menu items, the user can use the arrow keys to highlight a particular menu item, and then press F1. For toolbar buttons, the user can use the mouse to hold down the button and press F1 before letting the button up.

    If no user interface object is selected, or if no specific Help topic exists, pressing F1 invokes the main Contents topic for the application Help file.

    Note You can define a key other than F1 for help, but it is common among applications for Windows to use F1.

  • SHIFT+F1 help mode

    This level of help support enables the user, from within an active application, to press SHIFT+F1 to put the application into “help mode.”

    While the application is in help mode, the cursor changes to an arrow beside a question mark. So long as help mode is active, the user can click any window, dialog box, message box, menu item, or toolbar button to summon help specific to the item. This invokes the application’s Help file, and ends help mode. Pressing ESC or switching away from the application and back also ends help mode.

    Note When you choose the context-sensitive Help option from AppWizard, the toolbar resource that AppWizard creates includes an additional button that the user can use to invoke help mode. The graphic on the button resembles the mouse cursor as it appears in help mode. The following figure shows the help mode button.

The Help Mode Button

In addition to context-sensitive Help, most applications provide help support through one or more menu items. For instance, most Windows applications include a Help menu item that invokes the application’s Help file when chosen. Additional items on the Help menu might, for example, display a Search dialog box.


Implementing Context-Sensitive Help with AppWizard

You can use AppWizard to enable automatically the framework’s support for context-sensitive Help and the Help menu. The following topics explain how to select this option in AppWizard, and what AppWizard creates as a result.

You’ll see how to add help support to Scribble later in Adding Help to Scribble After the Fact.

Note The following topic outlines the first step in implementing help support in Scribble.


The Context-Sensitive Help Option

This procedure describes how to select the Context-Sensitive Help option when you create a new application with AppWizard. After creating this starter project with AppWizard, you will copy sections to your Scribble application and modify them so Context-Sensitive Help works in Scribble.

To generate AppWizard application help support

  1. Create a new file.

  2. In the New dialog box, click the Projects tab. Select MFC AppWizard (exe) in the list box.

  3. Fill in or select the remaining boxes as follows:
    • Create New Workspace option button: activated.

    • Project Name box: HelpApp.

      AppWizard creates a project directory with this name under the main (root) directory specified in the Location box. The workspace configuration file and project makefile are based on this name, in this case, HelpApp.dsw and HelpApp.mak, respectively.

    • Location box: (Optional: the directory in which you want your project files stored. AppWizard creates this directory if it doesn’t exist).

    • Platforms check box: Win32.

      Clear any other entries. If your installation includes other language packages, they are represented by check boxes as potential platforms. Choosing check boxes other than Win32 might mean that options you need for this application are not available.

  4. Click OK.

    AppWizard creates the project directory, and the MFC AppWizard – Step 1 dialog box appears.

  5. Accept the default options in the dialog boxes for AppWizard Steps 1, 2, and 3 by clicking Next.

  6. In the MFC AppWizard – Step 4 dialog box, click the Context Sensitive Help option, then click Finish.

    The New Project Information dialog box appears, summarizing the settings and features AppWizard will generate for you when it creates your project.

  7. Click OK in the New Project Information dialog box.

    AppWizard creates all necessary files and displays the project in ClassView in the Project Workspace window. See Viewing the Starter Classes in Lesson 2.

You can freely use the Help files that AppWizard creates in your applications and freely ship the compiled help.

Note By default, AppWizard generates the Help Project file in Windows Help version 4.01 format.


Help Support Provided by AppWizard

In addition to the other features AppWizard provides in the starter application files, AppWizard adds the following items to support context-sensitive Help:

  • Message-map entries in your derived frame-window class (CMainFrame) for handler functions to handle Help menu items and F1 and SHIFT+F1 help. These handlers are predefined by the framework.

    To view the message-map entries, look in the message map of file MainFrm.cpp, under the \\global help commands section.

  • A new Help Topics item in the menu definitions, including a status bar prompt.

    To view the menu item, open ResourceView, expand the Menu folder, and double-click IDR_HELPAPTYPE. To view the status bar prompt string, right-click one of the menu items, then click Properties and examine the Prompt box in the Menu Item Properties page.

  • A Windows Help Project file with an .HPJ extension, named for your project.

    This file is located in the project \HLP directory. To view the .HPJ file, open FileView, expand the Source Files folder, and double-click HelpApp.hpj. You edit this file to reflect the changes you make to your application Help system.

  • One or more RTF (rich-text format) files (.RTF extension) containing help contexts and boilerplate text.

    These files are located in the project \HLP directory. To view any of these files, open them in Microsoft Word or other word processing application that views RTF files.

    You can add application-specific help contexts and text to these files to customize your application Help system, and add new topics or delete unused topics.

    You can fine-tune your application help further by overriding portions of the class library to support more specific help contexts, such as individual controls in a dialog box.

  • Several bitmap (.BMP) files used to display graphics in the Help system.

    These are located in the project \HLP directory. To view any of these files, open FileView and expand the Help Files folder. Double-click the file title to open it in the resource editor.

  • A .CNT file which contains the information needed to create the help Contents screen.

    Note When you first build the starter AppWizard application with context-sensitive Help support, the Help files are compiled for you, so that you can immediately take advantage of this feature. Once you customize the Help files, you must recompile them to incorporate your changes into the application project. For more information, see Compiling Your Help Files later in this lesson.


See Context-Sensitive Help in Action

Once you’ve created an AppWizard application with the Context-Sensitive Help option, it’s easy to try out the help support provided by the framework and AppWizard.

To try out the help support

  • Build the HelpApp application you created in the procedure To generate AppWizard application help support.

    From the Build menu, click Rebuild All. The first time you build this application, the development environment automatically compiles the AppWizard-generated Help Project files as part of the project build process. You don’t need to compile them separately until you begin modifying them.

Once the project has been built, you can run the application and try out the various help options. Here are some suggestions for what to try:

  • Choose Help Topics from the Help menu.

    This invokes the application Help file generated for you by AppWizard, displaying the Contents tab of the Help Topics dialog box. The Contents tab already contains two top-level topics: Menus, which expands to display the menu topics AppWizard generated for the application; and a place-holder topic that you will replace with your application-specific topics.

  • Expand the top-level Menus topic.

    You’ll see all the topics AppWizard provided for you. They describe the standard menus that the framework provides. Continue choosing topics for a particular menu, such as the File menu, until you see a topic for a specific menu item or dialog box, such as the Print Preview command or the Print Setup dialog box. You’ll see that the Help file is already quite robust.

  • Click the Index tab.

    Select an index entry, and then click Display. Again you’ll see that the skeleton Help file already contains much relevant information, implemented in a useful help structure.

  • Click the Contents tab and then select the Find tab.

    Follow the simple instructions for the Find Setup Wizard, which sets up the full-text search capabilities of the Windows help engine. Once finished, your simple Help system suddenly becomes more sophisticated — the user can type in words or phrases, and if there is a match in any of the AppWizard-generated Help topics, that topic appears in the topic list. Click Display to see the topic.

    When you add new files to your Help system, recompile, and run the Find Setup Wizard again, the Windows Help engine re-creates this full-text search index, incorporating the text in your new topics seamlessly.

  • Click the help-mode button on the HelpApp toolbar, which appears as an arrow beside a question mark.

  • Open a menu and select a menu item by using the DOWN ARROW key. Then press the F1 key to get help for the selected item.

  • Press and hold down a toolbar button, and press F1 before releasing the toolbar button.


Compiling Your Help Files

AppWizard provides you with a robust help package: .RTF files that already document the standard (AppWizard-generated) menus, a help project (.HPJ) file, a Help contents topic (.CNT) file, and the other resources needed to generate an application with a fully functional online Help system.

Suggested Reading

  • Help: Authoring Help Topics, Visual C++ Programmer’s Guide

  • Setting Directories, Visual C++ User’s Guide

You will customize the AppWizard-generated files to suit your particular application as you develop the application. When you compile and build your developing application, you must also recompile your project Help files so they continue to reflect your customizations.

See Help: Compiling and Testing Your Help File.


Compiling Your Help Files from Within Visual C++

Compiling Your Help Files from Within the Visual C++ Environment

For this option to work, the executable files called by the custom build rules on the .hpj, .cnt and resource.h files must be in a directory listed in the Directories tab of the Options dialog box. (Click Options from the Tools menu.) By default, these files are installed to the \BIN directory of your Visual C++ installation and included in the Options dialog box, so unless you modify either of the default settings, your Help compilation will be a completely transparent part of building your project files.

To compile your Help files from within the development environment

  1. Open FileView and expand the Source Files folder.

  2. Select your help project (.HPJ) file. (You needn’t open the file; but it must remain selected while you perform the next step.)

  3. From the Build menu, click Compile HelpApp.hpj.

    This invokes the custom build rules on the .hpj, .cnt and resource.h files, which in turn calls the following two programs:

    • MAKEHM.EXE, a program that incorporates your context-sensitive topic IDs

    • The Windows Help Compiler, which builds your .HLP file


Compiling Your Help Files from the Command Line

To compile your application Help from the command line, the Windows Help Compiler and MAKEHM.EXE must be in your path statement. To ensure that this is the case (and to set other relevant environment variables for a specific build target), run the VCVARS32 batch file.

See Help: Compiling and Testing Your Help File.


Adding Help to Scribble After the Fact

This topic explains how to add context-sensitive Help to an application when you did not originally choose it as an AppWizard option.

Suggested Reading in the Visual C++ User's Guide

  • Overview: Menu Editor

  • Overview: Accelerator Editor

  • Overview: String Editor

  • Overview: Toolbar Editor

Merging context-sensitive Help support into Scribble at a later stage requires several general steps. Each step is explained in more detail in the following topics. The overall steps are:

  1. Run AppWizard and choose the context-sensitive Help option to create a new application that has the help-related files and code.

    See the procedure To generate AppWizard application help support. You’ll use this skeleton application to copy code and resources into Scribble.

  2. Copy resources from this application to Scribble.

    See the topic Copying Help-Specific Resources to Scribble.

  3. Copy help-related files and code from the application to Scribble.

    See the topic Copying the Help-Related Code and Files to Scribble.

  4. Customize the contents of the files you copied from HelpApp.

    See the topic Customizing the Help Files and Code for Scribble.

  5. Build the new version of Scribble and compile its Help file.

    See the topic Completing Scribble’s Help Implementation.


Copying Help-Specific Resources to Scribble

In the following procedures, you’ll copy menu items, accelerator keys, and status-bar prompt strings from HelpApp (the project you created with AppWizard help support) to Scribble. As you do this, you’ll not only learn about adding help to an application after the fact, you’ll also learn how easy it is to copy resources from one resource file to another. Finally, you’ll add a toolbar button to Scribble’s toolbar resource, just as you did in Lesson 5, Constructing the User Interface.

Note If you have not already created HelpApp, refer to the procedure To generate AppWizard application help support, earlier in this lesson. This is the first step in implementing help support in Scribble.

In the following procedures, you’ll use the menu editor, accelerator editor, string editor, and toolbar editor


Copying the Help Menu Resources

First, copy Help-related menu resources from HelpApp to Scribble.

To copy Help menu resources to Scribble

  1. Open the files Scribble.rc and HelpApp.rc, which are located in their respective project directories. These files display the contents of the ResourceView panes for both Scribble and HelpApp.

  2. Open the IDR_MAINFRAME menus from both resource files.

    Arrange the menu editor windows so that they don’t overlap.

  3. Open both Help menus.

  4. Click the Help Topics menu item in the HelpApp Help menu. Then hold down the SHIFT key while you click the separator below the Help Topics item. Release the SHIFT key.

    This selects the menu item and separator.

  5. Hold down the CTRL key and use the mouse to drag the highlighted menu items to the Help menu in Scribble.rc, above the About Scribble menu item. Release the mouse button and the CTRL key.

    This copies the menu item and the separator to Scribble.

  6. Close the two IDR_MAINFRAME menu editing windows.

  7. Repeat steps 3 through 6, using the application-specific menu resources in Scribble and HelpApp: IDR_SCRIBBTYPE and IDR_HELPAPTYPE, respectively.


Copying the Help Accelerator Resources

Next, copy the accelerator resources for the menu items.

To copy the accelerator keys for Help resources

  1. Open the Accelerator table resource, IDR_MAINFRAME, from both Scribble and HelpApp, again resizing the accelerator editor windows so that they do not overlap.

  2. Select the ID_HELP command from the HelpApp Accelerator table.

  3. Hold down the CTRL key while you drag this entry to the Scribble accelerator editor window.

    It doesn’t matter where in the window you drop the entry. This copies the accelerator keys (F1 and SHIFT+F1) for the Help menu item to Scribble.

  4. Repeat this procedure for the ID_CONTEXT_HELP command.

    This maps to the help mode toolbar button, described later in this lesson, in Copying the Help Mode Toolbar Button.

  5. Close both accelerator editor windows.


Copying the Help-Related String Resources

In this procedure you copy the command IDs for Help-related menu items.

To edit Help-related string resources

  1. Open the String Table resource for both Scribble and HelpApp, resizing both editor windows so they do not overlap.

  2. Locate the AFX_IDS_IDLEMESSAGE string in both tables, noting the difference in the captions.

    For an application without help, AppWizard defines the default status-bar prompt to be “Ready”. This is the string displayed in the status bar when no other command prompt is being displayed. This string resource is identified as AFX_IDS_IDLEMESSAGE.

  3. Select AFX_IDS_IDLEMESSAGE in Scribble’s string table, and click Properties from the Edit menu to open the String Properties window.

  4. Edit the Caption so that it matches that in the HelpApp version of this string resource: For Help, press F1.

  5. Next, copy the following strings from the HelpApp String Table to Scribble’s String Table:

    AFX_IDS_HELPMODEMESSAGE, ID_CONTEXT_HELP, and ID_HELP

    The copying procedure is similar to the procedures for copying menus and accelerators. To copy several contiguous strings, hold the SHIFT key down while selecting the strings. Then hold the CTRL key down while dragging the selected strings to the new window.

    The AFX_IDS_HELPMODEMESSAGE prompt displays in the status bar while the application is in help mode. The ID_CONTEXT_HELP prompt displays when the mouse pauses over the help mode toolbar button.

    Notice that Scribble’s string table already contains the ID_HELP_FINDER string, representing the prompt that displays for the Help Topics command on the Help menu. This was copied when you copied the Help Topics menu item.

    The ID_HELP command is called when the user presses F1.

  6. Close the String Table resources.


Copying the Help Mode Toolbar Button

When you generate context-sensitive Help from AppWizard, AppWizard creates a button on the toolbar which, when chosen, places the application in Help mode, just as pressing SHIFT+F1 does.

In this procedure you’ll copy the Help mode button to Scribble’s toolbar resource from HelpApp’s toolbar resource, simply by dragging it.

To copy the Help mode toolbar button

  1. Using ResourceView, open Scribble’s toolbar resource and HelpApp’s toolbar resource, resizing both editor windows so they do not overlap and so you can see both subject toolbars.

  2. Press CTRL and drag the Help mode button from HelpApp’s toolbar onto Scribble’s toolbar.

  3. Save the Scribble resource file, Scribble.rc, and close the HelpApp resource file, HelpApp.rc.


Copying the Help-Related Code and Files to Scribble

AppWizard generates not only help-specific code, but files that are used to build the application’s Help system. In the following topics, you’ll copy those files and the Help-related code from HelpApp into the Scribble project.


Copying the Help Message Map Commands

AppWizard places Help-specific code into the message map in the frame window class implementation file, MainFrm.cpp.

To copy help-related code to Scribble

  1. Open the HelpApp project MainFrm.cpp file and your Scribble project version of MainFrm.cpp.

  2. Scroll to the Help-related code, delineated by the //global help commands comment, in the message map section of the HelpApp MainFrm.cpp file.

  3. Copy this code, shown below, from the HelpApp MainFrm.cpp file, and paste into the same position in the message map in Scribble’s MainFrm.cpp.
    // Global help commands
ON_COMMAND(ID_HELP_FINDER, CMDIFrameWnd::OnHelpFinder)
ON_COMMAND(ID_HELP, CMDIFrameWnd::OnHelp)
ON_COMMAND(ID_CONTEXT_HELP, CMDIFrameWnd::OnContextHelp)
ON_COMMAND(ID_DEFAULT_HELP, CMDIFrameWnd::OnHelpFinder)

    You’ve already seen how ID_HELP_FINDER and ID_CONTEXT_HELP are called. When the user presses F1, the framework calls ID_HELP and directly handles this command so long as the application contains help support.

  4. Close the HelpApp project’s MainFrm.cpp file, and save the changes to Scribble’s MainFrm.cpp.


Copying the Help-Related Files to Scribble

To perform the following procedure, use Windows Explorer or another file management utility.

To copy help-related files to Scribble

  1. Copy the entire HELPAPP\HLP directory to the Scribble project directory.

    The files contained in the \HLP directory include, among others:

    • The Help Project (.HPJ) file, which tells the Help compiler how to build the Help file.

    • A file with the extension .CNT, needed to display the Help Contents screen.

    • A file with the extension .HM, created during the process of compiling the Help file, needed to provide context-sensitive Help for user interface objects. (If you did not build the HelpApp project, this file won’t have been created yet.)

    • Several bitmap (.BMP) files used to display graphics in the Help system.
  2. In the Scribble project \HLP directory, rename HelpApp.hpj to Scribble.hpj; HelpApp.hm to Scribble.hm (if this file exists); and HelpApp.cnt to Scribble.cnt.

    The bitmap and .RTF files do not need to be renamed.

  3. Copy Pen.rtf from the Scribble\Step6\HLP source code directory to your corresponding Scribble directory.

    This file contains the Scribble-specific Help Contents topic and Help topics for Scribble’s Pen menu.

    Note If you have not made a local copy of the sample source code for Scribble Step 6, see Installing the Sample Files.


Customizing the Help Files and Code for Scribble

Once the Help-specific files are copied and renamed, you need to modify their contents to refer to the Scribble project instead of to HelpApp. You can perform the following procedure in the text editor window.

To customize the Help files for Scribble

  1. Open the Scribble copy of Scribble.cnt in the text editor window and change all occurrences of the string “HelpApp” to “Scribble.”

  2. Save and close Scribble.cnt .

  3. Repeat step 1 for Scribble.hpj (located in the \HLP directory).

    Keep both files open.

  4. In Scribble.cnt, add an entry in the menu structure for the Pen menu, between the Edit and View entries:
    2 Pen menu=menu_pen
  5. Save and close Scribble.cnt.

  6. In Scribble.hpj, make the following changes:
    • Under the [FILES] section, add the line
    Pen.rtf

    This adds the file, Pen.rtf, to your Help project.

    • Under the [OPTIONS] section, change “CONTENTS=Main_index” to “CONTENTS=new_index”.

      Main_index is the context string for the Help Contents topic that AppWizard created; replacing it with new_index calls the topic, in Pen.rtf, for Scribble’s Help Contents screen instead.

    • Under the [ALIAS] section, change the string “HIDR_MAINFRAME = main_index” to “HIDR_MAINFRAME = new_index”; and change “HIDR_HELPAPTYPE” to “HIDR_SCRIBBTYPE”.


Scribble's Help Project File

Scribble’s Help Project File

Here’s how the edited Scribble.hpj should look:

[OPTIONS]
CONTENTS=new_index
TITLE=Scribble Application Help
COMPRESS=true
WARNING=2
BMROOT=..,.
ROOT=..,.

[FILES]
afxcore.rtf
afxprint.rtf
pen.rtf

[ALIAS]
…
[MAP]
#include c:\program files\\DEVSTUDIO\VC\MFC\INCLUDE\afxhelp.hm
#include <Scribble.hm>

Note If your product installation is in another directory, change the directory entry in the [MAP] section accordingly.


Completing Scribble's Help Implementation

Completing Scribble’s Help Implementation

The first time you build an AppWizard-generated project with Help support, Help project files are built for you. Thereafter, if you change the Help files, you must recompile them just as you would any other project file. As described earlier in this lesson, in Compiling Your Help Files, there are two ways to do this; you can either run a batch file from the command prompt, or compile the Help Project file from within the development environment.

To compile Scribble’s Help Project file from within the development environment, you must first perform two steps:

  • Add Scribble.hpj and the HLP directory to the Scribble project

  • Specify Custom Build settings

Both of these steps are optional: you can compile Scribble’s Help files from the command line without any further customizations. (Note that AppWizard performs these steps for you automatically when you choose the context-sensitive Help option.)


Compiling Scribble's Help from the Development Environment

Compiling Scribble’s Help from the Development Environment

You can easily prepare Scribble to compile the new Help files from within the development environment, by performing the following two procedures.

To add Scribble.hpj to the project

  1. With your Scribble project open, from the Project menu, click Add to Project. Then click Files.

  2. In the Insert Files into Project dialog box, navigate to the \HLP directory underneath the Scribble project root directory and specify Scribble.hpj.

  3. Click OK.

    The file now appears under Source Files in FileView.

To add the HLP directory to the project

  1. Select the name of the project (Scribble) in File View.

  2. Right click and select New Folder from the shortcut menu.

  3. Name the new folder Help Files.

  4. Right click on the Help Files folder and select Add Files to Folder.

  5. From the Insert Files into Project dialog box, select all the files in the HLP directory of the Scribble project.

When AppWizard adds context-sensitive Help support, it implements special build options for the application’s Help Project file. You can duplicate this functionality by specifying Custom Build settings for Scribble.hpj, Scribble.cnt and resource.h. Once you complete this step, you can build Scribble.hpj from within the development environment.

To specify custom build settings

  1. From the Project menu, choose Settings.

    The Project Settings dialog box opens.

  2. In the Settings For area, expand the Scribble icon and the Source Files folder and select Scribble.hpj.

    You can simultaneously select Scribble.hpj in both the Debug and Release project build types by clicking the combo box located above the Settings For area and clicking All Configurations.

  3. Click the Custom Build tab.

  4. In the Build Command(s) box, copy the following custom build rules into the Commands box:
    start /wait hcw /C /E /M "hlp\$(InputName).hpj"
if errorlevel 1 goto :Error
if not exist "hlp\$(InputName).cnt" goto :Error
if not exist "hlp\$(InputName).hlp" goto :Error
copy "hlp\$(InputName).hlp" $(OutDir)
goto :done
:Error
echo "hlp\$(InputName).hpj(1)" : error: Problem encountered creating help file
type "hlp\$(InputName).log"
:done
  5. In the Output File(s) edit box, type the following:
    $(OutDir)\$(InputName).hlp
  6. Select the Resource.h file and copy the following custom build rules into the Commands box:
    echo. >>"hlp\$(TargetName).hm"
echo // Commands (ID_* and IDM_*) >>"hlp\$(TargetName).hm"
makehm ID_,HID_,0x10000 IDM_,HIDM_,0x10000 resource.h >>"hlp\$(TargetName).hm"
echo. >>"hlp\$(TargetName).hm"
echo // Prompts (IDP_*) >>"hlp\$(TargetName).hm"
makehm IDP_,HIDP_,0x30000 resource.h >>"hlp\$(TargetName).hm"
echo. >>"hlp\$(TargetName).hm"
echo // Resources (IDR_*) >>"hlp\$(TargetName).hm"
makehm IDR_,HIDR_,0x20000 resource.h >>"hlp\$(TargetName).hm"
echo. >>"hlp\$(TargetName).hm"
echo // Dialogs (IDD_*) >>"hlp\$(TargetName).hm"
makehm IDD_,HIDD_,0x20000 resource.h >>"hlp\$(TargetName).hm"
echo. >>"hlp\$(TargetName).hm"
echo // Frame Controls (IDW_*) >>"hlp\$(TargetName).hm"
makehm IDW_,HIDW_,0x50000 resource.h >>"hlp\$(TargetName).hm"
  7. In the Output File(s) edit box, type the following:
    hlp\$(TargetName).hm
  8. Select the project’s .cnt file and copy the following custom build rules into the Commands box:
    copy "hlp\$(InputName).cnt" $(OutDir)
  9. In the Output File(s) edit box, type the following:
    $(OutDir)\$(InputName).cnt
  10. Click OK.

    You can now compile Scribble.hlp from the development environment anytime you need to update the contents of the Help Project files.

To compile Scribble Help from within the development environment

  1. After completing the previous two procedures, in FileView, expand Source Files and select Scribble.hpj.

  2. From the Build menu, click Compile Scribble.hpj.

You can also simply build Scribble.exe at this point to see Scribble running with full application Help support.

To try out Scribble's Help

  1. From the Build menu, click Execute Scribble.exe. Click Yes when prompted to build.

  2. From the Help menu, click Help Topics.

  3. In the Contents tab, expand the Menus heading, and double-click the Pen menu topic.

    This is the topic you added to Scribble’s Help system by editing the .CNT and .HPJ files. You’ll see the custom Help that has been provided in Pen.rtf.

  4. Return to the Scribble application, and press SHIFT+F1 to enter Help mode, then click one of the items on Scribble’s Pen menu.

    Help mode, invoked by pressing SHIFT+F1, is another way for users to access Scribble application Help topics.

  5. Click the Index and Find commands in the Help Topics dialog box to see the Windows Help interface.


Compiling Scribble's Help from the Command Line

Two files must be in your path statement: the Windows Help Compiler (HCW.EXE) and MAKEHM.EXE, a file which incorporates your context-sensitive topic IDs into the built Help system. These files are copied to your installation \BIN directory as part of a standard setup.

To compile Scribble’s Help from the command line

  1. See Help: Compiling and Testing Your Help File.

  2. In the development environment, rebuild Scribble.

This concludes Scribble Step 6.


Creating an OLE Server

Scribble: OLE Server Application, Lesson 1

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

Suggested Reading in the Win32 SDK

  • Containers and Servers

An OLE in-place editing server application can create OLE items that can be embedded or linked into container applications. Some server applications support only the creation of embedded items, while others support the creation of both embedded and linked items.

A container application must be able to start a server application when the user wants to edit an item. If a server application supports linked items, it must also be able to copy its data to the Clipboard so that a container can use that data to create OLE items.

An application can be both a container and a server; that is, it can both incorporate external data into its documents and create data that can be incorporated as items into the documents of other applications.

Scribble Step 7 addresses two general cases for adding OLE server support to an application:

  • Creating a new OLE in-place editing server application from scratch

  • Adding OLE in-place editing server support to an existing application

The technique described in this lesson illustrates both cases, even though the tutorial starts with an existing MFC application, Scribble Step 6, to which you will add OLE server support. For more information on Scribble, see Lessons 1 through 10.

As when adding support for context-sensitive help to Scribble Step 6, you will use AppWizard to provide a starter OLE server application in a scratch directory. Then you will copy files and code fragments from the scratch directory to the existing Scribble code base. By doing this, you will learn about the OLE server code that AppWizard provides. Therefore, even if you are starting a new MFC OLE server application from scratch, you are advised to read this tutorial, if not actually do the steps.

How does this approach differ from the traditional approach of copying source code from a sample application? AppWizard allows you to customize the sample code you will be borrowing. That is, when you create the scratch application, you will name it “Scribble,” give the classes the same names Scribble itself uses, and so on. Thus, when you copy source code from the AppWizard-created sample application, it will match the class names of your original application. You can use this approach to add other AppWizard-supported features to your existing MFC applications “after the fact.”


Previewing Scribble Running as an OLE Server

Before working through the steps of adding OLE server support to Scribble, try out the completed application. This will help you appreciate how Scribble behaves when it is activated by an ActiveX container.

The ActiveX container application you will use is the completed Container tutorial project (you’ll build it from the source files). Before you can view Scribble (the server application) files from within Container, you must register Scribble as an OLE server application.

To install and register Scribble as an OLE server application

Build and run Scribble.exe from the sample source files for Step 7:

  1. Go to the SCRIBBLE sample abstract in the documentation.

  2. Click STEP7.

  3. In the Sample Application dialog box, click Copy All.

  4. Accept the default directory.

  5. Open the project in the development environment.

  6. From the Build menu, click Execute Scribble.exe.

  7. When prompted to build the application, click Yes.

    Running Scribble briefly as a stand-alone application registers it in the system registry as an OLE server.

Note You can find a finished example of this lesson's code in the Scribble Step7 sample source code directory.

To preview the Container project as an ActiveX container application

  1. Go to the CONTAINER sample abstract in the documentation.

  2. From Step 2, copy the sample files to your hard drive.

  3. Build and run Container.exe.

  4. From Container’s Edit menu, click Insert New Object.

    The Insert Object dialog box appears.

  5. In the Object Type list, click Scribb Document.

    This opens a Scribble document inside the Container application. Notice how Scribble:

    • Opens a window inside Container for in-place editing. The window has a resizeable border so you can change the size of the window while visually editing the Scribble object.

    • Takes over part of Container’s menu bar and adds its own Pen menu.

    • Takes over Container’s toolbar.
  6. Draw something in Scribble’s in-place window.

    Notice how drawing in Scribble’s in-place window is just like drawing in the stand-alone Scribble application.

  7. Click outside the window.

    With focus taken away so the Scribble document is no longer selected, the Scribble object is redrawn inside Container’s view with the help of the Scribble server.

The rest of this lesson describes how to make Scribble an OLE server.


Steps to Provide OLE Server Support After the Fact

When you first used AppWizard to create your Scribble starter files, you could have chosen the Full-server option to create Scribble as an OLE server application. However, the purpose of this tutorial step is to show you how to build OLE server support into your existing MFC application.

You’ll complete the following steps:

  • Run AppWizard with the Full-server option

  • Copy new files from the AppWizard Full-server project to your Scribble project directory.

  • Add AfxOle.h to your precompiled header file.

  • Add OLE-specific server code to the application object.

  • Convert the CDocument class to the COleServerDoc class.

In addition, you’ll take several steps to:

  • Edit OLE-related resources.

  • Add application-specific server support.


Using AppWizard's Full-server Option

Using AppWizard’s Full-server Option

This step of Scribble is similar to Step 6 (Adding Context-Sensitive Help) in that you’ll start by running AppWizard with an option that you didn’t originally choose. In this case, the option is full-server support. Then you’ll copy the files and resources that AppWizard generates for an OLE Server application to your Scribble project.

To set the AppWizard options for an OLE server application

  1. From the File menu, click New.

    The New dialog box appears.

  2. Click the Projects tab.

  3. Click MFC AppWizard (exe).

  4. In the Project Name box, type Scribble.

    You’re using the same project name (Scribble) so that you can copy code that contains class names and other identifiers into your current Scribble project files.

  5. In the Location box, type the path to store your new Scribble application files. Type Scratch as the new project directory, so that your project path is C:\[optional directory]\Scratch\Scribble. This creates the new Scribble project under a subdirectory named Scratch.

    Note You must create this Scribble in a different project directory than the original Scribble.

  6. Click OK.

    The MFC AppWizard - Step 1 dialog box appears.

  7. Click Next in the MFC AppWizard – Step 1 and MFC AppWizard – Step 2 of 6 dialog boxes to accept the default options.

  8. In the MFC AppWizard – Step 3 of 6 dialog box, click Full-server and then click Next.

  9. In the MFC AppWizard – Step 4 of 6 dialog box, click Advanced.

  10. In the File extension box, type scb.

    The entry in the Filter name box changes appropriately.

  11. Click Close.

  12. Click the Context-sensitive Help option and click Next.

    When you click Context-sensitive Help with the Full-server option, AppWizard creates:

    • The toolbar bitmap for Scribble when it runs as a server.

    • An additional .RTF file for the OLE commands that appear on the menu when Scribble is running as a server.

    You’ll copy these resources to your Scribble project later.

  13. In MFC AppWizard – Step 5 of 6, click Next to accept the default options.

  14. In the MFC AppWizard – Step 6 of 6 dialog box, check and modify class names and filenames to make them match the original Scribble application.
    • Select the class CScribbleView and click CScrollView in the Base class box.

    • Select the class CScribbleSrvrItem and change its name to CScribbleItem, the header filename to ScribbleItem.h, and the implementation filename to ScribbleItem.cpp.

      Notice that COleServerItem is the base class, which reflects your full-server choice.

    Important If you are starting from the sample source code files for Step 6, rather than from a Scribble project that you have been developing step by step, you must modify the filenames and class names differently than those described in this procedure. Use the alternate filenames described in the next topic, Using Short Filenames.

    For more information about using short filenames, see the topic A Note About Long Filenames in Scribble Lesson 1.

  15. Click Finish.

  16. In the New Project Information dialog box, confirm the specifications and click OK.

    AppWizard creates the new files.

  17. Click Yes if the following message appears:
    A Microsoft Visual C++ project with the same name is already registered at [old scribble pathname]. Do you want to replace this project in the registry?

    This message box appears when you have already run a previous version of Scribble, from another directory, to register it with Windows.

  18. From the File menu, click Close Workspace to close the Scratch\Scribble project.

In the next procedure you’ll incorporate files and resources from this Scratch version of Scribble, into the Scribble you’ve been developing all along.


Using Short Filenames

If you are starting Scribble Step 7 from the Step 6 sample source files, your project filenames must match the short filenames found on the distribution CD. (Class names remain the same.) The distribution CD uses short (8.3) filenames in order to meet ISO standards.

The following list describes the filenames you need to change. For your convenience, class name modifications for this step, as described in the previous procedure, are listed as well.

Note As described in the previous procedure, you must modify these project filenames and class names in the MFC AppWizard – Step 6 of 6 dialog box before creating your project.

  • For class CScribbleSrvrItem, change the class name to CScribbleItem, the header file to ScribItm.h, and the implementation file to ScribItm.cpp.

  • For class CScribbleView, change the header file to ScribVw.h and the implementation file to ScribVw.cpp. As mentioned in the previous procedure, change the base class of CScribbleView to CScrollView.

  • For class CScribbleDoc, change the header file to ScribDoc.h and the implementation file to ScribDoc.cpp.

Note As you work through the procedures in this tutorial step, remember that whenever a procedure refers to a long filename you should substitute the equivalent short filename.


Transfer Scratch Files to Your Scribble Project

AppWizard provides several source files for an OLE server application that you can use as-is in Scribble. The following procedures describe how to incorporate source files and code from the Scratch version of Scribble, which you created with the AppWizard full-server option, into the version of Scribble you’ve been developing step by step, and which did not initially include full-server support.

Suggested Reading

  • Reading the FileView and ClassView Panes, Visual C++ User’s Guide

  • ClassView: Overview, Visual C++ User’s Guide

  • ClassView Topics, Visual C++ Programmer’s Guide

To add the new files to your Scribble project

  1. Copy the following files from Scratch\Scribble to your Scribble project root directory:
    • IpFrame.h

    • IpFrame.cpp

    • Scribble.cpp (click Yes if prompted to overwrite the existing version of this file)

    • ScribbleItem.h

    • ScribbleItem.cpp

    • Scribble.reg (click Yes if prompted to overwrite the existing version of this file)

    • Hlp\AfxOleSv.rtf

    • Res\Toolbar.bmp (click Yes if prompted to overwrite the existing version of this file)

    These files provide “In Place” (IP) editing functionality for Scribble. Placing them in your project directory makes dependent files available to the project. When AppWizard generates Scribble.cpp with Full-server support, it adds OLE-specific code to the file.

  2. Start Visual C++, if necessary, and from the File menu, click Open Workspace.

  3. Navigate to your original version of Scribble and open Scribble.dsw.

    Click Yes when prompted to register this version of Scribble.

  4. From the Project menu, point to Add to Project and click Files into Project.

    The Insert Files into Project dialog box appears. By default, the dialog box points to your current project directory.

  5. Select the files IpFrame.cpp, IpFrame.h, ScribbleItem.cpp, and ScribbleItem.h, and click the OK button.

    You can now view the added files in FileView and their associated classes in ClassView.

  6. From FileView, open Scribble.hpj.

    Because you added the help source file, Afxolesv.rtf, to the project, you need to modify the help project file.

  7. In the [FILES] section of Scribble.hpj, type “AfxOleSv.rtf,” and save the file.

    Similar to the process for adding files to a project, you can’t just copy a help source (.RTF) file; you must add it to the Help project. Modifying the Help Project (.HPJ) file accomplishes this.


Scribble's In Place Editing Files

Scribble’s In Place Editing Files

IpFrame.cpp contains the implementation of Scribble’s COleIPFrameWnd-derived class. This is the frame window for Scribble when it draws in the container. COleIPFrameWnd provides the resize border that you noticed in the preview demonstration. Note that Scribble only uses this COleIPFrameWnd object when the Scribble object is in-place activated in the container application. Only then does the server need to provide a window. When the Scribble object is not activated in place, but is just being drawn in the container’s window, the OLE server provides a metafile (a list of drawing commands) to the container so it can then play the metafile.

ScribbleItem.cpp contains the implementation of Scribble’s COleServerItem-derived class. The COleServerItem object represents the Scribble document when it is embedded in a container.


Registering an OLE Server Application with Windows

In order to run your application as an OLE server, you need to register it with Windows. AppWizard provides code that does this for you when you simply run the application, as you saw in Previewing Scribble Running as an OLE Server. AppWizard also provides a text file, Scribble.reg, which you can use to register Scribble as an OLE server. You do this by running REGEDIT and merging Scribble.reg with the existing Windows registry file.

To use REGEDIT to register Scribble as an OLE server

  1. Run REGEDIT. (Note that on Windows NT, both REGEDIT and REGEDT32 are available. You can only merge a .REG file using REGEDIT.)

  2. On Windows NT, from the File menu, click Merge Registration File. On Windows 95, from the Registry menu, click Import Registry File.

  3. Select Scribble.reg from your Scribble project directory.

  4. When notified that the information has been registered, click OK and then close REGEDIT.

If you’re going to distribute your application, you should provide this registration as part of the application setup routine.

There are several ways you can do this:

  • Spawn REGEDIT, using the /s (silent) option and specifying the .REG file as a command-line parameter, for example:
    regedit /s scribble.reg

    Naturally, you’d need to create and maintain a .REG file specific to your particular application.

  • Use the Windows registry APIs to set up all the registry keys.

  • Let the framework programmatically register the application for you, by allowing your application to be run with a special switch (for example, "/Register") that registers the application and then immediately quits, such that the application can be run with this special switch as part of setup. For example:
    scribble /register

    Of course, you'd need to modify your application's InitInstance code so that it recognizes this special switch. This works well for applications that are small and boot relatively quickly, since you can leverage the automatic registration that MFC provides as part of the framework.

    For more information, see Framework Support for Registering the Application with Windows later in this lesson.


Add AfxOle.h to Your Precompiled Header File

The MFC OLE support is kept in a separate extension header file, AfxOle.h. Because several Scribble implementation files refer to the MFC OLE classes, it is a good idea to include it in Stdafx.h, the precompiled header for Scribble.

Suggested Reading

  • Creating Precompiled Headers, Visual C++ Programmer’s Guide

  • Reading the FileView and ClassView Panes, Visual C++ User’s Guide

To add Afxole.h to the precompiled header file

  1. From FileView, open Stdafx.h (under the Header Files folder) and add the following #include statement:
    #include <afxole.h>        // MFC OLE support
  2. Save the file.


Add OLE Server Support to the Application Object

You’ve already added much OLE server support to the application object by using the Full-server version of Scribble.cpp. In the next two procedures, you’ll complete this process by adding a COleTemplateServer data member to CScribbleApp and by adding a string resource to Scribble’s string table.

Note For these procedures, you will be working in Scribble itself, not in Scratch/Scribble.

Suggested Reading

  • ClassView: Overview, Visual C++ User’s Guide

  • ClassView Topics, Visual C++ Programmer’s Guide

  • Project Workspace Views, Visual C++ User’s Guide

To add a COleTemplateServer data member to CScribbleApp

  • Use ClassView to jump to the CScribbleApp declaration in Scribble.h and add the following lines at the beginning of the // Implementation section:
    COleTemplateServer m_server;
// Server object for document creation

    You will find the same code in Scratch\Scribble.h, which was provided by AppWizard.

    The COleTemplateServer object is used to register a server application with OLE. See how m_server is used in Scribble’s InitInstance.

To copy the string resource

  1. Switch to ResourceView and open Scribble’s String table.

  2. Use the File menu to open the .RC file for the Scratch\Scribble project, and open its String table. (Don’t insert this resource file.)

  3. Arrange the String editor windows so that you can view both String tables and they don’t overlap.

  4. Press CTRL and then drag the resource IDP_OLE_INIT_FAILED from the Full-server version of Scribble to your Scribble project’s String table.

    This AppWizard provided string is used in Scribble’s InitInstance. The string text is:

    OLE initialization failed. Make sure that the OLE libraries are correct version.

    You can view this text by selecting the string and then clicking Properties from the View menu.

  5. Save your Scribble.rc and close both String tables. (You can also close the Scratch\Scribble version of Scribble.rc.)


Convert the CDocument Class to the COleServerDoc Class

The CDocument class implements standard document behavior in a stand-alone application. When the application runs as an OLE in-place editing server, however, the document must do extra work on behalf of OLE. The framework implements the bulk of this OLE document support in class COleServerDoc. The remaining work you have to do is:

  • Change the base class of CScribbleDoc from CDocument to COleServerDoc.

  • Implement the document’s support for embedded items.

Suggested Reading

  • ClassView: Overview, Visual C++ User’s Guide

  • ClassView Topics, Visual C++ Programmer’s Guide

  • Reading the FileView and ClassView Panes, Visual C++ User’s Guide

  • HIERSVR, Visual C++ Samples

  • COleServerDoc Overview, Microsoft Foundation Class Reference

To change the base class of CScribbleDoc

  1. Use ClassView to jump to the declaration for class CScribbleDoc and change:
    class CScribbleDoc : public CDocument

    to:

    class CScribbleDoc : public COleServerDoc
  2. Use FileView to open ScribbleDoc.cpp and replace all instances of CDocument with COleServerDoc.

    This changes the base class reference of CScribbleDoc from CDocument to COleServerDoc.

The COleServerItem object represents the Scribble document when the document is embedded in a container. To create a COleServerItem for a given document, AppWizard provides an override of GetEmbeddedItem in the COleServerDoc-derived class. The return type of OnGetEmbeddedItem is a pointer to a COleServerItem.

Note A COleServerItem object can also represent an OLE link item, but Scribble doesn’t illustrate that. The sample HIERSVR illustrates this link item.

In the following procedure, you’ll fill in some of the code that AppWizard would have generated had you originally chosen the Full-server option.

To implement the document’s support for embedded items

  1. Using FileView, switch to ScribbleDoc.h in the editor and add the following forward class reference for CScribbleItem immediately before the line class CScribbleDoc : public COleServerDoc:
    class CScribbleItem;
  2. In ScribbleDoc.cpp, add the following #include statement immediately after the line #include "scribbleDoc.h":
    #include "ScribbleItem.h"

    Note The short filename for this header file is ScribItm.h. If you’re starting from the sample source files from Scribble Step 6 to complete this tutorial step, you must use this short filename. Otherwise, you will not be able to compile your project.

    Now you’ll add the OnGetEmbeddedItem function override. (AppWizard provided this code in the Scratch version of Scribble you generated with Full-server support.)

  3. In ClassView, right-click the CScribbleDoc class icon.

  4. From the menu, click Add Member Function.

  5. In the Function Type box, type: COleServerItem*.

  6. In the Function Declaration box, type: OnGetEmbeddedItem().

  7. In the Access group box, click Protected.

  8. Click the Virtual check box, (note that this adds the virtual keyword to the declaration) and click OK.

  9. In ScribbleDoc.cpp, implement the function as follows:
    // OnGetEmbeddedItem is called by the framework to get the COleServerItem
//  that is associated with the document.  It is only called when necessary.

CScribbleItem* pItem = new CScribbleItem(this);
ASSERT_VALID(pItem);
return pItem;
  10. Switch back to ScribbleDoc.h and, in the Public Attributes section, add the following code:
    CScribbleItem* GetEmbeddedItem()
{ return (CScribbleItem*)COleServerDoc::GetEmbeddedItem(); }

    Note AppWizard provided this code in the \Scratch\Scribble project, but you must insert it manually in the Scribble project.

    This code provides a type-safe function to return a pointer to the specific COleServerItem-derived class, CScribbleItem.


Analyze OLE Server Code in InitInstance

Switch to Scribble.cpp and examine the OLE server code that AppWizard provided, beginning with CScribbleApp theApp. (Note that the default class names differ from those you customized for Scribble.)

All MFC OLE applications — whether container, server, or Automation server — must call AfxOleInit and the static function COleTemplateServer::RegisterAll from the application’s InitInstance to initialize framework support for OLE. For an application that is not an OLE server, the CDocTemplate object coordinates the creation of the frame window, view, and document object for the stand-alone application. The CDocTemplate uses menu, accelerator, and string resources passed to the constructor in InitInstance code, to determine the menu, accelerators, and Windows shell registration of the stand-alone application.

In the case of an OLE server application, additional information is needed. The InitInstance function passes this information as parameters to the CDocTemplate::SetServerInfo function, before it calls CWinApp::AddDocTemplate. Here is a description of the parameters:

pDocTemplate->SetServerInfo(
   IDR_SCRIBBTYPE_SRVR_EMB, IDR_SCRIBBTYPE_SRVR_IP,
   RUNTIME_CLASS(CInPlaceFrame));
  • IDR_SCRIBBTYPE_SRVR_EMB is the common ID of the menu and accelerator resources loaded when Scribble is fully opened by the container application when it edits an embedded item.

  • IDR_SCRIBBTYPE_SRVR_IP is the common ID of the menu, accelerators, and toolbar bitmap resources that are loaded when Scribble is activated in place in the container application. The purpose and design of these resources specifically for the in-place activated server application, as well as the purpose and design of the above resources for the fully opened server application, are explained later in this tutorial.

  • RUNTIME_CLASS(CInPlaceFrame) is the COleIPFrameWnd-derived class provided by AppWizard. This class defines the behavior of the window created by the framework on behalf of the server application when it is activated in place by the container. The AppWizard-provided implementation of this class adds the resize border to the in-place window so that the user can resize the object while it is activated in place.

The following code, from Scribble.cpp, defines the OLE class ID for the Scribble application and registers the application. AppWizard provides a default ID that is randomly generated. The call to COleTemplateServer::ConnectTemplate registers the class ID with Windows.

Note The clsid below will differ from the unique one that is provided when you run AppWizard.

static const CLSID clsid =
{ 0x0002180f, 0x0, 0x0, { 0xC0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x46 } };

...

m_server.ConnectTemplate(clsid, pDocTemplate, FALSE);

If the application was spawned by OLE as an in-place server or Automation server, then InitInstance returns before performing additional initialization tasks that are appropriate only for stand-alone applications:

if (cmdInfo.m_bRunEmbedded || cmdInfo.m_bRunAutomated)
{
   return TRUE;
}


Framework Support for Registering the Application with Windows

An MFC OLE server can use COleTemplateServer::UpdateRegistry to register the application as an OLE server.

Suggested Reading in the Microsoft Foundation Class Reference

  • COleTemplateServer::UpdateRegistry

The following AppWizard-provided code, in Scribble.cpp, is optional.

// When a server application is launched stand-alone, it is a good
// idea to update the system registry in case it has been damaged.
m_server.UpdateRegistry(OAT_INPLACE_SERVER);

This code enables the user to run the application once to register it as an OLE server. The preferred method of registering your application with Windows is to use one of the methods described earlier, in Registering an OLE Server Application with Windows.

  • Manually merge the Scribble.reg registration file into the system registry by using REGEDIT

    -or-

  • Programmatically merge the registry information as part of your application’s setup program. There are several ways you can accomplish this, as described in the topic mentioned.


Editing OLE-Related Resources

The next overall task is to edit the OLE-related resources in your server application. In general, this consists of the following steps:

  • Add OLE standard resources

  • Add menus

  • Add toolbar buttons

  • Add accelerators for in-place active or fully opened servers


Add OLE Standard Resources

To add OLE standard resources

  1. In ResourceView, right-click the Scribble Resources folder.

  2. From the pop-up menu, click Resource Includes.

    The Resource Includes dialog box appears.

  3. In the Compile-time directives list box, type the following:
    #include "afxolesv.rc"    // OLE server resources

    This #include statement takes care of including some string resources referred to by the framework OLE classes. AppWizard adds this #include statement to your application resource file if you choose the Container, Mini-server, Full-server, or Both container and server option. If you look at Scratch\Scribble.rc, you will see that AppWizard has added this same compile-time directive.

  4. Click OK to accept the changes you made in the Resource Includes dialog box.

  5. Click OK when the following message box appears:
    Directive text will be written verbatim into your resource script and may render it incompatible


Add OLE Menu Resources

A server application shows different menus, depending on whether it is running stand-alone, embedded, or in-place activated. AppWizard provides three different menus for these cases:

  • IDR_SCRIBBTYPE is the menu for the document when it is opened in the usual way.

  • IDR_SCRIBBTYPE_SRVR_EMB is the menu for the document when the server is opened fully from the container application.

  • IDR_SCRIBBTYPE_SRVR_IP is the menu for the document when it is activated in place (thus, “IP”) in a container. When an object is activated in place, OLE merges the menu of the container application with the menu provided by the server application. The merging of the two menus is based on separator bars. Scribble’s in-place menu (IDR_SCRIBBTYPE_SRVR_IP) looks like this:

    Edit | Pen | Help

    An example of a container’s menu is seen in Container (the ActiveX Container tutorial discussed in Lessons 13 through 15), as shown below:

    File | | Window

    OLE merges the two menus to create the following menu when the Scribble object is activated in place in the Container application. By doing so, you can perform any Scribble-related tasks from within the container.

    File Edit Pen Window Help

    OLE merges pop-up menus, from left to right, in the following order:

    • Container’s pop-up menu(s) before the first separator.

    • Server’s pop-up menu(s) before the first separator.

    • Container’s pop-up menu(s) between the first and second separators.

    • Server’s pop-up menu(s) between the first and second separators.

    • Container’s pop-up menu(s) after the second separator.

    • Server’s pop-up menu(s) after the second separator.

Scribble’s two distinct OLE-related menu resources are referred to in Scribble.cpp in CScribbleApp::InitInstance:

pDocTemplate->SetServerInfo(
   IDR_SCRIBBTYPE_SRVR_EMB, IDR_SCRIBBTYPE_SRVR_IP,
   RUNTIME_CLASS(CInPlaceFrame));

AppWizard provides all the code and resources described above, except for the Pen pop-up menu, which is application specific. If you are creating a new application from scratch, each new pop-up menu must be added to each of the three resources. This is easy to do with the drag and copy feature of the Menu editor. You can copy a resource by dragging it to the desired location while holding down the CTRL key and then releasing the mouse button.

In this part of the tutorial, you will:

  • Copy the two new resources, IDR_SCRIBBTYPE_SRVR_EMB and IDR_SCRIBBTYPE_SRVR_IP, from the AppWizard-provided Scratch\Scribble.rc to your Scribble.rc.

  • Add the Pen pop-up menu to each of the menus.

To copy menu resources

  1. Switch to ResourceView and expand the Scribble Resources folder, then expand the Menu folder.

  2. From the File menu, click Open and select Scratch\Scribble\Scribble.rc. Again, expand the Menu folder.

  3. Copy the menus IDR_SCRIBBTYPE_SRVR_EMB and IDR_SCRIBBTYPE_SRVR_IP from the scratch resource file to your Scribble project .RC file.

  4. Close the scratch version of Scribble.rc.

To copy menu items

  1. Open the menu resources IDR_SCRIBBTYPE and IDR_SCRIBBTYPE_SRVR_EMB in your Scribble project resource file.

  2. Arrange the two menus so they don’t overlap and so you can see both of them in the Menu editor window.

  3. Copy the Pen menu from IDR_SCRIBBTYPE into the IDR_SCRIBBTYPE_SRVR_EMB menu so that it is between the Edit and View menus, as in IDR_SCRIBBTYPE.

  4. Copy the Clear All menu item from the Edit menu in IDR_SCRIBBTYPE to the Edit menu in IDR_SCRIBBTYPE_SRVR_EMB.

  5. Close IDR_SCRIBBTYPE_SRVR_EMB and repeat steps 1 to 3 for IDR_SCRIBBTYPE_SRVR_IP, placing the Pen menu as follows:

    Edit | Pen | View | | | | Help

    (Don’t copy the Clear All menu item.)

  6. Save and then close the menu resources.


Add OLE Toolbar Resources

The server application also provides an in-place toolbar to the container. This toolbar typically supports a different set of commands than does the container’s usual toolbar, generally a subset. For example, the in-place toolbar does not support File menu commands, because the container, not the server, must handle those commands even if the server is activated in place.

Suggested Reading in the Visual C++ User’s Guide

  • Overview: Toolbar Editor

AppWizard provides a default in-place toolbar resource with five buttons. The COleIPFrameWnd-derived class is responsible for specifying the mapping of commands to toolbar buttons.

To view the default in-place toolbar

  1. Open the scratch version of Scribble.rc, expand the Toolbar folder and double-click IDR_SCRIBBTYPE_SRVR_IP.

    The Toolbar editor opens.

  2. Click Properties from the View menu to view the Toolbar Button Properties page, which displays the default resource IDs and prompt strings.

  3. Close the Toolbar editor for the next procedure.

The Toolbar editor makes it easy to edit your application toolbar resources. In the following procedure, you’ll copy the AppWizard-generated toolbar resources for in-place activation of Scribble into your Scribble project. Then you’ll edit the toolbars to match your other Scribble project toolbar resources.

To copy the toolbar buttons

  1. With the scratch Scribble.rc file open and the Toolbar folder expanded, switch to ResourceView in your Scribble project and expand the Toolbar folder.

  2. Hold down the CTRL key while you drag the toolbar resource, IDR_SCRIBBTYPE_SRVR_IP, from the AppWizard-provided scratch resource file into your Scribble project Toolbar folder.

    Close the scratch Scribble.rc file.

  3. In ResourceView, double-click both Scribble toolbar resources — IDR_MAINFRAME and IDR_SCRIBBTYPE_SRVR_IP and arrange them in the editor window so they don’t overlap.

  4. Hold down the CTRL key while you drag the Pen Width button from the IDR_MAINFRAME toolbar to the IDR_SCRIBBTYPE_SRVR_IP toolbar, just after the Paste button.

  5. Delete the Cut, Copy and Paste buttons from the IDR_SCRIBBTYPE_SRVR_IP toolbar by dragging them off the toolbar.

    Your in-place toolbar should now contain just three buttons: the application Help button, the Pen Widths button, and the Help Mode button. This is the toolbar that will appear when Scribble is activated as an OLE server application.

  6. Save your Scribble project .RC file and close the toolbar editors.


Add Accelerator Resources for In-Place Active or Fully Opened Servers

Just as a server application offers different menus when it is in-place active or fully opened versus running stand-alone, the server application offers a different set of accelerators. AppWizard provides two additional accelerator resources, IDR_SCRIBBTYPE_SRVR_EMB and IDR_SCRIBBTYPE_SRVR_IP, just as it creates two additional menu resources with these same identifications. You can copy these accelerator resources from the scratch version of Scribble.rc.

To copy accelerator resources

  1. Just as you have done in previous procedures to copy resources, expand the Accelerator folders from both the scratch version of Scribble.rc and your Scribble project .RC file.

  2. Copy accelerator resources IDR_SCRIBBTYPE_SRVR_EMB and IDR_SCRIBBTYPE_SRVR_IP from the scratch version of Scribble.rc to your Scribble project Accelerator folder.

    You can copy both resources simultaneously, as the Resource editors support multiple selection.

  3. Save your changes to Scribble.rc and close the scratch .RC file.


Adding Application-Specific Server Support

To complete Scribble Step 7 and implement Scribble as a fully functional OLE server application, these are the final steps you’ll take:

  • Add application-specific server support to the document class.

  • Implement the server item.

  • Implement OLE in-place support in the view class.


Add Application-Specific Server Support to the Document Class

To finish adding application-specific server support in the document class, you must:

  • Notify the OLE server that the embedded item has moved or changed size (see next procedure).

  • Change the initial size of the document.

  • Implement the document’s support for putting a link format on the Clipboard.

Suggested Reading

  • Using WizardBar, Visual C++ Programmer’s Guide

  • COleServerItem::CopyToClipboard, Microsoft Foundation Class Reference

In the following procedure, you’ll use the Add Member Function command to implement the OnSetItemRects function for class CScribbleDoc. The framework calls OnSetItemRects when the position or size of the embedded item has changed in the container, or when the clipping of the embedded item has changed in the container. Because Scribble’s view is a CScrollView, you need to call CScrollView::SetScrollSizes to reflect the change in the size of the item. Because there are multiple places where the logic associated with SetScrollSizes must be performed, you will later write a helper function, CScribbleView::ResyncScrollSizes, which you will call from the override of OnSetItemRects.

To notify the OLE server when the embedded item moves or changes size

  1. In ClassView, right-click CScribbleDoc.

  2. Click Add Member Function.

    The Add Member Function dialog box appears.

  3. Fill in the dialog box as follows:
    • In the Function Type box, type void.

    • In the Function Declaration box, type the following:
    OnSetItemRects(LPCRECT lpPosRect, LPCRECT lpClipRect)
    • In the Access area, click Protected.
  4. Click OK.

  5. In ScribbleDoc.cpp, implement the starter definition with the following code:
    // call base class to change the size of the window
COleServerDoc::OnSetItemRects(lpPosRect, lpClipRect);

// notify first view that scroll info should change
POSITION pos = GetFirstViewPosition();
CScribbleView* pView = (CScribbleView*)GetNextView(pos);
pView->ResyncScrollSizes();
  6. Add the following #include statement to ScribbleDoc.cpp, because the above implementation refers to CScribbleView:
    #include "ScribbleView.h"

    The short filename is ScribVw.h.

The next step is to modify CScribbleDoc::InitDocument so Scribble’s fixed document size changes from 8 by 9 inches, to 2 by 2 inches. The current size, 8 by 9 inches, is too large for most containers.

To change the initial size of the document

  1. Use ClassView to jump to CScribbleDoc::InitDocument and modify the comments and the parameters that return the default document size, as follows:
    //default document size is 200 x 200 screen pixels
...
m_sizeDoc = CSize(200, 200);
  2. Use ClassView to jump to the CScribbleDoc constructor and replace the //TODO comments with the following line:
    m_sizeDoc = CSize(200, 200);

    The m_sizeDoc variable is also initialized in the helper member function, InitDocument, when a document is newly created or reopened in a stand-alone running instance of Scribble. InitDocument is called by Scribble’s overrides of CDocument::OnNewDocument and OnOpenDocument. When Scribble is run as a server, the OnNewDocument and OnOpenDocument functions are not called. Therefore a good place to initialize m_sizeDoc is in the constructor.

Finally, in the following procedure you’ll implement the ability for containers to execute the Paste Link command on the server’s Edit menu.

To implement the document’s support for putting a link format on the Clipboard

  1. Display ScribbleDoc.cpp in the editor window.

  2. From the Filter list in WizardBar, click ID_EDIT_COPY, and from the Members list, click COMMAND.

  3. Click the action button, located on the right end of WizardBar.

    The New Windows Message and Event Handlers dialog box appears.

  4. Click Add Handler.

    The Add Member Function dialog box appears.

  5. Click OK in the Add Member Function dialog box to accept the default name OnEditCopy.

  6. Click Edit Existing to jump to the OnEditCopy member function definition.

  7. Replace the //TODO line with the following code:
    CScribbleItem* pItem = GetEmbeddedItem();
pItem->CopyToClipboard(TRUE);

The framework function COleServerItem::CopyToClipboard creates a COleDataSource object containing the OLE item's data in proper formats, then places the COleDataSource object on the Clipboard. The COleDataSource object includes the item's native data and its representation in CF_METAFILEPICT format, as well as data in any conversion formats you choose to support.


Implement the Server Item

AppWizard has done most of the work associated with implementing the server item by providing the COleServerItem-derived class in ScribbleItem.cpp. All you have to do is add the application-specific implementation.

Suggested Reading in the Microsoft Foundation Class Reference

  • COleServerItem::OnGetExtent

The server item’s OnDraw member function is called when the server document needs to draw itself as an inactive embedded object inside the container window. In contrast, the view’s OnDraw is called when the document is activated in place inside the container. CScribbleItem::OnDraw needs to do essentially the same drawing that CScribbleView::OnDraw does. If your OnDraw code in your view class is complex, you will probably want to reuse that code by having your client item’s OnDraw call a shared draw routine. In Scribble, the CStroke::DrawStroke routine is reused.

Recall that you previously added the AppWizard-generated file, ScribbleItem.cpp, to the project. Now you’ll modify the AppWizard-provided code to support Scribble-specific drawing functions.

To implement the OLE item’s OnDraw function

  1. Use ClassView to jump to CScribbleItem::OnDraw and modify the AppWizard-provided stubbed version of the function so that it looks like the following:
    BOOL CScribbleItem::OnDraw(CDC* pDC, CSize& rSize)
{
CScribbleDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);

pDC->SetMapMode(MM_ANISOTROPIC);
CSize sizeDoc = pDoc->GetDocSize();
sizeDoc.cy = -sizeDoc.cy;
pDC->SetWindowExt(sizeDoc);
pDC->SetWindowOrg(0,0);

CTypedPtrList<CObList, CStroke*>& strokeList
= pDoc->m_strokeList;
POSITION pos = strokeList.GetHeadPosition();
while (pos != NULL)
{
strokeList.GetNext(pos)->DrawStroke(pDC);
}

return TRUE;
}

    This code sets the window extent to the size of the document so that when the document is drawn in the in-place window, the drawing will stretch to the size of the window.

    Notice that the above code reverses the sign of the y dimension to reflect the fact that strokes’ positions are maintained in MM_LOENGLISH coordinates by using negative y coordinates.

  2. Similarly, use ClassView to jump to the AppWizard-provided version of CScribbleItem::OnGetExtent and modify it as follows:
    BOOL CScribbleItem::OnGetExtent(DVASPECT dwDrawAspect, CSize& rSize)
{
// This implementation of CScribbleItem::OnGetExtent only handles
// the "content" aspect indicated by DVASPECT_CONTENT.

if (dwDrawAspect != DVASPECT_CONTENT)
return COleServerItem::OnGetExtent(dwDrawAspect, rSize);

// CScribbleItem::OnGetExtent is called to get the extent in
//  HIMETRIC units of the entire item.  The default
//  implementation here simply returns a hard-coded
//  number of units.
CScribbleDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);

rSize = pDoc->GetDocSize();
CClientDC dc(NULL);

// use a mapping mode based on logical units
//    (we can't use MM_LOENGLISH because MM_LOENGLISH uses
//    physical inches)
dc.SetMapMode(MM_ANISOTROPIC);
dc.SetViewportExt(dc.GetDeviceCaps(LOGPIXELSX),
dc.GetDeviceCaps(LOGPIXELSY));
dc.SetWindowExt(100, -100);
dc.LPtoHIMETRIC(&rSize);

return TRUE;
}
  3. Save ScribbleItem.cpp.

The framework calls the virtual COleServerItem::OnGetExtent member function when the item needs to set the viewport and window extents of the server item window when the item is in-place active. The new code modifies the original code provided by AppWizard, which sets the size of the server item to an arbitrary fixed value. The function must return the size of the server item in HIMETRIC units. In Scribble, the server item must return the size of the document. Scribble stores the drawing size in physical MM_LOENGLISH units. For reasons explained in the next topic, Scribble needs to convert the drawing size to HIMETRIC units based on its logical MM_LOENGLISH size rather than physical MM_LOENGLISH size.


Implement OLE In-Place Support in the View Class

To implement OLE in-place support in the view class, you must:

  • Calculate logical MM_LOENGLISH sizes rather than physical MM_LOENGLISH sizes.

  • Adjust the scroll view’s scroll bars to reflect the use of the logical mapping mode.

  • Notify OLE when the embedded item changes.

Before Scribble was enhanced to be an OLE in-place editing server, it was lazy about its device context coordinates using MM_LOENGLISH: Scribble did not adjust for how many logical pixels per inch were on the screen display. Most serious Windows applications scale their screen output because small fonts are rarely readable when displayed in their true (physical) size. Applications can adjust for the number of pixels in the logical inch by applying the kind of logic illustrated by the following code sample. This logic relies primarily on values returned by CDC::GetDeviceCaps(LOGPIXELSX) and GetDeviceCaps(LOGPIXELSY). Now that Scribble is an OLE server, it should scale according to logical pixels per inch. Otherwise, you (and your user) will notice a difference in the scaling of a Scribble drawing when the Scribble server is fully open versus its scaling when it is displayed embedded in the container.

Suggested Reading in the Microsoft Foundation Class Reference

  • COleServerDoc::NotifyChanged

  • CDC::GetDeviceCaps

To implement logical MM_LOENGLISH rather than physical MM_LOENGLISH

  1. From the View menu, click ClassWizard.

  2. If necessary, click the Message Maps tab and ensure that CScribbleView is selected in the Class name and Object IDs boxes.

  3. In the Messages box, click OnPrepareDC, and click the Add Function button.

  4. Click Edit Code.

  5. Implement the override of OnPrepareDC as follows:
    void CScribbleView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo)
{
CScribbleDoc* pDoc = GetDocument();
CScrollView::OnPrepareDC(pDC, pInfo);

pDC->SetMapMode(MM_ANISOTROPIC);
CSize sizeDoc = pDoc->GetDocSize();
sizeDoc.cy = -sizeDoc.cy;
pDC->SetWindowExt(sizeDoc);

CSize sizeNum, sizeDenom;
pDoc->GetZoomFactor(&sizeNum, &sizeDenom);

int xLogPixPerInch = pDC->GetDeviceCaps(LOGPIXELSX);
int yLogPixPerInch = pDC->GetDeviceCaps(LOGPIXELSY);

long xExt = (long)sizeDoc.cx * xLogPixPerInch * sizeNum.cx;
xExt /= 100 * (long)sizeDenom.cx;
long yExt = (long)sizeDoc.cy * yLogPixPerInch * sizeNum.cy;
yExt /= 100 * (long)sizeDenom.cy;
pDC->SetViewportExt((int)xExt, (int)-yExt);
}

In the following procedure, you’ll implement the ResyncScrollSizes helper function mentioned previously.

To adjust the scroll view’s scroll bars to reflect the use of the logical MM_LOENGLISH mapping mode

  1. In ClassView, right-click CScribbleView.

  2. From the pop-up menu, click Add Member Function.

  3. In the Add Member Function dialog box, specify the following:
    • In the Function Type box, type void .

    • In the Function Declaration box, type ResyncScrollSizes().

    • In the Access area, click Public.

    Click OK.

  4. Fill in the starter definition with the following code:
    CClientDC dc(NULL);
OnPrepareDC(&dc);
CSize sizeDoc = GetDocument()->GetDocSize();
dc.LPtoDP(&sizeDoc);
SetScrollSizes(MM_TEXT, sizeDoc);
  5. Use ClassView to jump to CScribbleView::OnInitialUpdate and replace the call to CScrollView::SetScrollSizes with the call to the helper function, ResyncScrollSizes:
    void CScribbleView::OnInitialUpdate()
{
ResyncScrollSizes();
CScrollView::OnInitialUpdate();
}

    In the next several steps you’ll use WizardBar to add a function that updates the scroll bars appropriately when the window is sized.

  6. Switch to ScribbleView.cpp in the editor.

  7. In the WizardBar Class list box, click CScribbleView

  8. Click the Action arrow, located on the right end of WizardBar.

  9. On the menu, click Add Windows Message Handler.

    The New Windows Message and Event Handlers dialog box appears.

  10. In the New Windows messages/events list, click WM_SIZE.

  11. Click Add Handler to add WM_SIZE to the Existing message/event handlers list.

  12. Click Edit Existing.

  13. Replace the //TODO comment with a call to ResyncScrollSizes:
    ResyncScrollSizes();        //  ensure that scroll info is up-to-date
  14. Use ClassView to jump to the CScribbleView constructor and replace the comment with the following code:
    SetScrollSizes(MM_TEXT, CSize(0,0));

    You must initialize the scroll sizes in the CScribbleView constructor to default values, so that the extent is defined before the first call to OnPrepareDC.

When Scribble’s view finishes adding a new stroke in CScribbleView::OnLButtonDown, it calls the document’s UpdateAllViews to inform other views that they need to invalidate a portion of the client area occupied by the new stroke. This notification is fine for Scribble when it is running stand-alone, but it is not adequate when Scribble is fully opened and editing an embedded object. In the latter case, Scribble needs to inform the container that the object has changed. This requires an additional call to COleServerDoc::NotifyChanged.

To notify OLE when the embedded item changes

  • Use ClassView to jump to CScribbleView::OnLButtonUp and add the call to NotifyChanged just after the ReleaseCapture function call, before the final return:
    pDoc->NotifyChanged();


Testing Scribble Server Functionality Using a Container Application

You have now completed all the work needed to convert Scribble to an OLE in-place editing server application. Build the project in the usual way, and follow the steps in the preview of Scribble found at the beginning of this lesson. In particular, remember to run Scribble once stand alone.

You have now converted Scribble to an OLE server that works as in the preview demonstration.


Transforming Scribble Server to an ActiveX Component

Now that you have completed Scribble Step 7, you may want to convert your Scribble server to an ActiveX component. Follow the procedures outlined in the topic Upgrade to an Active Document Server, in the Visual C++ Programmer’s Guide.


Creating an OLE Container

Container Application, Lesson 1

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

A container application is an application that can incorporate embedded or linked items into its own documents. The documents managed by a container application must be able to store and display OLE items as well as their own data. A container application must also allow users to insert new items or edit existing ones.

In this tutorial you will create a simple OLE container application, Container. The Container document can hold several in-place items that the user can resize and move in the document.

Note This tutorial assumes that you are already familiar with Microsoft Visual C++ and with the basics of the Microsoft Foundation Class Library (MFC). If you are not, complete the Scribble Tutorial before you begin this tutorial.


The Tutorial Example: Container

The Container tutorial consists of two steps. The individual steps can be found under Samples \ MFC Samples \ Tutorial Samples \ Container in the online documentation or in the \SAMPLES subdirectory on the Visual C++ CD-ROM. The sample source files contain a subdirectory for each step: STEP1 and STEP2. Each step’s subdirectory contains complete source files and other files needed.

This lesson covers Step 1 of the OLE container tutorial. You will learn how to:

  • Preview the container application.

  • Create a starter OLE container application capable of visual editing.

  • Interpret the container code provided by AppWizard.

  • Coordinate the size of an embedded object within the server.

  • Add hit testing and selection to the AppWizard-created container application.

  • Implement activation of an embedded object.

  • Implement tracker rectangles for resizing and moving items.

  • Draw embedded and linked items.

  • Delete embedded items.

In Step 2, you will learn how to:

  • Implement the Copy and Paste commands on the Edit menu.

  • Implement smart invalidation.

  • Improve coordination with the server to determine the size of contained objects

Suggested Reading

  • Creating Applications Using AppWizard, Visual C++ Programmer’s Guide

  • Beginning Your Program Overview: Creating an ActiveX Control Container, Visual C++ Programmer’s Guide

  • OLE Background: Containers and Servers, Visual C++ Programmer’s Guide

  • Activation, Visual C++ Programmer’s Guide

  • Activation: Verbs, Visual C++ Programmer’s Guide

  • Menus and Resources: Menu Merging, Visual C++ Programmer’s Guide

  • Containers: Client Items, Visual C++ Programmer’s Guide

  • Trackers, Visual C++ Programmer’s Guide

  • CRectTracker, Microsoft Foundation Class Reference


Preview of the Container Application

Before you work through the steps of implementing Container, you might want to try out the completed application. This will help you appreciate OLE container functionality from a user’s point of view.

Other example container applications are provided on the Visual C++ distribution CD-ROM. Some of these samples were written using MFC, and others were written using the SDK. You can easily install the files needed to build and examine these sample applications.

Finally, the best examples are those world-class applications that support OLE and ActiveX today. These are the applications you will ultimately want to test your application against.


Registering an OLE Component Application

Before you can preview the Container application (by running either the Step 2 executable source file, Contain.exe, or the version you’ll develop yourself by following the lesson steps), you need to install and register at least one component application for Container to access.

The Step 7 version of Scribble is a good sample application. Another example is the HIERSVR sample. Abstracts for both of these samples can be found in Visual C++ Samples under \ MFC Samples \ Tutorial Samples.

To register the Scribble server application

  • Run Scribble.exe Step 7 directly; or build and run the sample. Close the sample.

    Installing the Sample Files in Scribble Lesson 1 describes how to copy sample files and how to run a sample directly. Running Scribble briefly as a stand-alone application registers it in the system registry as an OLE server.


Previewing the Container Application

To preview Container

  1. Run Contain.exe Step 2 directly; or build and run Contain.exe from the sample source files for Step 2.

  2. On the Edit menu, click Insert New Object.

  3. In the Insert Object dialog box, select Scribb Document from the Object Type box.

  4. Click OK.

    A blank Scribble document is opened inside the Container document.

    • Notice the tracker rectangle with resize handles and dashed border that appears in the upper-left corner of the Container document. Container negotiates with the server (Scribble) to determine where to place the initial rectangle and what size to make it.

    • Notice how Container’s menu has been merged with Scribble’s (for example, notice the Pen menu) and how Container’s toolbar is replaced by one provided by Scribble (notice the Pen toolbar button). This is menu merging, a feature that enables you to edit the document within the container application.
  5. Use the mouse to draw within the rectangle provided by the server running within the Container document.

  6. Try out Scribble’s menu and toolbar commands in place; for example, change the pen width.

  7. Click outside the Scribble object, somewhere else in the Container document.

    Notice how the Scribble server is deactivated; the dashed border and resize handles are removed.

  8. Click the Scribble object to select it.

    The selection rectangle and resize handles are drawn again to indicate that this object has been selected. Notice that the cursor changes to a four-way arrow when it is over the structure.

  9. Drag the Scribble object around and resize it.

  10. On the Edit menu, click Insert New Object to add additional embedded objects.

  11. Run HIERSVR stand-alone. Copy an object from HIERSVR.

  12. Paste the object into the Container document.

    You have now seen two ways to initiate an embedded object: You can use the Insert New Object command on the Edit menu, or you can copy an object from the component and paste it into the container, as shown in steps 11 and 12.

  13. Close HIERSVR and Container.


Creating a Starter OLE Container

AppWizard contains options that make it easy to create a starter application with OLE container functionality.

To create a starter OLE container application

  1. On the File menu, click New.

    The New dialog box appears.

  2. Click the Projects tab.

  3. Click MFC AppWizard (exe).

  4. In the Project Name box, type Container.

    AppWizard creates a project directory with this name under the main (root) directory specified in the Location box.

  5. If necessary, use the Location box to specify a different root directory for the Container project files that AppWizard creates under the Container project directory.

  6. If any check boxes other than Win32 appear in the Platforms box, clear them.

  7. Click OK.

    AppWizard creates the project directory, and the MFC AppWizard - Step 1 dialog box appears. Click Next to accept the default options.

  8. Click Next in the MFC AppWizard - Step 2 of 6 dialog box to accept the default options.

  9. In the MFC AppWizard - Step 3 of 6 dialog box, click Container, and then click Next to continue.

  10. In the MFC AppWizard - Step 4 of 6 dialog box, click Advanced.

    The Advanced Options dialog box appears.

  11. If necessary, click the Document Template Strings tab.
    • In the File extension box, type ctr without a period.

      The extension is reflected in the Filter name box.

    • In the Doc type name box, change “Contai” to “Contr”.

    • In the Filter name box, change the entry to read “Container Files (*.ctr)”.

    • Click Close, and Next to continue to Step 5.
  12. Click Next again to accept the default options for MFC AppWizard - Step 5 of 6.

  13. In the MFC AppWizard - Step 6 of 6 dialog box, change the defaults that AppWizard suggests for class CContainerCntrItem. Select the class and make the following changes:
    • In the Class name box, change the class name to CContainerItem.

    • In the Header file box, change the filename to ContainerItem.h.

    • In the Implementation file box, change the filename to ContainerItem.cpp.
  14. Click Finish.

  15. The New Project Information dialog box appears, summarizing the settings and features AppWizard will generate for you when it creates your project.

  16. Click OK in the New Project Information dialog box.

    AppWizard creates the starter files for Container and opens the project.

In the next topic, you will build the starter Container application AppWizard just created and try out some of the features it already has. Then, you will modify the code AppWizard created to add more features.


Trying Out the Newly Created OLE Container Application

AppWizard provides a starter OLE container application that already has a lot of the underlying architecture that most OLE container applications need. Build this new AppWizard-provided application and try it out.

To build the starter OLE container application

  • On the Build menu, click Build Container.exe.

This builds the default version of the container application, producing the file Container.exe under your project directory.

At this point, Container already has many of the features you saw in the preview of the completed Container application, but it is missing several capabilities, such as:

  • Inserting more than one OLE object

  • Selecting outside of the OLE object (back in the Container area of the document)

You will add this and other functionality during this lesson. First see for yourself the built-in capabilities of the starter Container application.

Note Before you can preview the Container application using Scribble, you need to register Scribble as described in Registering an OLE Component Application earlier in this lesson.

To create a new Scribble drawing within Container

  1. On the Edit menu, click Insert New Object.

  2. In the Insert Object dialog box, select Scribb Document from the Object Type list box.

  3. Click OK.

    A new Scribble object is activated in place. No additional code is required to implement the Insert New Object command on the Edit menu.

    Notice how Container’s menu is merged with Scribble’s and how Container’s toolbar is replaced by one provided by Scribble. Again, this is already working so no additional code is required here.

To edit the in-place activated object

  1. Drag the mouse to draw in the embedded Scribble object.

  2. Try out Scribble’s menu and toolbar commands in place, such as the Pen Width command.

  3. Press ESC to deactivate the Scribble object.

    Notice that the user interface (ESC) for canceling in-place editing mode for the selected object is already incorporated into the starter AppWizard-created application.

  4. Click the Scribble object to reactivate it.

  5. On the Edit menu, point to Scribb Object, and then click Edit.

    Edit activates the object in place inside the container. (Open fully opens the Scribble server in its own window.)

    Note that the Scribble server provides the tracker rectangle; Container provides the tracker only when the object is not activated in place.

  6. Exit Container.

The AppWizard-provided container application already has a lot of OLE container functionality, but it is still missing some basics that are implemented in the remainder of this tutorial.


Examining AppWizard-Provided Code

The following description of most of the container-specific code provided by AppWizard will help you gain a preliminary understanding of how MFC OLE container support works. If you add container support to an existing MFC application, this description will help you identify the code you need to manually add. The code in each of the following classes is described below.

  • CContainerApp

  • CContainerView

  • CContainerItem


CContainerApp

Tip Use ClassView to jump to the CContainerApp::InitInstance function and examine the code. Click the ClassView tab and expand CContainerApp to display its member functions. Double-click InitInstance to jump directly to its code.

CContainerApp::InitInstance

AppWizard provides the application’s InitInstance function as follows:

  • Initializes the OLE libraries by calling AfxOleInit.

  • Calls CDocTemplate::SetContainerInfo to assign the menu and accelerator resources that are used when an embedded item is activated in place. AppWizard gives the menu and accelerator resources the same identification: IDR_CONTRTYPE_CNTR_IP.

    The resulting menu looks like this:

    File | | Window

    The two separator bars in the menu tell the framework where to insert pop-up menus provided by the server when the embedded item is activated in place.

    The accelerator resource reflects that fewer accelerators are provided by the container application when an embedded item is activated in place, because the server provides accelerators specific to the activated item.


CContainerView

Tip Use ClassView to jump to the CContainerView member functions that are discussed in this section. Follow along by reading the code in ContainerView.cpp and comparing the member functions to their descriptions below.

Class CContainerView is derived from CView. The member CContainerItem* m_pSelection points to the currently selected OLE object. If no object is selected, its value is NULL.

CContainerView::OnDraw

The AppWizard-provided implementation of CContainerView::OnDraw relies on the simplifying assumption that there is only one object to be drawn, namely the sole m_pSelection object. Later you will replace this implementation with code that draws the multiple client items (embedded OLE objects) contained in the document.

CContainerView::IsSelected

The AppWizard-provided implementation of CContainerView::IsSelected returns TRUE if the specified CObject is the m_pSelection object. This code is used without changes for the Container application, which has a simple single-selection user interface. For an example of multiple selection, see the DRAWCLI sample application.

CContainerView::OnInsertObject

The AppWizard-provided implementation of CContainerView::OnInsertObject creates a standard COleInsertDialog object and displays the dialog box. OnInsertObject is the command handler for the Insert New Object command on the Edit menu. It then creates a COleClientItem-derived object and calls the CreateItem member function of the COleInsertDialog object to create the embedded object using the information specified by the user.

CContainerView::OnSetFocus

The AppWizard-provided implementation of CContainerView::OnSetFocus changes the focus from the view to an embedded OLE item if the embedded item is currently activated in place. This is exactly the implementation needed by Container and by most container applications.

CContainerView::OnSize

The AppWizard-provided implementation of CContainerView::OnSize determines if there is an OLE item (COleClientItem) currently activated in place. If so, the COleClientItem is notified that the clipping rectangle of the item has changed. This allows the server to know how much of the object is visible. When the size of the window changes, so does the size of the clipping rectangle. For example, the sample application HIERSVR uses this mechanism to implement scrolling and keyboard movement correctly.


CContainerItem

Tip Use ClassView to jump to the CContainerItem member functions that are discussed in this section. Follow along by reading the code in ContainerItem.cpp and comparing the member functions to their descriptions below.

Class CContainerItem is derived from COleClientItem. From the container application’s perspective, a COleClientItem object represents an embedded item, something it can draw and edit. The life of this object spans the life of the container document, as long as the particular item is embedded in the document. A container application typically creates a COleClientItem object in its implementation of the Insert Object command. Indeed, the implementation of CContainerView::OnInsertObject provided by AppWizard does create the CContainerItem object, as explained earlier. The application explicitly deletes a COleClientItem object only in certain cases, such as when the user presses DEL when this item is selected or when the entire containing document is destroyed.

CContainerItem::OnChange

The AppWizard-provided implementation of CContainerItem::OnChange simply calls OnChange in the base class, COleClientItem, and then, just to be safe, invalidates all views of the document.

COleClientItem::OnGetItemPosition

The framework calls COleClientItem::OnGetItemPosition during in-place activation when OLE needs to determine the location of the item. The AppWizard-provided implementation arbitrarily sets the rectangle of the item to (10, 10, 210, 210). In the next lesson, you will change this implementation to reflect the actual position and size of the embedded item.

COleClientItem::OnChangeItemPosition

The framework calls COleClientItem::OnChangeItemPosition on behalf of a server to change the position of the in-place window, usually as a result of the server window being resized or the extent of the server window being changed. The AppWizard-provided implementation of the OnChangeItemPosition function calls the base class COleClientItem::OnChangeItemPosition, which in turn calls COleClientItem::SetItemRects to move or resize the item to the new position or size.


Implementing the Client Item Rectangle

The AppWizard-provided implementation of CContainerItem does most of the work needed for Container, but some of its functionality needs to be enhanced.

To implement the client item rectangle

  1. Declare m_rect in the public attributes section of ContainerItem.h:
    CRect m_rect;  // position within the document

    The AppWizard-provided implementation of CContainerItem assumed an arbitrary rectangle that locates the object in the container document. A CRect is needed to store the location and size of the object.

  2. Use ClassView to jump to the CContainerItem constructor in ContainerItem.cpp and initialize m_rect. Replace the //TODO comment with the following code:
    m_rect.SetRect(10, 10, 50, 50);
  3. Use ClassView to jump to OnGetItemPosition and replace its default implementation with the following code (leave the ASSERT_VALID(this) line):
    // return rect relative to client area of view
rPosition = m_rect;

    The AppWizard-provided implementation arbitrarily sets the rectangle to (10, 10, 210, 210) when requested by the framework. Because the rectangle for each CContainerItem item is now being tracked by CRectTracker, the framework’s request is satisfied by returning the CRect member, m_rect.

    In Container Step 2, this implementation is replaced with one that allows the server to negotiate the size of the object.

    The framework calls COleClientItem::OnChangeItemPosition on behalf of a server to change the position of the in-place window. The CContainerItem updates its CRect m_rect according to the value requested by the framework. This means that the container document has changed. Thus views need to be notified and the document needs to be marked as dirty according to normal framework document/view rules.

    In Container Step 2, the simple UpdateAllViews call is replaced with smart invalidation.

  4. Complete the implementation of OnChangeItemPosition by adding the following code in place of the //TODO comment:
    GetDocument()->UpdateAllViews(NULL);
m_rect = rectPos;

// mark document as dirty
GetDocument()->SetModifiedFlag();
  5. Serialize the CRect m_rect member variable in CContainerItem::Serialize.
    • Replace the //TODO comment for storing code with:
    ar << m_rect;
    • Replace the //TODO comment for loading code with:
    ar >> m_rect;
  6. Save the header and implementation files.


Implementing Hit Testing and Selection

The AppWizard-provided starter application initially supports only one embedded object. This part of the tutorial adds support for multiple objects by implementing hit testing and selection. Hit testing determines which of the multiple objects lies at a given point.

In the next two procedures, you’ll use ClassView to add two helper functions to class CContainerView: HitTestItems and SetSelection. These functions implement hit testing and selection.

To implement hit testing

  1. In ClassView, right-click the CContainerView class icon.

  2. From the pop-up menu, click Add Member Function.

    The Add Member Function dialog box appears.

  3. Fill in the dialog box as follows:
    • In the Function Type box, type CContainerItem*.

    • In the Function Declaration box, type the following:
      HitTestItems(CPoint point)
    • In the Access area, select Public.

    • Click OK.

    ClassWizard adds the declaration to the header file and creates a starter definition in the implementation file.

  4. In ContainerView.cpp, type the following code to fill in the function definition for HitTestItems:
    CContainerDoc* pDoc = GetDocument();
CContainerItem* pItemHit = NULL;
POSITION pos = pDoc->GetStartPosition();
while (pos != NULL)
{
CContainerItem* pItem = (CContainerItem*)pDoc->GetNextItem(pos);
if (pItem->m_rect.PtInRect(point))
pItemHit = pItem;
}
return pItemHit;    // return top item at point

To implement selection

  1. Repeat steps 1 and 2 from the previous procedure to invoke the Add Member Function dialog box.

  2. Fill in the dialog box as follows:
    • In the Function Type box, type void.

    • In the Function Declaration box, type the following:
      SetSelection(CContainerItem* pItem)
    • In the Access area, select Public.

    • Click OK.
  3. In ContainerView.cpp, fill in the implementation code for SetSelection:
    // close in-place active item
if (pItem == NULL || m_pSelection != pItem)
{
COleClientItem* pActiveItem
= GetDocument()->GetInPlaceActiveItem(this);
if (pActiveItem != NULL && pActiveItem != pItem)
pActiveItem->Close();
}
Invalidate();
m_pSelection = pItem;
  4. Save the header and implementation files.

The above implementation is “lazy” in that it invalidates the entire client area of the view whenever the selection changes. In Container Step 2, this implementation is replaced with smarter invalidation.


Implementing Activation by Using a Mouse Click

Container has a standard user interface for selecting and activating embedded objects. A single click selects an object; a double-click activates it. If the object is selected, the user can move or resize it, or in general, manipulate the object as a whole. If the object is activated in place, the user can edit it.

Implementing the OnLButtonDown handler so that a single click selects the embedded object follows this scheme:

  • Call HitTestItems to find the CContainerItem at the point where the mouse was clicked.

  • Set the selection to be this CContainerItem. Note that if no CContainerItem is located at the point where the mouse was clicked, nothing (NULL) is selected.

  • If something is selected, set up a tracker rectangle (CRectTracker) around the selected object. A CRectTracker object is short lived. It exists only during the time a mouse event is being handled, or as you will see later, during the time a window is being repainted. In the case of a single click, the CRectTracker paints a rectangle with resize handles around the object.

    If the item is clicked, CRectTracker::Track captures the mouse, enabling the user to drag the tracker rectangle around on the screen and to:

    • Resize the item if the click was on a handle.

    • Drag the item if the click was inside the rectangle.

    When the user releases the mouse button, CRectTracker updates its public member variable, m_rect, which represents the new size of the object.

  • If the user has resized the object (indicated by a value of TRUE being returned from CRectTracker::Track), update the m_rect of the CContainerItem object.

To implement the OnLButtonDown mouse handler

  1. Using WizardBar, select CContainerView from the Class list.

  2. Click the action arrow located on the right end of WizardBar.

  3. Click Add Windows Message Handler.

    The New Windows Message and Event Handlers dialog box appears.

  4. From the New Windows messages/events list box, select WM_LBUTTONDOWN.

  5. Click Add and Edit.

  6. Implement CContainerView::OnLButtonDown in ContainerView.cpp by replacing the //TODO comments with the following code.
    CContainerItem* pItemHit = HitTestItems(point);
SetSelection(pItemHit);

if (pItemHit != NULL)
{
CRectTracker tracker;
SetupTracker(pItemHit, &tracker);

UpdateWindow();
if (tracker.Track(this, point))
{
Invalidate();
pItemHit->m_rect = tracker.m_rect;
GetDocument()->SetModifiedFlag();
}
}

    Note Your code should come before, but not replace, the call to the base class (CView) that WizardBar adds to the starter handler.

    For now, the entire client area of the view is invalidated. Smarter invalidation is implemented in Step 2.

The helper function SetupTracker sets up the styles of the tracker rectangle according to the state of the CContainerItem object, such as whether it has been selected.

To implement the helper function CContainerView::SetupTracker

  1. In ClassView, right-click the class CContainerView.

  2. From the pop-up menu, click Add Member Function.

  3. Fill in the Add Member Function dialog box as follows:
    • In the Function Type box, type void.

    • In the Function Declaration box, type:
    SetupTracker(CContainerItem* pItem, CRectTracker* pTracker)
    • In the Access area, select Public, and click OK.
  4. Implement the helper function with the following code:
    pTracker->m_rect = pItem->m_rect;

if (pItem == m_pSelection)
pTracker->m_nStyle |= CRectTracker::resizeInside;

if (pItem->GetType() == OT_LINK)
pTracker->m_nStyle |= CRectTracker::dottedLine;
else
pTracker->m_nStyle |= CRectTracker::solidLine;

if (pItem->GetItemState() == COleClientItem::openState ||
pItem->GetItemState() == COleClientItem::activeUIState)
{
pTracker->m_nStyle |= CRectTracker::hatchInside;
}

The OnLButtonDblClick handler needs to be implemented so that if the user double-clicks, the object is opened (OLEIVERB_OPEN). How the object is opened depends on whether the server supports in-place editing. If the user presses CTRL while double-clicking, the Open verb of the object should be called. Otherwise, call the primary verb, the meaning of which is determined by the server.

To implement the OnLButtonDblClick mouse handler

  1. Using WizardBar, select CContainerView from the Class list.

  2. Click the action arrow located on the right end of WizardBar.

  3. Click Add Windows Message Handler.

    The New Windows Message and Event Handlers dialog box appears.

  4. From the New Windows messages/events list box, select WM_LBUTTONDBLCLK.

  5. Click Add and Edit.

  6. Implement CContainerView::OnLButtonDblClk in ContainerView.cpp by replacing the //TODO comment inside the starter handler code that WizardBar generates with the following code:
    OnLButtonDown(nFlags, point);

if (m_pSelection != NULL)
{
m_pSelection->DoVerb(GetKeyState(VK_CONTROL) < 0 ?
OLEIVERB_OPEN : OLEIVERB_PRIMARY, this);
}

    Note Your code should come before, but not replace, the call to the base class (CView) that WizardBar adds to the starter handler.


Implementing Tracker Rectangles for Resizing and Moving Objects

When the user moves the cursor over a selected object, the cursor changes its shape to indicate the kind of manipulation available. For example, if the cursor is over the resize handle at the upper-middle or lower-middle side of the tracker rectangle, the cursor changes to a two-way vertical arrow to indicate that the user can drag the upper or lower edge of the object. The framework’s CRectTracker class implements this. All you need to do is call CRectTracker::SetCursor.

To implement special cursors for the tracker

  1. Using WizardBar, select CContainerView from the Class list.

  2. Click the action arrow located on the right end of WizardBar.

  3. Click Add Windows Message Handler.

    The New Windows Message and Event Handlers dialog box appears.

  4. From the New Windows messages/events list box, select WM_SETCURSOR.

  5. Click Add and Edit.

  6. Replace the highlighted //TODO comment with the following code to implement the CContainerView::OnSetCursor handler:
    if (pWnd == this && m_pSelection != NULL)
{
// give the tracker for the selection a chance
CRectTracker tracker;
SetupTracker(m_pSelection, &tracker);
if (tracker.SetCursor(this, nHitTest))
return TRUE;
}

    Note Your code should come before, but not replace, the call to the base class (CView) that WizardBar adds to the starter handler:

    return CView::OnSetCursor(pWnd, nHitTest, message);


Drawing the Embedded Objects

The AppWizard-provided implementation of CContainerView::OnDraw simply draws the one embedded object pointed to by m_pSelection. Now that Container supports multiple embedded objects, OnDraw must be reimplemented accordingly.

To support drawing of multiple embedded objects

  • Use ClassView to jump to the OnDraw function in class CContainerView. Replace the //TODO comment and the implementation provided by AppWizard with the following code (start after the ASSERT statement):
    // draw the OLE items from the list
POSITION pos = pDoc->GetStartPosition();
while (pos != NULL)
{
// draw the item
CContainerItem* pItem = (CContainerItem*)pDoc->GetNextItem(pos);
pItem->Draw(pDC, pItem->m_rect);

// draw the tracker over the item
CRectTracker tracker;
SetupTracker(pItem, &tracker);
tracker.Draw(pDC);
}

Again, a CRectTracker object is used to draw the rectangle and possibly resize handles around the embedded object. The CRectTracker object lives only long enough to draw during this particular repaint. Another CRectTracker object was used, as you saw above, to handle a click on one of the resize handles. Yet another CRectTracker object was used to change the shape of the cursor when it was over one of the resize handles. Each of these CRectTracker objects is short lived; that is, they are automatic (local) variables of the respective Windows event handler. They were all initialized with the common code you added earlier to SetupTracker.


Deleting Embedded Objects

To clear, or delete, an embedded object, you must:

  • Add a Delete command to the Edit menu in the IDR_CONTRTYPE menu and assign its command ID

  • Create handlers for the new Delete command

  • Implement code for the handlers.

The following procedure explains how to do these tasks.

Note For a refresher on working with the menu editor, complete the procedure outlined in Add the Clear All Command to Scribble's Edit Menu in Scribble Lesson 5.

To delete an embedded object

  1. Use the menu editor to add a Delete command and separator to the Edit menu in the IDR_CONTRTYPE menu resource:

    Edit

    ...

    Paste &Special

    -------------

    &Delete

    -------------

  2. Assign the standard framework command ID, ID_EDIT_CLEAR, to the Delete command. Note that the command prompt is already defined for you by the framework:
    Erase the selection\nErase

    (The prompt doesn’t show until you’re out of edit mode for this menu command.)

  3. Save the resource file.

  4. Using WizardBar, create a pair of ON_COMMAND and ON_UPDATE_COMMAND_UI handlers for ID_EDIT_CLEAR in ContainerView.cpp.

  5. Implement OnEditClear as follows:
    if (m_pSelection != NULL)
{
m_pSelection->Delete();
m_pSelection = NULL;
GetDocument()->UpdateAllViews(NULL);
}
  6. Implement OnUpdateEditClear as follows:
    pCmdUI->Enable(m_pSelection != NULL);

To delete an embedded object in a container, simply call the object’s COleClientItem::Delete function.


Building and Running Container Step 1

Build Container Step 1. When it compiles and links successfully, run the program. Here are some things to try:

  • From the Edit menu, choose Insert New Object to create a new Scribble drawing within the Container document. When the Insert Object dialog box appears, select Scribb Document in the Object Type list box.

    Notice that the Scribble object initially has a size of (10, 10, 50, 50), as determined by the CContainerItem constructor. Container does not consult the server about the initial size of the object.

  • Resize the object to make it bigger and draw in the new object.

  • Click outside the Scribble object. It is now properly deselected.

  • Click the Scribble object.

    Selection now works, and the cursor changes to a four-way arrow over the object.

  • Drag the object around, and resize it.

    Rectangle tracking is now working.

At this point you can try to insert linked items:

  • Start HIERSVR stand-alone, create a file, and save it to disk.

  • Close HIERSVR.

  • From the Container Edit menu, choose Insert New Object.

  • Click the Create from File option.

  • Type the name of the HIERSVR file just created, or use the Browse button to select it.

  • Click the Link check box.

  • Click OK.

You will see that the item now has a dashed border, and that double-clicking the item opens it instead of activating it, as would be expected from linked items.

This completes Step 1 of Container. In the next lesson, Refining OLE Container Functionality, you will add the Copy and Paste commands to the Edit menu, implement smart invalidation, and implement better coordination with the server to determine the size of contained objects.


Refining OLE Container Functionality

Container Application, Lesson 2

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

As implemented in Step 1, Container is almost fully functional as a general-purpose OLE container application, but it needs some refinement. To accomplish this, Step 2 adds the following:

  • Implementation of the Copy and Paste commands on the Edit menu.

  • Implementation of smart invalidation that optimizes Container to redraw only those objects that need to be redrawn, rather than redrawing all objects whenever one is changed.

  • Better coordination with the server to determine the size of contained objects.

    To demonstrate why this is necessary, you will be asked to run Step 1 of Container. For that reason, you should save the Step 1 version of Container.exe before you start working on Step 2.

You will probably need to make similar refinements in your container applications, although the details may vary.

Suggested Reading

  • OLE Background: Containers and Servers, Visual C++ Programmer’s Guide

  • ActiveX Control Containers, Visual C++ Programmer’s Guide

  • COleClientItem::CopyToClipboard, Microsoft Foundation Class Reference


Adding Command Handlers for Copy and Paste

AppWizard has already added the Copy and Paste menu items to Container’s Edit menu, but these commands still need to be implemented. The COleDocument implementation already provides an UPDATE_COMMAND_UI handler for the Paste command. This handler enables the Paste command if there is anything on the Clipboard.

To implement the Copy command on the Edit menu

  1. On WizardBar, select CContainerView and ID_EDIT_COPY. Click the action button arrow located on the right end of WizardBar and then click Add Windows Message Handler.

  2. In the New Windows Message and Event Handlers dialog box, click Add Handler for both the COMMAND and UPDATE_COMMAND_UI handlers for ID_EDIT_COPY and accept the default function names, OnEditCopy and OnUpdateEditCopy, respectively, in the Add Member Function dialog box.

  3. To implement the Copy command on the Edit menu, fill in the handler that WizardBar creates with the code below :
    if (m_pSelection != NULL)
m_pSelection->CopyToClipboard();

    The Copy command on the Edit menu copies the contents of the current selection to the Clipboard. Implementing the Copy command is easy because the framework function COleClientItem::CopyToClipboard does all the work.

  4. Fill in the handler for OnUpdateEditCopy with the following code:
    pCmdUI->Enable(m_pSelection != NULL);

    The UPDATE_COMMAND_UI handler for the Copy command enables the command if there is an active selection; otherwise, the command is disabled.

To implement the Paste command on the Edit menu

  1. On WizardBar, select CContainerView and ID_EDIT_PASTE. Click the action button arrow located on the right end of WizardBar and then click Add Windows Message Handler.

  2. In the New Windows Message and Event Handlers dialog box, click Add and Edit for the COMMAND handler for ID_EDIT_PASTE and accept the default function name, OnEditPaste, in the Add Member Function dialog box.

  3. Implement the Paste command with the following code:
    CContainerItem* pItem = NULL;

TRY
{
// Create new item connected to this document.
CContainerDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pItem = new CContainerItem(pDoc);
ASSERT_VALID(pItem);

// Initialize the item from clipboard data
if (!pItem->CreateFromClipboard())
AfxThrowMemoryException();    // any exception will do
ASSERT_VALID(pItem);

// update the size before displaying
pItem->UpdateFromServerExtent();

// set selection to newly pasted item
SetSelection(pItem);
pItem->InvalidateItem();
}
CATCH(CException, e)
{
if (pItem != NULL)
{
ASSERT_VALID(pItem);
pItem->Delete();
}
AfxMessageBox(IDP_FAILED_TO_CREATE);
}
END_CATCH

The Paste command on the Edit menu is somewhat like the Insert New Object command on the Edit menu in that it creates a new COleClientItem object. Compare the above implementation of OnEditPaste with the one that AppWizard provided for OnInsertObject. Both share some code for constructing a new CContainerItem.

The difference is that OnInsertObject initializes the item based on information requested from the user by means of a COleInsertDialog object as shown here:

   // Initialize the item from the dialog data.
   if (!dlg.CreateItem(pItem))
      AfxThrowMemoryException();   // any exception will do
   ASSERT_VALID(pItem);

   // If item created from class list (not from file) then launch
   //   the server to edit the item.
   if (dlg.GetSelectionType() == COleInsertDialog::createNewItem)
      pItem->DoVerb(OLEIVERB_SHOW, this);

The OnEditPaste function initializes the item from the Clipboard, using COleClientItem::CreateFromClipboard as shown below:

   if (!pItem->CreateFromClipboard())
      AfxThrowMemoryException();


Using Smart Invalidation

The next task in Step 2 is to implement smart invalidation. Smart invalidation involves several tasks:

  • Defining the update hint

  • Receiving the hint and invalidating the view

  • Centralizing the sending of the update hint

  • Invalidating selected and deselected objects

  • Invalidating an object moved by the server

  • Invalidating the tracked object


Define the Update Hint

The first task is to define the update hint.

To define the update hint

  • Add the following two #define values to ContainerDoc.h, prior to the class declaration:
    #define HINT_UPDATE_WINDOW            0
#define HINT_UPDATE_ITEM                1

    The two #define HINT_ values are used for the LPARAM lHint value passed to CContainerView::OnUpdate, which you’ll create in the next step. The first hint value, HINT_UPDATE_WINDOW, has the framework’s default lHint value of 0, which means “no hint”: in other words, it is an instruction to invalidate the entire client area of the view. The second, HINT_UPDATE_ITEM, is used to invalidate the rectangle of the view’s client area occupied by the COleClientItem object. That rectangle is passed to OnUpdate using the pHint parameter.


Receive the Hint and Invalidate the View

The framework provides a mechanism for invalidating portions of a view by using the lHint and pHint parameters of CView::OnUpdate. This “update hint” mechanism is described in the Scribble tutorial and is used in Container.

To receive the hint and invalidate the view

  1. On the View menu, click ClassWizard.

  2. Click the Message Maps tab.

  3. In the Class name drop-down list box, select CContainerView.

  4. In the Messages list box, select OnUpdate, and click Add Function. This adds the OnUpdate function declaration to CContainerView.h and a starter function definition to CContainerView.cpp.

  5. Click Edit code to jump to the function definition in CContainerView.cpp.

  6. Implement OnUpdate by replacing the //TODO comment with the following code:
    switch (lHint)
{
case HINT_UPDATE_WINDOW:    // invalidate entire window
Invalidate();
break;
case HINT_UPDATE_ITEM:        // invalidate single item
{
CRectTracker tracker;
SetupTracker((CContainerItem*)pHint, &tracker);
CRect rect;
tracker.GetTrueRect(rect);
InvalidateRect(rect);
}
break;
}

    The rectangle to be invalidated for HINT_UPDATE_ITEM should include the area that might be occupied by a tracker around the object. The implementation of CContainerView::OnUpdate takes this into account.


Centralize the Sending of Update Hints

There are several occasions when Container needs to send the HINT_UPDATE_ITEM hint for a CContainerItem object, for example, when the object is selected, deselected, resized, or otherwise changed. The HINT_UPDATE_ITEM hint must be passed to OnUpdate in conjunction with the CObject* pHint parameter, which is a pointer to CContainerItem. Thus, it makes sense to have the CContainerItem object itself send the update hint by using the document’s UpdateAllViews function. CContainerItem::InvalidateItem is a helper function that you can call whenever you want to send the hint.

To centralize the sending of update hints in the CContainerItem object

  1. In ClassView, right-click the class CContainerItem.

  2. From the pop-up menu, click Add Member Function.

  3. Fill in the Add Member Function dialog box as follows:
    • In the Function Type edit box, type void.

    • In the Function Declaration edit box, type:
      InvalidateItem()
    • In the Access area, select Public, and click OK.
  4. To implement CContainerItem::InvalidateItem, type the following code inside the handler:
    GetDocument()->UpdateAllViews(NULL, HINT_UPDATE_ITEM, this);

    Note that the framework keeps track of which document object owns the CContainerItem object and therefore implements CContainerItem::GetDocument.


Invalidate Selected and Deselected Objects

To eliminate unnecessary repainting whenever the user changes the selected object, only the old and the new selected objects need to be invalidated. This results in smarter repainting than simply invalidating the entire client area of the view.

To add update hints to selection code

  • Use ClassView to jump to the SetSelection member function in CContainerView and replace the following code:
    Invalidate();
m_pSelection = pItem;

    with this code:

    // update view to new selection
if (m_pSelection != pItem)
{
if (m_pSelection != NULL)
OnUpdate(NULL, HINT_UPDATE_ITEM, m_pSelection);

m_pSelection = pItem;
if (m_pSelection != NULL)
OnUpdate(NULL, HINT_UPDATE_ITEM, m_pSelection);
}


Invalidate Tracked Object

When the user clicks an object, the tracker needs to be drawn around the newly selected object. This code invalidates the extra area occupied by the tracker.

To add update hints to OnLButtonDown

  • Use ClassView to jump to the implementation of CContainerView::OnLButtonDown and replace the following code:
    Invalidate();
pItemHit->m_rect = tracker.m_rect;

    with this code:

    pItemHit->InvalidateItem();
pItemHit->m_rect = tracker.m_rect;
pItemHit->InvalidateItem();


Invalidate Object Moved by Server

The framework calls COleClientItem::OnChangeItemPosition whenever the server requests a change in the position of the in-place activated object. This is one of several occasions for which you can implement smart repainting.

To send an update hint when the position of the CContainerItem object changes

  • Use ClassView to jump to the implementation of CContainerItem::OnChangeItemPosition and replace the following code:
    GetDocument()->UpdateAllViews(NULL);
m_rect = rectPos;

    with this code:

    InvalidateItem();
m_rect = rectPos;
InvalidateItem();


Coordinating with Server to Determine Size of Object

The following exercise demonstrates why Container needs to coordinate better with servers to determine the size of embedded objects.

To demonstrate why coordinating with the server is necessary

  1. If you have not already run the HIERSVR sample, do so now. Run HIERSVR once to register this OLE component application and then close it.

  2. Similarly, run Step 1 of Container, but leave it running. (You can either use your version, or the sample source version.)

  3. On the Container Edit menu, click Insert New Object.

  4. Select MFC Hierarchy List as the object type.

    Notice that the initial size of the HIERSVR object is (10, 10, 50, 50), as determined by the CContainerItem constructor. Container does not give HIERSVR the opportunity to set the initial size of the object.

  5. From HIERSVR’s Edit menu, click Add Node to add a second node.

    Fill in a name for the node; the other options in the Add Node dialog box don’t matter for the purpose of this demonstration.

    Notice that Container correctly increases the height of the object to accommodate the new node. You can add more nodes, and Container continues to increase the height of the object. In Step 1, the implementation of CContainerItem::OnChangeItemPosition changes the height of the in-place activated object at the request of HIERSVR.

  6. Deactivate the HIERSVR object, then click once to select it.

  7. From the Edit menu, click Hierarchy List Object, then click Open from the submenu.

    This fully opens the HIERSVR server application. Arrange HIERSVR and Container on the screen so you can see both applications at once.

  8. In HIERSVR, from the Edit menu click Add Node to add another node.

    Notice that the size of the object in Container does not change to accommodate the new node. Rather, it stays the same size and compresses the nodes using a smaller font, so that the N+1 nodes now occupy the same space as the original N nodes. Add more nodes, and they become more and more compressed in the same space in the container.

  9. Close both applications.

Why does OnChangeItemPosition change the size of the in-place window when a new HIERSVR node is added, but not if it is being updated when HIERSVR is running fully opened?

Because OnChangeItemPosition is called by the framework only when the object is in-place activated. The server temporarily provides the object with its own in-place window and calls to give the container a chance to customize the size of the in-place window.

When the server is fully opened, the situation is much different (although it appears to be the same): When the server is fully opened, the object in the container is selected but not activated in place. When the user edits the fully opened object so that its natural size changes, as in the case of adding a node in HIERSVR, the server indirectly (through the framework) calls CContainerItem::OnChange instead of OnChangeItemPosition. At this time, Container needs to find out the new natural size of the object from HIERSVR. It does this by calling COleClientItem::GetCachedExtent.

COleClientItem::GetCachedExtent asks the server for the natural extent of the object. The natural extent is the size of the object as it would appear on the printed page (in MM_HIMETRIC units). In HIERSVR’s case, the natural extent reflects (1) the font size that the user can specify with the Change Font command on the Tree menu, and (2) the number of nodes in the HIERSVR object.

The CContainerItem::OnChange function is not the only place where Container needs to call COleClientItem::GetCachedExtent to get the natural extent of the object and then set the m_rect of the CContainerItem. Therefore, you will implement the helper function UpdateFromServerExtent as described in the next topic, Get the Extent of the CContainerItem Object from the Server.


Get the Extent of the CContainerItem Object from the Server

To get the extent of the CContainerItem object from the server, and update the m_rect of the container item, implement the helper function CContainerItem::UpdateFromServerExtent.

To get the extent of a CContainerItem object

  1. In ClassView, right-click the class CContainerItem.

  2. From the pop-up menu, click Add Member Function.

  3. Fill in the Add Member Function dialog box as follows:
    • In the Function Type box, type void.

    • In the Function Declaration box, type the following:
      UpdateFromServerExtent()
    • Under Access, select Public.

    • Click OK.
  4. In ContainerItem.cpp, implement the helper function by entering the following code:
    CSize size;
if (GetCachedExtent(&size))
{
// OLE returns the extent in HIMETRIC units -- we need pixels
CClientDC dc(NULL);
dc.HIMETRICtoDP(&size);

// only invalidate if it has actually changed and also only
// if it is not in-place active.
if (size != m_rect.Size() && !IsInPlaceActive())
{
// invalidate old, update, invalidate new
InvalidateItem();
m_rect.bottom = m_rect.top + size.cy;
m_rect.right = m_rect.left + size.cx;
InvalidateItem();

// mark document as modified
GetDocument()->SetModifiedFlag();
}
}


Update the CContainerItem Rectangle When the Item's Natural Extent Changes

Update the CContainerItem Rectangle When the Item’s Natural Extent Changes

When the fully opened server (for example, HIERSVR) notifies the container about a change (a new node) that affects the natural extent of the object, the CContainerItem rectangle needs to be updated.

To update the CContainerItem rectangle when the item’s natural extent changes

  • Use ClassView to jump to the implementation of OnChange in CContainerItem and replace the lines starting with //TODO with the following code:
    switch (nCode)
{
case OLE_CHANGED:
InvalidateItem();
UpdateFromServerExtent();
break;
case OLE_CHANGED_STATE:
case OLE_CHANGED_ASPECT:
InvalidateItem();
break;
}

Notice that the CContainerItem object has to be invalidated whenever the server sends a notification that the object has changed. The constant values that nCode can assume are defined by the framework.


Update the Rectangle of a Newly Inserted Object

As a user-interface design decision in Container, the rectangle of a newly inserted object is updated to reflect its natural extent, as determined by the server. A container application can ignore the natural extent if, for example, you prefer to clip the object in the rectangle.

To update the rectangle of a newly inserted object to its natural extent

  • Use ClassView to jump to CContainerView::OnInsertObject, just before the comment lines beginning with:
        // If item created from class list...

    insert this code:

        pItem->UpdateLink();
    pItem->UpdateFromServerExtent();

COleClientItem::UpdateLink is called so that if the server is fully open, Container has a visual representation of the newly created item, even though the item hasn’t been changed by the user yet.

To implement smart invalidation of the newly inserted item

  • In the same OnInsertObject function, replace the following code:
        // TODO: reimplement selection as appropriate
    // for your application
    m_pSelection = pItem;
    pDoc->UpdateAllViews(NULL);

    with this code:

        SetSelection(pItem);
    pItem->InvalidateItem();


Building and Running Container Step 2

Build the Step 2 version of Container. It now performs exactly as you previewed it in Lesson 1, in Previewing the Container Application.


Creating an Automation Server

AutoClik: Automation, Lesson 1

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

An “Automation server” is an application that exposes programmable objects to other applications, which are called “Automation clients.” This exposure enables clients to “automate” certain functions by directly accessing objects and using the services they provide.

Exposing objects is beneficial when applications provide functionality useful to other applications. For example, a word processor might expose its spell-checking functionality to other programs. Exposure of objects enables vendors to improve their applications by using the ready-made functionality of other applications. In this way, Automation applies some of the principles of object-oriented programming, such as reusability and encapsulation, at the level of applications themselves.

More important is the support Automation provides to users and solution providers. By exposing application functionality through a common, well-defined interface, Automation makes it possible to build comprehensive solutions in a single general programming language, such as Microsoft Visual Basic, instead of in diverse application-specific macro languages.

This tutorial leads you through the basic steps of implementing an Automation server. You will create a simple Automation server application and test it using the Autodriv sample application included with Visual C++.

If you want to learn about implementing an Automation client application, look at the CALCDRIV sample application and its description in Samples \ MFC Samples \ MFC OLE Samples in the online documentation.

Note This tutorial assumes that you are already familiar with the development environment and Visual C++, as well as the basics of the Microsoft Foundation Class Library (MFC). If you are not, complete the Scribble tutorial before you begin this tutorial.

Suggested Reading in Visual C++ Samples

  • Autodriv

  • AutoClik


The Tutorial Example: AutoClik

In this tutorial you will create AutoClik, a simple Automation server application. When running as a stand-alone application, AutoClik displays text at the last point at which the user clicked the mouse. The user can change the text displayed by specifying it in a dialog box. When running as an Automation server, AutoClik allows Automation clients to simulate both the mouse clicking and the changing of text (without opening the dialog box).

An Automation server is not necessarily an ActiveX object server; AutoClik isn’t. AutoClik could have been implemented as both an Automation server and an ActiveX object server, but this tutorial focuses entirely on adding Automation server functionality to an application.

How Automation Clients Access Automation Objects

For an Automation client to drive an Automation server, the client must gain knowledge of one or more “dispatch interfaces” of the server. A dispatch interface is the external programming interface of some grouping of functionality exposed by the Automation server. AutoClik provides two dispatch interfaces. The first exposes AutoClik’s mouse clicking and text data entry functions. The second, introduced for tutorial rather than practical reasons, represents a simple structure: a point given by x and y coordinates.

A dispatch interface consists of two types of programming interfaces: properties and methods. AutoClik exposes both. An Automation client can get or set the x and y properties representing the location of the text in AutoClik’s window. Or an Automation client can set the x and y coordinates and the text all at once by using a method with three parameters — x, y, and text.

To exercise AutoClik’s Automation functionality, you will use the Autodriv sample application provided with Visual C++. Autodriv is a simple Automation client application. The following preview of AutoClik illustrates how you can drive an Automation server using Autodriv.


Preview of the AutoClik Application

Before you work through the steps of implementing AutoClik, try out the completed application. This will help you appreciate Automation functionality in general, and AutoClik’s Automation server functionality in particular, from a user’s point of view.

The first step is to register AutoClik. Just as with ActiveX object servers, an Automation server must be registered before it can be driven by any Automation client.

To install and register the AutoClik Automation server

  1. Run AutoClik.exe from the sample source files for Step 3 (see Installing the Sample Files and Previewing the Sample Applications in Scribble Lesson 1 for detailed steps).

    Running AutoClik briefly as a stand-alone application registers it in the system registry as an ActiveX component.

  2. Close AutoClik.

To preview AutoClik

  1. Run Autodriv from the sample source files.

    This starts Autodriv, which launches and displays the AutoClik Test Driver dialog box shown below.

    AutoClik Test Driver Dialog Box

  2. Arrange the open windows on your desktop, minimizing any as necessary, so that AutoClik is visible next to Autodriv’s window, without obscuring it.

    Autodriv launches AutoClik on startup.

    AutoClik Window Next to Autodriv Window

To explore the Automation server features of AutoClik

  1. Note that the x, y, and Text fields in the AutoClik Test Driver dialog box are initially blank. To change this, click the Get All button.

    The current AutoClik coordinates and text are now displayed in Autodriv’s window.

  2. Click around in the AutoClik window.

    Notice that the x, y, and Text fields in Autodriv do not change. That is because the Automation is one direction between Autodriv and AutoClik. Although you can implement an Automation server to notify the Automation client about changes, such as the new coordinates in AutoClik, this feature requires that additional callback/notification interfaces be established so that the Automation client can implement them.

  3. From AutoClik’s Edit menu, click Change Text.

  4. In the Change Text dialog box, change the text to “hello”, and click OK.

  5. In the Autodriv window, click the Get All button.

    It now shows new x, y, and Text values.

  6. Change the x, y, and Text values in Autodriv, and click the Set All button.

    AutoClik displays the changes.

  7. Explore other Autodriv commands:
    Command | Description
    Set x, Set y | Accesses just the x or y coordinate of the text. The x and y properties of AutoClik’s document dispatch interface are exposed by using Get and Set methods. AutoClik’s implementation of SetX and SetY includes updating the window to reflect the change.
    Get Position, Set Position | Changes the x and y coordinates of the text by using AutoClik’s second dispatch interface, which represents a Point.
    Set Text | Changes the Text property of AutoClik, which is directly exposed as a string rather than by using a pair of Get and Set functions. This means that when you choose the Set Text command, AutoClik has no opportunity to detect the change as it did when you chose the Set x, Set y, or Set Position commands. Therefore, AutoClik does not immediately update its window. To do so, you must then click the Refresh Display button.
    Set All | Simultaneously changes AutoClik’s x, y, and Text properties through its SetAllProps method, which uses these three parameters. AutoClik’s implementation of SetAllProps includes immediate updating of the window.
    Get All | Queries the x, y, and Text properties of AutoClik and returns their current values.
    Refresh Display | Updates AutoClik’s window based on the most recent values of x, y, and Text, which might have been previously set using Automation.
    Animate x & y | Updates AutoClik’s x and y coordinates in 20 steps, by individually updating the x property and then the y property.
    Animate Position | Updates AutoClik’s x and y coordinates through its Point interface. Because the Point interface updates both the x and y values at the same time, the animation results in a smooth diagonal movement of the text across AutoClik’s window.


Overview of AutoClik Steps 1, 2, and 3

The AutoClik tutorial consists of three steps: STEP 1, STEP 2, and STEP 3. The sample source files contain a subdirectory for each step. Each step’s subdirectory contains complete source files, and other files needed for the step. You can create the program yourself by working through these steps, or you can use the sample files provided. If you do not have these files on your local drive, you can easily install them. For more information, see Installing the Sample Files in Scribble Lesson 1.

In Step 1, Enabling Automation in an Application, you will learn how to:

  • Create a starter Automation server using the Automation option in AppWizard.

  • Change the external name of the dispatch interface created by AppWizard.

  • Analyze the code created by AppWizard.

  • Implement AutoClik so it can run as a stand-alone application.

In Step 2, Implementing Automation Properties and Methods, you implement the document dispatch interface. You will learn how to use ClassWizard to:

  • Expose the CAutoClickDoc member variable m_pt by using the Get and Set methods in AutoClik’s document dispatch interface.

  • Expose the CAutoClickDoc member variable m_str as a property in AutoClik’s document dispatch interface.

  • Add Automation methods for RefreshWindow, SetAllProps, and ShowWindow.

In Step 3, Implementing Multiple Dispatch Interfaces, you will implement the second Point interface and expose AutoClik’s x and y values by using this Point interface. You will learn how to:

  • Use ClassWizard to create a new CCmdTarget-derived class with a dispatch interface.

  • Implement one dispatch interface with reference to a second dispatch interface.


Enabling Automation in an Application

AutoClik: Automation, Lesson 2

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

In Step 1 of AutoClik, you will:

  • Create a starter Automation server using the Automation option in AppWizard.

  • Change the external name of the dispatch interface created by AppWizard.

  • Analyze the code created by AppWizard.

  • Implement AutoClik so it can run as a stand-alone application.

  • Build and run AutoClik.

Suggested Reading

  • Creating Applications Using AppWizard, Visual C++ Programmer’s Guide

  • Reusing Code: Overview, Visual C++ Programmer’s Guide

  • CDocTemplate::GetDocString, Microsoft Foundation Class Reference

  • Autodriv, Visual C++ Samples


Creating a Starter Automation Server

The following procedure describes values you will enter into the various AppWizard dialog boxes in order to create the AutoClik starter files.

To create a starter Automation server application

  1. On the File menu, click New.

    The New dialog box appears.

  2. Click the Projects tab.

  3. Click MFC AppWizard (exe).

  4. In the Project name box, type AutoClik.

    AppWizard will create a project directory with this name under the main (root) directory specified in the Location box.

  5. If necessary, use the Location box to specify a different root directory for the AutoClik project files that AppWizard creates under the AutoClik project directory.

  6. If any check boxes other than Win32 appear in the Platforms box, clear them.

  7. Click OK.

    AppWizard creates the project directory and the MFC AppWizard – Step 1 dialog box appears.

  8. Click Next in the dialog boxes for AppWizard Steps 1 and 2 to accept the default options.

  9. In the MFC AppWizard – Step 3 of 6 dialog box, click the Automation check box and click Next.

  10. In the MFC AppWizard – Step 4 of 6 dialog box, click the Advanced button.

    The Advanced Options dialog box appears, with the Document Template Strings tab selected.

    • In the File extension box, specify ack.

      The specified file extension is reflected in the Filter Name box.

    • In the File type ID box, edit it to read AutoClick.Document.

      This identifier is initially created by AppWizard, based on the project name. The client application (such as Autodriv) uses this name to access the Automation object, or dispatch interface. This name corresponds to the regFileTypeID string in the CDocTemplate::GetDocString function.

    • Change the Main frame caption to AutoClick (optional).

      This string gets displayed in the title bar of the running application.

    • In the Doc type name box, change AutoCl to AClick (optional).

      Notice that the change is reflected in the File new name (short name) box as well. This name corresponds to the fileNewName string in CDocTemplate::GetDocString.

    • In the Filter name box, change AutoCl Files to AutoClick Files (optional).

    Note The File Type ID and Doc Type Name strings are discussed further in the next topic, Analyzing the Dispatch Interface Name.

  11. Click Close, and then click Next to accept the options for the Step 4 and Step 5 dialog boxes.

  12. Optionally, in the MFC AppWizard – Step 6 of 6 dialog box, make the following changes:
    • Change class name CAutoClikApp to CAutoClickApp.

    • Change class name CAutoClikDoc to CAutoClickDoc. Change the header and implementation files accordingly.

    • Change class name CAutoClikView to CAutoClickView. Change the header and implementation files accordingly.
  13. Click Finish.

    You may be prompted with the following message:

    “A unique class ID already exists in the registration database for this document type. Use existing ID?”

    This message appears if you ran the Step 3 version of AutoClik. Running AutoClik adds an entry to the registry for its document type (AutoClick.Document).

  14. Click Yes. Responding Yes tells AppWizard to overwrite the existing registry entries.

    The New Project Information dialog box appears, summarizing the settings and features AppWizard will generate for you when it creates your project.

    You might want to take a moment to examine the application type, classes, and features that AppWizard automatically provides.

  15. Click OK in the New Project Information dialog box.

    AppWizard creates all necessary files and opens the AutoClik project.


Analyzing the Dispatch Interface Name

The document template string resource is where MFC expects to find information about an application or a particular application’s documents, such as default filename extensions. If the application is an Automation server, MFC also expects to find information specific to Automation.

The name of the dispatch interface is a literal string that Automation clients use to access the Automation server application. If you open the application’s string resource, you can look at, or change, this string.

To examine the document template string resource

  • From ResourceView, expand the String Table folder and double-click the String Table resource.

    This opens the table of string resources for your application.

The ID for the string resource that contains the name of the dispatch interface is IDR_<Doc Type Name>TYPE, created by AppWizard, which is registered in the application’s InitInstance member function. The Doc Type Name appears in the Advanced Options dialog box (which you open by clicking Advanced in the MFC AppWizard – Step 4 of 6 dialog box). For AutoClik, the string ID is IDR_ACLICKTYPE.

To view the strings for a particular resource, open the String Properties dialog box.

To open the string properties page

  • With the application string table open, click Properties on the View menu.

    The String Properties dialog box opens, with the properties for the selected string displayed.

You can see the strings for IDR_ACLICKTYPE in the Caption area as illustrated in the figure below.

IDR_ACLICKTYPE in the String Editor

This string resource consists of several strings separated by newline characters (\n). It contains the following strings:

\nAClick\nAClick\nAutoClick Files (*.ack)\n.ACK\nAutoClick.Document\nAClick Document

The string "AutoClick.Document" is the name, provided by AppWizard, (and modified by you) of the Automation object, or dispatch interface. You modified this value in the Advanced Options dialog box (which you open by clicking Advanced in the MFC AppWizard – Step 4 of 6 dialog box). Autodriv refers to this object name in the OnCreate function of the dialog class:

int CAutoDrivDlg::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   if (CDialog::OnCreate(lpCreateStruct) == -1)
      return -1;   // fail

   if (!m_autoClikObject.CreateDispatch(_T("AutoClick.Document")))
   {
      AfxMessageBox(IDP_CANNOT_CREATE_AUTOCLICK);
      return -1;   // fail
   }
   m_autoClikObject.ShowWindow();

   return 0;   // success
}

Note An Automation server can have more than one Automation object. AutoClik will have two Automation objects. The initial AppWizard-created application has only one Automation object, which is the one identified in the document template string resource described above.


Analyzing AppWizard-Provided Code

Before implementing AutoClik’s basic behavior, let’s look at the AppWizard-provided code that enables Automation server support in AutoClik. The following topics describe the code AppWizard adds for you in the application class, document class, and ODL file.

  • Application Class of an Automation Server

  • Document Class of an Automation Server

  • ODL File for Creating a Type Library


Application Class of an Automation Server

Tip Use ClassView to open views to a project's AppWizard-generated files. If you are unfamiliar with ClassView, read Opening a Class with ClassView in the Scribble tutorial.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Automation Servers

  • ClassWizard: Automation Support

The work of enabling an MFC Automation server application is done mostly in the InitInstance member function of your application’s CWinApp-derived class. AutoClik’s application class is found in AutoClik.cpp. AppWizard provides this code for you.

All MFC OLE applications require the following call to AfxOleInit, which initializes the OLE DLLs so they can call OLE interfaces:

if (!AfxOleInit())
{
   AfxMessageBox(IDP_OLE_INIT_FAILED);
   return FALSE;
}

All MFC Automation server applications, as well as ActiveX object servers, require a Class ID. The call to the ConnectTemplate member function of class COleTemplateServer registers the Class ID in the registry.

static const CLSID BASED_CODE clsid =
{ 0x2106e720, 0xaef8, 0x101a, { 0x90, 0x5, 0x0, 0xdd, 0x1, 0x8, 0xd6, 0x51 } };

...

// Connect the COleTemplateServer to the document template.
//  The COleTemplateServer creates new documents on behalf
//  of requesting OLE containers by using information
//  specified in the document template.
m_server.ConnectTemplate(clsid, pDocTemplate, FALSE);

Note The numbers shown in the clsid line are generated at random, so the numbers in your code will be different from the ones shown here.

A framework application that is an Automation server can use COleTemplateServer::UpdateRegistry to register itself as an Automation server (OLE Application Type: OAT_DISPATCH_OBJECT). This AppWizard-provided code is optional.

m_server.UpdateRegistry(OAT_DISPATCH_OBJECT);
COleObjectFactory::UpdateRegistryAll();

Alternatively, you can register your application by using one of the two other methods:

  • Manually merge the AppWizard-provided AUTOCLIK.REG registration file into the Windows registration file, using REGEDIT.

  • Programmatically merge the registration as one of the tasks of your application’s installation program.


Document Class of an Automation Server

When you select the Automation option in AppWizard, it not only enables the application as a whole to support Automation but also specifically enables the document class (in AutoClickDoc.cpp) to expose properties and methods by using Automation.

Suggested Reading in the Microsoft Foundation Class Reference

  • Dispatch Maps topics

The document class provided by AppWizard is derived from CDocument; therefore, your application’s document class is derived indirectly from CCmdTarget. To be exposed through Automation, a CCmdTarget-derived class must call its member function, EnableAutomation, from its constructor and must also include a dispatch map. Dispatch maps are like MFC message maps in that you do not edit them directly. AppWizard and ClassWizard edit them for you.

The AppWizard-provided dispatch map in the document’s header file looks like this:

//{{AFX_DISPATCH(CAutoClickDoc)
   // NOTE - the ClassWizard will add and remove member
   //    functions here.
   //    DO NOT EDIT what you see in these blocks of generated code !
//}}AFX_DISPATCH
DECLARE_DISPATCH_MAP()

and is implemented in the document’s .CPP file like this:

BEGIN_DISPATCH_MAP(CAutoClickDoc, CDocument)
   //{{AFX_DISPATCH_MAP(CAutoClickDoc)
   // NOTE - the ClassWizard will add and remove mapping macros here.
   //     DO NOT EDIT what you see in these blocks of generated code!
   //}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()

As you will see in Steps 2 and 3 of AutoClik, whenever you add a new property or method, ClassWizard adds an entry to the dispatch map.

The constructor of an automated CCmdTarget object must call CCmdTarget::EnableAutomation, as implemented by AppWizard:

CAutoClickDoc::CAutoClickDoc()
{
   EnableAutomation();

   AfxOleLockApp();
}

If the Automation server application supports being initially loaded by using Automation, then the constructor and destructor of the document class should call AfxOleLockApp and AfxOleUnlockApp, respectively. AppWizard provides the constructor and destructor of the document class. The calls to AfxOleLockApp and AfxOleUnlockApp are required so that AutoClik gracefully terminates any interactions with Automation clients before exiting.

Generally, createable objects need this. That way, if a client application creates an object of that type causing the Automation server to start, the server will exit when the object goes out of scope in the client.

CAutoClickDoc::~CAutoClickDoc()
{
   AfxOleUnlockApp();
}


ODL File for Creating a Type Library

AppWizard adds a file named AutoClik.odl to the project. AutoClik.odl is an Object Definition Library text file. It is input to the MKTYPLIB.EXE tool which creates a type library (.TLB) file named AutoClik.tlb. The binary type library (.TLB) can be used by other applications to gain information about the Automation server. This information includes a list of the Automation objects provided by the Automation server, and for each Automation object, a list of properties and methods exposed by the Automation server.

Whenever you define new Automation objects and define new methods and properties for the Automation server, ClassWizard adds information to the .ODL file. When you build the application, MKTYPLIB.EXE runs to create an updated .TLB file.

The Read Type Library option of ClassWizard, used when creating Automation clients, is a good example of how the type library file is used. In addition to supporting development of Automation servers, ClassWizard also supports the development of Automation clients, which access the properties and methods of the Automation server. The Read Type Library option of ClassWizard creates a CCmdTarget-derived class for each Automation object defined by the Automation server. In the code for the Automation client, you can then refer to the methods and properties of the Automation server simply as C++ class member functions and member variables.


Implementing AutoClik's Basic Behavior

Implementing AutoClik’s Basic Behavior

The rest of Step 1 implements AutoClik’s basic behavior, which consists of displaying text at mouse clicks and accepting changed text by using a dialog box.

To complete this topic, you need to create a dialog box and add a menu item. For tutorial information, see Scribble Lesson 7, Adding a Dialog Box, and Add the Clear All Command to Scribble's Edit Menu in Scribble Lesson 5.

To add the member variables to AutoClik’s document class

  1. Declare the following member variables in the public Attributes section of AutoClickDoc.h:
    CPoint m_pt;
CString m_str;
  2. Use ClassView to jump to the CAutoClickDoc constructor and add the following lines (after the EnableAutomation call):
    m_pt = CPoint(10,10);
m_str = _T("Automation!");
  3. Serialize the member variables in the document class. Use ClassView to jump to the Serialize member function and implement it with the following code.
    • Add the following line in place of the //TODO comment for storing code:
    ar << m_pt << m_str;
    • Add the following line in place of the //TODO comment for loading code:
    ar >> m_pt >> m_str;

To implement AutoClik’s drawing code

  • Use ClassView to jump to the OnDraw member function of CAutoClickView and add the following line, just after the ASSERT_VALID call (you can replace the //TODO comment):
    pDC->TextOut(pDoc->m_pt.x, pDoc->m_pt.y,
pDoc->m_str);

The implementations of OnLButtonDown and OnEditChangeText make use of the helper function, Refresh.

To implement the Refresh helper function

  1. In ClassView, right-click the class CAutoClickDoc.

  2. On the pop-up menu that appears, click Add Member Function.

  3. In the Add Member Function dialog box:
    • In the Function Type edit box, type void.

    • In the Function Declaration box, type Refresh().

    • In the Access area, select Public, and click OK.

    ClassWizard adds the declaration to the Public section of the header file and creates a starter definition in the implementation file.

  4. From ClassView, jump to the newly-created Refresh and implement it with the following code.
    UpdateAllViews(NULL);
SetModifiedFlag();

To implement the mouse click handler

  1. Using WizardBar, select CAutoClickView from the Class list.

  2. Click the action arrow located on the right end of WizardBar.

  3. Click Add Windows Message Handler.

    The New Windows Message and Event Handlers dialog box appears.

  4. From the New Windows messages/events list box, select WM_LBUTTONDOWN.

  5. Click Add and Edit.

  6. Replace the highlighted //TODO comment with the following code:
    CAutoClickDoc* pDoc = GetDocument();
pDoc->m_pt = point;
pDoc->Refresh();

To implement the Change Text dialog box

  1. On the Insert menu, click Resource.

    The Insert Resource dialog box appears.

  2. Select Dialog and click OK.

  3. Open and pin down the dialog’s properties page. (To open the properties page, right click on the dialog box and click Properties.)

  4. In the Dialog Properties page, type the following information:
    • In the ID box, type IDD_CHANGE_TEXT.

    • In the Caption box, type Change Text.
  5. Add two controls to the dialog box:
    • A static text control labeled Enter Text:

    • An edit control for the text
  6. From the View menu, click ClassWizard.

    The Adding a Class dialog box appears, with a message that IDD_CHANGE_TEXT is a new resource, and with the Create a new class option selected by default.

    ClassWizard knows that a class hasn’t been defined yet for your dialog resource, so it displays this dialog box to enable you to define one.

    Note If you had created the dialog class before creating the dialog resource, you could specify the Select an existing class option in this dialog box to connect the dialog box to the existing class.

  7. Click OK to create the dialog class.

    The Create New Class dialog box appears.

  8. Under Class Information, in the Name box, type CChangeText.

    Note the following default selections:

    • CDialog is selected as the base class.

    • IDD_CHANGE_TEXT is selected as the dialog ID.
  9. Click OK.

    ClassWizard creates the class and returns you to the MFC ClassWizard dialog box. Click OK to close the dialog box.

  10. Switch to the IDD_CHANGE_TEXT dialog box in ResourceView.

  11. Hold down the CTRL key and double-click IDC_EDIT1. Type m_str in the Member Variable Name box to add the member variable for the edit control.

  12. Click OK.

To add the Change Text command to AutoClik’s Edit menu

  1. From ResourceView, expand the Menu folder and double-click IDR_ACLICKTYPE.

    The menu editor opens.

  2. Click AutoClik’s Edit menu.

  3. Add a separator below the Paste menu item.

  4. Add the following menu item text below the separator:

    Change &Text...

  5. Press ENTER.

    The menu editor automatically names the command ID_EDIT_CHANGETEXT. You can view this by selecting the menu item again.

  6. Type a prompt string such as:
    Change text displayed in the view.

Close the resource editors before proceeding to the next step.

To implement the handler for the Change Text command

  1. Using WizardBar, select CAutoClickDoc from the Class list.

  2. From the Filter list, select ID_EDIT_CHANGETEXT.

  3. Click the action arrow located on the right end of WizardBar.

  4. Click Add Windows Message Handler.

    The New Windows Message and Event Handlers dialog box appears. In the Class or object to handle list box, ID_EDIT_CHANGETEXT should be selected.

  5. Click COMMAND, and click Add and Edit. Accept the default name OnEditChangeText and click OK.

  6. Replace the highlighted //TODO comment with the following code:
    CChangeText dlg;
dlg.m_str = m_str;
if (dlg.DoModal())
{
m_str = dlg.m_str;
Refresh();
}
  7. Add the following #include statement to AutoClickDoc.cpp:
    #include "ChangeText.h"


Building and Running AutoClik Step 1

If you build and run AutoClik Step 1 now, it will run only as a stand-alone application and minimally as an Automation server. You will be able to launch AutoClik from Autodriv, but if you try to access any of the methods or properties not yet implemented, Autodriv will not be able to find them.

At this point there is enough information for Automation clients to create an AutoClik document, but not enough to call methods or get or set properties. You will add this functionality in Step 2 and Step 3.


Implementing Automation Properties and Methods

AutoClik: Automation, Lesson 3

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

By the end of Step 1, AppWizard has enabled AutoClik to work as an Automation server. Also, AutoClik’s basic behavior has been completely implemented. With the help of ClassWizard, you can easily add properties and methods to the dispatch maps.

In Step 2, you will:

  • Implement properties of a dispatch interface by using the Get and Set methods, and by exposing a member variable as a property in AutoClik’s document dispatch interface.

  • Implement methods of a dispatch interface using RefreshWindow, ShowWindow, and SetAllProps.

In the course of doing this, you’ll also learn about MFC dispatch maps.


Implementing Properties of a Dispatch Interface

AutoClik’s document class has two member variables, m_pt and m_str. They can be exposed to Automation by using AutoClik’s document dispatch interface.

Suggested Reading in the Microsoft Foundation Class Reference

  • Dispatch Maps topics

There are two ways to expose member variables of an automated CCmdTarget-derived class.

  • Directly expose the member variable as a dispatch interface property. This is analogous to declaring a member variable public in a C++ class. Objects of any other class can directly access the member variable.

  • Indirectly expose the member variable by using a pair of dispatch interface Get and Set methods. This is analogous to declaring a member variable protected or private in a C++ class and declaring Get and Set member functions that other C++ objects must call to access the member variable.

When should you expose a member variable directly, as a dispatch interface property, and when should you expose it indirectly, by using dispatch interface Get and Set methods? Again, the question is analogous to: when should you declare a member variable protected or private and provide Get and Set member functions? If you do not need to monitor access to a member variable, you can expose it directly. If your application needs to know when the member variable is being accessed, you should expose it indirectly.

In the case of AutoClik, it makes the most sense to expose both m_pt and m_str indirectly by using Get and Set methods. This way, any time m_pt and m_str are updated through Automation, AutoClik updates its view. For tutorial purposes, however, you handle m_pt and m_str differently. You expose m_pt indirectly, whereas you expose m_str directly. Both approaches are easy to do with the help of ClassWizard.


Exposing Autoclik's m_pt Indirectly

Exposing Autoclik’s m_pt Indirectly

In the following procedure, you will expose m_pt indirectly as a property and add code to implement it, while ClassWizard generates the stub handlers for it. ClassWizard enables you to implement methods and properties the same way you implement member functions.

To indirectly expose the m_pt member variable in the dispatch interface

  1. From the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. In the Class name box, select CAutoClickDoc, if it is not already selected.

  4. Click Add Property.

    The Add Property dialog box appears.

  5. In the External name box, type x.

  6. In the Type list box, select short.

  7. Under Implementation, select Get/Set methods.

  8. Click OK.

    This returns you to the Automation tab. The new property, listed as x in the Name list, is implemented with Get and Set member functions. The Implementation box shows:

    short GetX();
void SetX(short nNewValue);

    The gray glyph with a “C” indicates that there is code associated with these member functions.

  9. Click the Edit Code button.

  10. Implement the Get method with the following code (replace the //TODO comment and existing code):
    return m_pt.x;
  11. Implement the Set method with the following code (replace the //TODO comment)::
    m_pt.x = nNewValue;
Refresh();
  12. Repeat steps 1 through 11 for the y property, ending with:
    short CAutoClickDoc::GetY()
{
return m_pt.y;
}

void CAutoClickDoc::SetY(short nNewValue)
{
m_pt.y = nNewValue;
Refresh();
}

    Note The x and y members of a point are declared as long in Win32. This may generate the following warning when you build the project:

    warning C4244: 'return' : conversion from 'long' to 'short', possible loss of data

    You can disregard the warning; it will not affect how Autoclik runs.

ClassView displays the new methods. You can examine AutoClickDoc.cpp to see how ClassWizard updated the dispatch map of the document class:

BEGIN_DISPATCH_MAP(CAutoClickDoc, CDocument)
   //{{AFX_DISPATCH_MAP(CAutoClickDoc)
   DISP_PROPERTY_EX(CAutoClickDoc, "x", GetX, SetX, VT_I2)
   DISP_PROPERTY_EX(CAutoClickDoc, "y", GetY, SetY, VT_I2)
   //}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()

You can see how the information you entered in ClassWizard is reflected in the dispatch map.


Exposing Autoclik's m_str Directly

Exposing Autoclik’s m_str Directly

In the following procedure, you will expose m_str directly as a property and study how ClassWizard adds it to the dispatch map. You’ll then remove the redundant declaration you added earlier. Finally, you’ll change the dispatch map declaration from private to public to give the view access to m_str.

To directly expose the m_str member variable in the dispatch interface

  1. From the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. In the Class name box, select CAutoClickDoc, if it is not already selected.

  4. Click Add Property.

    The Add Property dialog box appears.

  5. In the External name box, type text1.

  6. In the Type list box, select CString.

  7. Under Implementation, select Member variable.

    Whereas you exposed m_pt indirectly by using the Get/Set methods option, expose m_str directly as a member variable.

  8. Replace ClassWizard’s proposed Variable name, m_text1 (which was based on the External name), with m_str.

    Soon you will see how to associate the text dispatch property with the m_str member variable already declared in the document class.

  9. Remove ClassWizard’s proposed notification function name, OnText1Changed.

    This step is included for instructional purposes. You could have implemented an OnText1Changed function by calling Refresh, just as you did for SetX and SetY. If you do not implement a similar OnText1Changed function, then you can see the different behavior when you drive AutoClik from an Automation client. When the Automation client updates the text, AutoClik does not automatically update its view as it does when the Automation client changes the x or y values. Instead, the Automation client must call the Refresh method to update AutoClik’s view with the most recently changed text.

  10. Click OK.

    This returns you to the Automation tab, which now displays the three properties: text, x, and y. The Implementation box for the text variable shows:

    CString m_str;
  11. Click OK.

  12. In ClassView, double-click CAutoClickDoc to open AutoClickDoc.h.

    ClassWizard has declared the following members in the dispatch map:

    //{{AFX_DISPATCH(CAutoClickDoc)
CString m_str;
afx_msg short GetX();
afx_msg void SetX(short nNewValue);
afx_msg short GetY();
afx_msg void SetY(short nNewValue);
//}}AFX_DISPATCH
DECLARE_DISPATCH_MAP()

    At this point, the document header file declares m_str twice. The first declaration is the one you originally wrote:

    // Attributes
public:
CPoint m_pt;
CString m_str;

    The second declaration is the one ClassWizard added above in the dispatch map.

  13. Remove the CString m_str declaration that you wrote, shown above.

  14. Add the public keyword just after the line:
    // Generated OLE dispatch map functions

    This changes the declaration of the dispatch map from protected to public. This is necessary because m_str had already been declared as public so it could be accessed by the view.


Implementing Methods of a Dispatch Interface

You now add three methods to AutoClik’s document dispatch interface:

Method | Description
RefreshWindow | Updates the view according to the current values of m_pt and m_str.
SetAllProps | Sets the m_pt and m_str member variables, and updates the view.
ShowWindow | Shows AutoClik’s frame window, which is initially hidden when AutoClik is launched as an Automation server.


Implementing Autoclik's RefreshWindow

Implementing Autoclik’s RefreshWindow

In the following procedure, you'll implement RefreshWindow as a method. RefreshWindow updates the view according to the current values of m_pt and m_str. The RefreshWindow method is the Refresh member function originally implemented in Step 1. Here you directly expose the Refresh member function, just as you directly exposed the m_str member variable of CAutoClickDocument.

To directly expose the Refresh member function in the dispatch interface

  1. From the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. In the Class name box, select CAutoClickDoc, if it is not already selected.

  4. Click Add Method.

    The Add Method dialog box appears.

  5. In the External name box, type RefreshWindow.

    This is the name that the Automation client, Autodriv, uses to refer to the method, as in the following code:

    void CAutoDrivDlg::OnRefresh()
{
m_autoClikObject.RefreshWindow();
}
  6. In the Internal name box, replace the proposed RefreshWindow with Refresh.

    Refresh is the name of the member function you implemented in Step 1. You do not need to make the Internal name the same as the External name, even though ClassWizard proposes that you do so.

  7. In the Return type box, type void, or select it from the drop-down list box.

  8. Click OK.

    This returns you to the Automation tab. The new method, RefreshWindow, is shown in the Name list. The gray glyph with an “M” in it indicates that this is a method. The Implementation box shows:

    void Refresh();
  9. Click the Edit Code button.

    Because Refresh was selected in the Automation tab, ClassWizard takes you to the implementation of Refresh in AutoClickDoc.cpp:

    void CAutoClickDoc::Refresh()
{
UpdateAllView(NULL);
SetModifiedFlag();
}

    However, you implemented the Refresh member function in Step 1; ClassWizard was not aware of that, so it implemented a second stub member function at the end of AutoClickDoc.cpp.

    You will need to remove this second stub implementation. This is similar to how you removed the redundant declaration of m_str earlier.

  10. Remove ClassWizard’s redundant (and empty) implementation of Refresh at the end of AutoClickDoc.cpp.

  11. Remove the redundant declaration in the public Implementation section of AutoClickDoc.h.

    Leave the dispatch map entry created by ClassWizard:

    afx_msg void Refresh( );

There are two more methods to implement: SetAllProps and ShowWindow.


Implementing Autoclik's SetAllProps

Implementing Autoclik’s SetAllProps

In the following procedure, you’ll add and implement SetAllProps as a method. SetAllProps sets the m_pt and m_str member variables, and updates the view.

To add a method with parameters

  1. From the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. Select CAutoClickDoc in the Class name box, if it is not already selected.

  4. Click Add Method.

    The Add Method dialog box appears.

  5. In the External name box, type SetAllProps. Accept ClassWizard’s proposal to reuse this as the Internal Name, which is the name of the class member function.

  6. In the Return type box, select void.

  7. Click in the Parameter list box to begin entering information for the first parameter of the SetAllProps method.

    This will highlight the first blank row in the Parameter box.

    • Under the Name heading, type x in the Name box.

    • If necessary, click under the Type heading, and select short.
  8. Repeat step 7 for the y parameter, entering it in the row under the x parameter.

  9. Add the third parameter, text, selecting LPCTSTR from the Type drop-down list box.

  10. Click OK.

    This returns you to the Automation tab of ClassWizard, which shows the following implementation:

    void SetAllProps(short x, short y, LPCTSTR text);
  11. Click Edit Code.

    This takes you to the stub handler that ClassWizard created in AutoClickDoc.cpp:

    void CAutoClickDoc::SetAllProps(short x, short y, LPCTSTR text)
{
// TODO:  Add your dispatch handler code here
}
  12. Implement the handler with the following code:
    m_pt.x = x;
m_pt.y = y;
m_str = text;
Refresh();

Take a look at the dispatch map for the SetAllProps method (in AutoClickDoc.cpp):

BEGIN_DISPATCH_MAP(CAutoClickDoc, CDocument)
   //{{AFX_DISPATCH_MAP(CAutoClickDoc)
   ...
   DISP_FUNCTION(CAutoClickDoc, "SetAllProps", SetAllProps, VT_EMPTY,
      VTS_I2 VTS_I2 VTS_BSTR)
   //}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()

The last four parameters of the DISP_FUNCTION entry for SetAllProps list the return type, VT_EMPTY for void, followed by the three parameters. You do not need to interpret the parameter types in dispatch maps; the framework interprets them at run time. But you can see that VTS_I2 represents short and VTS_BSTR represents LPCTSTR.

The last method you’ll implement is ShowWindow.


Implementing Autoclik's ShowWindow

Implementing Autoclik’s ShowWindow

In the following procedure, you will add and implement ShowWindow as a method. You need this method because AutoClik leaves its frame window hidden when it is initially launched by the Automation client. This is the default behavior implemented by AppWizard, which is appropriate for most Automation servers. Typically, the Automation server gives the Automation client control over when the server window is shown or hidden. If you want your Automation server to show its frame window right at the time it is launched by the Automation client, simply remove the RunAutomated condition in the following if statement provided by AppWizard in the application’s InitInstance routine (in AutoClik.cpp):

BOOL CAutoClickApp::InitInstance()
{
   ...

      // Check to see if launched as OLE server
   if (cmdInfo.m_bRunEmbedded || cmdInfo.m_bRunAutomated)
   {
      // Application was run with /Embedding or /Automation.  Don't show the
      //  main window in this case.
      return TRUE;
   }

   ...

// The main window has been initialized, so show and update it.
   pMainFrame->ShowWindow(m_nCmdShow);
   pMainFrame->UpdateWindow();

   return TRUE;

}

To add the ShowWindow method

  1. From the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. In the Class name box, select CAutoClickDoc, if it is not already selected, and click Add Method.

    The Add Method dialog box appears.

  4. In the External name box, type ShowWindow, and accept ClassWizard’s proposal to reuse this as the Internal name, which is the name of the class member function.

  5. In the Return type box, select void.

    Click OK to return to the Automation tab (this method has no parameters).

  6. Click Edit Code.

    This takes you to the starter implementation of ShowWindow created in AutoClickDoc.cpp by ClassWizard.

  7. Implement ShowWindow with the following code:
    POSITION pos = GetFirstViewPosition();
CView* pView = GetNextView(pos);
if (pView != NULL)
{
CFrameWnd* pFrameWnd = pView->GetParentFrame();
pFrameWnd->ActivateFrame(SW_SHOW);
pFrameWnd = pFrameWnd->GetParentFrame();
if (pFrameWnd != NULL)
pFrameWnd->ActivateFrame(SW_SHOW);
}


Adding a Stub Property

At this point, you can build AutoClik but Autodriv won’t be able to load it. In Step 3, you’ll implement a Position property, which exposes the document’s m_pt member variable to a second dispatch interface. In order to load AutoClik from Autodriv to see your Step 2 code at work, you need to add the stub Position property.

To add the Position property

  1. From the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. In the Class name box, select CAutoClickDoc if it is not already selected, and click Add Property.

    The Add Property dialog box appears,

  4. In the External name box, type Position.

  5. In the Type box, select LPDISPATCH.

  6. Under Implementation, click Get/Set methods.

  7. Click OK.

    This returns you to the Automation tab. The new Position property is implemented with stub Get and Set member functions. The Implementation box shows:

    LPDISPATCH GetPosition();
void SetPosition(LPDISPATCH newValue);

    At this point, you would normally click the Edit Code button to implement the Get and Set methods. However, you’ll do that in Step 3 of the tutorial. For now, this stub property makes it possible to run Autodriv and test the code you added in Step 2.

  8. Exit ClassWizard and save your changes to the project files.


Build and Test AutoClik Step 2

AutoClik is mostly implemented now. You can try it out with the Autodriv application. For more information on loading and using Autodriv, see Preview of the AutoClik Application in Lesson 1.

To test AutoClik Step 2

  1. Build and then run AutoClik.exe to register the Step 2 version as the component application that Autodriv will load.

  2. Close AutoClik.

  3. Run Autodriv (which launches the newly-registered AutoClik) and try the following things:
    • Click the Set X and Set Y buttons in Autodriv.

    • Change the x and y coordinates in Autodriv and click Set All.

    • Click around in AutoClik and click the Get All button in Autodriv.

    • Change the text in the Text box of Autodriv and click the Set Text button.

    Nothing happens—Why?

    Set X and Set Y call AutoClik’s SetX and SetY methods, which call CAutoClickDoc::Refresh. This means that when you use Autodriv’s Set X and Set Y functions, the change shows up immediately in AutoClik’s window.

    In contrast, Autodriv’s Set Text function directly accesses the m_str member variable of AutoClik’s document. The change does not show up in AutoClik’s window until you call Refresh Display from Autodriv, which calls AutoClik’s RefreshWindow method, which in turn calls CAutoClickDoc::Refresh.

    • Click the Refresh Display button.

    The changes you entered in the Text box are displayed in AutoClik’s window.

    Note that the GetPosition and SetPosition methods have not been implemented. You will implement them in Step 3.


Implementing Multiple Dispatch Interfaces

AutoClik: Automation, Lesson 4

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

In Step 2, you added properties and methods to AutoClik’s document dispatch interface, which was initially implemented by AppWizard. In Step 3, you create an entirely new CCmdTarget-derived class that is exposed by using a second dispatch interface.

In this step, you will:

  • Use ClassWizard to create a CCmdTarget-derived class with a dispatch interface, named CAutoClickPoint, which implements a second, unnamed dispatch interface.

  • Expose AutoClik’s x and y coordinates by having AutoClik’s document dispatch interface refer to the second Point dispatch interface.

AutoClik’s second dispatch interface, implemented in class CAutoClickPoint, is very simple. It has two properties: x and y. It has no methods. This dispatch interface has been included solely for tutorial reasons. AutoClik already fully exposes its behavior by using the document dispatch interface implemented in Step 2. CAutoClickPoint’s dispatch interface is introduced to illustrate techniques for managing multiple dispatch interfaces in the same application.

The design decision to split functionality into multiple dispatch interfaces is no different from design decisions to split a C++ application into multiple classes. This basic approach is so strong that the framework enforces a one-to-one relationship between dispatch interfaces and Automation-enabled CCmdTarget-derived classes.

AutoClik’s document dispatch interface will refer to the second dispatch interface in its implementation of the GetPosition and SetPosition methods. AutoClik’s document dispatch interface will expose the x and y coordinates using this Point interface as a programmatic alternative for the Automation client. The Automation client can get or set AutoClik’s x and y coordinates by using the GetX, GetY, SetX, and SetY methods implemented in Step 2. Alternatively, the Automation client can use the GetPosition and SetPosition methods implemented here in Step 3.


Creating a New CCmdTarget Class with a Dispatch Interface

Using ClassWizard is a simple way to derive a new class from CCmdTarget that implements a new dispatch interface.

To create a new CCmdTarget class with a dispatch interface

  1. From the View menu, click ClassWizard.

  2. Click Add Class and from the menu click New.

    The New Class dialog box appears.

  3. Under Class information, in the Name box, type CAutoClickPoint.

  4. From the Base class drop-down list box, select CCmdTarget.

    Notice that all Automation options are now available because these options pertain only to CCmdTarget-derived classes.

  5. Select the Automation option.

  6. Click OK.

    You are returned to the ClassWizard dialog box.

  7. Click the Automation tab and ensure that CAutoClickPoint is selected in the Class name box.

  8. Click the Add Property button and fill in the Add Property dialog box:
    • In the External name box, Type x.

    • In the Type box, select short.

    • Accept m_x as the Variable name.

    • Remove OnXChanged as the Notification function.

      As you will see later, the members of the CAutoClickPoint dispatch interface class do not need notification functions.

    • Use the default Implementation type, Member variable.
  9. Click OK.

  10. Repeat steps 8 and 9 for the y property.

  11. Click OK.

Study the CAutoClickPoint class created by ClassWizard in AutoClickPoint.h and AutoClickPoint.cpp.

Toward the end of AutoClickPoint.h, you’ll find the declaration for the dispatch map:

DECLARE_DISPATCH_MAP()

The AutoClickPoint.cpp file implements the dispatch map, reflecting the two properties you added in ClassWizard, x and y.

Tip To jump to the implementation file, from ClassView, right-click any of the members of CAutoClickPoint, and click Go to Definition.

BEGIN_DISPATCH_MAP(CAutoClickPoint, CCmdTarget)
   //{{AFX_DISPATCH_MAP(CAutoClickPoint)
   DISP_PROPERTY(CAutoClickPoint, "x", m_x, VT_I2)
   DISP_PROPERTY(CAutoClickPoint, "y", m_y, VT_I2)
   //}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()


Referring to One Dispatch Interface from Another

You will now implement the Position property you added at the end of Step 2. This property exposes the document’s m_pt member variable by using the second dispatch interface implemented by CAutoClickPoint. You will expose this new property by adding code to the Get and Set member functions (GetPosition and SetPosition) which you declared in Step 2. The return type of GetPosition and the type of the parameter passed to SetPosition is LPDISPATCH, a pointer to a dispatch object.

The following two Autodriv message handlers, OnGetPosition and OnSetPosition, found in the CAutoDrivDlg class, access the Position property of AutoClik’s document dispatch interface using the GetPosition property:

void CAutoDrivDlg::OnGetPosition()
{
   CClikPoint point;
   point.AttachDispatch(m_autoClikObject.GetPosition());

   m_x = point.GetX();
   m_y = point.GetY();
   UpdateData(FALSE);
}

void CAutoDrivDlg::OnSetPosition()
{
   CClikPoint point;
   point.AttachDispatch(m_autoClikObject.GetPosition());

   UpdateData(TRUE);
   point.SetX((short)m_x);
   point.SetY((short)m_y);
   m_autoClikObject.SetPosition(point.m_lpDispatch);
}

The code:

   CClikPoint point;
   point.AttachDispatch(m_autoClikObject.GetPosition());

accesses AutoClik’s Position property, which is declared as LPDISPATCH in the MFC Automation server’s dispatch map. The Automation client gets initial access to AutoClik’s document dispatch interface object by creating it (in the OnCreate method):

   if (!m_autoClikObject.CreateDispatch(_T("AutoClick.Document")))
   {
      AfxMessageBox(IDP_CANNOT_CREATE_AUTOCLICK);
      return -1;   // fail
   }
   m_autoClikObject.ShowWindow();

To declare one dispatch interface object as a property of another dispatch interface

  1. From the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. In the Class name box, select CAutoClickDoc.

  4. In the External names box, select Position.

    This is the property you added in Step 2.

  5. Click Edit Code to implement the Get and Set member functions.

    This takes you to their stub implementations in AutoClickDoc.cpp.

  6. Implement the Get and Set member functions as shown by the following code:
    LPDISPATCH CAutoClickDoc::GetPosition()
{
CAutoClickPoint* pPos = new CAutoClickPoint;
pPos->m_x = (short) m_pt.x;
pPos->m_y = (short) m_pt.y;

LPDISPATCH lpResult = pPos->GetIDispatch(FALSE);
return lpResult;
}

void CAutoClickDoc::SetPosition(LPDISPATCH newValue)
{
CAutoClickPoint* pPos =
(CAutoClickPoint*)CCmdTarget::FromIDispatch(newValue);
if (pPos != NULL && pPos->IsKindOf
(RUNTIME_CLASS(CAutoClickPoint)))
{
m_pt.x = pPos->m_x;
m_pt.y = pPos->m_y;
Refresh();
}
}
  7. Add the following #include statement at the top of AutoClickDoc.cpp:
    #include "AutoClickPoint.h"

    This is required because the implementation of SetPosition refers to the CAutoClickPoint class.

  8. In ClassView, double-click CAutoClickPoint to display AutoClickPoint.h in the text editor.

  9. Change the declaration of CAutoClickPoint’s OLE dispatch map from protected to public, by adding the public keyword just before the comment line:
    public:
// Generated OLE dispatch map functions
//{{AFX_DISPATCH(CAutoClickPoint)
short m_x;
short m_y;
//}}AFX_DISPATCH
DECLARE_DISPATCH_MAP()

    This is required because CAutoClickDoc directly accesses CAutoClickPoint’s member variables m_x and m_y in its implementation of GetPosition and SetPosition.

The implementation of GetPosition creates a new CAutoClickPoint object. The CAutoClickPoint object, which is an Automation-enabled CCmdTarget object, in turn creates a dispatch interface object, through the help of the framework.

Finally, GetPosition gets the OLE IDispatch pointer by calling the CCmdTarget::GetIDispatch member function of the CAutoClickPoint object and returns this IDispatch pointer to the Automation client. The AddRef parameter of GetIDispatch is FALSE, because the OLE reference count of this dispatch interface object was already set to 1 when the CAutoClickPoint object was constructed.

The implementation of SetPosition does a C++ down-casting of the IDispatch pointer to a CAutoClickPoint pointer. It tests the down-casting with IsKindOf to make sure the Automation client passed back an IDispatch pointer to a CAutoClickPoint object rather than an IDispatch pointer to some other kind of object.

Finally, SetPosition updates the view to reflect the new position of the text by calling the document’s Refresh function. Because Refresh is called by SetPosition, it is not necessary to implement the OnXChanged and OnYChanged member functions to update the views for the CAutoClickPoint class.


Build and Run AutoClik Step 3

Build the Step 3 version of AutoClik. It now performs exactly as you previewed it in Lesson 1. You have finished the AutoClik tutorial.


Creating the Circle Control

Circle: ActiveX Control, Lesson 1

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

An "ActiveX control" (formerly called an OLE control) is a component object with an interface that lets it behave like a control for Windows.

In this tutorial you will create a simple ActiveX control called Circle. The Circle control demonstrates most of the features of an ActiveX control, such as properties, events, property pages, and data binding.

For a guide to the whole process of building your ActiveX control, see the next topic, The Tutorial Example: Circle. That topic shows the progression of lessons in the ActiveX control tutorial and will help you find specific parts of the task of building an ActiveX control.


The Tutorial Example: Circle

The Circle tutorial consists of three steps, CIRC1, CIRC2 and CIRC3. The individual steps can be found in the CIRC sample abstract under Samples \ MFC Samples \ Tutorial Samples in the online documentation or in the \SAMPLES subdirectory on the Visual C++ CD-ROM. Installing the Sample Files in Scribble Lesson 1 describes how to copy sample files and how to run a sample directly. Each step contains a project file, complete source files, and other related files for a version of the Circle control that corresponds to a stage of the tutorial. CIRC1 shows Circle just after it has been created. CIRC2 shows Circle after several properties and events have been added. CIRC3 shows Circle with additional stock properties and property pages, and data binding and versioning support.

In successive lessons, you will learn how to:

  • Create starter files for an ActiveX control.

  • Use Test Container to test an ActiveX control.

  • Change the painting behavior of an ActiveX control.

  • Add stock properties to an ActiveX control.

  • Add custom properties to an ActiveX control.

  • Make an ActiveX control respond to mouse events.

  • Add custom events to an ActiveX control.

  • Use text and fonts in an ActiveX control.

  • Implement ActiveX control property pages.

  • Use simple data binding for control properties.

Note The sample source code in CIRC1, CIRC2, and CIRC3 is not exactly the same as the source code produced by the tutorial. In the tutorial, where strings, identifiers, or filenames contain the string "circ", CIRC1 uses "circ1", CIRC2 uses "circ2", and so on. The main control class in the tutorial is CircCtrl, but in the CIRC3 sample it is Circ3Ctrl. This naming difference permits each stage of the Circle control to be registered as an ActiveX control. All of the sample controls and the control you develop by following the tutorial can be used in Test Container at the same time. The sample controls behave the same way as the control produced by following the tutorial.


Other ActiveX Control Samples

The CIRC samples are just a few of the ActiveX control samples included with Visual C++. Other samples include:

  • BUTTON — A control subclassed from a Windows button control. Demonstrates use of an in-place active menu, a stock property page, and the About box control option.

  • DB — A control that illustrates use of the CRecordset and CDatabase classes.

  • LICENSED — A control that enforces use of a design-time and run-time license.

  • LOCALIZE — A control with a localized user interface. Demonstrates use of separate type libraries and resource DLLs for localization.

  • PAL — A control that displays the colors of a palette. Demonstrates read-only properties, persistent Get/Set properties, persistent parameterized properties, and picture properties.

  • PUSH — A control subclassed from a Windows owner-drawn button control. Demonstrates stock properties, custom events, and picture holders.

  • REGSVR — Registers controls in the system registry.

  • SPINDIAL — A control with the visual appearance of a spin-dial. Demonstrates property page data validation.

  • TIME — A control that is invisible at run time and fires a timer event at set intervals. Demonstrates notification functions and ambient properties.

  • XLIST — A control, subclassed from a Windows list box, that displays text or bitmap items. Demonstrates methods, ambient properties, picture holders, and font holders.


Creating the Circle Control

The Circle sample tutorial uses the Circle control to explore the features and functions provided by the ActiveX control classes. Each lesson in this tutorial represents one step in the development cycle.

Note You can find a finished example of this lesson's code in the CIRC1 sample source code directory.

The Circle control created at the beginning of this tutorial is a simple one: it meets minimum control requirements and provides only default behaviors.

In this lesson, you will preview the Circle control and learn how to:

  • Generate the Circle control template using ControlWizard.

  • Modify the default control bitmap.

  • Modify the default About Circ Control dialog box.

  • Build the control.

  • Test the control using Test Container.


Previewing the Circle Control

Before you work through the steps for creating the Circle control, you may want to try out the completed control in Test Container. This will help you understand ActiveX controls and ActiveX control containers in general, and the Circle control in particular. The CIRC3 sample is an ActiveX control that is very similar to the completed Circle control.

You'll begin by building the CIRC3 sample. An ActiveX control is built in much the same way as a typical dynamic-link library (DLL).

To build the CIRC3 version of the Circle control

  1. Open the CIRC3.DSW project file.

  2. On the Build menu, click Build Circ.ocx, or click the Build button on the Build toolbar.

After successfully building the CIRC3 control, insert it into Test Container.

To insert the CIRC3 control into Test Container

  1. On the Tools menu, click ActiveX Control Test Container.

  2. On the Test Container Edit menu, click Insert New Control.

    The Insert Control dialog box appears.

  3. In the Object Type list box, select CIRC3 Control and click OK.

    A CIRC3 control with a hashed border and resize handles is inserted into Test Container.

You can now experiment with CIRC3 to see how it works. Try any of the following:

  • Click the Test Container window outside of the control. The hashed border and resize handles disappear.

  • Click the control. The hashed border and resize handles reappear.

  • Move the control around by clicking the hashed border and dragging the control to different places in the window.

  • Change the size and shape of the control by clicking any of the resize handles and dragging the control outline to a different shape.

When you have finished, close Test Container.

By following similar steps, you can build CIRC1 and CIRC2 and insert them into Test Container.


Creating the Basic Control

The first step in developing an ActiveX control is to use the MFC ActiveX ControlWizard to create the project. ControlWizard creates an ActiveX control project framework, including a basic set of classes, resources, and definition files. You will use ControlWizard to create the first version of the Circle control, which can draw itself as the outline of an ellipse. No properties, events, or methods are implemented in this step.

Suggested Reading

  • Overview: Creating an MFC ActiveX Control Program, Visual C++ Programmer's Guide

To create the Circle control

  1. On the File menu, click New.

  2. In the New dialog box, click the Projects tab.

  3. Click MFC ActiveX ControlWizard.

  4. In the Project Name text box, type Circ.

  5. Using the Location edit box and Browse button, select an appropriate project path.

  6. In the Platforms box, choose the appropriate platforms (in this case, Win32).

  7. Click OK.

    The first MFC ActiveX ControlWizard dialog box appears.

  8. Click Finish.

    ActiveX ControlWizard closes and the New Project Information dialog box appears.

  9. Click OK.

    The New Control Information dialog box closes and the project is created and opened in the development environment.

ControlWizard creates all of the necessary files to build the Circle control. Of these files, three class templates are created:

Class | Files | Comments
Circapp | CIRC.H CIRC.CPP | Implements the main DLL source. Typically, there is no need to modify this code.
CCircCtrl | CIRCCTL.H CIRCCTL.CPP | Implements the actual control functionality. Modify this class's code to implement control-specific behavior.
CCircPropPage | CIRCPPG.H CIRCPPG.CPP | Provides a template for the control's property page. Modify this class and its dialog template to implement a control-specific property page.

ControlWizard creates several other files that you modify later in this lesson and in subsequent lessons.

File | Comments
CIRL.ODL | Textually defines the control's type information. This file is modified by ClassWizard when you add properties, events, or methods to the control. MKTYPLIB.EXE uses this file as input to generate the type library (CIRC.TLB) information that is ultimately added to the control's executable file as a resource.
CIRC.RC | Standard resource file. Contains a template for the control's property page.
CIRC.RC2 | Contains user-defined resources that define a control's version information, include its type library information, and state that the control self-registers.
CIRCCTL.BMP | The tool palette representation of the control.
CIRC.ICO | The About box dialog icon.

ControlWizard also creates several other standard files: CIRC.CLW, CIRC.DEF, CIRC.MAK, CIRC.VCW, MAKEFILE, README.TXT, RESOURCE.H, STDAFX.CPP, and STDAFX.H.


Modifying the Control Bitmap

When ControlWizard creates the Circle control classes, resources, and other files, it also generates a bitmap file called CIRCCTL.BMP. This bitmap represents the control as a tool palette. Tools such as Visual Basic load this bitmap from the Circle control and display it in a palette. The palette provides a pictorial representation of all the controls that are available to the user.

Editing the Circle Palette Bitmap with the Bitmap Editor

To modify the control bitmap

  1. In the ResourceView pane, open the Circ project folder.

  2. Open the Bitmap folder.

  3. Double-click IDB_CIRC to start the resource editor for that bitmap.

  4. Modify the bitmap as you like, drawing a circle or other look representative of a Circle control.

  5. From the File menu, click Save to save the changes to the bitmap.

  6. Close the bitmap editor window.


Modifying the About Circ Control Dialog Box

ControlWizard created an About Circ Control dialog box template in the file CIRC.RC. It also defined the AboutBox "method" for the Circle control. A method is a function in a control that can be called from outside the control. The About Circ Control dialog box is displayed when the control container calls the control's AboutBox method.

ControlWizard also created an icon file called CIRC.ICO, which is displayed in the About Circ Control dialog box. You will use the resource editor to customize this icon in much the same way as you modified the control bitmap.

To modify the About dialog box icon

  1. In the ResourceView pane, double-click the Circ project folder.

  2. Open the Icon folder.

  3. Double-click IDI_ABOUTDLL to start the resource editor for that icon.

  4. Edit the icon. Like the control bitmap, the icon should have a look representative of the Circle control. For example, select the single pixel pencil in the Graphics toolbox and draw a one-pixel-wide circle.

  5. From the File menu, click Save to save the changes to the icon.

  6. Close the icon editor window.


Building the Control

Now that you have created all the required Circle control files and modified the control bitmap, you can build the control.

To build the Circle control

  • On the Build menu, click Build Circ.ocx.

Notice that the type library (TLB) compilation is automatically handled as part of the normal build process.

The build process automatically registers the Circle control. You can also register it by clicking Register Control on the Tools menu.


Testing the Circle Control

Once you have built and registered the Circle control, you can use Test Container to see how it behaves.

To test the Circle control

  1. From the Tools menu, click ActiveX Control Test Container.

  2. From the Edit menu, click Insert New Control.

    The Insert Control dialog box appears.

  3. From the list box, select Circ Control.

  4. Click OK to close the Insert Control dialog box and insert the control into Test Container.

    The Circle control appears in Test Container, as shown in the figure below. Notice that the control is drawn as an ellipse within the bounding rectangle of the control.

  5. Move the control within the container to observe how the control is redrawn. Resize the control to see how its ellipse is redrawn to the size of the bounding rectangle.

  6. From the Control menu, click Invoke Methods.

    The Invoke Method dialog box appears.

    Notice that AboutBox is selected in the Method Name drop-down list box. This is the only method defined in the Circle control.

  7. Click Invoke.

    The About Circ Control dialog box appears. The icon you edited earlier is displayed in the dialog box.

  8. Click OK to close the About Circ Control dialog box.

  9. Click Close to close the Invoke Method dialog box.

The Circle Control

Notice that the background color of the control is white. You'll modify control painting in the next lesson.

At this point you have not defined any properties or events for the Circle control. From the Test Container Edit menu, click Properties. Notice that there are no properties. From the Options menu, select Logging and make sure that Log to output window is selected. Click, move, and resize the control. No events are displayed. As the Circle control evolves, you will add a number of new properties and events.

When you have finished, close Test Container.


Painting the Control

Circle: ActiveX Control, Lesson 2

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

Now that the framework for the Circle control is in place, you can modify the control to do something more useful. In this lesson, you'll implement the specialized background painting behavior of the Circle control.

Note You can find a finished example of this lesson's code in the CIRC2 sample source code directory.

The Circle control uses the container's ambient background color property as the default value for its background color and it uses the container's stock background color property to maintain the value of its current background color.

In this lesson, you will learn how to:

  • Use the BackColor stock property.

  • Set the default background color value.

  • Modify the default OnDraw function to implement new painting behavior.


Enabling the BackColor Property

The first step in implementing the new painting behavior is to add the background color property (BackColor) to the control. BackColor, among others, is a stock property.

To add the BackColor property

  1. On the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. From the Class name drop-down list box, select CCircCtrl.

  4. Click Add Property.

    The Add Property dialog box appears.

  5. From the External name drop-down combo box, select BackColor.

  6. Under Implementation, select Stock.

  7. Click OK to close the Add Property dialog box.

    This returns you to the Automation tab. Notice that the implementation of the BackColor property is listed as:

    Implementation:
Stock Property
  8. Click OK again to confirm your choices and close ClassWizard.

ClassWizard creates the code to add the BackColor property, modifying both the CCircCtrl class and the type library file CIRC.ODL.

ClassWizard adds the DISP_STOCKPROP_BACKCOLOR macro in CCircCtrl class's dispatch map in CIRCCTL.CPP as shown below:

BEGIN_DISPATCH_MAP(CCircCtrl, COleControl)
    //{{AFX_DISPATCH_MAP(CCircCtrl)
    DISP_STOCKPROP_BACKCOLOR()
    //}}AFX_DISPATCH_MAP
    DISP_FUNCTION_ID(CCircCtrl, "AboutBox", DISPID_ABOUTBOX, AboutBox, VT_EMPTY, VTS_NONE)
END_DISPATCH_MAP()

The control's type library file (CIRC.ODL) is modified by ClassWizard to add BackColor to its property section, as shown in the following code sample:

dispinterface _DCirc
{
    properties:
        // NOTE - ClassWizard will maintain property information here.
        //    Use extreme caution when editing this section.
        //{{AFX_ODL_PROP(CCircCtrl)
        [id(DISPID_BACKCOLOR), bindable, requestedit] OLE_COLOR BackColor;
        //}}AFX_ODL_PROP
    methods:
        // NOTE - ClassWizard will maintain method information here.
        //    Use extreme caution when editing this section.
        //{{AFX_ODL_METHOD(CCircCtrl)
        //}}AFX_ODL_METHOD

        [id(DISPID_ABOUTBOX)] void AboutBox();
};

Because the BackColor property is of type DISP_STOCKPROP_BACKCOLOR, its value can be modified only through its Get and Set methods.

The value of the BackColor property is maintained by class COleControl. The SetBackColor member function of COleControl automatically calls the OnBackColorChanged member function after setting the BackColor value, invalidating the control. Invalidating the control causes the OnDraw function to be called, and the control is redrawn using the new background color.


Setting the Default Background Color

The next step is to provide a default value for the control's background color. Normally, the background color of a control is the same as the background color of the control container's window, which can be obtained from the container's ambient properties.

COleControl implements a mechanism for obtaining the default values of stock properties. COleControl::OnResetState calls the function COleControl::DoPropExchange, which queries the container for its background color ambient property and sets the value of the control's BackColor property equal to this color.

In projects created by ControlWizard, the default background color is implemented automatically.


Modifying the Draw Behavior

Now that you have enabled the Circle control's background color property and set its default background color, the final step is to modify the CCircCtrl::OnDraw function in CIRCCTL.CPP to implement the painting behavior.

As created by ControlWizard, the CCircCtrl::OnDraw function implements the basic Circle control drawing behavior:

void CCircCtrl::OnDraw(
            CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
    // TODO: Replace the following code with your own drawing code.
    pdc->FillRect(rcBounds, CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));
    pdc->Ellipse(rcBounds);

}

The default behavior of the OnDraw function is to draw an ellipse with a white background — exactly what was displayed in Test Container earlier.

To modify OnDraw to use the background color value defined by the Circle control's BackColor property, remove the //TODO comment line and the line on which FillRect is called, and modify the code as follows (beginning with the fourth line in the code example, this is new code that you will be adding):

void CCircCtrl::OnDraw(
            CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
    CBrush* pOldBrush;
    CBrush bkBrush(TranslateColor(GetBackColor()));
    CPen* pOldPen;

    // Paint the background using the BackColor property
    pdc->FillRect(rcBounds, &bkBrush);

    // Draw the ellipse using the BackColor property and a black pen
    pOldBrush = pdc->SelectObject(&bkBrush);
    pOldPen = (CPen*)pdc->SelectStockObject(BLACK_PEN);
    pdc->Ellipse(rcBounds);
    pdc->SelectObject(pOldPen);
    pdc->SelectObject(pOldBrush);
}

The code constructs a brush, called bkBrush, that uses the BackColor property color. Because a COLORREF value is expected for initializing the brush, and the BackColor property value is an OLE_COLOR value, TranslateColor is called first. The bounding rectangle of the control is painted using CDC::FillRect, specifying bkBrush as the fill brush.

The ellipse is drawn within the bounding rectangle of the control using the CDC::Ellipse member function. Before the ellipse is drawn, the background color brush and the pen must be selected into the device context. This is done by calling CDC::SelectObject, as shown in the code. Now when the ellipse is drawn, it is filled with the proper background color and drawn using a black pen. Finally, the old brush and pen are selected back into the device context, restoring the device context to the state in which it entered the OnDraw function.


Rebuilding the Control with Painting Implemented

Now that you have implemented the Circle control's painting behavior, you must rebuild the control. Because you added the BackColor property to the control, the type library is automatically updated during the build.

To rebuild the control

  • On the Build menu, click Build Circ.ocx.


Testing the Control Drawing Behavior

To test the new drawing behavior of the control, run Test Container and load the control.

To insert the Circ control in Test Container

  1. Use Control Panel to modify the Windows system background color so that it is not white.

  2. On the Tools menu, click ActiveX Control Test Container.

  3. On the Edit menu, click Insert New Control.

    The Insert Control dialog box appears.

  4. From the list box, select Circ Control.

  5. Click OK to close the Insert Control dialog box and insert the control into Test Container.

The Circle control is displayed in Test Container and painted using the same background color as the Test Container window. You can also modify the BackColor property value.

To modify the BackColor property

  1. On the Control menu, click Invoke Methods.

  2. In the Method Name box, select the PropPut version of the BackColor property.

  3. In the Parameter Type control, select VT_COLOR. The Parameter Value control changes to a Choose Color button.

  4. Click the Choose Color button, and from the Color dialog, select red. Click OK.

  5. Click Invoke. The Circle control background color changes to red.

When you have finished, close Test Container.


Adding a Custom Notification Property

Circle: ActiveX Control, Lesson 3

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

The ability to define a custom set of properties, events, and methods for a control is one of the most powerful features of control writing. The previous lesson illustrated the use of a stock property. This lesson illustrates how to add a custom property to the Circle control.

Note You can find a finished example of this lesson's code in the CIRC2 sample source code directory.

Suggested Reading

  • ActiveX Controls: Properties, Visual C++ Programmer's Guide

  • Implementing a Parameterized Property, Visual C++ Programmer's Guide

ActiveX Control Properties — An Overview

Properties are typically used to represent control data or attributes. For example, a Date control might define a DateValue property, which provides access to the current date value displayed in the control. This type of property represents control data. In addition, the Date control may define a Format property, which allows the user to get and set the display format of the date. This type of property represents a control attribute.

ActiveX control classes support four types of custom properties:

  • DISP_PROPERTY

    Implemented using a member variable.

  • DISP_PROPERTY_NOTIFY

    Implemented using a member variable and a notification function.

  • DISP_PROPERTY_EX

    Implemented using Get/Set functions.

  • DISP_PROPERTY_PARAM

    Implemented using Get/Set functions and an index parameter.

In the following topics, you will add the CircleShape property, which is an example of a DISP_PROPERTY_NOTIFY custom property. CircleShape is a Boolean property that displays the control as a perfect circle, if set to TRUE; or as an ellipse, if set to FALSE. (The DISP_PROPERTY_EX and DISP_PROPERTY custom property types are used later in the tutorial.)

In this lesson, you will:

  • Define the CircleShape property functionality.

  • Add the CircleShape property to the control.

  • Set the default value of the CircleShape property.

  • Revise the Circle control draw behavior to reflect the value of the CircleShape property.


The CircleShape Property

This topic presents general information on:

  • CircleShape property functions and the property's effect on the Circle control.

  • Modifying the Circle control to implement CircleShape property functions.

CircleShape Property Functionality

When the CircleShape property's value is set to TRUE, the Circle control draws the largest possible perfect circle centered within the bounding rectangle of the control. When the CircleShape property is set to FALSE, the Circle control draws an ellipse whose major and minor axes touch the bounding rectangle of the control. The initial value of the CircleShape property should be TRUE. Whenever the CircleShape property is changed, the Circle control is redrawn to reflect the change.

The figure below shows the Circle control drawn as an ellipse, the desired effect when CircleShape is set to FALSE. Notice that the ellipse is drawn to the edges of the bounding rectangle. This is the standard drawing behavior of the Circle control, so very little code is modified to implement the required drawing behavior when CircleShape is set to FALSE.

The CircleShape Property Set to FALSE

When the CircleShape property is set to TRUE, the Circle control is drawn as a perfect circle. The following figure shows how the circle would be drawn within the bounding rectangle of the control. To determine how to draw the circle, you will calculate the square region centered within the bounding rectangle of the control.

The CircleShape Property Set to TRUE

Recall from the previous lesson that the CCircCtrl::OnDraw function used the CDC::Ellipse function to draw the ellipse. This function can also be used to draw the circle. By passing the calculated square region instead of the bounding rectangle of the control to the Ellipse function, the Ellipse function will draw a perfect circle.

Implementing the CircleShape Property

Now that the CircleShape property's functional specification is complete, and the basic logic is described, you can revise the Circle control's code as follows:

  • Add the CircleShape property to the control using ClassWizard.

  • Set the default value of the CircleShape property when the control is created.

  • Define the GetDrawRect member function in the CCircCtrl class.

    This function determines the drawing coordinates to use: if CircleShape is FALSE, use the entire bounding rectangle of the control; if CircleShape is TRUE, use the centered square region inside the bounding rectangle.

  • Modify CCircCtrl::OnDraw to use the coordinates returned by the CCircCtrl::GetDrawRect member function when drawing the control.

  • Modify the CCircCtrl::OnCircleShapeChanged member function to invalidate the control.

Aspects of this strategy apply whenever you add any custom property. Using ClassWizard to add the property greatly simplifies the process by updating the appropriate class and the object definition library (.ODL) files (which are used to create the type library). It is always good practice to provide a default value for the new property by adding initialization code for the property to the DoPropExchange member function in the control class.


Adding the CircleShape Property

The first step in implementing CircleShape functionality is to add the property to the control. Similar to a stock property, a custom property is added using ClassWizard.

To add the CircleShape property

  1. On the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. From the Class name drop-down list box, select CCircCtrl.

  4. Click Add Property.

    The Add Property dialog box appears.

  5. In the edit control of the External name drop-down combo box, type CircleShape.

  6. Under Implementation, select Member variable.

  7. From the Type list box, select BOOL.

  8. Verify that the Variable name edit control contains m_circleShape.

  9. Verify that the Notification function edit control contains OnCircleShapeChanged.

  10. Click OK to close the Add Property dialog box.

    This returns you to the Automation tab. Notice that the implementation of the CircleShape property appears as:

    Implementation:
BOOL m_circleShape;
void OnCircleShapeChanged();
  11. Click OK to confirm your choices and close ClassWizard.

ClassWizard creates the appropriate code to add the CircleShape property to the CCircCtrl class and to the CIRC.ODL file. When adding a DISP_PROPERTY_NOTIFY property type, ClassWizard modifies the CCircCtrl class's dispatch map by adding a DISP_PROPERTY_NOTIFY macro entry for the property:

BEGIN_DISPATCH_MAP(CCircCtrl, COleControl)
    //{{AFX_DISPATCH_MAP(CCircCtrl)
    DISP_PROPERTY_NOTIFY(CCircCtrl, "CircleShape",
        m_circleShape, OnCircleShapeChanged, VT_BOOL)
    DISP_STOCKPROP_BACKCOLOR()
    //}}AFX_DISPATCH_MAP
    DISP_FUNCTION_ID(CCircCtrl, "AboutBox",
        DISPID_ABOUTBOX, AboutBox, VT_EMPTY, VTS_NONE)
END_DISPATCH_MAP()

The DISP_PROPERTY_NOTIFY macro associates the CircleShape property name with its corresponding CCircCtrl class member variable (m_circleShape), as well as the name of the CCircCtrl class notification function (OnCircleShapeChanged), which is called whenever the value of CircleShape property is changed. The property type value is specified as VT_BOOL.

ClassWizard also adds a declaration for the OnCircleShapeChanged notification function in CIRCCTL.H and a function template in CIRCCTL.CPP:

void CCircCtrl::OnCircleShapeChanged()
{
    // TODO: Add notification handler code

    SetModifiedFlag();
}

You will modify OnCircleShapeChanged to invalidate the control in Modifying OnCircleShapeChanged, later in this lesson.


Setting the CircleShape Default Value

Because the Circle control uses the CircleShape property as a key to determine how to draw itself, it is important to initialize the CircleShape property to a specific value. Modify the CCircCtrl::DoPropExchange function. To do this, add the following line to CIRCCTL.CPP:

    PX_Bool(pPX, _T("CircleShape"), m_circleShape, TRUE);

as shown in the following code example:

void CCircCtrl::DoPropExchange(CPropExchange* pPX)
{
    ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
    COleControl::DoPropExchange(pPX);

    PX_Bool(pPX, _T("CircleShape"), m_circleShape, TRUE);
}

As stated earlier in CircleShape Property Functionality, the initialized state of CircleShape must be TRUE if you want the control initially drawn as a perfect circle. This is accomplished by adding a call to the PX_Bool function in the DoPropExchange function, which sets the default value of the CircleShape property to TRUE.

First, the property name string parameter to the PX_Bool function is passed through the _T macro. This macro is used for compatibility among different string representations. For instance, UNICODE strings are available for 32-bit ActiveX controls. All literal strings in an ActiveX control project must be handled in this manner.

Any property initialized with a call to a PX_ function in the DoPropExchange function is called a “persistent property” because the PX_ functions do more than initialize a property when a control is created. The DoPropExchange function is called when a control is created, restored from a file or stream, or saved to a file or stream. The PX_ functions determine whether the value of a persistent property must change under any of these conditions. Whenever a persistent property is changed, a modified flag for persistent properties must be set to indicate that at least one persistent property must be updated. Setting the modified flag is discussed later in this lesson, in Modifying OnCircleShapeChanged.


Implementing New Drawing Behavior

Now that you have added the CircleShape property to the CCircCtrl class and set its initial value in DoPropExchange, you can implement the new drawing behavior. To do this, you will make several additions to the CCircCtrl class code:

  • Implement the GetDrawRect function to calculate the coordinates of the square region.

  • Modify the OnDraw function to call GetDrawRect.

  • Modify the OnCircleShapeChanged function to invalidate the control.


The GetDrawRect Function

The GetDrawRect function determines the coordinates of the bounding rectangle in which the ellipse should be drawn. If the CircleShape property is TRUE, GetDrawRect calculates the coordinates of the square region centered in the rectangle rc, which was passed to GetDrawRect. The coordinates of the square region are put back into rc. If CircleShape is FALSE, the function leaves rc untouched.

To add the GetDrawRect function

  1. On WizardBar, select CCircCtl to edit the CCIRCCTL.CPP file.

  2. Click the arrow on the action button, located on the right end of WizardBar, and select Add Member Function.

    The Add Member Function dialog box is displayed.

  3. In the Function Type edit box, type void. This is the type of value the function will return.

  4. In the Function Declaration edit box, type GetDrawRect(CRect* rc).

  5. Under Access, click Protected.

  6. Click OK to close the Add Member Function dialog box.

This will add the following declaration in the protected section of CIRCCTL.H:

void GetDrawRect(CRect* rc);

It will also add the following function definition at the end of CIRCCTL.CPP and position the insertion point in the function:

void CCircCtrl::GetDrawRect(CRect* rc)
{

}

Add code to the function in CIRCCTL.CPP as shown below:

void CCircCtrl::GetDrawRect(CRect* rc)
{
    if (m_circleShape)
    {
        int cx = rc->right - rc->left;
        int cy = rc->bottom - rc->top;

        if (cx > cy)
        {
            rc->left += (cx - cy) / 2;
            rc->right = rc->left + cy;
        }
        else
        {
            rc->top += (cy - cx) / 2;
            rc->bottom = rc->top + cx;
        }
    }
}


Modifying OnDraw

The GetDrawRect function greatly simplifies the changes that need to be made to the OnDraw function. The modifications to OnDraw introduce a local CRect object, rc, whose value is initialized to the value of rcBounds. Before the ellipse is drawn, GetDrawRect is called. If the value of the CircleShape property is TRUE, GetDrawRect adjusts the coordinates in rc to be the square region centered within rcBounds.

Add the following individual lines of code to CIRCCTL.CPP:

    CRect rc = rcBounds;
    GetDrawRect(&rc);

and modify the following line of code:

    pdc->Ellipse(rcBounds);

to match what is shown in the following code example:

void CCircCtrl::OnDraw(
            CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
    CBrush* pOldBrush;
    CBrush bkBrush(TranslateColor(GetBackColor()));
    CPen* pOldPen;
    CRect rc = rcBounds;

    // Paint the background using the BackColor property
    pdc->FillRect(rcBounds, &bkBrush);

    // Draw the ellipse using the BackColor property and a black pen
    GetDrawRect(&rc);
    pOldBrush = pdc->SelectObject(&bkBrush);
    pOldPen = (CPen*)pdc->SelectStockObject(BLACK_PEN);
    pdc->Ellipse(rc);
    pdc->SelectObject(pOldPen);
    pdc->SelectObject(pOldBrush);
}


Modifying OnCircleShapeChanged

Because CircleShape is a DISP_PROPERTY_NOTIFY property type, ClassWizard creates the code required to force a notification function to be called if the CircleShape property's value changes. The default OnCircleShapeChanged notification function, created by ClassWizard, sets the modified flag for the control.

The CircleShape property is a persistent property because it is initialized by calling the PX_Bool function in the COleControl::DoPropExchange function. The modified flag for persistent properties must be set by calling the COleControl::SetModifiedFlag function whenever the value of the persistent property has changed. Because most properties are persistent, ClassWizard includes a call to the CCircCtrl::SetModifiedFlag function in all notification functions.

If the value of the CircleShape property changes, the control must redraw itself to ensure that it displays the correct representation of the control, either a circle or an ellipse. This is done by invalidating the control when the OnCircleShapeChanged member function is called. The invalidation causes the OnDraw member function to be called.

Add the following lines of code to CIRCCTL.CPP:

    // force the control to redraw itself
    InvalidateControl();

as shown in the following code example:

void CCircCtrl::OnCircleShapeChanged()
{
    // force the control to redraw itself
    InvalidateControl();

    SetModifiedFlag();
}


Rebuilding the Control with CircleShape Implemented

Now that the CircleShape property is implemented, you need to rebuild the control. Because the CIRC.ODL file was changed when you added the CircleShape property to the control, the type library is automatically updated during the build.

To rebuild the control

  • On the Build menu, click Build Circ.ocx.


Testing the Control CircleShape Property

You have implemented the CircleShape property's functionality. You can now use Test Container to test the code.

To insert the Circ control in Test Container

  1. On the Tools menu, click ActiveX Control Test Container.

  2. On the Edit menu, click Insert New Control.

    The Insert Control dialog box appears.

  3. From the list box, select Circ Control.

  4. Click OK to close the Insert Control dialog box and insert the control into Test Container.

    The Circle control is displayed in Test Container. Notice that the control is initially drawn as a perfect circle because the CircleShape property is set to TRUE in the DoPropExchange member function in the CCircCtrl class.

Next, change the value of the CircleShape property to cause the Circle control to be redrawn as an ellipse.

To change the CircleShape property

  1. On the Control menu, click Invoke Methods.

  2. From the Method Name drop-down combo box, select the PropPut version of CircleShape. The Value column of the Parameters control displays a default of 0.

    Note If the CircleShape property is not listed, the control might not be selected or the type library may not have been regenerated before the control was last built.

  3. Click Invoke.

    Invoking the PropPut method changes the value of the CircleShape property to the value reflected in the Parameter Value edit box — in this case, 0. It does not necessarily reflect the current value of the property. To check the current value of the CircleShape property, invoke the PropGet method and check its return value.

The Circle control is redrawn as an ellipse the size of the control's bounding rectangle. It is immediately redrawn, because changing the CircleShape property causes the OnCircleShapeChanged notification function to be called, which invalidates the control.

When you have finished, close Test Container.


Adding a Custom Get/Set Property

Circle: ActiveX Control, Lesson 4

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

The ActiveX control classes support a property type that can be accessed only through a Get/Set method pair. By wrapping Get and Set methods around the property, you can shield the user from the internal representation and implementation of the property.

Note You can find a finished example of this lesson's code in the CIRC2 sample source code directory.

Forcing access to a property through methods can be beneficial. For example, a Set method can be coded to validate an input value before the property's value is set to it, or a property can represent a computed value. When the computed property is accessed, its Get method performs a computation and returns the result as the property value.

In this lesson, you will implement a Get/Set property, CircleOffset, for the Circle control. The CircleOffset property allows the circle to be offset from the center of the control's bounding rectangle. Before the offset is modified, the Set method makes sure that the edge of the circle will stay within the control's bounding rectangle. In C++, Get and Set methods are implemented as member functions.

In this lesson, you will:

  • Define the CircleOffset property's functionality.

  • Add the CircleOffset property to the control.

  • Set the CircleOffset property's default value.

  • Set the CircleOffset property value.

  • Revise the control's draw behavior to reflect the value of the CircleOffset property.

  • Modify the OnCircleShapeChanged function.

  • Add the OnSize function.


The CircleOffset Property

This topic discusses the functional aspects of the CircleOffset property and the property's effect on the behavior of the Circle control. It also presents a strategy for modifying the Circle control's code to prepare for implementing the CircleOffset property's behavior.

CircleOffset Property Functionality

The CircleOffset property allows the user to offset the circle from the center of the control's bounding rectangle. The CircleOffset property has an effect only if the value of the CircleShape property is TRUE. When the CircleShape property is FALSE, the control is drawn as an ellipse the size of the bounding rectangle. When CircleShape is TRUE, the circle can be offset potentially from center in either the x or the y direction as shown in the figure below, depending on which has the greater extent.

Circle Offset 25 Units from Center

The circle can also be offset in a negative direction. For example, if the Circle control is drawn with a greater x-extent, a positive offset will move the circle to the right, and a negative offset will move the circle to the left, within the bounding rectangle. If the control is drawn with a greater y-extent, a positive offset will move the circle toward the top, and a negative offset will move the circle toward the bottom, of the bounding rectangle.

Circle Control with a Greater Y-Extent

The following rules apply to CircleOffset property behavior:

  • The control ignores any offset that would cause the circle to be placed outside the control's bounding rectangle.

  • If the control is resized, CircleOffset is set to 0.

  • If the CircleShape property is set to TRUE, CircleOffset is set to 0.

The control cannot allow the circle to be moved beyond its bounding rectangle. Thus, CircleOffset property's Set method must implement code that will validate the value of the offset passed to the Set method before setting the value of the CircleOffset property.

Implementing the CircleOffset Property

Implementing CircleOffset property functions is more complex than implementing the CircleShape property. Not only does the CircleOffset property affect the drawing behavior of the control, it also requires that the value of the CircleOffset property be reset to 0 when either the control is resized or the CircleShape property is set to TRUE. Given these factors, the following code revisions must be made:

  • Use ClassWizard to add the CircleOffset property to the control.

  • Set the default value of the CircleOffset property to 0 when the control is created.

  • Modify the CircleOffset property's Set method to perform offset validation.

  • Modify the CCircCtrl::GetDrawRect member function to properly calculate the coordinates of the square region based on the current CircleOffset property value.

  • Modify the CCircCtrl::OnCircleShapeChanged member function to reset the CircleOffset property value to 0 if the CircleShape property is changed to TRUE.

  • Add the CCircCtrl::OnSize notification function to reset the CircleOffset value to 0 if the size of the control is changed.


Adding the CircleOffset Property

The first step in implementing CircleOffset functionality is to add the CircleOffset property to the control.

To add the CircleOffset property

  1. On the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. In the Class name drop-down list box, select CCircCtrl.

  4. Click Add Property.

    The Add Property dialog box appears.

  5. In the edit control of the External name drop-down combo box, type CircleOffset.

  6. Under Implementation, select Get/Set methods.

    The Get function and Set function edit controls appear, replacing the Variable name and Notification function edit controls.

  7. From the Type list box, select short.

  8. Click OK to close the Add Property dialog box.

    This returns you to the Automation tab. Notice that the implementation of the CircleOffset property appears as:

    Implementation:
short GetCircleOffset();
void SetCircleOffset(short nNewValue);
  9. Click OK to confirm your choices and close ClassWizard.

ClassWizard creates the appropriate code to add the CircleOffset property to CCircCtrl and to CIRC.ODL. Because CircleOffset is a Get/Set property type, ClassWizard modifies the CCircCtrl class's dispatch map to include a DISP_PROPERTY_EX macro entry:

BEGIN_DISPATCH_MAP(CCircCtrl, COleControl)
    //{{AFX_DISPATCH_MAP(CCircCtrl)
    DISP_PROPERTY_NOTIFY(CCircCtrl, "CircleShape",
        m_circleShape, OnCircleShapeChanged, VT_BOOL)
    DISP_PROPERTY_EX(CCircCtrl, "CircleOffset",
        GetCircleOffset, SetCircleOffset, VT_I2)
    DISP_STOCKPROP_BACKCOLOR()
    //}}AFX_DISPATCH_MAP
    DISP_FUNCTION_ID(CCircCtrl, "AboutBox",
        DISPID_ABOUTBOX, AboutBox, VT_EMPTY, VTS_NONE)
END_DISPATCH_MAP()

The DISP_PROPERTY_EX macro associates the CircleOffset property name with its corresponding CCircCtrl class's Get and Set methods, GetCircleOffset and SetCircleOffset. The type of the property value is specified as VT_I2, which corresponds to short.

ClassWizard also adds a declaration for the GetCircleOffset and SetCircleOffset functions in CIRCCTL.H and also adds their function templates in CIRCCTL.CPP:

short CCircCtrl::GetCircleOffset()
{
    // TODO: Add your property handler here

    return 0;
}

void CCircCtrl::SetCircleOffset(short nNewValue)
{
    // TODO: Add your property handler here

    SetModifiedFlag();
}

You will modify the SetCircleOffset function to perform offset validation later in this lesson.

Because ClassWizard only creates the Get and Set functions, you must add a member variable to the CCircCtrl class to keep track of the actual value of the CircleOffset property. The Get and Set methods will query and modify this variable. You can add this variable by modifying the declaration of the CCircCtrl class in CIRCCTL.H. Add the member variable in the protected section after the destructor.

Add the following line of code in CIRCCTL.H:

    short m_circleOffset;

as shown in the following code example:

class CCircCtrl : public COleControl
{
...
protected:
    ~CCircCtrl();
    void GetDrawRect(CRect* rc);
    short m_circleOffset;
...
};

Modify the Get method created by ClassWizard, GetCircleOffset, in CIRCCTL.CPP to return the value of this new variable (the //TODO comment line can be removed):

short CCircCtrl::GetCircleOffset()
{
    return m_circleOffset;
}


Setting the CircleOffset Default Value

Similar to other properties, the CircleOffset property must be initialized to a default value when the control is created. In this case, setting CircleOffset to 0 is logical. Add the following line:

    PX_Short(pPX, _T("CircleOffset"), m_circleOffset, 0);

to the DoPropExchange function in CIRCCTL.CPP:

void CCircCtrl::DoPropExchange(CPropExchange* pPX)
{
    ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
    COleControl::DoPropExchange(pPX);

    PX_Bool(pPX, _T("CircleShape"), m_circleShape, TRUE);
    PX_Short(pPX, _T("CircleOffset"), m_circleOffset, 0);
}

The CircleOffset property is initialized by calling PX_Short in the CCircCtrl::DoPropExchange member function, passing 0 as the default value parameter (the last parameter in the function call). Now, when the circle is first drawn, it will be centered in the bounding rectangle of the Circle control. As with all literal strings, the property name parameter must be passed through the _T macro.

Properties initialized by calling a PX_ function in a control's DoPropExchange function are persistent properties; thus, CircleOffset is a persistent property. Steps described in Setting the Circle Offset Property will set the modified flag for persistent properties whenever the value of the CircleOffset property changes.


Setting the CircleOffset Property

The reason that CircleOffset is defined as a Get/Set property is to provide an entry point where the offset can be validated when the user attempts to change its value. Two rules govern the offset validation:

  • The CircleShape property must be set to TRUE.

  • The new offset must not force the circle outside the control's bounding rectangle.

The Set function for Get/Set properties can be written to ignore requests to set the property value to its current value. Properties implemented using only member variables do not provide this ability. Consequently, there is another optional rule that governs validation of Get/Set properties in general:

  • The new property value must be different from the old property value.

If any of the rules are not followed, SetCircleOffset ignores the request.

If the new offset is valid, the value of the CircleOffset property is updated. Because the CircleOffset property is persistent, the modified flag is set. Because CircleOffset affects the visual appearance of the control, the control is invalidated. Invalidating the control is the simplest way to force the control to be redrawn.

Modify SetCircleOffset in CIRCCTL.CPP to validate the new value. Remove the //TODO comment line. Add the if statement and other code below to the SetModifiedFlag function call that ClassWizard added:

void CCircCtrl::SetCircleOffset(short nNewValue)
{
    // Validate the specified offset value
    if ((m_circleOffset != nNewValue) && m_circleShape && InBounds(nNewValue))
    {
        m_circleOffset = nNewValue;
        SetModifiedFlag();
        InvalidateControl();
    }
}

Notice that the function SetCircleOffset calls the InBounds member function. This function returns TRUE if the specified offset does not force the circle outside the bounding rectangle of the control.

To implement the InBounds member function

  1. On WizardBar, select CCircCtl to edit the CCIRCCTL.CPP file.

  2. Click the arrow on the action button, located on the right end of WizardBar, and select Add Member Function.

    The Add Member Function dialog box is displayed.

  3. In the Function Type edit box, type BOOL. This is the type of value the function will return.

  4. In the Function Declaration edit box, type InBounds(short nOffset).

  5. Under Access, click Protected.

  6. Click OK to close the Add Member Function dialog box.

    This will add the following declaration in the protected section of CIRCCTL.H:

    BOOL InBounds(short nOffset);

    It will also add this function definition at the end of CIRCCTL.CPP and position the cursor in the function:

    void CCircCtrl::InBounds(short nOffset)
{

}
  7. Add code to the function in CIRCCTL.CPP as shown below:
    BOOL CCircCtrl::InBounds(short nOffset)
{
    CRect rc;
    int diameter;
    int length;
    int cx,cy;

    GetControlSize(&cx, &cy);

    if (cx > cy)
    {
        length = cx;
        diameter = cy;
    }
    else
    {
        length = cy;
        diameter = cx;
    }
    if (nOffset < 0)
        nOffset = -nOffset;
    return (diameter / 2 + nOffset) <= (length / 2);
}


Drawing the Control

The CircleOffset functionality does not require any major drawing modifications. In fact, the OnDraw function need not be changed at all. You will change the GetDrawRect function to offset the circle in the bounding rectangle. The coordinates of the square region returned by GetDrawRect are properly adjusted based on the current value of the CircleOffset property. Add the two sets of lines of code below beginning with:

// offset circle in bounding rect

to the existing GetDrawRect code.

void CCircCtrl::GetDrawRect(CRect* rc)
{
    if (m_circleShape)
    {
        int cx = rc->right - rc->left;
        int cy = rc->bottom - rc->top;

        if (cx > cy)
        {
            rc->left += (cx - cy) / 2;
            rc->right = rc->left + cy;

            // offset circle in bounding rect
            rc->left += m_circleOffset;
            rc->right += m_circleOffset;
        }
        else
        {
            rc->top += (cy - cx) / 2;
            rc->bottom = rc->top + cx;

            // offset circle in bounding rect
            rc->bottom -= m_circleOffset;
            rc->top -= m_circleOffset;
        }
    }
}

The changes to the code for GetDrawRect are adjustments to the square region's left and right coordinates (if the x-extent is greater) or the top and bottom coordinates (if the y-extent is greater).


Modifying the OnCircleShapeChanged Function

The CircleOffset functional specification requires that the value of the CircleOffset property be reset to 0 when CircleShape is set to TRUE. Recall from the previous lesson, Adding a Custom Notification Property, that the OnCircleShapeChanged function is called whenever the CircleShape property is modified, so it is the appropriate place to handle this requirement. Modify OnCircleShapeChanged by adding the lines of code beginning with:

    //reset the circle offset, if necessary:

as shown in the following example:

void CCircCtrl::OnCircleShapeChanged()
{
    // force the control to redraw itself
    InvalidateControl();

    SetModifiedFlag();

    // reset the circle offset, if necessary
    if (m_circleShape)
        SetCircleOffset(0);
}

Setting the CircleOffset property to 0 under this condition causes the circle to be centered whenever the CircleShape property is set back to TRUE. All actions related to the CircleShape property change are completed before the CircleOffset property is changed.


Adding the OnSize Function

The final requirement is to set CircleOffset to 0 whenever the size of the control changes. This can be done by adding a notification function, OnSize, to CCircCtrl that responds to a WM_SIZE message.

To add the OnSize function

  1. On WizardBar, select CCircCtl to edit the CCIRCCTL.CPP file.

  2. Click the arrow on the action button, located on the right end of WizardBar, and select Add Windows Message Handler.

    The New Windows Message and Event Handlers dialog box is displayed.

  3. In the New Windows messages/events list box, select WM_SIZE.

  4. Click Add and Edit.

    This returns you to the CIRCCTL.CPP file with the insertion point positioned at the code for the OnSize member function. Add the code shown below.

The OnSize member function resets the CircleOffset property to 0 only if CircleShape is TRUE. This ensures that the circle always stays within the bounding rectangle of the control no matter how the control is resized. Setting the CircleOffset property to 0 is the simplest way to accomplish this task.

void CCircCtrl::OnSize(UINT nType, int cx, int cy)
{
    COleControl::OnSize(nType, cx, cy);

    // If circle shape is true, reset the offset when control size is changed
    if (m_circleShape)
        SetCircleOffset(0);
}


Rebuilding the Control with CircleOffset Implemented

Now that you have completed the CircleOffset property modifications, you must rebuild the control. Because the CIRC.ODL file changed when you added the CircleOffset property to the control, the type library is automatically updated during the build.

To rebuild the control

  • On the Build menu, click Build Circ.ocx.


Testing the Control CircleOffset Property

You implemented CircleOffset functionality, and you can use Test Container to test it.

To insert the Circ control in Test Container

  1. On the Tools menu, click ActiveX Control Test Container.

  2. On the Edit menu, click Insert New Control.

    The Insert Control dialog box appears.

  3. From the list box, select Circ Control.

  4. Click OK to close the Insert Control dialog box and insert the control into Test Container.

The Circle control is displayed in Test Container. Notice that the control is initially drawn as a perfect circle. The default bounding rectangle of the control is a rectangle with an x-extent greater than its y-extent.

Next, change the value of the CircleOffset property to cause the circle to move from the center of the bounding rectangle:

To change the CircleOffset property

  1. On the Control menu, click Invoke Methods.

    The Invoke Methods dialog box appears.

  2. From the Method Name drop-down combo box, select the PropPut version of CircleOffset.

    The Value column in the Parameters control displays 0 (zero), which is the CircleOffset property's default value.

    In the Parameter Value control, type 10, which indicates 10 units to the right of center. (If the y-extent of the control were larger, it would indicate 10 units to the top of center.)

  3. Click Set Value and then Invoke.

    The Circle control is redrawn, with the circle offset from center by 10 units.

Experiment with different values for the CircleOffset property: type a negative number to force the circle to the left of center. Try a number larger than the width or height of the control; for example, 2000. Notice that nothing happens.

Now, resize the control so that its y-extent is greater than its x-extent. Specify positive and negative values for the CircleOffset property to see how the circle moves above and below the center of the control's bounding rectangle.

When you have finished, close Test Container.


Adding Special Effects

Circle: ActiveX Control, Lesson 5

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

This lesson further evolves the functionality of the Circle control. A new behavior is introduced that produces special effects in response to mouse events at run time.

Note You can find a finished example of this lesson's code in the CIRC2 sample source code directory.

When you click the left mouse button inside the circle, the control briefly flashes a different color. To implement this special effect, the control must respond to mouse events.

This lesson introduces a new custom member variable property called FlashColor. The value of the FlashColor property contains the color the control will flash when the left mouse button is clicked with the insertion point (or cursor) within the circle.

In this lesson, you will:

  • Add the FlashColor property to the Circle control.

  • Set the value of the default color for FlashColor.

  • Implement code to respond to mouse events.

  • Implement code to perform hit testing.

  • Add the FlashColor function.


Adding the FlashColor Property

The FlashColor property holds the color value that flashes within the control circle. Windows represents a color as a 32-bit value, defined as a COLORREF type. The ActiveX control classes do not directly support the COLORREF type, but they do support an OLE_COLOR type that can hold the required information. Thus, the FlashColor property is defined as an OLE_COLOR type.

The FlashColor property requires no special processing when its value is changed or accessed. For this reason, FlashColor can be defined as a simple member variable property.

To add the FlashColor property

  1. On the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. From the Class name drop-down list box, select CCircCtrl.

  4. Click Add Property.

    The Add Property dialog box appears.

  5. In the edit control of the External name drop-down combo box, type FlashColor.

  6. From the Type list box, select OLE_COLOR.

  7. Under Implementation, select Member variable (it may already be selected).

  8. Verify that the Notification function edit control contains OnFlashColorChanged and that the Variable name edit control contains m_flashColor.

  9. Click OK to close the Add Property dialog box.

    This returns you to the Automation tab. Notice that the implementation of the FlashColor property appears as:

    Implementation:
OLE_COLOR m_flashColor;
void OnFlashColorChanged();
  10. Click OK to confirm your choices and close ClassWizard.

ClassWizard creates the code to add the FlashColor property, modifying both the CCircCtrl class and the CIRC.ODL file. Because FlashColor is a member variable property type, ClassWizard modifies the CCircCtrl class's dispatch map in CIRCCTL.CPP to include a DISP_PROPERTY_NOTIFY macro entry:

BEGIN_DISPATCH_MAP(CCircCtrl, COleControl)
    //{{AFX_DISPATCH_MAP(CCircCtrl)
    DISP_PROPERTY_NOTIFY(CCircCtrl, "FlashColor",
        m_flashColor, OnFlashColorChanged, VT_COLOR)
    DISP_PROPERTY_NOTIFY(CCircCtrl, "CircleShape",
        m_circleShape, OnCircleShapeChanged, VT_BOOL)
    DISP_PROPERTY_EX(CCircCtrl, "CircleOffset",
       GetCircleOffset, SetCircleOffset, VT_I2)
    DISP_STOCKPROP_BACKCOLOR()
    //}}AFX_DISPATCH_MAP
    DISP_FUNCTION_ID(CCircCtrl, "AboutBox",
        DISPID_ABOUTBOX, AboutBox, VT_EMPTY, VTS_NONE)
END_DISPATCH_MAP()

The DISP_PROPERTY_NOTIFY macro associates the FlashColor property name with the following:

  • Its corresponding CCircCtrl class member variable

  • The name of the CCircCtrl class notification function (OnFlashColorChanged) that is called whenever the value of the FlashColor property is changed

  • Its type, VT_COLOR, which corresponds to an OLE_COLOR value

ClassWizard also adds a declaration for the OnFlashColorChanged notification function in CIRCCTL.H and a function template in CIRCCTL.CPP:

void CCircCtrl::OnFlashColorChanged()
{
    // TODO: Add notification handler code
    SetModifiedFlag();
}


Setting the Default FlashColor Value

Next, you will write the code that sets the default value of the FlashColor property. Choosing the default value is relatively unimportant for the example; any arbitrary value will do, as long as it's not the same as the background color.

Windows uses a 32-bit unsigned integer value to represent a color. The lowest three bytes specify red, green, and blue values, each with a range from 0 through 255. Therefore, the value 0x000000FF is red, 0x0000FF00 is green, and 0x00FF0000 is blue. To simplify the process of assigning color values, you will use the RGB macro. Its three parameters consist of red, green, and blue values, in that order.

The member function DoPropExchange initializes the m_flashColor member variable to a value corresponding to the color red. Because the PX_Long function expects a reference to a long and m_flashColor is an unsigned long, m_flashColor is cast to a long reference.

Add the following line to the DoPropExchange function in CIRCCTL.CPP:

    PX_Long(pPX, _T("FlashColor"), (long &)m_flashColor, RGB(0xFF, 0x00, 0x00));

as shown in the following code example:

void CCircCtrl::DoPropExchange(CPropExchange* pPX)
{
    ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
    COleControl::DoPropExchange(pPX);

    PX_Bool(pPX, _T("CircleShape"), m_circleShape, TRUE);
    PX_Short(pPX, _T("CircleOffset"), m_circleOffset, 0);
    PX_Long(pPX, _T("FlashColor"), (long &)m_flashColor, RGB(0xFF, 0x00, 0x00));
}

As with all literal strings, the property name string is passed through the _T macro before being passed as a parameter to the PX_Long function.


Responding to Mouse Events

To better understand how to implement FlashColor functionality, you need to understand the behavior the Circle control should have when the mouse is clicked inside it. When the left mouse button is pressed, the circle is painted using the color stored as the value of the FlashColor property. When the left mouse button is released, the circle is repainted using the color stored as the value of the BackColor property. Clicking in the circle causes it to flash.

To implement the flash behavior, the Circle control must handle mouse events. These events are mapped to the following Windows mouse messages:

Message | Response
WM_LBUTTONDOWN | Paints the circle with the color stored as the value of the FlashColor property.
WM_LBUTTONDBLCLK | Paints the circle with the color stored as the value of the FlashColor property.
WM_LBUTTONUP | Paints the circle with the color stored as the value of the BackColor property.

Notice that a WM_LBUTTONDBLCLK message is handled in the same manner as a WM_LBUTTONDOWN message. If the left mouse button is double-clicked in the circle, the desired flash effect occurs.

The next step is to use ClassWizard to add a message handler for each of the three mouse messages.

To add the message handlers

  1. On the View menu, click ClassWizard.

  2. Click the Message Maps tab.

  3. From the Class name drop-down list box, select CCircCtrl.

  4. From the Object IDs list box, select CCircCtrl.

    The list of message types appears in the Messages list box.

  5. From the Messages list box, select WM_LBUTTONDOWN.

  6. Click Add Function.

    Notice that this new handler appears in the Member functions list box as:

    Member functions:
OnLButtonDown          ON_WM_LBUTTONDOWN
  7. From the Messages list box, select WM_LBUTTONDBLCLK.

  8. Click Add Function.

    Notice that this new handler appears in the Member functions list box as:

    Member functions:
OnLButtonDblClk        ON_WM_LBUTTONDBLCLK
  9. From the Messages list box, select WM_LBUTTONUP.

  10. Click Add Function.

    Notice that this new handler appears in the Member functions list box as:

    Member functions:
OnLButtonUp        ON_WM_LBUTTONUP
  11. Select the OnLButtonDown ON_WM_LBUTTONDOWN entry in the Member functions list box.

  12. Click Edit Code.

    ClassWizard closes and the cursor is positioned at the CCircCtrl:OnLButtonDown function in CIRCCTL.CPP.

For each message handled, ClassWizard automatically inserts the function with a //TODO comment line and a call to the base class, as shown in the code for OnLButtonDown:

void CCircCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
    // TODO: Add your message handler code here and/or call default
    COleControl::OnLButtonDown(nFlags, point);
}

The code below shows the fully implemented mouse message handlers. Replace the //TODO comment lines with your Circle drawing code as shown in the following code.

void CCircCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
    CDC* pdc;

    // Flash the color of the control if within the ellipse.
    if (InCircle(point))
    {
        pdc = GetDC();
        FlashColor(pdc);
        ReleaseDC(pdc);
    }

    COleControl::OnLButtonDown(nFlags, point);
}

void CCircCtrl::OnLButtonDblClk(UINT nFlags, CPoint point)
{
    CDC* pdc;

    // Flash the color of the control if within the ellipse.
    if (InCircle(point))
    {
        pdc = GetDC();
        FlashColor(pdc);
        ReleaseDC(pdc);
    }

    COleControl::OnLButtonDblClk(nFlags, point);
}

void CCircCtrl::OnLButtonUp(UINT nFlags, CPoint point)
{
    // Redraw the control.
    if (InCircle(point))
        InvalidateControl();

    COleControl::OnLButtonUp(nFlags, point);
}

OnLButtonDown and OnLButtonDblClk implement the same code: The circle will be painted using the color stored as the value of the FlashColor property. The OnLButtonUp function invalidates the control, causing the circle to be redrawn with the default background color.

Notice that to implement the FlashColor property's behavior, two new functions, InCircle and FlashColor, have been introduced. These functions are described in Hit Testing and Adding the FlashColor Function.


Hit Testing

The rule governing the Circle control flash behavior is that the circle should flash only if the mouse is within the circular area of the control. To achieve this effect, you must implement "hit testing," which checks the coordinates of every mouse click within the control to see if they are within the circle. Hit testing is implemented in the Circle control by the InCircle function. InCircle returns TRUE if the given point is within the area of the circle or the ellipse.

To implement the InCircle function

  1. On WizardBar, select CCircCtl to edit the CCIRCCTL.CPP file.

  2. Click the arrow on the action button, located on the right end of the WizardBar, and select Add Member Function.

    The Add Member Function dialog box is displayed.

  3. In the Function Type edit box, type BOOL. This is the type of value the function will return.

  4. In the Function Declaration edit box, type InCircle(CPoint& point).

  5. Under Access, click Protected.

  6. Click OK to close the Add Member Function dialog box.

    This will add the following declaration in the protected section of CIRCCTL.H:

    BOOL InCircle(CPoint& point);

    It will also add a function definition at the end of CIRCCTL.CPP and position the cursor in the function:

    BOOL CCircCtrl::InCircle(CPoint& point)
{

}
  7. Add code to the function in CIRCCTL.CPP as shown:
    BOOL CCircCtrl::InCircle(CPoint& point)
{
    CRect rc;
    GetClientRect(rc);
    GetDrawRect(&rc);

    // Determine radii
    double a = (rc.right - rc.left) / 2;
    double b = (rc.bottom - rc.top) / 2;

    // Determine x, y
    double x = point.x - (rc.left + rc.right) / 2;
    double y = point.y - (rc.top + rc.bottom) / 2;

    // Apply ellipse formula
    return ((x * x) / (a * a) + (y * y) / (b * b) <= 1);
}

The function works by calculating whether the point is within the boundary of the ellipse. The GetDrawRect function is called to make the necessary adjustments to the bounding rectangle if the value of the CircleShape property is TRUE.

The variables a and b are set to the horizontal and vertical radii of the ellipse. Based on the given point, the variables x and y are translated into the coordinates that are offsets from the center of the ellipse. The last line returns the Boolean result of the calculation, using the standard formula for an ellipse. Note that this calculation is also valid for a circle because a circle is simply a special case of an ellipse.

The mouse message handlers perform hit testing by passing the point coordinates that they receive as parameters to the InCircle member function. If InCircle returns TRUE, the circle is painted appropriately.


Adding the FlashColor Function

The FlashColor function paints the circle using the color value stored as the value of the FlashColor property. The code is similar to the code in the OnDraw function. The difference is that the FlashColor function paints the circle itself using the value of the FlashColor property, rather than that of the BackColor property, to fill the background of the ellipse.

To implement the FlashColor function

  1. On WizardBar, select CCircCtl to edit the CCIRCCTL.CPP file.

  2. Click the arrow on the action button, located on the right end of WizardBar, and select Add Member Function.

    The Add Member Function dialog box is displayed.

  3. In the Function Type edit box, type void. This is the type of value the function will return.

  4. In the Function Declaration edit box, type FlashColor(CDC* pdc).

  5. Under Access, click Protected.

  6. Click OK to close the Add Member Function dialog box.

    This will add the following declaration in the protected section of CIRCCTL.H:

    void FlashColor(CDC* pdc);

    It will also add a function definition at the end of CIRCCTL.CPP and position the cursor in the function:

    void CCircCtrl::FlashColor(CDC* pdc)
{

}
  7. Add code to the function in CIRCCTL.CPP as shown:
    void CCircCtrl::FlashColor(CDC* pdc)
{
    CBrush* pOldBrush;
    CBrush flashBrush(TranslateColor(m_flashColor));
    CPen* pOldPen;
    CRect rc;

    GetClientRect(rc);
    GetDrawRect(&rc);
    pOldBrush = pdc->SelectObject(&flashBrush);
    pOldPen = (CPen*)pdc->SelectStockObject(BLACK_PEN);
    pdc->Ellipse(rc);
    pdc->SelectObject(pOldPen);
    pdc->SelectObject(pOldBrush);
}

The flashBrush variable constructs a solid brush using the value of the FlashColor property stored in m_flashBrush. Because m_flashBrush is an OLE_COLOR value, the TranslateColor function is called to convert it to a COLORREF value first. The code selects the brush into the device context pdc, making sure to retain the old brush value in pOldBrush. A stock black pen is also selected into the device context. The old pen value is saved in pOldPen. The ellipse is then drawn with the black pen and filled in with the color specified by the value of the FlashColor property. Finally, the code selects the original pen and brush back into the device context pdc. The solid brush created when flashBrush is constructed is deleted when the flashBrush destructor is called. This occurs when the FlashColor function returns and the flashBrush variable goes out of scope.


Rebuilding the Control with FlashColor Implemented

Now that you have completed the FlashColor property modifications, you must rebuild the control.

To rebuild the control

  • On the Build menu, click Build Circ.ocx.


Testing the FlashColor Property

You have implemented FlashColor functionality, and you can test it using Test Container.

To test the FlashColor property

  1. On the Tools menu, click ActiveX Control Test Container.

  2. On the Edit menu, click Insert New Control.

    The Insert Control dialog box appears.

  3. From the list box, select Circ Control.

  4. Click OK to close the Insert Control dialog box and insert the control into Test Container.

The Circle control is displayed in Test Container. Move the mouse over the circle and click once. The circle flashes red, the default value to which the FlashColor property was initialized in the CCircCtrl::DoPropExchange member function.

To change the FlashColor property

  1. On the Control menu, click Invoke Methods.

  2. From the Method Name drop-down combo box, select the PropPut version of FlashColor.

  3. The Value column in the Parameters control is blank, and the Parameter Type control shows VT_EMPTY.

  4. In the Parameter Type control, select VT_COLOR. The Parameter Value control changes to a Choose Color button.

  5. Click the Choose Color button and select black from the Color dialog box.

  6. Click OK to confirm your selection. The Value column in the Parameters control should read 00000000.

  7. Click Invoke.

  8. Click in the circle to verify that it flashes black.

Repeat the steps above, but select different colors in the Color dialog box to test the control. You will see the Value column in the Parameters control change to reflect the value of each color you select, as follows.

Color | Hex Value | Decimal Value
White | 0x00FFFFFF | 16777215
Black | 0x00000000 | 0
Gray | 0x00808080 | 8421504
Red | 0x000000FF | 255
Yellow | 0x0000FFFF | 65535
Green | 0x0000FF00 | 65280
Cyan | 0x00FFFF00 | 16776960
Blue | 0x00FF0000 | 16711680
Magenta | 0x00FF00FF | 16711935

When you have finished, close Test Container.


Adding Custom Events to the Circle Control

Circle: ActiveX Control, Lesson 6

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

This lesson illustrates how to add custom events to the Circle control. ActiveX controls use events to notify a container that something has happened to a control. Commonly, an event is caused by user interaction, such as mouse or keyboard input, a Windows interaction, or a special condition occurring in the control itself. When one of these actions occur, the control alerts the container by firing an event.

MFC supports two kinds of events: stock and custom. Stock events are those events that class COleControl handles automatically. Custom events require extra work by the developer, but allow a control to notify a container when an action specific to the control occurs.

This lesson shows you how to add the ClickIn and ClickOut custom events. The ClickIn event is fired when the user clicks the left mouse button with the insertion point inside the control circle; ClickOut is fired when the user clicks the left mouse button with the insertion point outside the circle.

Note You can find a finished example of this lesson's code in the CIRC2 sample source code directory.

In this lesson, you will:

  • Add the ClickIn event to the Circle control.

  • Implement the code to fire the ClickIn event.

  • Add the ClickOut event to the Circle control.

  • Implement the code to fire the ClickOut event.

Suggested Reading

  • ActiveX Controls: Events, Visual C++ Programmer’s Guide


Adding the ClickIn Event

Events, like properties, are added to a control using ClassWizard. When you add a custom event, ClassWizard creates the code necessary to declare the event; however, the control developer must write additional code to fire the event.

An event can also define parameters it can pass to the control’s container when it is fires. Event parameters can be added using ClassWizard. In this topic, you will define two parameters for the ClickIn event: x and y, which represent the x and y coordinates of the mouse position when the left mouse button is clicked.

To add the ClickIn event

  1. On the View menu, click ClassWizard.

  2. Click the ActiveX Events tab.

  3. From the Class name drop-down list box, select CCircCtrl.

  4. Click Add Event.

    The Add Event dialog box appears.

  5. In the edit control of the External name drop-down combo box, type ClickIn.

  6. In the Parameter list, use the grid control to add a parameter, called x (with a type of OLE_XPOS_PIXELS).

  7. Use the grid control to add a second parameter, called y (with a type of OLE_YPOS_PIXELS).

  8. Click OK to close the Add Event dialog box.

    This returns you to the ActiveX Events tab. Notice that the implementation of the ClickIn event appears as:

    Implementation:
void FireClickIn(OLE_XPOS_PIXELS x, OLE_YPOS_PIXELS y);
  9. Click OK to confirm your choices and close ClassWizard.

ClassWizard creates the code to add the ClickIn event, modifying both the CCircCtrl class files and the CIRC.ODL file.

ClassWizard modifies the CCircCtrl class event map in CIRCCTL.CPP to add a macro entry for the ClickIn event:

BEGIN_EVENT_MAP(CCircCtrl, COleControl)
    //{{AFX_EVENT_MAP(CCircCtrl)
    EVENT_CUSTOM("ClickIn", FireClickIn, VTS_XPOS_PIXELS  VTS_YPOS_PIXELS)
    //}}AFX_EVENT_MAP
END_EVENT_MAP()

The macro EVENT_CUSTOM associates the ClickIn event name with FireClickIn, the function that actually fires the event, and with the type definitions for the x and y parameters that ClickIn uses.

ClassWizard adds an inline function to the CCircCtrl class declaration in CIRCCTL.H which, when called, fires the ClickIn event. The FireClickIn function simply calls the FireEvent function to do its work. Event functions like FireClickIn are added to provide a type-safe way of firing an event.

class CCircCtrl : public COleControl
{
    ...
    //{{AFX_EVENT(CCircCtrl)
    void FireClickIn(OLE_XPOS_PIXELS x, OLE_YPOS_PIXELS y)
        {FireEvent(eventidClickIn,EVENT_PARAM(VTS_XPOS_PIXELS  VTS_YPOS_PIXELS),
    x, y);}
    //}}AFX_EVENT
    ...
}


Firing the ClickIn Event

Now that the ClickIn event has been declared, it can be fired. The control itself determines when a custom event should be fired. In this example, the ClickIn event fires when the user clicks the left mouse button with the insertion point inside the control circle.

The code that fires the ClickIn event should be triggered when a WM_LBUTTONDOWN message is received from Windows. The Circle control already has a message handler function defined for WM_LBUTTONDOWN — the OnLButtonDown function in CIRCCTL.CPP.

Add the following lines to change OnLButtonDown to fire the ClickIn event:

        // Fire the ClickIn event
        FireClickIn(point.x, point.y);

as shown in the following code example:

void CCircCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
    CDC* pdc;

    // Flash the color of the control if within the ellipse.
    if (InCircle(point))
    {
        pdc = GetDC();
        FlashColor(pdc);
        ReleaseDC(pdc);

       // Fire the ClickIn event
        FireClickIn(point.x, point.y);
    }

    COleControl::OnLButtonDown(nFlags, point);
}

Modifying OnLButtonDown to fire the event requires a call to the FireClickIn function. The call to FireClickIn is made only if the InCircle function returns TRUE, which means that the insertion point is within the circle.


Adding the ClickOut Event

When the user clicks the left mouse button and the insertion point is outside the control circle, the ClickOut event should fire. The ClickOut event is simpler than the ClickIn event in that it defines no arguments.

To add the ClickOut event

  1. On the View menu, click ClassWizard.

  2. Click the ActiveX Events tab.

  3. From the Class name drop-down list box, select CCircCtrl.

  4. Click Add Event.

    The Add Event dialog box appears.

  5. In the edit control of the External name drop-down combo box, type ClickOut.

  6. Click OK to close the Add Event dialog box.

    This returns you to the ActiveX Events tab. Notice that the implementation of the ClickOut event appears as:

    Implementation:
void FireClickOut();
  7. Click OK to confirm your choices and close ClassWizard.

As with the ClickIn event, ClassWizard creates the code to add the ClickOut event, modifying the files CIRC.ODL, CIRCCTL.H, and CIRCCTL.CPP.

ClassWizard modifies the CCircCtrl class’s event map in CIRCCTL.CPP to add a macro entry for the ClickOut event:

BEGIN_EVENT_MAP(CCircCtrl, COleControl)
    //{{AFX_EVENT_MAP(CCircCtrl)
    EVENT_CUSTOM("ClickIn", FireClickIn, VTS_XPOS_PIXELS  VTS_YPOS_PIXELS)
    EVENT_CUSTOM("ClickOut", FireClickOut, VTS_NONE)
    //}}AFX_EVENT_MAP
END_EVENT_MAP()

The macro EVENT_CUSTOM associates the ClickOut event name with the member function FireClickOut and with its argument type definition. The ClickOut argument type definition is set to VTS_NONE, which indicates that the ClickOut event passes no arguments.

ClassWizard adds an inline function to the CCircCtrl class declaration. The FireClickOut function provides a type-safe call to fire the event:

class CCircCtrl : public COleControl
{
    ...
    //{{AFX_EVENT(CCircCtrl)
    void FireClickIn(OLE_XPOS_PIXELS x, OLE_YPOS_PIXELS y)
        {FireEvent(eventidClickIn,EVENT_PARAM(VTS_XPOS_PIXELS  VTS_YPOS_PIXELS),
    x, y);}
    void FireClickOut()
        {FireEvent(eventidClickOut,EVENT_PARAM(VTS_NONE));}
    //}}AFX_EVENT
    ...
}


Firing the ClickOut Event

ClickOut fires under a condition opposite to the condition that fires ClickIn. You must modify the OnLButtonDown function in CIRCCTL.CPP to call FireClickOut when the insertion point is not within the circle (that is, the InCircle function returns FALSE). The FireClickOut function fires the event.

Add the following lines of code to CIRCCTL.CPP:

    else
        // Fire the ClickOut event
        FireClickOut();

as shown in the code example:

void CCircCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
    CDC*    pdc;

    // Flash the color of the control if within the ellipse.
    if (InCircle(point))
    {
        pdc = GetDC();
        FlashColor(pdc);
        ReleaseDC(pdc);

        // Fire the ClickIn event
        FireClickIn(point.x, point.y);
    }
    else
        // Fire the ClickOut event
        FireClickOut();

    COleControl::OnLButtonDown(nFlags, point);
}


Rebuilding the Control with ClickIn and ClickOut Implemented

Now that you have added the ClickIn and ClickOut events to the Circle control, you need to rebuild it.

To rebuild the control

  • On the Build menu, click Build Circ.ocx.


Testing the ClickIn and ClickOut Events

You have defined the ClickIn and ClickOut events and implemented the code to fire the events. Now you can use Test Container to test them.

To test the ClickIn and ClickOut events

  1. On the Tools menu, click ActiveX Control Test Container.

  2. On the Edit menu, click Insert New Control.

    The Insert Control dialog box appears.

  3. From the list box, select Circ Control.

  4. Click OK to close the Insert Control dialog box and insert the control into Test Container.

  5. On the Options menu, select Logging and make sure that event information is being routed to a file or window.

  6. Position the mouse within the circle and click the left mouse button.

    A ClickIn entry is added to the Event Log list, and the x and y mouse coordinates are shown.

  7. Position the mouse outside the circle and click the left mouse button.

    A ClickOut entry is added to the Event Log list. Notice that no arguments are shown because none were defined for this event.

When you have finished, close Test Container.


Handling Text and Fonts

Circle: ActiveX Control, Lesson 7

Tip If you prefer working from a printed tutorial, Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

This lesson explores the text and font support provided in the ActiveX control classes. You will add three stock properties, Caption, Font, and ForeColor, to implement a caption in the Circle control. With these properties, you can use any font to draw text in the control.

Note You can find a finished example of this lesson's code in the CIRC3 sample source code directory.

The Circle sample uses the stock Caption property to hold the text of the caption. The stock Font property holds the font that the Circle control uses to draw its caption. The resulting caption text is drawn using the color stored as the value of the ForeColor property.

In this lesson, you will:

  • Add the stock properties: Caption, Font, and ForeColor.

  • Implement the Caption drawing behavior.

  • Add stock color and font property pages.

Suggested Reading

  • ActiveX Controls: Properties, Visual C++ Programmer’s Guide

  • ActiveX Controls: Using Fonts in an ActiveX Control, Visual C++ Programmer’s Guide

  • ActiveX Controls: Property Pages, Visual C++ Programmer’s Guide

  • ActiveX Controls: Painting an ActiveX Control, Visual C++ Programmer’s Guide


Adding the Stock Properties

Use ClassWizard to add the stock Caption, Font, and ForeColor properties to the Circle control.

To add the stock properties

  1. On the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. In the Class name drop-down list box, select CCircCtrl.

  4. Click Add Property.

    The Add Property dialog box appears.

  5. From the External name drop-down combo box, select Caption.

  6. Under Implementation, select Stock (it may already be selected).

  7. Click OK to close the Add Property dialog box.

  8. Click Add Property.

  9. From the External name drop-down combo box, select Font.

  10. Click OK to close the Add Property dialog box.

  11. Click Add Property.

  12. From the External name drop-down combo box, select ForeColor.

  13. Click OK to close the Add Property dialog box.

    This returns you to the Automation tab. Notice that the implementation of the ForeColor property appears as:

    Implementation:
Stock Property
  14. Click OK to confirm your choices and close ClassWizard.

ClassWizard creates the code to add the Caption, Font, and ForeColor properties, modifying both the CCircCtrl class and the CIRC.ODL file.

ClassWizard modifies the CCircCtrl class dispatch map in CIRCCTL.CPP to add the DISP_STOCKPROP_CAPTION macro entry for each property:

BEGIN_DISPATCH_MAP(CCircCtrl, COleControl)
    //{{AFX_DISPATCH_MAP(CCircCtrl)
    DISP_PROPERTY_NOTIFY(CCircCtrl, "FlashColor", m_flashColor, OnFlashColorChanged, VT_COLOR)
    DISP_PROPERTY_NOTIFY(CCircCtrl, "CircleShape", m_circleShape, OnCircleShapeChanged, VT_BOOL)
    DISP_PROPERTY_EX(CCircCtrl, "CircleOffset", GetCircleOffset, SetCircleOffset, VT_I2)
    DISP_STOCKPROP_BACKCOLOR()
    DISP_STOCKPROP_CAPTION()
    DISP_STOCKPROP_FONT()
    DISP_STOCKPROP_FORECOLOR()
    DISP_FUNCTION_ID(CCircCtrl, "AboutBox", DISPID_ABOUTBOX, AboutBox, VT_EMPTY, VTS_NONE)
    //}}AFX_DISPATCH_MAP
END_DISPATCH_MAP()

The DISP_STOCKPROP_CAPTION macro enables the stock Caption property in the Circle control, implemented as a Get/Set property. Note that the Caption property is an alias for the stock Text property. The Get and Set methods, and notification function for the Caption property are the GetText, SetText, and OnTextChanged functions in class COleControl.

The OnTextChanged function is invoked when the Caption property is modified through the SetText function. By default, OnTextChanged simply invalidates the control.

Stock property notification functions such as OnTextChanged can be overridden in the descendent control class. For example, you may wish to provide an optimal solution for redrawing the caption other than invalidating the control, which forces the whole control to repaint.

In addition to the GetText function, the Caption property can be accessed directly through the InternalGetText function of COleControl. InternalGetText is useful when the control developer accesses the Caption property internally, for example, from the control’s OnDraw function. The CCircCtrl::OnDraw function will be changed to use the InternalGetText function to draw the caption text.

The Circle control uses the font information contained in the stock Font property to draw the caption text. The Font property is actually a pointer to a font object that is encapsulated by class CFontHolder. The font object contains several properties that describe the current font. These properties are accessible through the font object’s IDispatch interface.

Similar to the Caption property, the Font property is implemented as a Get/Set property. The Font property supports a notification function called OnFontChanged, which is defined in COleControl. By default, this function invalidates the control. OnFontChanged can be overridden in the control class to provide an optimal solution to reflecting the font change.

The stock ForeColor property contains the foreground color that the control uses to paint the caption text. Like the stock BackColor property, the ForeColor property is a Get/Set property type.


Implementing Caption Drawing Behavior

Now that all the required properties are in place, you can implement caption drawing. To draw the caption text, you must modify the CCircCtrl::OnDraw function in CIRCCTL.CPP.

The drawing code changes require that the device context passed to the OnDraw function be modified to reflect the background and foreground colors, stored as values of the BackColor and ForeColor properties, respectively. The font contained in the Font property must also be selected into the device context before the caption text can be drawn. The InternalGetText function retrieves the caption text drawn using the ExtTextOut function.

To implement caption drawing in the Circle control

  • Modify the OnDraw member function in CIRCCTL.CPP by adding the two following code blocks:

    Block 1:

        CFont* pOldFont;
    TEXTMETRIC tm;
    const CString& strCaption = InternalGetText();

    // Set the ForeColor property color and transparent background mode into the device context
    pdc->SetTextColor(TranslateColor(GetForeColor()));
    pdc->SetBkMode(TRANSPARENT);

    Block 2:

        // Draw the caption using the stock Font and ForeColor properties
    pOldFont = SelectStockFont(pdc);
    pdc->GetTextMetrics(&tm);
    pdc->SetTextAlign(TA_CENTER | TA_TOP);
    pdc->ExtTextOut((rc.left + rc.right) / 2, (rc.top + rc.bottom - tm.tmHeight) / 2,
        ETO_CLIPPED, rc, strCaption, strCaption.GetLength(), NULL);
    pdc->SelectObject(pOldFont);

    as show in the following example:

    void CCircCtrl::OnDraw(
            CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
    CBrush* pOldBrush;
    CBrush bkBrush(TranslateColor(GetBackColor()));
    CPen* pOldPen;
    CRect rc = rcBounds;
    CFont* pOldFont;
    TEXTMETRIC tm;
    const CString& strCaption = InternalGetText();

    // Set the ForeColor property color and transparent background mode into the device context
    pdc->SetTextColor(TranslateColor(GetForeColor()));
    pdc->SetBkMode(TRANSPARENT);

    // Paint the background using the BackColor property
    pdc->FillRect(rcBounds, &bkBrush);

    // Draw the ellipse
    GetDrawRect(&rc);
    pOldBrush = pdc->SelectObject(&bkBrush);
    pOldPen = (CPen*)pdc->SelectStockObject(BLACK_PEN);
    pdc->Ellipse(rc);

    // Draw the caption using the stock Font and ForeColor properties
    pOldFont = SelectStockFont(pdc);
    pdc->GetTextMetrics(&tm);
    pdc->SetTextAlign(TA_CENTER | TA_TOP);
    pdc->ExtTextOut((rc.left + rc.right) / 2, (rc.top + rc.bottom - tm.tmHeight) / 2,
        ETO_CLIPPED, rc, strCaption, strCaption.GetLength(), NULL);
    pdc->SelectObject(pOldFont);

    pdc->SelectObject(pOldPen);
    pdc->SelectObject(pOldBrush);
}

The code added in the first block in the preceding example starts by declaring three new local variables. The pOldFont variable holds the old font from the device context. The tm variable holds text metric information about the font used to draw the text. The strCaption variable is the text to be drawn. It is initialized by calling the InternalGetText function to get the value of the Caption property. The InternalGetText function should be used instead of the GetText function whenever the returned text will not be modified. This is because GetText returns a string of type BSTR, containing a copy of the caption text, that must be freed.

    CFont* pOldFont;
    TEXTMETRIC tm;
    const CString& strCaption = InternalGetText();

The text color of the device context is set to the current value of the ForeColor property. The OLE_COLOR value returned by the GetForeColor function is translated by the TranslateColor function into a COLORREF value. This value is then passed as a parameter to the SetTextColor function to set the text color in the device context. OLE_COLOR values must be translated by the TranslateColor function whenever a COLORREF value is required.

    pdc->SetTextColor(TranslateColor(GetForeColor()));

The background mode of the text is made transparent in the device context by calling the SetBkMode function.

    pdc->SetBkMode(TRANSPARENT);

In the second block of code added in the previous example, calling the SelectStockFont function (defined in the COleControl base class) selects the current font, stored as the value of the stock Font property, into the device context. The font that used to be selected into the device context is kept in the pOldFont variable to be selected back into the device context later.

    pOldFont = SelectStockFont(pdc);

The text metrics for the font in the device context are retrieved by calling the GetTextMetrics function and are used to center the text vertically. The text alignment of the device context is set using the SetTextAlign function, and this centers the text horizontally. The text is drawn by calling the ExtTextOut function. The text is clipped to the bounding rectangle of the circle or ellipse (which may be different from the bounding rectangle of the control if the CircleShape property is TRUE) and centered both horizontally and vertically. Note that any function affecting the device context passed to the OnDraw function must be among the subset of functions that are allowed for both metafile device contexts and standard device contexts.

    pdc->GetTextMetrics(&tm);
    pdc->SetTextAlign(TA_CENTER | TA_TOP);
    pdc->ExtTextOut((rc.left + rc.right) / 2, (rc.top + rc.bottom - tm.tmHeight) / 2,
        ETO_CLIPPED, rc, strCaption, strCaption.GetLength(), NULL);

The font that used to be selected in the device context is replaced using the SelectObject function.

    pdc->SelectObject(pOldFont);


Adding the Color and Font Property Pages

MFC supports stock color and font property pages that can be easily implemented in a control by adding entries to a control’s property page ID table. ClassWizard produces a default property page ID table in CIRCCTL.CPP that looks like this:

BEGIN_PROPPAGEIDS(CCircCtrl, 1)
    PROPPAGEID(CCircPropPage::guid)
END_PROPPAGEIDS(CCircCtrl)

When editing the property page section, you must modify the page count number in the BEGIN_PROPPAGEIDS macro to reflect the actual number of property pages implemented by the control. In the case of the Circle control, this number is 3 when the color and font property pages are added. The first property page is the default generated by ControlWizard, the second is the color property page, and the third is the font property page.

To change the ID count and add lines for the color and font property pages

  • Change the count to 3 in BEGIN_PROPPAGEIDS. Add two lines to the code generated by ClassWizard in CIRCCTL.CPP as follows:
    BEGIN_PROPPAGEIDS(CCircCtrl, 3)
   PROPPAGEID(CCircPropPage::guid)
   PROPPAGEID(CLSID_CColorPropPage)
   PROPPAGEID(CLSID_CFontPropPage)
END_PROPPAGEIDS(CCircCtrl)

The default property page allows properties to be viewed and edited. MFC also provides stock property page support for picture properties.


Rebuilding the Control with Caption, Font, and ForeColor Support Implemented

Now that you have added caption, font, and color support and two stock property pages, you must rebuild the control. Because you changed the CIRC.ODL file, the type library is automatically updated during the build.

To rebuild the control

  • On the Build menu, click Build Circ.ocx.


Testing the Caption Property

Use Test Container to test the caption drawing behavior.

To test the caption property

  1. On the Tools menu, click ActiveX Control Test Container.

  2. On the Edit menu, click Insert New Control.

    The Insert Control dialog box appears.

  3. From the list box, select Circ Control.

  4. Click OK to close the Insert Object dialog box and insert the control into Test Container.

  5. On the Control menu, click Invoke Methods.

  6. From the Method Name drop-down combo box, select the PropPut version of Caption.

  7. In the Parameter Value control, type Hello.

  8. Click Set Value and then click Invoke.

The string “Hello” is displayed in the circle. Now try typing some longer phrases for the caption. Notice that the phrase is clipped to the bounds of the circle.

Next, modify the caption color and font.

To modify the color and font properties

  1. On the Edit menu, click Properties... Circ Control Object.

    The Circ Control Properties property page dialog box appears.

  2. Click the Fonts tab.

    Modify the different font properties and click Apply to see your changes reflected in the control.

  3. Click the Colors tab.

    Modify the different color properties and click Apply to see your changes reflected in the control. Notice that the Property Name drop-down list box contains three entries — the stock BackColor, stock ForeColor, and custom FlashColor properties.

When you have finished, close Test Container.


Modifying the Default Property Page

Circle: ActiveX Control, Lesson 8

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

This lesson illustrates how to work with the default property page. The default property page is a dialog box that allows the user to add controls for viewing and changing properties. ControlWizard supplies a default property page when it creates the ActiveX control project.

Note You can find a finished example of this lesson's code in the CIRC3 sample source code directory.

A property page provides the interface for viewing and editing a control’s properties at design time. ActiveX controls use the default property page to display and modify properties that are not handled by stock property pages, such as the stock Font property page added to the Circle control in Lesson 7, Handling Text and Fonts.

This lesson modifies a default property page to display and modify the Caption property.

In this lesson, you will:

  • Add controls to the default property page.

  • Link the controls to the Caption, CircleOffset, and CircleShape properties.

Suggested Reading

  • ActiveX Controls: Using Stock Property Pages, Visual C++ Programmer’s Guide


Adding Controls to the Default Property Page

When ControlWizard was used to create the Circle control, it included a default property page as part of the project. Property pages are listed in the property page ID table in CIRCCTL.CPP:

BEGIN_PROPPAGEIDS(CCircCtrl, 3)
    PROPPAGEID(CCircPropPage::guid)
    PROPPAGEID(CLSID_CColorPropPage)
    PROPPAGEID(CLSID_CFontPropPage)
END_PROPPAGEIDS(CCircCtrl)

The first entry in the Property page ID table is the default (or general) property page. The second and third entries are the stock color and font property pages that were added in Lesson 7, Handling Text and Fonts.

Initially, the default property page dialog box template is empty. You can use the dialog editor to add controls to the default property page.

To add the Caption property to the default property page

  1. In the ResourceView pane, open the Circ project folder.

  2. Open the Dialog folder.

  3. Open the IDD_PROPPAGE_CIRC entry in the Dialog folder to edit the property page template.

  4. Right click the static text control and click Properties on the shortcut menu to open the Text Properties dialog box.

  5. Using the Text Properties dialog box, change the static text control caption to &Caption:.

  6. Select the Edit Box tool in the Controls toolbar and place an edit box control next to the static text control in the dialog box.

  7. Right click the edit box control and click Properties on the shortcut menu to open the Edit Properties dialog box.

  8. Using the Edit Properties dialog box, change the edit control’s ID to IDC_CAPTION.

To add the CircleOffset property to the default property page

  1. Select the Static Text tool in the Controls toolbar and place another static text control in the dialog box.

  2. Right click the static text control and click Properties on the shortcut menu to open the Text Properties dialog box.

  3. Using the Text Properties dialog box, change the static text control’s caption to Circle&Offset:.

  4. Select the Edit Box tool in the Controls toolbar and place an edit box control next to the static text control in the dialog box.

  5. Right click the edit box control and click Properties on the shortcut menu to open the Edit Properties dialog box.

  6. Using the Edit Properties dialog box, change the edit control’s ID to IDC_CIRCLEOFFSET.

To add the CircleShape property to the default property page

  1. Select the Check Box tool in the Controls toolbar and place a check box control in the dialog box.

  2. Right click the check box control and click Properties on the shortcut menu to open the Check Box Properties dialog box.

  3. Using the Check Box Properties dialog box, change the check box’s ID to IDC_CIRCLESHAPE and change the check box’s caption to Circle&Shape; on the Styles tab, check the Left Text check box.

Once you have completed these procedures, arrange the controls using the Layout menu options and click Save on the File menu to save the property page template.


Linking Controls with Properties

Now that the property page has controls for displaying and modifying properties, the controls need to be linked to the properties. You can link controls in the property page with properties by using a shortcut to the Add Member Variable dialog box in ClassWizard.

To link the property page controls to properties

  1. In the ResourceView pane, open the Circ project folder.

  2. Open the Dialog folder.

  3. Open the IDD_PROPPAGE_CIRC entry in the Dialog folder to load the property page template.

  4. While holding down the CTRL key, double-click the edit box control for the Caption property.

    The Add Member Variable dialog box of ClassWizard appears.

  5. After the m_ that is already in the Member variable name edit control, type caption, so the edit control contains m_caption.

  6. In the Category drop-down list box, click Value.

  7. In the Variable type drop-down list box, click CString.

  8. In the Optional property name drop-down combo box, click Caption.

  9. Click OK to close the Add Member Variable dialog box.

    If you were to open ClassWizard, the Member Variables tab would contain the new member variable mapping for the Caption property, as shown below:

    Control IDs:                 Type      Member
IDC_CAPTION                  CString   m_caption
IDC_CIRCLEOFFSET
IDC_CIRCLESHAPE
  10. Repeat steps 4 through 9, holding down the CTRL key and double-clicking the edit box control for the CircleOffset property, and entering the following values in the controls:

    Member variable name: m_circleOffset

    Category: Value

    Variable type: int

    Optional property name: CircleOffset

  11. Repeat steps 4 through 9, holding down the CTRL key and double-clicking the check box for the CircleShape property, and entering the following values in the controls:

    Member variable name: m_circleShape

    Category: Value

    Variable type: BOOL

    Optional property name: CircleShape

    If you were to open ClassWizard, the Member Variables tab would contain the new member variable mapping for the CircleShape property, as shown below:

    Control IDs:                 Type      Member
IDC_CAPTION                  CString   m_caption
IDC_CIRCLEOFFSET             int       m_circleOffset
IDC_CIRCLESHAPE              BOOL      m_circleShape

ClassWizard adds the member variables to the CCircPropPage class. ClassWizard also adds functions to the CCircPropPage class to initialize the member variables and to handle the exchange of data between the dialog controls, the member variables, and the properties.

The m_caption, m_circleOffset, and m_circleShape member variables are declared in CIRCPPG.H:

class CCircPropPage : public COlePropertyPage
{
    ...
    //{{AFX_DATA(CCircPropPage)
    ...
    CString m_caption;
    int m_circleOffset;
    BOOL m_circleShape;
    //}}AFX_DATA
    ...
};

The member variables are initialized in the constructor for the CCircPropPage class, the CCircPropPage function in CIRCPPG.CPP:

CCircPropPage::CCircPropPage() :
    COlePropertyPage(IDD, IDS_CIRCCTRL_PPG_CAPTION)
{
    //{{AFX_DATA_INIT(CCircPropPage)
    m_caption = _T("");
    m_circleOffset = 0;
    m_circleShape = FALSE;
    //}}AFX_DATA_INIT
}

Notice that strings assigned to member variables are first passed through the _T macro. This is the same macro used for string parameters to PX_ functions in CCircCtrl::DoPropExchange. The _T macro is used to maintain compatibility between different string representations, and it must be used for all literal strings in an ActiveX control project.

Data transfer is handled by the DDP_ and DDX_ macros in the DoDataExchange function in CIRCPPG.CPP:

void CCircPropPage::DoDataExchange(CDataExchange* pDX)
{
    //{{AFX_DATA_MAP(CCircPropPage)
    DDP_Text(pDX, IDC_CAPTION, m_caption, _T("Caption"));
    DDX_Text(pDX, IDC_CAPTION, m_caption);
    DDP_Text(pDX, IDC_CIRCLEOFFSET, m_circleOffset, _T("CircleOffset"));
    DDX_Text(pDX, IDC_CIRCLEOFFSET, m_circleOffset);
    DDP_Check(pDX, IDC_CIRCLESHAPE, m_circleShape, _T("CircleShape"));
    DDX_Check(pDX, IDC_CIRCLESHAPE, m_circleShape);
    //}}AFX_DATA_MAP
    DDP_PostProcessing(pDX);
}

The DDX_ macros are the same macros used for standard MFC dialog boxes. They synchronize dialog controls with dialog member variables. The DDP_macros are used only in ActiveX control property pages. They synchronize property page dialog member variables with specific control properties. Translations between an edit control and a short value and between a check box and a BOOL value are automatic. Similar to strings assigned to member variables, strings passed as parameters to DDP_ macros are first passed through the _T macro.

The Circle control now has a general property page that can be used to display and modify the values of several of its properties. The IDC_CAPTION edit control, IDC_CIRCLEOFFSET edit control, and IDC_CIRCLESHAPE check box are linked through property page member variables to the Caption, CircleOffset, and CircleShape properties, respectively. Between the default property page added in this lesson and the stock color and font property pages added earlier, all Circle control properties can now be accessed through property pages.


Rebuilding the Control with the Default Property Page

Rebuild the Circle control to reflect these latest changes. Because the CIRC.ODL file did not change, the type library will not need to be updated as part of the build.

To rebuild the control

  • On the Build menu, click Build Circ.ocx.


Testing the Default Property Page

Use Test Container to verify that the default property page has controls linked to the Caption, CircleOffset, and CircleShape properties.

To test the default property page

  1. On the Tools menu, click ActiveX Control Test Container.

  2. On the Edit menu, click Insert New Control.

    The Insert Control dialog box appears.

  3. From the list box, select Circ Control.

  4. Click OK to close the Insert Control dialog box and insert the control into Test Container.

  5. On the Edit menu, click Properties... Circ Control Object.

    The Circ Control Properties dialog box appears.

  6. Click the General tab.

    The modified property page appears.

  7. Change the Caption, CircleOffset, and CircleShape properties using the property page. Click Apply to see your changes reflected in the control.

When you have finished, close Test Container.


Simple Data Binding

Circle: ActiveX Control, Lesson 9

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

Data binding is a powerful feature of ActiveX controls. Data binding is a notification mechanism that links control properties through the container to a data source, such as a database field. In this lesson, you will add a bindable property to the Circle control to illustrate simple data binding.

Note There is a difference between a bindable property and a bound property. Bindable refers to the fact that a property is available to be bound. A bindable property becomes a bound property at run time, when the control is created and inserted into a container that responds to bound property notifications.

Suggested Reading

  • ActiveX Controls: Using Data Binding in an ActiveX Control, Visual C++ Programmer's Guide

Optimistic and Pessimistic Data Binding

There are two levels of data binding: optimistic and pessimistic. With optimistic data binding, the control assumes that changes can be made to a bound property; with pessimistic data binding, the control is required to ask the container's permission before making changes to the bound property. Whenever a bound property is changed, the control must notify the container by calling the appropriate function, depending on the level of data binding supported.

When optimistic data binding is used, the control notifies the container by calling the BoundPropertyChanged function. This form of data binding is used for the Note property in the Circle control.

When pessimistic data binding is used, the control requests permission from the container by calling the BoundPropertyRequestEdit function. If the BoundPropertyRequestEdit function returns TRUE, the control can change the bound property. However, if the BoundPropertyRequestEdit function returns FALSE, the control must not change the bound property.

Test Container's Notification Log dialog box helps you test bound properties that use either optimistic or pessimistic data binding. When the BoundPropertyChanged function is called, a notification is logged in the dialog box. You are also allowed to choose the response to each call to the BoundPropertyRequestEdit function. A different container might update a field in a database record when the BoundPropertyChanged function is called. It might also return FALSE from a call to the BoundPropertyRequestEdit function if the field or record was in use.

Note You can find a finished example of this lesson's code in the CIRC3 sample source code directory.

To clearly show the differences between bindable and nonbindable properties, a custom Get/Set property, called Note, will be implemented and changed into a bindable property in two separate steps.

In this lesson, you will:

  • Define the Note custom Get/Set property.

  • Make the Note property bindable.

  • Notify the control's container of changes.


Defining the Note Property

Initially, the Note property is a typical custom Get/Set property of type BSTR. After the Note property is added to the Circle control, the GetNote and SetNote functions are completed, the property is made persistent, and a control to edit its value is added to the property page.

To add the Note Get/Set property

  1. On the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. In the Class name drop-down list box, select CCircCtrl.

  4. Click Add Property.

    The Add Property dialog box appears.

  5. In the edit control of the External name drop-down combo box, type Note.

  6. Under Implementation, select Get/Set methods.

  7. From the Type list box, select BSTR.

  8. Click OK to close the Add Property dialog box.

    This returns you to the Automation tab. Notice that the implementation of the Note property appears as:

    Implementation:
BSTR GetNote();
void SetNote(LPCTSTR lpszNewValue);
  9. Click OK to confirm your choices and close ClassWizard.

ClassWizard adds the Note property to the Circle control. As you've seen in earlier lessons, ClassWizard modifies CIRC.ODL, CIRCCTL.CPP, and CIRCCTL.H to define the Note property.


Completing the GetNote and SetNote Functions

To complete the GetNote and SetNote functions, add the m_note member variable to the CCircCtrl class to hold the value of the Note property. The GetNote and SetNote functions will be modified to use this member variable.

Because Note is a persistent property, the SetNote function must call the SetModifiedFlag function to set the modified flag. A call to the SetModifiedFlag function is already included in the SetNote function created by ClassWizard. Because the Note property affects the visual appearance of the control, the SetNote function must also call the InvalidateControl function to redraw the control.

To complete the GetNote and SetNote functions

  1. Add the following line to CIRCCTL.H:
        CString m_note;

    as shown in the following example:

    class CCircCtrl : public COleControl
{
...
protected:
    ~CCircCtrl();
    void GetDrawRect(CRect* rc);
    short m_circleOffset;
    BOOL InBounds(short nOffset);
    BOOL InCircle(CPoint& point);
    void FlashColor(CDC* pdc);
    CString m_note;
...
};
  2. Modify the return statement in the GetNote function in CIRCCTL.CPP as shown below:
    BSTR CCircCtrl::GetNote()
{
    return m_note.AllocSysString();
}
  3. Modify the SetNote function in CIRCCTL.CPP by adding the if statement shown below:
    void CCircCtrl::SetNote(LPCTSTR lpszNewValue)
{
    if (m_note != lpszNewValue)
    {
        m_note = lpszNewValue;
        SetModifiedFlag();
        InvalidateControl();
    }
}


Making the Note Property Persistent

To manage the persistence of the Note property and to initialize it to a default value when the Circle control is created, add a call to a PX_ function in DoPropExchange in CIRCCTL.CPP.

Add the following line to CIRCCTL.CPP:

    PX_String(pPX, _T("Note"), m_note, _T(""));

as shown in the following code example:

void CCircCtrl::DoPropExchange(CPropExchange* pPX)
{
    ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
    COleControl::DoPropExchange(pPX);

    PX_Bool(pPX, _T("CircleShape"), m_circleShape, TRUE);
    PX_Short(pPX, _T("CircleOffset"), m_circleOffset, 0);
    PX_Long(pPX, _T("FlashColor"), (long &)m_flashColor, RGB(0xFF, 0x00, 0x00));
    PX_String(pPX, _T("Note"), m_note, _T(""));
}

Since the Note property is of type BSTR, the PX_String function is used. The m_note member variable (which holds the Note property value) and a zero-length string (which is the default value for the Note property) are parameters to this function. As with all literal strings, the property name string and default value string are passed through the _T macro before being passed as parameters to the PX_String function.


Displaying the Note Property

To display the Note property, change the drawing behavior in the OnDraw function in CIRCCTL.CPP. The Note property is drawn in the upper-left corner of the control's bounding rectangle, using the stock Font and ForeColor properties.

To change the OnDraw function to display the Note property

  • Change or add the following lines in the OnDraw function in CIRCCTL.CPP:
       // Draw the caption and note using the stock Font and ForeColor properties
...
   pdc->SetTextAlign(TA_LEFT | TA_TOP);
   pdc->ExtTextOut(rc.left, rc.top,
       ETO_CLIPPED, rc, m_note, m_note.GetLength(), NULL);

    as shown in the following example:

    void CCircCtrl::OnDraw(
            CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
    CBrush* pOldBrush;
    CBrush bkBrush(TranslateColor(GetBackColor()));
    CPen* pOldPen;
    CRect rc = rcBounds;
    CFont* pOldFont;
    TEXTMETRIC tm;
    const CString& strCaption = InternalGetText();

    // Set the ForeColor property color and transparent background mode into the device context
    pdc->SetTextColor(TranslateColor(GetForeColor()));
    pdc->SetBkMode(TRANSPARENT);

    // Paint the background using the BackColor property
    pdc->FillRect(rcBounds, &bkBrush);

    // Draw the ellipse using the BackColor property and a black pen
    GetDrawRect(&rc);
    pOldBrush = pdc->SelectObject(&bkBrush);
    pOldPen = (CPen*)pdc->SelectStockObject(BLACK_PEN);
    pdc->Ellipse(rc);

   // Draw the caption and note using the stock Font and ForeColor properties
    pOldFont = SelectStockFont(pdc);
    pdc->GetTextMetrics(&tm);
    pdc->SetTextAlign(TA_CENTER | TA_TOP);
    pdc->ExtTextOut((rc.left + rc.right) / 2, (rc.top + rc.bottom - tm.tmHeight) / 2,
        ETO_CLIPPED, rc, strCaption, strCaption.GetLength(), NULL);
   pdc->SetTextAlign(TA_LEFT | TA_TOP);
   pdc->ExtTextOut(rc.left, rc.top,
       ETO_CLIPPED, rc, m_note, m_note.GetLength(), NULL);
    pdc->SelectObject(pOldFont);

    pdc->SelectObject(pOldPen);
    pdc->SelectObject(pOldBrush);
}


Adding the Note Property to the Default Property Page

Add the Note property to the default property page to allow Circle control users to change its value.

First, use the resource editor to add an edit control to the default property page.

To add an edit control to the default property page

  1. In the ResourceView pane, open the Circ project folder.

  2. Open the Dialog folder.

  3. Open IDD_PROPPAGE_CIRC to edit the property page template.

  4. Select the Static Text tool in the Controls toolbar and place a static text control in the dialog box.

  5. Right click the static text control and click Properties on the shortcut menu to open the Text Properties dialog box.

  6. Using the Text Properties dialog box, change the static text control's caption to &Note:.

  7. In the Controls toolbar, select the Edit Box tool and place an edit box control next to the static text control in the dialog box.

  8. Right click the edit box control and click Properties on the shortcut menu to open the Edit Properties dialog box.

  9. Using the Edit Properties dialog box, change the edit control's ID to IDC_NOTE.

  10. Save the changes to the dialog box.

Link the Note property to the new edit control in the default property page using a shortcut to the Add Member Variable dialog box in ClassWizard.

To link the edit control with the Note property

  1. In the ResourceView pane, open the Circ project folder.

  2. Open the Dialog folder.

  3. Open the IDD_PROPPAGE_CIRC entry in the Dialog folder to load the property page template.

  4. While holding down the CTRL key, double-click the edit box control for the Note property.

    This automatically opens the ClassWizard Add Member Variable dialog box.

  5. Type note in the Member variable name edit control, after the m_ that is already there, so that the edit control contains m_note.

  6. In the Category drop-down list box, select Value.

  7. In the Variable type drop-down list box, select CString.

  8. Type Note in the Optional property name drop-down combo box.

  9. Click OK to confirm your choices and close the Add Member Variable dialog box.

    If you were to open ClassWizard, the Member Variables tab would contain the new member variable mapping for the Caption property.

    Control IDs:       Type      Member
IDC_CAPTION        CString   m_caption
IDC_CIRCLEOFFSET   int       m_circleOffset
IDC_CIRCLESHAPE    BOOL      m_circleShape
IDC_NOTE           CString   m_note

ClassWizard adds the new m_note member variable to the CCircPropPage class. ClassWizard also modifies the DoDataExchange function in the CCircPropPage class.

The Note property is now fully implemented as a normal Get/Set property in the Circle control.


Making the Note Property Bindable

At this point, the Note property is a fully implemented, normal Get/Set property. The next step is to use the Automation tab in ClassWizard to make the Note property a bindable property.

To make the Note property bindable

  1. On the View menu, click ClassWizard.

  2. Click the Automation tab.

  3. In the Class name drop down list box, select CCircCtrl.

  4. In the External names list box, select Note.

  5. Click Data Binding.

    The Data Binding dialog box appears.

  6. Select the Bindable Property check box.

    This specifies that this is a bindable property and that the container will be notified of all changes to this property.

  7. Click OK to confirm the data binding settings and close the Data Binding dialog box.

  8. Click OK to confirm your choices and close ClassWizard.

ClassWizard changes the Note property definition in the file CIRC.ODL so that the type library includes the information that the Note property is bindable.


Notifying the Container of Changes

A control must notify the container when changes are made to a bound property by calling the BoundPropertyChanged function.

Because Note is a Get/Set property, all changes to the property are confined to the SetNote function, which is where BoundPropertyChanged is called.

To modify the SetNote function to notify the container about changes to the Note property

  • Add the following line to the SetNote function in CIRCCTL.CPP:
           BoundPropertyChanged(dispidNote);

    as shown in the following example:

    void CCircCtrl::SetNote(LPCTSTR lpszNewValue)
{
    if (m_note != lpszNewValue)
    {
        m_note = lpszNewValue;
        SetModifiedFlag();
        InvalidateControl();
        BoundPropertyChanged(dispidNote);
    }
}

The call to BoundPropertyChanged is added after the m_note member variable is updated, and all other actions involved in changing the Note property are completed.

The single parameter to BoundPropertyChanged, dispidNote, is the dispatch ID for the Note property. This parameter is defined in an enumeration in CIRCCTL.H:

// Dispatch and event IDs
public:
    enum {
    //{{AFX_DISP_ID(CCircCtrl)
    ...
    dispidNote = 4L,
    ...
    //}}AFX_DISP_ID
    };


Rebuilding the Control with Data Binding Support

Now that you have added a property with data binding support, you must rebuild the control.

To rebuild the control

  • On the Build menu, click Build Circ.ocx.


Testing the Control Data Binding Changes

Because the Note property uses optimistic data binding, the Circle control assumes that all changes to the Note property are allowed. The SetNote function simply changes the Note property and notifies the container by calling the BoundPropertyChanged function. This notification displays in Test Container's Notification Log dialog box.

To test the control data binding changes

  1. On the Tools menu, click ActiveX Control Test Container.

  2. Use the Logging option in the Options menu to ensure that output information is going to either a window or file.

  3. On the Edit menu, click Insert New Control.

    The Insert Control dialog box appears.

  4. From the list box, select Circ Control.

  5. Click OK to close the Insert Control dialog box and insert the control into Test Container.

  6. On the Edit menu, select the Properties option.

    The Circ Control Properties dialog box appears.

  7. Enter a new value for the Note property and click Apply.

    The Note property is displayed in the control.

    Information about the change to the Note property appears as shown below:

    Circ3 Control: Property Change: Note

    This kind of information appears every time the BoundPropertyChanged function is called for a bound property. The line shows the type and the property name, and states that the property has changed.

  8. Change the Note property several more times and observe the logged information. Close Test Container when you are finished.

Using the BoundPropertyChanged function to notify the container of changes to bound properties, illustrates the simplest level of data binding.


Versions and Serialization

Circle: ActiveX Control, Lesson 10

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

Because different versions of an ActiveX control might have different properties, support is included in the ActiveX control classes for handling different versions of persistent data. In this lesson, the Circle control is modified to reject persistent data that is of a different version than the control itself. Selective loading and/or storing of persistent data for different versions of controls are also discussed.

Note You can find a finished example of this lesson's code in the CIRC3 sample source code directory.

In this lesson, you will:

  • Learn about serialization of control version information.

  • Modify the Circle control so that it ignores other versions of persistent data.

  • Learn how a control can serialize earlier versions of persistent data.


Serialization of Control Version Information

Serialization is the process by which persistent data is loaded or stored through an exchange object. One type of exchange object is used to initialize a control's persistent properties to default values; another type is used to load and store persistent data in persistent storage. A control's persistent data is serialized in the DoPropExchange function.

The Circle control's version, stock properties, and persistent custom properties, in that order, are serialized in the DoPropExchange function in CIRCCTL.CPP.

void CCircCtrl::DoPropExchange(CPropExchange* pPX)
{
    ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
    COleControl::DoPropExchange(pPX);

    PX_Bool(pPX, _T("CircleShape"), m_circleShape, TRUE);
    PX_Short(pPX, _T("CircleOffset"), m_circleOffset, 0);
    PX_Long(pPX, _T("FlashColor"), (long &)m_flashColor, RGB(0xFF, 0x00, 0x00));
    PX_String(pPX, _T("Note"), m_note, _T(""));
}

When the Circle control project was created, ControlWizard produced the code that calls the ExchangeVersion and COleControl::DoPropExchange functions. ControlWizard also defined the global constants _wVerMajor and _wVerMinor in CIRC.CPP as 1 and 0 respectively, which represents version 1.0.

The ExchangeVersion function serializes a control's version and sets the version used by the exchange object, pPX. This call should always be made before any version-sensitive persistent data is serialized. When persistent data is being initialized or written into persistent storage through the exchange object, the exchange object's version is set to the version parameter passed to the function. When persistent data is being read from persistent storage through the exchange object, the exchange object's version is read from persistent storage.

The COleControl::DoPropExchange function serializes all of a control's stock properties. The PX_ functions serialize each of the Circle control's persistent custom properties. These calls were added in previous lessons of the tutorial.


Serializing Different Versions of Persistent Data

The control writer knows which properties earlier versions of the control have and in what order they are serialized. Rather than ignoring the exchange object, the persistent custom properties of the old version of the control can be loaded or stored through the exchange object in a selective manner.

When an earlier version of persistent data is to be loaded, any new properties can be initialized to their default values.

Note It is not recommended that new versions of a control remove features (such as properties) of earlier versions.

When an earlier version of persistent data is to be stored, any new properties can be skipped.

Consider a hypothetical version 2.0 of the Circle control that has a new custom property called BorderWidth. The BorderWidth property is of type short and has a default value of 1.

It is assumed that all versions of the Circle control will have all the properties of earlier versions, and that the properties are stored in the same order as in earlier versions. This is a recommendation for all ActiveX controls.

Here is what the version 2.0 DoPropExchange function might look like:

void CCircCtrl::DoPropExchange(CPropExchange* pPX)
{
    ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor), FALSE);
    COleControl::DoPropExchange(pPX);
    PX_Bool(pPX, _T("CircleShape"), m_circleShape, TRUE);
    PX_Short(pPX, _T("CircleOffset"), m_circleOffset, 0);
    PX_Long(pPX, _T("FlashColor"), (long &)m_flashColor, RGB(0xFF,
        0x00, 0x00));
    PX_String(pPX, _T("Note"), m_note, _T(""));

    if (pPx->GetVersion() >= (DWORD)MAKELONG(_wVerMinor, _wVerMajor))
        PX_Short(pPX, _T("BorderWidth"), m_borderWidth, 1);
    else
        if (pPx->IsLoading())
          m_borderWidth = 1;
}

Notice the third parameter to the ExchangeVersion function. This is an optional parameter that specifies whether or not the control should store the same version of persistent data as was last loaded. By passing FALSE, the old version is maintained. The default for this parameter is TRUE, which specifies that the control should always store the current version of persistent data, no matter what version of persistent data is loaded.

All stock properties and custom properties from versions before 2.0 are serialized as usual, in the same order as they were in the earlier versions.

If the exchange object's version is greater than or equal to 2.0, the BorderWidth property is serialized as usual.

If the exchange object's version is less than 2.0, and the IsLoading function returns TRUE, the BorderWidth property is initialized to its default value.


Ignoring Different Versions of Persistent Data

When a control is requested to load persistent data through an exchange object with a version different from the control, it must do something reasonable to set the values of its persistent custom properties. The simplest approach is to ignore the exchange object and initialize each persistent custom property to its default value by modifying the Circle control's DoPropExchange function. The changes involve using two member functions of the exchange object, GetVersion and IsLoading.

The GetVersion member function returns the exchange object's version. The exchange object's IsLoading member function returns TRUE if the control is to load values through the exchange object. It returns FALSE if the control is to store values through the exchange object.

The GetVersion function is used to compare the exchange object's version and the control's version. If the versions are the same, each persistent custom property is serialized as usual. If the versions are different and the IsLoading function returns TRUE, the Circle control's persistent properties are initialized to their default values. If the versions are different and the IsLoading function returns FALSE, the Circle control ignores the exchange object. Technically, this situation never occurs, because the Circle control always stores the current version of persistent data.

Modify the DoPropExchange function to look like this:

void CCircCtrl::DoPropExchange(CPropExchange* pPX)
{
    ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));
    COleControl::DoPropExchange(pPX);
    if (pPX->GetVersion() == (DWORD)MAKELONG(_wVerMinor, _wVerMajor))
    {
        PX_Bool(pPX, _T("CircleShape"), m_circleShape, TRUE);
        PX_Short(pPX, _T("CircleOffset"), m_circleOffset, 0);
        PX_Long(pPX, _T("FlashColor"), (long &)m_flashColor, RGB(0xFF,
        0x00, 0x00));
        PX_String(pPX, _T("Note"), m_note, _T(""));
    }
    else
        if (pPX->IsLoading())
        {
            m_circleShape = TRUE;
            m_circleOffset = 0;
            m_flashColor = RGB(0xFF, 0x00, 0x00);
            m_note = _T("");
        }
}

As described earlier, the ExchangeVersion function sets the exchange object's version. This is the version of persistent data that the control is to load or store through the exchange object. This version is returned by the GetVersion function.

The control version and stock properties are serialized as usual. The COleControl::DoPropExchange function correctly loads or stores stock property values for different versions because part of what is serialized is information about which stock properties were stored for that version.

If the exchange object's version and the control's version are the same, the Circle control's persistent custom properties are loaded or stored through the exchange object as usual.

If the versions are different, and persistent data is to be loaded, all persistent properties are initialized to their default values instead.

If the versions are different, and persistent data is to be stored, the exchange object is ignored and no persistent data is stored through the exchange object. This case should never happen with the Circle control because the exchange object's version and the control's version may be different only when data is to be loaded through the exchange object. Using FALSE as the third parameter to the ExchangeVersion function allows a control to specify that persistent data should be stored as the same version that was last loaded.


Rebuilding the Control with Version Support Implemented

Now that you have added version support, you must rebuild the control.

To rebuild the control

  • On the Build menu, click Build Circ.ocx.


Testing the Control with Version Support

Testing different versions of the same control is not easy, simply because only one of the versions can be registered at a time. This is one way in which you might check to see if a newer version of the Circle control correctly ignores persistent data stored by an earlier version of the control.

To insert the Circ control in Test Container

  1. From the Tools menu, click ActiveX Control Test Container.

  2. From the Edit menu, click Insert New Control.

    The Insert Control dialog box appears.

  3. From the list box, select Circ Control.

  4. Click OK to close the Insert Control dialog box and insert the control into Test Container.

To test the version checking serialization code

  1. From the Edit menu, click Properties... Circ Control Object.

    The Circ Control Properties dialog box appears.

  2. Change the value of the CircleShape property to FALSE (clear the check box and then click OK).

  3. From the Control menu, click Save to Stream or Save to Storage. Name the file as appropriate.

  4. From the Edit menu, click Delete All.

  5. In CIRC.CPP, change the version number to 2.0 by changing the value of the _wVerMajor global constant to 2.

  6. On the Build menu, click Build Circ.ocx to rebuild the control.

  7. From Test Container's Edit menu, click Insert Control from Stream or Insert Control from Storage, as appropriate. Insert your control.

  8. Select the control. From the Edit menu, click Properties... Circ Control Object.

    The Circ Control Properties dialog box appears.

  9. View the value of the CircleShape property in the property page.

    The persistent data saved in step 3 is version 1.0. When the control is recreated in step 6, the control's version is 2.0. Because the persistent data and the control have different versions, the control's persistent properties are initialized to their default values. The stored values are ignored. In particular, the CircleShape property should be TRUE (the check box will be checked).

  10. Change the value of the CircleShape property to FALSE again (clear the check box, then click OK).

  11. From the Control menu, click Save to Stream or Save to Storage. Name the file as appropriate.

  12. From the Edit menu, click Delete All.

  13. From Test Container's Edit menu, click Insert Control from Stream or Insert Control from Storage, as appropriate. Insert your control.

  14. Select the control. From the Edit menu, click Properties... Circ Control Object.

    The Circ Control Properties dialog box appears.

  15. View the value of the CircleShape property using the property page.

    The persistent data saved in step 11 is version 2.0. When the control is recreated in step 13, the control's version is 2.0. Because the persistent data and the control have the same version, the control's persistent properties are set to the stored values. In particular, the CircleShape property should be FALSE (the check box is cleared).

    When you have finished, close Test Container.

If you want your Circle control to match the control found in the sample directories, be sure to restore the control version to 1.0 (change the value of _wVerMajor in CIRC.CPP to 1 and rebuild the control).

This concludes the Circle tutorial.


Creating a Database Application

Enroll: Database Application, Lesson 1

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

This tutorial shows you how to develop a form-based Microsoft Foundation Class Library (MFC) database application. You’ll learn how to:

  • Use AppWizard to create the starter database application.

  • Create and use CRecordset objects to open tables and run queries.

  • Create and use CRecordView objects for form-based applications.

  • Use database support within the framework’s document/view architecture.

  • Add, update, and delete records.

  • Manage multiple tables.

  • Handle database exceptions.

Important This tutorial assumes you are familiar with Visual C++ and the Microsoft Foundation Class Library. If you aren’t, try the Scribble: MDI Drawing Application tutorial before you begin this tutorial. The Scribble tutorial introduces important class library concepts and techniques and teaches you to use the views, the wizards, and the resource editors.


The Tutorial Example: Enroll

The tutorial example program, Enroll, manages a student registration database similar to, but simpler than, a college registration system. It will help you to follow the tutorial if you understand the structure of the student registration database.

Enroll is based on a database, StdReg32.mdb, that you will register with ODBC as the “Student Registration” data source name. The following table lists the tables, what they store, and the columns in them.

Tables in the Student Registration Database

Table name | Contents | Column list
Course | Think of each record as an entry in a course catalog. Example: the MATH101 course. | CourseID* CourseTitle Hours
Section† | A section record is a specific offering of a course at a specific time. For example, MATH101 may have many sections. | SectionNo* CourseID* InstructorID Schedule RoomNo
Student | A record for each student at the school. | StudentID* Name GradYear
Enrollment | A record for each student in a particular section of a course. For a given student, there is an enrollment record for each course the student is taking. | CourseID* SectionNo* StudentID* Grade
Instructor | A record for each instructor at the school. | InstructorID* Name RoomNo

* Indicates the column (or columns) that comprise the table’s primary key.

† The Dynabind_Section table is used in the Dynabind sample, but not in the Enroll tutorial.

To copy StdReg32.mdb to your local drive

  1. Jump to the STDREG sample in Visual C++ Samples.

  2. Click the link "Click to open or copy the STDREG project files."

    The Sample Application dialog box appears

  3. From the Files box, click StdReg32.mdb.

  4. Click the Copy button.

    The Copy dialog box appears

  5. Navigate to the directory where you want to copy the database and click OK.

    The database is copied to your local drive. You can examine it, add records, and so on, using Microsoft Access.

Note You can also install the sample source project files for Enroll. For more information, see Installing the Sample Files in Scribble, Lesson 1.

In Enroll, you use a “form” (a view with dialog-style controls) to view registration information for courses, section by section. Section information displayed includes the course name, section number, instructor, room, and schedule (such as “MWF 10-11”). For example, you can view section 1 of the course MATH 101, then section 2, and so on. The initial tutorial step provides updatable fields and viewing of all sections. Later steps add more capabilities, including the ability to view more than one recordset with the same record view, and for adding and deleting records. The following figure shows what the Enroll application looks like at the end of the tutorial.

The Enroll Tutorial Application


Setting Up the Student Registration Data Source

Before you start the Enroll tutorial, you need to set up the Student Registration database and register it as an ODBC data source. Choose a database format for which you have the corresponding database management system (DBMS) and 32-bit ODBC driver. Microsoft ships with Visual C++ the 32-bit ODBC drivers for most standard database formats, including: SQL Server, Access, Paradox, dBase, FoxPro, Excel, and Oracle.

  • If you want to use MFC database support for SQL Server, you need the SQL Server product in addition to the ODBC driver for SQL Server that is provided with Visual C++.

  • If you want to use other database formats, you need the DBMS as well as the ODBC driver. See Installing Additional 32-bit ODBC Drivers for Your DBMS, later in this lesson, for more information.

  • If you want to use the Microsoft Access database format, you need only the 32-bit Microsoft Access ODBC driver to create a database schema. This driver is installed automatically when you run a Typical setup. This is an exception to the requirements listed above. You may find it helpful, however, to use Microsoft Access itself in conjunction with MFC database support, as it will facilitate working interactively with your database schema and data.

Suggested Reading

  • STDREG sample , Visual C++ Samples

  • ODBC Administrator, Visual C++ Programmer’s Guide

  • Data Source: Managing Connections (ODBC), Visual C++ Programmer’s Guide

To set up the student registration database, you must:

  • Specify a database.

  • Register the database with ODBC.

If you are using a DBMS other than the prebuilt Microsoft Access StdReg32.mdb database file, you need to add tables to your database so that it matches the Student Registration database schema. Additionally, you may need to install drivers other than those that Visual C++ Setup installs for you. In this case, perform the following procedure(s):

  • Use the StdReg.exe tool to add tables to the Student Registration database.

  • Install additional 32-bit ODBC drivers for your DBMS.

    This step is not necessary if your DBMS uses the dBase, FoxPro, Access or SQL Server drivers, as Setup installs them automatically when you choose the Typical installation option. Perform this step to install additional ODBC drivers, including those supplied by Visual C++: Paradox, Excel, or Oracle.

Each of these procedures is described in the following topics.


Specify a Database

The easiest way to supply a database for the Enroll tutorial is to use the StdReg32.mdb Microsoft Access database file, included in the MFC Samples directory for this purpose. Alternatively, you can create your own database, as described below.

StdReg32.mdb already contains the tables and records used in the tutorial. If you use this file, you do not need to use the StdReg.exe tool to add any tables.

To create your own database

  • Create a new database schema using the database administration capability of your DBMS.

    Depending on the type of DBMS, you might create the new database on a server that is different from the computer where you will be doing MFC database development. In either case, you need to use the StdReg.exe tool to add tables to the new database.


Register the New Database with ODBC

You will register the new database with the ODBC data source name “Student Registration.” This data source name (DSN) is referred to by the Enroll application. You must register the database even if you are using the pre-built StdReg32.mdb Microsoft Access database file.

You can register the data source by using ODBC Administrator from the Control Panel, or by running StdReg.exe.

Note The STDREG tool is provided for use with this tutorial as a convenient method for registering data sources with ODBC and for populating databases with the appropriate tables and data. Normally, you will use ODBC Administrator from the Control Panel to register your data sources with the appropriate tables and data if you are not going to use the pre-built StdReg32.mdb database.

The following two procedures describe how to register the data source if you are using the Microsoft Access ODBC driver.

  • If you are using Microsoft Access and the StdReg.mdb, make sure you have copied the database to your hard drive before you perform the following procedures.

  • If you are using another driver, the basic procedure will vary somewhat. For more information, refer to the ODBC SDK topic, Adding Data Sources.

To register the data source by using ODBC

  1. Open Control Panel, and double-click the 32bit ODBC icon.

  2. Click the User DSN tab, if it is not already selected.

  3. Click the Add button.

    The Create New Data Source dialog box appears.

  4. From the list, choose the driver you want to use with your database, in this case, Microsoft Access Driver (*.mdb), and then click Finish.

    The ODBC Setup dialog box specific to the driver you indicated appears.

  5. In the Data Source Name box, type Student Registration.

  6. Optionally, in the Description box, enter a description for the database.

  7. In the Database group box, click Select, and then navigate to the location of StdReg32.mdb.

  8. Click OK to select the database.

  9. Click OK to exit the ODBC Setup dialog box, and click Close to exit the ODBC Data Source Administrator dialog box.

To register the data source by using StdReg.exe

  1. First run StdReg.exe, by using the following procedure:
    • Jump to the STDREG sample in Visual C++ Samples.

    • Click the link "Click to open or copy the STDREG project files."

    • The Sample Application dialog box appears

    • From the Files box, click StdReg.exe.

    • Click the Copy button.

    • The Copy dialog box appears.

    • Navigate to the directory where you want to copy the file and click OK.
  2. Navigate to the location of StdReg.exe, and double-click to run the program.

  3. Click Add Data Source.

  4. Follow the instructions in steps 3 through 9 of the previous procedure.


Use StdReg.exe to Add Tables

If you are using a DBMS other than the prebuilt Microsoft Access StdReg32.mdb database file, use the STDREG tool to add tables to the Student Registration database. This tool creates the Student Registration tables listed in the table Tables in the Student Registration Database, earlier in this lesson. The tool also adds records to the newly created tables for use as test data by the Enroll application.

Note This procedure assumes you have used StdReg.exe to register the Student Registration data source.

To add tables by using StdReg.exe

  1. If necessary, start StdReg.exe, and then click the Initialize Data option.

    Depending on the type of database you are using, you may need to respond to a login dialog box, such as the SQL Server Login dialog box.

  2. After logging in to the Student Registration data source, respond to a series of three Enter SQL Column Syntax dialog boxes.

    After you respond to three successive Enter SQL Column Syntax dialog boxes, STDREG creates the tables in the new database.

  3. When STDREG has completed this task, click Exit.

    You can rerun the STDREG tool at any time to remove and re-create the tables in the Student Registration data source.

For more information about using StdReg.exe, see the STDREG sample topic.


Installing Additional 32-bit ODBC Drivers for Your DBMS

You need only install the ODBC driver for your DBMS once, and you can use it with more than one data source. If you choose the Typical setup option, Setup provides MFC Database Support and installs the ODBC drivers for Access, dBase, FoxPro, and SQL Server automatically. You can install the additional drivers provided with Visual C++ (Text, Paradox, Excel, and Oracle) by running Setup again, as described in the following procedure.

If you want to install other drivers not shipped with Visual C++, refer to the documentation that came with your driver. You can also review the ODBC SDK documentation, in particular, the Installing Drivers topic.

To install additional drivers provided with Visual C++

  1. Run Setup, and under Installation Options, select Custom.

  2. Click Next, and in the Microsoft Visual C++ Setup dialog box, clear each option except Database Options.

  3. Highlight Database Options and then click the Details button.

  4. In the Database Options dialog box, highlight Microsoft ODBC Drivers, and again click the Details button.

    If you do not want to install DAO database support, clear the Microsoft Data Access Objects check box.

  5. In the Microsoft ODBC Drivers dialog box, check the boxes next to any additional drivers you want to install.

  6. Click OK, and then click the Next button to start the installation.


Tutorial Steps

The tutorial consists of three steps. The following table describes the steps briefly.

Tutorial step | Description
1 | Use AppWizard to create an application with database support. The document embeds a CRecordset object for the Section table of the Student Registration data source. Use the dialog editor to design the form. Use ClassWizard to bind controls on the form to fields in the recordset.
2 | Provide a combo box control on the form so the user can select a course and view its sections. Fill the combo box from a recordset object representing the Course table. Filter and parameterize the recordset to constrain the records it selects.
3 | Implement a user interface for adding, updating, and deleting records. Handle database exceptions.

The ENROLL sample program directory contains a subdirectory for each step of the tutorial, named STEP1, STEP2, and STEP3. Each step’s subdirectory contains complete source files, and other files needed for the step.

If you do not have local copies of the Sample files, you can easily install them. For more information, see Installing the Sample Files in Scribble, Lesson 1. While you don’t need to have a copy of these files locally in order to develop the project yourself, you might find it useful, for example to compare source code.

Enroll Step 4

The sample source code for ENROLL includes a fourth step, not covered in the tutorial. Step 4 illustrates additional class library database programming techniques, summarized below. See ENROLL in Visual C++ Samples \ MFC Samples \Tutorial Samples for a discussion of Enroll Step 4. The main techniques illustrated by Enroll Step 4 are:

  • Using multiple record view classes.

  • Switching views in a frame window.

  • Using the document object to coordinate multiple forms via UpdateAllViews and update hints.


A Simple Form

Enroll: Database Application, Lesson 2

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

This tutorial lesson implements an updatable database form that lets the user examine the records in the Section table one record at a time. You’ll create a form that looks like the one shown in the figure below.

Enroll’s Section Form

This lesson explains:

  • Creating the Enroll application.

  • Examining the Enroll Step 1 classes.

  • Viewing the Database Menu Resource

  • Customizing Enroll’s database form.

  • Binding Enroll’s form controls to recordset fields.

  • Building and running Enroll Step 1.

At the end of this lesson, you’ll be able to build and run the Enroll Step 1 application.


About Step 1

Step 1 teaches the basics of:

  • Using AppWizard to create an application with database support.

  • Using ClassWizard, ClassView, and the resource editors to bind controls on a form to data.

  • Using recordsets.

  • Using record views.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Recordset (ODBC)

  • Record Views

  • ClassWizard: Database Support

  • Overview: Creating a Program That Supports a Database

  • Data Source (ODBC)

  • ODBC

In the next topic, Understanding Recordsets and Record Views, you will learn the difference between recordsets and record views, and their respective roles in representing and manipulating the view to the data in a database. You will also get an introduction to the MFC classes that access database data and views.


Understanding Recordsets and Record Views

A recordset object represents a set of records selected from a data source. A recordset may represent a selection of one or more specified columns from rows of one or more database tables.

A CRecordset object represents both the recordset's selection of records and the actual field values for one currently selected record.

A record view is a specialized view class that uses controls laid out in a dialog template resource to view and/or edit the fields of a recordset in a dialog-like form. A CRecordView object is associated with both a recordset object and a dialog template resource. The dialog template resource has an ID of the form IDD_XXX_FORM, where XXX is based on the project name.

CRecordView derives its form behavior from class CFormView. CRecordView supports end-user navigation through records, one at a time, using Move First, Move Next, Move Previous, and Move Last commands of the associated CRecordset object. When you update the value in a control on the form and navigate to another record, the corresponding recordset field is automatically updated.

In the next topic, you will create a new database application using AppWizard. While in AppWizard, you identify an Open Database Connectivity (ODBC) data source and a table in the data source. AppWizard creates a pair of classes: a recordset class and a record view class.


Creating a New Database Application

AppWizard lets you specify whether your database application uses a file as well as a database. The Enroll application doesn’t need a file, so it is based on the Database view without file support option in AppWizard.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Overview: Creating a Program That Supports a Database

  • Serialization: Serialization vs. Database Input/Output

Note In order to successfully complete the following procedure, you must have completed the steps required to register the Student Registration database with ODBC. If you have not done so, see Setting Up the Student Registration Data Source, in Lesson 1 of Enroll.

To create the tutorial database application

  1. From the File menu, choose New.

    The New dialog box appears.

  2. Click the Projects tab.

  3. Complete the Projects dialog box as follows:
    • In the Type box, select MFC AppWizard (exe).

    • In the Project Name box, type Enroll.

    • In the Location box, type the name of the directory to keep your source file project directory.

      AppWizard creates a project directory with the project's name (Enroll) under the directory specified in the Location box.

  4. Click OK.

    AppWizard creates the project directory, and the MFC AppWizard – Step 1 dialog box appears.

  5. Click the Single document radio button, and then click Next.

  6. In the MFC AppWizard – Step 2 of 6 dialog box:
    • Click Database view without file support.

      When you create a database application without file support, AppWizard always creates it as an SDI application.

      This enables the Data Source button.

    • Click Data Source.

      The Database Options dialog box appears.

    • Click ODBC and, from the drop-down list box, select Student Registration. Click OK.

      The Select Database Tables dialog box appears.

    • Select the table name Section, and click OK.

      This returns you to the MFC AppWizard – Step 2 of 6 dialog box.

      Depending on the data source type you are using, additional qualifiers may precede or follow the table name.

  7. Click Next to accept the options you have specified, then click Next in the AppWizard dialog boxes for Steps 3, 4, and 5 to accept the default options.

    In the MFC AppWizard – Step 6 of 6 dialog box, you can check and, if necessary, modify the default names that AppWizard creates for your program’s classes and files.

    Note By default, AppWizard bases the names of classes on the project name you supply. This naming is probably fine if your application has only one recordset/view pair. If your application has multiple recordsets and record views, it’s a good idea to change the name of the first recordset/view pair created by AppWizard so the naming better reflects the name of the table in the data source. For Enroll, you’ll modify two class names and their related header and implementation filenames, even though the tutorial uses only one recordset/view pair.

  8. In the MFC AppWizard – Step 6 of 6 dialog box, make the following changes to class names:
    • Select the class CEnrollSet, and change it as follows:

      In the Class name box, change its name to CSectionSet.

      In the Header file box, change the header filename to SectionSet.h.

      In the Implementation file box, change the name to SectionSet.cpp.

      The base class is CRecordset. The edit item is disabled to show that you can’t change it.

    • Select the class CEnrollView, and change it as follows:

      In the Class name box, change its name to CSectionForm.

      In the Header file box, change the header filename to SectionForm.h.

      In the Implementation file box, change the implementation filename to SectionForm.cpp.

      The base class is CRecordView.

  9. Click Finish.

    The New Project Information dialog box appears, summarizing the settings and features AppWizard will generate for you when it creates your project.

    Take a moment to examine the application type, classes, and features that AppWizard automatically provides.

  10. Click OK in the New Project Information dialog box.

    AppWizard creates all necessary files and opens the project. The next topics describe the AppWizard-createf files in more detail.


Examining the Step 1 Classes

Once AppWizard creates the Enroll project, you can use ClassView to see a graphical representation of the classes and any default member functions AppWizard created. You can also use ClassWizard to view member variable bindings that AppWizard specifies for you.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Using ClassView

  • Using ClassWizard

  • ClassWizard: Binding Recordset Fields to Table Columns

The following topics outline how to view the AppWizard-created classes and provide a description of each class:

  • The CSectionSet Recordset Class

  • The CSectionForm Record View Class

  • The CEnrollDoc Document Class


The CSectionSet Recordset Class

The following procedure describes how to view the new recordset class, CSectionSet, in the text editor. After examining CSectionSet, you’ll use the text editor to examine the source files for classes CSectionForm and CEnrollDoc.

To examine the new recordset class

  1. In ClassView, expand Enroll classes by clicking the plus sign next to the icon.

    Examine the set of classes AppWizard created for you automatically to support the Enroll application.

  2. From the list of classes, expand CSectionSet.

    ClassView displays all of the member variables that AppWizard created for you, including a variable for each of the Section table’s columns. You can use ClassWizard to view how AppWizard has bound the Section table’s columns to these member variables.

  3. From the View menu, click ClassWizard.

  4. Click the Member Variables tab.

  5. In the Class Name box, select CSectionSet.

    What you see in this tab corresponds to the variables displayed in ClassView. AppWizard has bound all of the table’s columns to member variables of the CSectionSet class. These member variables are called “field data members.” AppWizard names the data members automatically, based on the column names from the data source. AppWizard also assigns the correct C++ or class library data type to the data members, based on the column type. In this example, all of the columns are text columns, mapped to type CString, except the Capacity column, which is an int.

  6. Once you’ve finished examining the CSectionSet recordset class, click OK to exit ClassWizard.

The figure below shows what you see in ClassWizard’s Column Names box.

Table Columns Mapped to Recordset Data Members

Tip For this tutorial, you will need all of the column bindings. However, in your own application, if you don’t want all of a table’s columns bound to your recordset, you can delete the recordset field data members for those columns you don’t want; from within ClassWizard, select the data member and click the Delete Variable button.

Caution Don’t delete any fields that are part of the table’s primary key (in this case, the SectionNo and CourseID fields).

In your own application, if you want to change the name of a field data member, use ClassWizard to delete the member and add it again with the new name.

In the next procedure, you’ll examine the CSectionForm record view class from inside the text editor.


The CSectionForm Record View Class

To examine the source code for class CSectionForm

  • Use ClassView to jump to the OnInitialUpdate member function of class CSectionForm.

    SectionForm.cpp opens inside the text editor, with your cursor just before the OnInitialUpdate member function:

    void CSectionForm::OnInitialUpdate()
{
m_pSet = &GetDocument()->m_sectionSet;
CRecordView::OnInitialUpdate();
}

    The base class function CRecordView::OnInitialUpdate opens the database if not already open, opens the recordset.

Suggested Reading in the Microsoft Foundation Class Reference

  • CView::OnInitialUpdate

For now, the “form” represented by class CSectionForm is empty of controls. Later, in Customizing the Dialog Template for the Section Form, you’ll use the dialog editor to design the form and to map controls on the form to the recordset.


The CEnrollDoc Document Class

In ClassView, you can see that AppWizard created a class, CEnrollDoc, derived from CDocument. In this topic, you will examine the member functions in this class.

Suggested Reading

  • CDocument::UpdateAllViews, Microsoft Foundation Class Reference

  • Viewing Resources, Visual C++ User’s Guide

  • MFC: Using Database Classes Without Documents and Views, Visual C++ Programmer’s Guide

  • Serialization: Serialization vs. Database Input/Output, Visual C++ Programmer’s Guide.

  • Multiple Document Types, Views, and Frame Windows, Visual C++ Programmer’s Guide

  • Dialog Box Topics, Visual C++ Programmer’s Guide

  • Control Bar Topics, Visual C++ Programmer’s Guide

To view the document class

  • In ClassView, double-click the icon for class CEnrollDoc.

    This opens the file EnrollDoc.h in the text editor, placing your cursor beside the CEnrollDoc class declaration.

What is the role of a document in a database application? In most other applications, the document stores data and serializes it to a file on disk. Often the application reads the whole file into memory at once and writes it back to disk as a whole. In a database application, however, the data is stored in the database, and the user usually views the data as records. Such an application doesn’t need a file.

A document in a database application, then, isn’t normally used for its serialization support. So why does Enroll have a document class?

The following code, at the beginning of EnrollDoc.h, reveals that the role of the document class in Enroll is to own the recordset.

class CEnrollDoc : public CDocument
{
   ...
   // Attributes
   public:
      CSectionSet m_sectionSet;
   ...
};

The recordset object, m_sectionSet, is embedded in the document object. Therefore, the recordset object is automatically constructed when the document object is constructed, and automatically deleted when the document object is deleted.

The document class can own any number of recordset objects in this way. For example, Step 4 of Enroll adds a second form and corresponding recordset; the document embeds this second recordset.

In a sense, then, the document class is a proxy for the database. This approach isn’t strictly necessary, but if you (or AppWizard) design your database application to use the document class this way, you can better take advantage of the framework’s document/view architecture. For example, if you have multiple views (forms) simultaneously showing some of the contents of the database, you can take advantage of the CDocument::UpdateAllViews mechanism to conveniently notify all views about an update that might have been initiated in one of the views.


Viewing the Database Menu Resource

In this topic, you will examine the menu resource that AppWizard created and learn about the options provided for a database view with no file support.

Suggested Reading

  • Viewing Resources, Visual C++ User’s Guide

  • Overview: Resource Editors, Visual C++ User’s Guide

  • MFC: Using Database Classes with Documents and Views, Visual C++ Programmer’s Guide

  • MFC: Using Database Classes Without Documents and Views, Visual C++ Programmer’s Guide

To view the Enroll menu resource

  1. In ResourceView, expand the Enroll resources folder.

    This displays the resources associated with a project.

  2. Expand the Menu folder.

  3. Double-click IDR_MAINFRAME.

    The Menu editor opens, displaying the default menu that AppWizard created for the Enroll application.

  4. Click the File menu item to view its structure.

    When you created the project, you chose the option Database view without file support. Consequently, the menu resource shows only the Print, Print Preview, Print Setup, and Exit commands. The File menu has no New, Open, Save, or Save As commands. If you had chosen Both a database view and file support, AppWizard would have supplied these missing File menu commands.

  5. Close the Menu editor when you’re finished.

Note If you choose the option Both a database view and file support in AppWizard, the document class plays two roles. First, it serves as a proxy for the database. Second, it represents the file that is opened and saved via the New, Open, Save, and Save As commands on the File menu.


Customizing the Dialog Template for the Section Form

Along with the classes, AppWizard creates a dialog template resource named IDD_ENROLL_FORM, which the CRecordView-derived class, CSectionForm, uses to display its form controls. Because CRecordView is derived from CFormView, a record view’s client area is laid out by a dialog template resource. The layout of the form is up to you. AppWizard places one static text control on the dialog template resource, labeled “TODO: Place form controls on this dialog.”

Suggested Reading

  • CRecordView Overview, Microsoft Foundation Class Reference

  • Overview: Dialog Editor, Visual C++ User’s Guide

In the following procedure, you will replace this text with controls that correspond to columns in the table (via the field data members of the recordset). The table below lists the columns and their associated edit control IDs and variable names.

Enroll's Controls and Member Variables

Column name Control ID Variable Name
Course IDC_COURSE
m_pSet->m_CourseID
Section IDC_SECTION
m_pSet->m_SectionNo
Instructor IDC_INSTRUCTOR
m_pSet->m_InstructorID
Room IDC_ROOM
m_pSet->m_RoomNo
Schedule IDC_SCHEDULE
m_pSet->m_Schedule
Capacity IDC_CAPACITY
m_pSet->m_Capacity

To customize Enroll’s form

  1. In ResourceView, expand the Enroll resources folder.

  2. Expand the Dialog folder.

  3. Double-click IDD_ENROLL_FORM.

    The dialog editor opens and displays the dialog box with the corresponding ID.

  4. Select and delete the static control that says TODO: Place form controls on this dialog.

  5. Design Enroll’s Section form to resemble the figure below, using static controls and edit controls.

    Tip You can press CTRL before you click a dialog box control; then release the CTRL key and click in the dialog box multiple times to add multiple copies of the control. For instance, if you want six edit controls, click six times. Click the selection arrow to stop adding controls.

    Resize the dialog box as needed. You can either add the controls in pairs (that is, first a static text control and then the corresponding edit control, and so on) or later change the Tab order of the controls, as described at the end of this topic, so that they are paired in this way. This becomes important when you bind the controls to recordset fields, described in the next topic.

  6. Choose Properties from the View menu to display the Properties window, and then pin it down so that it stays open while you add and edit the dialog box controls.

  7. For each edit control, use the ID box in the Properties window to specify an ID based on the table column names (for example, IDC_COURSE). This is only a convention, but it is used throughout the tutorial.

    Note The “Edit” caption that appears in each edit control is not visible to the user at run time, so you needn’t worry about deleting it. To view the dialog as it will appear at run time, press CTRL+T to enter test mode for the dialog box. Press ESC to cancel test mode.

    The Layout of Enroll’s Section Form

  8. Make the Course and Section edit controls read-only. To do so, select the Styles tab in the Properties window and set the Read-only check box. (The other edit controls are updatable.)

    According to a common rule in the user-interface design of database forms, the user shouldn’t be able to update these key fields. If users want to change a course number or section of a Section record, they must delete the old Section record and add a new one to avoid possibly violating the referential integrity of the database. Enroll tutorial Step 3 implements Add and Delete functionality.

  9. Save your work.

    It’s a good idea to periodically back up your work.

If you did not add the static text and corresponding edit controls in order, one after the other, you need to change the tab order. In either case, you can easily check the current tab order and change it if necessary.

To view or change the tab order of controls

  1. With the dialog resource open, from the Layout menu click Tab Order.

    You’ll see numbers depicting the current tab order of the controls.

  2. Specify the tab order you want by clicking each control in that order.

    As you click, you’ll see the numbering change to reflect your choice.

    For Enroll, specify a tab order such that each edit control is preceded in the tab order by the static text control that describes it. By specifying this tab order, you enable ClassWizard to derive a name for the edit control when you bind it to a data member, as you’ll do in the next section.

  3. Press ESC to exit Tab Order mode.

    Leave the dialog editor open for the next procedure.


Binding Enroll's Controls to Recordset Fields

Binding Enroll’s Controls to Recordset Fields

With the form designed, it’s time to indicate which edit controls map to which table columns — or, more precisely, which controls map to which recordset field data members. To perform this task, you use ClassWizard’s “foreign object” mechanism.

Suggested Reading in the Visual C++ Programmer’s Guide

  • ClassWizard: Foreign Objects

Normally, you use ClassWizard to bind controls in a dialog box or form to member variables of your CDialog- or CFormView-derived class. In the case of CRecordView, though, you bind the form’s controls not to data members of the record view class but to data members of the recordset class associated with the record view.

Your CRecordView-derived class — CSectionForm in this case — has a data member called m_pSet. You can view m_pSet using ClassView. This data member is a pointer to CSectionSet, Enroll’s recordset class. Recall that you viewed this recordset class on the Member Variables tab of ClassWizard (see the figure Table Columns Mapped to Recordset Data Members, earlier in this lesson).

The control bindings go through m_pSet to the corresponding field data members of CSectionSet. For example, in the following procedure, you will bind the Course edit control to:

m_pSet->m_CourseID

To bind a form control to a recordset data member

  1. If necessary, go to ResourceView, expand the Dialog folder, and double-click IDD_ENROLL_FORM. This opens the dialog box inside the dialog editor.

  2. In the dialog editor window, hold down the CTRL key and double-click the Course edit control.

    ClassWizard’s Add Member Variable dialog box appears, with a proposed field name selected for you in the Member variable name box. ClassWizard chooses this name based on the caption of a static text control that falls previous to the edit control in the tab order.

    For example, for IDC_COURSE, the control’s caption is “Course,” and the Member variable name box should display:

    m_pSet->m_CourseID
  3. Click OK in the Add Member Variable dialog box to accept the name.

  4. Repeat steps 2 and 3 for each of the other edit controls on the form.

    It isn’t necessary to create mappings for the static text controls.

  5. Save your work.

Note Using CTRL+double-click in the dialog editor is a ClassWizard shortcut for mapping form controls to members of the associated dialog, form view, or record view class. You can use it on a pushbutton to create a command handler function for the button. You can use it on other controls to create a class member variable.

You can view the complete mappings in the Class Wizard Member Variables tab for class CSectionForm. For example, where IDC_COURSE appears in the Control IDs column, you’ll see ->m_CourseID in the corresponding Member column.


Build and Run Enroll Step 1

Build and run Enroll Step 1. For information on building, see Building the Starter Application in Scribble Lesson 2.

When the CSectionSet recordset opens, it selects records from the Section table in the Student Registration database. The first record becomes the “current record” in the recordset. Enroll’s database form displays the controls you designed, now filled with data from the current record.

Try a few things:

  • Take a look at the Record menu, which has First Record, Previous Record, Next Record, and Last Record commands. (The toolbar has buttons that correspond to the menu commands.) Try using the commands to scroll through the records in the recordset.

  • Update some of the fields. The new values are accepted into the data source when you move to another record. As mentioned earlier, the key fields Course and Section are read-only.

    Note In order to make changes to the fields, you must have write access to the data source.

This completes Step 1 of the database tutorial. In the next lesson, you will learn how to add a second recordset and use it to fill a combo box control on the form.


Using a Second Recordset

Enroll: Database Application, Lesson 3

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

Note If you are working through the DaoEnrol tutorial lessons, you will work through the topic Binding the Combo Box Control to a Recordset Field and a CComboBox Variable in this lesson.

Although AppWizard starts you off with one initial pair of recordset and record view classes, you can later use ClassWizard to add more recordset and record view classes. Multiple record views can view the same recordset. Conversely, a record view class can view more than one recordset, although only one of the recordsets can be its primary recordset.

In this lesson, you’ll add some code so that you can view more than one recordset with the same record view.

This lesson explains:

  • Replacing the Course edit box with a Course List combo box.

  • Creating a second recordset class with ClassWizard.

  • Filling the Course List combo box from the second recordset.

  • Parameterizing the Section recordset.

  • Requerying the Section recordset.

  • Building and running Enroll Step 2.

At the end of this lesson, you’ll be able to build and run the Enroll Step 2 application.


About Step 2

In Step 2, you will learn how to:

  • Use more than one recordset in the same record view.

  • Fill a combo box from a recordset.

  • Use a recordset filter (CRecordset::m_strFilter).

  • Sort a recordset (CRecordset::m_strSort).

  • Use recordset parameters.

  • Refresh a recordset by calling the CRecordset::Requery member function — a common task if you use filters and parameters.

Suggested Reading in the Microsoft Foundation Class Reference

  • CRecordset::m_strFilter

  • CRecordset::m_strSort

  • CRecordset::Requery

Step 2 illustrates using two recordsets in one record view by implementing a second recordset for the Course table, which is used to fill a combo box in the CSectionForm view. In this way, the CSectionForm view has a primary association with the CSectionSet recordset — the form shows one record from CSectionSet — while the combo box is associated with a second recordset, CCourseSet.

This step changes the CSectionSet recordset so it selects only the available class sections for a single course name, rather than selecting all class sections for all courses. You’ll change the Course edit control to a combo box control and fill the combo box with all of the course names from the Course table. When the user selects a different course name from the combo box, you’ll requery the Section table to select only those class sections for the course name the user chose.


Trying Out Step 2

Try Step 2 out now, if you like, by running it from the Samples directory on the distribution CD. (For information about how to do this, see Previewing the Sample Applications in Scribble, Lesson 1.)

Note If you are running the DaoEnrol tutorial, you will first need to set up the data source. See the section Setting Up the Student Registration Data Source in Enroll, Lesson 1.

Enroll’s database form now displays a list of course names in the combo box. The other controls are filled from the first Section record for that course name.

Use the First Record, Next Record, Previous Record, and Last Record commands on the Record menu (or the equivalent toolbar buttons) to move through the different class sections for the same course name.

Note The CSectionForm record view detects the end of a recordset only if the user has moved past it. The user must move beyond the last record before the record view can tell that it must disable any user-interface objects for moving to the next or last record.

Select a new course name from the Course combo box. The application then requeries the CSectionSet recordset for the new course name. Move through the class sections for the new course name.

Exit Enroll Step 2 when you finish exploring.


Changing the Course Control to a Combo Box

In this topic, using the dialog editor, you will convert the Course edit control to a drop-list style combo box.

Suggested Reading

  • Changing the Tab Order, Visual C++ User’s Guide

  • Overview: Dialog Editor, Visual C++ User’s Guide

  • Using ClassWizard, Visual C++ User’s Guide

To change the Course control to a combo box

  1. Click the ResourceView tab and expand the Dialog folder.

  2. Double-click the dialog resource IDD_ENROLL_FORM.

  3. Select, and then delete the Course edit control.

  4. Add a combo box in its place.

  5. Right-click the combo box and click Properties from the pop-up menu. Pin down the Combo Box Properties box, and then specify the following:
    • Click the General tab, and in the ID box, type IDC_COURSELIST.

    • Click the Styles tab, and from the Type list box, choose Drop List.
  6. Increase the size of the combo drop-down box so it can show more than two course names at a time:
    • Click the drop-down arrow on the right side of the combo box.

    • Use the bottom sizing handle to extend the drop-down area downward enough to hold several lines of text.
  7. Change the tab order of the dialog box to make sure that the new control follows its associated static text label in the tab order. This enables ClassWizard to match the recordset member to the static text caption.
    • From the Layout menu, choose Tab Order.

    • With the tab order showing, click the Course static text control, and then click the Course combo box control.

    The tab order should ripple through the other controls, keeping the sequential relationship between each static text and its associated edit control.

  8. Press ESC to exit Tab Order mode.

    Leave the dialog editor open for the next procedure.

The figure below shows the final appearance of Enroll Step 2 with the combo box in place.

Enroll Step 2 with a Combo Box


Binding the Combo Box Control to a Recordset Field and a CComboBox Variable

Now that you have replaced the edit control for the m_CourseID member of CSectionSet with a combo box, you need to:

  • Unbind the old edit control.

  • Bind the new combo box control to the CourseID field.

  • Bind the combo box control to a second member variable, a CComboBox variable in CSectionForm.

You will later use member functions of CComboBox, such as AddString, to fill and read the combo box.

When you complete the next three procedures, the combo box control will have two member variables associated with it: (1) the foreign member variable, m_CourseID, in the recordset associated with the record view, and (2) the CComboBox member variable in the record view class.

ClassWizard supports having two such member variables bound to the same control.

To remove the old edit control binding

  1. From the View menu, click ClassWizard.

  2. Click the Member Variables tab.

  3. In the Class name box, select class CSectionForm (if it isn’t already selected).

  4. In the Control IDs box, select IDC_COURSE and click Delete Variable.

  5. Click OK.

To bind the combo box control to the recordset member

  1. In the dialog editor, press CTRL and double-click the combo box control to open the Add Member Variable dialog box.

    Recall that when you changed the edit control to a combo box, you also changed the tab order so the combo box would directly follow the Course static text control. ClassWizard chooses the member variable name based on the recordset member associated with the static text control that directly precedes it in tab order.

  2. Click OK to accept m_pSet->m_CourseID as the member variable name and exit the Add Member Variable dialog box.

To bind the combo box control to the view’s CComboBox member variable

  1. Open ClassWizard and, if necessary, click the Member Variables tab.

  2. In the Control IDs list, highlight IDC_COURSELIST, and click Add Variable.

    The Add Member Variable dialog box appears.

  3. In the Member variable name box, type m_ctlCourseList.

  4. In the Category box, select Control.

    Note that this automatically selects CComboBox as the Variable type.

  5. Click OK to exit the Add Member Variable dialog box.

  6. Click OK to exit ClassWizard.

  7. Close the dialog editor and property page and save your work.
  • If you are working through the DaoEnrol tutorial, proceed to the topic Creating a Recordset for the Course Table in DaoEnrol

  • If you are working through the Enroll tutorial, proceed to the topic Creating a Recordset for the Course Table.


Creating a Recordset for the Course Table

Enroll currently has one recordset, for the Section table. This recordset fills the controls on the CSectionForm record view with information about a single class section of the currently selected course name. Now you will add a second recordset, for the Course table, used to fill the combo box control with a list of all available course names.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Reusing Code: Overview

  • ClassWizard: Creating a Recordset Class

To create a new recordset class

  1. From the View menu, click ClassWizard.

  2. Click Add Class, and from the menu, click New.

    This opens the New Class dialog box.

  3. Under Class information, in the Name box, type CCourseSet.

  4. From the Base class drop list, select CRecordset.

  5. Click OK.

    This opens the Database Options dialog box.

To connect the recordset class to the Course table

  1. From the ODBC drop list, select Student Registration, and click OK.

    Depending on the database type, you may need to supply additional information to log in to the data source.

    The Select Database Tables dialog box opens.

  2. Select the table name Course, and click OK. Depending on the data source type you are using, additional qualifiers may precede or follow the table name.

    This connects the table name to class CCourseSet and returns you to the ClassWizard. If necessary, click the Member Variables tab. The Class name box shows CCourseSet, and three names are listed in the Column Names box.

    The table below shows the column names, their data members, and their data types.

  3. Click OK to close ClassWizard.

Note On the Member Variables tab, you can see that all of the table’s columns are already assigned to field member variable. You can use ClassWizard to delete those variables if you don’t need to access or modify the columns — but be careful not to delete a field member variable for a column that is part of the table’s primary key.

CCourseSet Data Members

Column name Type Data member
CourseID CString
m_CourseID
CourseTitle CString
m_CourseTitle
Hours int
m_Hours


Embedding the Recordset Object in the Document Object

In Step 1, AppWizard embedded the CSectionSet object in the document. In this step, you’ll do the same for the second recordset object — an object of the CCourseSet class that you created earlier with ClassWizard.

Suggested Reading in the Visual C++ User’s Guide

  • Reading the FileView and ClassView Pane

To embed the recordset in the document

  1. Click the FileView tab and expand the Enroll files list. Expand the Header Files folder. Double-click file EnrollDoc.h to open it in the text editor.

  2. Declare an embedded CCourseSet object, by adding the following line to the public Attributes section, just beneath the CSectionSet declaration:
    CCourseSet     m_courseSet;
  3. Similarly, expand the Source Files folder and open files EnrollDoc.cpp, Enroll.cpp, and SectionForm.cpp, and add an #include directive for “CourseSet.h” before the existing #include directive for “EnrollDoc.h”, as shown in the following line:
    #include "CourseSet.h"
  4. Save EnrollDoc.h and the .cpp files.

The document’s m_courseSet member is referred to in the implementation of OnInitialUpdate that you’ll complete later.


Filling the Combo Box with a List of Courses

A good place to fill the combo box with a list of course names is in CSectionForm’s override of CRecordView’s OnInitialUpdate member function. As part of its own initialization, the form fills the combo box. The overall logic is as follows:

  1. Construct and open a CCourseSet recordset based on the Course table.

  2. Remove any current entries in the combo box.

  3. For each course name in CCourseSet, add the CourseID to the combo box.

  4. Set the selection to the first course name (as sorted) in the combo box.

Suggested Reading in the Visual C++ Programmer’s Guide

  • UsingWizardBar

  • Record Views: Filling a List Box from a Second Recordset

The code in the following procedure fills the combo box and also filters, parameterizes, and sorts the CSectionSet recordset. Filtering, parameterization, and sorting are explained in topics that follow.

To fill the combo box

  1. From the Window menu, select the file SectionForm.cpp. (This file should still be open from the previous procedure.)

  2. From WizardBar, in the Members combo box, click OnInitialUpdate.

  3. Just after the first line — m_pSet = &GetDocument()->m_sectionSet; — add the code below (don’t replace any code):
    // Fill the combo box with all of the courses
CEnrollDoc* pDoc = GetDocument();
pDoc->m_courseSet.m_strSort = "CourseID";
if (!pDoc->m_courseSet.Open())
return;

// Filter, parameterize and sort the CSectionSet recordset
m_pSet->m_strFilter = "CourseID = ?";
m_pSet->m_strCourseIDParam = pDoc->m_courseSet.m_CourseID;
m_pSet->m_strSort = "SectionNo";
m_pSet->m_pDatabase = pDoc->m_courseSet.m_pDatabase;

    You will add the necessary member declaration in Setting Up the Parameter later in this lesson.

  4. Next, after the last line (CRecordView::OnInitialUpdate();), add the following code:
    m_ctlCourseList.ResetContent();
if (pDoc->m_courseSet.IsOpen())
{
while (!pDoc->m_courseSet.IsEOF())
{
m_ctlCourseList.AddString(
pDoc->m_courseSet.m_CourseID);
pDoc->m_courseSet.MoveNext();
}
}
m_ctlCourseList.SetCurSel(0);
  5. Save your work.

The code you have added above is explained in the topics Setting Up the Filter and Setting Up the Parameter, later in this lesson.


Filtering and Parameterizing the Recordset

The Step 1 version of Enroll selects into CSectionSet all of the records in the Section table. In Step 2, only the class sections for a specific course name should be selected. This discussion introduces the concepts of recordset filters and parameters.

In the next two topics, you will learn how to:

  • Set up a filter

  • Set up a parameter


Setting Up the Filter

Note You have already added the code to filter and parameterize the CSectionSet recordset (in OnInitialUpdate); the code in this topic is for illustrative purposes only. Do not add the code from this topic to your source files.

A recordset filter determines what subset of records are selected from a table or query. To add a filter, you simply set the value of CRecordset::m_strFilter before calling CRecordset::Open. For example, the following code selects just the class section records for course MATH101:

m_pSet->m_strFilter = "CourseID = 'MATH101'";
m_pSet->Open();

Since the base class CRecordView::OnInitialUpdate calls CRecordset::Open, all you need to do to initially select the records for MATH101, for example, is replace the following AppWizard implementation of OnInitialUpdate:

void CSectionForm::OnInitialUpdate()
{
   m_pSet = &GetDocument()->m_sectionSet;
   CRecordView::OnInitialUpdate();
}

with:

void CSectionForm::OnInitialUpdate()
{
   m_pSet = &GetDocument()->m_sectionSet;
   m_pSet->m_strFilter = "CourseID = 'MATH101'";
   CRecordView::OnInitialUpdate();
}

The filter can be any logical expression that is legal for the SQL WHERE clause. For example, the following is legal:

m_pSet->m_strFilter =
            "CourseID = 'MATH101' AND InstructorID = 'ROGERSN'";

Examine the OnInitialUpdate code you added earlier. It shows the filter for CSectionSet in Enroll Step 2.

Caution In Enroll, filter strings typically use a parameter placeholder, “?”, rather than assigning a specific literal value, such as “MATH101”, at compile time. If you do use literal strings in your filters (or other parts of the SQL statement), you may have to “quote” such strings with a DBMS-specific “literal prefix” and “literal suffix” character(s). For example, the code in this topic uses a single quote character to bracket the value assigned as the filter, “MATH101”. You may also encounter special syntactic requirements for operations such as outer joins, depending on your DBMS. Use ODBC functions to obtain this information from your driver for the DBMS. For example, call ::SQLGetTypeInfo for a particular data type, such as SQL_VARCHAR, to request the LITERAL_PREFIX and LITERAL_SUFFIX characters.


Setting Up the Parameter

Enroll reselects, or “requeries,” class section records every time the user selects a new course name from the combo box. One way to implement this is to close the old CSectionSet object and reopen it by supplying a new m_strFilter value before calling Open. This works but is somewhat inefficient, because the framework has to completely reconstruct and invoke a new SQL SELECT statement. A more efficient way to requery the same recordset is to “parameterize” the filter — call Requery with a new filter value and a specific parameter value.

In order to parameterize the filter, you will perform the following procedures:

  • Declare a parameter data member in the recordset’s header file

  • Bind the parameter data member to the recordset

To implement the Requery with a new filter and a specific parameter value supplied at run time, you:

  • Specify a parameterized filter

  • Supply the run-time parameter value

To declare a parameter data member in the recordset’s header file

  1. From FileView, open file SectionSet.h.

  2. Add the following member variable declaration for m_strCourseIDParam, just before the //Overrides section, after the //}}AFX_FIELD line:
    CString m_strCourseIDParam;

To bind the parameter data member to the recordset

  1. From ClassView, jump to the CSectionSet constructor, and initialize the parameter count variable, m_nParams, which by default is zero. Also initialize Enroll’s single parameter, m_strCourseIDParam.

    Place the following two lines of code after the line //}}AFX_FIELD_INIT:

    m_nParams = 1;
m_strCourseIDParam = "";
  2. From ClassView, jump to the DoFieldExchange member function definition, and add the following two lines of code to identify m_strCourseIDParam as a parameter data member. Add the code at the end of the function, after the //}}AFX_FIELD_MAP line.
    pFX->SetFieldType(CFieldExchange::param);
RFX_Text(pFX, "CourseIDParam", m_strCourseIDParam);

    DoFieldExchange recognizes two kinds of fields: columns and parameters. The call to the CFieldExchange member function SetFieldType indicates what kind of field(s) follow in the RFX function calls. In this example, there is one parameter: m_strCourseIDParam.

    The name of the column for the parameter in the RFX_Text call — “CourseIDParam” — is arbitrary; you can provide any name you want.

  3. Save your work.

To specify a parameterized filter

  • Before the call to the base class function CRecordset::Open, which is called by CRecordView::OnInitialUpdate, specify the parameterized filter, as shown in this line (which you’ve already added):
    m_pSet->m_strFilter = "CourseID = ?";

The question mark “?” indicates where the parameter value will be substituted at run time. If you have more than one parameter in your m_strFilter, such as:

m_pSet->m_strFilter = "CourseID = ? AND SectionNo = ?";

you must make multiple RFX calls after the call to:

pFX->SetFieldType(CFieldExchange::param);

You must make the RFX calls for multiple parameters in exactly the same order as the question marks in the m_strFilter and/or m_strSort.

Note If you have both a filter and a sort with parameters, specify the filter parameters first, then the sort parameters. Not all ODBC drivers permit parameters on a sort. Consult the Help file for your ODBC driver.

To supply the run-time parameter value

  • Assign the value to the previously bound parameter data member, as shown in the following line (which you’ve already added in the OnInitialUpdate function).
    m_pSet->m_strCourseIDParam = pDoc->m_courseSet.m_CourseID;

    This sets the parameter value to be the first course record retrieved from the CCourseSet recordset. All parameter values must be assigned before calling CRecordset::Open (or CRecordView::OnInitialUpdate), or as you will see later, before calling CRecordset::Requery.


Reusing a Database Object Opened by Another Recordset

Note You add no new code to Enroll in this topic.

Suggested Reading in the Microsoft Foundation Class Reference

  • CDatabase Overview

AppWizard and ClassWizard both implement CRecordset-derived classes such that the recordset object owns its own CDatabase object. Up to now, the CDatabase object has been transparent because the framework created it for you when you constructed a recordset object. The default implementation of CRecordView::OnInitialUpdate indirectly calls the wizard-implemented GetDefaultConnect function for the recordset. The implementation looks like this:

CString CSectionSet::GetDefaultConnect()
{
   return "ODBC;DSN=Student Registration;";
}

The framework passes this “connection” string to CDatabase::Open for the CDatabase object that the framework creates in its implementation of CRecordset::Open. If your application has two or more recordsets, each recordset will, by default, create and open its own CDatabase object. If multiple recordsets access the same data source, it’s a good idea to have them share the same CDatabase object.

One way to share the same CDatabase object among multiple recordsets is to pass the m_pDatabase member of the first recordset object to the Open function of the other recordsets. This is what you’ve already implemented in CSectionForm::OnInitialUpdate:

m_pSet->m_pDatabase = pDoc->m_courseSet.m_pDatabase;
CRecordView::OnInitialUpdate();

If CRecordset::Open finds that the m_pDatabase member is already allocated, it simply reuses the open CDatabase.

Another way to share the same CDatabase object among multiple recordsets is to embed the CDatabase object in the document object. For an example of this approach, see the source code for Enroll Step 4 in Samples \ MFC Samples \ Tutorials in the table of contents.


Sorting the Recordset

Note You add no new code to Enroll in this topic.

The procedure for sorting a recordset is very simple: set the member variable CRecordset::m_strSort before calling CRecordset::Open. The syntax for m_strSort is exactly that of the SQL ORDER BY clause, which is one or more columns separated by commas.

Suggested Reading

  • CRecordset::m_strSort, Microsoft Foundation Class Reference

  • SQL, Visual C++ Programmer’s Guide

  • Using WizardBar, Visual C++ User’s Guide

  • Recordset: Requerying a Recordset (ODBC), Visual C++ Programmer’s Guide

The CCourseSet records are all sorted by CourseID (which you have already added):

pDoc->m_courseSet.m_strSort = "CourseID";

Also, the CSectionSet records for a given course name are sorted by class section:

m_pSet->m_strSort = "SectionNo";


Requerying the CSectionSet Recordset

Whenever the user selects a new course name from the combo box, Enroll must “requery” the CSectionSet recordset to refresh its records. By selecting a new course name, the user will see records only for the class sections of that course name. The existing CSectionSet recordset contains records for the previous course name. Requerying the recordset brings it up to date for the new course name, using the current values of the filter and sort strings.

When the user accepts a selection in the combo box, the CSectionForm record view gets a CBN_SELENDOK notification message. The record view uses its handler for this message to reselect records based on the course name selected, passing the course ID as a parameter.

The following procedure describes how to use WizardBar to create this handler.

To requery the CSectionSet recordset

  1. From the Window menu, select SectionForm.cpp.

  2. From the WizardBar Filter combo box, select IDC_COURSELIST.

  3. In the Members combo box, select CBN_SELENDOK.

  4. The New Windows Message and Event Handlers dialog box opens.

  5. Click Add Handler.

  6. Click OK to accept the default name OnSelendokCourselist.

  7. Click Edit Existing to jump to the code created for the handler.

  8. In the editor window, add the following code in place of the //TODO comment:
    if (!m_pSet->IsOpen() )
return;
m_ctlCourseList.GetLBText(m_ctlCourseList.GetCurSel(),
m_pSet->m_strCourseIDParam);
m_pSet->Requery();
if (m_pSet->IsEOF())
{
m_pSet->SetFieldNull(&(m_pSet->m_CourseID), FALSE);
m_pSet->m_CourseID = m_pSet->m_strCourseIDParam;
}
UpdateData(FALSE);
  9. Save your work.

This code requeries records from the database into the recordset, based on the parameter value in m_strCourseIDParam. The parameter value is set to the currently selected course name from the Course List combo box before requerying the database.

If you requery and it turns out that the selected course name has no class sections, the recordset is initialized with Null database field values except for CourseID. (In database terminology, Null means "having no value" and is not the same as NULL in C++.)

If you are working through the DaoEnrol tutorial, at this point you are ready to compile and run the application. After you have examined the working application, return to DaoEnrol Step 3.


Build and Run Enroll Step 2

Build and run Enroll Step 2. Use the navigation user interface to move through all class sections for the course name currently selected in the Course combo box. Select a different course name in the combo box and navigate through its class sections.

This completes Step 2 of the database tutorial. The next lesson, Adding and Deleting Records, (Step 3) concludes the tutorial by showing you how to add and delete records.


Adding and Deleting Records

Enroll: Database Application, Lesson 4

Tip If you prefer working from a printed tutorial, see Copying and Printing in the MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

Note If you are working through the DaoEnrol tutorial lessons, you will work through the topic Implementing the Add Command in this lesson.

Step 3 implements new commands for adding and deleting records and for abandoning an update in progress. The lesson explains:

  • Creating the Step 3 user interface.

  • Adding, editing, and deleting records.

  • Implementing the Add, Refresh, and Delete commands.

  • Building and running Enroll Step 3.

Perform all the tasks in the procedures in this lesson. At the end, you will be able to build and run your Enroll Step 3 application.


About Step 3

Step 3 teaches:

  • The basics of adding, editing, and deleting records.

  • How to implement commands for these operations.

Up to now Enroll has supported editing (updating) records but not adding or deleting records.

There are many different user-interface styles for adding records. For example, when a Microsoft Access user reaches the end of a recordset, Access considers the next record to be a new record. Other applications have an explicit Add command. Enroll’s user interface is only one among many possible user interfaces that you might implement using MFC.

The user interface in Step 3 includes three new commands on the Record menu, with corresponding toolbar buttons:

  • The Add command prepares a blank record into which the user enters data. The user saves the new record by moving to another record, just as he or she saves an edited record by moving to another record. The user can also save the new record by issuing the Add command again.

  • The Refresh command abandons an operation to add or edit a record. Refresh restores the modified record to its original state or returns to the record shown before Add.

  • The Delete command deletes a record.

To try Step 3 now, run Enroll.exe from the Samples directory on the distribution CD. For more information, see Previewing the Sample Applications in Scribble Lesson 1. Note that if you are running the DaoEnrol tutorial, you will first need to set up the data source. See the section Setting Up the Student Registration Data Source in Enroll Lesson 1.

The figure below shows the finished Enroll application.

The Enroll Step 3 Application

Here are some things to try:

  • Try the new Add, Refresh, and Delete commands.

  • Try forcing the two exceptions handled by Enroll:
    • Try to delete a section that has Enrollment records.

    • Try to add a duplicate section.

When you finish, exit the program.


Creating the Step 3 User Interface

In the following topics, you will use set up the user interface for Enroll, including:

  • Adding Menu Items for Add, Refresh, and Delete

  • Adding an Accelerator for the Refresh Command

  • Creating Handlers for Add, Refresh, and Delete

Suggested Reading

  • Overview: Menu Editor, Visual C++ User’s Guide

  • Menu Editor Topics for Visual C++, Visual C++ Programmer’s Guide

  • Viewing Resource, Visual C++ User’s Guide


Add Menu Items for Add, Refresh, and Delete

In the following procedure, you will add three new menu items to Enroll’s Record menu: Add, Refresh and Delete. The command IDs you assign to the new menu items are application-specific IDs, not predefined by the framework as are ID_RECORD_FIRST and the other commands on the Record menu.

Tip You may find it helpful to “pin down” the Menu Item Properties dialog box during the following procedure.

To add menu items for the commands

  1. In ResourceView, expand the Enroll resources folder, and then expand the Menu folder.

  2. Open the IDR_MAINFRAME menu resource.

  3. From the View menu, click Properties to open the Menu Item Properties dialog box. Pin it to keep it open.

  4. At the top of the existing Record menu items, add an “Add” menu item with the following caption, resource ID, and command prompt:
    • &Add

    • ID_RECORD_ADD

    • Add a new section
  5. Below the new Add menu item, add a “Refresh” menu item with the following caption, resource ID, and command prompt:
    • &Refresh \tEsc

    • ID_RECORD_REFRESH

    • Cancel changes on form, or cancel Add

    The “\t Esc” coding specifies that the ESC key can be used as an accelerator. If you do not complete the next procedure, Add an Accelerator for the Refresh Command, don’t include this code.

  6. Below the new Refresh menu item, add a “Delete” menu item with the following caption, resource ID, and command prompt:
    • &Delete

    • ID_RECORD_DELETE

    • Delete section
  7. Below the new Delete menu item, add a separator:
    • Insert a new menu item and, in the Menu Item Properties dialog box, click the Separator checkbox.
  8. Save your work and leave the menu editor window open.

    You will need the editor open to establish a context when you use ClassWizard to create command handler functions for the menu commands you have just added.

The following figure shows the completed menu in the menu editor.

The Record Menu with New Commands


Add an Accelerator for the Refresh Command

If you don't want to implement an accelerator, you can skip this step and test the application.

Suggested Reading in the Visual C++ User’s Guide

  • Overview: Accelerator Editor

To add an accelerator

  1. In ResourceView, expand the Accelerator folder.

  2. Open the IDR_MAINFRAME accelerator resource.

    Note The name for this resource doesn’t need to match the menu resource name, so long as the ID you assign to an accelerator matches the ID for the corresponding menu item.

  3. Create a new accelerator with the following ID: ID_RECORD_REFRESH.

  4. From the Key drop-down list, choose, or type in, VK_ESCAPE.

  5. Clear the “Ctrl” modifier box.

  6. Save your work but leave the accelerator editor open.

    You will need this editor or the menu editor open to establish a context when you use ClassWizard to create command handler functions for the menu commands.


Create Handlers for Add, Refresh, and Delete

Each of the new Record menu commands needs a command handler function in the CSectionForm class. Since the Enroll menu resource is associated with the CMainFrame class, you must make an association between the menu IDs and the CSectionForm class. You do this by giving focus to the IDR_MAINFRAME resource (accelerator or menu) so ClassWizard can glean the available command IDs from the resource.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Using ClassWizard

To create handlers for the commands

  1. With focus on the IDR_MAINFRAME resource, from the View menu, click ClassWizard and click the Message Maps tab.

    Notice that the CMainFrame class is automatically selected.

  2. In the Class Name box, select CSectionForm.

  3. In the Object IDs list, select the ID_RECORD_ADD command ID; in the Messages box, select COMMAND; and then click Add Function to create a command handler function.

    Accept the default handler name: OnRecordAdd.

  4. Repeat step 3 for the ID_RECORD_DELETE and ID_RECORD_REFRESH command IDs.

  5. Click OK to exit ClassWizard.

    You can also close the resource editors at this point.

You will fill in the command handlers in later topics.


The Basics of Adding, Editing, and Deleting Records

Before you implement the new command handlers, you should know some basic facts about how the framework supports database updating:

  • CRecordView automatically updates the current record when the user moves to another record.

  • CRecordView takes three steps to modify an edited record in the associated recordset when the user moves to another record. The record view:
    • Prepares the current record for updating by calling the recordset’s Edit member function.

    • Calls the UpdateData member function derived from CFormView, which changes the recordset’s member variables, usually by getting the new values from the form’s controls.

    • Calls the recordset’s Update member function to actually update the data source with the modified values.
  • CRecordView does not provide a default implementation for Add, since user interfaces for Add functionality vary widely among database applications.

  • The steps for adding a new record parallel the steps for updating a modified record:
    • Prepare a new record by calling the recordset’s AddNew member function. The fields of the new record are initially Null. (In database terminology, Null means “having no value” and is not the same as NULL in C++.)

    • Change the recordset’s member variables, usually by getting the new values from the form’s controls with UpdateData.

    • Call the recordset’s Update member function to actually update the data source with the values for the new record.
  • Deleting a record is simpler than adding or editing one. The record view simply calls the recordset’s Delete member function.

    There are two main concerns when you delete a record. First, if you delete a record from one table and there are related records in other tables, you may damage the integrity of your database. For example, deleting a class section for which there are records in the Enrollment table makes the Section and Enrollment tables inconsistent.

    Second, after deleting a record, you or the user must move off the deleted record to another record.

Suggested Reading in the Microsoft Foundation Class Reference

  • CRecordView Overvew


Implementing the Add Command

Step 3 implements a user interface for Add that closely parallels CRecordView’s default user interface for modifying an existing record. The user starts a new record with the Add command on the Record menu.

In response to the Add command, the record view calls its OnRecordAdd member function and enters an “add mode” by setting an m_bAddMode data member to TRUE. The add mode is completed when the user moves off the record. The Step 3 implementation overrides the record view’s OnMove member function to implement completion of the add mode. The following procedures implement the add mode, and create a CEdit member variable used to turn on and off the read-only style of the Section edit control.

Suggested Reading

  • Overview: ClassView, Visual C++ User’s Guide

  • ClassView Topics, Visual C++ Programmer’s Guide

  • Using ClassWizard, Visual C++ Programmer’s Guide

  • CRecordset::AddNew, Microsoft Foundation Class Reference

To implement the Add mode

  1. In the Attributes section of file SectionForm.h, add the protected m_bAddMode data member:
    protected:
BOOL m_bAddMode;
  2. Initialize m_bAddMode in the CSectionForm constructor in file SectionForm.cpp. (You can jump directly to the constructor from ClassView.) Add the following line after the //}}AFX_DATA_INIT line:
    m_bAddMode = FALSE;

In Steps 1 and 2 of the tutorial, the Section control was read-only because it was necessary to prevent the user from changing this primary key value of the Section record. In Step 3, you need to turn off the read-only style of the Section control when the user is in add mode. The control is still read-only if the user is in browse/update mode rather than add mode.

To change the read-only style, you must call the CEdit member function SetReadOnly with the appropriate parameter. This requires a member variable of type CEdit in CSectionForm. At this point, the class has a CString data member representing the Section control, but you need a CEdit member variable as well.

To define the CEdit member variable

  1. From the View menu, click ClassWizard.

  2. Click the Member Variables tab.

  3. In the Class Name box, select CSectionForm.

  4. In the Control IDs box, select IDC_SECTION, which is already associated with a CString member.

  5. Click Add Variable to open the Add Member Variable dialog box.

  6. In the Member Variable Name box, type the name m_ctlSection.

  7. In the Category box, select Control.

    Notice that the Variable Type box changes appropriately to CEdit.

  8. Click OK to close the Add Member Variable dialog box.

    Notice that a second member variable is now associated with the IDC_SECTION control ID. You access the control’s value through m_pSet->m_SectionNo. You access the control itself, to call its member functions, through m_ctlSection.

  9. Click OK to close ClassWizard.

The Add command initiates add mode and calls the recordset’s AddNew function to prepare a new record, but doesn’t add the record to the data source. The record isn’t actually added to the data source until a subsequent call to OnMove calls the recordset’s Update function.

To implement the OnRecordAdd command handler function

  1. Use ClassView to jump to the OnRecordAdd starter handler that ClassWizard created in SectionForm.cpp.

  2. Add the following code to implement the handler:
    // If already in add mode, complete the previous new record
if (m_bAddMode)
OnMove(ID_RECORD_FIRST);

CString strCurrentCourse = m_pSet->m_CourseID;
m_pSet->AddNew();
m_pSet->SetFieldNull(&(m_pSet->m_CourseID), FALSE);
m_pSet->m_CourseID = strCurrentCourse;
m_bAddMode = TRUE;
m_ctlSection.SetReadOnly(FALSE);
UpdateData(FALSE);

The most important line of this code is the call to CRecordset::AddNew, which prepares a new record. The rest of the code does the following:

  • If the user is already in add mode, complete the current record by simulating the user’s moving to another record. Moving to another record is the normal user interface for completing a record.

  • Save the CourseID for the current record and use it as the default for the new record, based on the assumption that more often than not the user will want to add another section for the course currently being viewed.

  • In add mode, change the Section control to read/write rather than read-only, so the user can enter a new section number.

  • If you are working through the DaoEnrol tutorial, proceed to the topic Updating the Data Source with the Added Record in DaoEnrol.

  • If you are working through the Enroll tutorial, proceed to the topic Updating the Data Source with the Added Record.


Updating the Data Source with the Added Record

Add mode is completed when the user moves off the record. Enroll implements this by overriding the CRecordView::OnMove member function.

Suggested Reading

  • CRecordView::OnMove, Microsoft Foundation Class Reference

  • Using WizardBar, Visual C++ Programmer’s Guide

  • Recordset: Adding, Updating, and Deleting Records (ODBC), Visual C++ Programmer’s Guide.

To implement Add functionality in the OnMove function override

  1. From the View menu, click ClassWizard.

  2. In the Class name box, click CSectionForm.

  3. In the Object IDs list, select CSectionForm.

  4. In the Messages box, select OnMove.

  5. Click Add function.

  6. Click Edit code to jump to the starter OnMove function, and replace the highlighted //TODO comment with the following code:
    if (m_bAddMode)
{
if (!UpdateData())
return FALSE;
TRY
{
m_pSet->Update();
}
CATCH(CDBException, e)
{
AfxMessageBox(e->m_strError);
return FALSE;
}
END_CATCH

m_pSet->Requery();
UpdateData(FALSE);
m_ctlSection.SetReadOnly(TRUE);
m_bAddMode = FALSE;
return TRUE;
}
else
{
return CRecordView::OnMove(nIDMoveCommand);
}

Catch any exceptions thrown by the recordset’s Update function so that errors are reported to the user. The CDBException data member m_strError is a fairly user-friendly error message, prepared by the underlying ODBC driver.

In its default CRecordView implementation, OnMove moves to the next, previous, first, or last record. If the application has changed the recordset field data members for the current record before the move, the framework updates the data source before moving to another record.

Note Some ODBC drivers do not reflect newly added records in the recordset; others do. For those drivers that don’t display newly added records, to make the added records visible you must requery the database.

Step 3 augments the default CRecordView user interface for updating the current record. If the user is in add mode and then moves off the new record, Enroll adds the newly prepared record to the data source before moving to another record. But you must decide whether it’s important for added records to be immediately visible. For the tutorial, the decision is to requery the recordset (evident in the code above) after each add operation so the newly added record is included in the recordset.

Normally, the move commands behave as you might expect: Move Next moves to the next record, and so on. But as a consequence of the decision to requery during the add operation, when the user clicks any move command when adding a record, Enroll always effectively moves to the first record. That’s because requerying the recordset automatically sets the recordset to the first record.


Disabling Combo Box Logic in Add Mode

Step 2 implemented a handler for selecting a course in the combo box. The handler requeried the parameterized CSectionSet for the newly selected course. In Step 3, the combo box takes on the additional duty of allowing the user to specify the course for a new section record being added. During add mode, you don’t want to requery the recordset when the user selects a course from the combo box. Therefore, you need to put the requery logic inside an if clause that is executed only if add mode isn’t in effect.

Suggested Reading

  • Overview: ClassView, Visual C++ User’s Guide

  • ClassView Topics, Visual C++ Programmer’s Guide

To disable normal combo box logic while in add mode

  1. Use ClassView to jump to the OnSelendokCourseList handler in class CSectionForm.

  2. Place an if block around the requery code in the OnSelendokCourseList handler, so the handler now appears as follows:
    void CSectionForm::OnSelendokCourselist()
{
m_ctlCourseList.GetLBText(m_ctlCourseList.GetCurSel(),
m_pSet->m_strCourseIDParam);
if (!m_bAddMode)
{
m_pSet->Requery();
if (m_pSet->IsEOF())
{
m_pSet->SetFieldNull(&(m_pSet->m_CourseID), FALSE);
m_pSet->m_CourseID = m_pSet->m_strCourseIDParam;
}
UpdateData(FALSE);
}
}


Implementing the Delete Command

In response to a Delete command, the record view deletes the current record by calling the Delete member function of its associated recordset.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Using ClassView

To implement the Delete command

  1. Use ClassView to jump to the OnRecordDelete starter function in class CSectionForm.

  2. Implement the handler with the following code:
    TRY
{
m_pSet->Delete();
}
CATCH(CDBException, e)
{
AfxMessageBox(e->m_strError);
return;
}
END_CATCH

// Move to the next record after the one just deleted
m_pSet->MoveNext();

// If we moved off the end of file, move back to last record
if (m_pSet->IsEOF())
m_pSet->MoveLast();

// If the recordset is now empty, clear the fields left over
// from the deleted record
if (m_pSet->IsBOF())
m_pSet->SetFieldNull(NULL);
UpdateData(FALSE);

Catch any exceptions thrown by the recordset’s Delete function so that errors are reported to the user. The CDBException data member m_strError is a fairly user-friendly error message, prepared by the underlying ODBC driver.

If you want to customize the error message, you can force the error condition, then examine m_strStateNativeOrigin for a particular state or native value. You can look up error messages in the ODBC Programmer’s Reference, Appendix A, ODBC Error Codes. Enroll takes the easy approach by displaying m_strError.

For Enroll, the decision was to move to the record following the deleted record. You could move to the previous record after a delete operation or anywhere else as long as you, or the user, moves off the deleted record.


Implementing the Refresh Command

The Refresh command cancels add mode, if the user had previously chosen Add, or it discards any changes the user may have made on the form for the current record. In the first case, Enroll cancels the add mode by calling:

CRecordset::Move(0);

When you call AddNew to begin the add operation, the framework stores a copy of the current record’s fields before allowing the user to enter new values in the record view’s controls. Calling Move as shown here “refreshes” the current record — and effectively cancels the add operation. It restores the record that was current before add mode began. This also works if you called Edit instead of AddNew.

When the user cancels add mode, Enroll makes the Section control read-only again, for reasons explained earlier.

Suggested Reading in the Visual C++ User’s Guide

  • Overview: Toolbar Editor

To implement the Refresh command

  1. Use ClassView to jump to the OnRecordRefresh starter handler in class CSectionForm.

  2. Implement the handler function with the following code:
    if (m_bAddMode)
{
m_pSet->Move(0);
m_ctlSection.SetReadOnly(TRUE);
m_bAddMode = FALSE;
}
// Copy fields from recordset to form, thus
// overwriting any changes the user may have made
// on the form
UpdateData(FALSE);

Note The source files for Enroll Step 3 on your distribution CD-ROM include functional toolbar buttons connected to the Add, Refresh, and Delete commands on the Record menu. The installed Step 3 source code supplies these toolbar buttons for Enroll. If you want to create toolbar buttons and link them to the Enroll command, examine the buttons and code provided in the Step 3 source code. For more information on creating toolbar buttons, see Edit Scribble’s Toolbar in Scribble, Lesson 5.


Building and Running Enroll Step 3

Build and run your version of Enroll Step 3. Try the new Add, Refresh, and Delete commands. Try forcing the two exceptions handled by Enroll — try to delete a section that has Enrollment records, and try to add a duplicate section.

When you finish, exit the program.

This completes the database tutorial.

Note The sample source code for Enroll includes a fourth step, not covered in this tutorial. To read a summary of what’s included in Step 4, see Enroll Step 4 in Tutorial Steps in Lesson 1 of Enroll. To jump directly to the Enroll sample abstract and copy the code to your hard drive, see ENROLL.


Data Access Objects (DAO) Tutorial

DAOEnrol: Database Application, Lesson 1

Tip If you prefer working from a printed tutorial, see Copying and Printing in MSDN Library Help for details about printing a lesson, a set of topics, or a single topic.

The DaoEnrol tutorial shows you how to develop an MFC database application using the Data Access Objects (DAO) database classes. This tutorial uses similar procedures and creates nearly the same application as the Enroll tutorial.

In the DaoEnrol tutorial, you’ll learn how to:

  • Use AppWizard and ClassWizard for DAO database support.

  • Create and use CDaoRecordset objects to open tables and run queries.

  • Create and use CDaoRecordView objects for form-based applications.

  • Use database support within the framework’s document/view architecture.

  • Add, update, and delete records.

  • Manage multiple tables.

  • Handle database exceptions.

This lesson contains:

  • Instructions for setting up the Student Registration database for DAO.

  • A brief overview of DAO.

  • Step 1 of the DaoEnrol tutorial, which explains how to create a database application using AppWizard.

  • Step 2 of the DaoEnrol tutorial, which explains how to add a second recordset and use it to fill a combo box control on the form. Step 2 of DaoEnrol leverages material in Lesson 3 of the Enroll tutorial, with slight variations to accomodate minor differences in DAO.

  • Step 3 of the DaoEnrol tutorial, which explains how to add and delete records. Step 3 of DaoEnrol leverages material from Lesson 4 of the Enroll tutorial, with slight variations to accomodate minor differences in DAO.

There are some differences in the code generated for DaoEnrol and the code generated for Enroll. Consequently there are separate instructions for some topics of DaoEnrol that need to be substituted for topics in Lessons 3 and 4. These topics are included at the end of this lesson in DaoEnrol Step 2 and DaoEnrol Step 3.

Note The DAOENROL sample reflects the completed DaoEnrol tutorial, plus an undocumented fourth step. Separate source code is not provided for each of the DaoEnrol tutorial steps either under Samples \ MFC Samples in the online documentation or on the Visual C++ distribution CD.

Important This tutorial assumes you are familiar with Visual C++ and MFC. If you aren’t, try the Scribble tutorial before you begin this tutorial. The Scribble tutorial introduces important class library concepts and techniques and teaches you to use the wizards and the resource editors.

It is not necessary to complete Enroll, the ODBC database classes tutorial, before starting DaoEnrol.

DaoEnrol and Enroll

DAO recordset, database, and record view class implementations are very similar to that of the ODBC database classes. This means that skills and knowledge you may already have as a result of using the ODBC database classes can apply to the DAO database classes. By completing both the Enroll and DaoEnrol tutorials, you have the opportunity to compare the code and study both implementations. You can also use DaoEnrol as a basis for an application that uses more of MFC’s DAO functionality.

Suggested Reading in the Visual C++ Programmer’s Guide

  • DAO: Writing a Database Application

  • Overview: Creating a Program That Supports a Database


The Tutorial Example: DaoEnrol

The tutorial example program, DaoEnrol, manages a student registration database similar to, but simpler than, a college registration system. It will help you to follow the tutorial if you understand the structure of the student registration database.

DaoEnrol is based on the same student registration database, StdReg32.mdb, that you use with the Enroll tutorial. However, it is not necessary to register this data source with the ODBC Administrator to use it with the DAO database classes in the DaoEnrol tutorial.

The following table lists the database tables, what they store, and the columns in them.

Tables in the Student Registration Database

Table name | Contents | Column list
Course | Think of each record as an entry in a course catalog. Example: the MATH101 course. | CourseID* CourseTitle Hours
Section† | A section record is a specific offering of a course at a specific time. For example, MATH101 may have many sections. | SectionNo* CourseID* InstructorID Schedule RoomNo
Student | A record for each student at the school. | StudentID* Name GradYear
Enrollment | A record for each student in a particular section of a course. For a given student, there is an Enrollment record for each course the student is taking. | CourseID* SectionNo* StudentID* Grade
Instructor | A record for each instructor at the school. | InstructorID* Name RoomNo

* Indicates the column (or columns) that comprise the table’s primary key.

† The Dynabind_Section table is used in the Dynabind sample, but not in the DaoEnrol tutorial.

DaoEnrol lets you use a “form” — a view with dialog-style controls — to view registration information for courses, section by section. Section information displayed includes the course name, section number, instructor, room, and schedule (such as “MWF 10-11”). For example, you can view section 1 of the course MATH 101, then section 2, and so on. The initial tutorial step provides read-only viewing of all sections. Steps 2 and 3 add more capabilities, including updates. The following figure shows what the DaoEnrol application looks like at the end of the tutorial.

The Completed DaoEnrol Tutorial Application


Setting Up the Student Registration Data Source for DaoEnrol

Before you begin the DaoEnrol tutorial you must specify a database. You can either:

  • Copy and use the prebuilt StdReg32.mdb database

    -or-

  • Create your own database

Suggested Reading in the Visual C++ Programmer’s Guide

  • DAO External: Attaching External Tables

  • ODBC Driver List

  • ODBC Administrator

  • Data Source (ODBC)

MFC support for Data Access Objects (DAO) does not rely on Open Database Connectivity (ODBC) for most database formats. Because of the flexibility of the DAO database classes, you have several options for creating and using the DaoEnrol database:

  • Microsoft Access (.MDB) While you can create a database, tables, queries, fields, indexes, and recordsets with DAO, it is much easier to just use the StdReg32.mdb database supplied with Visual C++ for this tutorial. See the procedure To use StdReg32.mdb with DaoEnrol, in the next topic.

  • ISAM databases The Microsoft Jet database engine works natively with Microsoft Access .MDB files, but it can also directly read Indexed Sequential Access Method (ISAM) databases such as Paradox, dBase, and FoxPro. For better performance, attach these external data sources as tables in a Microsoft Access .MDB file. For this tutorial, the tables must match the Student Registration database schema. See the procedure To create your own database for DaoEnrol.

  • Other database formats If you want to use a database format other than one read by the Microsoft Jet database engine, you must install the corresponding 32-bit ODBC driver, as well as its related database management system (DBMS). Microsoft Visual C++ ships 32-bit ODBC drivers for most standard database formats. Use the DBMS to add tables to your database so that it matches the Student Registration database schema. See the procedure To create your own database for DaoEnrol.

Note When you use AppWizard to create a DAO database application, the only database type offered as a selection is a Microsoft Access .MDB file. This tutorial assumes that you will use the StdReg32.mdb file. If you use another database type, you must use Microsoft Access to create an .MDB file, attach the database table to that .MDB file, and select that .MDB file during the selection of the Data Source in MFC AppWizard – Step 2 of 6.


Use the Prebuilt StdReg32.mdb

If you are using Microsoft Access, follow these steps:

To use StdReg32.mdb with DaoEnrol

  1. Open the sample abstract topic for the STDREG sample.

  2. In the topic, click the link provided to copy sample project files.

  3. In the Sample Application dialog box, select StdReg32.mdb and click Copy.

  4. In the Copy dialog box, navigate to the directory where you want to copy the file and click OK.

    The development environment creates the directory for you, if necessary, and copies the file. StdReg32.mdb already contains the tables and records used in the tutorial.


Create Your Own Database for DaoEnrol

If you are using a database format other than Microsoft Access, and you have created a database to use with this tutorial, follow these steps:

To create your own database for DaoEnrol

  • Create a new database schema using the database administration capability of your DBMS.

    Depending on the type of DBMS, you might create the new database on a server that is different from the machine where you will be doing MFC database development. In either case, you need to add tables to the new database to match the schema of StdReg32.mdb. You must also install the ODBC driver that corresponds to your DBMS, and register the database with the ODBC Administrator, as described in Lesson 1 of Enroll.

Note When you run a Typical Visual C++ Setup, the ODBC drivers for dBase, FoxPro, Access, and SQL Server are installed automatically. You can rerun Setup (or choose a Custom installation the first time you run Setup) to install drivers for Paradox, Microsoft Test, Excel, and Oracle. If you need an ODBC driver not provided with Visual C++, you must use the installation program shipped with the program to install it, and use ODBC Administrator to register it.


DAO Tutorial Steps

The following table briefly describes the DaoEnrol tutorial steps:

Tutorial Step | Description
1 | Use AppWizard to create an application with database support. The document embeds a CDaoRecordset object for the Section table of the Student Registration data source. Use the dialog editor to design the form. Use ClassWizard to bind controls on the form to fields in the recordset.
2 | Provide a combo box control on the form so the user can select a course and view its sections. Fill the combo box from a recordset object representing the Course table. Filter and parameterize the recordset to constrain the records it selects. (Use Enroll Step 2 with substituted topics from DaoEnrol Step 2.)
3 | Implement a user interface for adding, updating, and deleting records. Handle database exceptions. (Use Enroll Step 3 with substituted topics from DaoEnrol Step 3.)


DaoEnrol Step 4: A Preview

The sample source code for DaoEnrol consists of the code for Steps 1, 2, and 3, plus a Step 4 not covered in the tutorial. Step 4 illustrates additional database programming techniques as summarized below. See DAOENROL under Samples \ MFC Samples in the online documentation for a discussion of DaoEnrol Step 4. The main techniques illustrated by DaoEnrol Step 4 include:

  • Using multiple record view classes.

  • Switching views in a frame window.

  • Using the document object to coordinate multiple forms via UpdateAllViews and update hints.


A Brief Overview of DAO

MFC includes database classes for programming with Data Access Objects (DAO). DAO is an application programming interface (API) based on OLE.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Overview: Creating a Program That Supports a Database

  • DAO and MFC

  • Data Access Objects (DAO)

  • DAO Collections

  • DAO: Writing a Database Application

  • DAO: Database Tasks

In general, the DAO database classes offer more complete database functionality than the ODBC database classes, which were first introduced in MFC 2.5.

DAO supplies a hierarchical set of objects that use the Microsoft Jet database engine to access data and database structure in:

  • Microsoft Access (.MDB) databases.

  • ODBC data sources, using an ODBC driver.

  • Installable ISAM databases, such as dBASE, Paradox, and FoxPro which the database engine can read directly.

DAO can access other types of databases through ODBC.

Not all DAO objects are exposed in MFC, although most of the DAO functionality is available. You can make direct calls to DAO, and the DAO Software Development Kit (SDK) and DAO Help are included with Visual C++ version 5.0.

Data Access Objects in MFC

MFC encapsulates the following DAO objects (listed in hierarchical order).

  • Workspace

  • Database

  • TableDef

  • QueryDef

  • RecordSet

  • Exceptions


Workspace

In DAO, the Workspace object defines a session for a user. It contains open databases and provides mechanisms for simultaneous transactions. MFC includes access to the DBEngine object that controls the Microsoft Jet database engine and manipulates its properties.

Suggested Reading

  • CDaoWorkspace, Microsoft Foundation Class Reference

  • DAO Workspace, Visual C++ Programmer’s Guide

  • DAO Workspace: The Database Engine, Visual C++ Programmer’s Guide


Database

The Database object represents a connection to the database. The DAO database classes have greater ability to manipulate databases than do the ODBC database classes. DAO can read Microsoft Access .MDB files directly. DAO can also read installable ISAM databases directly (dBASE, Paradox and FoxPro) if they are used as attached tables. Other databases, such as Oracle and SQL Server, can be read by using ODBC and their corresponding drivers.

Suggested Reading

  • CDaoDatabase, Microsoft Foundation Class Reference

  • DAO Database, Visual C++ Programmer’s Guide

  • DAO External: Working with External Data Sources, Visual C++ Programmer’s Guide


Tabledef

The Tabledef object represents the schema (structure) of a table. The DAO database classes let you create and manipulate tables in a database using the Data Manipulation Language (DML) subset of SQL. You can use the DAO database classes to create and delete fields (columns) in a table and to create and delete indexes for a table.

Suggested Reading

  • CDaoTableDef, Microsoft Foundation Class Reference

  • DAO Tabledef, Visual C++ Programmer’s Guide


Querydef

Most of the applications you create require only a subset of database records. The Querydef object represents a query you create and execute, or use to create a recordset, filtering and sorting records as desired. You can also create and execute action queries and SQL pass-through queries.

Suggested Reading

  • CDaoQueryDef, Microsoft Foundation Class Reference

  • DAO Querydef, Visual C++ Programmer’s Guide

  • DAO Querydef: Action Queries and SQL Pass-Through Queries, Visual C++ Programmer’s Guide


Recordset

A Recordset object represents a set of records selected from a data source. The recordset may represent a selection of one or more specified columns from rows of one or more database tables. A CDaoRecordset object represents both (a) this selection of records and (b) the actual field values for one currently selected record.

Suggested Reading

  • CDaoRecordset, Microsoft Foundation Class Reference

  • DAO Recordset, Visual C++ Programmer’s Guide

  • DAO Recordset: Recordset Navigation, Visual C++ Programmer’s Guide

  • DAO Record Field Exchange (DFX), Visual C++ Programmer’s Guide

  • DAO Record Field Exchange: Double Buffering Records, Visual C++ Programmer’s Guide


Exceptions

DAO error handling is accomplished through the use of MFC exceptions.

Suggested Reading

  • CDaoException, Microsoft Foundation Class Reference

  • Exceptions: Database Exceptions, Visual C++ Programmer’s Guide


DaoEnrol Step 1

Step 1 of the DaoEnrol tutorial implements an updatable database form that lets the user examine the records in the Section table one record at a time. You’ll create a form that looks like the one shown in the following figure.

DaoEnrol’s Section Form

In Step 1 you will:

  • Create the DaoEnrol application.

  • Examine the DaoEnrol Step 1 classes.

  • Customize DaoEnrol’s database form.

  • Bind DaoEnrol’s form controls to recordset fields.

  • Build and run DaoEnrol Step 1.

At the end of this step, you will be able to build and run the DaoEnrol Step 1 application.


Creating a New DAO Database Application

When you use AppWizard to create a DAO database application, the only database type offered as a selection is the Microsoft Access .MDB database. Using either Access data in an .MDB file or an installable ISAM database as an attached table to an .MDB file gives your application the best performance for data retrieval and manipulation. The procedures in this tutorial assume that you are using the StdReg32.mdb file supplied on the Visual C++ distribution CD.

Suggested Reading

  • Overview: Creating a Program That Supports a Database, Visual C++ Programmer’s Guide

  • Serialization: Serialization vs. Database Input/Output, Visual C++ Programmer’s Guide

  • Visual C++ Wizards that Help You Begin Your Program, Visual C++ Programmer’s Guide

  • Overview: ClassView, Visual C++ User’s Guide

AppWizard lets you specify whether your database application uses a file as well as a database. The DaoEnrol application does not need a file, so it is based on the Database view without file support option in AppWizard.

Note The following procedure describes how to enter the correct values in the AppWizard Steps to create the DaoEnrol application. Many of the Steps contain choices that you will not use to create the starter files for DaoEnrol.

To create the tutorial database application

  1. From the File menu, click New.

    The New dialog box appears.

  2. Click the Projects tab.

  3. Fill in or select the remaining boxes as follows:
    • In the Type box, select MFC AppWizard (exe).

    • In the Project Name box, type DAOEnrol.

      In the Location box, type the name of the directory in which you want your project files stored. AppWizard creates this directory if it doesn't exist.

  4. Click OK.

    AppWizard creates the project directory, and the MFC AppWizard – Step 1 dialog box appears.

  5. Click the Single document radio button, and then click Next.

  6. In the MFC AppWizard – Step 2 of 6 dialog box:
    • Click Database view without file support.

      When you create a database application without file support, AppWizard always creates it as an SDI application.

      This enables the Data Source button.

    • Click Data Source.

      The Database Options dialog box appears.

    • Click DAO and, from the drop-down list box, navigate to and select the Microsoft Access file StdReg32.mdb, then click OK.

    • AppWizard uses Dynaset as the default recordset type. The check box Detect dirty columns is checked by default. Click OK. The Select Database Tables dialog box appears.

    • Select the table name Section, and click OK to return to the MFC AppWizard – Step 2 of 6 dialog box.
  7. Click Next to proceed to the rest of the AppWizard Steps.

  8. Click Next in AppWizard for Steps 3, 4, and 5 to accept the default options.

    In the MFC AppWizard – Step 6 of 6 dialog box, you can check and, if necessary, modify the default names that AppWizard creates for your program’s classes and files.

    Note By default, AppWizard bases the names of classes on the project name you supply. This naming is probably fine if your application has only one recordset/record view pair. If your application has multiple recordsets and record views, it’s a good idea to change the name of the first recordset/record view pair created by AppWizard so the naming better reflects the name of the table in the data source. For DaoEnrol, you’ll modify two class names and their related header and implementation filenames, even though the tutorial uses only one recordset/record view pair.

  9. In MFC AppWizard – Step 6 of 6, make the following changes to class names:
    • Select the class CDaoEnrolDoc, and change it as follows:

      In the Header file box, change the header filename to DenrlDoc.h.

      In the Implementation file box, change the name to DenrlDoc.cpp.

    • Select the class CDaoEnrolView, and change it as follows:

      In the Class name box, change its name to CSectionForm.

      In the Header file box, change the header filename to SectForm.h.

      In the Implementation file box, change the name to SectForm.cpp.

      The base class is CDaoRecordView.

    • Select the class CDaoEnrolSet, and change it as follows

      In the Class name box, change its name to CSectionSet.

      In the Header file box, change the header filename to SectSet.h.

      In the Implementation file box, change the name to SectSet.cpp.

      The base class is CDaoRecordset. The edit item is disabled to show that you can’t change it.

  10. Click Finish.

    The New Project Information dialog box appears, summarizing the settings and features AppWizard will generate for you when it creates your project.

    You might want to take a moment to examine the application type, classes, and features that AppWizard automatically provides.

  11. Click OK in the New Project Information dialog box.

    AppWizard creates all necessary files and opens the project.

Use ClassView to view the classes that AppWizard just created.


Examining the DaoEnrol Step 1 Classes

Once AppWizard creates the DaoEnrol project, you can use ClassView to examine the classes. The following classes are discussed in-depth in this lesson:

  • CSectionSet

  • CSectionForm

  • CDaoEnrolDoc

You can also use ClassView to examine the default member functions and any member variable bindings AppWizard created.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Using ClassWizard

  • ClassWizard: Binding Recordset Fields to Table Columns


The CSectionSet Recordset Class (DAO)

The following procedure describes how to view the new recordset class, CSectionSet. After examining CSectionSet, you’ll use the text editor to examine the source files for classes CSectionForm and CDaoEnrolDoc.

To examine the new recordset class

  1. In ClassView, expand the DaoEnrol folder.

    Notice the set of classes AppWizard created for you automatically to support the DaoEnrol application.

  2. From the list of classes, expand CSectionSet.

    ClassView displays all the member variables that AppWizard created, including a variable for each of the Section table’s columns. You can use ClassWizard to view how AppWizard has bound the Section table’s columns to these member variables.

  3. From the View menu, click ClassWizard.

  4. Click the Member Variables tab.

  5. In the Class name box, select CSectionSet.

    What you see in this tab corresponds to the variables displayed in ClassView. AppWizard has bound all of the table’s columns to member variables of the CSectionSet class. These member variables are called “field data members.” AppWizard names the data members automatically, based on the column names from the data source. AppWizard also assigns the correct C++ or class library data type to the data members, based on the column type. In this example, all of the columns are text columns, mapped to type CString, except the Capacity column, which is a short.

  6. Once you’ve finished examining the CSectionSet recordset class, click Cancel to exit ClassWizard.

The following figure shows the ClassWizard Column Names list.

Table Columns Mapped to Recordset Data Members

If you don’t want all of a table’s columns bound to your recordset, you can delete the recordset field data members for those columns you don’t want by selecting the data member and clicking the Delete Variable button. For the tutorial, you will need them all.

Caution Don’t delete any fields that are part of the table’s primary key (in this case, the SectionNo and CourseID fields).

To change the name of a field data member, delete the member and add it again with the new name.


The CSectionForm Record View Class (DAO)

CSectionForm is derived from CDaoRecordView, which is one of the MFC record view classes. A record view is a specialized class that uses controls laid out in a dialog template resource to view and/or edit the fields of a recordset in a dialog-like form. A CDaoRecordView object is associated with both (a) a recordset object and (b) a dialog template resource. The dialog template resource has an ID of the form IDD_XXX_FORM, where XXX is based on the project name. CDaoRecordView derives its behavior from class CFormView. CDaoRecordView supports navigation through records, one record at a time, using commands in the associated CDaoRecordset object. When you update the value in a control on the form and navigate to another record, the corresponding recordset field is automatically updated.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Record Views

To examine the source code for the record view class

  • Use ClassView to jump to the OnInitialUpdate member function of class CSectionForm.

    SectForm.cpp opens in the text editor, with your cursor just before the OnInitialUpdate member function:

    void CSectionForm::OnInitialUpdate()
{
m_pSet = &GetDocument()->m_sectionSet;
CDaoRecordView::OnInitialUpdate();
}

    The base class framework function CDaoRecordView::OnInitialUpdate opens the database if it is not already open, then opens the recordset, and initializes the form by calling CFormView::OnInitialUpdate.

For now, the “form” represented by class CSectionForm is empty of controls. Later, in Customizing the Dialog Template for the DaoEnrol Section Form, you’ll use the dialog editor to design the form and to map controls on the form to the recordset.


The CDaoEnrolDoc Document Class

In ClassView, you can see that AppWizard created a class, CDaoEnrolDoc, derived from CDocument.

Suggested Reading

  • CDocument::UpdateAllViews, Microsoft Foundation Class Reference

  • Resource Editor Topics (Specific to Visual C++)

  • MFC: Using Database Classes with Documents and Views, Visual C++ Programmer’s Guide

  • Serialization: Serialization vs. Database Input/Output, Visual C++ Programmer’s Guide

  • Multiple Document Types, Views, and Frame Windows, Visual C++ Programmer’s Guide

  • Dialog Box Topics, Visual C++ Programmer’s Guide

To view the document class

  • In ClassView, double-click the icon for class CDaoEnrolDoc.

    This opens the file DenrlDoc.h in the text editor, placing the insertion point beside the CDaoEnrolDoc class declaration.

What is the document’s role in a database application? In most other applications, the document stores data and serializes it to a file on disk. Often the application reads the whole file into memory at once and writes it back to disk as a whole. In a database application, however, the data is stored in the database, and the user usually views the data as records. Such an application doesn’t need a file.

A document in a database application, then, isn’t normally used for its serialization support. So why does DaoEnrol have a document class?

The following code, at the beginning of DenrlDoc.h, reveals that the role of the document class in DaoEnrol is to own the recordset.

class CDaoEnrolDoc : public CDocument
{
   ...
   // Attributes
   public:
      CSectionSet m_sectionSet;
   ...
};

The recordset object, m_sectionSet, is embedded in the document object. Therefore, the recordset object is automatically constructed when the document object is constructed, and automatically deleted when the document object is deleted.

The document class can own any number of recordset objects in this way. For example, Step 4 of DaoEnrol adds a second form and corresponding recordset; the document embeds this second recordset.

In a sense, then, the document class is a proxy for the database. This approach isn’t strictly necessary, but if you (or AppWizard) design your database application to use the document class this way, you can better take advantage of the framework’s document/view architecture. For example, if you have multiple views (forms) simultaneously showing some of the contents of the database, you can take advantage of the CDocument::UpdateAllViews mechanism to conveniently notify all views about an update that might have been initiated in one of the views.

If you look at the menu resource that AppWizard created when you chose the option Database view without file support, you’ll see that there are no New, Open, Save, or Save As commands on the File menu. The File menu has only the Print, Print Preview, Print Setup, and Exit commands. If you had chosen Database view with file support, AppWizard would have supplied the missing File menu commands.

To view the DaoEnrol menu resource

  1. From ResourceView, expand the DaoEnrol folder.

    This displays the resource browser, which shows the resources associated with a project.

  2. In the resource browser, expand the Menu folder.

  3. Double-click IDR_MAINFRAME.

    The Menu editor opens, displaying the default menu that AppWizard created for the DaoEnrol application.

  4. Click the File menu item to view its structure. Notice the absence of New, Open, Save, and Save As.

  5. Close the Menu editor when you’re finished.

Note If you choose the option Database view with file support in AppWizard, the document class plays two roles. First, it serves as a proxy for the database. Second, it represents the file that is opened and saved via the New, Open, Save, and Save As commands on the File menu.


Customizing the Dialog Template for the DaoEnrol Section Form

Along with the classes, AppWizard creates a dialog template resource named IDD_DAOENROL_FORM, which the CDaoRecordView-derived class, CSectionForm, uses to display its form controls. Because CDaoRecordView is derived from CFormView, a record view’s client area is laid out by a dialog template resource. The layout of the form is up to you. AppWizard places one static text control on the dialog template resource, labeled “TODO: Place form controls on this dialog.”

Suggested Reading in the Visual C++ User’s Guide

  • Overview: Dialog Editor

In the following procedure, you’ll replace this text with controls that correspond to columns in the table (via the field data members of the recordset). The table below lists the columns and their associated edit control IDs and variable names.

DaoEnrol's Controls and Member Variables

Column name Control ID Variable Name
Course IDC_COURSE
m_pSet->m_CourseID
Section IDC_SECTION
m_pSet->m_SectionNo
Instructor IDC_INSTRUCTOR
m_pSet->m_InstructorID
Room IDC_ROOM
m_pSet->m_RoomNo
Schedule IDC_SCHEDULE
m_pSet->m_Schedule
Capacity IDC_CAPACITY
m_pSet->m_Capacity

To customize DaoEnrol’s form

  1. In the ResourceView, double-click the DaoEnrol folder.

  2. Expand the Dialog folder.

  3. Double-click IDD_DAOENROL_FORM.

    The dialog editor opens and displays the dialog box with the corresponding ID.

  4. Select and then delete the static control that says “TODO: Place form controls on this dialog.”

  5. Design DaoEnrol’s Section form to resemble the figure The Layout of DaoEnrol’s Section Form, using static controls and edit controls.

    Tip Press ALT+ENTER to display the Properties window, and then pin it down so it stays open while you add and edit the dialog box controls.

    Resize the dialog box as needed. You may want to add the controls in pairs, for example: static text control, then the corresponding edit control, and so on.

  6. For each edit control, use the ID box in the Properties window to specify an ID based on the table column names (for example, IDC_COURSE). This is only a convention, but it is used throughout the tutorial.

    Note The “Edit” caption that appears in each edit control is not visible to the user at run time, so you needn’t worry about deleting it. To view the dialog box as it will appear at run time, press CTRL+T to enter test mode for the dialog box. Press ESC to cancel test mode.

  7. Make the Course and Section edit controls read-only. To do so, select the Styles tab in the Properties window and set the Read-only check box. (The other edit controls are updatable.)

    According to a common rule in the user interface design of database forms, the user shouldn’t be able to update these key fields. If users want to change a course number or section of a Section record, they must delete the old Section record and add a new one to avoid possibly violating the referential integrity of the database. DaoEnrol Step 3 implements Add and Delete functionality.

  8. Save the DaoEnrol Resources (DaoEnrol.rc).

    It’s a good idea to periodically back up your work.

The Layout of DaoEnrol’s Section Form

If you did not add the static text and corresponding edit controls in order, one after the other, you need to change the tab order. You can easily check the current tab order and change it if necessary.

To view or change the tab order of controls

  1. With the dialog resource open, from the Layout menu click Tab Order.

    You’ll see numbers depicting the current tab order of the controls.

  2. Specify the tab order you want by clicking each control in that order.

    As you click, you’ll see the numbering change to reflect your choices.

    For DaoEnrol, specify a tab order such that each edit control is preceded in the tab order by the static text control that describes it. By specifying this tab order, you enable ClassWizard to derive a name for the edit control when you bind it to a data member, as you’ll do in the next topic.


Binding DaoEnrol's Controls to Recordset Fields

Binding DaoEnrol’s Controls to Recordset Fields

With the form designed, it’s time to indicate which edit controls map to which table columns — or, more precisely, which controls map to which recordset field data members. To perform this task, you use ClassWizard’s “foreign object” mechanism.

Suggested Reading

  • ClassWizard: Foreign Objects, Visual C++ Programmer’s Guide

  • Overview: Dialog Editor, Visual C++ User’s Guide

Normally, you use ClassWizard to bind controls in a dialog box or form to member variables of your CDialog- or CFormView-derived class. In the case of CDaoRecordView, though, you bind the form’s controls not to data members of the record view class but to data members of the recordset class associated with the record view.

Your CDaoRecordView-derived class — CSectionForm in this case — has a data member called m_pSet. You can view m_pSet in the ClassView of CSectionForm. This data member is a pointer to CSectionSet, DaoEnrol’s recordset class. Recall that you viewed this recordset class in the Member Variables tab of ClassWizard (see the figure Table Columns Mapped to Recordset Data Members).

The control bindings go through m_pSet to the corresponding field data members of CSectionSet. For example, in the following procedure, you will bind the Course edit control to:

m_pSet->m_CourseID

To bind a form control to a recordset data member

  1. If necessary, from ResourceView, expand the Dialog folder and double-click IDD_DAOENROL_FORM to open the dialog box inside the dialog editor.

  2. In the dialog editor window, hold down the CTRL key and double-click the Course edit control.

    ClassWizard’s Add Member Variable dialog box appears, with a proposed field name selected for you in the Member variable name box. ClassWizard chooses this name based on the caption of a static text control that falls previous to the edit control in the tab order. For example, for IDC_COURSE, the control’s caption is “Course,” and the Member variable name box should display:

    m_pSet->m_CourseID
  3. Click OK in the Add Member Variable dialog box to accept the name.

  4. Repeat steps 2 and 3 for each of the other edit controls on the form.

    It isn’t necessary to create mappings for the static text controls.

  5. Save your work.

Note Using CTRL+double-click in the dialog editor is a ClassWizard shortcut for mapping form controls to members of the associated dialog box, form view, or record view class. Use it on a button to create a command handler function for the button. Use it on other controls to create a class member variable.

You can view the complete mappings for class CSectionForm in the ClassWizard Member Variables tab. For example, where IDC_COURSE appears in the Control IDs column, you’ll see ->m_CourseID in the corresponding Member column.


Build and Run DaoEnrol Step 1

Build and run DaoEnrol Step 1. For information on building, see Build Scribble, Step 1 in Scribble Lesson 4.

When the CSectionSet recordset opens, it selects records from the Section table in the Student Registration database. The first record becomes the “current record” in the recordset. DaoEnrol’s database form displays the controls you designed, now filled with data from the current record.

Here are some things to try:

  • Take a look at the Record menu, which has First Record, Previous Record, Next Record, and Last Record commands. (The toolbar has buttons that correspond to the menu commands.) Try using the commands to scroll through the records in the recordset.

  • Try updating some of the fields. The new values are accepted into the database when you move to another record. As mentioned earlier, the key fields Course and Section are read-only.

    Important You must have write access to the database in order to update records. You can use Windows Explorer to view the database properties and, if necessary, clear the Read-Only check box.

When you finish, exit the program.

This completes Step 1 of DaoEnrol. To continue with this tutorial, see the next topic, Completing the DaoEnrol Tutorial.


Completing the DaoEnrol Tutorial

The procedures for creating the Enroll and DaoEnrol applications are identical except for a few differences in the implementation details of DAO. Because of this, you will be referred to Enroll Lesson 3 and Enroll Lesson 4 for most of the tasks, and DaoEnrol Step 2 and DaoEnrol Step 3 will only cover the DAO implementation differences.

  • DaoEnrol Step 2 and DaoEnrol Step 3 will guide you through the procedures you need to complete.

  • DaoEnrol Step 4 consists of a review of the files in the DAOENROL sample.


DaoEnrol Step 2

Lesson 3 of the Enroll tutorial, Using a Second Recordset, continues both the DaoEnrol and Enroll tutorials by showing you how to add a second recordset and use it to fill a combo box control on the form. Follow the topics in Lesson 3 of the Enroll tutorial listed below to complete Step 2 of DaoEnrol.

First, in the Enroll tutorial Lesson 3, complete these topics:

  • Using a Second Recordset

  • About Step 2

  • Changing the Course Control to a Combo Box

  • Binding the Combo Box Control to a Recordset Field and a CComboBox Variable

Next, complete the following DaoEnrol Step 2 topics, which vary slightly from those in the Enroll tutorial:

  • Creating a Recordset for the Course Table in DaoEnrol

    The steps for selecting a data source are different for a DAO-based database application.

  • Embedding the Recordset Object in the Document Object in DaoEnrol

    The names of files you use differ in DaoEnrol.

  • Filling the Combo Box in DaoEnrol

    A substitute code block for DaoEnrol is provided because the DAO database classes handle parameterizing a query differently than the ODBC database classes.

  • Filtering and Parameterizing the Recordset in DaoEnrol

    This topic does not add code to the tutorial, but provides details on how the DAO database classes handle parameterized queries.

  • Parameterizing the Filter in DaoEnrol

    Discussion

Finally, from the Enroll tutorial, Lesson 3, complete these topics:

  • Reusing a Database Object Opened by Another Recordset

  • Sorting the Recordset

  • Requerying the CSectionSet Recordset

Note The figures in Lesson 3 of the Enroll tutorial show “Enroll” in the title bar of the mainframe window. Your application will show “DaoEnrol”. Remember to substitute CDaoRecordset for CRecordset, CDaoRecordView for CRecordView, and so on.


Creating a Recordset for the Course Table in DaoEnrol

Note Before starting this topic, you should have already completed the following instructions in the Enroll tutorial, Lesson 3:

  • Using a Second Recordset

  • About Step 2

  • Changing the Course Control to a Combo Box

  • Binding the Combo Box Control to a Recordset Field and a CComboBox Variable

Suggested Reading in the Visual C++ Programmer’s Guide

  • Using ClassWizard

  • Reusing Code: Overview

  • ClassWizard: Creating a Recordset Class

DaoEnrol already has one recordset, for the Section table, which fills the controls on the CSectionForm record view with information about a single class section of the currently selected course name. Now you’ll add a second recordset, for the Course table, used to fill the combo box control with a list of all available course names.

To create a new recordset class

  1. From the View menu, click ClassWizard.

  2. Click Add Class and, from the menu, select New.

    This opens the New Class dialog box.

  3. Under Class Information, in the Name box, type CCourseSet.

  4. From the Base Class list, select CDaoRecordset.

  5. Click Change and shorten the filenames to CourseSe.h and CourseSe.cpp. Click OK.

  6. Click OK.

    This opens the Database Options dialog box.

To connect the recordset class to the Course table

  1. In the Datasource area, click the DAO radio button to activate the list. Click the browse button [...] next to the DAO list.

    This displays the Open dialog box.

  2. Navigate to the StdReg32.mdb file and select it. Click OK to return to the Database Options dialog box, then click OK again.

    The Select Database Tables dialog box opens.

  3. Select the table name “Course,” and click OK.

    This connects the table name to class CCourseSet and returns you to the ClassWizard Member Variables tab. The Class name box shows CCourseSet, and three names are listed in the Column Names box. The table below shows the column names, their data members, and their data types.

  4. Click OK to close ClassWizard.

Note On the Member Variables tab, you can see that all of the table’s columns are already assigned to field member variables. You can use ClassWizard to delete those variables if you don’t need to access or modify the columns — but be careful not to delete a field member variable for a column that is part of the table’s primary key.

CCourseSet Data Members

Column name Type Data member
CourseID CString
m_CourseID
CourseTitle CString
m_CourseTitle
Hours short
m_Hours


Embedding the Recordset Object in the Document Object in DaoEnrol

In DaoEnrol Step 1, AppWizard embedded the CSectionSet object in the document. In this step, you’ll do the same for the second recordset object — an object of the CCourseSet class that you created earlier with ClassWizard.

Suggested Reading in the Visual C++ User’s Guide

  • Reading the FileView and ClassView Pane

To embed the recordset in the document

  1. From FileView, open the Header Files folder and double-click the file DenrlDoc.h to open it.

  2. Declare an embedded CCourseSet object by adding the following line to the public Attributes section, just beneath the CSectionSet declaration:
    CCourseSet     m_courseSet;
  3. Similarly, expand the Source Files folder and open files DenrlDoc.cpp, Daoenrol.cpp, and SectForm.cpp, and add a #include directive for “CourseSe.h” before the existing #include directive for “DenrlDoc.h”, as shown in the following line:
    #include "CourseSe.h"
  4. Save DenrlDoc.h and the .cpp files.

The document’s m_courseSet member is referred to in the implementation of OnInitialUpdate that you’ll complete later.


Filling the Combo Box in DaoEnrol

A good place to fill the combo box with a list of course names is the CSectionForm override of CDaoRecordView’s OnInitialUpdate member function. As part of its own initialization, the form fills the combo box. The overall logic is as follows:

  1. Construct and open a CCourseSet recordset based on the Course table.

  2. Remove any current entries in the combo box.

  3. For each course name in CCourseSet, add the CourseID to the combo box.

  4. Set the selection to the first course name (as sorted) in the combo box.

Suggested Reading in the Visual C++ Programmer’s Guide

  • Record Views: Filling a List Box from a Second Recordset

The code in the following procedure fills the combo box and also filters, parameterizes, and sorts the CSectionSet recordset. Filtering, parameterization, and sorting are explained in topics that follow.

To fill the combo box

  1. Using ClassView, expand the view of CSectionForm.

  2. Double-click OnInitialUpdate to jump to its definition in SectForm.cpp.

  3. Just after the first line:
    m_pSet = &GetDocument()->m_sectionSet;

    add the code below (don’t replace any code):

    // Fill the combo box with all of the courses
CDaoEnrolDoc* pDoc = GetDocument();
pDoc->m_courseSet.m_strSort = "CourseID";

try
{
pDoc->m_courseSet.Open();
}
catch(CDaoException* e)
{
AfxMessageBox(e->
        m_pErrorInfo->m_strDescription);
e->Delete();
return;
}

// Filter, parameterize and sort the
// CSectionSet recordset

m_pSet->m_strFilter =
    "CourseID = CourseIDParam";
m_pSet->m_strCourseIDParam =
    pDoc->m_courseSet.m_CourseID;
m_pSet->m_strSort = "SectionNo";
m_pSet->m_pDatabase =
    pDoc->m_courseSet.m_pDatabase;

    Catch any exceptions so that errors are reported to the user. Unlike ODBC, DAO errors come directly from the underlying DAO object. Additional information about DAO errors can be found in the DAO SDK.

    You’ll add the necessary member declaration in the topic Parameterizing the Filter in DaoEnrol.

  4. Next, after the last line:
    CDaoRecordView::OnInitialUpdate();

    add the following code:

    m_ctlCourseList.ResetContent();
if (pDoc->m_courseSet.IsOpen())
{
while(!pDoc->m_courseSet.IsEOF())
{
m_ctlCourseList.AddString(
pDoc->m_courseSet.m_CourseID);
pDoc->m_courseSet.MoveNext();
}
}
m_ctlCourseList.SetCurSel(0);
  5. Save your work.


Filtering and Parameterizing the Recordset in DaoEnrol

The Step 1 version of DaoEnrol selects all of the records in the Section table into CSectionSet. In Step 2, only the class sections for a specific course name should be selected. The following topics introduce the concepts of recordset filters and parameters. In these topics, you will learn about:

  • Setting up a filter

  • Parameterizing a filter


Setting Up the Filter in DaoEnrol

Note You’ve already added the code to filter and parameterize the CSectionSet recordset (in OnInitialUpdate); the code in this topic is for illustrative purposes only. Do not add the code from this topic to your source files.

Suggested Reading in the Visual C++ Programmer’s Guide

  • DAO Queries: Filtering and Parameterizing Queries

A recordset filter determines what subset of records are selected from a table or query. To add a filter, you simply set the value of CDaoRecordset::m_strFilter before calling CDaoRecordset::Open. For example, the following code selects just the class section records for course MATH101:

m_pSet->m_strFilter = "CourseID = 'MATH101'";
m_pSet->Open();

Because the base class CDaoRecordView::OnInitialUpdate calls CDaoRecordset::Open, all you need to do to initially select the records for MATH101, for example, is replace the following AppWizard implementation of OnInitialUpdate:

void CSectionForm::OnInitialUpdate()
{
   m_pSet = &GetDocument()->m_sectionSet;
   CDaoRecordView::OnInitialUpdate();
}

with:

void CSectionForm::OnInitialUpdate()
{
   m_pSet = &GetDocument()->m_sectionSet;
   m_pSet->m_strFilter = "CourseID = 'MATH101'";
   CDaoRecordView::OnInitialUpdate();
}

The filter can be any logical expression that is legal for the SQL WHERE clause. For example, the following is legal:

m_pSet->m_strFilter =
            "CourseID = 'MATH101' AND InstructorID = 'ROGERSN'";

As an alternative, you can specify a complete SQL SELECT statement with a WHERE clause. In that case, you don’t use m_strFilter.

Examine the OnInitialUpdate code you added earlier in the procedure To fill the combo box. This code shows the filter for CSectionSet.


Parameterizing the Filter in DaoEnrol

DaoEnrol reselects, or “requeries,” class section records every time the user selects a new course name from the combo box. One way to implement this is to close the old CSectionSet object and reopen it, supplying a new m_strFilter value before calling Open. This works, but is somewhat inefficient, because the framework has to completely reconstruct and run a new SQL SELECT statement. A more efficient way to requery the same recordset is to “parameterize” the filter — call Requery with a new filter value and a specific parameter value.

Suggested Reading

  • CDaoRecordset::Open, Microsoft Foundation Class Reference

  • DAO Queries: Filtering and Parameterizing Queries, Visual C++ Programmer’s Guide

  • DAO Recordset: Binding Records Dynamically

In order to parameterize the filter, you’ll perform the following procedures:

  • Declare a parameter data member in the recordset’s header file.

  • Bind the parameter data member to the recordset.

To implement the Requery with a new filter and a specific parameter value supplied at run time, you:

  • Specify a parameterized filter.

  • Supply the run-time parameter value.

To declare a parameter data member in the recordset’s header file

  1. Using FileView, open the SectSet.h file.

  2. Add the following member variable declaration for m_strCourseIDParam, just before the //Overrides section, after the //}}AFX_FIELD line:
    CString m_strCourseIDParam;

To bind the parameter data member to the recordset

  1. Use ClassView to navigate to the CSectionSet constructor, and initialize the parameter count variable, m_nParams, which by default is zero. Also initialize DaoEnrol’s single parameter, m_strCourseIDParam.

    Place the following two lines of code after the line m_nDefaultType = dbOpenDynaset;:

    m_nParams = 1;
m_strCourseIDParam = "";
  2. Use ClassView to navigate to the DoFieldExchange member function definition, and add the following two lines of code to identify m_strCourseIDParam as a parameter data member. Add the code at the end of the function, after the //}}AFX_FIELD_MAP line.
    pFX->SetFieldType(CDaoFieldExchange::param);
DFX_Text(pFX, "CourseIDParam", m_strCourseIDParam);

    DoFieldExchange recognizes two kinds of fields: columns and parameters. The call to the CDaoFieldExchange member function SetFieldType indicates what kind of field(s) follow in the DFX function calls. In this example, there is one parameter: m_strCourseIDParam.

    The name of the column for the parameter in the DFX_Text call — “CourseIDParam” — is arbitrary; you can provide any name you want.

  3. Save your work.

Note No code is added during the next two procedures; the code already exists in the previous code block.

To specify a parameterized filter

  • Before the call to the base class function CDaoRecordset::Open, which is called by CDaoRecordView::OnInitialUpdate, specify the parameterized filter, as shown in this line (which you’ve already added):
    m_pSet->m_strFilter = "CourseID = CourseIDParam";

CourseID is a column (field) name, and CourseIDParam is a named parameter associated with the column. Its value will be substituted at run time. DAO parameters are always named, rather than positional, as in most ODBC code. If you have more than one parameter in m_strFilter, such as:

m_pSet->m_strFilter = "CourseID = CourseIDParam AND SectionNo = SectionNoParam";

you must make multiple RFX calls after the call to:

pFX->SetFieldType(CDaoFieldExchange::param);

To supply the run-time parameter value

  • Assign the value to the previously bound parameter data member, as shown in the following line (which you’ve already added in the OnInitialUpdate function).
    m_pSet->m_strCourseIDParam =
    pDoc->m_courseSet.m_CourseID;

    This sets the parameter value to be the first course record retrieved from the CCourseSet recordset. All parameter values must be assigned before calling CDaoRecordset::Open (or CDaoRecordView::OnInitialUpdate) or, as you will see later, before calling CDaoRecordset::Requery.

Note The technique just described for parameterizing a query is not the only approach available. The MFC DAO classes provide two alternative ways to manage recordsets, including any parameters you might give them. The approach described here relies on the wizards and uses the DAO record field exchange (DFX) mechanism to move data between the database and the recordset and to manage parameters. The alternative approach uses a different mechanism, called dynamic binding.


Finishing DaoEnrol Step 2

Use the remainder of the instructions in the Enroll tutorial to complete Step 2 of the DaoEnrol tutorial, as follows:

  • Reusing a Database Object Opened by Another Recordset

  • Sorting the Recordset

  • Requerying the CSectionSet Recordset

At the end of Step 2, compile and run the application.


DaoEnrol Step 3

In the Enroll tutorial Lesson 4, in Adding and Deleting Records, you will learn how to add and delete records. Follow the topics listed below to complete Step 3 of DaoEnrol.

In the Enroll tutorial, Lesson 4:

  • Adding and Deleting Records

  • About Step 3

  • Creating the Step 3 User Interface

  • Add an Accelerator for the Refresh Command

  • Create Handlers for Add, Refresh, and Delete

  • The Basics of Adding, Editing, and Deleting Records

  • Implementing the Add Command

Next, complete the following DaoEnrol Step 3 topics, which vary slightly from those in the Enroll tutorial:

  • Updating the Data Source with the Added Record in DaoEnrol

    The steps for updating a data source are different for a DAO-based database application.

  • Disabling Combo Box Logic in Add Mode in DaoEnrol

    The instructions here are identical, but are provided for your convenience.

  • Implementing the Delete Command in DaoEnrol

    Exception handling differs in the DAO database classes, so an appropriate code block is provided in this topic.

  • Implementing the Refresh Command in DaoEnrol

    CDaoRecordset are slightly different than those in CRecordset.

Note The figures in Lesson 4 show “Enroll” in the title bar of the mainframe window. Your application will show “DaoEnrol”. Remember to substitute CDaoRecordset for CRecordset and CDaoRecordView for CRecordView as appropriate.


Updating the Data Source with the Added Record in DaoEnrol

Note You should have completed the topics Adding and Deleting Records through Implementing the Add Command in Lesson 4 of the Enroll tutorial before starting this topic.

Add mode is completed when the user moves off the record. DaoEnrol implements this by overriding the CDaoRecordView::OnMove member function.

Suggested Reading

  • CDaoRecordView::OnMove, Microsoft Foundation Class Reference

  • Using WizardBar, Visual C++ Programmer’s Guide

To implement Add functionality in the OnMove function override

  1. From the View menu, click ClassWizard.

  2. Click the Message Maps tab, if necessary.

  3. In the Class name box, click CSectionForm.

  4. In the Object IDs list, select CSectionForm.

  5. In the Messages box, select OnMove.

  6. Click Add Function.

  7. Click Edit code to jump to the starter OnMove function, and replace the highlighted //TODO comment with the following code:
        if ( m_bAddMode )
    {
       if ( !UpdateData( ) )
          return FALSE;

       try
       {
          m_pSet->Update( );
       }
       catch( CDaoException* e )
       {
          AfxMessageBox( e->
             m_pErrorInfo->m_strDescription );
          e->Delete( );
          return FALSE;
       }

       m_pSet->Requery( );
       UpdateData( FALSE );
       m_ctlSection.SetReadOnly( TRUE );
       m_bAddMode = FALSE;
       return TRUE;
    }

    else
    {
       return CDaoRecordView::OnMove( nIDMoveCommand );
    }

Error handling in the DAO database classes differs from that of the ODBC database classes. The implementation of else in this code block requires a message box with error information to complete the if/else control structure.

In its default CDaoRecordView implementation, OnMove moves to the next, previous, first, or last record. If the application has changed the recordset field data members for the current record before the move, the framework updates the data source before moving to another record.

Normally, the Move commands behave as you might expect: MoveNext moves to the next record, and so on. But as a consequence of the decision to Requery during the Add operation, when the user chooses any move command when adding a record, DaoEnrol always effectively moves to the first record. That’s because requerying the recordset automatically sets the recordset to the first record.


Disabling Combo Box Logic in Add Mode in DaoEnrol

Step 2 implemented a handler for selecting a course in the combo box. The handler requeried the parameterized CSectionSet for the newly selected course. In Step 3, the combo box takes on the additional duty of allowing the user to specify the course for a new section record being added. During add mode, you don’t want to requery the recordset when the user selects a course from the combo box. Therefore, you need to put the requery logic inside an if clause that is executed only if add mode isn’t in effect.

Suggested Reading in the Visual C++ User’s Guide

Overview: ClassView

To disable normal combo box logic while in add mode

  1. Use ClassView to jump to the OnSelendokCourseList handler in class CSectionForm.

  2. Place an if block around the requery code in the OnSelendokCourseList handler, so the handler now appears as follows:
    void CSectionForm::OnSelendokCourselist()
{
if (!m_pSet->IsOpen() )
return;    m_ctlCourseList.GetLBText(m_ctlCourseList.GetCurSel(),
m_pSet->m_strCourseIDParam);
if (!m_bAddMode)
{
m_pSet->Requery();
if (m_pSet->IsEOF())
{
m_pSet->SetFieldNull(&(m_pSet->m_CourseID), FALSE);
m_pSet->m_CourseID = m_pSet->m_strCourseIDParam;
}
UpdateData(FALSE);
}
}


Implementing the Delete Command in DaoEnrol

In response to a Delete command, the record view deletes the current record by calling the Delete member function of its associated recordset.

Suggested Reading in the Visual C++ User’s Guide

Overview: ClassView

To implement the Delete command

  1. Use ClassView to jump to the OnRecordDelete starter function in class CSectionForm.

  2. Implement the handler with the following code:
    try
{
m_pSet->Delete();
}
catch(CDaoException* e)
{
AfxMessageBox(e->
m_pErrorInfo->m_strDescription);
e->Delete();
}

// Move to the next record after the one just deleted
m_pSet->MoveNext();

// If we moved off the end of file, move back to last record
if (m_pSet->IsEOF())
m_pSet->MoveLast();

// If the recordset is now empty, clear the fields left over
// from the deleted record
if (m_pSet->IsBOF())
m_pSet->SetFieldNull(NULL);
UpdateData(FALSE);

Catch any exceptions thrown by the recordset’s Delete function so that errors are reported to the user. The CDaoException data member m_pErrorInfo retrieves fairly user-friendly error messages prepared by the underlying CDAOException object.

For the DaoEnrol sample and tutorial, the decision was made to move to the record following the deleted record. You could move to the previous record after a delete operation or anywhere else as long as you, or the user, moves off the deleted record.


Implementing the Refresh Command in DaoEnrol

The Refresh command cancels add mode, if the user had previously clicked Add, or it discards any changes the user may have made on the form for the current record. In the first case, DaoEnrol cancels add mode by calling:

CDaoRecordset::Move(0);

When you call AddNew to begin the add operation, the framework stores a copy of the current record’s fields before allowing the user to enter new values in the record view’s controls. Calling Move as shown here “refreshes” the current record — and effectively cancels the add operation. It restores the record that was current before add mode began. This also works if you called Edit instead of AddNew.

When the user cancels add mode, DaoEnrol makes the Section control read-only again, for reasons explained earlier.

Suggested Reading in the Visual C++ User’s Guide

Overview: ClassView

To implement the Refresh command

  1. Use ClassView to jump to the OnRecordRefresh starter handler in class CSectionForm.

  2. Implement the handler function with the following code:
    if (m_bAddMode)
{
m_pSet->CancelUpdate();
m_pSet->Move(0);
m_ctlSection.SetReadOnly(TRUE);
m_bAddMode = FALSE;
}
// Copy fields from recordset to form, thus
// overwriting any changes the user may have made
// on the form
UpdateData(FALSE);

You are now ready to build and run Step 3 of DaoEnrol. Try the new Add, Refresh, and Delete commands. Try forcing the two exceptions handled by DaoEnrol — deleting a section that has Enrollment records, or adding a duplicate section.

This concludes the Step 3 of the DaoEnrol tutorial.


DaoEnrol Step 4: The DAOENROL Sample

The sample DAOENROL contains code that is very similar to the sample ENROLL.

Step 4 of the DAOEnrol tutorial is an exercise in reading and comparing source code. To complete Step 4 of the DaoEnrol tutorial:

  • Jump to the DAOENROL abstract (above) and, if you have not done so, copy the sample files to your hard drive.

  • Jump to the ENROLL sample (above) and, if you have not done so, click the jump STEP4 and copy the sample files to your hard drive.

  • Alternately open and examine the source code files for the two samples. Study the how the files vary to understand some of the differences in the implementation of the ODBC and DAO database classes.


Topic Not Found

No matching topic was found. Some possible reasons and remedies are as follows:

  • The file (.chm) that contains the destination topic is not in the expected location.

    • Remedy: Rerun the MSDN Library Setup.

  • The file (.chm) that contains the current topic or destination topic is out-of-date and the link is no longer valid.

    • Remedy: Install the latest version of the MSDN Library.

  • The MSDN Library CD is not in the CD-ROM drive.

    • Remedy: Insert the MSDN Library CD into the CD-ROM drive.


Topic Not Found

No matching topic was found. Some possible reasons and remedies are as follows:

  • The file (.chm) that contains the destination topic is not in the expected location.

    Remedy: Rerun the MSDN Library Setup.

  • The file (.chm) that contains the current topic or destination topic is out-of-date and the link is no longer valid.

    Remedy: Install the latest version of the MSDN Library.

  • The MSDN Library CD is not in the CD-ROM drive.

    Remedy: Insert the MSDN Library CD into the CD-ROM drive.