Join the discussion @ p2p.wrox.com
Wrox Programmer to Programmer™
Ivor Horton’s
Beginning
Visual C++ 2010 ®
Ivor Horton
Programmer to Programmer™
Get more out of wrox.com Interact
Join the Community
Take an active role online by participating in our P2P forums @ p2p.wrox.com
Sign up for our free monthly newsletter at newsletter.wrox.com
Wrox Online Library
Browse
Hundreds of our books are available online through Books24x7.com
Ready for more Wrox? We have books and e-books available on .NET, SQL Server, Java, XML, Visual Basic, C#/ C++, and much more!
Wrox Blox Download short informational pieces and code to keep you up to date and out of trouble!
Contact Us. We always like to get feedback from our readers. Have a book idea? Need community support? Let us know by e-mailing
[email protected] IVOR HORTON’S BEGINNING VISUAL C++® 2010 INTRODUCTION . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxxiii CHAPTER 1
Programming with Visual C++ 2010 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
CHAPTER 2
Data, Variables, and Calculations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
CHAPTER 3
Decisions and Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
CHAPTER 4
Arrays, Strings, and Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
CHAPTER 5
Introducing Structure into Your Programs . . . . . . . . . . . . . . . . . . . . . . . . 251
CHAPTER 6
More about Program Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
CHAPTER 7
Defining Your Own Data Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
CHAPTER 8
More on Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 435
CHAPTER 9
Class Inheritance and Virtual Functions . . . . . . . . . . . . . . . . . . . . . . . . . 549
CHAPTER 10
The Standard Template Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 645
CHAPTER 11
Debugging Techniques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 755
CHAPTER 12
Windows Programming Concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 807
CHAPTER 13
Programming for Multiple Cores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 843
CHAPTER 14
Windows Programming with the Microsoft Foundation Classes . . . . . 875
CHAPTER 15
Working with Menus and Toolbars . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 903
CHAPTER 16
Drawing in a Window. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 945
CHAPTER 17
Creating the Document and Improving the View . . . . . . . . . . . . . . . . . 1009
CHAPTER 18
Working with Dialogs and Controls . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1059
CHAPTER 19
Storing and Printing Documents . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1123
CHAPTER 20
Writing Your Own DLLs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1175
INDEX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1193
IVOR HORTON’S BEGINNING
Visual C++® 2010
IVOR HORTON’S BEGINNING
Visual C++® 2010 Ivor Horton
Ivor Horton’s Beginning Visual C++® 2010 Published by Wiley Publishing, Inc. 10475 Crosspoint Boulevard Indianapolis, IN 46256 www.wiley.com Copyright © 2010 by Ivor Horton Published by Wiley Publishing, Inc., Indianapolis, Indiana Published simultaneously in Canada ISBN: 978-0-470-50088-0 Manufactured in the United States of America 10 9 8 7 6 5 4 3 2 1 No part of this publication may be reproduced, stored in a retrieval system or transmitted in any form or by any means, electronic, mechanical, photocopying, recording, scanning or otherwise, except as permitted under Sections 107 or 108 of the 1976 United States Copyright Act, without either the prior written permission of the Publisher, or authorization through payment of the appropriate per-copy fee to the Copyright Clearance Center, 222 Rosewood Drive, Danvers, MA 01923, (978) 750-8400, fax (978) 646-8600. Requests to the Publisher for permission should be addressed to the Permissions Department, John Wiley & Sons, Inc., 111 River Street, Hoboken, NJ 07030, (201) 748-6011, fax (201) 748-6008, or online at http://www.wiley.com/go/permissions. Limit of Liability/Disclaimer of Warranty: The publisher and the author make no representations or warranties with respect to the accuracy or completeness of the contents of this work and specifically disclaim all warranties, including without limitation warranties of fitness for a particular purpose. No warranty may be created or extended by sales or promotional materials. The advice and strategies contained herein may not be suitable for every situation. This work is sold with the understanding that the publisher is not engaged in rendering legal, accounting, or other professional services. If professional assistance is required, the services of a competent professional person should be sought. Neither the publisher nor the author shall be liable for damages arising herefrom. The fact that an organization or Web site is referred to in this work as a citation and/or a potential source of further information does not mean that the author or the publisher endorses the information the organization or Web site may provide or recommendations it may make. Further, readers should be aware that Internet Web sites listed in this work may have changed or disappeared between when this work was written and when it is read. For general information on our other products and services please contact our Customer Care Department within the United States at (877) 762-2974, outside the United States at (317) 572-3993 or fax (317) 572-4002. Wiley also publishes its books in a variety of electronic formats. Some content that appears in print may not be available in electronic books. Library of Congress Control Number: 2010921242 Trademarks: Wiley, the Wiley logo, Wrox, the Wrox logo, Wrox Programmer to Programmer, and related trade dress are trademarks or registered trademarks of John Wiley & Sons, Inc. and/or its affi liates, in the United States and other countries, and may not be used without written permission. Visual C++ is a registered trademark of Microsoft Corporation in the United States and/or other countries. All other trademarks are the property of their respective owners. Wiley Publishing, Inc., is not associated with any product or vendor mentioned in this book.
This book is for Edward Gilbey, who arrived in September 2009 and has been a joy to have around ever since.
ABOUT THE AUTHOR
IVOR HORTON graduated as a mathematician and was lured into information technology by promises of great rewards for very little work. In spite of the reality usually being a great deal of work for relatively modest rewards, he has continued to work with computers to the present day. He has been engaged at various times in programming, systems design, consultancy, and the management and implementation of projects of considerable complexity.
Horton has many years of experience in the design and implementation of computer systems applied to engineering design and manufacturing operations in a variety of industries. He has considerable experience in developing occasionally useful applications in a wide variety of programming languages, and in teaching scientists and engineers primarily to do likewise. He has been writing books on programming for more than 15 years now, and his currently published works include tutorials on C, C++, and Java. At the present time, when he is not writing programming books or providing advice to others, he spends his time fishing, traveling, and enjoying life in general.
ABOUT THE TECHNICAL EDITOR
MARC GREGOIRE is a software engineer from Belgium. He graduated from the Catholic University
Leuven, Belgium, with a degree in “Burgerlijk ingenieur in de computer wetenschappen” (equivalent to Master of Science in Engineering in Computer Science). After that, he received the cum laude degree of Master in Artificial Intelligence at the same university, started working for a big software consultancy company (Ordina: http://www.ordina.be). His main expertise is C/C++, specifically Microsoft VC++ and the MFC framework. Next to C/C++, Marc also likes C# and uses PHP for creating web pages. In addition to his main interest for Windows development, he also has experience in developing C++ programs running 24x7 on Linux platforms, and in developing critical 2G and 3G software running on Solaris for big telecom operators. In April of the years 2007, 2008, and 2009 he received the Microsoft MVP (Most Valuable Professional) award for his Visual C++ expertise. Marc is an active member on the CodeGuru forum (as Marc G) and also wrote some articles and FAQ entries for CodeGuru. He creates freeware and shareware programs that are distributed through his website at www.nuonsoft.com and maintains a blog on www.nuonsoft.com/blog/.
CREDITS
EXECUTIVE EDITOR
Robert Elliott
VICE PRESIDENT AND EXECUTIVE GROUP PUBLISHER
Richard Swadley PROJECT EDITOR
John Sleeva
VICE PRESIDENT AND EXECUTIVE PUBLISHER
Barry Pruett TECHNICAL EDITOR
Marc Gregoire
ASSOCIATE PUBLISHER
Jim Minatel PRODUCTION EDITOR
Eric Charbonneau
PROJECT COORDINATOR, COVER
Lynsey Stanford COPY EDITOR
Sadie Kleinman
PROOFREADERS
Maraya Cornell and Paul Sagan, Word One EDITORIAL DIRECTOR
Robyn B. Siesky
INDEXER
Johnna VanHoose Dinse EDITORIAL MANAGER
Mary Beth Wakefield
COVER DESIGNER
Michael E. Trent MARKETING MANAGER
Ashley Zurcher
COVER IMAGE
©istockphoto PRODUCTION MANAGER
Tim Tate
ACKNOWLEDGMENTS
THE AUTHOR IS ONLY ONE MEMBER of the large team of people necessary to get a book into print. I’d like to thank the John Wiley & Sons and Wrox Press editorial and production teams for their help and support throughout.
I would particularly like to thank my technical editor, Marc Gregoire, for doing such an outstanding job of reviewing the text and checking out all the code fragments and examples in the book. His many constructive comments and suggestions for better ways of presenting the material has undoubtedly made the book a much better tutorial. As always, the love and support of my wife, Eve, has been fundamental to making it possible for me to write this book alongside my other activities. She provides for my every need and remains patient and cheerful in spite of the effect that my self-imposed workload has on family life.
CONTENTS
INTRODUCTION CHAPTER 1: PROGRAMMING WITH VISUAL C++ 2010
xxxiii 1
The .NET Framework The Common Language Runtime Writing C++ Applications Learning Windows Programming
2 2 3 5
Learning C++ The C++ Standards Attributes Console Applications Windows Programming Concepts
5 5 6 6 7
What Is the Integrated Development Environment? The Editor The Compiler The Linker The Libraries
Using the IDE Toolbar Options Dockable Toolbars Documentation Projects and Solutions Setting Options in Visual C++ 2010 Creating and Executing Windows Applications Creating a Windows Forms Application
Summary CHAPTER 2: DATA, VARIABLES, AND CALCULATIONS
The Structure of a C++ Program The main() Function Program Statements Whitespace Statement Blocks Automatically Generated Console Programs
9 9 10 10 10
10 12 12 13 13 27 28 31
32 35
36 44 44 46 47 47
CONTENTS
Defining Variables Naming Variables Declaring Variables Initial Values for Variables
Fundamental Data Types Integer Variables Character Data Types Integer Type Modifiers The Boolean Type Floating-Point Types Literals Defining Synonyms for Data Types Variables with Specific Sets of Values
49 50 51
52 52 53 55 56 56 58 59 59
Basic Input/Output Operations
61
Input from the Keyboard Output to the Command Line Formatting the Output Escape Sequences
61 62 63 64
Calculating in C++ The Assignment Statement Arithmetic Operations Calculating a Remainder Modifying a Variable The Increment and Decrement Operators The Sequence of Calculation
Type Conversion and Casting Type Conversion in Assignments Explicit Type Conversion Old-Style Casts
The Auto Keyword Discovering Types The Bitwise Operators The Bitwise AND The Bitwise OR The Bitwise Exclusive OR The Bitwise NOT The Bitwise Shift Operators
Introducing Lvalues and Rvalues Understanding Storage Duration and Scope Automatic Variables Positioning Variable Declarations
xviii
49
66 66 67 72 73 74 76
78 79 79 80
81 81 82 82 84 85 86 86
88 89 89 92
CONTENTS
Global Variables Static Variables
Namespaces Declaring a Namespace Multiple Namespaces
C++/CLI Programming C++/CLI Specific: Fundamental Data Types C++/CLI Output to the Command Line C++/CLI Specific — Formatting the Output C++/CLI Input from the Keyboard Using safe_cast C++/CLI Enumerations
Discovering C++/CLI Types Summary CHAPTER 3: DECISIONS AND LOOPS
Comparing Values The if Statement Nested if Statements Nested if-else Statements Logical Operators and Expressions The Conditional Operator The switch Statement Unconditional Branching
Repeating a Block of Statements What Is a Loop? Variations on the for Loop The while Loop The do-while Loop Nested Loops
C++/CLI Programming The for each Loop
Summary CHAPTER 4: ARRAYS, STRINGS, AND POINTERS
Handling Multiple Data Values of the Same Type Arrays Declaring Arrays Initializing Arrays Character Arrays and String Handling Multidimensional Arrays
92 96
96 97 99
100 101 105 106 109 110 111
116 116 121
121 123 124 128 130 133 135 139
139 139 142 150 152 154
157 161
163 167
168 168 169 172 174 177 xix
CONTENTS
Indirect Data Access What Is a Pointer? Declaring Pointers Using Pointers Initializing Pointers The sizeof Operator Constant Pointers and Pointers to Constants Pointers and Arrays
Dynamic Memory Allocation The Free Store, Alias the Heap The new and delete Operators Allocating Memory Dynamically for Arrays Dynamic Allocation of Multidimensional Arrays
Using References What Is a Reference? Declaring and Initializing Lvalue References Defining and Initializing Rvalue References
Native C++ Library Functions for Strings Finding the Length of a Null-Terminated String Joining Null-Terminated Strings Copying Null-Terminated Strings Comparing Null-Terminated Strings Searching Null-Terminated Strings
C++/CLI Programming Tracking Handles CLR Arrays Strings Tracking References Interior Pointers
Summary CHAPTER 5: INTRODUCING STRUCTURE INTO YOUR PROGRAMS
Understanding Functions Why Do You Need Functions? Structure of a Function Using a Function
Passing Arguments to a Function The Pass-by-value Mechanism Pointers as Arguments to a Function Passing Arrays to a Function References as Arguments to a Function
xx
180 181 181 182 183 190 192 194
201 201 202 203 206
206 207 207 208
208 209 210 211 212 213
215 216 217 233 244 244
247 251
252 253 253 256
259 260 262 263 267
CONTENTS
Use of the const Modifier Rvalue Reference Parameters Arguments to main() Accepting a Variable Number of Function Arguments
Returning Values from a Function Returning a Pointer Returning a Reference Static Variables in a Function
Recursive Function Calls Using Recursion
C++/CLI Programming Functions Accepting a Variable Number of Arguments Arguments to main()
Summary CHAPTER 6: MORE ABOUT PROGRAM STRUCTURE
Pointers to Functions Declaring Pointers to Functions A Pointer to a Function as an Argument Arrays of Pointers to Functions
Initializing Function Parameters Exceptions Throwing Exceptions Catching Exceptions Exception Handling in the MFC
Handling Memory Allocation Errors Function Overloading What Is Function Overloading? Reference Types and Overload Selection When to Overload Functions
Function Templates
270 271 273 275
277 277 280 283
285 288
289 289 290
292 295
295 296 299 301
302 303 305 306 307
308 310 310 313 313
314
Using a Function Template
314
Using the decltype Operator An Example Using Functions
317 318
Implementing a Calculator Eliminating Blanks from a String Evaluating an Expression Getting the Value of a Term Analyzing a Number Putting the Program Together Extending the Program
319 322 322 325 326 330 331
xxi
CONTENTS
Extracting a Substring Running the Modified Program
C++/CLI Programming Understanding Generic Functions A Calculator Program for the CLR
Summary CHAPTER 7: DEFINING YOUR OWN DATA TYPES
The struct in C++ What Is a struct? Defining a struct Initializing a struct Accessing the Members of a struct IntelliSense Assistance with Structures The struct RECT Using Pointers with a struct
Data Types, Objects, Classes, and Instances
336 337 343
349 353
354 354 354 355 355 359 360 361
363
First Class Operations on Classes Terminology
364 364 365
Understanding Classes
366
Defining a Class Declaring Objects of a Class Accessing the Data Members of a Class Member Functions of a Class Positioning a Member Function Definition Inline Functions
Class Constructors What Is a Constructor? The Default Constructor Assigning Default Parameter Values in a Class Using an Initialization List in a Constructor Making a Constructor Explicit
Private Members of a Class Accessing private Class Members The friend Functions of a Class The Default Copy Constructor
The Pointer this const Objects const Member Functions of a Class Member Function Definitions Outside the Class
xxii
333 336
366 367 367 370 372 372
374 374 376 378 381 381
382 385 386 388
390 393 393 394
CONTENTS
Arrays of Objects Static Members of a Class Static Data Members Static Function Members of a Class
Pointers and References to Class Objects Pointers to Objects References to Class Objects
C++/CLI Programming Defining Value Class Types Defining Reference Class Types Defining a Copy Constructor for a Reference Class Type Class Properties initonly Fields Static Constructors
Summary CHAPTER 8: MORE ON CLASSES
Class Destructors What Is a Destructor? The Default Destructor Destructors and Dynamic Memory Allocation
Implementing a Copy Constructor Sharing Memory Between Variables Defining Unions Anonymous Unions Unions in Classes and Structures
Operator Overloading Implementing an Overloaded Operator Implementing Full Support for a Comparison Operator Overloading the Assignment Operator Overloading the Addition Operator Overloading the Increment and Decrement Operators Overloading the Function Call Operator
The Object Copying Problem Avoiding Unnecessary Copy Operations Applying Rvalue Reference Parameters Named Objects are Lvalues
Class Templates Defining a Class Template Creating Objects from a Class Template Class Templates with Multiple Parameters Templates for Function Objects
395 397 397 401
401 401 404
406 407 412 415 416 429 431
432 435
435 436 436 438
442 444 444 446 446
446 447 450 454 459 463 465
466 466 470 472
477 478 481 483 486 xxiii
CONTENTS
Using Classes
486 487 487
Organizing Your Program Code
508
Naming Program Files
Native C++ Library Classes for Strings Creating String Objects Concatenating Strings Accessing and Modifying Strings Comparing Strings Searching Strings
C++/CLI Programming Overloading Operators in Value Classes Overloading the Increment and Decrement Operators Overloading Operators in Reference Classes Implementing the Assignment Operator for Reference Types
Summary CHAPTER 9: CLASS INHERITANCE AND VIRTUAL FUNCTIONS
509
510 510 512 516 520 523
533 534 540 540 543
544 549
Object-Oriented Programming Basics Inheritance in Classes
549 551
What Is a Base Class? Deriving Classes from a Base Class
551 552
Access Control Under Inheritance
555
Constructor Operation in a Derived Class Declaring Protected Class Members The Access Level of Inherited Class Members
The Copy Constructor in a Derived Class Class Members as Friends Friend Classes Limitations on Class Friendship
Virtual Functions What Is a Virtual Function? Using Pointers to Class Objects Using References with Virtual Functions Pure Virtual Functions Abstract Classes Indirect Base Classes Virtual Destructors
Casting Between Class Types xxiv
486
The Idea of a Class Interface Defining the Problem Implementing the CBox Class
558 562 565
566 569 571 572
572 574 576 578 580 581 584 586
590
CONTENTS
Nested Classes C++/CLI Programming Boxing and Unboxing Inheritance in C++/CLI Classes Interface Classes Defining Interface Classes Classes and Assemblies Functions Specified as new Delegates and Events Destructors and Finalizers in Reference Classes Generic Classes
Summary CHAPTER 10: THE STANDARD TEMPLATE LIBRARY
What Is the Standard Template Library?
590 594 594 595 602 602 606 611 612 625 628
638 645
645
Containers Container Adapters Iterators Algorithms Function Objects in the STL Function Adapters
646 647 648 649 650 650
The Range of STL Containers Sequence Containers
651 651
Creating Vector Containers The Capacity and Size of a Vector Container Accessing the Elements in a Vector Inserting and Deleting Elements in a Vector Storing Class Objects in a Vector Sorting Vector Elements Storing Pointers in a Vector Double-Ended Queue Containers Using List Containers Using Other Sequence Containers
Associative Containers Using Map Containers Using a Multimap Container
More on Iterators Using Input Stream Iterators Using Inserter Iterators Using Output Stream Iterators
More on Function Objects
652 655 660 661 663 668 669 671 675 685
701 702 714
715 715 719 720
723 xxv
CONTENTS
More on Algorithms fill() replace() find() transform()
Lambda Expressions The Capture Clause Capturing Specific Variables Templates and Lambda Expressions Wrapping a Lambda Expression
The STL for C++/CLI Programs
725 725 725 726
727 728 730 730 734
736
STL/CLR Containers Using Sequence Containers Using Associative Containers
737 737 745
Lambda Expressions in C++/CLI Summary
752 752
CHAPTER 11: DEBUGGING TECHNIQUES
Understanding Debugging Program Bugs Common Bugs
Basic Debugging Operations Setting Breakpoints Setting Tracepoints Starting Debugging Changing the Value of a Variable
Adding Debugging Code Using Assertions Adding Your Own Debugging Code
755
756 757 758
759 761 763 763 767
767 768 769
Debugging a Program
775
The Call Stack Step Over to the Error
775 777
Testing the Extended Class Finding the Next Bug
Debugging Dynamic Memory Functions for Checking the Free Store Controlling Free Store Debug Operations Free Store Debugging Output
Debugging C++/CLI Programs Using the Debug and Trace Classes Getting Trace Output in Windows Forms Applications
Summary xxvi
724
780 783
783 784 785 786
793 793 803
804
CONTENTS
CHAPTER 12: WINDOWS PROGRAMMING CONCEPTS
Windows Programming Basics Elements of a Window Windows Programs and the Operating System Event-Driven Programs Windows Messages The Windows API Windows Data Types Notation in Windows Programs
The Structure of a Windows Program The WinMain() Function Message Processing Functions A Simple Windows Program
807
808 808 810 811 811 811 812 813
814 815 827 833
Windows Program Organization The Microsoft Foundation Classes
834 835
MFC Notation How an MFC Program Is Structured
836 836
Using Windows Forms Summary CHAPTER 13: PROGRAMMING FOR MULTIPLE CORES
840 841 843
Parallel Processing Basics Introducing the Parallel Patterns Library Algorithms for Parallel Processing
843 844 844
Using the parallel_for Algorithm Using the parallel_for_each Algorithm Using the parallel_invoke Algorithm
845 846 849
A Real Parallel Problem Critical Sections Using critical_section Objects Locking and Unlocking a Section of Code
The combinable Class Template Tasks and Task Groups Summary CHAPTER 14: WINDOWS PROGRAMMING WITH THE MICROSOFT FOUNDATION CLASSES
The Document/View Concept in MFC What Is a Document? Document Interfaces What Is a View?
850 864 865 865
867 869 873 875
876 876 876 877 xxvii
CONTENTS
Linking a Document and Its Views Your Application and MFC
Creating MFC Applications Creating an SDI Application MFC Application Wizard Output Creating an MDI Application
Summary CHAPTER 15: WORKING WITH MENUS AND TOOLBARS
Communicating with Windows Understanding Message Maps Message Categories Handling Messages in Your Program
Extending the Sketcher Program Elements of a Menu Creating and Editing Menu Resources
Adding Handlers for Menu Messages Choosing a Class to Handle Menu Messages Creating Menu Message Functions Coding Menu Message Functions Adding Message Handlers to Update the User Interface
Adding Toolbar Buttons Editing Toolbar Button Properties Exercising the Toolbar Buttons Adding Tooltips
Menus and Toolbars in a C++/CLI Program Understanding Windows Forms Understanding Windows Forms Applications Adding a Menu to CLR Sketcher Adding Event Handlers for Menu Items Implementing Event Handlers Setting Menu Item Checks Adding a Toolbar
Summary CHAPTER 16: DRAWING IN A WINDOW
Basics of Drawing in a Window The Window Client Area The Windows Graphical Device Interface
xxviii
878 879
880 882 886 897
899 903
903 904 907 908
909 910 910
913 914 915 916 920
924 925 926 927
928 928 929 932 935 936 937 938
942 945
945 946 946
CONTENTS
The Drawing Mechanism in Visual C++ The View Class in Your Application The CDC Class
948 949 950
Drawing Graphics in Practice Programming for the Mouse
959 961
Messages from the Mouse Mouse Message Handlers Drawing Using the Mouse
962 963 965
Exercising Sketcher Running the Example Capturing Mouse Messages
Drawing with the CLR Drawing on a Form Adding Mouse Event Handlers Defining C++/CLI Element Classes Implementing the MouseMove Event Handler Implementing the MouseUp Event Handler Implementing the Paint Event Handler for the Form
Summary CHAPTER 17: CREATING THE DOCUMENT AND IMPROVING THE VIEW
Creating the Sketch Document Using a list Container for the Sketch
Improving the View Updating Multiple Views Scrolling Views Using MM_LOENGLISH Mapping Mode
Deleting and Moving Shapes Implementing a Context Menu
990 991 992
993 993 994 996 1004 1005 1006
1007 1009
1009 1010
1014 1014 1016 1021
1022 1022
Associating a Menu with a Class Exercising the Pop-Ups Highlighting Elements Servicing the Menu Messages
1023 1026 1026 1030
Dealing with Masked Elements Extending CLR Sketcher
1038 1039
Coordinate System Transformations Defining a Sketch Class Drawing the Sketch in the Paint Event Handler Implementing Element Highlighting Creating Context Menus
Summary
1039 1042 1044 1044 1050
1056 xxix
CONTENTS
CHAPTER 18: WORKING WITH DIALOGS AND CONTROLS
Understanding Dialogs Understanding Controls Creating a Dialog Resource Adding Controls to a Dialog Box Testing the Dialog
Programming for a Dialog
1059 1060 1061 1062 1063
1063
Adding a Dialog Class Modal and Modeless Dialogs Displaying a Dialog
1063 1064 1065
Supporting the Dialog Controls
1067
Initializing the Controls Handling Radio Button Messages
Completing Dialog Operations Adding Pen Widths to the Document Adding Pen Widths to the Elements Creating Elements in the View Exercising the Dialog
Using a Spin Button Control Adding the Scale Menu Item and Toolbar Button Creating the Spin Button Generating the Scale Dialog Class Displaying the Spin Button
Using the Scale Factor Scalable Mapping Modes Setting the Document Size Setting the Mapping Mode Implementing Scrolling with Scaling
Using the CTaskDialog Class Displaying a Task Dialog Creating CTaskDialog Objects
Working with Status Bars Adding a Status Bar to a Frame
Using a List Box Removing the Scale Dialog Creating a List Box Control
Using an Edit Box Control Creating an Edit Box Resource Creating the Dialog Class Adding the Text Menu Item Defining a Text Element Implementing the CText Class xxx
1059
1068 1069
1069 1070 1070 1071 1072
1072 1073 1073 1074 1077
1078 1078 1080 1080 1082
1084 1084 1086
1089 1089
1093 1093 1094
1096 1096 1097 1099 1100 1100
CONTENTS
Dialogs and Controls in CLR Sketcher Adding a Dialog Creating Text Elements
Summary CHAPTER 19: STORING AND PRINTING DOCUMENTS
Understanding Serialization Serializing a Document Serialization in the Document Class Definition Serialization in the Document Class Implementation Functionality of CObject-Based Classes How Serialization Works How to Implement Serialization for a Class
Applying Serialization Recording Document Changes Serializing the Document Serializing the Element Classes
Exercising Serialization Printing a Document The Printing Process
Implementing Multipage Printing Getting the Overall Document Size Storing Print Data Preparing to Print Cleaning Up after Printing Preparing the Device Context Printing the Document Getting a Printout of the Document
Serialization and Printing in CLR Sketcher Understanding Binary Serialization Serializing a Sketch Printing a Sketch
Summary CHAPTER 20: WRITING YOUR OWN DLLs
1105 1106 1112
1120 1123
1123 1124 1124 1125 1128 1129 1131
1131 1131 1133 1135
1139 1140 1141
1144 1145 1146 1147 1148 1149 1150 1154
1155 1155 1160 1171
1172 1175
Understanding DLLs
1175
How DLLs Work Contents of a DLL DLL Varieties
1177 1180 1180
xxxi
CONTENTS
Deciding What to Put in a DLL Writing DLLs Writing and Using an Extension DLL
Summary INDEX
xxxii
1181 1182 1182
1190 1193
INTRODUCTION
WELCOME TO IVOR HORTON ’ S BEGINNING VISUAL C++ 2010 . With this book, you can become an
effective C++ programmer using Microsoft’s latest application-development system. I aim to teach you the C++ programming language, and then how to apply C++ in the development of your own Windows applications. Along the way, you will also learn about many of the exciting new capabilities introduced by this latest version of Visual C++, including how you can make full use of multi- core processors in your programs.
PROGRAMMING IN C++ Visual C++ 2010 supports two distinct, but closely related flavors of the C++ language, ISO/IEC standard C++ (which I will refer to as native C++) and C++/CLI. Although native C++ is the language of choice for many professional developers, especially when performance is the primary consideration, the speed and ease of development that C++/CLI and the Windows Forms capability bring to the table make that essential, too. This is why I cover both flavors of C++ in depth in this book. Visual C++ 2010 fully supports the original ISO/IEC standard C++ language, with the added advantage of some powerful new native C++ language features from the upcoming ISO/IEC standard for C++. This is comprehensively covered in this book, including the new language features. Visual C++ 2010 also supports C++/CLI, which is a dialect of C++ that was developed by Microsoft as an extension to native C++. The idea behind C++/CLI is to add features to native C++ that allow you to develop applications that target the virtual machine environment supported by .NET. This adds C++ to the other languages such as BASIC and C# that can use the .NET Framework. The C++/CLI language is now an ECMA standard alongside the CLI standard that defi nes the virtual machine environment for .NET. The two versions of C++ available with Visual C++ 2010 are complementary and fulfill different roles. ISO/IEC C++ is there for the development of high-performance applications that run natively on your computer, whereas C++/CLI has been developed specifically for writing applications that target the .NET Framework. Once you have learned the essentials of how you write applications in both versions of C++, you will be able to make full use of the versatility and scope of Visual C++ 2010.
DEVELOPING WINDOWS APPLICATIONS With a good understanding of C++, you are well placed to launch into Windows application development. The Microsoft Foundation Classes (MFC) encapsulate the Windows API to provide comprehensive, easy-to -use capabilities for developing high-performance Windows applications in native C++.
INTRODUCTION
You get quite a lot of assistance from automatically generated code when writing native C++ programs, but you still need to write a lot of C++ yourself. You need a solid understanding of object- oriented programming (OOP) techniques, as well as a good appreciation of what’s involved in programming for Windows. You will learn all that you need using this book. C++/CLI targets the .NET Framework and is the vehicle for the development of Windows Forms applications. With Windows Forms, you can create much, if not all of the user interface required for an application interactively, without writing a single line of code. Of course, you still must customize a Windows Forms application to make it do what you want, but the time required for development is a fraction of what you would need to create the application using native C++. Even though the amount of customizing code you have to add may be a relatively small proportion of the total, you still need an in-depth knowledge of the C++/CLI language to do it successfully. This book aims to provide you with that.
ADVANCED LIBRARY CAPABILITIES The Parallel Patterns Library (PPL) is an exciting new capability introduced in Visual C++ 2010 that makes it easy to program for using multiple processors. Programming for multiple processors has been difficult in the past, but the PPL really does make it easy. I’ll show you the various ways in which you can use the PPL to speed up your compute-intensive applications.
WHO THIS BOOK IS FOR This book is for anyone who wants to learn how to write C++ applications for the Microsoft Windows operating system using Visual C++ 2010. I make no assumptions about prior knowledge of any particular programming language, so there are no prerequisites other than some aptitude for programming and sufficient enthusiasm and commitment for learning to program in C++ to make it through this book. This tutorial is for you if:
xxxiv
➤
You are a newcomer to programming and sufficiently keen to jump into the deep end with C++. To be successful, you need to have at least a rough idea of how your computer works, including the way in which the memory is organized and how data and instructions are stored.
➤
You have a little experience of programming in some other language, such as BASIC, and you are keen to learn C++ and develop practical Microsoft Windows programming skills.
➤
You have some experience in C or C++, but not in a Microsoft Windows context and want to extend your skills to program for the Windows environment using the latest tools and technologies.
➤
You have some knowledge of C++ and you want to extend your C++ skills to include C++/CLI.
INTRODUCTION
WHAT THIS BOOK COVERS Essentially, this book covers two broad topics: the C++ programming language, and Windows application programming using either MFC or the .NET Framework. Before you can develop fullyfledged Windows applications, you need to have a good level of knowledge of C++, so the C++ tutorial comes fi rst. The first part of the book teaches you the essentials of C++ programming using both of the C++ language technologies supported by Visual C++ 2010, through a detailed, step-by-step tutorial on both flavors of the C++ language. You’ll learn the syntax and use of the native ISO/IEC C++ language and gain experience and confidence in applying it in a practical context through an extensive range of working examples. There are also exercises that you can use to test your knowledge, with solutions available for download if you get stuck. You’ll learn C++/CLI as an extension to native C++, again through working examples that illustrate how each language feature works. Of course, the language tutorial also introduces and demonstrates the use of the C++ standard library facilities you are most likely to need. You’ll add to your knowledge of the standard libraries incrementally as you progress through the C++ language. Additionally, you will learn about the powerful tools provided by the Standard Template Library (STL) in both its forms: the native C++ version and the C++/CLI version. There is also a chapter dedicated to the new Parallel Patterns Library that enables you to harness the power of the multi- core processing capabilities of your PC for computationally intensive applications. Once you are confident in applying C++, you move on to Windows programming. You will learn how to develop native Windows applications using the MFC by creating a substantial working application of more than 2000 lines of code. You develop the application over several chapters, utilizing a broad range of user interface capabilities provided by the MFC. To learn Windows programming with C++/CLI, you develop a Windows Forms application with similar functionality user interface features to the native C++ application.
HOW THIS BOOK IS STRUCTURED The book is structured as follows: ➤
Chapter 1 introduces you to the basic concepts you need to understand for programming in C++, for native applications and for .NET Framework applications, together with the main ideas embodied in the Visual C++ 2010 development environment. It describes how you use the capabilities of Visual C++ 2010 for creating the various kinds of C++ applications you learn about in the rest of the book.
➤
Chapters 2 through 9 teach you both versions of the C++ language. The content of each of Chapters 2 through 9 is structured in a similar way; the fi rst part of each chapter deals with native C++ language elements, and the second part deals with how the same capabilities are provided in C++/CLI.
xxxv
INTRODUCTION
➤
Chapter 10 teaches you how you use the STL, which is a powerful and extensive set of tools for organizing and manipulating data in your native C++ programs. The STL is applicationneutral, so you will be able to apply it in a wide range of contexts. Chapter 10 also teaches you the STL/CLR, which is new in Visual C++ 2010. This is a version of the STL for use in C++/CLI applications.
➤
Chapter 11 introduces you to techniques for fi nding errors in your C++ programs. It covers the general principles of debugging your programs and the basic features that Visual C++ 2010 provides to help you fi nd errors in your code.
➤
Chapter 12 discusses how Microsoft Windows applications are structured and describes and demonstrates the essential elements that are present in every application written for the Windows operating system. The chapter explains elementary examples of how Windows applications work, with programs that use native C++ and the Windows API, native C++ and the MFC, as well as an example of a basic Windows Forms application using C++/CLI.
➤
Chapter 13 introduces how you can program to use multiple processors if your PC has a multicore processor. You learn about the basic techniques for parallel processing through complete working examples of Windows API applications that are very computationally intensive.
➤
Chapters 14 through 19 teach you Windows programming. You learn to write native C++ Windows applications using the MFC for building a GUI. You will also learn how you use the .NET Framework in a C++/CLI Windows application. You learn how you create and use common controls to build the graphical user interface for your application, and how you handle the events that result from user interaction with your program. In addition to the techniques you learn for building a GUI, the application that you develop also shows you how you handle printing and how you can save your application data on disk.
➤
Chapter 20 teaches you the essentials you need to know for creating your own libraries using MFC. You learn about the different kinds of libraries you can create, and you develop working examples of these that work with the application that you have evolved over the preceding six chapters.
All chapters in the book include numerous working examples that demonstrate the programming techniques discussed. Every chapter concludes with a summary of the key points that were covered, and most chapters include a set of exercises at the end that you can attempt, to apply what you have learned. Solutions to the exercises, together with all the code from the book, are available for download from the publisher’s Web site (see the “Source Code” section later in this Introduction for more details). The tutorial on the C++ language uses examples that are console programs, with simple command-line input and output. This approach enables you to learn the various capabilities of C++ without getting bogged down in the complexities of Windows GUI programming. Programming for Windows is really practicable only after you have a thorough understanding of the programming language. If you want to keep things as simple as possible, or if you are new to programming, you can just learn the native C++ programming language in the first instance. Each of the chapters that cover the C++ language (Chapters 2 through 9) first discuss particular aspects of the capabilities of native C++, and then the new features introduced by C++/CLI in the same context. The reason for organizing things this way is that C++/CLI is defined as an extension to the ISO/IEC standard language, so an xxxvi
INTRODUCTION
understanding of C++/CLI is predicated on knowledge of ISO/IEC C++. Thus, you can just work through the native topics in each of the chapters and ignore the C++/CLI sections that follow. You then can to progress to Windows application development using native C++ without having to keep the two versions of the language in mind. You can return to C++/CLI when you are comfortable with ISO/IEC C++. Of course, if you already have some programming expertise, you can also work straight through and add to your knowledge of both versions of the C++ language incrementally.
WHAT YOU NEED TO USE THIS BOOK To fully use this book, you need a version of Visual C++ 2010 (or Visual Studio 2010) that supports the Microsoft Foundation Classes (MFC). Note that the free Visual C++ 2010 Express Edition is not sufficient. The Express Edition provides only the C++ compiler and access to the basic Windows API; it does not provide the MFC library. Thus, any fee edition of either Visual C++ 2010 or Visual Studio 2010 will enable you to compile and execute all the examples in the book.
CONVENTIONS To help you get the most from the text and keep track of what’s happening, we’ve used a number of conventions throughout the book.
TRY IT OUT The Try It Out is an exercise you should work through, following the text in the book.
1. 2. 3.
They usually consist of a set of steps. Each step has a number. Follow the steps through with your copy of the database.
How It Works After each Try It Out, the code you’ve typed will be explained in detail. WARNING Boxes like this one hold important, not-to -be forgotten information that is directly relevant to the surrounding text.
NOTE Notes, tips, hints, tricks, and asides to the current discussion are off set and placed in italics like this.
xxxvii
INTRODUCTION
As for styles in the text: ➤
We highlight new terms and important words when we introduce them.
➤
We show keyboard strokes like this: Ctrl+A.
➤
We show fi le names, URLs, and code within the text like so: persistence.properties.
➤
We present code in two different ways: We use a monofont type with no highlighting for most code examples. We use bold highlighting to emphasize code that is of particular importance in the present context.
SOURCE CODE As you work through the examples in this book, you may choose either to type in all the code manually, or to use the source code fi les that accompany the book. All the source code used in this book is available for download at http://www.wrox.com. When at the site, simply locate the book’s title (use the Search box or one of the title lists) and click the Download Code link on the book’s detail page to obtain all the source code for the book. Code that is included on the Web site is highlighted by the following icon:
Available for download on Wrox.com
Listings include the fi lename in the title. If it is just a code snippet, you’ll fi nd the fi lename in a code note such as this: code snippet filename
NOTE Because many books have similar titles, you may find it easiest to search by ISBN; this book’s ISBN is 978- 0 - 470 -50088- 0.
Once you download the code, just decompress it with your favorite compression tool. Alternately, you can go to the main Wrox code download page at http://www.wrox.com/dynamic/books/ download.aspx to see the code available for this book and all other Wrox books.
ERRATA We make every effort to ensure that there are no errors in the text or in the code. However, no one is perfect, and mistakes do occur. If you fi nd an error in one of our books, like a spelling mistake or faulty piece of code, we would be very grateful for your feedback. By sending in errata, you may xxxviii
INTRODUCTION
save another reader hours of frustration, and at the same time, you will be helping us provide even higher quality information. To fi nd the errata page for this book, go to http://www.wrox.com and locate the title using the Search box or one of the title lists. Then, on the book’s detail page, click the Book Errata link. On this page, you can view all errata that has been submitted for this book and posted by Wrox editors. A complete book list, including links to each book’s errata, is also available at www.wrox.com/ misc-pages/booklist.shtml. If you don’t spot “your” error on the Book Errata page, go to www.wrox.com/contact/ techsupport.shtml and complete the form there to send us the error you have found. We’ll check the information and, if appropriate, post a message to the book’s errata page and fi x the problem in subsequent editions of the book.
P2P.WROX.COM For author and peer discussion, join the P2P forums at p2p.wrox.com. The forums are a Web-based system for you to post messages relating to Wrox books and related technologies and interact with other readers and technology users. The forums offer a subscription feature to e-mail you topics of interest of your choosing when new posts are made to the forums. Wrox authors, editors, other industry experts, and your fellow readers are present on these forums. At http://p2p.wrox.com, you will fi nd a number of different forums that will help you, not only as you read this book, but also as you develop your own applications. To join the forums, just follow these steps:
1. 2. 3.
Go to p2p.wrox.com and click the Register link.
4.
You will receive an e-mail with information describing how to verify your account and complete the joining process.
Read the terms of use and click Agree. Complete the required information to join, as well as any optional information you wish to provide, and click Submit.
NOTE You can read messages in the forums without joining P2P, but in order to post your own messages, you must join.
Once you join, you can post new messages and respond to messages other users post. You can read messages at any time on the Web. If you would like to have new messages from a particular forum e-mailed to you, click the Subscribe to this Forum icon by the forum name in the forum listing. For more information about how to use the Wrox P2P, be sure to read the P2P FAQs for answers to questions about how the forum software works, as well as many common questions specific to P2P and Wrox books. To read the FAQs, click the FAQ link on any P2P page. xxxix
1
Programming with Visual C++ 2010 WHAT YOU WILL LEARN IN THIS CHAPTER:
➤
What the principal components of Visual C++ 2010 are
➤
What the .NET Framework consists of and the advantages it offers
➤
What solutions and projects are and how you create them
➤
About console programs
➤
How to create and edit a program
➤
How to compile, link, and execute C++ console programs
➤
How to create and execute basic Windows programs
Windows programming isn’t difficult. Microsoft Visual C++ 2010 makes it remarkably easy, as you’ll see throughout the course of this book. There’s just one obstacle in your path: Before you get to the specifics of Windows programming, you have to be thoroughly familiar with the capabilities of the C++ programming language, particularly the object- oriented aspects of the language. Object- oriented techniques are central to the effectiveness of all the tools provided by Visual C++ 2010 for Windows programming, so it’s essential that you gain a good understanding of them. That’s exactly what this book provides. This chapter gives you an overview of the essential concepts involved in programming applications in C++. You’ll take a rapid tour of the integrated development environment (IDE) that comes with Visual C++ 2010. The IDE is straightforward and generally intuitive in its operation, so you’ll be able to pick up most of it as you go along. The best way to get familiar with it is to work through the process of creating, compiling, and executing a simple program. So power up your PC, start Windows, load the mighty Visual C++ 2010, and begin your journey.
2
❘
CHAPTER 1
PROGRAMMING WITH VISUAL C++ 2010
THE .NET FRAMEWORK The .NET Framework is a central concept in Visual C++ 2010 as well as in all the other .NET development products from Microsoft. The .NET Framework consists of two elements: the Common Language Runtime (CLR) in which your application executes, and a set of libraries called the .NET Framework class libraries. The .NET Framework class libraries provide the functional support your code will need when executing with the CLR, regardless of the programming language used, so .NET programs written in C++, C#, or any of the other languages that support the .NET Framework all use the same .NET libraries. There are two fundamentally different kinds of C++ applications you can develop with Visual C++ 2010. You can write applications that natively execute on your computer. These applications will be referred to as native C++ programs; you write native C++ programs in the version of C++ defi ned by the ISO/IEC (International Standards Organization/International Electrotechnical Commision) language standard. You can also write applications to run under the control of the CLR in an extended version of C++ called C++/CLI . These programs will be referred to as CLR programs, or C++/CLI programs. The .NET Framework is not strictly part of Visual C++ 2010 but rather a component of the Windows operating system that makes it easier to build software applications and Web services. The .NET Framework offers substantial advantages in code reliability and security, as well as the ability to integrate your C++ code with code written in over 20 other programming languages that target the .NET Framework. A slight disadvantage of targeting the .NET Framework is that there is a small performance penalty compared to native code, but you won’t notice this in the majority of circumstances.
THE COMMON LANGUAGE RUNTIME The Common Language Runtime (CLR) is a standardized environment for the execution of programs written in a wide range of high-level languages including Visual Basic, C#, and of course C++. The specification of the CLR is now embodied in the European Computer Manufacturers Association (ECMA) standard for the Common Language Infrastructure (CLI), the ECMA-335, and also in the equivalent ISO standard, ISO/IEC 23271, so the CLR is an implementation of this standard. You can see why C++ for the CLR is referred to as C++/CLI — it’s C++ for the Common Language Infrastructure, so you are likely to see C++/CLI compilers on other operating systems that implement the CLI.
NOTE Information about all ECMA standards is available from www.ecma-international.org, and ECMA - 335 is currently available as a
free download.
The CLI is essentially a specification for a virtual machine environment that enables applications written in diverse high-level programming languages to be executed in different system
Writing C++ Applications
environments without the original source code’s being changed or replicated. The CLI specifies a standard intermediate language for the virtual machine to which the high-level language source code is compiled. With the .NET Framework, this intermediate language is referred to as Microsoft Intermediate Language (MSIL). Code in the intermediate language is ultimately mapped to machine code by a just-in-time (JIT) compiler when you execute a program. Of course, code in the CLI intermediate language can be executed within any other environment that has a CLI implementation. The CLI also defines a common set of data types called the Common Type System (CTS) that should be used for programs written in any programming language targeting a CLI implementation. The CTS specifies how data types are used within the CLR and includes a set of predefined types. You may also define your own data types, and these must be defined in a particular way to be consistent with the CLR, as you’ll see. Having a standardized type system for representing data allows components written in different programming languages to handle data in a uniform way and makes it possible to integrate components written in different languages into a single application. Data security and program reliability is greatly enhanced by the CLR, in part because dynamic memory allocation and release for data is fully automatic, but also because the MSIL code for a program is comprehensively checked and validated before the program executes. The CLR is just one implementation of the CLI specification that executes under Microsoft Windows on a PC; there will undoubtedly be other implementations of the CLI for other operating system environments and hardware platforms. You’ll sometimes fi nd that the terms CLI and CLR are used interchangeably, although it should be evident that they are not the same thing. The CLI is a standard specification; the CLR is Microsoft’s implementation of the CLI.
WRITING C++ APPLICATIONS You have tremendous flexibility in the types of applications and program components that you can develop with Visual C++ 2010. As noted earlier in this chapter, you have two basic options for Windows applications: You can write code that executes with the CLR, and you can also write code that compiles directly to machine code and thus executes natively. For window-based applications targeting the CLR, you use Windows Forms as the base for the GUI provided by the .NET Framework libraries. Using Windows Forms enables rapid GUI development because you assemble the GUI graphically from standard components and have the code generated completely automatically. You then just need to customize the code that has been generated to provide the functionality that you require. For natively executing code, you have several ways to go. One possibility is to use the Microsoft Foundation Classes (MFC) for programming the graphical user interface for your Windows application. The MFC encapsulates the Windows operating system Application Programming Interface (API) for GUI creation and control and greatly eases the process of program development. The Windows API originated long before the C++ language arrived on the scene, so it has none of the object- oriented characteristics that would be expected if it were written today; however, you are not obliged to use the MFC. If you want the ultimate in performance, you can write your C++ code to access the Windows API directly.
❘3
4
❘
CHAPTER 1
PROGRAMMING WITH VISUAL C++ 2010
C++ code that executes with the CLR is described as managed C++ because data and code are managed by the CLR. In CLR programs, the release of memory that you have allocated dynamically for storing data is taken care of automatically, thus eliminating a common source of error in native C++ applications. C++ code that executes outside the CLR is sometimes described by Microsoft as unmanaged C++ because the CLR is not involved in its execution. With unmanaged C++ you must take care of all aspects of allocating and releasing memory during execution of your program yourself, and also forego the enhanced security provided by the CLR. You’ll also see unmanaged C++ referred to as native C++ because it compiles directly to native machine code. Figure 1-1 shows the basic options you have for developing C++ applications.
Managed C++
Native C++
Native C++
Framework Classes
MFC
Common Language Runtime
Operating System
Hardware
FIGURE 1-1
Figure 1-1 is not the whole story. An application can consist partly of managed C++ and partly of native C++, so you are not obliged to stick to one environment or the other. Of course, you do lose out somewhat by mixing code, so you would choose to follow this approach only when necessary, such as when you want to convert an existing native C++ application to run with the CLR. You obviously won’t get the benefits inherent in managed C++ in the native C++ code, and there can also be appreciable overhead involved in communications between the managed and unmanaged code components. The ability to mix managed and unmanaged code can be invaluable, however, when you need to develop or extend existing unmanaged code but also want to obtain the advantages of using the CLR. Of course, for new applications you should decide at the outset whether you want to create a managed C++ application or a native C++ application.
Learning Windows Programming
❘5
LEARNING WINDOWS PROGRAMMING There are always two basic aspects to interactive applications executing under Windows: You need code to create the graphical user interface (GUI) with which the user interacts, and you need code to process these interactions to provide the functionality of the application. Visual C++ 2010 provides you with a great deal of assistance in both aspects of Windows application development. As you’ll see later in this chapter, you can create a working Windows program with a GUI without writing any code yourself at all. All the basic code to create the GUI can be generated automatically by Visual C++ 2010; however, it’s essential to understand how this automatically generated code works because you need to extend and modify it to make it do what you want, and to do that you need a comprehensive understanding of C++. For this reason you’ll first learn C++ — both the native C++ and C++/CLI versions of the language — without getting involved in Windows programming considerations. After you’re comfortable with C++ you’ll learn how to develop fully-fledged Windows applications using native C++ and C++/CLI. This means that while you are learning C++, you’ll be working with programs that involve only command line input and output. By sticking to this rather limited input and output capability, you’ll be able to concentrate on the specifics of how the C++ language works and avoid the inevitable complications involved in GUI building and control. After you become comfortable with C++ you’ll find that it’s an easy and natural progression to applying C++ to the development of Windows application programs.
Learning C++ Visual C++ 2010 fully supports two versions of C++, defi ned by two separate standards: ➤
The ISO/IEC C++ standard is for implementing native applications — unmanaged C++. This version of C++ is supported on the majority of computer platforms.
➤
The C++/CLI standard is designed specifically for writing programs that target the CLR and is an extension of the ISO/IEC C++.
Chapters 2 through 9 of this book teach you the C++ language. Because C++/CLI is an extension of ISO/IEC C++, the fi rst part of each chapter introduces elements of the ISO/IEC C++ language; the second part explains the additional features that C++/CLI introduces. Writing programs in C++/CLI enables you to take full advantage of the capabilities of the .NET Framework, something that is not possible with programs written in ISO/IEC C++. Although C++/ CLI is an extension of ISO/IEC C++, to be able to execute your program fully with the CLR means that it must conform to the requirements of the CLR. This implies that there are some features of ISO/IEC C++ that you cannot use in your CLR programs. One example of this that you might deduce from what I have said up to now is that the dynamic memory allocation and release facilities offered by ISO/IEC C++ are not compatible with the CLR; you must use the CLR mechanism for memory management, and this implies that you must use C++/CLI classes, not native C++ classes.
The C++ Standards At the time of writing, the currently approved C++ language standard is defi ned by the document ISO/IEC 14882:1998, published by the International Organization for Standardization (ISO). This is the well- established version of C++ that has been around since 1998 and is supported by
6
❘
CHAPTER 1
PROGRAMMING WITH VISUAL C++ 2010
compilers on the majority of computer hardware platforms and operating systems. There is a new standard for C++ in draft form that is expected to be approved in the near future. Visual C++ 2010 already supports several of the new language capabilities offered by this new standard, and these are described in this book. Programs that you write in standard C++ can be ported from one system environment to another reasonably easily, although the library functions that a program uses — particularly those related to building a graphical user interface — are a major determinant of how easy or difficult it will be. ISO/ IEC standard C++ is the first choice of many professional program developers because it is so widely supported, and because it is one of the most powerful programming languages available today. C++/CLI is a version of C++ that extends the ISO/IEC standard for C++ to better support the Common Language Infrastructure (CLI) defi ned by the standard ECMA-355. The C++/CLI language specification is defi ned by the standard ECMA-372 and is available for download from the ECMA web site. This standard was developed from an initial technical specification that was produced by Microsoft to support the execution of C++ programs with the .NET Framework. Thus, both the CLI and C++/CLI were originated by Microsoft in support of the .NET Framework. Of course, standardizing the CLI and C++/CLI greatly increases the likelihood of implementation in environments other than Windows. It’s important to appreciate that although C++/CLI is an extension of ISO/IEC C++, there are features of ISO/IEC C++ that you must not use when you want your program to execute fully under the control of the CLR. You’ll learn what these are as you progress through the book. The CLR offers substantial advantages over the native environment. If you target your C++ programs at the CLR, they will be more secure and not prone to the potential errors you can make when using the full power of ISO/IEC C++. The CLR also removes the incompatibilities introduced by various high-level languages by standardizing the target environment to which they are compiled, and thus permits modules written in C++ to be combined with modules written in other languages, such as C# or Visual Basic.
Attributes Attributes are an advanced feature of programming with C++/CLI that enable you to add descriptive declarations to your code. At the simplest level, you can use attributes to annotate particular programming elements in your program, but there’s more to attributes than just additional descriptive data. Attributes can affect how your code behaves at run time by modifying the way the code is compiled or by causing extra code to be generated that supports additional capabilities. A range of standard attributes is available for C++/CLI, and it is also possible to create your own. A detailed discussion of attributes is beyond the scope of this book, but I mention them here because you will make use of attributes in one or two places in the book, particularly in Chapter 19, where you learn how to write objects to a fi le.
Console Applications As well as developing Windows applications, Visual C++ 2010 also enables you to write, compile, and test C++ programs that have none of the baggage required for Windows programs — that is, applications that are essentially character-based, command-line programs. These programs are
Learning Windows Programming
❘7
called console applications in Visual C++ 2010 because you communicate with them through the keyboard and the screen in character mode. When you write console applications it might seem as if you are being sidetracked from the main objective of Windows programming, but when it comes to learning C++ (which you do need to do before embarking on Windows-specific programming) it’s the best way to proceed. There’s a lot of code in even a simple Windows program, and it’s very important not to be distracted by the complexities of Windows when learning the ins and outs of C++. Therefore, in the early chapters of the book, which are concerned with how C++ works, you’ll spend time walking with a few lightweight console applications before you get to run with the heavyweight sacks of code in the world of Windows. While you’re learning C++, you’ll be able to concentrate on the language features without worrying about the environment in which you’re operating. With the console applications that you’ll write, you have only a text interface, but this will be quite sufficient for understanding all of C++ because there’s no graphical capability within the defi nition of the language. Naturally, I will provide extensive coverage of graphical user interface programming when you come to write programs specifically for Windows using Microsoft Foundation Classes (MFC) in native C++ applications, and Windows Forms with the CLR. There are two distinct kinds of console applications, and you’ll be using both. ➤
Win32 console applications compile to native code. You’ll be using these to try out the capabilities of ISO/IEC C++.
➤
CLR console applications target the CLR. You’ll be using these when you are working with the features of C++/CLI.
Windows Programming Concepts The project creation facilities provided with Visual C++ 2010 can generate skeleton code for a wide variety of native C++ application programs automatically, including basic Windows programs. For Windows applications that you develop for the CLR you get even more automatic code generation. You can create complete applications using Windows Forms that require only a small amount of customized code to be written by you, and some that require no additional code at all. Creating a project is the starting point for all applications and components that you develop with Visual C++ 2010, and to get a flavor of how this works you’ll look at the mechanics of creating some examples, including an outline Windows program, later in this chapter. A Windows program, whether a native C++ program or a program written for the CLR, has a different structure from that of the typical console program you execute from the command line, and it’s more complicated. In a console program you can get input from the keyboard and write output back to the command line directly, whereas a Windows program can access the input and output facilities of the computer only by way of functions supplied by the host environment; no direct access to the hardware resources is permitted. Because several programs can be active at one time under Windows, Windows has to determine which application a given raw input, such as a mouse click or the pressing of a key on the keyboard, is destined for, and signal the program concerned accordingly. Thus, the Windows operating system has primary control of all communications with the user.
8
❘
CHAPTER 1
PROGRAMMING WITH VISUAL C++ 2010
The nature of the interface between a user and a Windows application is such that a wide range of different inputs is usually possible at any given time. A user may select any of a number of menu options, click a toolbar button, or click the mouse somewhere in the application window. A welldesigned Windows application has to be prepared to deal with any of the possible types of input at any time because there is no way of knowing in advance which type of input is going to occur. These user actions are received by the operating system in the fi rst instance, and are all regarded by Windows as events. An event that originates with the user interface for your application will typically result in a particular piece of your program code being executed. How program execution proceeds is therefore determined by the sequence of user actions. Programs that operate in this way are referred to as event- driven programs, and are different from traditional procedural programs that have a single order of execution. Input to a procedural program is controlled by the program code and can occur only when the program permits it; therefore, a Windows program consists primarily of pieces of code that respond to events caused by the action of the user, or by Windows itself. This sort of program structure is illustrated in Figure 1-2.
Events:
Keyboard Input
Press Left Mouse Button
Press Right Mouse Button
Other Event
WINDOWS
Process Keyboard Input
Process Left Mouse Button
Process Right Mouse Button
Program Data Your Program
FIGURE 1-2
Process Other Event
What Is the Integrated Development Environment?
❘9
Each square block in Figure 1-2 represents a piece of code written specifically to deal with a particular event. The program may appear to be somewhat fragmented because of the number of disjointed blocks of code, but the primary factor welding the program into a whole is the Windows operating system itself. You can think of your program as customizing Windows to provide a particular set of capabilities. Of course, the modules servicing various external events, such as the selection of a menu or a mouse click, all typically have access to a common set of application-specific data in a particular program. This application data contains information that relates to what the program is about — for example, blocks of text recording scoring records for a player in a program aimed at tracking how your baseball team is doing — as well as information about some of the events that have occurred during execution of the program. This shared collection of data allows various parts of the program that look independent to communicate and operate in a coordinated and integrated fashion. I will go into this in much more detail later in the book. Even an elementary Windows program involves several lines of code, and with Windows programs generated by the application wizards that come with Visual C++ 2010, “several” turns out to be “many.” To simplify the process of understanding how C++ works, you need a context that is as uncomplicated as possible. Fortunately, Visual C++ 2010 comes with an environment that is readymade for the purpose.
WHAT IS THE INTEGRATED DEVELOPMENT ENVIRONMENT? The integrated development environment (IDE) that comes with Visual C++ 2010 is a completely self- contained environment for creating, compiling, linking, and testing your C++ programs. It also happens to be a great environment in which to learn C++ (particularly when combined with a great book). Visual C++ 2010 incorporates a range of fully integrated tools designed to make the whole process of writing C++ programs easy. You will see something of these in this chapter, but rather than grind through a boring litany of features and options in the abstract, you can fi rst take a look at the basics to get a view of how the IDE works, and then pick up the rest in context as you go along. The fundamental parts of Visual C++ 2010, provided as part of the IDE, are the editor, the compiler, the linker, and the libraries. These are the basic tools that are essential to writing and executing a C++ program. Their functions are as follows.
The Editor The editor provides an interactive environment in which to create and edit C++ source code. As well as the usual facilities, such as cut and paste, which you are certainly already familiar with, the editor also provides color cues to differentiate between various language elements. The editor automatically recognizes fundamental words in the C++ language and assigns a color to them according to what they are. This not only helps to make your code more readable, but also provides a clear indicator of when you make errors in keying such words.
10
❘
CHAPTER 1
PROGRAMMING WITH VISUAL C++ 2010
The Compiler The compiler converts your source code into object code, and detects and reports errors in the compilation process. The compiler can detect a wide range of errors caused by invalid or unrecognized program code, as well as structural errors, such as parts of a program that can never be executed. The object code output from the compiler is stored in fi les called object fi les that have names with the extension .obj.
The Linker The linker combines the various modules generated by the compiler from source code fi les, adds required code modules from program libraries supplied as part of C++, and welds everything into an executable whole. The linker can also detect and report errors — for example, if part of your program is missing, or a nonexistent library component is referenced.
The Libraries A library is simply a collection of prewritten routines that supports and extends the C++ language by providing standard professionally-produced code units that you can incorporate into your programs to carry out common operations. The operations implemented by routines in the various libraries provided by Visual C++ 2010 greatly enhance productivity by saving you the effort of writing and testing the code for such operations yourself. I have already mentioned the .NET Framework library, and there are a number of others — too many to enumerate here — but I’ll identify the most important ones. The Standard C++ Library defi nes a basic set of routines common to all ISO/IEC C++ compilers. It contains a wide range of routines, including numerical functions, such as the calculation of square roots and the evaluation of trigonometrical functions; character- and string-processing routines, such as the classification of characters and the comparison of character strings; and many others. You’ll get to know quite a number of these as you develop your knowledge of ISO/IEC C++. There are also libraries that support the C++/CLI extensions to ISO/IEC C++. Native window-based applications are supported by a library called the Microsoft Foundation Classes (MFC). The MFC greatly reduces the effort needed to build the graphical user interface for an application. (You’ll see a lot more of the MFC when you fi nish exploring the nuances of the C++ language.) Another library contains a set of facilities called Windows Forms, which are roughly the equivalent of the MFC for Windows-based applications executed with the .NET Framework. You’ll be seeing how you make use of Windows Forms to develop applications, too.
USING THE IDE All program development and execution in this book is performed from within the IDE. When you start Visual C++ 2010 you’ll notice an application window similar to that shown in Figure 1-3.
Using the IDE
❘ 11
FIGURE 1-3
The pane to the left in Figure 1-3 is the Solution Explorer window, the top right pane presently showing the Start page is the Editor window, and the tab visible in the pane at the bottom is the Output window. The Solution Explorer window enables you to navigate through your program files and display their contents in the Editor window, and to add new files to your program. The Solution Explorer window has an additional tab (only three are shown in Figure 1-3) that displays the Resource View for your application, and you can select which tabs are to be displayed from the View menu. The Editor window is where you enter and modify source code and other components of your application. The Output window displays the output from build operations in which a project is compiled and linked. You can choose to display other windows by selecting from the View menu. Note that a window can generally be undocked from its position in the Visual C++ application window. Just right- click the title bar of the window you want to undock and select Float from the pop -up menu. In general, I will show windows in their undocked state in the book. You can restore a window to its docked state by right- clicking its title bar and selecting Dock from the pop -up.
12
❘
CHAPTER 1
PROGRAMMING WITH VISUAL C++ 2010
Toolbar Options You can choose which toolbars are displayed in your Visual C++ window by right- clicking in the toolbar area. The range of toolbars in the list depends on which edition of Visual C++ 2010 you have installed. A pop -up menu with a list of toolbars (Figure 1- 4) appears, and the toolbars currently displayed have checkmarks alongside them. This is where you decide which toolbars are visible at any one time. You can make your set of toolbars the same as those shown in Figure 1-3 by making sure the Build, Class Designer, Debug, Standard, and View Designer menu items are checked. Clicking a toolbar in the list checks it if it is unchecked, and results in its being displayed; clicking a toolbar that is checked unchecks it and hides the toolbar. You don’t need to clutter up the application window with all the toolbars you think you might need at some time. Some toolbars appear automatically when required, so you’ll probably fi nd that the default toolbar selections are perfectly adequate most of the time. As you develop your applications, from time to time you might think it would be more convenient to have access to toolbars that aren’t displayed. You can change the set of visible toolbars whenever it suits you by right- clicking in the toolbar area and choosing from the context menu.
FIGURE 1-4
NOTE As in many other Windows applications, the toolbars that make up Visual C++ 2010 come complete with tooltips. Just let the mouse pointer linger over a toolbar button for a second or two, and a white label will display the function of that button.
Dockable Toolbars A dockable toolbar is one that you can move around to position it at a convenient place in the window. You can arrange for any of the toolbars to be docked at any of the four sides of the application window. If you right- click in the toolbar area and select Customize from the pop -up, the Customize dialog will be displayed. You can choose where a particular toolbar is docked by selecting it and clicking the Modify Selection button. You can then choose from the drop -down list to dock the toolbar where you want. Figure 1-5 shows how the dialog looks after the user selects the Build toolbar on the left and clicks the Modify Selection button.
Using the IDE
❘ 13
FIGURE 1-5
You’ll recognize many of the toolbar icons that Visual C++ 2010 uses from other Windows applications, but you may not appreciate exactly what these icons do in the context of Visual C++, so I’ll describe them as we use them. Because you’ll use a new project for every program you develop, looking at what exactly a project is and understanding how the mechanism for defi ning a project works is a good place to start fi nding out about Visual C++ 2010.
Documentation There will be plenty of occasions when you’ll want to fi nd out more information about Visual C++ 2010 and its features and options. Press Ctrl+Alt+F1 to access the product documentation. The Help menu also provides various routes into the documentation, as well as access to program samples and technical support.
Projects and Solutions A project is a container for all the things that make up a program of some kind — it might be a console program, a window-based program, or some other kind of program — and it usually consists of one or more source files containing your code, plus possibly other files containing auxiliary data. All the files for a project are stored in the project folder; detailed information about the project is stored in an XML file with the extension .vcxproj, also in the project folder. The project folder also contains other folders that are used to store the output from compiling and linking your project. The idea of a solution is expressed by its name, in that it is a mechanism for bringing together all the programs and other resources that represent a solution to a particular data-processing problem. For example, a distributed order- entry system for a business operation might be composed of several different programs that could each be developed as a project within a single solution; therefore, a
14
❘
CHAPTER 1
PROGRAMMING WITH VISUAL C++ 2010
solution is a folder in which all the information relating to one or more projects is stored, and one or more project folders are subfolders of the solution folder. Information about the projects in a solution is stored in two files with the extensions .sln and .suo, respectively. When you create a project a new solution is created automatically unless you elect to add the project to an existing solution. When you create a project along with a solution, you can add further projects to the same solution. You can add any kind of project to an existing solution, but you will usually add only a project related in some way to the existing project, or projects, in the solution. Generally, unless you have a good reason to do otherwise, each of your projects should have its own solution. Each example you create with this book will be a single project within its own solution.
Defining a Project The fi rst step in writing a Visual C++ 2010 program is to create a project for it using the File ➪ New ➪ Project menu option from the main menu or by pressing Ctrl+Shift+N; you can also simply click New Project . . . on the Start page. As well as containing files that defi ne all the code and any other data that makes up your program, the project XML file in the project folder also records the Visual C++ 2010 options you’re using. That’s enough introductory stuff for the moment. It’s time to get your hands dirty.
TRY IT OUT
Creating a Project for a Win32 Console Application
You’ll now take a look at creating a project for a console application. First, select File ➪ New ➪ Project or use one of the other possibilities mentioned earlier to bring up the New Project dialog box, as shown in Figure 1-6.
FIGURE 1-6
Using the IDE
The left pane in the New Project dialog box displays the types of projects you can create; in this case, click Win32. This also identifies an application wizard that creates the initial contents for the project. The right pane displays a list of templates available for the project type you have selected in the left pane. The template you select is used by the application wizard in creating the fi les that make up the project. In the next dialog box you have an opportunity to customize the fi les that are created when you click the OK button in this dialog box. For most of the type/template options, a basic set of program source modules is created automatically. You can now enter a suitable name for your project by typing into the “Name:” edit box — for example, you could call this one Ex1_01, or you can choose your own project name. Visual C++ 2010 supports long fi le names, so you have a lot of flexibility. The name of the solution folder appears in the bottom edit box and, by default, the solution folder has the same name as the project. You can change this if you want. The dialog box also enables you to modify the location for the solution that contains your project — this appears in the “Location:” edit box. If you simply enter a name for your project, the solution folder is automatically set to a folder with that name, with the path shown in the “Location:” edit box. By default the solution folder is created for you, if it doesn’t already exist. If you want to specify a different path for the solution folder, just enter it in the “Location:” edit box. Alternatively, you can use the Browse button to select another path for your solution. Clicking OK displays the Win32 Application Wizard dialog box shown in Figure 1-7.
FIGURE 1-7
This dialog box explains the settings currently in effect. If you click the Finish button the wizard creates all the project fi les based on the settings in this box. In this case, you can click Application Settings on the left to display the Application Settings page of the wizard, shown in Figure 1-8.
❘ 15
16
❘
CHAPTER 1
PROGRAMMING WITH VISUAL C++ 2010
FIGURE 1-8
The Application Settings page enables you to choose options that you want to apply to the project. Here, you can leave things as they are and click Finish. The application wizard then creates the project with all the default files. The project folder will have the name that you supplied as the project name and will hold all the fi les making up the project defi nition. If you didn’t change it, the solution folder has the same name as the project folder and contains the project folder plus the files defi ning the contents of the solution. If you use Windows Explorer to inspect the contents of the solution folder, you’ll see that it contains four fi les: ➤
A fi le with the extension .sln that records information about the projects in the solution.
➤
A fi le with the extension .suo in which user options that apply to the solution will be recorded.
➤
A fi le with the extension .sdf that records data about Intellisense for the solution. Intellisense is the facility that provides auto completion and prompts you for code in the Editor window as you enter it.
➤
A file with the extension .opensdf that records information about the state of the project. This file exists only while the project is open.
If you use Windows Explorer to look in the project folder, you will see that there are eight files initially, including a file with the name ReadMe.txt that contains a summary of the contents of the files that have been created for the project. The project you have created will automatically open in Visual C++ 2010 with the Solution Explorer pane, as in Figure 1-9.
FIGURE 1-9
Using the IDE
❘ 17
The Solution Explorer pane presents a view of all the projects in the current solution and the files they contain — here, of course, there is just one project. You can display the contents of any file as an additional tab in the Editor pane just by double-clicking the name in the Solution Explorer tab. In the Editor pane, you can switch instantly to any of the files that have been displayed just by clicking on the appropriate tab. The Class View tab displays the classes defi ned in your project and also shows the contents of each class. You don’t have any classes in this application, so the view is empty. When I discuss classes you will see that you can use the Class View tab to move around the code relating to the defi nition and implementation of all your application classes quickly and easily. The Property Manager tab shows the properties that have been set for the Debug and Release versions of your project. I’ll explain these versions a little later in this chapter. You can change any of the properties shown by right- clicking a property and selecting Properties from the context menu; this displays a dialog box where you can set the project property. You can also press Alt+F7 to display the properties dialog box at any time. I’ll discuss this in more detail when we go into the Debug and Release versions of a program. You can display the Resource View tab by selecting from the View menu or by pressing Ctrl+Shift+E. The Resource View shows the dialog boxes, icons, menus, toolbars, and other resources used by the program. Because this is a console program, no resources are used; however, when you start writing Windows applications, you’ll see a lot of things here. Through this tab you can edit or add to the resources available to the project. As with most elements of the Visual C++ 2010 IDE, the Solution Explorer and other tabs provide context-sensitive pop -up menus when you right- click items displayed in the tab, and in some cases when you right- click in the empty space in the tab, too. If you fi nd that the Solution Explorer pane gets in your way when you’re writing code, you can hide it by clicking the Autohide icon. To redisplay it, click the Name tab on the left of the IDE window.
Modifying the Source Code The application wizard generates a complete Win32 console program that you can compile and execute. Unfortunately, the program doesn’t do anything as it stands, so to make it a little more interesting you need to change it. If it is not already visible in the Editor pane, double- click Ex1_01.cpp in the Solution Explorer pane. This is the main source fi le for the program that the application wizard generated, and it looks like what is shown in Figure 1-10.
FIGURE 1-10
18
❘
CHAPTER 1
PROGRAMMING WITH VISUAL C++ 2010
If the line numbers are not displayed on your system, select Tools ➪ Options from the main menu to display the Options dialog box. If you extend the C/C++ option in the Text Editor subtree in the left pane and select General from the extended tree, you can select Line Numbers in the right pane of the dialog box. I’ll fi rst give you a rough guide to what this code in Figure 1-10 does, and you’ll see more on all of this later. The fi rst two lines are just comments. Anything following “//” in a line is ignored by the compiler. When you want to add descriptive comments in a line, precede your text with “//”. Line 4 is an #include directive that adds the contents of the fi le stdafx.h to this file in place of this #include directive. This is the standard way to add the contents of .h source fi les to a .cpp source fi le in a C++ program. Line 7 is the fi rst line of the executable code in this fi le and the beginning of the function _tmain(). A function is simply a named unit of executable code in a C++ program; every C++ program consists of at least one — and usually many more — functions. Lines 8 and 10 contain left and right braces, respectively, that enclose all the executable code in the function _tmain(). The executable code is, therefore, just the single line 9, and all this does is end the program. Now you can add the following two lines of code in the Editor window: // Ex1_01.cpp : Defines the entry point for the console application. // Available for download on Wrox.com
#include "stdafx.h" #include int _tmain(int argc, _TCHAR* argv[]) { std::cout ^args) { int vowels(0), consonants(0); String^ proverb(L"A nod is as good as a wink to a blind horse."); for each(wchar_t ch in proverb) { if(Char::IsLetter(ch)) { ch = Char::ToLower(ch); // Convert to lowercase switch(ch) { case 'a': case 'e': case 'i': case 'o': case 'u': ++vowels; break; default: ++consonants; break; } } } Console::WriteLine(proverb); Console::WriteLine(L"The proverb contains {0} vowels and {1} consonants.", vowels, consonants); return 0; } code snippet Ex3_17.cpp
162
❘
CHAPTER 3
DECISIONS AND LOOPS
This example produces the following output: A nod is as good as a wink to a blind horse. The proverb contains 14 vowels and 18 consonants.
How It Works The program counts the number of vowels and consonants in the string referenced by the proverb variable. The program does this by iterating through each character in the string using a for each loop. You fi rst defi ne the two variables used to accumulate the total number of vowels and the total number of consonants. int vowels(0), consonants(0);
Under the covers, both are of the C++/CLI Int32 type, which stores a 32-bit integer. Next, you defi ne the string to analyze. String^ proverb(L"A nod is as good as a wink to a blind horse.");
The proverb variable is of type String^, which is described as type “handle to String“; a handle is used to store the location of an object on the garbage- collected heap that is managed by the CLR. You’ll learn more about handles and type String^ when we get into C++/CLI class types; for now, just take it that this is the type you use for C++/CLI variables that store strings. The for each loop that iterates over the characters in the string referenced by proverb is of this form: for each(wchar_t ch in proverb) { // Process the current character stored in ch... }
The characters in the proverb string are Unicode characters, so you use a variable of type wchar_t (equivalent to type Char) to store them. The loop successively stores characters from the proverb string in the loop variable ch, which is of the C++/CLI type Char. This variable is local to the loop — in other words, it exists only within the loop block. On the fi rst iteration ch contains the fi rst character from the string, on the second iteration it contains the second character, on the third iteration the third character, and so on until all the characters have been processed and the loop ends. Within the loop you determine whether the character is a letter in the if expression: if(Char::IsLetter(ch))
The Char::IsLetter() function returns the value true if the argument — ch in this case — is a letter, and false otherwise. Thus, the block following the if only executes if ch contains a letter. This is necessary because you don’t want punctuation characters to be processed as though they were letters. Having established that ch is indeed a letter, you convert it to lowercase with the following statement: ch = Char::ToLower(ch);
// Convert to lowercase
Summary
❘ 163
This uses the Char::ToLower() function from the .NET Framework library, which returns the lowercase equivalent of the argument — ch in this case. If the argument is already lowercase, the function just returns the same character code. By converting the character to lowercase, you avoid having to test subsequently for both upper- and lowercase vowels. You determine whether ch contains a vowel or a consonant within the switch statement. switch(ch) { case 'a': case 'e': case 'i': case 'o': case 'u': ++vowels; break; default: ++consonants; break; }
For any of the five cases where ch is a vowel, you increment the value stored in vowels; otherwise, you increment the value stored in consonants. This switch is executed for each character in proverb, so when the loop fi nishes, vowels contains the number of vowels in the string and consonants contains the number of consonants. You then output the result with the following statements: Console::WriteLine(proverb); Console::WriteLine(L"The proverb contains {0} vowels and {1} consonants.", vowels, consonants);
In the last statement, the value of vowels replaces the “{0}” in the string and the value of consonants replaces the “{1}”. This is because the arguments that follow the fi rst format string argument are referenced by index values starting from 0.
SUMMARY In this chapter, you learned all the essential mechanisms for making decisions in C++ programs. The ability to compare values and change the course of program execution is what differentiates a computer from a simple calculator. You need to be comfortable with all of the decision-making statements I have discussed because they are all used very frequently. You have also gone through all the facilities for repeating a group of statements. Loops are a fundamental programming technique that you will need to use in every program of consequence that you write. You will fi nd you use the for loop most often in native C++, closely followed by the while loop. The for each loop in C++/CLI is typically more efficient than other loop forms, so be sure to use that where you can.
164
❘
CHAPTER 3
DECISIONS AND LOOPS
EXERCISES You can download the source code for the examples in the book and the solutions to the following exercises from www.wrox.com.
1.
Write a program that reads numbers from cin and then sums them, stopping when 0 has been entered. Construct three versions of this program, using the while, do-while, and for loops.
2.
Write an ISO/IEC C++ program to read characters from the keyboard and count the vowels. Stop counting when a Q (or a q) is encountered. Use a combination of an indefinite loop to get the characters, and a switch statement to count them.
3.
Write a program to print out the multiplication table from 2 to 12, in columns.
4.
Imagine that in a program you want to set a ‘file open mode’ variable based on two attributes: the file type, which can be text or binary, and the way in which you want to open the file to read or write it, or append data to it. Using the bitwise operators (& and |) and a set of flags, devise a method to allow a single integer variable to be set to any combination of the two attributes. Write a program that sets such a variable and then decodes it, printing out its setting, for all possible combinations of the attributes.
5.
Repeat Ex3_2 as a C++/CLI program — you can use Console::ReadKey() to read characters from the keyboard.
6.
Write a CLR console program that defines a string (as type String^) and then analyzes the characters in the string to discover the number of uppercase letters, the number of lowercase letters, the number of non-alphabetic characters, and the total number of characters in the string.
Summary
❘ 165
WHAT YOU LEARNED IN THIS CHAPTER TOPIC
CONCEPT
Relational operators
The decision- making capability in C++ is based on the set of relational operators, which allow expressions to be tested and compared, and yield a bool value — true or false — as the result.
Decisions based on numerical values
You can also make decisions based on conditions that return non -bool values. Any non -zero value is cast to true when a condition is tested; zero casts to false.
Statements for decision-making
The primary decision - making capability in C++ is provided by the if statement. Further flexibility is provided by the switch statement, and by the conditional operator.
Loop statements
There are three basic methods provided in ISO/IEC C++ for repeating a block of statements: the for loop, the while loop, and the do-while loop. The for loop allows the loop to repeat a given number of times. The while loop allows a loop to continue as long as a specified condition returns true. Finally, do-while executes the loop at least once and allows continuation of the loop as long as a specified condition returns true.
The for each statement
C++/CLI provides the for each loop statement in addition to the three loop statements defined in ISO/IEC C++.
Nested loops
Any kind of loop may be nested within any other kind of loop.
The continue keyword
The keyword continue allows you to skip the remainder of the current iteration in a loop and go straight to the next iteration.
The break keyword
The keyword break provides an immediate exit from a loop. It also provides an exit from a switch at the end of statements in a case.
4
Arrays, Strings, and Pointers WHAT YOU WILL LEARN IN THIS CHAPTER:
➤
How to use arrays
➤
How to declare and initialize arrays of different types
➤
How to declare and use multidimensional arrays
➤
How to use pointers
➤
How to declare and initialize pointers of different types
➤
The relationship between arrays and pointers
➤
How to declare references and some initial ideas on their uses
➤
How to allocate memory for variables dynamically in a native C++ program
➤
How dynamic memory allocation works in a Common Language Runtime (CLR) program
➤
Tracking handles and tracking references and why you need them in a CLR program
➤
How to work with strings and arrays in C++/CLI programs
➤
How to create and use interior pointers
So far, we have covered all the fundamental data types of consequence, and you have a basic knowledge of how to perform calculations and make decisions in a program. This chapter is about broadening the application of the basic programming techniques that you have learned so far, from using single items of data to working with whole collections of data items. In this chapter, you’ll be using objects more extensively. Although you have not yet explored the details of how they are created, don’t worry if everything is not completely clear. You’ll learn about classes and objects in detail starting in Chapter 7.
168
❘
CHAPTER 4
ARRAYS, STRINGS, AND POINTERS
HANDLING MULTIPLE DATA VALUES OF THE SAME TYPE You already know how to declare and initialize variables of various types that each holds a single item of information; I’ll refer to single items of data as data elements. The most obvious extension to the idea of a variable is to be able to reference several data elements of a particular type with a single variable name. This would enable you to handle applications of a much broader scope. Let’s consider an example of where you might need this. Suppose that you needed to write a payroll program. Using a separately named variable for each individual’s pay, their tax liability, and so on, would be an uphill task to say the least. A much more convenient way to handle such a problem would be to reference an employee by some kind of generic name — employeeName to take an imaginative example — and to have other generic names for the kinds of data related to each employee, such as pay, tax, and so on. Of course, you would also need some means of picking out a particular employee from the whole bunch, together with the data from the generic variables associated with them. This kind of requirement arises with any collection of like entities that you want to handle in your program, whether they’re baseball players or battleships. Naturally, C++ provides you with a way to deal with this.
Arrays The basis for the solution to all of these problems is provided by the array in ISO/IEC C++. An array is simply a number of memory locations called array elements or simply elements, each of which can store an item of data of the same given data type, and which are all referenced through the same variable name. The employee names in a payroll program could be stored in one array, the pay for each employee in another, and the tax due for each employee could be stored in a third array. Individual items in an array are specified by an index value which is simply an integer representing the sequence number of the elements in the array, the first having the sequence number 0, the second 1, and so on. You can also envisage the index value of an array element as being an offset from the fi rst element in an array. The fi rst element has an offset of 0 and therefore an index of 0, and an index value of 3 will refer to the fourth element of an array. For the payroll, you could arrange the arrays so that if an employee’s name was stored in the employeeName array at a given index value, then the arrays pay and tax would store the associated data on pay and tax for the same employee in the array positions referenced by the same index value. The basic structure of an array is illustrated in Figure 4 -1. Index value for the 2nd element
Index value for the 5th element
Array name
Array name
height[0]
height[1]
height[2]
height[3]
height[4]
height[5]
73
62
51
42
41
34
The height array has 6 elements. FIGURE 4-1
Handling Multiple Data Values of the Same Type
❘ 169
Figure 4-1 shows an array with the name height that has six elements, each storing a different value. These might be the heights of the members of a family, for instance, recorded to the nearest inch. Because there are six elements, the index values run from 0 through 5. To refer to a particular element, you write the array name, followed by the index value of the particular element between square brackets. The third element is referred to as height[2], for example. If you think of the index as being the offset from the first element, it’s easy to see that the index value for the fourth element will be 3. The amount of memory required to store each element is determined by its type, and all the elements of an array are stored in a contiguous block of memory.
Declaring Arrays You declare an array in essentially the same way as you declared the variables that you have seen up to now, the only difference being that the number of elements in the array is specified between square brackets immediately following the array name. For example, you could declare the integer array height, shown in the previous figure, with the following declaration statement: long height[6];
Because each long value occupies 4 bytes in memory, the whole array requires 24 bytes. Arrays can be of any size, subject to the constraints imposed by the amount of memory in the computer on which your program is running. You can declare arrays to be of any type. For example, to declare arrays intended to store the capacity and power output of a series of engines, you could write the following: double cubic_inches[10]; double horsepower[10];
// Engine size // Engine power output
If auto mechanics is your thing, this would enable you to store the cubic capacity and power output of up to 10 engines, referenced by index values from 0 to 9. As you have seen before with other variables, you can declare multiple arrays of a given type in a single statement, but in practice it is almost always better to declare variables in separate statements.
TRY IT OUT
Using Arrays
As a basis for an exercise in using arrays, imagine that you have kept a record of both the amount of gasoline you have bought for the car and the odometer reading on each occasion. You can write a program to analyze this data to see how the gas consumption looks on each occasion that you bought gas:
Available for download on Wrox.com
// Ex4_01.cpp // Calculating gas mileage #include #include using std::cin; using std::cout; using std::endl;
170
❘
CHAPTER 4
ARRAYS, STRINGS, AND POINTERS
using std::setw; int main() { const int MAX(20); double gas[ MAX ]; long miles[ MAX ]; int count(0); char indicator('y');
// // // // //
Maximum number of values Gas quantity in gallons Odometer readings Loop counter Input indicator
while( ('y' == indicator || 'Y' == indicator) && count < MAX ) { cout gas[count]; // Read gas quantity cout > miles[count]; // Read odometer value ++count; cout > indicator; } if(count ^args) { array< array< String^ >^ >^ grades = gcnew array< array< String^ >^ > {
A B C D E
232
❘
CHAPTER 4
ARRAYS, STRINGS, AND POINTERS
gcnew gcnew gcnew gcnew gcnew
array<String^>{"Louise", "Jack"}, array<String^>{"Bill", "Mary", "Ben", "Joan"}, array<String^>{"Jill", "Will", "Phil"}, array<String^>{"Ned", "Fred", "Ted", "Jed", "Ed"}, array<String^>{"Dan", "Ann"}
// // // // //
Grade Grade Grade Grade Grade
A B C D E
}; wchar_t gradeLetter('A'); for each(array< String^ >^ grade in grades) { Console::WriteLine(L"Students with Grade {0}:", gradeLetter++); for each( String^ student in grade) Console::Write(L"{0,12}",student);
// Output the current name
Console::WriteLine(); } return 0; }
// Write a newline
code snippet Ex4_17.cpp
This example produces the following output: Students with Louise Students with Bill Students with Jill Students with Ned Students with Dan
Grade A: Jack Grade B: Mary Grade C: Will Grade D: Fred Grade E: Ann
Ben
Joan
Phil Ted
Jed
Ed
How It Works The array defi nition is exactly as you saw in the previous section. Next, you defi ne the gradeLetter variable as type wchar_t with the initial value 'A'. This is to be used to present the grade classification in the output. The students and their grades are listed by the nested loops. The outer for each loop iterates over the elements in the grades array: for each(array< String^ >^ grade in grades) { // Process students in the current grade... }
The loop variable grade is of type array< String^ >^ because that’s the element type in the grades array. The variable grade references each of the arrays of String^ handles in turn: the fi rst time around
C++/CLI Programming
❘ 233
the loop references the array of grade A student names, the second time around it references grade B student names, and so on until the last loop iteration when it references the grade E student names. On each iteration of the outer loop, you execute the following code: Console::WriteLine(L"Students with Grade {0}:", gradeLetter++); for each( String^ student in grade) Console::Write(L"{0,12}",student);
// Output the current name
Console::WriteLine();
// Write a newline
The fi rst statement writes a line that includes the current value of gradeLetter, which starts out as 'A'. The statement also increments gradeLetter to be, 'B', 'C', 'D', and 'E' successively on subsequent iterations of the outer loop. Next, you have the inner for each loop that iterates over each of the names in the current grade array in turn. The output statement uses the Console::Write() function so all the names appear on the same line. The names are presented right-justified in the output in a field width of 12, so the names in the lines of output are aligned. After the loop, the WriteLine() just writes a newline to the output, so the next grade output starts on a new line. You could have used a for loop for the inner loop: for (int i = 0 ; i < grade->Length ; i++) Console::Write(L"{0,12}",grade[i]);
// Output the current name
Here the loop is constrained by the Length property of the current array of names that is referenced by the grade variable. You could have used a for loop for the outer loop as well, in which case the inner loop needs to be changed further, and the nested loop looks like this: for (int j = 0 ; j < grades->Length ; j++) { Console::WriteLine(L"Students with Grade {0}:", gradeLetter+j); for (int i = 0 ; i < grades[j]->Length ; i++) Console::Write(L"{0,12}",grades[j][i]); // Output the current name Console::WriteLine(); }
Now grades[j] references the jth array of names; the expression grades[j][i] references the ith name in the jth array of names.
Strings You have already seen that the String class type that is defi ned in the System namespace represents a string in C++/CLI — in fact, a string consists of Unicode characters. To be more precise, it represents a string consisting of a sequence of characters of type System::Char. You get a huge
234
❘
CHAPTER 4
ARRAYS, STRINGS, AND POINTERS
amount of powerful functionality with String class objects, making string processing very easy. Let’s start at the beginning with string creation. You can create a String object like this: System::String^ saying(L"Many hands make light work.");
The variable saying is a tracking handle that references the String object initialized with the string that appears between the parentheses. You must always use a tracking handle to store a reference to a String object. The string literal here is a wide character string because it has the prefi x L. If you omit the L prefi x, you have a string literal containing 8 -bit characters, but the compiler ensures it is converted to a wide- character string. You can access individual characters in a string by using a subscript, just like an array; the fi rst character in the string has an index value of 0. Here’s how you could output the third character in the string saying: Console::WriteLine(L"The third character in the string is {0}", saying[2]);
Note that you can only retrieve a character from a string using an index value; you cannot update the string in this way. String objects are immutable and therefore cannot be modified. You can obtain the number of characters in a string by accessing its Length property. You could output the length of saying with this statement: Console::WriteLine(L"The string has {0} characters.", saying->Length);
Because saying is a tracking handle — which, as you know, is a kind of pointer — you must use the -> operator to access the Length property (or any other member of the object). You’ll learn more about properties when you get to investigate C++/CLI classes in detail.
Joining Strings You can use the + operator to join strings to form a new String object. Here’s an example: String^ name1(L"Beth"); String^ name2(L"Betty"); String^ name3(name1 + L" and " + name2);
After executing these statements, name3 contains the string “Beth and Betty”. Note how you can use the + operator to join String objects with string literals. You can also join String objects with numerical values or bool values, and have the values converted automatically to a string before the join operation. The following statements illustrate this: String^ String^ String^ String^
str(L"Value: "); str1(str + 2.5); str2(str + 25); str3(str + true);
// Result is new string L"Value: 2.5" // Result is new string L"Value: 25" // Result is new string L"Value: True"
You can also join a string and a character, but the result depends on the type of character:
C++/CLI Programming
char ch('Z'); wchar_t wch(L'Z'); String^ str4(str + ch); String^ str5(str + wch);
❘ 235
// Result is new string L"Value: 90" // Result is new string L"Value: Z"
The comments show the results of the operations. A character of type char is treated as a numerical value, so you get the character code value joined to the string. The wchar_t character is of the same type as the characters in the String object (type Char), so the character is appended to the string. Don’t forget that String objects are immutable; once created, they cannot be changed. This means that all operations that apparently modify String objects always result in new String objects being created. The String class also defi nes a Join() function that you use when you want to join a series of strings stored in an array into a single string with separators between the original strings. Here’s how you could join names together in a single string with the names separated by commas: array<String^>^ names = { L"Jill", L"Ted", L"Mary", L"Eve", L"Bill"}; String^ separator(L", "); String^ joined = String::Join(separator, names);
After executing these statements, joined references the string L“Jill, Ted, Mary, Eve, Bill”. The separator string has been inserted between each of the original strings in the names array. Of course, the separator string can be anything you like — it could be L“ and ”, for example, which results in the string L“Jill and Ted and Mary and Eve and Bill”. Let’s try a full example of working with String objects.
TRY IT OUT
Working with Strings
Suppose you have an array of integer values that you want to output aligned in columns. You want the values aligned, but you want the columns to be just sufficiently wide to accommodate the largest value in the array with a space between columns. This program does that. // Ex4_18.cpp : main project file. // Creating a custom format string Available for download on Wrox.com
#include "stdafx.h" using namespace System; int main(array<System::String ^> ^args) { array^ values = { 2, 456, 23, -46, 34211, 456, 5609, 112098, 234, -76504, 341, 6788, -909121, 99, 10}; String^ formatStr1(L"{0,"); // 1st half of format string String^ formatStr2(L"}"); // 2nd half of format string
236
❘
CHAPTER 4
ARRAYS, STRINGS, AND POINTERS
String^ number;
// Stores a number as a string
// Find the length of the maximum length value string int maxLength(0); // Holds the maximum length found for each(int value in values) { number = L"" + value; // Create string from value if(maxLengthLength) maxLength = number->Length; } // Create the format string to be used for output String^ format(formatStr1 + (maxLength+1) + formatStr2); // Output the values int numberPerLine(3); for(int i = 0 ; i< values->Length ; i++) { Console::Write(format, values[i]); if((i+1)%numberPerLine == 0) Console::WriteLine(); } return 0; } code snippet Ex4_18.cpp
The output from this program is: 2 -46 5609 -76504 -909121
456 34211 112098 341 99
23 456 234 6788 10
How It Works The objective of this program is to create a format string to align the output of integers from the values array in columns, with a width sufficient to accommodate the maximum length string representation of the integers. You create the format string initially in two parts: String^ formatStr1(L"{0,"); String^ formatStr2(L"}");
// 1st half of format string // 2nd half of format string
These two strings are the beginning and end of the format string you ultimately require. You need to work out the length of the maximum-length number string, and sandwich that value between formatStr1 and formatStr2, to form the complete format string. You fi nd the length you require with the following code: int maxLength(0); for each(int value in values)
// Holds the maximum length found
C++/CLI Programming
❘ 237
{ number = L"" + value; if(maxLengthLength) maxLength = number->Length;
// Create string from value
}
Within the loop you convert each number from the array to its String representation by joining it to an empty string. You compare the Length property of each string to maxLength, and if it’s greater than the current value of maxLength, it becomes the new maximum length. The statement that creates a string from value shows how an integer is automatically converted to a string when you combine it with a string using the addition operator. You could also obtain value as a string with the following statement: number = value.ToString();
This uses the ToString() function that is defi ned in the System::Int32 value class that converts an integer value to a string. Creating the format string is simple: String^ format(formatStr1 + (maxLength+1) + formatStr2);
You need to add 1 to maxLength to allow one additional space in the field when the maximum length string is displayed. Placing the expression maxLength+1 between parentheses ensures that it is evaluated as an arithmetic operation before the string-joining operations are executed. Finally, you use the format string in the code to output values from the array: int numberPerLine(3); for(int i = 0 ; i< values->Length ; i++) { Console::Write(format, values[i]); if((i+1)%numberPerLine == 0) Console::WriteLine(); }
The output statement in the loop uses format as the string for output. With the maxLength plugged into the format string, the output is in columns that are one greater than the maximum length output value. The numberPerLine variable determines how many values appear on a line, so the loop is quite general in that you can vary the number of columns by changing the value of numberPerLine.
Modifying Strings The most common requirement for trimming a string is to trim spaces from both the beginning and the end. The Trim() function for a string object does that: String^ str = {L" Handsome is as handsome does... "}; String^ newStr(str->Trim());
238
❘
CHAPTER 4
ARRAYS, STRINGS, AND POINTERS
The Trim() function in the second statement removes any spaces from the beginning and end of str and returns the result as a new String object stored in newStr. Of course, if you did not want to retain the original string, you could store the result back in str. There’s another version of the Trim() function that allows you to specify the characters that are to be removed from the start and end of the string. This function is very flexible because you have more than one way of specifying the characters to be removed. You can specify the characters in an array and pass the array handle as the argument to the function: String^ toBeTrimmed(L"wool wool sheep sheep wool wool wool"); array<wchar_t>^ notWanted = {L'w',L'o',L'l',L' '}; Console::WriteLine(toBeTrimmed->Trim(notWanted));
Here you have a string, toBeTrimmed, that consists of sheep covered in wool. The array of characters to be trimmed from the string is defi ned by the notWanted array; passing that to the Trim() function for the string removes any of the characters in the array from both ends of the string. Remember, String objects are immutable, so the original string is not being changed in any way — a new string is created and returned by the Trim() operation. Executing this code fragment produces the output: sheep sheep
If you happen to specify the character literals without the L prefi x, they will be of type char (which corresponds to the SByte value class type); however, the compiler arranges that they are converted to type wchar_t. You can also specify the characters that the Trim() function is to remove explicitly as arguments, so you could write the last line of the previous fragment as: Console::WriteLine(toBeTrimmed->Trim(L'w', L'o', L'l', L' '));
This produces the same output as the previous version of the statement. You can have as many arguments of type wchar_t as you like, but if there are a lot of characters to be specified, an array is the best approach. If you want to trim only one end of a string, you can use the TrimEnd() or TrimStart() functions. These come in the same variety of versions as the Trim() function. So: without arguments you trim spaces, with an array argument you trim the characters in the array, and with explicit wchar_t arguments those characters are removed. The inverse of trimming a string is padding it at either end with spaces or other characters. You have PadLeft() and PadRight() functions that pad a string at the left or right end, respectively. The primary use for these functions is in formatting output where you want to place strings either left- or right-justified in a fi xed width field. The simpler versions of the PadLeft() and PadRight() functions accept a single argument specifying the length of the string that is to result from the operation. For example: String^ value(L"3.142"); String^ leftPadded(value->PadLeft(10)); String^ rightPadded(value->PadRight(10));
// Result is L" 3.142" // Result is L"3.142 "
C++/CLI Programming
❘ 239
If the length you specify as the argument is less than or equal to the length of the original string, either function returns a new String object that is identical to the original. To pad a string with a character other than a space, you specify the padding character as the second argument to the PadLeft() or PadRight() functions. Here are a couple of examples of this: String^ value(L"3.142"); String^ leftPadded(value->PadLeft(10, L'*')); String^ rightPadded(value->PadRight(10, L'#'));
// Result is L"*****3.142" // Result is L"3.142#####"
Of course, with all these examples, you could store the result back in the handle referencing the original string, which would discard the original string. The String class also has the ToUpper() and ToLower() functions to convert an entire string to upper- or lowercase. Here’s how that works: String^ proverb(L"Many hands make light work."); String^ upper(proverb->ToUpper()); // Result L"MANY HANDS MAKE LIGHT WORK."
The ToUpper() function returns a new string that is the original string converted to uppercase. You use the Insert() function to insert a string at a given position in an existing string. Here’s an example of doing that: String^ proverb(L"Many hands make light work."); String^ newProverb(proverb->Insert(5, L"deck "));
The function inserts the string specified by the second argument, starting at the index position in the old string, which is specified by the first argument. The result of this operation is a new string containing: Many deck hands make light work.
You can also replace all occurrences of a given character in a string with another character, or all occurrences of a given substring with another substring. Here’s a fragment that shows both possibilities: String^ proverb(L"Many hands make light work."); Console::WriteLine(proverb->Replace(L' ', L'*')); Console::WriteLine(proverb->Replace(L"Many hands", L"Pressing switch"));
Executing this code fragment produces the output: Many*hands*make*light*work. Pressing switch make light work.
The fi rst argument to the Replace() function specifies the character or substring to be replaced, and the second argument specifies the replacement.
Comparing Strings You can compare two String objects using the Compare() function in the String class. The function returns an integer that is less than zero, equal to zero, or greater than zero, depending on
240
❘
CHAPTER 4
ARRAYS, STRINGS, AND POINTERS
whether the fi rst argument is less than, equal to, or greater than the second argument. Here’s an example: String^ him(L"Jacko"); String^ her(L"Jillo"); int result(String::Compare(him, her)); if(result < 0) Console::WriteLine(L"{0} is less than {1}.", him, her); else if(result > 0) Console::WriteLine(L"{0} is greater than {1}.", him, her); else Console::WriteLine(L"{0} is equal to {1}.", him, her);
You store the integer that the Compare() function returns in result, and use that in the if statement to decide the appropriate output. Executing this fragment produces the output: Jacko is less than Jillo.
There’s another version of Compare() that requires a third argument of type bool. If the third argument is true, then the strings referenced by the fi rst two arguments are compared, ignoring case; if the third argument is false, then the behavior is the same as the previous version of Compare().
Searching Strings Perhaps the simplest search operation is to test whether a string starts or ends with a given substring. The StartsWith() and EndsWith() functions do that. You supply a handle to the substring you are looking for as the argument to either function, and the function returns a bool value that indicates whether or not the substring is present. Here’s a fragment showing how you might use the StartsWith() function: String^ sentence(L"Hide, the cow's outside."); if(sentence->StartsWith(L"Hide")) Console::WriteLine(L"The sentence starts with 'Hide'.");
Executing this fragment results in the output: The sentence starts with 'Hide'.
Of course, you could also apply the EndsWith() function to the sentence string: Console::WriteLine(L"The sentence does{0} end with 'outside'.", sentence->EndsWith(L"outside") ? L"" : L" not");
The result of the conditional operator expression is inserted into the output string. This is an empty string if EndsWith() returns true, and L“not” if it returns false. In this instance the function returns false (because of the period at the end of the sentence string). The IndexOf() function searches a string for the fi rst occurrence of a specified character or substring, and returns the index if it is present, or -1 if it is not found. You specify the character or the substring you are looking for as the argument to the function. For example:
C++/CLI Programming
String^ sentence(L"Hide, the cow's outside."); int ePosition(sentence->IndexOf(L'e')); int thePosition(sentence->IndexOf(L"the"));
❘ 241
// Returns 3 // Returns 6
The fi rst search is for the letter ‘e’ and the second is for the word “the”. The values returned by the IndexOf() function are indicated in the comments. More typically, you will want to fi nd all occurrences of a given character or substring. Another version of the IndexOf() function is designed to be used repeatedly, to enable you to do that. In this case, you supply a second argument specifying the index position where the search is to start. Here’s an example of how you might use the function in this way: String^ words(L"wool wool sheep sheep wool wool wool"); String^ word(L"wool"); int index(0); int count(0); while((index = words->IndexOf(word,index)) >= 0) { index += word->Length; ++count; } Console::WriteLine(L"'{0}' was found {1} times in:\n{2}", word, count, words);
This fragment counts the number of occurrences of “wool” in the words string. The search operation appears in the while loop condition, and the result is stored in index. The loop continues as long as index is non-negative; when IndexOf() returns -1 the loop ends. Within the loop body, the value of index is incremented by the length of word, which moves the index position to the character following the instance of word that was found, ready for the search on the next iteration. The count variable is incremented within the loop, so when the loop ends it has accumulated the total number of occurrences of word in words. Executing the fragment results in the following output: 'wool' was found 5 times in: wool wool sheep sheep wool wool wool
The LastIndexOf() function is similar to the IndexOf() function except that it searches backwards through the string from the end or from a specified index position. Here’s how the operation performed by the previous fragment could be performed using the LastIndexOf() function: int index(words->Length - 1); int count(0); while(index >= 0 && (index = words->LastIndexOf(word,index)) >= 0) { --index; ++count; }
With the word and words strings the same as before, this fragment produces the same output. Because LastIndexOf() searches backwards, the starting index is the last character in the string, which is words->Length-1. When an occurrence of word is found, you must now decrement index
242
❘
CHAPTER 4
ARRAYS, STRINGS, AND POINTERS
by 1, so that the next backward search starts at the character preceding the current occurrence of word. If word occurs right at the beginning of words — at index position 0 — decrementing index results in –1, which is not a legal argument to the LastIndexOf() function because the search starting position must always be within the string. The additional check for a negative value of index in the loop condition prevents this from happening; if the left operand of the && operator is false, the right operand is not evaluated. The last search function I want to mention is IndexOfAny(), which searches a string for the fi rst occurrence of any character in the array of type array<wchar_t> that you supply as the argument. Similar to the IndexOf() function, the IndexOfAny() function comes in versions that search from the beginning of a string or from a specified index position. Let’s try a full working example of using the IndexOfAny() function.
TRY IT OUT
Searching for Any of a Set of Characters
This example searches a string for punctuation characters: // Ex4_19.cpp : main project file. // Searching for punctuation Available for download on Wrox.com
#include "stdafx.h" using namespace System; int main(array<System::String ^> ^args) { array<wchar_t>^ punctuation = {L'"', L'\'', L'.', L',', L':', L';', L'!', L'?'}; String^ sentence(L"\"It's chilly in here\", the boy's mother said coldly."); // Create array of space characters same length as sentence array<wchar_t>^ indicators(gcnew array<wchar_t>(sentence->Length){L' '}); int index(0); // Index of character found int count(0); // Count of punctuation characters while((index = sentence->IndexOfAny(punctuation, index)) >= 0) { indicators[index] = L'^'; // Set marker ++index; // Increment to next character ++count; // Increase the count } Console::WriteLine(L"There are {0} punctuation characters in the string:", count); Console::WriteLine(L"\n{0}\n{1}", sentence, gcnew String(indicators)); return 0; } code snippet Ex4_19.cpp
C++/CLI Programming
❘ 243
This example should produce the following output: There are 6 punctuation characters in the string: "It's chilly in here", the boy's mother said coldly. ^ ^ ^^ ^ ^
How It Works You fi rst create an array containing the characters to be found and the string to be searched: array<wchar_t>^ punctuation = {L'"', L'\'', L'.', L',', L':', L';', L'!', L'?'}; String^ sentence(L"\"It's chilly in here\", the boy's mother said coldly.");
Note that you must specify a single quote character using an escape sequence because a single quote is a delimiter in a character literal. You can use a double quote explicitly in a character literal, as there’s no risk of it being interpreted as a delimiter in this context. Next you defi ne an array of characters with the elements initialized to a space character: array<wchar_t>^ indicators(gcnew array<wchar_t>(sentence->Length){L' '});
This array has as many elements as the sentence string has characters. You’ll be using this array in the output to mark where punctuation characters occur in the sentence string. You’ll just change the appropriate array element to ‘^’ whenever a punctuation character is found. Note how a single initializer between the braces following the array specification can be used to initialize all the elements in the array. The search takes place in the while loop: while((index = sentence->IndexOfAny(punctuation, index)) >= 0) { indicators[index] = L'^'; // Set marker ++index; // Increment to next character ++count; // Increase the count }
The loop condition is essentially the same as you have seen in earlier code fragments. Within the loop body, you update the indicators array element at position index to be a ‘^’ character, before incrementing index, ready for the next iteration. When the loop ends, count will contain the number of punctuation characters that were found, and indicators will contain ‘^’ characters at the positions in the sentence where such characters were found. The output is produced by the following statements: Console::WriteLine(L"There are {0} punctuation characters in the string:", count); Console::WriteLine(L"\n{0}\n{1}" sentence, gcnew String(indicators));
The second statement creates a new String object on the heap from the indicators array by passing the array to the String class constructor. A class constructor is a function that will create a class object when it is called. You’ll learn more about constructors when you get into defi ning your own classes.
244
❘
CHAPTER 4
ARRAYS, STRINGS, AND POINTERS
Tracking References A tracking reference provides a similar capability to a native C++ reference in that it represents an alias for something on the CLR heap. You can create tracking references to value types on the stack and to handles in the garbage- collected heap; the tracking references themselves are always created on the stack. A tracking reference is automatically updated if the object referenced is moved by the garbage collector. You defi ne a tracking reference using the % operator. For example, here’s how you could create a tracking reference to a value type: int value(10); int% trackValue(value);
The second statement defi nes trackValue to be a tracking reference to the variable value, which has been created on the stack. You can now modify value using trackValue: trackValue *= 5; Console::WriteLine(value);
Because trackValue is an alias for value, the second statement outputs 50.
Interior Pointers Although you cannot perform arithmetic on the address in a tracking handle, C++/CLI does provide a form of pointer with which it is possible to apply arithmetic operations; it’s called an interior pointer, and it is defi ned using the keyword interior_ptr. The address stored in an interior pointer can be updated automatically by the CLR garbage collection when necessary. An interior pointer is always an automatic variable that is local to a function. Here’s how you could defi ne an interior point containing the address of the fi rst element in an array: array<double>^ data = {1.5, 3.5, 6.7, 4.2, 2.1}; interior_ptr<double> pstart(&data[0]);
You specify the type of object pointed to by the interior pointer between angled brackets following the interior_ptr keyword. In the second statement here you initialize the pointer with the address of the fi rst element in the array using the & operator, just as you would with a native C++ pointer. If you do not provide an initial value for an interior pointer, it is initialized with nullptr by default. An array is always allocated on the CLR heap, so here’s a situation where the garbage collector may adjust the address contained in an interior pointer. There are constraints on the type specification for an interior pointer. An interior pointer can contain the address of a value class object on the stack, or the address of a handle to an object on the CLR heap; it cannot contain the address of a whole object on the CLR heap. An interior pointer can also point to a native class object or a native pointer. You can also use an interior pointer to hold the address of a value class object that is part of an object on the heap, such as an element of a CLR array. This way, you can create an interior pointer
C++/CLI Programming
❘ 245
that can store the address of a tracking handle to a System::String object, but you cannot create an interior pointer to store the address of the String object itself. For example: interior_ptr<String^> pstr1; interior_ptr<String> pstr2;
// OK - pointer to a handle // Will not compile - pointer to a String object
All the arithmetic operations that you can apply to a native C++ pointer you can also apply to an interior pointer. You can increment and decrement an interior pointer to change the address it contains, to refer to the following or preceding data item. You can also add or subtract integer values and compare interior pointers. Let’s put together an example that does some of that.
TRY IT OUT
Creating and Using Interior Pointers
This example exercises interior pointers with numerical values and strings: // Ex4_20.cpp : main project file. // Creating and using interior pointers Available for download on Wrox.com
#include "stdafx.h" using namespace System; int main(array<System::String ^> ^args) { // Access array elements through a pointer array<double>^ data = {1.5, 3.5, 6.7, 4.2, 2.1}; interior_ptr<double> pstart(&data[0]); interior_ptr<double> pend(&data[data->Length - 1]); double sum(0.0); while(pstart ^ strings = { L"Land ahoy!", L"Splice the mainbrace!", L"Shiver me timbers!", L"Never throw into the wind!" }; for(interior_ptr<String^> pstrings = &strings[0] ; pstrings-&strings[0] < strings->Length ; ++pstrings) Console::WriteLine(*pstrings); return 0; } code snippet Ex4_20.cpp
246
❘
CHAPTER 4
ARRAYS, STRINGS, AND POINTERS
The output from this example is: Total of data array elements = 18 Land ahoy! Splice the mainbrace! Shiver me timbers! Never throw into the wind!
How It Works After creating the data array of elements of type double, you defi ne two interior pointers: interior_ptr<double> pstart(&data[0]); interior_ptr<double> pend(&data[data->Length - 1]);
The fi rst statement creates pstart as a pointer to type double and initializes it with the address of the fi rst element in the array, data[0]. The interior pointer, pend, is initialized with the address of the last element in the array, data[data->Length - 1]. Because data->Length is the number of elements in the array, subtracting 1 from this value produces the index for the last element. The while loop accumulates the sum of the elements in the array: while(pstart pend). Within the loop, pstart starts out containing the address of the fi rst array element. The value of the fi rst element is obtained by dereferencing the pointer with the expression *pstart; the result of this is added to sum. The address in the pointer is then incremented using the ++ operator. On the last loop iteration, pstart contains the address of the last element, which is the same as the address value that pend contains So, incrementing pstart makes the loop condition false because pstart is then greater than pend. After the loop ends the value of sum is written out, so you can confi rm that the while loop is working as it should. Next you create an array of four strings: array<String^>^ strings = { L"Land ahoy!", L"Splice the mainbrace!", L"Shiver me timbers!", L"Never throw into the wind!" };
The for loop then outputs each string to the command line: for(interior_ptr<String^> pstrings = &strings[0] ; pstrings-&strings[0] < strings->Length ; ++pstrings) Console::WriteLine(*pstrings);
Summary
❘ 247
The fi rst expression in the for loop condition declares the interior pointer, pstrings, and initializes it with the address of the fi rst element in the strings array. The second expression determines whether the for loop continues: pstrings-&strings[0] < strings->Length
As long as pstrings contains the address of a valid array element, the difference between the address in pstrings and the address of the fi rst element in the array is less than the number of elements in the array, given by the expression strings->Length. Thus, when this difference equals the length of the array, the loop ends. You can see from the output that everything works as expected. The most frequent use of an interior pointer is referencing objects that are part of a CLR heap object, and you’ll see more about this later in the book.
SUMMARY You are now familiar with all of the basic types of values in C++, how to create and use arrays of those types, and how to create and use pointers. You have also been introduced to the idea of a reference. However, we have not exhausted all of these topics. I’ll come back to the topics of arrays, pointers, and references later in the book. The pointer mechanism is sometimes a bit confusing because it can operate at different levels within the same program. Sometimes it is operating as an address, and at other times it can be operating with the value stored at an address. It’s very important that you feel at ease with the way pointers are used, so if you fi nd that they are in any way unclear, try them out with a few examples of your own until you feel confident about applying them.
EXERCISES You can download the source code for the examples in the book and the solutions to the following exercises from www.wrox.com.
1.
Write a native C++ program that allows an unlimited number of values to be entered and stored in an array allocated in the free store. The program should then output the values, five to a line, followed by the average of the values entered. The initial array size should be five elements. The program should create a new array with five additional elements, when necessary, and copy values from the old array to the new.
2.
Repeat the previous exercise but use pointer notation throughout instead of arrays.
3.
Declare a character array, and initialize it to a suitable string. Use a loop to change every other character to uppercase. Hint : In the ASCII character set, values for uppercase characters are 32 less than their lowercase counterparts.
248
❘
CHAPTER 4
ARRAYS, STRINGS, AND POINTERS
4.
Write a C++/CLI program that creates an array with a random number of elements of type int. The array should have from 10 to 20 elements. Set the array elements to random values between 100 and 1000. Output the elements, five to a line, in ascending sequence without sorting the array; for example, find the smallest element and output that, then the next smallest, and so on.
5.
Write a C++/CLI program that will generate a random integer greater than 10,000. Output the integer and then output the digits in the integer in words. For example, if the integer generated were 345678, then the output should be: The value is 345678 three four five six seven eight
6.
Write a C++/CLI program that creates an array containing the following strings: "Madam I'm Adam." "Don't cry for me, Marge and Tina." "Lid off a daffodil." "Red lost soldier." "Cigar? Toss it in a can. It is so tragic."
The program should examine each string in turn, output the string, and indicate whether it is or is not a palindrome (that is, whether it is the same sequence of letters reading backward or forward, ignoring spaces and punctuation).
Summary
❘ 249
WHAT YOU LEARNED IN THIS CHAPTER TOPIC
CONCEPT
Native C++ arrays
An array allows you to manage a number of variables of the same type using a single name. Each dimension of an array is defined between square brackets, following the array name in the declaration of the array.
Array dimensions
Each dimension of an array is indexed starting from zero. Thus, the fifth element of a one - dimensional array has the index value 4.
Initializing arrays
Arrays can be initialized by placing the initializing values between curly braces in the declaration.
Pointers
A pointer is a variable that contains the address of another variable. A pointer is declared as a ‘pointer to type’ and may only be assigned addresses of variables of the given type.
Pointers to const and const pointers
A pointer can point to a constant object. Such a pointer can be reassigned to another object. A pointer may also be defined as const, in which case it can’t be reassigned.
References
A reference is an alias for another variable, and can be used in the same places as the variable it references. A reference must be initialized in its declaration. A reference can’t be reassigned to another variable.
The sizeof operator
The operator sizeof returns the number of bytes occupied by the object specified as its argument. Its argument may be a variable or a type name between parentheses.
The new operator
The operator new allocates memory dynamically in the free store in a native C++ application. When memory has been assigned as requested, it returns a pointer to the beginning of the memory area provided. If memory cannot be assigned for any reason, an exception is thrown that by default causes the program to terminate.
The gcnew operator
In a CLR program, you allocate memory in the garbage - collected heap using the gcnew operator.
Reference class objects
Reference class objects in general, and String objects in particular, are always allocated on the CLR heap.
String class objects
You use String objects when working with strings in a CLR program.
CLR arrays
The CLR has its own array types with more functionality that native array types. CLR arrays are created on the CLR heap.
Tracking handles
A tracking handle is a form of pointer used to reference variables defined on the CLR heap. A tracking handle is automatically updated if what it refers to is relocated in the heap by the garbage collector. Variables that reference objects and arrays on the heap are always tracking handles.
250
❘
CHAPTER 4
ARRAYS, STRINGS, AND POINTERS
TOPIC
CONCEPT
Tracking references
A tracking reference is similar to a native reference, except that the address it contains is automatically updated if the object referenced is moved by the garbage collector.
Interior pointers
An interior pointer is a C++/CLI pointer type to which you can apply the same operation as a native pointer.
Modifying interior pointers
The address contained in an interior pointer can be modified using arithmetic operations and still maintain an address correctly, even when referring to something stored in the CLR heap.
5
Introducing Structure into Your Programs WHAT YOU WILL LEARN IN THIS CHAPTER:
➤
How to declare and write your own C++ functions
➤
How function arguments are defined and used
➤
How to pass arrays to and from a function
➤
What pass-by-value means
➤
How to pass pointers to functions
➤
How to use references as function arguments, and what pass-by-reference means
➤
How the const modifier affects function arguments
➤
How to return values from a function
➤
How to use recursion
Up to now, you haven’t really been able to structure your program code in a modular fashion, because you have only been able to construct a program as a single function, main(); but you have been using library functions of various kinds as well as functions belonging to objects. Whenever you write a C++ program, you should have a modular structure in mind from the outset, and as you’ll see, a good understanding of how to implement functions is essential to object- oriented programming in C++. There’s quite a lot to structuring your C++ programs, so to avoid indigestion, you won’t try to swallow the whole thing in one gulp. After you have chewed over and gotten the full flavor of these morsels, you’ll move on to the next chapter, where you will get further into the meat of the topic.
252
❘
CHAPTER 5
INTRODUCING STRUCTURE INTO YOUR PROGRAMS
UNDERSTANDING FUNCTIONS First, let us take a look at the broad principles of how a function works. A function is a selfcontained block of code with a specific purpose. A function has a name that both identifies it and is used to call it for execution in a program. The name of a function is global but is not necessarily unique in C++, as you’ll see in the next chapter; however, functions that perform different actions should generally have different names. The name of a function is governed by the same rules as those for a variable. A function name is, therefore, a sequence of letters and digits, the fi rst of which is a letter, where an underscore (_) counts as a letter. The name of a function should generally reflect what it does, so, for example, you might call a function that counts beans count_beans(). You pass information to a function by means of arguments specified when you invoke it. These arguments need to correspond with parameters that appear in the defi nition of the function. The arguments that you specify replace the parameters used in the defi nition of the function when the function executes. The code in the function then executes as though it were written using your argument values. Figure 5-1 illustrates the relationship between arguments in the function call and the parameters specified in the defi nition of the function.
Arguments
cout Length; i++) if(max->CompareTo(x[i]) < 0) max = x[i]; return max; }
This generic function does the same job as the native C++ function template you saw earlier in this chapter. The generic keyword in the fi rst line identifies what follows as a generic specification, and the fi rst line defi nes the type parameter for the function as T; the typename keyword between the angled brackets indicates that the T that follows is the name of a type parameter in the generic function, and that this type parameter is replaced by an actual type when the generic function is used. For a generic function with multiple type parameters, the parameter names go between the angled brackets, each preceded by the typename keyword and separated by commas. The where keyword that follows the closing angled bracket introduces a constraint on the actual type that may be substituted for T when you use the generic function. This particular constraint says that any type that is to replace T in the generic function must implement the IComparable interface. You’ll learn about interfaces later in the book, but for now, I’ll say that it implies that the type must define the CompareTo() function that allows two objects of the type to be compared. Without this constraint, the compiler has no knowledge of what operations are possible for the type that is to replace T because, until the generic function is used, this is completely unknown. With the constraint, you can use the CompareTo() function to compare max with an element of the array. The CompareTo() function returns an integer value that is less than zero when the object for which it is called (max, in this case) is less than the argument, zero if it equals the argument, and greater than zero if it is greater than the argument. The second line specifies the generic function name MaxElement, its return type T, and its parameter list. This looks rather like an ordinary function header, except that it involves the generic type parameter T. The return type for the generic function and the array element type that is part of the parameter type specification are both of type T, so both these types are determined when the generic function is used.
Using Generic Functions The simplest way of calling a generic function is just to use it like any ordinary function. For example, you could use the generic MaxElement() from the previous section like this:
C++/CLI Programming
❘ 339
array<double>^ data = {1.5, 3.5, 6.7, 4.2, 2.1}; double maxData = MaxElement(data);
The compiler is able to deduce that the type argument to the generic function is double in this instance, and generates the code to call the function accordingly. The function executes with instances of T in the function being replaced by double. As I said earlier, this is not like a template function; there is no creation of function instances at compile time. The compiled generic function is able to handle type argument substitutions when it is called. Note that if you pass a string literal as an argument to a generic function, the compiler deduces that the type argument is String^, regardless of whether the string literal is a narrow string constant such as “Hello!” or a wide string constant such as L“Hello!“. It is possible that the compiler may not be able to deduce the type argument from a call of a generic function. In these instances, you can specify the type argument(s) explicitly between angled brackets following the function name in the call. For example, you could write the call in the previous fragment as: double maxData = MaxElement<double>(data);
With an explicit type argument specified, there is no possibility of ambiguity. There are limitations on what types you can supply as a type argument to a generic function. A type argument cannot be a native C++ class type, nor a native pointer or reference, nor a handle to a value class type such as int^. Thus, only value class types such as int or double and tracking handles such as String^ are allowed (but not a handle to a value class type). Let’s try a working example.
TRY IT OUT
Using a Generic Function
Here’s an example that defi nes and uses three generic functions: // Ex6_11.cpp : main project file. // Defining and using generic functions Available for download on Wrox.com
#include "stdafx.h" using namespace System; // Generic function to find the maximum element in an array generic where T:IComparable T MaxElement(array^ x) { T max(x[0]); for(int i = 1; i < x->Length; i++) if(max->CompareTo(x[i]) < 0) max = x[i]; return max; } // Generic function to remove an element from an array generic where T:IComparable
340
❘
CHAPTER 6
MORE ABOUT PROGRAM STRUCTURE
array^ RemoveElement(T element, array^ data) { array^ newData = gcnew array(data->Length - 1); int index(0); // Index to elements in newData array bool found(false); // Indicates that the element to remove was found for each(T item in data) { // Check for invalid index or element found if((!found) && item->CompareTo(element) == 0) { found = true; continue; } else { if(newData->Length == index) { Console::WriteLine(L"Element to remove not found"); return data; } newData[index++] = item; } } return newData; } // Generic function to list an array generic void ListElements(array^ data) { for each(T item in data) Console::Write(L"{0,10}", item); Console::WriteLine(); } int main(array<System::String ^> ^args) { array<double>^ data = {1.5, 3.5, 6.7, 4.2, 2.1}; Console::WriteLine(L"Array contains:"); ListElements(data); Console::WriteLine(L"\nMaximum element = {0}\n", MaxElement(data)); array<double>^ result = RemoveElement(MaxElement(data), data); Console::WriteLine(L" After removing maximum, array contains:"); ListElements(result); array^ numbers = {3, 12, 7, 0, 10, 11}; Console::WriteLine(L"\nArray contains:"); ListElements(numbers); Console::WriteLine(L"\nMaximum element = {0}\n", MaxElement(numbers)); Console::WriteLine(L"\nAfter removing maximum, array contains:"); ListElements(RemoveElement(MaxElement(numbers), numbers)); array<String^>^ strings = {L"Many", L"hands", L"make", L"light", L"work"}; Console::WriteLine(L"\nArray contains:"); ListElements(strings);
C++/CLI Programming
❘ 341
Console::WriteLine(L"\nMaximum element = {0}\n", MaxElement(strings)); Console::WriteLine(L"\nAfter removing maximum, array contains:"); ListElements(RemoveElement(MaxElement(strings), strings)); return 0; } code snippet Ex6_11.cpp
The output from this example is: Array contains: 1.5
3.5
6.7
4.2
2.1
Maximum element = 6.7 After removing maximum, array contains: 1.5 3.5 4.2 2.1 Array contains: 3
12
7
0
10
After removing maximum, array contains: 3 7 0 10
11
11
Maximum element = 12
Array contains: Many hands
make
light
work
Maximum element = work
After removing maximum, array contains: Many hands make light
How It Works The fi rst generic function that this example defi nes is MaxElement(), which is identical to the one you saw in the preceding section that fi nds the maximum element in an array, so I won’t discuss it again. The next generic function, RemoveElement(), removes the element passed as the fi rst argument from the array specified by the second argument, and the function returns a handle to the new array that results from the operation. You can see from the fi rst two lines of the function defi nition that both parameter types and the return type involve the type parameter, T. generic where T:IComparable array^ RemoveElement(T element, array^ data)
The constraint on T is the same as for the fi rst generic function and implies that whatever type is used as the type argument must implement the CompareTo() function to allow objects of the type to be compared. The second parameter and the return type are both handles to an array of elements of type T. The fi rst parameter is simply type T.
342
❘
CHAPTER 6
MORE ABOUT PROGRAM STRUCTURE
The function fi rst creates an array to hold the result: array^ newData = gcnew array(data->Length - 1);
The newData array is of the same type as the second argument — an array of elements of type T — but has one fewer element because one element is to be removed from the original. The elements are copied from the data array to the newData array in the for each loop: int index(0); // Index to elements in newData array bool found(false); // Indicates that element to remove was found for each(T item in data) { // Check for invalid index or element found if((!found) && item->CompareTo(element) == 0) { found = true; continue; } else { if(newData->Length == index) { Console::WriteLine(L"Element to remove not found"); return data; } newData[index++] = item; } }
All elements are copied except the one identified by the fi rst argument to the function. You use the index variable to select the next newData array element that is to receive the next element from the data array. Each element from data is copied unless it is equal to element, in which case, found is set to true and the continue statement skips to the next iteration. It is quite possible that an array could have more than one element equal to the fi rst argument, and the found variable prevents subsequent elements that are the same as element from being skipped in the loop. You also have a check for the index variable exceeding the legal limit for indexing elements in newData. This could arise if there is no element in the data array equal to the fi rst argument to the function. In this eventuality, you just return the handle to the original array. The third generic function just lists the elements from an array of type array: generic void ListElements(array^ data) { for each(T item in data) Console::Write(L"{0,10}", item); Console::WriteLine(); }
C++/CLI Programming
❘ 343
This is one of the few occasions when no constraint on the type parameter is needed. There is very little you can do with objects that are of a completely unknown type, so, typically, generic function type parameters will have constraints. The operation of the function is very simple — each element from the array is written to the command line in an output field with a width of 10 in the for each loop. If you want, you could add a little sophistication by adding a parameter for the field width and creating the format string to be used as the fi rst argument to the Write() function in the Console class. You could also add logic to the loop to write a specific number of elements per line based on the field width. The main() function exercises these generic functions with type parameters of types double, int, and String^. Thus, you can see that all three generic functions work with value types and handles. In the second and third examples, the generic functions are used in combination in a single statement. For example, look at this statement in the third sample use of the functions: ListElements(RemoveElement(MaxElement(strings), strings));
The fi rst argument to the RemoveElement() generic function is produced by the MaxElement() generic function call, so these generic functions can be used in the same way as equivalent ordinary functions. The compiler is able to deduce the type argument in all instances where the generic functions are used but you could specify them explicitly if you wanted to. For example, you could write the previous statement like this: ListElements(RemoveElement<String^>(MaxElement<String^>(strings), strings));
A Calculator Program for the CLR Let’s re-implement the calculator as a C++/CLI example. We’ll assume the same program structure and hierarchy of functions as you saw for the native C++ program, but the functions will be declared and defi ned as C++/CLI functions. A good starting point is the set of function prototypes at the beginning of the source fi le — I have used Ex6_12 as the CLR project name: // Ex6_12.cpp : main project file. // A CLR calculator supporting parentheses Available for download on Wrox.com
#include "stdafx.h" #include
// For exit()
using namespace System; String^ eatspaces(String^ str); double expr(String^ str); double term(String^ str, int^ index); double number(String^ str, int^ index); String^ extract(String^ str, int^ index);
// // // // //
Function Function Function Function Function
to eliminate blanks evaluating an expression analyzing a term to recognize a number to extract a substring code snippet Ex6_12.cpp
344
❘
CHAPTER 6
MORE ABOUT PROGRAM STRUCTURE
All the parameters are now handles; the string parameters are type String^ and the index parameter that records the current position in the string is also a handle of type int^. Of course, a string is returned as a handle of type String^. The main() function implementation looks like this:
Available for download on Wrox.com
int main(array<System::String ^> ^args) { String^ buffer; // Input area for expression to be evaluated Console::WriteLine(L"Welcome to your friendly calculator."); Console::WriteLine(L"Enter an expression, or an empty line to quit."); for(;;) { buffer = eatspaces(Console::ReadLine()); if(String::IsNullOrEmpty(buffer)) return 0;
// Read an input line // Empty line ends calculator
Console::WriteLine(L" = {0}\n\n",expr(buffer)); // Output value of expression } return 0; } code snippet Ex6_12.cpp
The function is a lot shorter and easier to read. Within the indefi nite for loop, you call the String class function, IsNullOrEmpty(). This function returns true if the string passed as the argument is null or of zero length, so it does exactly what you want here.
Removing Spaces from the Input String The function to remove spaces is much simpler and shorter:
Available for download on Wrox.com
// Function to eliminate spaces from a string String^ eatspaces(String^ str) { return str->Replace(L" ", L""); } code snippet Ex6_12.cpp
You use the Replace() function that the System::String class defi nes to replace any space in str by an empty string, thus removing all spaces from str. The Replace() function returns the new string with the spaces removed.
C++/CLI Programming
❘ 345
Evaluating an Arithmetic Expression You can implement the function to evaluate an expression like this:
Available for download on Wrox.com
// Function to evaluate an arithmetic expression double expr(String^ str) { int^ index(0); // Keeps track of current character position double value(term(str, index)); while(*index < str->Length) { switch(str[*index]) { case '+': ++(*index); value += term(str, index); break; case '-': ++(*index); value -= term(str, index); break;
// Get first term
// Choose action based on current character // + found so // increment index and add // the next term
// - found so // increment index and subtract // the next term
default: // If we reach here the string is junk Console::WriteLine(L"Arrrgh!*#!! There's an error.\n"); exit(1); } } return value; } code snippet Ex6_12.cpp
The index variable is declared as a handle because you want to pass it to the term() function and allow the term() function to modify the original variable. If you declared index simply as type int, the term() function would receive a copy of the value and could not refer to the original variable to change it. The declaration of index results in a warning message from the compiler, because the statement relies on autoboxing of the value 0 to produce the Int32 value class object that the handle references, and the warning is because people often write the statement like this but intend to initialize the handle to null. Of course, to do this, you must use nullptr as the initial value in place of 0. If you want to eliminate the warning, you could rewrite the statement as: int^ index(gcnew int(0));
This statement uses a constructor explicitly to create the object and initialize it with 0, so there’s no warning from the compiler.
346
❘
CHAPTER 6
MORE ABOUT PROGRAM STRUCTURE
After processing the initial term by calling the term() function, the while loop steps through the string looking for + or - operators followed by another term. The switch statement identifies and processes the operators. If you have been using native C++ for a while, you might be tempted to write the case statements in the switch a little differently — for example: // Incorrect code!! Does not work!! case '+': value += term(str, ++(*index)); break;
// + found so // increment index & add the next term
Of course, this would typically be written without the fi rst comment. This code is wrong, but why is it wrong? The term() function expects a handle in type int^ as the second argument, and that is what is supplied here, although maybe not as you expect. The compiler arranges for the expression ++(*index) to be evaluated and the result stored in a temporary location. The expression indeed updates the value referenced by index, but the handle that is passed to the term() function is a handle to the temporary location holding the result of evaluating the expression, not the handle index. This handle is produced by autoboxing the value stored in the temporary location. When the term() function updates the value referenced by the handle that is passed to it, the temporary location is updated, not the location referenced by index; thus, all the updates to the index position of the string made in the term() function are lost. If you expect a function to update a variable in the calling program, you must not use an expression as the function argument — you must always use the handle variable name.
Obtaining the Value of a Term As in the native C++ version, the term() function steps through the string that is passed as the fi rst argument, starting at the character position referenced by the second argument:
Available for download on Wrox.com
// Function to get the value of a term double term(String^ str, int^ index) { double value(number(str, index));
// Get the first number in the term
// Loop as long as we have characters and a good operator while(*index < str->Length) { if(L'*' == str[*index]) // If it's multiply, { ++(*index); // increment index and value *= number(str, index); // multiply by next number } else if(L'/' == str[*index]) // If it's divide { ++(*index); // increment index and value /= number(str, index); // divide by next number } else break; // Exit the loop }
C++/CLI Programming
❘ 347
// We've finished, so return what we've got return value; } code snippet Ex6_12.cpp
After calling the number() function to get the value of the fi rst number or parenthesized expression in a term, the function steps through the string in the while loop. The loop continues while there are still characters in the input string, as long as a * or / operator followed by another number or parenthesized expression is found.
Evaluating a Number The number() function extracts and evaluates a parenthesized expression if there is one; otherwise, it determines the value of the next number in the input.
Available for download on Wrox.com
// Function to recognize a number double number(String^ str, int^ index) { double value(0.0);
// Store for the resulting value
// Check for expression between parentheses if(L'(' == str[*index]) // Start of parentheses { ++(*index); String^ substr = extract(str, index); // Extract substring in brackets return expr(substr); // Return substring value } // There must be at least one digit... if(!Char::IsDigit(str, *index)) { Console::WriteLine(L"Arrrgh!*#!! There's an error.\n"); exit(1); } // Loop accumulating leading digits while((*index < str->Length) && Char::IsDigit(str, *index)) { value = 10.0*value + Char::GetNumericValue(str[(*index)]); ++(*index); } // Not a digit when we get to here if((*index == str->Length) || str[*index] != '.') return value; double factor(1.0); ++(*index);
// so check for decimal point // and if not, return value
// Factor for decimal places // Move to digit
// Loop as long as we have digits while((*index < str->Length) && Char::IsDigit(str, *index))
348
❘
CHAPTER 6
MORE ABOUT PROGRAM STRUCTURE
{ factor *= 0.1; // Decrease factor by factor of 10 value = value + Char::GetNumericValue(str[*index])*factor; // Add decimal place ++(*index); } return value;
// On loop exit we are done
} code snippet Ex6_12.cpp
As in the native C++ version, the extract() function is used to extract a parenthesized expression, and the substring that results is passed to the expr() function to be evaluated. If there’s no parenthesized expression — indicated by the absence of an opening parenthesis — and there is at least one digit, the input is scanned for a number, which is a sequence of zero or more digits followed by an optional decimal point plus fractional digits. The IsDigit() function in the Char class returns true if a character is a digit and false otherwise. The character here is in the string passed as the fi rst argument to the function at the index position specified by the second argument. There’s another version of the IsDigit() function that accepts a single argument of type wchar_t, so you could use this with the argument str[*index]. The GetNumericValue() function in the Char class returns the value of a Unicode digit character that you pass as the argument as a value of type double. There’s another version of this function to which you can pass a string handle and an index position to specify the character.
Extracting a Parenthesized Substring You can implement the extract() function that returns a parenthesized substring from the input like this:
Available for download on Wrox.com
// Function to extract a substring between parentheses String^ extract(String^ str, int^ index) { int numL(0); // Count of left parentheses found int bufindex(*index); // Save starting value for index while(*index < str->Length) { switch(str[*index]) { case ')': if(0 == numL) return str->Substring(bufindex, (*index)++ - bufindex); else numL--; // Reduce count of '(' to be matched break; case '(': numL++; break; }
// Increase count of '(' to be // matched
Summary
❘ 349
++(*index); } Console::WriteLine(L"Ran off the end of the expression, must be bad input."); exit(1); } code snippet Ex6_12.cpp
The strategy is similar to the native C++ version, but the code is a lot simpler. To fi nd the complementary right parenthesis, the function keeps track of how many new left parentheses are found using the variable numL. When a right parenthesis is found and the left parenthesis count in numL is zero, the Substring() member of the System::String class is used to extract the substring that was between the parentheses. The substring is then returned. If you assemble all the functions in a CLR console project, you’ll have a C++/CLI implementation of the calculator running with the CLR. The output should be much the same as the native C++ version.
SUMMARY You now have a reasonably comprehensive knowledge of writing and using functions, and you have used overloading to implement a set of functions providing the same operation with different types of parameters. You’ll see more about overloading functions in the following chapters. You also got some experience of using several functions in a program by working through the calculator example. But remember that all the uses of functions up to now have been in the context of a traditional procedural approach to programming. When you come to look at object- oriented programming, you will still use functions extensively, but with a very different approach to program structure and to the design of a solution to a problem.
EXERCISES You can download the source code for the examples in the book and the solutions to the following exercises from www.wrox.com.
1.
Consider the following function: int ascVal(size_t i, const char* p) { // print the ASCII value of the char if (!p || i > strlen(p)) return -1; else return p[i]; }
Write a program that will call this function through a pointer and verify that it works. You’ll need a #include directive for the cstring header in your program to use the strlen() function.
350
❘
CHAPTER 6
MORE ABOUT PROGRAM STRUCTURE
2.
Write a family of overloaded functions called equal(), which take two arguments of the same type, returning 1 if the arguments are equal, and 0 otherwise. Provide versions having char, int, double, and char* arguments. (Use the strcmp() function from the runtime library to test for equality of strings. If you don’t know how to use strcmp(), search for it in the online help. You’ll need a #include directive for the cstring header file in your program.) Write test code to verify that the correct versions are called.
3.
At present, when the calculator hits an invalid input character, it prints an error message, but doesn’t show you where the error was in the line. Write an error routine that prints out the input string, putting a caret (^) below the offending character, like this: 12 + 4,2*3 ^
4.
Add an exponentiation operator, ^, to the calculator, fitting it in alongside * and /. What are the limitations of implementing it in this way, and how can you overcome them?
5.
(Advanced) Extend the calculator so it can handle trig and other math functions, allowing you to input expressions such as: 2 * sin(0.6)
The math library functions all work in radians; provide versions of the trigonometric functions so that the user can use degrees, for example: 2 * sind(30)
Summary
❘ 351
WHAT YOU LEARNED IN THIS CHAPTER TOPIC
CONCEPT
Pointers to functions
A pointer to a function stores the address of a function, plus information about the number and types of parameters and return type for a function.
Pointers to functions
You can use a pointer to a function to store the address of any function with the appropriate return type, and number and types of parameters.
Pointers to functions
You can use a pointer to a function to call the function at the address it contains. You can also pass a pointer to a function as a function argument.
Exceptions
An exception is a way of signaling an error in a program so that the error handling code can be separated from the code for normal operations.
Throwing exceptions
You throw an exception with a statement that uses the keyword throw.
try blocks
Code that may throw exceptions should be placed in a try block, and the code to handle a particular type of exception is placed in a catch block immediately following the try block. There can be several catch blocks following a try block, each catching a different type of exception.
Overloaded functions
Overloaded functions are functions with the same name, but with different parameter lists.
Calling an overloaded function
When you call an overloaded function, the function to be called is selected by the compiler based on the number and types of the arguments that you specify.
Function templates
A function template is a recipe for generating overloaded functions automatically.
Function template parameters
A function template has one or more parameters that are type variables. An instance of the function template — that is, a function definition — is created by the compiler for each function call that corresponds to a unique set of type arguments for the template.
Function template instances
You can force the compiler to create a particular instance from a function template by specifying the function you want in a prototype declaration.
The decltype operator
The decltype operator produces the type that results from evaluating an expression. You can use the decltype operator to determine the return type of a template function when the return type depends on the particular template parameters.
7
Defining Your Own Data Types WHAT YOU WILL LEARN IN THIS CHAPTER:
➤
How structures are used
➤
How classes are used
➤
The basic components of a class and how you define class types
➤
How to create, and use, objects of a class
➤
How to control access to members of a class
➤
How to create constructors
➤
The default constructor
➤
References in the context of classes
➤
How to implement the copy constructor
➤
How C++/CLI classes differ from native C++ classes
➤
How to define and use properties in a C++/CLI class
➤
How to define and use literal fields
➤
How to define and use initonly fields
➤
What a static constructor is
This chapter is about creating your own data types to suit your particular problem. It’s also about creating objects, the building blocks of object- oriented programming. An object can seem a bit mysterious to the uninitiated, but as you will see in this chapter, an object can be just an instance of one of your own data types.
354
❘
CHAPTER 7 DEFINING YOUR OWN DATA TYPES
THE STRUCT IN C++ A structure is a user-defi ned type that you defi ne using the keyword struct, so it is often referred to as a struct. The struct originated back in the C language, and C++ incorporates and expands on the C struct. A struct in C++ is functionally replaceable by a class insofar as anything you can do with a struct, you can also achieve by using a class; however, because Windows was written in C before C++ became widely used, the struct appears pervasively in Windows programming. It is also widely used today, so you really need to know something about structs. We’ll fi rst take a look at (C -style) structs in this chapter before exploring the more extensive capabilities offered by classes.
What Is a struct? Almost all the variables that you have seen up to now have been able to store a single type of entity — a number of some kind, a character, or an array of elements of the same type. The real world is a bit more complicated than that, and just about any physical object you can think of needs several items of data to describe it even minimally. Think about the information that might be needed to describe something as simple as a book. You might consider title, author, publisher, date of publication, number of pages, price, topic or classification, and ISBN number, just for starters, and you can probably come up with a few more without too much difficulty. You could specify separate variables to contain each of the parameters that you need to describe a book, but ideally, you would want to have a single data type, BOOK, say, which embodied all of these parameters. I’m sure you won’t be surprised to hear that this is exactly what a struct can do for you.
Defining a struct Let’s stick with the notion of a book, and suppose that you just want to include the title, author, publisher, and year of publication within your defi nition of a book. You could declare a structure to accommodate this as follows: struct BOOK { char Title[80]; char Author[80]; char Publisher[80]; int Year; };
This doesn’t define any variables, but it does create a new type for variables, and the name of the type is BOOK. The keyword struct defines BOOK as such, and the elements making up an object of this type are defined within the braces. Note that each line defining an element in the struct is terminated by a semicolon, and that a semicolon also appears after the closing brace. The elements of a struct can be of any type, except the same type as the struct being defined. You couldn’t have an element of type BOOK included in the structure definition for BOOK, for example. You may think this is a limitation, but note that you could include a pointer to a variable of type BOOK, as you’ll see a little later on.
The struct in C++
❘ 355
The elements Title, Author, Publisher, and Year enclosed between the braces in the defi nition above may also be referred to as members or fields of the BOOK structure. Each object of type BOOK contains the members Title, Author, Publisher, and Year. You can now create variables of type BOOK in exactly the same way that you create variables of any other type: BOOK Novel;
// Declare variable Novel of type BOOK
This declares a variable with the name Novel that you can now use to store information about a book. All you need now is to understand how you get data into the various members that make up a variable of type BOOK.
Initializing a struct The fi rst way to get data into the members of a struct is to defi ne initial values in the declaration. Suppose you wanted to initialize the variable Novel to contain the data for one of your favorite books, Paneless Programming, published in 1981 by the Gutter Press. This is a story of a guy performing heroic code development while living in an igloo, and, as you probably know, inspired the famous Hollywood box office success, Gone with the Window. It was written by I.C. Fingers, who is also the author of that seminal three-volume work, The Connoisseur’s Guide to the Paper Clip. With this wealth of information, you can write the declaration for the variable Novel as: BOOK Novel = { "Paneless Programming", "I.C. Fingers", "Gutter Press", 1981 };
// // // //
Initial Initial Initial Initial
value value value value
for for for for
Title Author Publisher Year
The initializing values appear between braces, separated by commas, in much the same way that you defined initial values for members of an array. As with arrays, the sequence of initial values obviously needs to be the same as the sequence of the members of the struct in its definition. Each member of the structure Novel has the corresponding value assigned to it, as indicated in the comments.
Accessing the Members of a struct To access individual members of a struct, you can use the member selection operator, which is a period; this is sometimes referred to as the member access operator. To refer to a particular member, you write the struct variable name, followed by a period, followed by the name of the member that you want to access. To change the Year member of the Novel structure, you could write: Novel.Year = 1988;
This would set the value of the Year member to 1988. You can use a member of a structure in exactly the same way as any other variable of the same type as the member. To increment the member Year by two, for example, you can write: Novel.Year += 2;
This increments the value of the Year member of the struct, just like any other variable.
356
❘
CHAPTER 7 DEFINING YOUR OWN DATA TYPES
TRY IT OUT
Using structs
You can use another console application example to exercise a little further how referencing the members of a struct works. Suppose you want to write a program to deal with some of the things you might fi nd in a yard, such as those illustrated in the professionally landscaped yard in Figure 7-1.
House
10
Position 0,0
Hut 25
90
80
40
30
70
30
110
Pool 70
10
Hut 25
Position 100,120
FIGURE 7-1
I have arbitrarily assigned the coordinates 0,0 to the top -left corner of the yard. The bottom-right corner has the coordinates 100,120. Thus, the fi rst coordinate value is a measure of the horizontal position relative to the top -left corner, with values increasing from left to right, and the second coordinate is a measure of the vertical position from the same reference point, with values increasing from top to bottom. Figure 7-1 also shows the position of the pool and those of the two huts relative to the top -left corner of the yard. Because the yard, huts, and pool are all rectangular, you could defi ne a struct type to represent any of these objects: struct RECTANGLE { int Left;
// Top-left point
The struct in C++
int Top;
// coordinate pair
int Right; int Bottom;
// Bottom-right point // coordinate pair
❘ 357
};
The fi rst two members of the RECTANGLE structure type correspond to the coordinates of the top-left point of a rectangle, and the next two to the coordinates of the bottom-right point. You can use this in an elementary example dealing with the objects in the yard as follows:
Available for download on Wrox.com
// Ex7_01.cpp // Exercising structures in the yard #include using std::cout; using std::endl;
// Definition of a struct to represent rectangles struct RECTANGLE { int Left; // Top-left point int Top; // coordinate pair int Right; int Bottom;
// Bottom-right point // coordinate pair
}; // Prototype of function to calculate the area of a rectangle long Area(const RECTANGLE& aRect); // Prototype of a function to move a rectangle void MoveRect(RECTANGLE& aRect, int x, int y); int main(void) { RECTANGLE Yard = { 0, 0, 100, 120 }; RECTANGLE Pool = { 30, 40, 70, 80 }; RECTANGLE Hut1, Hut2; Hut1.Left = 70; Hut1.Top = 10; Hut1.Right = Hut1.Left + 25; Hut1.Bottom = 30; Hut2 = Hut1; MoveRect(Hut2, 10, 90); cout (const CBox& aBox) const { return this->Volume() > aBox.Volume(); } // Function to compare a CBox object with a constant bool operator>(const double& value) const { return Volume() > value; } // Function to add two CBox objects CBox operator+(const CBox& aBox) const { // New object has larger length & width, and sum of heights return CBox(m_Length > aBox.m_Length ? m_Length : aBox.m_Length, m_Width > aBox.m_Width ? m_Width : aBox.m_Width, m_Height + aBox.m_Height); } // Function to show the dimensions of a box void ShowBox() const { cout theMax) theMax = m_Values[i]; return theMax;
// Check all the samples // Store any larger sample
} };
The value supplied for Size when you create an object will replace the appearance of the parameter throughout the template defi nition. Now, you can declare the CSamples object from the previous example as: CSamples myBoxes(boxes, sizeof boxes/sizeof CBox);
Because you can supply any constant expression for the Size parameter, you could also have written this as: CSamples myBoxes(boxes, sizeof boxes/sizeof CBox);
The example is a poor use of a template, though — the original version was much more usable. A consequence of making Size a template parameter is that instances of the template that store the same types of objects but have different size parameter values are totally different classes and cannot be mixed. For instance, an object of type CSamples<double, 10> cannot be used in an expression with an object of type CSamples<double, 20>. You need to be careful with expressions that involve comparison operators when instantiating templates. Look at this statement: CSamples y ? 10 : 20 > MyType();
// Wrong!
This will not compile correctly because the > preceding y in the expression will be interpreted as a right-angled bracket. Instead, you should write this statement as: CSamples y ? 10 : 20) > MyType();
// OK
The parentheses ensure that the expression for the second template argument doesn’t get mixed up with the angled brackets.
486
❘
CHAPTER 8
MORE ON CLASSES
Templates for Function Objects A class that defi nes function objects is typically defi ned by a template, for the obvious reason that it allows function objects to be defi ned that will work with a variety of argument types. Here’s a template for the Area class that you saw earlier: template class Area { public: T operator()(T length, T width){ return length*width; };
}
This template will allow you to defi ne function objects to calculate areas with the dimensions of any numeric type. You could defi ne the printArea() function that you saw earlier as a function template: template void printArea(T length, T width, Area area) { cout operator.
➤
Compare the volume of a CBox object with a specified value, and vice versa. You also have an implementation of this for the > operator, but you will also need to implement functions supporting the other comparison operators.
➤
Add two CBox objects to produce a new CBox object that will contain both the original objects. Thus, the result will be at least the sum of the volumes, but may be larger. You have a version of this already that overloads the + operator.
➤
Multiply a CBox object by an integer (and vice versa) to provide a new CBox object that will contain a specified number of the original objects. This is effectively designing a carton.
➤
Determine how many CBox objects of a given size can be packed in another CBox object of a given size. This is effectively division, so you could implement this by overloading the / operator.
➤
Determine the volume of space remaining in a CBox object after packing it with the maximum number of CBox objects of a given size.
I had better stop right there! There are undoubtedly other functions that would be very useful but, in the interest of saving trees, we’ll consider the set to be complete, apart from ancillaries such as accessing dimensions, for example.
Implementing the CBox Class You really need to consider the degree of error protection that you want to build into the CBox class. The basic class that you defi ned to illustrate various aspects of classes is a starting point, but you should also consider some points a little more deeply. The constructor is a little weak in that it doesn’t ensure that the dimensions for a CBox are valid, so, perhaps, the fi rst thing you should do is to ensure that you always have valid objects. You could redefi ne the basic class as follows to do this: class CBox // Class definition at global scope { public: // Constructor definition explicit CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0)
488
❘
CHAPTER 8
MORE ON CLASSES
{ lv = lv , >=, ==, a CBox object bool operator>(const double& value, const CBox& aBox) {
Using Classes
❘ 489
return value > aBox.Volume(); }
You can now write the operator(const CBox& aBox, const double& value) { return value < aBox; } // Function for testing if CBox object is < a constant int operator aBox; }
You just use the appropriate overloaded operator function that you wrote before, with the arguments from the call to the new function switched. The functions implementing the >= and tc2 ? tc1 : tc2)); }
This function fi rst determines how many of the right- operand CBox objects can fit in a layer with their lengths aligned with the length dimension of the left- operand CBox. This is stored in tc1. You then calculate how many can fit in a layer with the lengths of the right- operand CBoxes lying in the width direction of the left- operand CBox. Finally, you multiply the larger of tc1 and tc2 by the number of layers you can pack in, and return that value. This process is illustrated in Figure 8-7. How aBox/bBox is calculated L=8
L=3 W=2
aBox
W=6
bBox
H=1
H=2
There are two rectangular arrangements possible for fitting a number of bBox objects within aBox, as shown below.
With this configuration 12 can be stored 8/3 = 2
bBox bBox
2/1 = 2
bBox
bBox
bBox
bBox
bBox
bBox
bBox
bBox
bBox
bBox
bBox
6/2 = 3
bBox
With this configuration 16 can be stored 8/2 = 4
6/3 = 2
2/1 = 2
Result is 16 FIGURE 8-7
Consider two possibilities: fitting bBox into aBox with the length aligned with that of aBox, and then with the length of bBox aligned with the width of aBox. You can see from Figure 8-7 that the best packing results from rotating bBox so that the width divides into the length of aBox.
Using Classes
❘ 493
The other analytical operator function, operator%(), for obtaining the free volume in a packed aBox is easier, because you can use the operator you have just written to implement it. You can write it as an ordinary global function because you don’t need access to the private members of the class. // Operator to return the free volume in a packed box double operator%(const CBox& aBox, const CBox& bBox) { return aBox.Volume() - ((aBox/bBox)*bBox.Volume()); }
This computation falls out very easily using existing class functions. The result is the volume of the big box, aBox, minus the volume of the bBox boxes that can be stored in it. The number of bBox objects packed into aBox is given by the expression aBox/bBox, which uses the previous overloaded operator. You multiply this by the volume of bBox objects to get the volume to be subtracted from the volume of the large box, aBox. That completes the class interface. Clearly, there are many more functions that might be required for a production problem solver but, as an interesting working model demonstrating how you can produce a class for solving a particular kind of problem, it will suffice. Now you can go ahead and try it out on a real problem.
TRY IT OUT
A Multifile Project Using the CBox Class
Before you can actually start writing the code to use the CBox class and its overloaded operators, fi rst you need to assemble the defi nition for the class into a coherent whole. You’re going to take a rather different approach from what you’ve seen previously, in that you’re going to write multiple fi les for the project. You’re also going to start using the facilities that Visual C++ 2010 provides for creating and maintaining code for our classes. This will mean that you do rather less of the work, but it will also mean that the code will be slightly different in places. Start by creating a new WIN32 project for a console application called Ex8_11 and check the Empty project application option. If you select the Class View tab, you’ll see the window shown in Figure 8-8. This shows a view of all the classes in a project but, of course, there are none here for the moment. Although there are no classes defi ned — or anything else, for that matter — Visual C++ 2010 has already made provision for including some. You can use Visual C++ 2010 to create a skeleton for our CBox class, and the fi les that relate to it, too. Right- click FIGURE 8-8 Ex8_11 in Class View and select Add/Class . . . from the pop -up menu that appears. You can then select C++ from the class categories in the left pane of the Add Class dialog that is displayed and the C++ Class template in the right pane, and press Enter. You will then be able to enter the name of the class that you want to create, CBox, in the Generic C++ Class Wizard dialog, as shown in Figure 8-9.
494
❘
CHAPTER 8
MORE ON CLASSES
FIGURE 8-9
The name of the fi le that’s indicated on the dialog, Box.cpp, will be used to contain the class implementation, which consists of the defi nitions for the function members of the class. This is the executable code for the class. You can change the name of this fi le, if you want, but Box.cpp looks like a good name for the fi le in this case. The class defi nition will be stored in a fi le called Box.h. This is the standard way of structuring a program. Code that consists of class defi nitions is stored in fi les with the extension .h, and code that defi nes functions is stored in fi les with the extension .cpp. Usually, each class defi nition goes in its own .h fi le, and each class implementation goes in its own .cpp fi le. When you click the Finish button in the dialog, two things happen:
1.
A fi le Box.h is created, containing a skeleton defi nition for the class CBox. This includes a no-argument constructor and a destructor.
2.
A fi le Box.cpp is created, containing a skeleton implementation for the functions in the class with defi nitions for the constructor and the destructor — both bodies are empty, of course.
The editor pane displaying the code should be as shown in Figure 8-10. If it is not presently displayed, just double- click CBox in Class View, and it should appear. As you can see, above the pane containing the code listing for the class, there are two controls. The left control displays the current class name, CBox, and clicking the button to the right of the class
FIGURE 8-10
Using Classes
❘ 495
name will display the list of all the classes in the project. In general, you can use this control to switch to another class by selecting it from the list, but here, you have just one class defi ned. The control to the right relates to the members defi ned in the .cpp fi le for the current class, and clicking its button will display the members of the class. Selecting a member from the list will cause its code to be visible in the pane below. Let’s start developing the CBox class based on what Visual C++ has provided automatically for us. DEFINING THE CBOX CLASS
If you click the ▲ to the left of Ex8_11 in the Class View, the tree will be expanded and you will see that CBox is now defi ned for the project. All the classes in a project are displayed in this tree. You can view the source code supplied for the defi nition of a class by double- clicking the class name in the tree, or by using the controls above the pane displaying the code, as I described in the previous section. The CBox class defi nition that was generated starts with a preprocessor directive: #pragma once
The effect of this is to prevent the fi le from being opened and included into the source code more than once by the compiler in a build. Typically, a class defi nition will be included into several fi les in a project because each fi le that references the name of a particular class will need access to its defi nition. In some instances, a header fi le may, itself, have #include directives for other header fi les. This can result in the possibility of the contents of a header fi le appearing more than once in the source code. Having more than one defi nition of a class in a build is not allowed and will be flagged as an error. Having the #pragma once directive at the start of every header fi le will ensure this cannot happen. Note that #pragma once is a Microsoft-specific directive that may not be supported in other development environments. If you are developing code that you anticipate may need to be compiled in other environments, you can use the following form of directive in a header fi le to achieve the same effect: // Box.h header file #ifndef BOX_H #define BOX_H // Code that must not be included more than once // such as the CBox class definition #endif
The important lines are bolded and correspond to directives that are supported by any ISO/ANSI C++ compiler. The lines following the #ifndef directive down to the #endif directive will be included in a build as long as the symbol BOX_H is not defi ned. The line following #ifndef defi nes the symbol BOX_H, thus ensuring that the code in this header fi le will not be included a second time. Thus, this has the same effect as placing the #pragma once directive at the beginning of a header fi le. Clearly, the #pragma once directive is simpler and less cluttered, so it ’s better to use that when you only expect to be using your code in the Visual C++ 2010 development environment. You will sometimes see the #ifndef/#endif combination written as: #if !defined BOX_H #define BOX_H
496
❘
CHAPTER 8
MORE ON CLASSES
// Code that must not be included more than once // such as the CBox class definition #endif
The Box.cpp fi le that was generated by Class Wizard contains the following code: #include "Box.h" CBox::CBox(void) { } CBox::~CBox(void) { }
The fi rst line is a #include preprocessor directive that has the effect of including the contents of the Box.h fi le — the class defi nition — into this fi le, Box.cpp. This is necessary because the code in Box.cpp refers to the CBox class name, and the class defi nition needs to be available to assign meaning to the name CBox. ADDING DATA MEMBERS
First, you can add the private data members m_Length, m_Width, and m_Height. Right- click CBox in Class View and select Add/Add Variable . . . from the pop -up menu. You can then specify the name, type, and access for the fi rst data member that you want to add to the class in the Add Member Variable Wizard dialog. The way you specify a new data member in this dialog is quite self- explanatory. If you specify a lower limit for a data member, you must also specify an upper limit. When you specify limits, the constructor defi nition in the .cpp fi le will be modified to add a default value for the data member corresponding to the lower limit. You can add a comment in the lower input field, if you wish. When you click the OK button, the variable will be added to the class defi nition, along with the comment if you have supplied one. You should repeat the process for the other two class data members, m_Width and m_Height. The class defi nition in Box.h will then be modified to look like this: #pragma once class CBox { public: CBox(void); ~CBox(void); private: // Length of a box in inches double m_Length; // Width of a box in inches double m_Width; // Height of a box in inches double m_Height; };
Using Classes
❘ 497
Of course, you’re quite free to enter the declarations for these members manually, directly into the code, if you want. You always have the choice of whether you use the automation provided by the IDE. You can also manually delete anything that was generated automatically, but don’t forget that sometimes, both the .h and .cpp fi le will need to be changed. It’s a good idea to save all the fi les whenever you make manual changes, as this will cause the information in Class View to be updated. If you look in the Box.cpp fi le, you’ll see that the Wizard has also added an initialization list to the constructor defi nition for the data members you have added, with each variable initialized to 0. You’ll modify the constructor to do what you want next. DEFINING THE CONSTRUCTOR
You need to change the declaration of the no-arg constructor in the class defi nition so that it has arguments with default values, so modify it to: explicit CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0);
Now, you’re ready to implement it. Open the Box.cpp fi le, if it isn’t open already, and modify the constructor defi nition to: CBox::CBox(double lv, double wv, double hv) { lv = lv aBox.m_Length ? m_Length : aBox.m_Length,
Using Classes
❘ 501
m_Width > aBox.m_Width ? m_Width : aBox.m_Width, m_Height + aBox.m_Height); }
You need to repeat this process for the operator*() and operator/() functions that you saw earlier. When you have completed this, the class defi nition in Box.h will look something like this: #pragma once class CBox { public: explicit CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0); ~CBox(void); double GetHeight(void) const { return m_Height; } double GetWidth(void) const { return m_Width; } double GetLength(void) const { return m_Length; } double Volume(void) const { return m_Length*m_Width*m_Height;
}
// Overloaded addition operator CBox operator+(const CBox& aBox) const; // Multiply a box by an integer CBox operator*(int n) const; // Divide one box into another int operator/(const CBox& aBox) const; private: // Length of a box in inches double m_Length; // Width of a box in inches double m_Width; // Height of a box in inches double m_Height; };
You can edit or rearrange the code in any way that you want — as long as it’s still correct, of course. I have modified the code and removed the repeated public keyword occurrences so that the code occupies a bit less space on this page. The contents of the Box.cpp fi le ultimately look something like this: #include ".\box.h" CBox::CBox(double lv, double wv, double hv) { lv = lv aBox.m_Width ? m_Width : aBox.m_Width, m_Height + aBox.m_Height); } // Multiply a box by an integer CBox CBox::operator*(int n) const { if(n%2) return CBox(m_Length, m_Width, n*m_Height); else return CBox(m_Length, 2.0*m_Width, (n/2)*m_Height); }
// n odd // n even
// Divide one box into another int CBox::operator/(const CBox& aBox) const { // Temporary for number in horizontal plane this way int tc1 = 0; // Temporary for number in a plane that way int tc2 = 0; tc1 = static_cast((m_Length/aBox.m_Length))* static_cast((m_Width/aBox.m_Width)); // to fit this way tc2 = static_cast((m_Length/aBox.m_Width))* static_cast((m_Width/aBox.m_Length)); // and that way //Return best fit return static_cast((m_Height/aBox.m_Height))*(tc1>tc2 ? tc1 : tc2); }
The bolded lines are those that you should have modified or added manually. The very short functions, particularly those that just return the value of a data member, have their defi nitions within the class defi nition so that they are inline. If you take a look at ClassView by clicking on the tab, and then click the ▲ beside the CBox class name, you’ll see that all the members of the class are shown in the lower pane.
Using Classes
❘ 503
This completes the CBox class, but you still need to defi ne the global functions that implement operators to compare the volume of a CBox object with a numerical value. ADDING GLOBAL FUNCTIONS
You need to create a .cpp fi le that will contain the defi nitions for the global functions supporting operations on CBox objects. The fi le also needs to be part of the project. Click the Solution Explorer tab to display it (you currently will have the Class View tab displayed) and right-click the Source Files folder. Select Add ➪ New Item. . . from the context menu to display the dialog. Choose the category as Code and the template as C++ File (.cpp) in the right pane of the dialog, and enter the fi le name as BoxOperators. You can now enter the following code in the editor pane: // BoxOperators.cpp // CBox object operations that don't need to access private members #include "Box.h" // Function for testing if a constant is > a CBox object bool operator>(const double& value, const CBox& aBox) { return value > aBox.Volume(); } // Function for testing if a constant is < CBox object bool operator a constant bool operator>(const CBox& aBox, const double& value) { return value < aBox; } // Function for testing if CBox object is < a constant bool operator aBox; } // Function for testing if a constant is >= a CBox object bool operator>=(const double& value, const CBox& aBox) { return value >= aBox.Volume(); } // Function for testing if a constant is Available for download on Wrox.com
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); // Insert code for WinMain() here (Listing OFWIN_1) // Insert code for WindowProc() here (Listing OFWIN_2) code snippet Ex12_01.cpp
Of course, you’ll need to create a project for this program, but instead of choosing Win32 Console Application as you’ve done up to now, you should create this project using the Win32 Project template. You should elect to create it as an empty project and then add the Ex12_01.cpp fi le to hold the code.
TRY IT OUT
A Simple Windows API Program
If you build and execute the example, it produces the window shown in Figure 12- 4. Note that the window has a number of properties provided by the operating system that require no programming effort on your part to manage. The boundaries of the window can be dragged to resize it, and the whole window can be moved about onscreen. The maximize and minimize buttons also work. Of course, all of these actions do affect the program. Every time you modify the position or size of the window, a WM_PAINT message is queued and your program FIGURE 12-4 has to redraw the client area, but all the work of drawing and modifying the window itself is done by Windows. The system menu and Close button are also standard features of your window because of the options that you specified in the WindowClass structure. Again, Windows takes care of the management. The only additional effect on your program arising from this is the passing of a WM_DESTROY message if you close the window, as previously discussed.
834
❘
CHAPTER 12
WINDOWS PROGRAMMING CONCEPTS
WINDOWS PROGRAM ORGANIZATION In the previous example, you saw an elementary Windows program that used the Windows API and displayed a short quote from the Bard. It’s unlikely to win any awards, being completely free of any useful functionality, but it does serve to illustrate the two essential components of a Windows program: the WinMain() function that provides initialization and setup, and the WindowProc() function that services Windows messages. The relationship between these is illustrated in Figure 12-5.
WINDOWS
Windows API
Messages
Program Start
API Calls
WinMain() Initialize variables Define windows Create windows
API Calls
WindowProc() Process messages
Your Program
FIGURE 12-5
It may not always be obvious in the code that you will see, but this structure is at the heart of all Windows programs, including programs written for the CLR. Understanding how Windows applications are organized can often be helpful when you are trying to determine why things are
The Microsoft Foundation Classes
❘ 835
not working as they should be in an application. The WinMain() function is called by Windows at the start of execution of the program, and the WindowProc() function, which you’ll sometimes see with the name WndProc(), is called by the operating system whenever a message is to be passed to your application’s window. In general, there typically is a separate WindowProc() function in an application for each window in an application. The WinMain() function does any initialization that’s necessary and sets up the window or windows that are the primary interface to the user. It also contains the message loop for retrieving messages that are queued for the application. The WindowProc() function handles all the messages for a given window that aren’t queued, which includes those initiated in the message loop in WinMain(). WindowProc(), therefore, ends up handling both kinds of messages. This is because the code in the message loop sorts out what kind of message it has retrieved from the queue, and then dispatches it for processing by WindowProc(). WindowProc() is where you code your application-specific response to each Windows message, which should handle all the communications with the user by processing the Windows messages generated by user actions, such as moving or clicking the mouse or entering information at the keyboard. The queued messages are largely those caused by user input from either the mouse or the keyboard. The non-queued messages, for which Windows calls your WindowProc() function directly, are either messages that your program created, typically as a result of obtaining a message from the queue and then dispatching it, or messages that are concerned with window management — such as handling menus and scrollbars, or resizing the window.
THE MICROSOFT FOUNDATION CLASSES The Microsoft Foundation Classes (MFC) are a set of predefi ned classes upon which Windows programming with Visual C++ is built. These classes represent an object- oriented approach to Windows programming that encapsulates the Windows API. MFC does not adhere strictly to the object- oriented principles of encapsulation and data hiding, principally because much of the MFC code was written before such principles were well established. The process of writing a Windows program involves creating and using MFC objects or objects of classes derived from MFC. In the main, you’ll derive your own classes from MFC, with considerable assistance from the specialized tools in Visual C++ 2010 that make this easy. The objects of these MFC -based class types incorporate member functions for communicating with Windows, for processing Windows messages, and for sending messages to each other. These derived classes, of course, inherit all of the members of their base classes. These inherited functions do practically all of the general grunt work necessary for a Windows application to work. All you need to do is to add data and function members to customize the classes to provide the application-specific functionality that you need in your program. In doing this, you’ll apply most of the techniques that you’ve been grappling with in the preceding chapters, particularly those involving class inheritance and virtual functions.
836
❘
CHAPTER 12
WINDOWS PROGRAMMING CONCEPTS
MFC Notation All the classes in MFC have names beginning with C , such as CDocument or CView. If you use the same convention when defi ning your own classes, or when deriving them from those in the MFC library, your programs will be easier to follow. Data members of an MFC class are prefi xed with m_. I’ll also follow this convention in the examples that use MFC. You’ll fi nd that MFC uses Hungarian notation for many variable names, particularly those that originate in the Windows API. As you recall, this involves using a prefi x of p for a pointer, n for an unsigned int, l for long, h for a handle, and so on. The name m_lpCmdLine, for example, refers to a data member of a class (because of the m_ prefi x) that is of type ‘pointer to long’. This practice of explicitly showing the type of a variable in its name was important in the C environment because of the lack of type checking; because you could determine the type from the name, you had a fair chance of not using or interpreting its value incorrectly. The downside is that the variable names can become quite cumbersome, making the code look more complicated than it really is. Because C++ has strong type checking that picks up the sort of misuse that used to happen regularly in C, this kind of notation isn’t essential, so I won’t use it generally for variables in the examples in the book. I will, however, retain the p prefi x for pointers and some of the other simple type denotations because this helps to make the code more readable.
How an MFC Program Is Structured You know from Chapter 1 that you can produce a Windows program using the Application Wizard without writing a single line of code. Of course, this uses the MFC library, but it ’s quite possible to write a Windows program that uses MFC without using the Application Wizard. If you fi rst scratch the surface by constructing the minimum MFC -based program, you’ll get a clearer idea of the fundamental elements involved. The simplest program that you can produce using MFC is slightly less sophisticated than the example that you wrote earlier in this chapter using the raw Windows API. The example you’ll produce here has a window, but no text displayed in it. This is sufficient to show the fundamentals, so try it out.
TRY IT OUT
A Minimal MFC Application
Create a new project using the File ➪ New ➪ Project menu option, as you’ve done many times before. You won’t use the Application Wizard that creates the basic code here, so select the template for the project as Win32 Project and choose Windows Application and the Empty project options in the second dialog. After the project is created, select Project > Ex12_02 properties from the main menu, and on the General sub -page from Configuration Properties, click the Use of MFC property to set its value to Use MFC in a Shared DLL. With the project created, you can create a new source fi le in the project as Ex12_02.cpp. So that you can see all the code for the program in one place, put the class defi nitions you need together with their implementations in this fi le. To achieve this, just add the code manually in the edit window — there isn’t very much of it.
The Microsoft Foundation Classes
❘ 837
To begin with, add a statement to include the header fi le afxwin.h, as this contains the defi nitions for many MFC classes. This allows you to derive your own classes from MFC. #include
// For the class library
To produce the complete program, you’ll only need to derive two classes from MFC: an application class and a window class. You won’t even need to write a WinMain() function, as you did in the previous example in this chapter, because this is automatically provided by the MFC library behind the scenes. Take a look at how you defi ne the two classes that you need. THE APPLICATION CLASS
The class CWinApp is fundamental to any Windows program written using MFC. An object of this class includes everything necessary for starting, initializing, running, and closing the application. You need to produce the application to derive your own application class from CWinApp. You will defi ne a specialized version of the class to suit your application needs. The code for this is as follows: class COurApp: public CWinApp { public: virtual BOOL InitInstance(); };
As you might expect for a simple example, there isn’t a great deal of specialization necessary in this case. You’ve only included one member in the defi nition of the class: the InitInstance() function. This function is defi ned as a virtual function in the base class, so it’s not a new function in your derived class; you are simply redefi ning the base class function for your application class. All the other data and function members that you need in the class, you’ll inherit from CWinApp unchanged. The application class is endowed with quite a number of data members defi ned in the base, many of which correspond to variables used as arguments in Windows API functions. For example, the member m_pszAppName stores a pointer to a string that defi nes the name of the application. The member m_nCmdShow specifies how the application window is to be shown when the application starts up. You don’t need to go into all the inherited data members now. You’ll see how they are used as the need arises in developing application-specific code. In deriving your own application class from CWinApp, you must override the virtual function InitInstance(). Your version is called by the version of WinMain() that’s provided for you by MFC, and you’ll include code in the function to create and display your application window. However, before you write InitInstance(), I should introduce you to a class in the MFC library that defi nes a window. THE WINDOW CLASS
Your MFC application needs a window as the interface to the user, referred to as a frame window. You derive a window class for the application from the MFC class CFrameWnd, which is designed specifically for this purpose. Because the CFrameWnd class provides everything for creating and managing a window
838
❘
CHAPTER 12
WINDOWS PROGRAMMING CONCEPTS
for your application, all you need to add to the derived window class is a constructor. This enables you to specify a title bar for the window to suit the application context: class COurWnd: public CFrameWnd { public: // Constructor COurWnd() { Create(0, L"Our Dumb MFC Application"); } };
The Create() function that you call in the constructor is inherited from the base class. It creates the window and attaches it to the COurWnd object that is being created. Note that the COurWnd object is not the same thing as the window that is displayed by Windows — the class object and the physical window are distinct entities. The fi rst argument value for the Create() function, 0, specifies that you want to use the base class default attributes for the window — you’ll recall that you needed to defi ne window attributes in the previous example in this chapter that used the Windows API directly. The second argument specifies the window name that is used in the window title bar. You won’t be surprised to learn that there are other parameters to the function Create(), but they all have default values that are quite satisfactory, so you can afford to ignore them here. COMPLETING THE PROGRAM
Having defi ned a window class for the application, you can write the InitInstance() function in our COurApp class: BOOL COurApp::InitInstance(void) { // Construct a window object in the free store m_pMainWnd = new COurWnd; m_pMainWnd->ShowWindow(m_nCmdShow); // ...and display it return TRUE; }
This overrides the virtual function defi ned in the base class CWinApp, and as I said previously, it is called by the WinMain() function that’s automatically supplied by the MFC library. The InitInstance() function constructs a main window object for the application in the free store by using the operator new. You store the address that is returned in the variable m_pMainWnd, which is an inherited member of your class COurApp. The effect of this is that the window object is owned by the application object. You don’t even need to worry about freeing the memory for the object you have created — the supplied WinMain() function takes care of any cleanup necessary. The only other item you need for a complete, albeit rather limited, program is to define an application object. An instance of our application class, COurApp, must exist before WinMain() is executed, so you must declare it at global scope with the statement: COurApp AnApplication;
// Define an application object
The Microsoft Foundation Classes
❘ 839
The reason that this object needs to exist at global scope is that it is the application, and the application needs to exist before it can start executing. The WinMain() function that is provided by MFC calls the InitInstance() function member of the application object to construct the window object and, thus, implicitly assumes the application object already exists. THE FINISHED PRODUCT
Now that you’ve seen all the code, you can add it to the Ex12_02.cpp source fi le in the project. In a Windows program, the classes are usually defi ned in .h fi les, and the member functions that are not defi ned within the class defi nitions are defi ned in .cpp fi les. Your application is so short, though, that you may as well put it all in a single .cpp fi le. The merit of this is that you can view the whole lot together. The program code is structured as follows: // Ex12_02.cpp An elementary MFC program #include Available for download on Wrox.com
// For the class library
// Application class definition class COurApp:public CWinApp { public: virtual BOOL InitInstance(); }; // Window class definition class COurWnd:public CFrameWnd { public: // Constructor COurWnd() { Create(0, L"Our Dumb MFC Application"); } }; // Function to create an instance of the main application window BOOL COurApp::InitInstance(void) { // Construct a window object in the free store m_pMainWnd = new COurWnd; m_pMainWnd->ShowWindow(m_nCmdShow); // ...and display it return TRUE; } // Application object definition at global scope COurApp AnApplication; // Define an application object code snippet Ex12_02.cpp
That’s all you need. It looks a bit odd because no WinMain() function appears, but as noted previously, there is a WinMain() function supplied by the MFC library.
840
❘
CHAPTER 12
WINDOWS PROGRAMMING CONCEPTS
How It Works Now you’re ready to roll, so build and run the application. Select the Build ➪ Build Ex12_02.exe menu item, click the appropriate toolbar button, or just press Ctrl+Shift+B to build the solution. You should end up with a clean compile and link, in which case you can press Ctrl+F5 to run it. Your minimum MFC program appears as shown in Figure 12- 6. You can resize the window by dragging the border, move the whole thing around, and minimize or maximize it FIGURE 12-6 in the usual ways. The only other function that the program supports is “close,” for which you can use the system menu, the Close button at the upper right of the window, or just key Alt+F4. It doesn’t look like much, but considering that there are so few lines of code, it’s quite impressive.
USING WINDOWS FORMS A Windows form is an entity that represents a window of some kind. By a window, I mean window in its most general sense, being an area on the screen that can be a button, a dialog, a regular window, or any other kind of visible GUI component. A Windows form is encapsulated by a subclass of the System::Windows::Forms::Form class, but you don’t need to worry about this much initially, because all the code to create a form is created automatically. To see just how easy it’s going to be, create a basic window using Windows Forms that has a standard menu.
TRY IT OUT
A Windows Forms Application
Choose the CLR project type in the New Project dialog and select the Windows Forms Application as the template for the project. Enter the project name as Ex12_03. When you click the OK button, the Application Wizard generates the code for the Windows form application and displays the design window containing the form as it is displayed by the application. This is shown in Figure 12-7. You can now make changes to the form in the design pane graphically, and the changes are automatically reflected in the code that creates the form. For a start, you can drag the bottom corner of the form with the mouse cursor to increase the size of the form window. You can also change the text in the title bar — right- click in the client area of the form and select Properties from the context menu. This displays the Properties window that allows you to change the properties for the form. From the list of properties to the right of the
FIGURE 12-7
Summary
❘ 841
design pane, select Text, and then enter the new title bar text in the adjacent column showing the property value — I entered A Simple Form Window. When you press the Enter key, the new text appears in the title bar of the form. Just to see how easy it really is to add something to the form window, display the Toolbox pane by selecting the tab on the right of the window if it is present, or by pressing Ctrl+Alt+X or selecting Toolbox from the View menu. Find the MenuStrip option in the Menus and Toolbars list and drag it on to the form window in the Design tab pane. Right- click the MenuStrip1 that appears below the form window and select Insert Standard Items from the pop-up. You’ll then have the menu in the form window populated with the standard File, FIGURE 12-8 Edit, Tools, and Help menus, each complete with its drop-down list of menu items. The result of this operation is shown in Figure 12-8.
How It Works If you build the project by pressing the Ctrl+Shift+B and then execute it by pressing Ctrl+F5, you’ll see the form window displayed complete with its menus. Naturally, the menu items don’t do anything because you haven’t added any code to deal with the events that result from clicking on them, but the icons at the right end of the title bar work, so you can close the application. You’ll look into how to develop a Windows Forms application further, including handling events, later in the book.
SUMMARY In this chapter you’ve seen three different ways of creating an elementary Windows application with Visual C++ 2010. You should now have a feel for the essential differences between these three approaches. In later chapters of the book, you’ll be exploring in more depth how you develop applications using the MFC and using Windows Forms.
842
❘
CHAPTER 12
WINDOWS PROGRAMMING CONCEPTS
WHAT YOU LEARNED IN THIS CHAPTER TOPIC
CONCEPT
Windows API
The Windows API provides a standard programming interface by which an application communicates with the operating system.
The WinMain() function
All Windows applications include a WinMain() function that is called by the operating system to begin execution of the application. The WinMain() function also includes code to retrieve messages from the operating system.
Message processing
The Windows operating system calls a particular function in an application to handle processing of specific messages. An application identifies the message processing function for each window in an application by calling a Windows API function.
The MFC
The MFC consists of a set of classes that encapsulate the Windows API and simplify programming using the Windows API.
Windows Forms applications
A Windows Forms application executes with the CLR. The windows in a Windows Forms application can be created graphically with all the required code being generated automatically.
13
Programming for Multiple Cores WHAT YOU WILL LEARN IN THIS CHAPTER:
➤
The principle features of the Parallel Patterns Library
➤
How to execute loop iterations in parallel
➤
How to execute two or more tasks concurrently
➤
How to manage shared resources in parallel operations
➤
How to indicate and manage sections in your parallel code that must not be executed by multiple processors
If your PC or laptop is relatively new, in all probability it has a processor chip with two or more cores. If it doesn’t, you can skip this chapter and move on to the next chapter. Each core in a multi- core processor chip is an independent processing unit, so your computer can be executing code from more than one application at any given time. However, it is not limited to that. Having multiple cores or multiple processors provides you with the possibility of overlapping computations within a single application by having each processor handling a separate piece of the calculations required in parallel. Because at least some of the calculations in your application are overlapped, your application should execute faster. This chapter will show you how you can use multiple cores within a single application. You will also get to use what you learned about the Windows API in the previous chapter in a significant parallel computation.
PARALLEL PROCESSING BASICS Programming an application to use multiple cores or processors requires that you organize the computation differently from what you have been used to with serial processing. You must be able to divide the computation into a set of sub-tasks that can be executed independently of
844
❘
CHAPTER 13
PROGRAMMING FOR MULTIPLE CORES
one another, and this is not always easy, or indeed possible. Sub -tasks that can run in parallel are usually referred to as threads. Any interdependence between threads places serious constraints on parallel execution. Shared resources are a common limitation. Sub-tasks executing in parallel that access common resources, at best, can slow execution — potentially slower than if you programmed for serial execution — and, at worst, can produce incorrect results or cause the program to hang. A simple example is a shared variable that is accessed and updated by two or more threads. If one thread stored a result in the variable after the other thread has retrieved the value for updating, the result stored by the first thread will be lost and the result will be incorrect. The same kind of problem arises with updating files. So, what are the typical characteristics of applications that may benefit from using more than one processor? First, these applications will involve operations that require substantial amount of processor time, measured in seconds rather than milliseconds. Second, the heavy computation will involve a loop of some kind. Third, the computation will involve operations that can be divided into discrete but significant units of calculation that can be executed independently of one another.
INTRODUCING THE PARALLEL PATTERNS LIBRARY The Parallel Patterns Library (PPL) that is defi ned in the ppl.h header provides you with tools for implementing parallel processing in your applications, and these tools are defi ned within the Concurrency namespace. The PPL defi nes three kinds of facilities for parallel processing: ➤
Templates for algorithms for parallel operations
➤
A class template for managing shared resources
➤
Class templates for managing and grouping parallel tasks
In general, the PPL operates on a unit of work or task that is defi ned by a function object or, more typically, a lambda expression. You don’t have to worry about creating threads of execution or allocating work to particular processors because in general the PPL takes care of this automatically. What you do have to do is to make sure your code is organized appropriately for parallel execution.
ALGORITHMS FOR PARALLEL PROCESSING The PPL provides three algorithms for initiating parallel execution on multiple cores: ➤
The parallel_for algorithm is the equivalent of a for loop that executes loop iterations in parallel.
➤
The parallel_for_each algorithm executes repeated operations on a STL container in parallel.
➤
The parallel_invoke algorithm executes a set of two or more independent tasks in parallel.
These are defi ned as templates, but I won’t go into the details of the defi nitions of the templates themselves. You can take a look at the ppl.h header fi le, if you are interested. Let’s take a closer look at how you use each of them.
Algorithms for Parallel Processing
❘ 845
Using the parallel_for Algorithm The parallel_for algorithm executes parallel loop iterations on a single task specified by a function object or lambda expression. The loop iterations are controlled by an integer index that is incremented on each iteration until a given limit is reached, just like a regular for loop. The function object or lambda expression that does the work must have a single parameter; the current index value will be passed as the argument corresponding to this parameter on each iteration. Because of the parallel nature of the operation, the iterations will not be executed in any particular order. There are two versions of the parallel_for algorithm. The fi rst has the form: parallel_for(start_value, end_value, function_object);
This executes the task specified by the third argument, with index values running from start_value to end_value-1, and the default increment of 1. The first two arguments must be of the same integer type. If they are not, you will get a compile-time error. As I said, because iterations are executed in parallel, they will not be executed in sequence. This implies that each execution of the function object or lambda expression must be independent of the others, and the result must not depend on executing the task in a particular sequence. Here’s a fragment that shows how the parallel_for algorithm works: const size_t count(100000); long long squares[count]; Concurrency::parallel_for(static_cast<size_t>(0), count, [&squares](size_t n) { squares[n] = (n+1)*(n+1); });
The computational task for each loop iteration is defined by the lambda expression, which is the third argument. The capture clause just accesses the squares array by reference. The function computes the square of n+1, where n is the current index value that is passed to the lambda, and stores the result in the nth element of the squares array. This will be executed for values of n, from 0 to count-1, as specified by the fi rst two arguments. Note the cast for the fi rst argument. Because count is of type size_t, the code will not compile without it. Because 0 is a literal of type int, without the cast, the compiler will not be able to decide whether to use type int or type size_t as a type parameter for the parallel_for algorithm template. Although this fragment shows how the parallel_for algorithm can be applied, the increase in performance in this case may not be great, and indeed, you may find that things run more slowly than serial operations. The computation on each iteration in the example is very short, and inevitably there is some overhead in allocating the task to a processor on each iteration. The overhead may severely erode any reduction in execution time in computations that are relatively short. You should also not be misled by this simple example into thinking that implementing the use of parallel processing is trivial, and that you can just replace a for loop by a parallel_for algorithm. As you will see later in this chapter, all sorts of complications can arise in a more realistic situation. Although with a parallel_for algorithm the index must always be ascending, you can arrange for descending index values inside the function that is being executed: const size_t count(100000); long long squares[count];
846
❘
CHAPTER 13
PROGRAMMING FOR MULTIPLE CORES
Concurrency::parallel_for(static_cast<size_t>(0), count, [&squares, &count](size_t n) { squares[count-n-1] = (n+1)*(n+1);
});
Here, the squares of the index values passed to the lambda will be stored in descending sequence in the array. The second version of the parallel_for algorithm has the following form: parallel_for(start_value, end_value, increment, pointer_to_function);
Here, the second argument specifies the increment to use in iterating from start_value to end_value-1, so this provides for steps greater than 1. The second argument must be positive because only increasing index values are allowed in a parallel_for algorithm. The first three arguments must all be of the same integer type. Here’s a fragment using this form of the parallel_for algorithm: size_t start(1), end(300001), step(3); vector<double> cubes(end/step + 1); Concurrency::parallel_for(start, end, step, [&cubes](int n) { cubes[(n-1)/3] = static_cast<double>(n)*n*n;
});
This code fragment computes the cubes of integers from 1 to 300001 in steps of 3 and stores the values in the cubes vector. Thus, the cubes of 1, 4, 7, 10, and so on, up to 29998, will be stored. Of course, because this is a parallel operation, the values may not be stored in sequence in the vector. Note that operations on STL containers are not thread-safe, so you must take great care when using them in parallel operations. Here, it is necessary to pre-allocate sufficient space in the vector to store the results of the operation. If you were to use the push_back() function to store values, the code would crash. Because parallel_for is a template, you need to take care to ensure that the start_value, end_value, and increment arguments are of the same type. It is easy to get it wrong if you mix literals with variables. For example, the following will not compile: size_t end(300001); vector<double> cubes(end/3 + 1); Concurrency::parallel_for(1, end, 3, [&cubes](size_t n) { cubes.push_back (static_cast<double>(n)*n*n);
});
This doesn’t compile because the literals that are the fi rst and third arguments are of type int, whereas count is of type size_t. The effect is that no compatible template instance can be found by the compiler.
Using the parallel_for_each Algorithm The parallel_for_each algorithm works in a similar way to parallel_for, but the function operates on an STL container. The number of iterations to be carried out is determined by a pair of iterators. This algorithm is of the form: Concurrency::parallel_for_each(start_iterator, end_iterator, function_object);
Algorithms for Parallel Processing
❘ 847
The function object specified by the third argument must have a single parameter of the type stored in the container that the iterators will access. This will enable you to access the object in the container that is referred to by the current iterator. Iterations run from start_iterator to end_iterator-1. Here’s a code fragment that illustrates how this works: std::array<string, 6> people = {"Mel Gibson", "John Wayne", "Charles Chaplin", "Clint Eastwood", "Jack Nicholson", "George Clooney"}; Concurrency::parallel_for_each(people.begin(), people.end(), [](string& person) { size_t space(person.find(' ')); std::string first(person.substr(0, space)); std::string second(person.substr(space + 1, person.length() - space - 1)); person = second + ' ' + first; }); std::for_each(people.begin(), people.end(), [](std::string& person) {std::cout (0), people.size(), [=](size_t n) { size_t space(people[n].find(' ')); if(people[n].substr(0, space) == name) throw n; }); } catch(size_t index) { return index; } return -1; }
This function searches the array from the previous fragment to fi nd a particular name string. The parallel_for compares the name, up to the fi rst space in the current people array element, with the second argument to the findIndex() function. If it’s a match, the lambda expression throws an exception that will be the current index to the people array. This will be caught in the catch clause in the findIndex() function, and the value that was thrown will be returned. Throwing the exception terminates all current activity with the parallel_for operation. The findIndex()
848
❘
CHAPTER 13
PROGRAMMING FOR MULTIPLE CORES
function returns -1 if the parallel_for operation terminates with no exception having been thrown, because this indicates that name was not found. Let’s see if it works.
TRY IT OUT
Terminating a parallel_for operation
To exercise the findIndex() function, you can append some code to the previous fragment that uses the parallel_for_each algorithm to reverse the sequence of names. Here’s the complete program:
Available for download on Wrox.com
// Ex13_01.cpp Terminating a parallel operation #include #include "ppl.h" #include <array> #include <string> using namespace std; using namespace Concurrency; // Find name as the first name in any people element int findIndex(const array<std::string, 6>& people, const string& name) { try { parallel_for(static_cast<size_t>(0), people.size(), [=](size_t n) { size_t space(people[n].find(' ')); if(people[n].substr(0, space) == name) throw n; }); } catch(size_t index) { return index; } return -1; } int main() { array<string, 6> people = {"Mel Gibson", "John Wayne", "Charles Chaplin", "Clint Eastwood", "Jack Nicholson", "George Clooney"}; parallel_for_each(people.begin(), people.end(), [](string& person) { size_t space(person.find(' ')); string first(person.substr(0, space)); string second(person.substr(space+1, person.length()-space-1)); person = second +' ' + first; }); for_each(people.begin(), people.end(), [](string& person) {cout SetCheck(m_Color==BLACK); }
The statement you have added calls the SetCheck() function for the Color ➪ Black menu item, and the argument expression m_Color==BLACK results in true if m_Color is BLACK, or false otherwise. The effect, therefore, is to check the menu item only if the current color stored in m_Color is BLACK, which is precisely what you want. You can implement the update handlers for the other Color pop-up menu items in the same way; just choose the appropriate COLORREF constant in the argument to SetCheck().
Adding Handlers for Menu Messages
❘ 923
The update handlers for all the menu items in a menu are always called before the menu is displayed, so you can code the other handlers in the same way to ensure that only the item corresponding to the current color (or the current element) is checked. A typical Element menu item update handler is coded as follows: void CSketcherDoc::OnUpdateElementLine(CCmdUI* pCmdUI) { // Set Checked if the current element is a line pCmdUI->SetCheck(m_Element==LINE); }
You can now code all the other update handlers for items in the Element pop -up in a similar manner.
Exercising the Update Handlers When you’ve added the code for all the update handlers, you can build and execute the Sketcher application again. Now, when you change a color or an element type selection, this is reflected in the menu, as shown in Figure 15-11.
FIGURE 15-11
When you click on a menu item, the default way the menu works is to display the most recently used menu item fi rst without displaying the complete pop-up. You can change this by right- clicking the menu bar in the Sketcher application window and selecting Customize. . . from the pop -up. If you select the Options from the dialog that displays, you can uncheck the option “Menus show recently used commands fi rst.” Now, the full menu pop-up will display when you select a menu item.
924
❘
CHAPTER 15
WORKING WITH MENUS AND TOOLBARS
You have completed all the code that you need for the menu items. Make sure that you have saved everything before embarking on the next stage. These days, toolbars are a must in any Windows program of consequence, so the next step is to take a look at how you can add toolbar buttons to support our new menus.
ADDING TOOLBAR BUTTONS Select the Resource View and extend the toolbar resource. You’ll see that there are two toolbars, one that has the same ID as the main menu, IDR_MAINFRAME, and another that has the ID IDR_MAINFRAME_256. The former provides you with toolbar icons that have four-bit color (16 colors) that will be displayed when small icons are selected the toolbar, whereas the latter provides icons with 24 -bit color (256 colors) that correspond to the large icon toolbar. To implement Sketcher properly, you must add buttons to both toolbars. If you right- click IDR_MAINFRAME_256 and select Open from the pop -up, the editor window appears, as shown in Figure 15-12.
FIGURE 15-12
If the toolbar displays as a contiguous image, rather than as shown in Figure 15-12, right- click in the editor window and select “Toolbar editor . . .” from the pop -up. A toolbar button is a 16 -by-15 array of pixels that contains a pictorial representation of the function it initiates. You can see in Figure 15-12 that the resource editor provides an enlarged view of a toolbar button so that you can see and manipulate individual pixels. It also provides you with a color palette to the right, from which you can choose the current working color. If you click the new button icon at the right end of the row as indicated, you’ll be able to draw this icon. Before starting the editing, drag the new icon about half a button-width to the right. It separates from its neighbor on the left to start a new block. You should keep the toolbar button icons in the same sequence as the items on the menu bar, so you’ll create the element type selection buttons fi rst. You’ll probably want to use the following editing buttons provided by the resource editor, which appear in the toolbar for the Visual C++ 2010 application window:
Adding Toolbar Buttons
➤
Pencil for drawing individual pixels
➤
Eraser for erasing individual pixels
➤
Fill an area with the current color
➤
Zoom the view of the button
➤
Draw a line
➤
Draw a rectangle
➤
Draw an ellipse
➤
Draw a curve
❘ 925
If it is not already visible, you can display the window that displays the palette, from which you can choose a color by right- clicking a toolbar button icon and selecting Show Colors Window from the pop -up. You select a foreground color by left- clicking a color from the palette, and a background color by right- clicking. The fi rst icon will be for the toolbar button corresponding to the Element ➪ Line menu option. You can let your creative juices flow and draw whatever representation you like here to depict the act of drawing a line, but I’ll keep it simple. If you want to follow my approach, make sure that black is selected as the foreground color and use the line tool to draw a diagonal line in the enlarged image of the new toolbar button icon. In fact, if you want the icon to appear a bit bigger, you can use the Magnification Tool editing button to make it up to eight times its actual size: just click the down arrow alongside the button and select the magnification you want. I drew a double black line then a very mute blue line as highlighting. If you make a mistake, you can select the Undo button or you can change to the Erase Tool editing button and use that, but in the latter case, you need to make sure that the color selected corresponds to the background color for the button you are editing. You can also erase individual pixels by clicking them using the right mouse button, but again, you need to be sure that the background color is set correctly when you do this. After you’re happy with what you’ve drawn, the next step is to edit the toolbar button properties.
Editing Toolbar Button Properties Right- click your new button icon in the toolbar and select Properties from the pop -up to bring up its Properties window, as shown in Figure 15-13. The Properties window shows a default ID for the button, but you want to associate the button with the menu item Element ➪ Line that we’ve already defi ned, so click ID and then click the down arrow to display alternative values. You can then select ID_ELEMENT_LINE from the drop -down box. If you click
FIGURE 15-13
926
❘
CHAPTER 15
WORKING WITH MENUS AND TOOLBARS
Prompt, you’ll fi nd that this also causes the same prompt to appear in the status bar because the prompt is recorded along with the ID. You can close the Properties window and click Save to complete the button defi nition. You can now move on to designing the other three element buttons. You can use the rectangle editing button to draw a rectangle and the ellipse button to draw a circle. You can draw a curve using the pencil to set individual pixels, or use the curve button. You need to associate each button with the ID corresponding to the equivalent menu item that you defi ned earlier. Now add the buttons for the colors. You should also drag the fi rst button for selecting a color to the right, so that it starts a new group of buttons. You could keep the color buttons very simple and just color the whole button with the color it selects. You can do this by selecting the appropriate foreground color, then selecting the Fill editing button and clicking on the enlarged button image. Again, you need to use ID_COLOR_BLACK, ID_COLOR_RED, and so on as IDs for the buttons. The toolbar editing window should look like the one shown in Figure 15 -14.
FIGURE 15-14
You now need to repeat the whole process for the IDR_MAINFRAME toolbar. Create equivalent buttons for the element types and colors, and assign the same IDs to the buttons that you assigned to the corresponding IDR_MAINFRAME_256 toolbar buttons. That’s all you need for the moment, so save the resource file and give Sketcher another spin.
Exercising the Toolbar Buttons Build the application once again and execute it. You should see the application window shown in Figure 15-15.
Adding Toolbar Buttons
❘ 927
FIGURE 15-15
There are some amazing things happening here. The toolbar buttons that you added already reflect the default settings that you defi ned for the new menu items, and the selected button is shown depressed. If you let the cursor linger over one of the new buttons, the prompt for the button appears in the status bar. The new buttons work as a complete substitute for the menu items, and any new selection made, with either the menu or the toolbar, is reflected by the toolbar button being shown depressed. The menu items now have the button icons in front of the text, with the selected menu item having the icon depressed. You can also enable large icons for the toolbar by right- clicking in the toolbar area or selecting Toolbars and Docking Windows from the View menu to display the Customize dialog. If you close the document view window, Sketch1, you’ll see that our toolbar buttons are automatically grayed and disabled. If you open a new document window, they are automatically enabled once again. You can also try dragging the toolbar with the cursor. You can move it to either side of the application window, or have it free-floating. You can also enable or disable it through the Customize dialog. You got all this without writing a single additional line of code!
Adding Tooltips There’s one further tweak that you can add to your toolbar buttons that is remarkably easy: tooltips. A tooltip is a small box that appears adjacent to the toolbar button when you let the cursor linger on the button. The tooltip contains a text string that is an additional clue to the purpose of the toolbar button. To add a tooltip for a toolbar button, select the button in the toolbar editor tab. Select the Prompt property for the button in the Properties pane and enter \n followed by the text for the tooltip. For example, for the toolbar button with the ID ID_ELEMENT_LINE, you could enter the Prompt property value as \nDraw lines. The tooltip text will be displayed when you hover the mouse cursor over the
928
❘
CHAPTER 15
WORKING WITH MENUS AND TOOLBARS
toolbar button. If you put the \n after the text in the Prompt property value — for example, Draw lines\n — the text will be displayed in the status bar rather than as a tooltip when the mouse cursor is over the button. You can have a different text in the status bar and in the tooltip. Set the value for the Prompt property for the Line menu item as Draw Lines\nLine. This will display “Draw Lines” in the status bar when you hover the mouse cursor over the toolbar button. The tooltip will display the button icon image and the word “Line” with the status bar prompt text below it. Change the Prompt property text for each of the toolbar buttons corresponding to the Element and Color menus in a similar way. That’s all you have to do. After saving the resource fi le, you can now rebuild the application and execute it. Placing the cursor over one of the new toolbar buttons causes the prompt text to appear in the status bar and the tooltip to be displayed after a second or two.
MENUS AND TOOLBARS IN A C++/CLI PROGRAM Of course, your C++/CLI programs can also have menus and toolbars. A good starting point for a Windows-based C++/CLI program is to create a Windows Forms application. The Windows Forms technology is oriented toward the development of applications that use the vast array of standard controls that are provided, but there’s no reason you can’t draw with a form-based application. There is much less programming for you to do with a Windows Forms application, because you add the standard components for the GUI using the Windows Forms Designer capability, and the code to generate and service the GUI components will be added automatically. Your programming activity will involve adding application-specific classes and customizing the behavior of the application by implementing event handlers.
Understanding Windows Forms Windows Forms is a facility for creating Windows applications that execute with the CLR. A form is a window that is the basis for an application window or a dialog window to which you can add other controls that the user can interact with. Visual C++ 2010 comes with a standard set of more than 60 controls that you can use with a form. Because there is a very large number of controls, you’ll get to grips only with a representative sample in this book, but that should give you enough of an idea of how they are used so you can explore the others for yourself. Many of the standard controls provide a straightforward interactive function, such as Button controls that represent buttons to be clicked, or TextBox controls that allow text to be entered. Some of the standard controls are containers, which means that they are controls that can contain other controls. For example, a GroupBox control can contain other controls such as Button controls or TextBox controls, and the function of a GroupBox control is simply to group the controls together for some purpose and optionally provide a label for the group in the GUI. There are also a lot of third-party controls available. If you can’t fi nd the control you want in Visual C++, it is very likely that you can fi nd a third party that produces it. A form and the controls that you use with a form are represented by a C++/CLI class. Each class has a set of properties that determines the behavior and appearance of the control or form. For example,
Menus and Toolbars in a C++/CLI Program
❘ 929
whether or not a control is visible in the application window and whether or not a control is enabled to allow user interaction are determined by the property values that are set. You can set a control’s properties interactively when you assemble the GUI using the IDE. You can also set property values at run time using functions that you add to the program, or via code that you add to existing functions through the code editor pane. The classes also define functions that you call to perform operations on the control. When you create a project for a Windows Forms application, an application window based on the Form class is created along with all the code to display the application window. After you create a Windows Forms project, there are four distinct operations involved in developing a Windows Forms application: ➤
You create the GUI interactively in the Form Design tab that is displayed in the Editor pane by selecting controls in the Toolbox window and placing them on the form. You can also create additional form windows.
➤
You modify the properties for the controls and the forms to suit your application needs in the Properties window.
➤
You create click event handlers for a control by double- clicking the control on the Form Design tab. You can also set an existing function as the handler for an event for a control from its Properties window.
➤
You modify and extend the classes that are created automatically from your interaction with the Form Design tab to meet the needs of your application.
You’ll get a chance to see how it works in practice.
Understanding Windows Forms Applications You fi rst need to get an idea of how the default code for a Windows Forms application works. Create a new CLR project using the Windows Forms Application template and assign the name CLR Sketcher to the project. The procedure is exactly the same as the one you used back in Chapter 12 when you created Ex12_03, so I won’t repeat it here. The Design window for CLR Sketcher will show the form as it is defi ned initially, and you’ll customize it to suit the development of a version of Sketcher for the CLR. It won’t have the full capability of the MFC version, but it will demonstrate how you implement the major features in a C++/CLI context. You’ll continue to add functionality to this application over the next four chapters. The Editor pane displays a graphical representation of the application window because with a Windows Forms application, you build the GUI graphically. Even double- clicking Form1.h in the Solution Explorer pane does not display the code, but you can see it by right- clicking in the editor window and selecting View Code from the context menu. The code defi nes the Form1 class that represents the application window, and the fi rst thing to note is that the code is defi ned in its own namespace: namespace CLRSketcher { using namespace System; using namespace System::ComponentModel;
930
❘
CHAPTER 15
using using using using
WORKING WITH MENUS AND TOOLBARS
namespace namespace namespace namespace
System::Collections; System::Windows::Forms; System::Data; System::Drawing;
// rest of the code }
When you compile the project, it creates a new assembly, and the code for this assembly is within the namespace CLRSketcher, which is the same as the project name. The namespace ensures that a type in another assembly that has the same name as a type in this assembly is differentiated, because each type name is qualified by its own namespace name. There are also six directives for .NET library namespaces, and these cover the library functionalities you are most likely to need in your application. These namespaces are: NAMESPACE
CONTENTS
System
This namespace contains classes that define data types that are used in all CLR applications. It also contains classes for events and event handling, exceptions, and classes that support commonly used functions.
System::ComponentModel
This namespace contains classes that support the operation of GUI components in a CLR application.
System::Collections
This namespace contains collection classes for organizing data in various ways, and includes classes to define lists, queues, dictionaries (maps), and stacks.
System::Windows::Forms
This namespace contains the classes that support the use of Windows Forms in an application.
System::Data
This namespace contains classes that support ADO.NET, which is used for accessing and updating data sources.
System::Drawing
This namespace defines classes that support basic graphical operations such as drawing on a form or a component.
The Form1 class is derived from the Form class that is defi ned in the System::Windows::Forms namespace. The Form class represents either an application window or a dialog window, and the Form1 class that defi nes the window for CLR Sketcher inherits all the members of the Form class. The section at the end of the Form1 class contains the defi nition of the InitializeComponent() function. This function is called by the constructor to set up the application window and any components that you add to the form. The comments indicate that you must not modify this section of code using the code editor, and this code is updated automatically as you alter the application window interactively. It’s important when you do use Form Design that you do not ignore these comments and modify the automatically generated code yourself; if you do, things are certain to go
Menus and Toolbars in a C++/CLI Program
❘ 931
wrong at some point. Of course, you can write all the code for a Windows Forms application from the ground up, but it is much quicker and less error-prone to use the Form Design capability to set up the GUI for your application interactively. This doesn’t mean you shouldn’t know how it works. The code for the InitializeComponent() function initially looks like this: void InitializeComponent(void) { this->components = gcnew System::ComponentModel::Container(); this->Size = System::Drawing::Size(300,300); this->Text = L"Form1"; this->Padding = System::Windows::Forms::Padding(0); this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::Font; }
The components member of the Form1 class is inherited from the base class, and its role is to keep track of components that you subsequently add to the form. The fi rst statement stores a handle to a Container object in components, and this object represents a collection that stores GUI components in a list. Each new component that you add to the form using the Form Design capability is added to this Container object.
Modifying the Properties of a Form The remaining statements in the InitializeComponent() function set properties for Form1. You must not modify any of these directly in the code, but you can choose your own values for these through the Properties window for the form, so return to the Form1.h (Design) tab for the form in the editor window and right- click it to display the Properties window shown in Figure 15-16. Figure 15-16 shows the properties for the form categorized by function. You can also display the properties in alphabetical sequence by clicking the second button at the top of the window. This is helpful when you know the name of the property you want to change. It’s worth browsing through the list of properties for the form to get an idea of the possibilities. Clicking any FIGURE 15-16 of the properties displays a description at the bottom of the window. Figure 15-16 shows the Properties window with the width increased to make the descriptions visible. You can select the cell in the right column to modify the property value. The properties that have an [unfilled] symbol to the left have multiple values, and clicking the symbol displays the values so you can change them individually. You can make the form a little larger by changing the value for the Size property in the Layout group to 500,350. You don’t want to make it too large at this point because that will make it difficult to work with when you have several windows open in the IDE, so just size the form so it’s easy to work with on your display. You could
932
❘
CHAPTER 15
WORKING WITH MENUS AND TOOLBARS
also change the Text property (in the Appearance category) to CLRSketcher. This changes the text in the title bar for the application window. If you go back to the Form1.h tab in the editor window, you’ll see that the code in the InitializeComponent() function has been altered to reflect the property changes you have made. As you’ll see, the Properties window is a fundamental tool for implementing support for GUI components in a Forms-based application. Note that you can also arrange for the application window to be maximized when the program starts by setting the value for the WindowState property. With the default Normal setting, the window is the size you have specified, but you can set the property to Maximized or Minimized by selecting from the list of values for this property.
How the Application Starts Execution of the application begins, as always, in the main() function, and this is defi ned in the CLRSketcher.cpp fi le as follows: int main(array<System::String ^> ^args) { // Enabling Windows XP visual effects before any controls are created Application::EnableVisualStyles(); Application::SetCompatibleTextRenderingDefault(false); // Create the main window and run it Application::Run(gcnew Form1()); return 0; }
The main() function calls three static functions that are defi ned in the Application class that is defi ned in the System::Windows::Forms namespace. The static functions in the Application class are at the heart of every Windows Forms application. The EnableVisualStyles() function that is called fi rst in main() enables visual styles for the application. The SetCompatibleTextRenderingDefault() function determines how text is to be rendered in new controls based on the argument value. A value of true for the argument specifies that the Graphics class is to be used, and a false value specifies that the TextRenderer class is to be used. The Run() function starts a Windows message loop for the application and makes the Form object that is passed as the argument visible. An application running with the CLR is still ultimately a Windows application, so it works with a message loop in the same way as all other Windows applications.
Adding a Menu to CLR Sketcher The IDE provides you with a standard set of controls that you can add to your application interactively through the Design window. Press Ctrl+Alt+X or select from the View menu to display the Toolbox window. The window containing the list of available groups of controls is displayed, as shown in Figure 15-17.
FIGURE 15-17
Menus and Toolbars in a C++/CLI Program
❘ 933
The block in the Toolbox window, labeled All Windows Forms, lists all the controls available for use with a form. You can click the [unfi lled] symbol for the All Windows Forms tab (if it is not already expanded) to see what is in it. This shows all the controls that you can use with Windows Forms. You can collapse any expanded group by clicking the [filled] symbol to the left of the block heading. The controls are also grouped by type in the list, starting with the Common Controls block. You may fi nd it most convenient, initially, to use the group containing all the controls at the beginning of the list, but after you are familiar with what controls are available, it will be easier to collapse the groups and just have one group expanded at one time. A menu strip is a container for menu items, so add one to the form by dragging a MenuStrip from the Menus & Toolbars group in the Toolbox window to the form; the menu strip attaches itself to the top of the form, below the title bar. You’ll see a small arrow at the top right of the control. If you click this, a pop-up window appears, as shown in Figure 15-18.
FIGURE 15-18
The fi rst item in the MenuStrip Tasks pop -up embeds the menu strip in a ToolStripContainer control that provides panels on all four sides of the form plus a central area in which you can place another control. The second item generates four standard menu items: File, Edit, Tools, and Help, complete with menu item lists. As you’ll see in a moment, you can also do this without displaying this pop -up. The RenderMode menu enables you to choose the painting style for the menu strip, and you can leave this at the default selection. The Dock option enables you to choose on which side of the form the menu strip is to be docked, or you can elect to have it undocked. The GripStyle option determines whether the grip for dragging the menu around is visible or not. The Visual C++ 2010 menu strips have visible grips that enable you to move them around. Selecting the fi nal Edit Items. . . option displays a dialog box in which you can edit the properties for the menu strip or for any of its menu items. If you right- click the menu strip and select Insert Standard Items from the pop -up, the standard menu items will be added; you’ll be implementing only the File menu item, but you can leave the others there if you want. Alternatively, you can delete any of them by right- clicking the item and selecting Delete from the pop -up. I ’ll leave them in so they will appear in the subsequent figures. To add the Element menu, click the box displaying “Type Here” and type E &lement . You can then click the box that displays below Element and type Line for the fi rst submenu item. Continue to add the remaining submenu items by entering Rectangle , Circle , and Curve in successive boxes in the drop - down menu. To shift the Element menu to the left of the Help menu, select Element on the menu strip and drag it to the left of Help. Next, you can add the Color menu by typing &Color in the “Type Here” fi eld on the menu strip. You can then enter Black , Red , Green , and Blue , successively, in the menu slots below Color to create the drop - down menu. After dragging Color to the left of Help, your application window should look like Figure 15 -19.
934
❘
CHAPTER 15
WORKING WITH MENUS AND TOOLBARS
FIGURE 15-19
To check the default Color ➪ Black menu item, rightclick the menu item and select Checked from the pop -up. Do the same for the Element ➪ Line menu item. All the code to create the menus is already in the application. You can see this if you select the Class View tab, select the [unfi lled] symbol next to CLRSketcher to extend the tree, then click CLRSketcher in the tree, and fi nally double- click the Form1 class name. The code for the Form1 class displays in a new tabbed window. Around line 39, you’ll see the class members that reference the menu items, and you’ll see the code creating the objects that encapsulate the menu items in the body of the InitializeComponent() member of the Form1 class. It’s worth browsing the code in this function from time to time as you develop CLR Sketcher, because it will show you the code for creating GUI components as well as how delegates for events are created and registered. The code in the Form1 class defi nition will grow considerably and look pretty daunting as you increase the capabilities of the application, but you always can navigate through it relatively easily using Class View. You can add shortcut key combinations for menu items by setting the value of the ShortcutKeys property. Figure 15-20 shows this property for the Line menu item.
FIGURE 15-20
Menus and Toolbars in a C++/CLI Program
❘ 935
Select the modifier or modifiers by clicking the checkboxes, and select the key from the drop -down list. Figure 15-20 shows Ctrl+Shift+L specified as the shortcut key combination. You can control whether or not the shortcut key combination is displayed in the menu by setting the value of the ShowShortcutKeys property appropriately. Take care not to specify duplicate key combinations for shortcuts. The next step is to add event handlers for the menu items.
Adding Event Handlers for Menu Items Event handlers are delegates in a C++/CLI program, but you hardly need to be aware of this in practice because all the functions that are delegates will be created automatically. Return to the Design tab by clicking on it. You can start by adding handlers for the items in the Element menu. If it is not already visible, extend the Element drop-down menu on the form by clicking it, then right- click the Line menu item and select Properties from the pop -up. Click the Events button in the Properties window (the one that looks like a lightning flash) and double- click the Click event at the top of the list. The IDE will switch to the code tab showing the delegate function for the menu item, which looks like this: private: System::Void lineToolStripMenuItem_Click(System::Object^ sender, System::EventArgs^ e) { }
I have rearranged the code slightly to make it easier to read. This function will be called automatically when you click the Line menu item in the application. The fi rst parameter identifies the source of the event, the menu item object in this case, and the second parameter provides information relating to the event. You’ll learn more about EventArgs objects a little later in this chapter. This function has already been identified as the event handler for the menu item with the following code: this->lineToolStripMenuItem->Click += gcnew System::EventHandler( this, &Form1::lineToolStripMenuItem_Click);
This is in the InitializeComponent() function, located at about line 364 in the Form1.h listing on my system, but if you deleted the surplus standard menu items, it will be a different line; you’ll fi nd it in the section labeled in comments as // lineToolStripMenuItem. You can add event handlers for all the menu items in the Element and Color menus in the same way. If you switch back to the Design tab and you have not closed the Properties window, clicking a new menu item will display the properties for that item in the window. You can then double- click the Click property to generate the handler function. Note that the names of the handler functions are created by default, but if you want to change the name, you can edit it in the value field to the right of the event name in the Properties window. The default names are quite satisfactory in this instance, though.
936
❘
CHAPTER 15
WORKING WITH MENUS AND TOOLBARS
Implementing Event Handlers Following the model of the MFC version of Sketcher, you need a way to identify different element types in the CLR version. An enum class will do nicely for this. Add the following line of code to the Form1.h fi le following the using directives at the beginning of the fi le, and immediately before the comments preceding the Form1 class defi nition: enum class ElementType {LINE, RECTANGLE, CIRCLE, CURVE};
This defi nes the four constants you need to specify the four types of elements in Sketcher. As soon as you have entered this code, you will see the new class appear in the Class View window as part of CLR Sketcher. To identify the colors, you can use objects of type System::Drawing::Color that represent colors. The System::Drawing namespace defi nes a wide range of types for use in drawing operations, and you’ll be using quite a number of them before CLR Sketcher is fi nished. The Color structure defi nes a lot of standard colors, including Color::Black, Color::Red, Color::Green, and Color::Blue, so you have everything you need. The element type and the drawing color are modal, so you should add variables to the Form1 class to store the current element type and color. You can do this manually or use a code wizard to do it. To use a code wizard, right- click Form1 in Class View and select Add ➪ Add Variable. . . from the pop -up. Select private for the access, double- click the default variable type and enter ElementType, and enter the variable name as elementType. You can also add a comment if you want, Current element type, and click Finish to add the variable to the class. Repeat the process for a private variable of type Color with the name color. The new variables need to be initialized when the Form1 constructor is called, so double- click the constructor name, Form1(void), in the lower part of the Class View window. (For the members of Form1 to be displayed, Form1 must be selected in Class View.) Modify the code for the constructor to the following: Form1(void) : elementType(ElementType::LINE), color(Color::Black) { InitializeComponent(); // //TODO: Add the constructor code here // }
The new variables are initialized in the fi rst line. You should see IntelliSense prompts for possible values for the ElementType and Color types as you type the initialization list. You now have enough in place to implement the menu event handlers. You can fi nd the Line menu item handler toward the end of the code in Form1.h, or double- click the Line menu item in the Design window to go directly to it. Modify the handler code to the following: private: System::Void lineToolStripMenuItem_Click( System::Object^ { elementType = ElementType::LINE; }
sender, System::EventArgs^
e)
Menus and Toolbars in a C++/CLI Program
❘ 937
This just sets the current element type appropriately. You can do the same for the other three Element menu handlers. Simple, isn’t it? The fi rst Color menu handler should be modified as follows: private: System::Void blackToolStripMenuItem_Click( System::Object^ sender, System::EventArgs^ e) { color = Color::Black; }
The other Color menu handlers should set the value of color appropriately to Color::Red, Color::Green, or Color::Blue.
Setting Menu Item Checks At present, selecting element and color menu items sets the mode in the program, but the checks don’t get set on the menus. You don’t have the COMMAND and COMMAND_UI message types with a CLR program that you have with the MFC, so you need a different approach to set the checkmark against the element and color menu items that are in effect. You can handle messages for the Element and Color menu items on the menu strip when one or the other is selected, and, conveniently, the handler for one of the messages is called before the corresponding drop -down menu is displayed. Right- click the Element menu item in the Design window and select Properties from the pop -up. When you select the Events button in the Properties window, the window should look like Figure 15-21. The Action group shows the events relating to when the menu item is clicked. Double- click the DropDownOpening event in the Action group to create a handler for it. This handler will be executed before the drop -down Element menu is displayed, so you can set the checkmark in the function by modifying it like this:
FIGURE 15-21
private: System::Void elementToolStripMenuItem_DropDownOpening( System::Object^ sender, System::EventArgs^ { lineToolStripMenuItem->Checked = elementType == ElementType::LINE; rectangleToolStripMenuItem->Checked = elementType == ElementType::RECTANGLE; circleToolStripMenuItem->Checked = elementType == ElementType::CIRCLE; curveToolStripMenuItem->Checked = elementType == ElementType::CURVE; }
The four lines of code you add set the Checked property for each of the menu items in the dropdown list. The Checked property will be set to true for the case in which the value stored in
e)
938
❘
CHAPTER 15
WORKING WITH MENUS AND TOOLBARS
elementType matches the element type set by the menu item, and all the others will be false. The
drop -down will then be displayed with the appropriate menu item checked. You can create a DropDownOpening event handler for the Color menu item and implement it like this: private: System::Void colorToolStripMenuItem_DropDownOpening( System::Object^ sender, System::EventArgs^ { blackToolStripMenuItem->Checked = color == Color::Black; redToolStripMenuItem->Checked = color == Color::Red; greenToolStripMenuItem->Checked = color == Color::Green; blueToolStripMenuItem->Checked = color == Color::Blue; }
e)
If you recompile CLR Sketcher and execute it, you should see that the menu item checks are working as they should.
Adding a Toolbar You can add a toolbar with buttons corresponding to the Element and Color menu items. To add a toolbar to the application, it’s back to the Design window and the toolbox. Toolbar buttons sit on a tool strip, so drag a ToolStrip control from the Menus & Toolbars group in the Toolbox window onto the form; the tool strip will attach itself to the top of the form, below the menu strip. You can add a set of standard toolbar buttons by right- clicking the tool strip and selecting Insert Standard Items from the pop -up. You get more than you need, and, because you will be adding eight more buttons, remove all but the four on the left that control creating a new document, opening a fi le, saving a fi le, and printing. To remove a button, just right- click it and select Delete from the pop -up. You will need bitmap images to display on the new toolbar buttons, so it’s a good idea to create these fi rst. Select the Resource View tab and expand the tree in the window so the app.rc file name is visible. The app.rc file contains resources such as icons and bitmaps that are used by the application, so you will add the bitmaps to this file. Right- click app.rc in the Resource View window, and then select Add Resource. . . from the pop-up. Select Bitmap in the dialog that displays and click the New button. You will then see a window with a default 48 48 pixel image that you can edit, and the Image Editor toolbar will be displayed for editing bitmap and icon images. The bitmap will be identified in the Resource View window with a default ID, IDB_BITMAP1. It will be convenient to change the ID and fi le name for the bitmap to something more meaningful, so extend app.rc in the Resource pane, right- click the IDB_BITMAP1 resource ID, and select Properties from the pop -up. In the Properties window, you can change the Filename property value to line .bmp and the ID property value to IDB_LINE . The Filename property here contains the full path to the bitmap resource fi le, so change only the file name part of the property value to what you entered before, line.bmp. You can then click the pane displaying the bitmap itself and change the Height and Width property values to 16 instead of 48, respectively. You can also change the Colors property value to 24 -bit, but I will stick with four-bit color values. The new ID will enable you to identify the bitmap in Resource View in the event that you want to edit it. The new file name will make it easy to choose the correct bitmap for a given toolbar button.
Menus and Toolbars in a C++/CLI Program
❘ 939
If the Colors window that displays the color palette is not visible, right- click in the window displaying the bitmap and select Show Colors Window from the pop-up. As with the icon editor you used in the native version of Sketcher, right- clicking a color in the Colors window sets the background color, and left- clicking a color sets the foreground color. When using the Image Editor tools, holding the left mouse button down when drawing in the bitmap uses the foreground color, and holding the right button down uses the background color. Use the Image Editor toolbar buttons to draw your own image to represent the Line menu item action. You can then save the file before adding the next bitmap. You can add three more bitmaps corresponding to the Rectangle, Circle, and Curve menu item actions to complete the FIGURE 15-22 element set. To add each of these, you can right-click the Bitmap folder name in the Resource View window and select Insert Bitmap from the pop-up. You should end up with four items in the Bitmap folder in Resource View, as shown in Figure 15-22. Create four more bitmap resources in app.rc for the color toolbar buttons. You can use the same principle for fi le names and IDs that you used for the element type bitmaps, so the fi les will be black.bmp, red.bmp, green.bmp, and blue.bmp, and the IDs will be IDB_BLACK, IDB_RED, IDB_GREEN, and IDB_BLUE. You can now return to the Design window to add the toolbar buttons. Click the down arrow adjacent to the Add ToolStripButton icon on the tool strip to display the list shown in Figure 15-23.
FIGURE 15-23
Select the Button item at the top of the list to add a button to the tool strip. You can then right- click the new button, select Set Image from the pop -up, and select line.bmp from the dialog that displays. Select the Open button in the dialog to set this image for the toolbar button.
940
❘
CHAPTER 15
WORKING WITH MENUS AND TOOLBARS
Right- click the new line button and display its properties. In the Properties window, change the value of the (Name) property in the Design group to toolStripLineButton. You can also change the value of the ToolTipText property to something helpful, such as Draw lines. The DisplayStyle property determines how the button is displayed on the toolbar. The default value is Image, which causes just the bitmap image to be displayed. You could change this to Text, which causes the value of the Text property to be displayed with no image. We need something short and meaningful for the value of the Text property, so I chose just the name of the shape, such as Circles for the button to draw circles. Setting the value of DisplayStyle to ImageAndText results in the image and the value of the Text property being displayed. To be consistent with the standard buttons, you really want the new button to be transparent. To arrange this, set the ImageTransparentColor property value for each of the buttons to White. Select the Events button to display the events for the button. You don’t want to create a new handler for this button as the handler for the Line menu item will work perfectly well. Click the down arrow in the value column for the Click event for the button to display the available handler functions, as shown in Figure 15-24.
FIGURE 15-24
Select the lineToolStripMenuItem_Click handler from the list to register this function as the delegate for the Click event for the button. Repeat this process to create buttons to create the element toolbar buttons for drawing rectangles, circles, and curves, and, in each case, set the Click event handler to be the corresponding menu item Click event handler. Before you add the buttons for element colors, click the down arrow at the side of the Add ToolStripButton icon on the toolbar and select Separator from the list; this inserts a separator to divide the element type button group from the colors group. You can now add the four buttons for colors, with the Click event handler being the same as the handler for the corresponding menu item. Don’t forget to set the button properties in the same way as for the element type buttons. The toolbar buttons don’t get updated when you change the element type or drawing color, so you need to add some code for this. You can add two private functions to the Form1 class, one to update the state of the element buttons and the other to update the state of the color buttons. You can add these by right- clicking Form1 in Class View and selecting Add ➪ Add Function. . . from the pop -up. Here’s the code for the fi rst function: // Set the state of the element type toolbar buttons void SetElementTypeButtonsState(void) { toolStripLineButton->Checked = elementType == ElementType::LINE; toolStripRectangleButton->Checked = elementType == ElementType::RECTANGLE;
Menus and Toolbars in a C++/CLI Program
❘ 941
toolStripCircleButton->Checked = elementType == ElementType::CIRCLE; toolStripCurveButton->Checked = elementType == ElementType::CURVE; }
Calling this function will set the state of all four element type buttons based on the value of elementType. Thus the button matching elementType will be set as checked. The function to set checks for the color buttons works in exactly the same way: // Set the state of the color toolbar buttons void SetColorButtonsState(void) { toolStripBlackButton->Checked = color == Color::Black; toolStripRedButton->Checked = color == Color::Red; toolStripGreenButton->Checked = color == Color::Green; toolStripBlueButton->Checked = color == Color::Blue; }
To set the initial state of the buttons correctly, you must call both functions in the Form1 class constructor: Form1(void) : elementType(ElementType::LINE), color(Color::Black) { InitializeComponent(); // SetElementTypeButtonsState(); SetColorButtonsState(); // }
You must also update the state of the buttons whenever a change occurs to either the element type or the current drawing color. Each of the Click event handlers for the element type menu items must call the SetElementTypeButtonsState() function, so modify all four handler functions like this: System::Void lineToolStripMenuItem_Click( System::Object^ { elementType = ElementType::LINE; SetElementTypeButtonsState(); }
sender, System::EventArgs^
e)
You can add a statement to each of the functions that deal with Click events for the color menu items in a similar way: System::Void blackToolStripMenuItem_Click( System::Object^ { color = Color::Black; SetColorButtonsState(); }
sender, System::EventArgs^
e)
942
❘
CHAPTER 15
WORKING WITH MENUS AND TOOLBARS
If you now recompile CLR Sketcher and execute it, you should see your version of the application as something like the window shown in Figure 15-25. Figure 15-25 shows the tooltip text for the red button, and you should fi nd that the menu items checks are all working regardless of whether you use the toolbar or the menus to select the element type and drawing color. You now have a CLR version of Sketcher that is essentially the same as the MFC version you developed earlier in this chapter. The two versions of Sketcher won’t correspond exactly in subsequent chapters, but the main features will be in both.
FIGURE 15-25
SUMMARY In this chapter, you learned how MFC connects a message with a class member function to process it, and you wrote your fi rst message handlers. Much of the work in writing a Windows program is writing message handlers, so it’s important to have a good grasp of what happens in the process. When we get to consider other message handlers, you’ll see that the process for adding them is just the same. You have also extended the standard menu and the toolbar in the MFC Application Wizard – generated program, which provides a good base for the application code that you add in the next chapter. Although there’s no functionality under the covers yet, the menu and toolbar operation looks very professional, courtesy of the Application Wizard – generated framework and the Event Handler Wizard. You learned about creating menus and toolbars in a CLR version of the Sketcher program and how you handle events to provide equivalent function to the MFC version. In the next chapter, you’ll add the code necessary to both versions of Sketcher to draw elements in a view, and use the menus and toolbar buttons that you created here to select what to draw and in which color. This is where the Sketcher program begins to live up to its name.
Summary
❘ 943
EXERCISES You can download the source code for the examples in the book and the solutions to the following exercises from www.wrox.com. You’ll be developing both versions of Sketcher in subsequent chapters, so you need to retain the state of the program at the end of each chapter. The exercises involve modifying the existing Sketcher versions, but you can retain the original state quite easily by copying the entire contents of the project folder to another folder — say, one called Save Sketcher. When you have finished the exercises on the original project, you can delete the contents of the project file (or save it somewhere else if you want to keep it) and copy the contents of the Save Sketcher folder back to the original project directory.
1.
Add the menu item Ellipse to the Element pop -up.
2.
Implement the command and command update handlers for it in the document class.
3.
Add a toolbar button corresponding to the Ellipse menu item, and add a tooltip for the button.
4.
Modify the command update handlers for the color menu items so that the currently selected item is displayed in uppercase, and the others in lowercase.
5.
Add an Ellipse menu item and toolbar button with a tooltip to CLR Sketcher.
944
❘
CHAPTER 15
WORKING WITH MENUS AND TOOLBARS
WHAT YOU LEARNED IN THIS CHAPTER TOPIC
CONCEPT
Message maps
MFC defines the message handlers for a class in a message map that appears in the .cpp file for the class.
Handling command messages
Command messages that arise from menus and toolbars can be handled in any class that ’s derived from CCmdTarget. These include the application class, the frame and child frame window classes, the document class, and the view class.
Handling non-command messages
Messages other than command messages can be handled only in a class derived from CWnd. This includes frame window and view classes, but not application or document classes.
Identifying a message handler for command messages
MFC has a predefined sequence for searching the classes in your program to find a message handler for a command message.
Adding message handlers
You should always use the Event Handler Wizard to add message handlers to your program.
Resource files
The physical appearances of menus and toolbars are defined in resource files, which are edited by the built- in resource editor.
Menu IDs
Items in a menu that can result in command messages are identified by a symbolic constant with the prefix ID. These IDs are used to associate a handler with the message from the menu item.
Relating toolbar buttons to menu items
To associate a toolbar button with a particular menu item, give it the same ID as the menu item.
Adding tooltips
To add a tooltip to a toolbar button corresponding to a menu item in your MFC application, select the Prompt property for the button in the Properties pane and enter \n, followed by the text for the tooltip as the property value.
Creating menus and toolbars in CLR programs
You create menus and toolbars interactively in a CLR program through the Design window. Code is generated automatically to create menu items and toolbar buttons and display them on a form.
Handling events in CLR programs
You can create a delegate to handle a specific event for a control by double - clicking the event type in the Property window for the control. You can optionally select an existing delegate to handle the event. This is particularly relevant to handling toolbar button events that are equivalent to existing menu items events.
16
Drawing in a Window WHAT YOU WILL LEARN IN THIS CHAPTER:
➤
What coordinate systems Windows provides for drawing in a window
➤
How to use the capabilities provided by a device context to draw shapes
➤
How and when your program draws in a window
➤
How to define handlers for mouse messages
➤
How to define your own shape classes
➤
How to program the mouse to draw shapes in a window
➤
How to get your program to capture the mouse
➤
How to implement Sketcher drawing capabilities in a C++/CLI program
In this chapter, you will add some meat to the Sketcher application. You’ll focus on understanding how you get graphical output displayed in the application window. By the end of this chapter, you’ll be able to draw all but one of the elements for which you have added menu items. I’ll leave the problem of how to store them in a document until the next chapter.
BASICS OF DRAWING IN A WINDOW As you learned in Chapter 12, if you want to draw in the client area of a window, you must obey the rules. In general, you must redraw the client area whenever a WM_PAINT message is sent to your application. This is because there are many events that require your application to redraw the window — such the user’s resizing the window you’re drawing in, or moving another window to expose part of your window that was previously hidden. The Windows
946
❘
CHAPTER 16
DRAWING IN A WINDOW
operating system sends information along with the WM_PAINT message that enables you to determine which part of the client area needs to be re- created. This means that you don’t have to draw all the client area in response to every WM_PAINT message, just the area identified as the update region. In an MFC application, MFC intercepts the WM_PAINT message and redirects it to a function in one of your classes. I’ll explain how to handle this a little later in this chapter.
The Window Client Area A window doesn’t have a fi xed position on-screen, or even a fi xed visible area, because the user can drag a window around using the mouse and resize it by dragging its borders. How, then, do you know where to draw on-screen? Fortunately, you don’t. Because Windows provides you with a consistent way of drawing in a window, you don’t have to worry about where it is on-screen; otherwise, drawing in a window would be inordinately complicated. Windows does this by maintaining a coordinate system for the client area of a window that is local to the window. It always uses the upper-left corner of the client area as its reference point. All points within the client area are defined relative to this point, as shown in Figure 16-1.
FIGURE 16 -1
The horizontal and vertical distances of a point from the upper-left corner of the client area will always be the same, regardless of where the window is on-screen or how big it is. Of course, Windows needs to keep track of where the window is, and when you draw something at a point in the client area, it needs to figure out where that point actually is on-screen.
The Windows Graphical Device Interface As you saw in Chapters 12 and 13, you don’t actually write data to the screen in any direct sense. All output to your display screen is graphical, regardless of whether it is lines and circles or text. Windows insists that you define this output using the Graphical Device Interface (GDI). The GDI enables you to program graphical output independently of the hardware on which it is displayed, meaning that your program works on different machines with different display hardware. In addition to display screens, the Windows GDI also supports printers and plotters, so outputting data to a printer or a plotter involves essentially the same mechanisms as displaying information on-screen.
Basics of Drawing in a Window
❘ 947
Working with a Device Context When you want to draw something on a graphical output device such as the display screen, you must use a device context. You used a device context back in Chapter 13 to draw the Mandelbrot set in the client area of a window. You also use a device context when you want to send output to other graphical devices, such as printers or plotters. A device context is a data structure that’s defi ned by Windows and contains information that allows Windows to translate your output requests, which are in the form of device-independent GDI function calls, into actions on the particular physical output device being used. You obtain a pointer to a device context by calling a Windows API function. A device context provides you with a choice of coordinate systems called mapping modes, which are automatically converted to client coordinates. You can also alter many of the parameters that affect the output to a device context by calling GDI functions: such parameters are called attributes. Examples of attributes that you can change are the drawing color, the background color, the line thickness to be used when drawing, and the font for text output. There are also GDI functions that provide information about the physical device with which you’re working, such as the resolution and aspect ratio of a display.
Mapping Modes Each mapping mode in a device context is identified by an ID, much like Windows messages. Each symbol has the prefi x MM_ to indicate that it defi nes a mapping mode. The mapping modes provided by Windows are as follows: MAPPING MODE
DESCRIPTION
MM_TEXT
A logical unit is one device pixel with positive x from left to right, and positive y from top to bottom of the window client area.
MM_LOENGLISH
A logical unit is 0.01 inches with positive x from left to right, and positive y from the top of the client area upward.
MM_HIENGLISH
A logical unit is 0.001 inches with the x and y directions as in MM_LOENGLISH.
MM_LOMETRIC
A logical unit is 0.1 millimeters with the x and y directions as in MM_LOENGLISH.
MM_HIMETRIC
A logical unit is 0.01 millimeters with the x and y directions as in MM_LOENGLISH.
MM_ISOTROPIC
A logical unit is of arbitrary length, but the same along both the x and y axes. The x and y directions are as in MM_LOENGLISH.
MM_ANISOTROPIC
This mode is similar to MM_ISOTROPIC, but allows the length of a logical unit on the x-axis to be different from that of a logical unit on the y-axis.
MM_TWIPS
A logical unit is a TWIP where a TWIP is 0.05 of a point and a point is 1/72 of an inch. Thus, a TWIP corresponds to 1/1440 of an inch, which is 6.9*10 - 4 of an inch. (A point is a unit of measurement for fonts.) The x and y directions are as in MM_LOENGLISH.
948
❘
CHAPTER 16
DRAWING IN A WINDOW
You will not use all these mapping modes with this book; however, the ones you will use form a good cross-section of those available, so you won’t have any problem using the others when you need to. MM_TEXT is the default mapping mode for a device context. If you need to use a different mapping mode, you have to take steps to change it. Note that the direction of the positive y-axis in the MM_TEXT
mode is the opposite of what you saw in high-school coordinate geometry, as shown in Figure 16-1. By default, the point at the upper-left corner of the client area has the coordinates (0,0) in every mapping mode, although it’s possible to move the origin away from the upper-left corner of the client area if you want to. For example, some applications that present data in graphical form move the origin to the center of the client area to make plotting of the data easier. With the origin at the upperleft corner in MM_TEXT mode, a point 50 pixels from the left border and 100 pixels down from the top of the client area will have the coordinates (50,100). Of course, because the units are pixels, the point will be nearer the upper-left corner of the client area if your monitor is set to use a resolution of 1280×1024 than if it’s working with the resolution set as 1024×768. An object drawn in this mapping mode will be smaller at the 1280×1024 resolution than at the 1024×768 resolution. Note that the DPI setting for your display affects presentation in all mapping modes. The default settings assume 96 DPI, so if the DPI for your display is set to a different value, this affects how things look. Coordinates are always 32-bit signed integers unless you are programming for the old Windows 95/98 operating systems, in which case, they are limited to 16 bits. The maximum physical size of the total drawing varies with the physical length of a coordinate unit, which is determined by the mapping mode. The directions of the x and y coordinate axes are the same in MM_LOENGLISH and all the remaining mapping modes, but different in MM_TEXT. The coordinate axes for MM_LOENGLISH are shown in Figure 16 -2. Although positive y is consistent with what you learned in high school (y values increase as you move up the screen), MM_LOENGLISH is still slightly odd because the origin is at the upper-left corner of the client area, so for points within the visible client area, y is always negative.
FIGURE 16 -2
In the MM_LOENGLISH mapping mode, the units along the axes are 0.01 inches, so a point at the position (50,-100) is half an inch from the left border and one inch down from the top of the client area. An object is the same size in the client area, regardless of the resolution of the monitor on which it is displayed. If you draw anything in the MM_LOENGLISH mode with negative x or positive y coordinates, it is outside the client area and, therefore, not visible, because the reference point (0,0) is the upper-left corner by default. It’s possible to move the position of the reference point, though, by calling the Windows API function SetViewportOrg() (or the SetViewportOrg() member of the CDC class that encapsulates a device context, which I’ll discuss shortly).
THE DRAWING MECHANISM IN VISUAL C++ The MFC encapsulates the Windows interface to your screen and printer and relieves you of the need to worry about much of the details involved in programming graphical output. As you saw in the last chapter, the Application Wizard – generated program already contains a class derived from the MFC class CView that’s specifically designed to display document data on-screen.
The Drawing Mechanism in Visual C++
❘ 949
The View Class in Your Application The MFC Application Wizard generated the class CSketcherView to display information from a document in the client area of a document window. The class definition includes overrides for several virtual functions, but the one of particular interest here is the function OnDraw(). This is called whenever the client area of the document window needs to be redrawn. It’s the function that’s called by the application framework when a WM_PAINT message is received in your program.
The OnDraw() Member Function The implementation of the OnDraw() member function that’s created by the MFC Application Wizard looks like this: void CSketcherView::OnDraw(CDC* /*pDC*/) { CSketcherDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // TODO: add draw code for native data here }
A pointer to an object of the CDC class type is passed to the OnDraw() member of the view class. This object has member functions that call the Windows API functions, and these enable you to draw in a device context. Note that the parameter name is commented out: you must uncomment the name or replace it with your own name before you can use the pointer. Because you’ll put all the code to draw the document in this function, the Application Wizard has included a declaration for the pointer pDoc and initialized it using the function GetDocument(), which returns the address of the document object related to the current view: CSketcherDoc* pDoc = GetDocument();
The GetDocument() function that is defi ned in the CSketcherView class retrieves the pointer to the document from m_pDocument, an inherited data member of the view object. The function performs the important task of casting the pointer stored in this data member to the type corresponding to the document class in the application, CSketcherDoc. This is so you can use the pointer to access the members of the document class that you’ve defi ned; otherwise, you could use the pointer only to access the members of the base class. Thus, pDoc points to the document object in your application that is associated with the current view, and you will be using it to access the data that you’ve stored in the document object when you want to draw it. The following line — ASSERT_VALID(pDoc);
— just makes sure that the pointer pDoc contains a valid address, and the if statement that follows ensures that pDoc is not null. ASSERT_VALID is ignored in a release version of the application. The name of the parameter pDC for the OnDraw() function stands for “pointer to device context.” The object of the CDC class pointed to by the pDC argument that’s passed to the OnDraw()
950
❘
CHAPTER 16
DRAWING IN A WINDOW
function is the key to drawing in a window. It provides a device context, plus the tools you need to write graphics and text to it, so you clearly need to look at it in more detail.
The CDC Class You should do all the drawing in your program using members of the CDC class. All objects of this class and classes derived from it contain a device context and the member functions you need for sending graphics and text to your display and your printer. There are also member functions for retrieving information about the physical output device you are using. Because CDC class objects can provide almost everything you’re likely to need in the way of graphical output, there are a lot of member functions of this class — in fact, well over a hundred. Therefore, in this chapter, you’ll look only at the ones you’re going to use in the Sketcher program; we’ll go into others as you need them later on. Note that MFC includes some more specialized classes for graphics output that are derived from CDC. For example, you will be using objects of CClientDC because it is derived from CDC and contains all the members we will discuss at this point. The advantage that CClientDC has over CDC is that it always contains a device context that represents only the client area of a window, and this is precisely what you want in most circumstances.
Displaying Graphics In a device context, you draw entities such as lines, circles, and text relative to a current position. A current position is a point in the client area that was set either by the previous entity that was drawn or by your calling a function to set it. For example, you could extend the OnDraw() function to set the current position as follows: void CSketcherView::OnDraw(CDC* pDC) { CSketcherDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; pDC->MoveTo(50, 50);
// Set the current position as 50,50
}
Note that the fi rst line is bolded because it’s necessary to uncomment the parameter name if this code is to compile. The second bolded line calls the MoveTo() function for the CDC object pointed to by pDC. This member function simply sets the current position to the x and y coordinates you specify as arguments. As you saw earlier, the default mapping mode is MM_TEXT, so the coordinates are in pixels and the current position will be set to a point 50 pixels from the inside-left border of the window, and 50 pixels down from the top of the client area. The CDC class overloads the MoveTo() function to provide flexibility as to how you specify the position that you want to set as the current position. There are two versions of the function, declared in the CDC class as the following: CPoint MoveTo(int x, int y); CPoint MoveTo(POINT aPoint);
// Move to position x,y // Move to position defined by aPoint
The Drawing Mechanism in Visual C++
❘ 951
The fi rst version accepts the x and y coordinates as separate arguments. The second accepts one argument of type POINT, which is a structure defi ned as follows: typedef struct tagPOINT { LONG x; LONG y; } POINT;
The coordinates are members of the struct and are of type LONG (which is a type defi ned in the Windows API as a 32-bit signed integer). You may prefer to use a class instead of a structure, in which case, you can use objects of the class CPoint anywhere that a POINT object can be used. The class CPoint has data members x and y of type LONG, and using CPoint objects has the advantage that the class also defi nes member functions that operate on CPoint and POINT objects. This may seem weird because CPoint would seem to make POINT objects obsolete, but remember that the Windows API was built before MFC was around, and POINT objects are used in the Windows API and have to be dealt with sooner or later. I’ll use CPoint objects in examples, so you’ll have an opportunity to see some of the member functions in action. The return value from the MoveTo() function is a CPoint object that specifies the current position as it was before the move. You might think this a little odd, but consider the situation in which you want to move to a new position, draw something, and then move back. You may not know the current position before the move, and after the move occurs, it is lost, so returning the position before the move makes sure it’s available to you if you need it.
Drawing Lines You can follow the call to MoveTo() in the OnDraw() function with a call to the function LineTo(), which draws a line in the client area from the current position to the point specified by the arguments to the LineTo() function, as illustrated in Figure 16 -3. The CDC class also defi nes two versions of the LineTo() function that have the following prototypes: BOOL LineTo(int x, int y); BOOL LineTo(POINT aPoint);
FIGURE 16 -3
// Draw a line to position x,y // Draw a line to position defined by aPoint
This offers you the same flexibility in specifying the argument to the function as with MoveTo(). You can use a CPoint object as an argument to the second version of the function. The function returns TRUE if the line was drawn and FALSE otherwise. When the LineTo() function is executed, the current position is changed to the point specifying the end of the line. This enables you to draw a series of connected lines by just calling the LineTo() function for each line. Look at the following version of the OnDraw() function: void CSketcherView::OnDraw(CDC* pDC) { CSketcherDoc* pDoc = GetDocument();
952
❘
CHAPTER 16
DRAWING IN A WINDOW
ASSERT_VALID(pDoc); if(!pDoc) return; pDC->MoveTo(50,50); pDC->LineTo(50,200); pDC->LineTo(150,200); pDC->LineTo(150,50); pDC->LineTo(50,50);
// // // // //
Set the current position Draw a vertical line down 150 units Draw a horizontal line right 100 units Draw a vertical line up 150 units Draw a horizontal line left 100 units
}
If you plug this code into the Sketcher program and execute it, it will display the document window shown in Figure 16 - 4. Don’t forget to uncomment the parameter name.
FIGURE 16 -4
The four calls to the LineTo() function draw the rectangle shown counterclockwise, starting with the upper-left corner. The fi rst call uses the current position set by the MoveTo() function; the succeeding calls use the current position set by the previous LineTo() function call. You can use this method to draw any figure consisting of a sequence of lines, each connected to the previous line. Of course, you are also free to use MoveTo() to change the current position at any time.
Drawing Circles You have a choice of several function members in the CDC class for drawing circles, but they’re all designed to draw ellipses. As you know from high-school geometry, a circle is a special case of an ellipse, with the major and minor axes equal. You can, therefore, use the member function Ellipse() to draw a circle. Like other closed shapes supported by the CDC class, the Ellipse() function fi lls the interior of the shape with a color that you set. The interior color is determined by a brush that is selected into the device context. The current brush in the device context determines how any closed shape is fi lled. There are two versions of the Ellipse() function: BOOL Ellipse(int x1, int y1, int x2, int y2); BOOL Ellipse(LPCRECT lpRect);
The Drawing Mechanism in Visual C++
❘ 953
The fi rst version draws an ellipse bounded by the rectangle defi ned by the points x1,y1, and x2,y2. In the second version, the ellipse is defi ned by a RECT object that is pointed to by the argument to the function. The function also accepts a pointer to an object of the MFC class CRect, which has four public data members: left, top, right, and bottom. These correspond to the x and y coordinates of the upper-left and lower-right points of the rectangle, respectively. The CRect class also provides a range of function members that operate on CRect objects, and we shall be using some of these later. The Ellipse() function returns TRUE if the operation was successful and FALSE if it was not. With either function, the ellipse that is drawn extends up to, but does not include, the right and bottom sides of the rectangle. This means that the width and height of the ellipse are x2–x1 and y2–y1, respectively. MFC provides the CBrush class that you can use to defi ne a brush. You can set the color of a CBrush object and also defi ne a pattern to be produced when you are fi lling a closed shape. If you want to draw a closed shape that isn’t fi lled, you can select a null brush in the device context, which leaves the interior of the shape empty. I’ll come back to brushes a little later in this chapter. Another way to draw circles that aren’t fi lled is to use the Arc() function, which doesn’t involve brushes. This has the advantage that you can draw any arc of an ellipse, rather than the complete curve. There are two versions of this function in the CDC class, declared as follows: BOOL Arc(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4); BOOL Arc(LPCRECT lpRect, POINT startPt, POINT endPt);
In the fi rst version, (x1,y1) and (x2,y2) defi ne the upper-left and lower-right corners of a rectangle enclosing the complete curve. If you make these coordinates into the corners of a square, the curve drawn is a segment of a circle. The points (x3,y3) and (x4,y4) defi ne the start and end points of the segment to be drawn. The segment is drawn counterclockwise. If you make (x4,y4) identical to (x3,y3), you’ll generate a complete, apparently closed curve, although it will not, in fact, be closed. In the second version of Arc(), the enclosing rectangle is defi ned by a RECT object, and a pointer to this object is passed as the fi rst argument. The POINT objects startPt and endPt in the second version of Arc() defi ne the start and end of the arc to be drawn, respectively. Here’s some code that exercises both the Ellipse() and Arc() functions: void CSketcherView::OnDraw(CDC* pDC) { CSketcherDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; pDC->Ellipse(50,50,150,150);
// Draw the 1st (large) circle
// Define the bounding rectangle for the 2nd (smaller) circle CRect rect(250,50,300,100); CPoint start(275,100); // Arc start point CPoint end(250,75); // Arc end point pDC->Arc(&rect, start, end); // Draw the second circle }
954
❘
CHAPTER 16
DRAWING IN A WINDOW
Note that you used a CRect class object instead of a RECT structure to defi ne the bounding rectangle, and that you used CPoint class objects instead of POINT structures. You’ll also be using CRect objects later, but they have some limitations, as you’ll see. The Arc() and Ellipse() functions do not require a current position to be set, as the position and size of the arc are completely defi ned by the arguments you supply. The current position is unaffected by the drawing of an arc or an ellipse — it remains exactly wherever it was before the shape was drawn. Now, try running Sketcher with this code in the OnDraw() function. You should get the results shown in Figure 16 -5.
FIGURE 16 -5
Try resizing the borders. The client area is automatically redrawn as you cover or uncover the arcs in the picture. Remember that screen resolution affects the scale of what is displayed. The lower the screen resolution you’re using, the larger and farther from the upper-left corner of the client area the arcs will be.
Drawing in Color Everything that you’ve drawn so far has appeared on the screen in black. Drawing always uses a pen object that has a color and a thickness, and you’ve been using the default pen object that is provided in a device context. You’re not obliged to do this, of course — you can create your own pen with a given thickness and color. MFC defi nes the class CPen to help you do this. All closed curves that you draw are fi lled with the current brush in the device context. As mentioned earlier, you can defi ne a brush as an instance of the class CBrush. Take a look at some of the features of CPen and CBrush objects.
Creating a Pen The simplest way to create a pen object is fi rst to declare an object of the CPen class: CPen aPen;
// Declare a pen object
The Drawing Mechanism in Visual C++
❘ 955
This object now needs to be initialized with the properties you want. You initialize it by calling the CreatePen() function for the object. The function is declared in the CPen class as follows: BOOL CreatePen (int aPenStyle, int aWidth, COLORREF aColor);
The function returns TRUE as long as the pen is successfully initialized and FALSE otherwise. The fi rst argument defi nes the line style that you want to use when drawing. You must specify it with one of the following symbolic values: PEN STYLE
DESCRIPTION
PS_SOLID
The pen draws a solid line.
PS_DASH
The pen draws a dashed line. This line style is valid only when the pen width is specified as 1.
PS_DOT
The pen draws a dotted line. This line style is valid only when the pen width is specified as 1.
PS_DASHDOT
The pen draws a line with alternating dashes and dots. This line style is valid only when the pen width is specified as 1.
PS_DASHDOTDOT
The pen draws a line with alternating dashes and double dots. This line style is valid only when the pen width is specified as 1.
PS_NULL
The pen doesn’t draw anything.
PS_INSIDEFRAME
The pen draws a solid line but, unlike with PS_SOLID, the points that specify the line occur on the edge of the pen rather than in the center, so that the drawn object never extends beyond the enclosing rectangle for closed shapes such as ellipses.
The second argument to the CreatePen() function defi nes the line width. If aWidth has the value 0, the line drawn is one pixel wide, regardless of the mapping mode in effect. For values of 1 or more, the pen width is in the units determined by the mapping mode. For example, a value of 2 for aWidth in MM_TEXT mode is two pixels; in MM_LOENGLISH mode, the pen width is 0.02 inches. The last argument specifies the color to be used when drawing with the pen, so you could initialize a pen with the following statement: aPen.CreatePen(PS_SOLID, 2, RGB(255,0,0));
// Create a red solid pen
Assuming that the mapping mode is MM_TEXT, this pen draws a solid red line that is two pixels wide. You can also create a pen object with specified line type, width, and color: CPen aPen(PS_SOLID, 2, RGB(255, 0, 0));
956
❘
CHAPTER 16
DRAWING IN A WINDOW
Using a Pen To use a pen, you must select it into the device context in which you are drawing. To do this, you use the CDC class member function SelectObject(). To select the pen you want to use, you call this function with a pointer to the pen object as an argument. The function returns a pointer to the previous pen object being used, so that you can save it and restore the old pen when you have fi nished drawing. A typical statement selecting a pen is the following: CPen* pOldPen = pDC->SelectObject(&aPen);
// Select aPen as the pen
You must always return a device context to its original state once you have fi nished with whatever you have selected into it. To restore the old pen when you’re done, you simply call the function again, passing the pointer returned from the original call: pDC->SelectObject(pOldPen);
// Restore the old pen
When you create a pen of your own in this way, you are creating a Windows GDI PEN object that is encapsulated by the CPen object. When the CPen object is destroyed, the CPen class destructor will delete the GDI pen object automatically. If you create a GDI PEN object explicitly, you must delete the Windows GDI object by calling DeleteObject() with the PEN object as the argument. You can see all this in action if you amend the previous version of the OnDraw() function in the CSketcherView class to the following: void CSketcherView::OnDraw(CDC* pDC) { CSketcherDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; // Declare a pen object and initialize it as // a red solid pen drawing a line 2 pixels wide CPen aPen; aPen.CreatePen(PS_SOLID, 2, RGB(255, 0, 0)); CPen* pOldPen = pDC->SelectObject(&aPen); // Select aPen as the pen pDC->Ellipse(50,50,150,150); // Draw the 1st (large) circle // Define the bounding rectangle for the 2nd (smaller) circle CRect rect(250,50,300,100); CPoint start(275,100); // Arc start point CPoint end(250,75); // Arc end point pDC->Arc(&rect,start, end); // Draw the second circle pDC->SelectObject(pOldPen); // Restore the old pen }
If you build and execute the Sketcher application with this version of the OnDraw() function, you get the same arcs drawn as before, but this time, the lines will be thicker and they’ll be red. You could usefully experiment with this example by trying different combinations of arguments to the CreatePen() function and seeing their effects. Note that we have ignored the value returned from
The Drawing Mechanism in Visual C++
❘ 957
the CreatePen() function, so you run the risk of the function failing and not detecting it in the program. It doesn’t matter here, as the program is still trivial, but as you develop the program, it becomes important to check for failures of this kind.
Creating a Brush An object of the CBrush class encapsulates a Windows brush. You can defi ne a brush to be solid, hatched, or patterned. A brush is actually an eight-by- eight block of pixels that’s repeated over the region to be fi lled. To defi ne a brush with a solid color, you can specify the color when you create the brush object. For example: CBrush aBrush(RGB(255,0,0));
// Define a red brush
This statement defi nes a red brush. The value passed to the constructor must be of type COLORREF, which is the type returned by the RGB() macro, so this is a good way to specify the color. Another constructor is available to defi ne a hatched brush. It requires two arguments to be specified, the fi rst defi ning the type of hatching, and the second specifying the color, as before. The hatching argument can be any of the following symbolic constants: HATCHING STYLE
DESCRIPTION
HS_HORIZONTAL
Horizontal hatching
HS_VERTICAL
Vertical hatching
HS_BDIAGONAL
Downward hatching from left to right at 45 degrees
HS_FDIAGONAL
Upward hatching from left to right at 45 degrees
HS_CROSS
Horizontal and vertical crosshatching
HS_DIAGCROSS
Crosshatching at 45 degrees
So to obtain a red, 45-degree crosshatched brush, you could defi ne the CBrush object with the following statement: CBrush aBrush(HS_DIAGCROSS, RGB(255,0,0));
You can also initialize a CBrush object much as you do a CPen object, by using the CreateSolidBrush() member function of the class for a solid brush, and the CreateHatchBrush() member for a hatched brush. They require the same arguments as the equivalent constructors. For example, you could create the same hatched brush as before with the following statements: CBrush aBrush; // Define a brush object aBrush.CreateHatchBrush(HS_DIAGCROSS, RGB(255,0,0));
958
❘
CHAPTER 16
DRAWING IN A WINDOW
Using a Brush To use a brush, you select the brush into the device context by calling the SelectObject() member of the CDC class much as you would for a pen. This member function is overloaded to support selecting brush objects into a device context. To select the brush defi ned previously, you would simply write the following: CBrush* pOldBrush = pDC->SelectObject(&aBrush);
// Select the brush into the DC
The SelectObject() function returns a pointer to the old brush, or NULL if the operation fails. You use the pointer that is returned to restore the old brush in the device context when you are done. There are a number of standard brushes available. Each of the standard brushes is identified by a predefi ned symbolic constant, and there are seven that you can use. They are the following: GRAY_BRUSH
LTGRAY_BRUSH
BLACK_BRUSH
WHITE_BRUSH
HOLLOW_BRUSH
NULL_BRUSH
DKGRAY_BRUSH
The names of these brushes are quite self-explanatory. To use one, call the SelectStockObject() member of the CDC class, passing the symbolic name for the brush that you want to use as an argument. To select the null brush, which leaves the interior of a closed shape unfilled, you could write this: CBrush* pOldBrush = pDC->SelectStockObject(NULL_BRUSH);
Here, pDC is a pointer to a CDC object, as before. You can also use one of a range of standard pens through this function. The symbols for standard pens are BLACK_PEN, NULL_PEN (which doesn’t draw anything), and WHITE_PEN. The SelectStockObject() function returns a pointer to the object being replaced in the device context. This enables you to restore it later, when you have finished drawing. Because the function works with a variety of objects — you’ve seen pens and brushes in this chapter, but it also works with fonts — the type of the pointer returned is CGdiObject*. The CGdiObject class is a base class for all the graphic device interface object classes, and thus, a pointer to this class can be used to store a pointer to any object of these types. You could store the pointer returned by SelectObject() or SelectStockObject() as type CGdiObject* and pass that to SelectObject() when you want to restore it. However, it is better to cast the pointer value returned to the appropriate type so that you can keep track of the types of objects you are restoring in the device context. The typical pattern of coding for using a stock brush and later restoring the old brush when you’re done is this:
Drawing Graphics in Practice
❘ 959
CBrush* pOldBrush = (CBrush*)pDC->SelectStockObject(NULL_BRUSH); // draw something... pDC->SelectObject(pOldBrush);
// Restore the old brush
You’ll be using this pattern in an example later in the chapter.
DRAWING GRAPHICS IN PRACTICE You now know how to draw lines and arcs, so it’s about time to consider how the user is going to defi ne what he or she wants drawn in Sketcher. In other words, you need to decide how the user interface is going to work. Because the Sketcher program is to be a sketching tool, you don’t want the user to worry about coordinates. The easiest mechanism for drawing is using just the mouse. To draw a line, for instance, the user could position the cursor and press the left mouse button where he or she wanted the line to start, and then defi ne the end of the line by moving the cursor with the left button held down. It would be ideal if you could arrange for the line to be continuously drawn as the cursor was moved with the left button down (this is known as “rubber-banding” to graphic designers). The line would be fi xed when the left mouse button was released. This process is illustrated in Figure 16 - 6.
Left mouse button down
Line is fixed when the mouse button is released
Left mouse button up
Cursor movement Line is continuously updated as the cursor moves FIGURE 16 - 6
You could allow circles to be drawn in a similar fashion. The fi rst press of the left mouse button would defi ne the center, and as the cursor was moved with the button down, the program would track it. The circle would be continuously redrawn, with the current cursor position defi ning a point on the circumference of the circle. Like the line, the circle would be fi xed when the left mouse button was released. You can see this process illustrated in Figure 16 -7.
960
❘
CHAPTER 16
DRAWING IN A WINDOW
Circle is fixed when the mouse button is released
Left mouse button down
Left mouse button up
Cursor movement
Circle is continuously updated as the cursor moves FIGURE 16 -7
You can draw a rectangle as easily as you draw a line, as illustrated in Figure 16 -8.
Left mouse button down
Rectangle is fixed when the mouse button is released
Left mouse button up
Cursor movement
Rectangle is continuously updated as the cursor moves FIGURE 16 -8
Programming for the Mouse
❘ 961
The fi rst point is defi ned by the position of the cursor when the left mouse button is pressed. This is one corner of the rectangle. The position of the cursor when the mouse is moved with the left button held down defi nes the diagonal opposite corner of the rectangle. The rectangle actually stored is the last one defi ned when the left mouse button is released. A curve is somewhat different. An arbitrary number of points may defi ne a curve. The mechanism you’ll use is illustrated in Figure 16 -9.
Left mouse button down
Left mouse button up stops tracking of the cursor and ends curve.
Cursor path
Curve is defined by straight line segments joining successive cursor positions FIGURE 16 - 9
As with the other shapes, the fi rst point is defi ned by the cursor position when the left mouse button is pressed. Successive positions recorded when the mouse is moved are connected by straight line segments to form the curve, so the mouse track defi nes the curve to be drawn. Now that you know how the user is going to defi ne an element, clearly, the next step in understanding how to implement this process is to get a grip on how the mouse is programmed.
PROGRAMMING FOR THE MOUSE To be able to program the drawing of shapes in the way that I have discussed, you need to focus on how the mouse will be used: ➤
Pressing a mouse button signals the start of a drawing operation.
➤
The location of the cursor when the mouse button is pressed defi nes a reference point for the shape.
962
❘
CHAPTER 16
DRAWING IN A WINDOW
➤
A mouse movement after detecting that a mouse button has been pressed is a cue to draw a shape, and the cursor position provides a defi ning point for the shape.
➤
The mouse button’s being released signals the end of the drawing operation and that the fi nal version of the shape should be drawn according to the cursor position.
As you may have guessed, all this information is provided by Windows in the form of messages sent to your program. The implementation of the process for drawing lines and circles consists almost entirely of writing message handlers.
Messages from the Mouse When the user of our program is drawing a shape, he or she will interact with a particular document view. The view class is, therefore, the obvious place to put the message handlers for the mouse. Right- click the CSketcherView class name in Class View and then display its properties window by selecting Properties from the context menu. If you then click the messages button (wait for the button tooltips to display if you don’t know which it is) you’ll see the list of message IDs for the standard Windows messages sent to the class, which are prefi xed with WM_ (see Figure 16 -10).
FIGURE 16 -10
You need to know about the following three mouse messages at the moment: MESSAGE
DESCRIPTION
WM_LBUTTONDOWN
A message occurs when the left mouse button is pressed.
WM_LBUTTONUP
A message occurs when the left mouse button is released.
WM_MOUSEMOVE
A message occurs when the mouse is moved.
These messages are quite independent of one another and are being sent to the document views in your program even if you haven’t supplied handlers for them. It’s quite possible for a window to receive a WM_LBUTTONUP message without having previously received a WM_LBUTTONDOWN message. This can happen if the button is pressed with the cursor over another window and the cursor is then moved to your view window before being released, so you must keep this in mind when coding handlers for these messages. If you look at the list in the Properties window, you’ll see there are other mouse messages that can occur. You can choose to process any or all of the messages, depending on your application requirements. Let’s defi ne in general terms what we want to do with the three messages we are currently interested in, based on the process for drawing lines, rectangles, and circles that you saw earlier.
Programming for the Mouse
❘ 963
WM_LBUTTONDOWN This starts the process of drawing an element. You should do the following:
1. 2.
Note that the element-drawing process has started. Record the current cursor position as the fi rst point for defi ning an element.
WM_MOUSEMOVE This is an intermediate stage in which you want to create and draw a temporary version of the current element, but only if the left mouse button is down. Complete the following steps:
1. 2. 3. 4. 5.
Check that the left button is down. If it is, delete any previous version of the current element that was drawn. If it isn’t, then exit. Record the current cursor position as the second defi ning point for the current element. Cause the current element to be drawn using the two defi ning points.
WM_LBUTTONUP This indicates that the process for drawing an element is fi nished. All that you need to do is as follows:
1.
Store the fi nal version of the element defi ned by the fi rst point recorded, together with the position of the cursor when the button is released for the second point.
2.
Record the end of the process of drawing an element.
Now you are ready to generate handlers for these three mouse messages.
Mouse Message Handlers You can create a handler for one of the mouse messages by clicking the ID to select it in the Properties window for the view class and then selecting the down arrow in the adjacent column position; try selecting OnLButtonUp for the WM_LBUTTONUP message, for example. Repeat the process for each of the messages WM_LBUTTONDOWN and WM_MOUSEMOVE. The functions generated in the CSketcherView class are OnLButtonDown(), OnLButtonUp(), and OnMouseMove(). You don’t get the option of changing the names of these functions, because you’re adding overrides for versions that are already defi ned in the base class for the CSketcherView class. Now, we can look at how you implement these handlers. You can start by looking at the WM_LBUTTONDOWN message handler. This is the skeleton code that’s generated: void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default CView::OnLButtonDown(nFlags, point); }
964
❘
CHAPTER 16
DRAWING IN A WINDOW
You can see that there is a call to the base class handler in the skeleton version. This ensures that the base handler is called if you don’t add any code here. In this case, you don’t need to call the base class handler when you handle the message yourself, although you can if you want to. Whether you need to call the base class handler for a message depends on the circumstances. Generally, the comment indicating where you should add your own code is a good guide. Where it suggests, as in the present instance, that calling the base class handler is optional, you can omit it when you add your own message-handling code. Note that the position of the comment in relation to the call of the base class handler is also important, as sometimes you must call the base class message handler before your code, and other times afterward. The comment indicates where your code should appear in relation to the base class message handler call. The handler in your class is passed two arguments: nFlags, which is of type UINT and contains a number of status flags indicating whether various keys are down, and the CPoint object point, which defi nes the cursor position when the left mouse button was pressed. The UINT type is defi ned in the Windows API and corresponds to a 32-bit unsigned integer. The value of nFlags that is passed to the function can be any combination of the following symbolic values: FLAG
DESCRIPTION
MK_CONTROL
Corresponds to the control key ’s being pressed
MK_LBUTTON
Corresponds to the left mouse button’s being down
MK_MBUTTON
Corresponds to the middle mouse button’s being down
MK_RBUTTON
Corresponds to the right mouse button’s being down
MK_XBUTTON1
Corresponds to the first extra mouse button’s being down
MK_XBUTTON2
Corresponds to the second extra mouse button’s being down
MK_SHIFT
Corresponds to the shift key ’s being pressed
Being able to detect if a key is down in the message handler enables you to support different actions for the message, depending on what else you fi nd. The value of nFlags may contain more than one of these indicators, each of which corresponds to a particular bit in the word, so you can test for a particular key using the bitwise AND operator. For example, to test for the control key’s being pressed, you could write the following: if(nFlags & MK_CONTROL) // Do something...
The expression nFlags & MK_CONTROL will have the value true only if the nFlags variable has the bit defi ned by MK_CONTROL set. In this way, you can perform different actions when the left mouse
Programming for the Mouse
❘ 965
button is pressed, depending on whether or not the control key is also pressed. You use the bitwise AND operator here, so corresponding bits are AND- ed together. Don’t confuse this operator with the logical and, &&, which would not do what you want here. The arguments passed to the other two message handlers are the same as those for the OnLButtonDown() function; the code generated for them is as follows: void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default CView::OnLButtonUp(nFlags, point); } void CSketcherView::OnMouseMove(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default CView::OnMouseMove(nFlags, point); }
Apart from the function names, the skeleton code is more or less the same for each. If you take a look at the end of the code for the CSketcherView class defi nition, you’ll see that three function declarations have been added: // Generated message map functions protected: DECLARE_MESSAGE_MAP() public: afx_msg void OnLButtonDown(UINT nFlags, CPoint point); afx_msg void OnLButtonUp(UINT nFlags, CPoint point); afx_msg void OnMouseMove(UINT nFlags, CPoint point); };
These identify the functions that you added as message handlers. Now that you have an understanding of the information passed to the message handlers you have created, you can start adding your own code to make them do what you want.
Drawing Using the Mouse For the WM_LBUTTONDOWN message, you want to record the cursor position as the fi rst point defi ning an element. You also want to record the position of the cursor after a mouse move. The obvious place to store these positions is in the CSketcherView class, so you can add data members to the class for these. Right- click the CSketcherView class name in Class View and select Add ➪ Add Variable from the pop -up. You’ll then be able to add details of the variable to be added to the class, as Figure 16 -11 shows.
966
❘
CHAPTER 16
DRAWING IN A WINDOW
FIGURE 16 -11
The drop -down list of types includes only fundamental types, so to enter the type as CPoint, you just highlight the type that is displayed by double- clicking it and then enter the type name that you want. The new data member should be protected to prevent direct modification of it from outside the class. When you click the Finish button, the variable will be created and an initial value will be set arbitrarily as 0 in the initialization list for the constructor. You’ll need to amend the initial value to CPoint(0,0), so the code is as follows: // CSketcherView construction/destruction CSketcherView::CSketcherView(): m_FirstPoint(CPoint(0,0)) { // TODO: add construction code here }
This initializes the member to a CPoint object at position (0,0). You can now add m_SecondPoint as a protected member of type CPoint to the CSketcherView class that stores the next point for an element. You should also amend the initialization list for the constructor to initialize it to CPoint(0,0). You can now implement the handler for the WM_LBUTTONDOWN message as follows: void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default m_FirstPoint = point; // Record the cursor position }
Programming for the Mouse
❘ 967
All the handler does is note the coordinates passed by the second argument. You can ignore the fi rst argument in this situation altogether. You can’t complete the WM_MOUSEMOVE message handler yet, but you can have a stab at outlining the code for it: void CSketcherView::OnMouseMove(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default if(nFlags & MK_LBUTTON) // Verify the left button is down { m_SecondPoint = point; // Save the current cursor position // Test for a previous temporary element { // We get to here if there was a previous mouse move // so add code to delete the old element } // Add code to create new element // and cause it to be drawn } }
It’s important to check that the left mouse button is down because you want to handle the mouse move only when this is the case. Without the check, you would be processing the event when the right button was down or when the mouse was moved with no buttons pressed. After verifying that the left mouse button is down, you save the current cursor position. This is used as the second defi ning point for an element. The rest of the logic is clear in general terms, but you need to establish a few more things before you can complete the function. You have no means of defi ning an element — you’ll want to defi ne an element as an object of a class, so some classes must be defi ned. You also need to devise a way to delete an element and get one drawn when you create a new one. A brief digression is called for.
Getting the Client Area Redrawn Drawing or erasing elements involves redrawing all or part of the client area of a window. As you’ve already discovered, the client area gets drawn by the OnDraw() member function of the CSketcherView class, and this function is called when a WM_PAINT message is received by the Sketcher application. Along with the basic message to repaint the client area, Windows supplies information about the part of the client area that needs to be redrawn. This can save a lot of time when you’re displaying complicated images because only the area specified actually needs to be redrawn, which may be a very small proportion of the total area. As you saw in Chapter 13, you can tell Windows that a particular area should be redrawn by calling the Windows API function InvalidateRect(). There is an inherited member of your view class with the same name that calls the Windows API function. The view member function accepts two arguments, the fi rst of which is a pointer to a RECT or CRect object that defi nes the rectangle in the client area to be redrawn. Passing a null for this parameter causes the whole client area to be
968
❘
CHAPTER 16
DRAWING IN A WINDOW
redrawn. The second parameter is a BOOL value, which is TRUE if the background to the rectangle is to be erased and FALSE otherwise. This argument has a default value of TRUE, because you normally want the background erased before the rectangle is redrawn, so you can ignore it most of the time. A typical situation in which you’d want to cause an area to be redrawn would be where something has changed in a way that makes it necessary for the contents of the area to re- created — the moving of a displayed entity might be an example. In this case, you want to erase the background to remove the old representation of what was displayed before you draw the new version. When you want to draw on top of an existing background, you just pass FALSE as the second argument to InvalidateRect(). Calling the InvalidateRect() function doesn’t directly cause any part of the window to be redrawn; it just communicates to Windows the rectangle that you would like to have it redraw at some time. Windows maintains an update region — actually, a rectangle — that identifies the area in a window that needs to be redrawn. The area specified in your call to InvalidateRect() is added to the current update region, so the new update region encloses the old region plus the new rectangle you have indicated as invalid. Eventually, a WM_PAINT message is sent to the window, and the update region is passed to the window along with the message. When processing of the WM_PAINT message is complete, the update region is reset to the empty state. Thus, all you have to do to get a newly created shape drawn is:
1.
Make sure that the OnDraw() function in your view includes the newly created item when it redraws the window.
2.
Call InvalidateRect() with a pointer to the rectangle bounding the shape to be redrawn passed as the fi rst argument.
3.
Cause the window to be redrawn by calling the inherited UpdateWindow() function for the view.
Similarly, if you want a shape removed from the client area of a window, you need to do the following:
1. 2.
Remove the shape from the items that the OnDraw() function will draw.
3.
Cause the window to be redrawn by calling the inherited UpdateWindow() function for the view.
Call InvalidateRect() with the fi rst argument pointing to the rectangle bounding the shape that is to be removed.
Because the background to the update region is automatically erased, as long as the OnDraw() function doesn’t draw the shape again, the shape disappears. Of course, this means that you need to be able to obtain the rectangle bounding any shape that you create, so you’ll include a function to provide this rectangle as a member of the classes that define the elements that can be drawn by Sketcher.
Programming for the Mouse
❘ 969
Defining Classes for Elements Thinking ahead a bit, you will need to store elements in a document in some way. You must also be able to store the document in a file for subsequent retrieval if a sketch is to have any permanence. I’ll deal with the details of file operations later on, but for now, it’s enough to know that the MFC class CObject includes the tools for us to do this, so you’ll use CObject as a base class for the element classes. You also have the problem that you don’t know in advance what sequence of element types the user will create. The Sketcher program must be able to handle any sequence of elements. This suggests that using a base class pointer for selecting a particular element class function might simplify things a bit. For example, you don’t need to know what an element is to draw it. As long as you are accessing the element through a base class pointer, you can always get an element to draw itself by using a virtual function. This is another example of the polymorphism I talked about when I discussed virtual functions. All you need to do to achieve this is to make sure that the classes defi ning specific elements share a common base class and that, in this class, you declare all the functions you want to be selected automatically at run time as virtual. The element classes could be organized as shown in Figure 16 -12.
CObject
CElement
CLine
CRectangle
CCircle
CCurve
FIGURE 16 -12
The arrows in the diagram point toward the base class in each case. If you need to add another element type, all you need to do is derive another class from CElement. Because these classes are closely related, you’ll be putting the defi nitions for all these classes in a single new header fi le that you can call Elements.h. You can create the new CElement class by right- clicking Sketcher in Class View and selecting Add ➪ Class from the pop -up. Select MFC from the list of installed templates, and then select MFC Class in the center pane. When you click the Add button in the dialog, another dialog displays in which you can specify the class name and select the base class, as shown in Figure 16 -13.
970
❘
CHAPTER 16
DRAWING IN A WINDOW
FIGURE 16 -13
I have already fi lled in the class name as CElement and selected CObject from the drop -down list as the base class. I have also adjusted the names of the source fi les to be Elements.h and Elements .cpp, because eventually, these fi les will contain defi nitions for the other element classes you need. When you click the Finish button, the code for the class defi nition is generated: #pragma once
// CElement command target class CElement : public CObject { public: CElement(); virtual ~CElement(); };
The only members that have been declared for you are a constructor and a virtual destructor, and skeleton defi nitions for these appear in the Elements.cpp fi le. You can see that the Class Wizard has included a #pragma once directive to ensure that the contents of the header fi le cannot be included into another fi le more than once. You can add the other element classes using essentially the same process. Because the other element classes have CElement rather than an MFC class as the base class, you should set the class category as C++ and the template as “C++ class.” For the CLine class, the Class Wizard window should look as shown in Figure 16 -14.
Programming for the Mouse
❘ 971
FIGURE 16 -14
The Class Wizard supplies the names of the header fi le and .cpp fi le as Line.h and Line.cpp by default, but you can change these to use different names for the fi les or use existing fi les. To have the CLine class code inserted in the fi les for the CElement class, just click the button alongside the fi le name and select the appropriate file to be used. I have already done this in Figure 16 -14. You’ll see a dialog displayed when you click the Finish button that asks you to confi rm that you want to merge the new class into the existing header fi le. Just click Yes to confi rm this, and do the same in the dialog that appears relating to the Elements.cpp fi le. When you have created the CLine class defi nition, do the same for CRectangle, CCircle, and CCurve. When you are done, you should see the defi nitions of all four subclasses of CElement in the Elements.h fi le, each with a constructor and a virtual destructor declared.
Storing a Temporary Element in the View When I discussed how shapes would be drawn, it was evident that as the mouse was dragged after the left mouse button was pressed, a series of temporary element objects were created and drawn. Now that you know that the base class for all the shapes is CElement, you can add a pointer to the view class that you’ll use to store the address of the temporary element. Right- click the CSketcherView class once more and again select the Add ➪ Add Variable option. The m_pTempElement variable should be of type CElement*, and it should be protected like the previous two data members that you added earlier. The Add Member Variable Wizard ensures that the new variable is initialized when the view object is constructed. The default value of NULL that is set will do nicely, but you could change it to nullptr if you wish. You’ll be able to use the m_pTempElement pointer in the WM_MOUSEMOVE message handler as a test for previous temporary elements because you’ll arrange for it to be null when there are none.
972
❘
CHAPTER 16
DRAWING IN A WINDOW
Because you are creating CElement class objects in the view class member functions, and you refer to the CElement class in defi ning the data member that points to a temporary element, you should add a #include directive for Elements.h to SketcherView.h, immediately following the #pragma once directive.
The CElement Class You can now start to fi ll out the element class defi nitions. You’ll be doing this incrementally as you add more and more functionality to the Sketcher application — but what do you need right now? Some data items, such as color, are clearly common to all types of element, so you can put those in the CElement class so that they are inherited in each of the derived classes; however, the other data members in the classes that defi ne specific element properties will be quite disparate, so you’ll declare these members in the particular derived class to which they belong. Thus, the CElement class will contain only virtual functions that are replaced in the derived classes, plus data and function members that are the same in all the derived classes. The virtual functions will be those that are selected automatically for a particular object through a pointer. You could use the Add Member Wizard you’ve used previously to add these members to the CElement class, but modify the class manually, for a change. For now, you can modify the CElement class defi nition to the following: class CElement: public CObject { protected: int m_PenWidth; COLORREF m_Color; public: virtual ~CElement(); virtual void Draw(CDC* pDC) {} CRect GetBoundRect() const; protected: CElement();
// Pen width // Color of an element
// Virtual draw operation // Get the bounding rectangle for an element
// Here to prevent it being called
};
I have changed the access for the constructor from public to protected to prevent it from being called from outside the class. At the moment, the members to be inherited by the derived classes are a data member storing the color, m_Color, a member storing the pen width, and a member function that calculates the rectangle bounding an element, GetBoundRect(). This function returns a value of type CRect that is the rectangle bounding the shape. You also have a virtual Draw() function that is implemented in the derived classes to draw the particular object in question. The Draw() function needs a pointer to a CDC object passed to it to provide access to the drawing functions that you saw earlier that allow drawing in a device context. You might be tempted to declare the Draw() member as a pure virtual function in the CElement class — after all, it can have no meaningful content in this class. This would also force its definition in any derived class. Normally, you would do this, but the CElement class inherits a facility from CObject called serialization that you’ll use later for storing objects in a fi le, and this requires that it
Programming for the Mouse
❘ 973
be possible for an instance of the CElement class to be created. A class with a pure virtual function member is an abstract class, and instances of an abstract class can’t be created. If you want to use MFC ’s serialization capability for storing objects, your classes mustn’t be abstract. You must also supply a no-arg constructor for a class to be serializable.
NOTE Note that serialization is a general term for writing objects to a file. Serialization in a C++/CLI application will work differently from serialization in the MFC.
You might also be tempted to declare the GetBoundRect() function as returning a pointer to a CRect object — after all, you’re going to pass a pointer to the InvalidateRect() member function in the view class; however, this could lead to problems. You’ll be creating the CRect object as local to the function, so the pointer would be pointing to a nonexistent object on return from the GetBoundRect() function. You could get around this by creating the CRect object on the heap, but then, you’d need to take care that it’s deleted after use; otherwise, you’d be fi lling the heap with CRect objects — a new one for every call of GetBoundRect().
The CLine Class You can amend the defi nition of the CLine class to the following: class CLine: public CElement { public: ~CLine(void); virtual void Draw(CDC* pDC);
// Function to display a line
// Constructor for a line object CLine(const CPoint& start, const CPoint& end, COLORREF aColor); protected: CPoint m_StartPoint; CPoint m_EndPoint; CLine(void);
// Start point of line // End point of line // Default constructor should not be used
};
The data members that defi ne a line are m_StartPoint and m_EndPoint, and these are both declared to be protected. The class has a public constructor that has parameters for the values that defi ne a line, and the no -arg default constructor has been moved to the protected section of the class to prevent its use externally. The fi rst two constructor parameters are const references to avoid copying the CPoint objects when a CLine object is being created.
Implementing the CLine Class You add the implementation of the member functions to the Elements.cpp fi le that was created by the Class Wizard when you created the CElement class. The stdafx.h fi le was included in this fi le to make the defi nitions of the standard system header fi les available. You’re now ready to add the constructor for the CLine class to the Elements.cpp fi le.
974
❘
CHAPTER 16
DRAWING IN A WINDOW
The CLine Class Constructor The code for the constructor is as follows: // CLine class constructor CLine::CLine(const CPoint& start, const CPoint& end, COLORREF aColor): m_StartPoint(start), m_EndPoint(end) { m_PenWidth = 1; // Set pen width m_Color = aColor; // Set line color }
You fi rst initialize the m_StartPoint and m_EndPoint members with the object references that are passed as the fi rst two arguments. The pen width is set to 1 in the inherited m_PenWidth member, and the color is stored in the m_Color member that is inherited from the CElement class.
Drawing a Line The Draw() function for the CLine class isn’t too difficult, either, although you do need to take into account the color that is to be used when the line is drawn: // Draw a CLine object void CLine::Draw(CDC* pDC) { // Create a pen for this object and // initialize it to the object color and line width m_PenWidth CPen aPen; if(!aPen.CreatePen(PS_SOLID, m_PenWidth, m_Color)) { // Pen creation failed. Abort the program AfxMessageBox(_T("Pen creation failed drawing a line"), MB_OK); AfxAbort(); } CPen* pOldPen = pDC->SelectObject(&aPen);
// Select the pen
// Now draw the line pDC->MoveTo(m_StartPoint); pDC->LineTo(m_EndPoint); pDC->SelectObject(pOldPen);
// Restore the old pen
}
You create a new pen of the appropriate color and with a line thickness specified by m_Penwidth, only this time, you make sure that the creation works. In the unlikely event that it doesn’t, the most likely cause is that you’re running out of memory, which is a serious problem. This is almost invariably caused by an error in the program, so you have written the function to call AfxMessageBox(), which is an MFC global function to display a message box, and then call AfxAbort() to terminate the program. The fi rst argument to AfxMessageBox() specifies the message that is to appear, and the second specifies that it should have an OK button. You can get more information on either of these functions by placing the cursor within the function name in the Editor window and then pressing F1.
Programming for the Mouse
❘ 975
After selecting the pen into the device context, you move the current position to the start of the line, defi ned in the m_StartPoint data member, and then draw the line from this point to the end point. Finally, you restore the old pen in the device context, and you are done.
Creating Bounding Rectangles At fi rst sight, obtaining the bounding rectangle for a shape looks trivial. For example, a line is always a diagonal of its enclosing rectangle, and a circle is defi ned by its enclosing rectangle, but there are a couple of slight complications. The shape must lie completely inside the rectangle; otherwise, part of the shape may not be drawn. Therefore, you must allow for the thickness of the line used to draw the shape when you create the bounding rectangle. Also, how you work out adjustments to the coordinates that defi ne the bounding rectangle depends on the mapping mode, so you must take that into account too. Look at Figure 16 -15, which relates to the method for obtaining the bounding rectangle for a line and a circle.
Negative X-Axis Negative
Negative Negative
Positive
Bounding rectangles
Positive
Positive
Enclosing rectangle for the line
Enclosing rectangle for the circle
MM_TEXT
Positive Y-Axis
Positive X-Axis Positive
Positive Negative
Positive
Bounding rectangles
Negative
Negative
Enclosing rectangle for the line Negative Y-Axis
Enclosing rectangle for the circle
MM_LOENGLISH
FIGURE 16 -15
I have called the rectangle that is used to draw a shape the enclosing rectangle, while the rectangle that takes into account the width of the pen I’ve called the bounding rectangle to differentiate it. Figure 16 -15 shows the shapes with their enclosing rectangles, and with their bounding rectangles offset by the line thickness. This is obviously exaggerated here so you can see what’s happening. The differences in how you calculate the coordinates for the bounding rectangle in different mapping modes only concern the y coordinate; calculation of the x coordinate is the same for
976
❘
CHAPTER 16
DRAWING IN A WINDOW
all mapping modes. To get the corners of the bounding rectangle in the MM_TEXT mapping mode, subtract the line thickness from the y coordinate of the upper-left corner of the defi ning rectangle, and add it to the y coordinate of the lower-right corner. However, in MM_LOENGLISH (and all the other mapping modes), the y-axis increases in the opposite direction, so you need to add the line thickness to the y coordinate of the upper-left corner of the defi ning rectangle, and subtract it from the y coordinate of the lower-right corner. For all the mapping modes, you subtract the line thickness from the x coordinate of the upper-left corner of the defi ning rectangle, and add it to the x coordinate of the lower-right corner. To implement the element types as consistently as possible in Sketcher, you could store an enclosing rectangle for each shape in a member in the base class. The enclosing rectangle needs to be calculated when a shape is constructed. The job of the GetBoundRect() function in the base class will then be to calculate the bounding rectangle by offsetting the enclosing rectangle by the pen width. You can amend the CElement class defi nition by adding a data member to store the enclosing rectangle, as follows: class CElement: public CObject { protected: int m_PenWidth; COLORREF m_Color; CRect m_EnclosingRect; public: virtual ~CElement(); virtual void Draw(CDC* pDC) {} CRect GetBoundRect(); protected: CElement();
// Pen width // Color of an element // Rectangle enclosing an element
// Virtual destructor // Virtual draw operation // Get the bounding rectangle for an element
// Here to prevent it being called
};
You can add this data member by right- clicking the class name and selecting from the pop -up, or you can add the statements directly in the Editor window along with the comments. You can now implement the GetBoundRect() member of the base class, assuming the MM_TEXT mapping mode: // Get the bounding rectangle for an element CRect CElement::GetBoundRect() const { CRect boundingRect(m_EnclosingRect);
// Object to store bounding rectangle
// Increase the rectangle by the pen width and return it boundingRect.InflateRect(m_PenWidth, m_PenWidth); return boundingRect; }
This returns the bounding rectangle for any derived class object. You define the bounding rectangle by using the InflateRect() method of the CRect class to modify the coordinates of the enclosing rectangle stored in the m_EnclosingRect member so that the rectangle is enlarged all around by the pen width.
Programming for the Mouse
❘ 977
NOTE As a reminder, the individual data members of a CRect object are left and top (storing the x and y coordinates, respectively, of the upper-left corner) and right and bottom (storing the coordinates of the lower-right corner). These are all public members, so you can access them directly. A mistake often made, especially by me, is to write the coordinate pair as (top, left) instead of in the correct order: (left, top).
Normalized Rectangles The InflateRect() function works by subtracting the values that you give it from the top and left members of the rectangle and adding those values to the bottom and right. This means that you may find your rectangle actually decreasing in size if you don’t make sure that the rectangle is normalized. A normalized rectangle has a left value that is less than or equal to the right value, and a top value that is less than or equal to the bottom value. As long as your rectangles are normalized, InflateRect() will work correctly whatever the mapping mode. You can make sure that a CRect object is normalized by calling the NormalizeRect() member of the object. Most of the CRect member functions require the object to be normalized for them to work as expected, so you need to make sure that when you store the enclosing rectangle in m_EnclosingRect, it is normalized.
Calculating the Enclosing Rectangle for a Line All you need now is code in the constructor for a line to calculate the enclosing rectangle: CLine::CLine(const CPoint& start, const CPoint& end, COLORREF aColor) m_StartPoint(start), m_EndPoint(end) { m_Color = aColor; // Set line color m_PenWidth = 1; // Set pen width // Define the enclosing rectangle m_EnclosingRect = CRect(start, end); m_EnclosingRect.NormalizeRect(); }
The arguments to the CRect constructor you are using here are the start and end points of the line. To ensure that the bounding rectangle has a top value that is less than the bottom value, regardless of the relative positions of the start and end points of the line, you call the NormalizeRect() member of the m_EnclosingRect object.
The CRectangle Class Although you’ll be defining a rectangle object with the same data that you use to define a line — a start point and an end point on a diagonal of the rectangle — you don’t need to store the defining points. The enclosing rectangle in the data member that is inherited from the base class completely defines the shape, so you don’t need any additional data members. You can therefore define the class like this: // Class defining a rectangle object class CRectangle: public CElement {
978
❘
CHAPTER 16
DRAWING IN A WINDOW
public: ~CRectangle(void); virtual void Draw(CDC* pDC);
// Function to display a rectangle
// Constructor for a rectangle object CRectangle(const CPoint& start, const CPoint& end, COLORREF aColor); protected: CRectangle(void);
// Default constructor - should not be used
};
The no -arg constructor is now protected to prevent its being used externally. The defi nition of the rectangle becomes very simple — just a constructor, the virtual Draw() function, plus the no -arg constructor in the protected section of the class.
The CRectangle Class Constructor The code for the new CRectangle class constructor is somewhat similar to that for a CLine constructor: // CRectangle class constructor CRectangle:: CRectangle(const CPoint& start, const CPoint& end, COLORREF aColor) { m_Color = aColor; // Set rectangle color m_PenWidth = 1; // Set pen width // Define the enclosing rectangle m_EnclosingRect = CRect(start, end); m_EnclosingRect.NormalizeRect(); }
If you modified the CRectangle class defi nition manually, there is no skeleton defi nition for the constructor, so you just need to add the defi nition directly to Elements.cpp. The constructor just stores the color and pen width and computes the enclosing rectangle from the points passed as arguments.
Drawing a Rectangle There is a member of the CDC class called Rectangle() that draws a rectangle. This function draws a closed figure and fi lls it with the current brush. You may think that this isn’t quite what you want because you want to draw rectangles as outlines only, but by selecting a NULL_BRUSH that is exactly what you will draw. Just so you know, there’s also a function PolyLine(), which draws shapes consisting of multiple line segments from an array of points, or you could have used LineTo() again, but the easiest approach is to use the Rectangle() function: // Draw a CRectangle object void CRectangle::Draw(CDC* pDC) { // Create a pen for this object and // initialize it to the object color and line width of m_PenWidth CPen aPen; if(!aPen.CreatePen(PS_SOLID, m_PenWidth, m_Color))
Programming for the Mouse
❘ 979
{ // Pen creation failed AfxMessageBox(_T("Pen creation failed drawing a rectangle"), MB_OK); AfxAbort(); } // Select the pen CPen* pOldPen = pDC->SelectObject(&aPen); // Select the brush CBrush* pOldBrush = (CBrush*)pDC->SelectStockObject(NULL_BRUSH); // Now draw the rectangle pDC->Rectangle(m_EnclosingRect); pDC->SelectObject(pOldBrush); pDC->SelectObject(pOldPen);
// Restore the old brush // Restore the old pen
}
After setting up the pen and the brush, you simply pass the whole rectangle directly to the Rectangle() function to get it drawn. All that remains to do is to clear up afterward and restore the device context’s old pen and brush.
The CCircle Class The interface of the CCircle class is no different from that of the CRectangle class. You can defi ne a circle solely by its enclosing rectangle, so the class defi nition is as follows: // Class defining a circle object class CCircle: public CElement { public: ~CCircle(void); virtual void Draw(CDC* pDC);
// Function to display a circle
// Constructor for a circle object CCircle(const CPoint& start, const CPoint& end, COLORREF aColor); protected: CCircle(void);
// Default constructor - should not be used
};
You have defi ned a public constructor that creates a circle from two points, and the no-arg constructor is protected to prevent its being used externally. You have also added a declaration for the draw function to the class defi nition.
Implementing the CCircle Class As discussed earlier, when you create a circle, the point at which you press the left mouse button is the center, and after you move the cursor with the left button down, the point at which you release the button is a point on the circumference of the fi nal circle. The job of the constructor is to convert these points into the form used in the class to defi ne a circle.
980
❘
CHAPTER 16
DRAWING IN A WINDOW
The CCircle Class Constructor The point at which you release the left mouse button can be anywhere on the circumference, so the coordinates of the points specifying the enclosing rectangle need to be calculated, as illustrated in Figure 16-16.
This distance is 2r
Left mouse button down here
x1,y1
2=(x2-x1)2+(y2-y1)2
r
This distance is 2r
x2,y2
Left mouse button up here
FIGURE 16 -16
From Figure 16 -16, you can see that you can calculate the coordinates of the upper-left and lowerright points of the enclosing rectangle relative to the center of the circle (x1, y1), which is the point you record when the left mouse button is pressed. Assuming that the mapping mode is MM_TEXT, for the upper-left point, you just subtract the radius from each of the coordinates of the center. Similarly, you obtain the lower-right point by adding the radius to the x and y coordinates of the center. You can, therefore, code the constructor as follows: // Constructor for a circle object CCircle::CCircle(const CPoint& start, const CPoint& end, COLORREF aColor) { // First calculate the radius // We use floating point because that is required by // the library function (in cmath) for calculating a square root. long radius = static_cast (sqrt( static_cast<double>((end.x-start.x)*(end.x-start.x)+ (end.y-start.y)*(end.y-start.y)))); // Now calculate the rectangle enclosing // the circle assuming the MM_TEXT mapping mode m_EnclosingRect = CRect(start.x-radius, start.y-radius, start.x+radius, start.y+radius); m_EnclosingRect.NormalizeRect(); // Normalize-in case it's not MM_TEXT m_Color = aColor; m_PenWidth = 1;
// Set the color for the circle // Set pen width to 1
}
To use the sqrt() function, you must add the line to the beginning of the Elements.cpp fi le: #include
Programming for the Mouse
❘ 981
You can place this after the #include directive for stdafx.h. The maximum coordinate values are 32 bits, and the CPoint members x and y are declared as long, so evaluating the argument to the sqrt() function can safely be carried out as an integer. The result of the square root calculation is of type double, so you cast it to long because you want to use it as an integer.
Drawing a Circle The implementation of the Draw() function in the CCircle class is as follows: // Draw a circle void CCircle::Draw(CDC* pDC) { // Create a pen for this object and // initialize it to the object color and line width of m_PenWidth CPen aPen; if(!aPen.CreatePen(PS_SOLID, m_PenWidth, m_Color)) { // Pen creation failed AfxMessageBox(_T("Pen creation failed drawing a circle"), MB_OK); AfxAbort(); } CPen* pOldPen = pDC->SelectObject(&aPen);
// Select the pen
// Select a null brush CBrush* pOldBrush = (CBrush*)pDC->SelectStockObject(NULL_BRUSH); // Now draw the circle pDC->Ellipse(m_EnclosingRect); pDC->SelectObject(pOldPen); pDC->SelectObject(pOldBrush);
// Restore the old pen // Restore the old brush
}
After selecting a pen of the appropriate color and a null brush, you draw the circle by calling the Ellipse() function. The only argument is the CRect object that encloses the circle you want.
The CCurve Class The CCurve class is different from the others in that it needs to be able to deal with a variable number of defi ning points. A curve is defi ned by two or more points, which can be stored in either a list or a vector container. It won’t be necessary to remove points from a curve, so a vector STL container is a good candidate for storing the points. You already looked at the vector template in some detail back in Chapter 10, so this should be easy. However, before we can complete the defi nition of the CCurve class, we need to explore in more detail how a curve will be created and drawn by a user.
Drawing a Curve Drawing a curve is different from drawing a line or a circle. With a line or a circle, as you move the cursor with the left button down, you are creating a succession of different line or circle elements
982
❘
CHAPTER 16
DRAWING IN A WINDOW
that share a common reference point — the point at which the left mouse button was pressed. This is not the case when you draw a curve, as shown in Figure 16 -17.
X-Axis
Minimum y
Y-Axis
The first two points define a basic curve
Maximum y Each additional point defines another segment Minimum x
Maximum x
Drawing a curve with MM_TEXT mapping mode FIGURE 16 -17
When you move the cursor while drawing a curve, you’re not creating a sequence of new curves, but rather extending the same curve, so each successive point adds another segment to the curve’s defi nition. Therefore, you need to create a curve object as soon as you have the two points from the WM_LBUTTONDOWN message and the fi rst WM_MOUSEMOVE message. Points defi ned with subsequent mouse move messages then defi ne additional segments to the existing curve object. You’ll need to add a function, AddSegment(), to the CCurve class to extend the curve once it has been created by the constructor. A further point to consider is how you are to calculate the enclosing rectangle. You can defi ne this by getting the minimum-x-and-minimum-y pair from all the defi ning points to establish the upperleft corner of the rectangle, and the maximum-x-and-maximum-y pair for the bottom right. The easiest way of generating the enclosing rectangle is to compute it in the constructor based on the fi rst two points and then re- compute it incrementally in the AddSegment() function as points are added to the curve. In Elements.h, you can modify the CCurve class defi nition to this: // Class defining a curve object class CCurve: public CElement { public: ~CCurve(void);
Programming for the Mouse
virtual void Draw(CDC* pDC);
❘ 983
// Function to display a curve
// Constructor for a curve object CCurve(const CPoint& first, const CPoint& second, COLORREF aColor); void AddSegment(const CPoint& point); // Add a segment to the curve protected: std::vector m_Points; CCurve(void); };
// Points defining the curve // Default constructor - should not be used
Don’t forget to add a #include directive for the vector header to Elements.h. You can add the following defi nition for the constructor in Elements.cpp: // Constructor for a curve object CCurve::CCurve(const CPoint& first, const CPoint& second, COLORREF aColor) { // Store the points m_Points.push_back(first); m_Points. push_back(second); m_Color = aColor; m_EnclosingRect = CRect(min(first.x, second.x), min(first.y, second.y), max(first.x, second.x), max(first.y, second.y)); m_PenWidth = 1; }
The constructor has the fi rst two defi ning points and the color as parameters, so it defi nes a curve with only one segment. You use the min() and max() functions defi ned in the cmath header in the creation of the enclosing rectangle. I’m sure you recall that the push_back() function adds an element to the end of a vector. The Draw() function can be defi ned as follows: // Draw a curve void CCurve::Draw(CDC* pDC) { // Create a pen for this object and // initialize it to the object color and line width of m_PenWidth CPen aPen; if(!aPen.CreatePen(PS_SOLID, m_PenWidth, m_Color)) { // Pen creation failed AfxMessageBox(_T("Pen creation failed drawing a curve"), MB_OK); AfxAbort(); } CPen* pOldPen = pDC->SelectObject(&aPen);
// Select the pen
// Now draw the curve pDC->MoveTo(m_Points[0]); for(size_t i = 1 ; i<m_Points.size() ; ++i) pDC->LineTo(m_Points[i]); pDC->SelectObject(pOldPen); }
// Restore the old pen
984
❘
CHAPTER 16
DRAWING IN A WINDOW
The Draw() function has to provide for a curve with an arbitrary number of points. Once the pen has been set up, the fi rst step is to move the current position in the device context to the fi rst point in the vector, m_Points. Line segments for the curve are drawn in the for loop. A LineTo() operation moves the current position to the end of the line it draws, and each call of the function here draws a line from the current position to the next point in the vector. In this way, you draw all the segments that make up the curve in sequence. You can implement the AddSegment() function like this: // Add a segment to the curve void CCurve::AddSegment(const CPoint& point) { m_Points. push_back(point);
// Add the point to the end
// Modify the enclosing rectangle for the new point m_EnclosingRect = CRect(min(point.x, m_EnclosingRect.left), min(point.y, m_EnclosingRect.top), max(point.x, m_EnclosingRect.right), max(point.y, m_EnclosingRect.bottom)); }
You add the point that is passed to the function to the end of the vector and adjust the enclosing rectangle by ensuring the top-left point is the minimum x and y, and the bottom-right is the maximum x and y.
Completing the Mouse Message Handlers You can now come back to the WM_MOUSEMOVE message handler and fi ll out the details. You can get to it through selecting CSketcherView in the Class View and double- clicking the handler name, OnMouseMove(). This handler is concerned only with drawing a succession of temporary versions of an element as you move the cursor because the fi nal element is created when you release the left mouse button. You can therefore treat the drawing of temporary elements to provide rubber-banding as being entirely local to this function, leaving the fi nal version of the element that is created to be drawn by the OnDraw() function member of the view. This approach results in the drawing of the rubberbanded elements being reasonably efficient because it won’t involve the OnDraw() function that ultimately is responsible for drawing the entire document. You can execute this approach very easily with the help of a member of the CDC class that is particularly effective in rubber-banding operations: SetROP2().
Setting the Drawing Mode The SetROP2() function sets the drawing mode for all subsequent output operations in the device context associated with a CDC object. The “ROP” bit of the function name stands for Raster OPeration, because the setting of drawing modes applies to raster displays. In case you’re wondering what SetROP1() is — it doesn’t exist. The function name stands for Set Raster OPeration to, not the number two! The drawing mode determines how the color of the pen that you use for drawing is to combine with the background color to produce the color of the entity you are displaying. You specify the drawing mode with a single argument to the function that can be any of the following values:
Programming for the Mouse
❘ 985
DRAWING MODE
EFFECT
R2_BLACK
All drawing is in black.
R2_WHITE
All drawing is in white.
R2_NOP
Drawing operations do nothing.
R2_NOT
Drawing is in the inverse of the screen color. This ensures that the output is always visible because it prevents your drawing in the same color as the background.
R2_COPYPEN
Drawing is in the pen color. This is the default drawing mode if you don’t set a mode.
R2_NOTCOPYPEN
Drawing is in the inverse of the pen color.
R2_MERGEPENNOT
Drawing is in the color produced by OR- ing the pen color with the inverse of the background color.
R2_MASKPENNOT
Drawing is in the color produced by AND - ing the pen color with the inverse of the background color.
R2_MERGENOTPEN
Drawing is in the color produced by OR- ing the background color with the inverse of the pen color.
R2_MASKNOTPEN
Drawing is in the color produced by AND - ing the background color with the inverse of the pen color.
R2_MERGEPEN
Drawing is in the color produced by OR-ing the background color with the pen color.
R2_NOTMERGEPEN
Drawing is in the color that is the inverse of the R2_MERGEPEN color.
R2_MASKPEN
Drawing is in the color produced by AND-ing the background color with the pen color.
R2_NOTMASKPEN
Drawing is in the color that is the inverse of the R2_MASKPEN color.
R2_XORPEN
Drawing is in the color produced by exclusive OR-ing the pen color and the background color.
R2_NOTXORPEN
Drawing is in the color that is the inverse of the R2_XORPEN color.
Each of these symbols is predefi ned and corresponds to a particular drawing mode. There are a lot of options here, but the one that can work some magic for us is the last of them, R2_NOTXORPEN. When you set the mode as R2_NOTXORPEN, the fi rst time you draw a particular shape on the default white background, it is drawn normally in the pen color you specify. If you draw the same shape again, overwriting the fi rst, the shape disappears because the color that the shape is drawn in corresponds to that produced by exclusive OR-ing the pen color with itself. The drawing color that results from this is white. You can see this more clearly by working through an example.
986
❘
CHAPTER 16
DRAWING IN A WINDOW
White is formed from equal proportions of the “maximum” amounts of red, blue, and green. For simplicity, I’ll represent this as 1,1,1 — the three values represent the RGB components of the color. In the same scheme, red is defi ned as 1,0,0. These combine as follows: R
G
B
Background — white
1
1
1
Pen — red
1
0
0
XOR-ed
0
1
1
NOT XOR (produces red)
1
0
0
So, the fi rst time you draw a red line on a white background, it comes out red, as the last line above indicates. If you now draw the same line a second time, overwriting the existing line, the background pixels you are writing over are red. The resultant drawing color works out as follows: R
G
B
Background (red)
1
0
0
Pen (red)
1
0
0
XOR-ed
0
0
0
NOT XOR (produces white)
1
1
1
As the last line indicates, the line comes out as white, and because the rest of the background is white, the line disappears. You need to take care to use the right background color here. You should be able to see that drawing with a white pen on a red background is not going to work too well, as the fi rst time you draw something, it is red and, therefore, invisible. The second time, it appears as white. If you draw on a black background, things appear and disappear as on a white background, but they are not drawn in the pen color you choose.
Coding the OnMouseMove() Handler You can start by adding the code that creates the element after a mouse move message. Because you are going to draw the element from within the handler function, you need to create a device context that you can draw in. The most convenient class to use for this is CClientDC, which is derived from CDC. As I said earlier, the advantage of using this class rather than CDC is that it automatically takes care of creating the device context for you and destroying it when you are done. The device context that it creates corresponds to the client area of a window, which is exactly what you want. The logic for drawing an element is somewhat complicated. You can draw a shape using the R2_NOTXORPEN mode as long as the shape is not a curve. When a curve is being created, you
Programming for the Mouse
❘ 987
don’t want to draw a new curve each time the mouse moves. You just want to extend the curve by a new segment. This means you must treat the case of drawing a curve as an exception in the OnMouseMove() handler. You also want to delete an old element when it exists, but only when it is not a curve. Here’s how that functionality can be implemented: void CSketcherView::OnMouseMove(UINT nFlags, CPoint point) { // Define a Device Context object for the view CClientDC aDC(this); // DC is for this view if(nFlags & MK_LBUTTON) { m_SecondPoint = point; // Save the current cursor position if(m_pTempElement) { if(CURVE == GetDocument()->GetElementType()) // Is it a curve? { // We are drawing a curve so add a segment to the existing curve static_cast(m_pTempElement)->AddSegment(m_SecondPoint); m_pTempElement->Draw(&aDC); // Now draw it return; // We are done } // If we get to here it’s not a curve so // redraw the old element so it disappears from the view aDC.SetROP2(R2_NOTXORPEN); // Set the drawing mode m_pTempElement->Draw(&aDC); // Redraw the old element delete m_pTempElement; // Delete the old element m_pTempElement = nullptr; // Reset the pointer } // Create a temporary element of the type and color that // is recorded in the document object, and draw it m_pTempElement = CreateElement(); // Create a new element m_pTempElement->Draw(&aDC); // Draw the element } }
The fi rst new line of code creates a local CClientDC object. The this pointer that you pass to the constructor identifies the current view object, so the CClientDC object has a device context that corresponds to the client area of the current view. As well as the characteristics I mentioned, this object has all the drawing functions you need because they are inherited from the CDC class. A previous temporary element exists if the pointer m_pTempElement is not nullptr. If it’s not a curve, you need to redraw the element to which it points to remove it from the client area of the view, so you fi rst check whether the user is drawing a curve. You call the function GetElementType() for the document object to get the current element type (you’ll add this function to the CSketcherDoc class in a moment). If the current element type is CURVE, you cast m_pTempElement to type CCurve so you can call the AddSegment() function for the object to add the next segment to the curve. You then draw the curve and return. When the current element is not a curve, you redraw the old element after calling the SetROP2() function of the aDC object to set the mode to R2_NOTXORPEN. This erases the old temporary element.
988
❘
CHAPTER 16
DRAWING IN A WINDOW
You then delete the element object and reset m_pTempElement to null. The new element is then created and drawn. This combination automatically rubber-bands the shape being created, so it appears to be attached to the cursor position as the cursor moves. You must not forget to reset the pointer m_pTempElement back to nullptr in the WM_LBUTTONUP message handler after you create the fi nal version of the element. If there is no temporary element, this is the fi rst time through the OnMouseMove() handler and you need to create a temporary element. To create a new element, you call a view member function, CreateElement(). (You’ll defi ne the CreateElement() function as soon as you have fi nished this handler.) This function will create an element using the two points stored in the current view object, with the color and type specification stored in the document object, and return the address of the element. You store the pointer to the new element that is returned in m_pTempElement. Using the pointer to the new element, you call its Draw() member to get the object to draw itself. The address of the CClientDC object is passed as the argument. Because you defi ned the Draw() function as virtual in the CElement base class, the function for whatever type of element m_pTempElement is pointing to is automatically selected. The new element is drawn normally with the R2_NOTXORPEN mode, because you are drawing it for the fi rst time on a white background.
Creating an Element You should add the CreateElement() function as a protected member to the Operations section of the CSketcherView class: class CSketcherView: public CView { // Rest of the class definition as before... // Operations public: protected: CElement* CreateElement(void) const;
// Create a new element on the heap
// Rest of the class definition as before... };
To do this, you can either amend the class defi nition directly by adding the bolded line, or you can right- click on the class name, CSketcherView, in Class View, select Add ➪ Add Function from the context menu, and add the function through the dialog that is displayed. The function is protected because it is called only from inside the view class. If you added the declaration to the class defi nition manually, you’ll need to add the complete defi nition for the function to the .cpp fi le. This is as follows: // Create an element of the current type CElement* CSketcherView::CreateElement(void) const { // Get a pointer to the document for this view CSketcherDoc* pDoc = GetDocument();
Programming for the Mouse
ASSERT_VALID(pDoc);
❘ 989
// Verify the pointer is good
// Now select the element using the type stored in the document switch(pDoc->GetElementType()) { case RECTANGLE: return new CRectangle(m_FirstPoint, m_SecondPoint, pDoc->GetElementColor()); case CIRCLE: return new CCircle(m_FirstPoint, m_SecondPoint, pDoc->GetElementColor()); case CURVE: return new CCurve(m_FirstPoint, m_SecondPoint, pDoc->GetElementColor()); case LINE: return new CLine(m_FirstPoint, m_SecondPoint, pDoc->GetElementColor()); default: // Something's gone wrong AfxMessageBox(_T("Bad Element code"), MB_OK); AfxAbort(); return nullptr; } }
The fi rst thing you do here is get a pointer to the document by calling GetDocument(), as you’ve seen before. For safety, you use the ASSERT_VALID() macro to ensure that a good pointer is returned. In the debug version of MFC that is used in the debug version of your application, this macro calls the AssertValid() member of the object, which is specified as the argument to the macro. This checks the validity of the current object, and if the pointer is NULL or the object is defective in some way, an error message is displayed. In the release version of MFC, the ASSERT_VALID() macro does nothing. The switch statement selects the element to be created based on the type returned by a function in the document class, GetElementType(). Another function in the document class is used to obtain the current element color. You can add the defi nitions for both these functions directly to the CSketcherDoc class defi nition, because they are very simple: class CSketcherDoc: public CDocument { // Rest of the class definition as before... // Operations public: unsigned int GetElementType() const { return m_Element; } COLORREF GetElementColor() const { return m_Color; }
// Get the element type // Get the element color
// Rest of the class definition as before... };
990
❘
CHAPTER 16
DRAWING IN A WINDOW
Each of the functions returns the value stored in the corresponding data member. Remember that putting a member function defi nition in the class defi nition is equivalent to a request to make the function inline; so as well as being simple, these functions should be fast.
Dealing with WM_LBUTTONUP Messages The WM_LBUTTONUP message completes the process of creating an element. The job of the handler for this message is to pass the fi nal version of the element that was created to the document object, and then to clean up the view object data members. You can access and edit the code for this handler as you did for the previous one. Add the following lines to the function: void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point) { // Make sure there is an element if(m_pTempElement) { // Call a document class function to store the element // pointed to by m_pTempElement in the document object delete m_pTempElement; m_pTempElement = nullptr;
// This code is temporary // Reset the element pointer
} }
The if statement verifies that m_pTempElement is not zero before processing it. It’s always possible that the user could press and release the left mouse button without moving the mouse, in which case, no element would have been created. As long as there is an element, the pointer to the element is passed to the document object; you’ll add the code for this in the next chapter. In the meantime, you just delete the element here, so as not to pollute the heap. Finally, the m_pTempElement pointer is reset to nullptr, ready for the next time the user draws an element.
EXERCISING SKETCHER Before you can run the example with the mouse message handlers, you must update the OnDraw() function in the CSketcherView class implementation to get rid of any old code that you added earlier. To make sure that the OnDraw() function is clean, go to Class View and double- click the function name to take you to its implementation in SketcherView.cpp. Delete any old code that you added, but leave in the fi rst four lines that the wizard provided to get a pointer to the document object. You’ll need this later to get to the elements when they’re stored in the document. The code for the function should now be as follows: void CSketcherView::OnDraw(CDC* pDC) { CSketcherDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; }
Exercising Sketcher
❘ 991
Because you have no elements in the document as yet, you don’t need to add anything to this function at this point. When you start storing data in the document in the next chapter, you’ll add code to draw the elements in response to a WM_PAINT message. Without it, the elements just disappear whenever you resize the view, as you’ll see.
Running the Example After making sure that you have saved all the source fi les, build the program. If you haven’t made any mistakes entering the code, you’ll get a clean compile and link, so you can execute the program. You can draw lines, circles, rectangles, and curves in any of the four colors the program supports. A typical window is shown in Figure 16 -18.
FIGURE 16 -18
Try experimenting with the user interface. Note that you can move the window around and that the shapes stay in the window as long as you don’t move it so far that they’re outside the borders of the application window. If you do, the elements do not reappear after you move it back. This is because the existing elements are never redrawn. When the client area is covered and uncovered, Windows sends a WM_PAINT message to the application that causes the OnDraw() member of the view object to be called. As you know, the OnDraw() function for the view doesn’t do anything at present. This gets fi xed when you use the document to store the elements. When you resize the view window, the shapes disappear immediately, but when you move the whole view around, they remain (as long as they don’t slide beyond the application window border). How come? Well, when you resize the window, Windows invalidates the whole client area and expects your application to redraw it in response to the WM_PAINT message. If you move the view around, Windows takes care of relocating the client area as it is. You can demonstrate this by moving the view so that a shape is partially obscured. When you slide it back, you still have a partial shape, with the bit that was obscured erased. If you try drawing a shape while dragging the cursor outside the client view area, you’ll notice some peculiar effects. Outside the view window, you lose track of the mouse, which tends to mess up the rubber-banding mechanism. What’s going on?
992
❘
CHAPTER 16
DRAWING IN A WINDOW
Capturing Mouse Messages The problem is caused by the fact that Windows is sending the mouse messages to the window under the cursor. As soon as the cursor leaves the client area of your application view window, the WM_MOUSEMOVE messages are being sent elsewhere. You can fi x this by using some inherited members of the CSketcherView class. The view class inherits a function, SetCapture(), that you can call to tell Windows that you want your view window to get all the mouse messages until such time as you say otherwise by calling another inherited function in the view class, ReleaseCapture(). You can capture the mouse as soon as the left button is pressed by modifying the handler for the WM_LBUTTONDOWN message: // Handler for left mouse button down message void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point) { m_FirstPoint = point; // Record the cursor position SetCapture(); // Capture subsequent mouse messages }
Now, you must call the ReleaseCapture() function in the WM_LBUTTONUP handler. If you don’t do this, other programs cannot receive any mouse messages as long as your program continues to run. Of course, you should release the mouse only if you’ve captured it earlier. The GetCapture() function that the view class inherits returns a pointer to the window that has captured the mouse, and this gives you a way of telling whether or not you have captured mouse messages. You just need to add the following to the handler for WM_LBUTTONUP: void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point) { if(this == GetCapture()) ReleaseCapture(); // Stop capturing mouse messages // Make sure there is an element if(m_pTempElement) { // Call a document class function to store the element // pointed to by m_pTempElement in the document object delete m_pTempElement; m_pTempElement = nullptr; }
// This code is temporary // Reset the element pointer
}
If the pointer returned by the GetCapture() function is equal to the pointer this, your view has captured the mouse, so you release it. The fi nal alteration you should make is to modify the WM_MOUSEMOVE handler so that it deals only with messages that have been captured by the view. You can do this with one small change: void CSketcherView::OnMouseMove(UINT nFlags, CPoint point) { // Define a Device Context object for the view
Drawing with the CLR
❘ 993
CClientDC aDC(this); // DC is for this view if((nFlags & MK_LBUTTON) && (this == GetCapture())) { m_SecondPoint = point; // Save the current cursor position if(m_pTempElement) { if(CURVE == GetDocument()->GetElementType()) // Is it a curve? { // We are drawing a curve so add a segment to the existing curve static_cast(m_pTempElement)->AddSegment(m_SecondPoint); m_pTempElement->Draw(&aDC); // Now draw it return; // We are done } // If we get to here it's not a curve so // redraw the old element so it disappears from the view aDC.SetROP2(R2_NOTXORPEN); // Set the drawing mode m_pTempElement->Draw(&aDC); // Redraw the old element delete m_pTempElement; // Delete the old element m_pTempElement = nullptr; // Reset the pointer to 0 } // Create a temporary element of the type and color that // is recorded in the document object, and draw it m_pTempElement = CreateElement();// Create a new element m_pTempElement->Draw(&aDC); // Draw the element } }
The handler now processes only the message if the left button is down and the left-button-down handler for your view has been called so that the mouse has been captured by your view window. If you rebuild Sketcher with these additions you’ll fi nd that the problems that arose earlier when the cursor was dragged off the client area no longer occur.
DRAWING WITH THE CLR It’s time to augment the CLR Sketcher application from the previous chapter with some drawing capability. I won’t discuss the principles of drawing shapes using the mouse again, because they are essentially the same in both environments. The MFC is a set of classes that wraps the Windows API, so inevitably, most of the processes are very similar to those of the Windows API. Of course, the CLR is a virtual machine that insulates you from the host environment, so there is no need for the CLR to follow the patterns for handling events and drawing in a window that are in the Windows API. There are significant differences, as you’ll see, but the CLR is not so different from the MFC that you will have any problems understanding it. Indeed, in a CLR program, things are typically somewhat simpler.
Drawing on a Form In a CLR program, you have none of the complications of mapping modes when you are drawing on a form. The origin, for drawing purposes, is at the top left of the form, with the positive x-axis
994
❘
CHAPTER 16
DRAWING IN A WINDOW
running from left to right and the positive y-axis running from top to bottom. The classes that enable you to draw on a form are within the System::Drawing namespace, and drawing on the form is carried out by a function in the Form1 class that handles the Paint event. Right- click on the form in the Design window for CLR Sketcher and select Properties from the pop -up. Select the Events button to display the events for the form, and double- click the Paint event; it’s the only event in the Appearance group in the Properties window. This generates the Form1_Paint() function that will be called in response to a Paint event that occurs when the form must be redrawn. You’ll implement this method to draw a sketch in the next chapter. One thing you can do at this point is change the background color for the form. By default, the background color is a shade of gray that is not ideal as a drawing background; white would be better. Change the value of the BackColor property for the form in the Properties window by selecting White from the Custom or Web tab on the drop -down list to the right. The form will now display with a nice white background to draw on.
Adding Mouse Event Handlers As I said, the drawing principles in CLR Sketcher are essentially the same as in MFC Sketcher, so you need handlers for the corresponding mouse events. If you scroll down the list of events in the Form1 Properties window, you can fi nd the Mouse group of events, as shown in Figure 16 -19.
FIGURE 16 -19
The ones that are of particular interest for drawing operations are the MouseDown, MouseMove, and MouseUp events. The MouseDown event occurs when a mouse button is pressed and you determine which mouse button it is from information passed to the event handler. Similarly, the MouseUp event occurs when any mouse button is released and the arguments passed to the handler for the event enable you to figure out which. You need handlers for these two events as well as the MouseMove event, so double- click each in the Properties window to generate the event handler functions for them. The handler for the MouseDown event looks like this: private: System::Void Form1_MouseDown( System::Object^ sender, System::Windows::Forms::MouseEventArgs^ { }
e)
The fi rst parameter identifies the source of the event and the second parameter provides information about the event itself. The MouseEventArgs object that is passed to this handler function (in fact, all the mouse event handlers have a parameter of this type) contains several properties that provide information you can use in handling the event, and these are described in the following table.
Drawing with the CLR
PROPERTY NAME
DESCRIPTION
Button
A property of enum type System::Windows::Forms::MouseButtons that identifies which mouse button was pressed. The MouseButtons enum defines the following possible values for the property:
❘ 995
MouseButtons::Left: The left mouse button MouseButtons::Right: The right mouse button MouseButtons::None: None of the mouse buttons MouseButtons::Middle: The middle mouse button MouseButtons::XButton1: The first XButton MouseButtons::XButton2: The second XButton Location
A property of type System::Drawing::Point that identifies the location of the mouse cursor. The X and Y properties of a Point object are the integer x and y coordinate values.
X
The x coordinate of the mouse cursor, as a value of type int.
Y
The y coordinate of the mouse cursor, as a value of type int.
Clicks
A count of the number of times the mouse button was pressed and released, as a value of type int.
Delta
A signed count of the number of detents (a detent being one notch on the mouse wheel) the mouse wheel has rotated, as a value of type int.
Following the same logic you used in the MFC Sketcher program, the MouseDown event handler needs to check that the left mouse button is pressed and record the current cursor position somewhere for use in the MouseMove handler. It also should record that the drawing of an element is in progress. To support this, you can add two private data members to the Form1 class, a bool variable with the name drawing to record when drawing an element is in progress, and a variable of type Point with the name firstPoint to record the initial mouse cursor position. You can do this manually, or from Class View using the Add ➪ Add Variable capability. Make sure that the variables are initialized by the Form1 constructor, drawing to false and firstPoint to 0; the wizard should insert these values automatically. The Point class is defi ned in the System::Drawing namespace, and you should already have a using directive for the System::Drawing namespace at the beginning of Form1.h. You can now add the code for the MouseDown delegate like this: private: System::Void Form1_MouseDown( System::Object^ sender, System::Windows::Forms::MouseEventArgs^ { if(e->Button == System::Windows::Forms::MouseButtons::Left) { drawing = true; firstPoint = e->Location; } }
e)
996
❘
CHAPTER 16
DRAWING IN A WINDOW
As long as the left mouse button is pressed, drawing an element has started, so you set drawing to true. The Location property for the parameter e supplies the cursor location as a Point object so you can store it directly in firstPoint. The MouseMove event handler will take care of creating elements for the sketch, and the MouseUp event handler will finalize the process, but before you complete the code for these two mouse event handlers, you need to take a little detour to define the classes that encapsulate the elements you can draw.
Defining C++/CLI Element Classes The pattern for the classes that defi ne elements will be similar to those in the MFC version of Sketcher; an Element base class with the specific element types defi ned by classes derived from Element. Go to the Solution Explorer window, right- click the Header Files folder, and select Add ➪ New Item from the pop -up. Use the dialog to add a header fi le with the name Elements.h. Add the code for Element base class defi nition to Elements.h, like this: // Elements.h // Defines element types #pragma once using namespace System; using namespace System::Drawing; namespace CLRSketcher { public ref class Element abstract { protected: Point position; Color color; System::Drawing::Rectangle boundRect; public: virtual void Draw(Graphics^ g) abstract; }; }
You are using classes from the System and System::Drawing namespaces in the element class definitions, so there are using directives for both. The code for the application is defined within the CLRSketcher namespace, so you put the definitions for the element classes in the same namespace. The Element class and its subclasses have to be ref classes because value classes cannot be derived classes. The Element class also must be defined as abstract because there is no implementation for the Draw() function. The Draw() function will be overridden in each of the derived classes to draw a specific type of element, and you will call the function polymorphically, just as you did in MFC Sketcher. The position member is of type System::Drawing::Point, which is a value type that defi nes a point object with two members, X and Y. The position, color, and boundRect members will be inherited in the derived classes and will store the position of the element in the client area, the element color, and the bounding rectangle for the element. All elements will be drawn relative to the point position. For the moment, you won’t worry about the distinction between a bounding rectangle and an enclosing rectangle. Incidentally, the type name for the boundRect variable,
Drawing with the CLR
❘ 997
System::Drawing::Rectangle, is fully qualified here because you will add a derived class with the name Rectangle; without the qualified name here, the compiler would assume you meant the Rectangle class in this header fi le, which is not what you want.
Defining a Line Next, you can add an initial defi nition for the Line class to Elements.h. Make sure you add this code before the closing brace for the CLRSketcher namespace block: public ref class Line : Element { protected: Point end; public: // Constructor Line(Color color, Point start, Point end) { this->color = color; position = start; this->end = end; boundRect = System::Drawing::Rectangle(Math::Min(position.X, end.X), Math::Min(position.Y, end.Y), Math::Abs(position.X - end.X), Math::Abs(position.Y - end.Y)); // Provide for lines that are horizontal or vertical if(boundRect.Width < 2) boundRect.Width = 2; if(boundRect.Height < 2) boundRect.Height = 2; } // Function to draw a line virtual void Draw(Graphics^ g) override { // Code to draw a line... } };
The constructor sets the value for the color member that is inherited from the base class. It also stores the start and end points of the line that are passed as arguments in the inherited position member and the end member, respectively. The inherited boundRect member is initialized from the position and end points. You have several ways to create a System::Drawing::Rectangle object. Here, the fi rst two arguments to the Rectangle constructor are the coordinates of the top-left corner of the rectangle. The third and fourth arguments are the width and height of the rectangle. You use the Min() function from the System::Math class to obtain the minimum values of the coordinates of position and end, and the Abs() function to obtain the absolute (positive) value for the difference between pairs of corresponding coordinates, to get the width and height, respectively. The Math class defi nes several other static utility functions that are very handy when you want to carry out basic mathematical operations. If a line is exactly horizontal or vertical, the bounding rectangle will have a width or height that is 0, so the last two if statements in the constructor ensure that the rectangle always has a width and height of greater than 0.
998
❘
CHAPTER 16
DRAWING IN A WINDOW
Defining a Color System::Drawing::Color is a value class that encapsulates an ARGB color value. An ARGB
color is a 32-bit value comprising four eight-bit components, an alpha component that determines transparency and three primary color components, red, green, and blue. Each of the component values can be from 0 to 255, where an alpha of 0 is completely transparent and an alpha of 255 is completely opaque; an eight-bit color component value represents the intensity of that color, where 0 is zero intensity (i.e. no color) and 255 is the maximum. A lot of the time, you can make use of standard color constants that the Color class defi nes, such as Color::Blue or Color::Red. If you look at the documentation for members of the Color class, you’ll see the complete set. I haven’t listed them here because there are more than 130 predefi ned colors.
Drawing Lines The Draw() function will use the System::Drawing::Graphics object that is passed as the argument to draw the line in the appropriate color. The Graphics class defi nes a large number of functions that you use for drawing shapes, including those in the following table. FUNCTION
DESCRIPTION
DrawLine(Pen pen,
Draws a line from p1 to p2 using pen. A Pen object draws in a specific color and line thickness.
Point p1, Point p2) DrawLine(Pen pen,
Draws a line from (x1,y1) to (x2,y2) using pen.
int x1, int y1, int x2, int y2) DrawLines(Pen pen, Point[] pts)
Draws a series of lines connecting the points in the pts array using pen.
DrawRectangle(Pen pen,
Draws the rectangle rect using pen.
Rectangle rect) DrawRectangle(Pen pen, int X, int Y, int width, int height) DrawEllipse(Pen pen, Rectangle rect) DrawEllipse(Pen pen, int x, int y, int width, int height)
Draws a rectangle at (x,y) using pen with the rectangle dimensions specified by width and height. Draws the ellipse that is specified by the bounding rectangle rect using pen. Draws an ellipse using pen. The ellipse is specified by the bounding rectangle at (x,y) with the dimensions width and height.
In each case, the Pen parameter determines the color and style of line used to draw the shape, so we’ll explore the Pen class in a little more depth.
Drawing with the CLR
❘ 999
Defining a Pen for Drawing The System::Drawing::Pen class represents a pen for drawing lines and curves. The simplest Pen object draws in a specified color with a default line width, for example: Pen^ pen =
gcnew Pen(Color::CornflowerBlue);
This defi nes a Pen object that you can use to draw in cornflower blue. You can also defi ne a Pen with a second argument to the constructor that defines the thickness of the line as a float value: Pen^ pen =
gcnew Pen(Color::Green, 2.0f);
This defi nes a Pen object that will draw green lines with a thickness of 2.0f. Note that the pen width is a public property, so you can always set the width for a given Pen object explicitly: pen->Width = 3.0f;
You can now conceivably complete the defi nition of the Draw() function in the Line class, like this: virtual void Draw(Graphics^ g) override { g->DrawLine(gcnew Pen(color), position, end); }
This implementation of the function draws a line from position to end with a default line width of 1 and in the color specified by color. However, don’t add this to CLR Sketcher yet, as there’s a better approach. The default Pen object draws a continuous line of a default thickness of 1.0f, but you can draw other types of lines by setting properties for a Pen object; the following table shows some of the Pen properties. PEN PROPERTY
DESCRIPTION
Color
Gets or sets the color of the pen as a value of type System::Drawing:: Color. For example, to set the drawing color for a Pen object, pen, to red, you set the property like this: pen->Color = Color::Red;
Width
Gets or sets the width of the line drawn by the pen as a value of type float.
DashPattern
Gets or sets an array of float values specifying a dash pattern for a line. The array values specify the lengths of alternating dashes and spaces in a line. For example: array^ pattern = { 5.0f, 2.0f, 4.0f, 3.0f}; pen->DashPattern = pattern;
This defines a pattern that will be a dash of length 5 followed by a space of length 2 followed by a dash of length 4 followed by a space of length 3; the pattern repeats as often as necessary along a line. continues
1000
❘
CHAPTER 16
DRAWING IN A WINDOW
(continued) PEN PROPERTY
DESCRIPTION
DashOffset
Specifies the distance from the start of a line to the beginning of a dash pattern as a value of type float.
StartCap
Specifies the cap style for the start of a line. Cap styles are defined by the System::Drawing::Drawing2D::LineCap enumeration that defines the following possible values: Flat, Square, Round, Triangle, NoAnchor, SquareAnchor, RoundAnchor, DiamondAnchor, ArrowAnchor, Custom, AnchorMask. For example, to draw lines with a round line cap at the start, set
the property as follows: pen->StartCap = System::Drawing::Drawing2D::LineCap::Round; EndCap
Specifies the cap style for the end of a line. The possible values for the cap style for the end of a line are the same as for StartCap.
When you are drawing with different line styles and colors, you have the option of creating a new Pen object for each drawing operation or using a single Pen object and changing the properties to provide the line you want. It will be useful later to adopt the latter approach in CLR Sketcher, so add a protected Pen member to the Element base class: Pen^ pen;
This will be inherited in each of the concrete derived classes, and each derived class constructor will initialize the pen member appropriately. This will preempt the need to create a new Pen object each time an element is drawn. Modify the Line class constructor like this: Line(Color color, Point start, Point end) { pen = gcnew Pen(color); this->color = color; position = start; this->end = end; boundRect = System::Drawing::Rectangle(Math::Min(position.X, end.X), Math::Min(position.Y, end.Y), Math::Abs(position.X - end.X), Math::Abs(position.Y - end.Y)); }
With the pen set up, you can now implement the Draw() function for the Line class: virtual void Draw(Graphics^ g) override { g->DrawLine(pen, position, end); }
It’s important to the performance of the application that the Draw() function does not carry excess overhead when it executes, because it will be called many times, sometimes very frequently. Now the
Drawing with the CLR
❘ 1001
Draw() function doesn’t create a new Pen object each time it is called. A positive side effect is that adding the ability to draw using different line styles will now be a piece of cake, because all that’s needed is to change the pen’s properties.
Standard Pens Sometimes, you don’t want full flexibility to change the properties of a Pen object you use. In this case, you can use the System::Drawing::Pens class that defi nes a large number of standard pens that draw a line of width 1 in a given color. For example, Pens::Black and Pens::Beige are standard pens that draw in black and beige, respectively. Note that you cannot modify a standard pen — what you see is what you get. Consult the documentation for the Pens class for the full list of standard pen colors.
Defining a Rectangle Add the defi nition of the class encapsulating CLR Sketcher rectangles to Elements.h: public ref class Rectangle : Element { protected: int width; int height; public: Rectangle(Color color, Point p1, Point p2) { pen = gcnew Pen(color); this->color = color; position = Point(Math::Min(p1.X, p2.X), Math::Min(p1.Y,p2.Y)); width = Math::Abs(p1.X - p2.X); height = Math::Abs(p1.Y - p2.Y); boundRect = System::Drawing::Rectangle(position, Size(width, height)); } virtual void Draw(Graphics^ g) override { g->DrawRectangle(pen, position.X, position.Y, width, height); } };
A rectangle is defi ned by the top-left corner position and the width and height so you have defi ned class members to store these. As in the MFC version of Sketcher, you need to figure out the coordinates of the top-left corner of the rectangle from the points supplied to the constructor. Here, you defi ne the value of the inherited boundRect member of the class as a System::Drawing::Rectangle using a constructor that accepts a Point object specifying the top -left corner as the fi rst argument. The second argument is a System::Drawing::Size object encapsulating the width and height of the rectangle. The Draw() function draws the rectangle on the form using the DrawRectangle() function for the Graphics object, g, which is passed to the Draw() function.
1002
❘
CHAPTER 16
DRAWING IN A WINDOW
Defining a Circle The defi nition of the Circle class in Elements.h is also very straightforward: public ref class Circle : Element { protected: int width; int height; public: Circle(Color color, Point center, Point circum) { pen = gcnew Pen(color); this->color = color; int radius = safe_cast(Math::Sqrt( (center.X-circum.X)*(center.X-circum.X) + (center.Y-circum.Y)*(center.Y-circum.Y))); position = Point(center.X - radius, center.Y - radius); width = height = 2*radius; boundRect = System::Drawing::Rectangle(position, Size(width, height)); } virtual void Draw(Graphics^ g) override { g->DrawEllipse(pen, position.X, position.Y, width, height); } };
The two points that are passed to the constructor are the center and a point on the circumference of the circle. The radius is the distance between these two points, and the Math::Sqrt() function calculates this. You need the width and height of the rectangle enclosing the circle to draw it, and the value of each of these is twice the radius. To draw the circle, you use the DrawEllipse() function for the Graphics object, g.
Defining a Curve A class to represent a curve is a little trickier, as you will draw it as a series of line segments joining a set of points. As in the MFC version of Sketcher, you need a way to store an arbitrary number of points in a Curve object. An STL/CLR container looks to be a promising solution, and a vector container is a good choice for storing the points on a curve. Here’s how the Curve class defi nition looks based on that: public ref class Curve : Element { private: vector^ points; public: Curve(Color color, Point p1, Point p2) { pen = gcnew Pen(color); this->color = color;
Drawing with the CLR
❘ 1003
points = gcnew vector(); position = p1; points->push_back(p2 - Size(position)); // Find the minimum and maximum coordinates int minX = p1.X < p2.X ? p1.X : p2.X; int minY = p1.Y < p2.Y ? p1.Y : p2.Y; int maxX = p1.X > p2.X ? p1.X : p2.X; int maxY = p1.Y > p2.Y ? p1.Y : p2.Y; int width = Math::Max(2, maxX - minX); int height = Math::Max(2, maxY - minY); boundRect = System::Drawing::Rectangle(minX, minY, width, height); } // Add a point to the curve void Add(Point p) { points->push_back(p - Size(position)); // Modify the bounding rectangle to accommodate the new point if(p.X < boundRect.X) { boundRect.Width = boundRect.Right - p.X; boundRect.X = p.X; } else if(p.X > boundRect.Right) boundRect.Width = p.X - boundRect.Left; if(p.Y < boundRect.Y) { boundRect.Height = boundRect.Bottom - p.Y; boundRect.Y = p.Y; } else if(p.Y > boundRect.Bottom) boundRect.Height = p.Y - boundRect.Top; } virtual void Draw(Graphics^ g) override { Point previous(position); Point temp; for each(Point p in points) { temp = position + Size(p); g->DrawLine(pen, previous, temp); previous = temp; } } };
Don’t forget to add a #include directive for the cliext/vector header to Elements.h. You will also need a using directive for the cliext namespace. You create a Curve object initially from the fi rst two points on the curve. The fi rst point, p1, defi nes the position of the curve, so you store it in position. Because you draw the curve relative to the
1004
❘
CHAPTER 16
DRAWING IN A WINDOW
origin, the second point must be specified relative to position, so you calculate this by subtracting a Size object you construct from p2 and store the result in the points container. The addition and subtraction operators that the Point class defi nes provide for adding or subtracting a Size object, respectively. Size is a value class with properties Width and Height, typically of a rectangle. Width is treated as the X component and Height as the Y component for the purposes of the addition and subtraction operators in the Point class. You add subsequent points to the vector container by calling the Add() member function after modifying the coordinates of each point so it is defi ned relative to position. The constructor creates the initial bounding rectangle for the curve from the fi rst two points by obtaining the minimum and maximum coordinate values and using these to determine the top -left point coordinates and the width and height of the rectangle, respectively. A curve could conceivably be a vertical or horizontal line that would result in a width or height of 0, so the width and height are always set to at least 2. The Add() function updates the object referenced by boundRect to accommodate the new point that it adds to the container. The Right property of a System::Drawing::Rectangle object corresponds to the sum of the x coordinate for the top-left position and the width. The Bottom property is the sum of the y coordinate and the height. These properties are get- only. There are also Left and Top properties that are get- only, and these correspond to the x coordinate of the left edge and the y coordinate of the top edge of the rectangle, respectively. The X, Y, Width, and Height properties of a System:: Drawing::Rectangle object are the x and y coordinates of the top -left corner and the width and height, respectively; you can get and set these properties. You commence drawing the curve in the Draw() method by fi rst initializing the previous variable to position, which defi nes the start point for the fi rst line segment. You then iterate through the points in the points container, obtaining the absolute coordinates of each point by adding a Size object constructed from p to position. The DrawLine() member of the Graphics object draws the line; then, you store the last absolute point in previous so it becomes the fi rst point for the next line segment. You will be able to improve this rather cumbersome mechanism for drawing a curve considerably in the next chapter.
Implementing the MouseMove Event Handler The MouseMove event handler creates a temporary element object of the current type. Add a private member, tempElement, to the Form1 class of type Element^ to store the reference to the temporary element, and initialize it to nullptr in the initialization list for the Form1 constructor. This temporary element will be stored eventually in the sketch by the MouseUp event handler, because the MouseUp event signals the end of the drawing process for an element, but you’ll deal with this in the next chapter. Don’t forget to add a #include directive for the Elements.h header at the beginning of the Form1.h header file; otherwise, the compiler will not recognize the Element type and the code won’t compile. You want to create an element in the MouseMove event handler only when the drawing member of the Form1 class is true, because this is how the MouseDown event handler records that the drawing of an element has started. Here’s the code for implementing the MouseMove event handler that you added to the Form1 class earlier: private: System::Void Form1_MouseMove(System::Object^ System::Windows::Forms::MouseEventArgs^ e) {
sender,
Drawing with the CLR
❘ 1005
if(drawing) { switch(elementType) { case ElementType::LINE: tempElement = gcnew Line(color, firstPoint, e->Location); break; case ElementType::RECTANGLE: tempElement = gcnew Rectangle(color, firstPoint, e->Location); break; case ElementType::CIRCLE: tempElement = gcnew Circle(color, firstPoint, e->Location); break; case ElementType::CURVE: if(tempElement) safe_cast(tempElement)->Add(e->Location); else tempElement = gcnew Curve(color, firstPoint, e->Location); break; } Invalidate(); } }
If drawing is false, the handler does nothing. The switch statement determines which type of element is to be created from the elementType member of the Form1 object. Creating lines, rectangles, and circles is very simple: just use the appropriate class constructor. You obtain the current mouse cursor position as you did in the MouseDown event handler — by using the Location property for the MouseEventArgs object. Creating a curve is different. You create a new Curve object only if tempElement is nullptr, which will occur the fi rst time this handler is called when you are drawing a curve. On all subsequent occasions, tempElement won’t be nullptr, and you cast the reference to type Curve so that you can call the Add() function for the Curve object. Calling Invalidate() for the Form1 object causes the form to be redrawn, which results in the Paint event handler for the form being called. Obviously, this is where you will draw the entire sketch eventually, and you’ll implement that mechanism properly in the next chapter.
Implementing the MouseUp Event Handler The task of the MouseUp event handler is to store the new element in the sketch, to reset tempElement back to nullptr and the drawing indicator back to false, and to redraw the sketch. Here’s the code to do that: private: System::Void Form1_MouseUp(System::Object^ System::Windows::Forms::MouseEventArgs^ e) { if(!drawing) return; if(tempElement) { // Store the element in the sketch...
sender,
1006
❘
CHAPTER 16
DRAWING IN A WINDOW
tempElement = nullptr; Invalidate();
//
} drawing = false; }
If drawing is false, you return immediately, because the element drawing process has not started. The second if statement verifies that tempElement is not nullptr before adding the element to the sketch. You might think that if you get to this point, tempElement must exist, but this is not so; apart from the fact that the ability to draw a curve is not yet implemented, a user might press the left mouse button and release it immediately, in which case, the MouseMove handler would not be invoked at all. Of course, you need to call Invalidate() only to redraw the form when you have added a new element to the sketch. The call to Invalidate() is commented out for the moment because there is no sketch at present, and redrawing the form here would cause the temporary element to disappear as soon as you released the mouse button.
Implementing the Paint Event Handler for the Form The Paint event delegate that you generated earlier has two parameters: the fi rst parameter, of type Object^, identifies the source of the event, and the second, of type System::Windows::Forms:: PaintEventArgs, provides information about the event. In particular, the Graphics property for the second parameter provides the Graphics object that you use to draw on the form. You call the Invalidate() function for the form in the MouseUp event handler to draw the sketch, and in the MouseMove event handler to draw the temporary element. Therefore, the Paint event handler must eventually do two things: it must draw the sketch and it must draw the temporary element, but only if there is one. Here’s the code: private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ { Graphics^ g = e->Graphics; // Code to draw the sketch... if(tempElement != nullptr) { tempElement->Draw(g); } }
e)
You initialize the local variable, g, with the value obtained from the Graphics property for the PaintEventArgs object, e. If tempElement is not nullptr, you call the Draw() function to display the element. If you compile and execute CLR Sketcher, you’ll see that you can create elements in any color. Of course, the elements are not displayed on an ongoing basis because there is no sketch object. As soon as you draw a new element, the existing one disappears. You will fi x that in the next chapter.
Summary
❘ 1007
SUMMARY After completing this chapter, you should have a good grasp of how to write message handlers for the mouse, and how to organize drawing operations in your Windows programs. You have seen how using polymorphism with the shape classes makes it possible to operate on any shape in the same way, regardless of its actual type. The C++/CLI version of Sketcher demonstrates how much easier the .NET environment is to work with than native C++ with the MFC.
EXERCISES You can download the source code for the examples in the book and the solutions to the following exercises from www.wrox.com. Don’t forget to back up the MFC Sketcher and CLR Sketcher projects before you modify them for the exercises, because you will be extending them further in the next chapter.
1.
Add the menu item and Toolbar button to MFC Sketcher for an element of type ellipse, as in the exercises from Chapter 15, and define a class to support drawing ellipses defined by two points on opposite corners of their enclosing rectangle.
2.
Which functions now need to be modified to support the drawing of an ellipse? Modify the program to draw an ellipse.
3.
Which functions must you modify in the example from the previous exercise so that the first point defines the center of the ellipse, and the current cursor position defines a corner of the enclosing rectangle? Modify the example to work this way. (Hint: look up the CPoint class members in Help.)
4.
Add a new menu pop -up to the IDR_SketcherTYPE menu for Pen Style, to allow solid, dashed, dotted, dash- dotted, and dash - dot- dotted lines to be specified.
5.
Which parts of the program need to be modified to support the operation of the menu, and the drawing of elements in the line types listed in the previous exercise?
6.
Implement support for the new menu pop -up and drawing elements in any of the line types.
7.
Modify CLR Sketcher to support the drawing of an ellipse defined by two points on opposite corners of the enclosing rectangle. Add a toolbar button as well as a menu item to select ellipse mode.
8.
Implement a Line Style menu in CLR Sketcher with menu items Solid, Dashed, and Dotted. Add toolbar buttons with tooltips for the menu items. Implement the capability in CLR Sketcher for drawing elements in any of the three line styles in the new menu. Create your own representation for the line styles by setting the DashPattern property for the pen appropriately.
1008
❘
CHAPTER 16
DRAWING IN A WINDOW
WHAT YOU LEARNED IN THIS CHAPTER TOPIC
CONCEPT
The client coordinate system
By default, Windows addresses the client area of a window using a client coordinate system with the origin in the upper- left corner of the client area. The positive x direction is from left to right, and the positive y direction is from top to bottom.
Drawing in the client area
You can draw in the client area of a window only by using a device context.
Device contexts
A device context provides a range of logical coordinate systems called mapping modes for addressing the client area of a window.
Mapping modes
The default origin position for a mapping mode is the upper- left corner of the client area. The default mapping mode is MM_TEXT, which provides coordinates measured in pixels. The positive x-axis runs from left to right in this mode, and the positive y-axis from top to bottom.
Drawing in a window
Your program should always draw the permanent contents of the client area of a window in response to a WM_PAINT message, although temporary entities can be drawn at other times. All the drawing for your application document should be controlled from the OnDraw() member function of a view class. This function is called when a WM_PAINT message is received by your application.
Redrawing a window
You can identify the part of the client area you want to have redrawn by calling the InvalidateRect() function member of your view class. The area passed as an argument is added by Windows to the total area to be redrawn when the next WM_PAINT message is sent to your application.
Mouse messages
Windows sends standard messages to your application for mouse events. You can create handlers to deal with these messages by using the Class Wizard.
Capturing mouse messages
You can cause all mouse messages to be routed to your application by calling the SetCapture() function in your view class. You must release the mouse when you’re finished with it by calling the ReleaseCapture() function. If you fail to do this, other applications are unable to receive mouse messages.
Rubber-banding
You can implement rubber- banding during the creation of geometric entities by drawing them in the message handler for mouse movements.
Selecting a drawing mode
The SetROP2() member of the CDC class enables you to set drawing modes. Selecting the right drawing mode greatly simplifies rubber-banding operations.
Adding event handlers
The Properties window for a GUI component also enables you to add event handler functions automatically.
The Graphics class
The System::Drawing::Graphics class defines functions that enable you to draw on a form.
The Pen class
The System::Drawing::Pen class defines an object for drawing in a given color and line style.
17
Creating the Document and Improving the View WHAT YOU WILL LEARN IN THIS CHAPTER:
➤
How to use an STL list container and an STL/CLR list container to store sketch data
➤
How to implement drawing a sketch in the MFC and CLR versions of Sketcher
➤
How to implement scrolling in a view in MFC Sketcher
➤
How to create a context menu at the cursor
➤
How to highlight the element nearest the cursor to provide feedback to the user for moving and deleting elements
➤
How to program the mouse to move and delete elements
In this chapter, you’ll extend the MFC version of Sketcher to make the document view more flexible, introducing several new techniques in the process. You’ll also extend the MFC and CLR versions of Sketcher to store elements in an object that encapsulates a complete sketch.
CREATING THE SKETCH DOCUMENT The document in the Sketcher application needs to be able to store a sketch consisting of an arbitrary collection of lines, rectangles, circles, and curves in any sequence, and an excellent vehicle for handling this is a list. Using a list will also enable you to change an element ’s position in the list or to delete an element easily if it becomes necessary to do so. Because all the element classes that you’ve defi ned include the capability for the objects to draw themselves, the user will be able to a draw a sketch easily by stepping through the list.
1010
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
Using a list Container for the Sketch You can defi ne an STL list container that stores pointers to instances of the shape classes that make up a sketch. You just need to add the list declaration as a new member in the CSketcherDoc class defi nition. You also need a member function to add an element to the list, and AddElement() is a good, if unoriginal, name for this: // SketcherDoc.h : interface of the CSketcherDoc class // Available for download on Wrox.com
#pragma once #include <list> #include “Elements.h” #include “SketcherConstants.h” class CSketcherDoc: public CDocument { protected: // create from serialization only CSketcherDoc(); DECLARE_DYNCREATE(CSketcherDoc) // Attributes public: // Operations public: unsigned int GetElementType() const // Get the element type { return m_Element; } COLORREF GetElementColor() const // Get the element color { return m_Color; } void AddElement(CElement* pElement) // Add an element to the list { m_ElementList.push_back(pElement); } // Rest of the class as before... protected: ElementType m_Element; COLORREF m_Color; std::list m_ElementList;
// Current element type // Current drawing color // List of elements in the sketch
// Rest of the class as before... }; code snippet SketcherDoc.h
The CSketcherDoc class now refers to the CElement class, so there is an #include directive for Elements.h in SketcherDoc.h. There is also one for the list header because you are using the STL list template. You create shape objects on the heap, so you can just pass a pointer to the AddElement() function. Because all this function does is add an element, you have put the implementation of AddElement() in the class definition. Adding an element to the list requires only one statement, which calls the push_back() member function. That’s all you need to create the document, but you still have to consider what happens when a document is closed. You must ensure that the list of pointers and all
Creating the Sketch Document
❘ 1011
the elements they point to are destroyed properly. To do this, you need to add code to the destructor for CSketcherDoc objects.
Implementing the Document Destructor In the destructor, you’ll fi rst go through the list deleting the element pointed to by each entry. After that is complete, you must delete the pointers from the list. The code to do this is as follows: CSketcherDoc::~CSketcherDoc(void) { // Delete the element pointed to by each list entry for(auto iter = m_ElementList.begin() ; iter != m_ElementList.end() ; ++iter) delete *iter; m_ElementList.clear();
// Finally delete all pointers
}
You should add this code to the defi nition of the destructor in SketcherDoc.cpp. You can go directly to the code for the destructor through the Class View. The for loop iterates over all the elements in the list. The auto keyword ensures the correct type is specified for the iterator iter. When the for loop ends, you call the clear() function for the list to remove all the pointers from the list.
Drawing the Document Because the document owns the list of elements and the list is protected, you can’t use it directly from the view. The OnDraw() member of the view does need to be able to call the Draw() member for each of the elements in the list, though, so you need to consider how best to arrange this. One possibility is to make some const iterators for the m_ElementList member of the document class available. This will allow the view class object to call the Draw() function for each element, but will prevent the list from being changed. You can add two more functions to the CSketcherDoc class definition to provide for this: class CSketcherDoc: public CDocument { // Rest of the class as before... // Operations public: unsigned int GetElementType() const // Get the element type { return m_Element; } COLORREF GetElementColor() const // Get the element color { return m_Color; } void AddElement(CElement* pElement) // Add an element to the list { m_ElementList.AddTail(pElement); } std::list::const_iterator begin() const // Get list begin iterator { return m_ElementList.begin(); } std::list::const_iterator end() const // Get list end iterator { return m_ElementList.end(); } // Rest of the class as before... };
1012
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
The begin() function returns an iterator that points to the fi rst element in the list, and the end() function returns an iterator that points to one past the last element in the list. The iterator type is defi ned in the list class template as const_iterator, but because the iterator type is specific to a particular instance of the template, you must qualify the type name with the list instance type. By using the two functions you have added to the document class, the OnDraw() function for the view will be able to iterate through the list, calling the Draw() function for each element. This implementation of OnDraw()is as follows:
Available for download on Wrox.com
void CSketcherView::OnDraw(CDC* pDC) { CSketcherDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if(!pDoc) return; CElement* pElement(nullptr); for(auto iter = pDoc->begin() ; iter != pDoc->end() ; ++iter) { pElement = *iter; if(pDC->RectVisible(pElement->GetBoundRect())) // If the element is visible pElement->Draw(pDC); // ...draw it } } Code snippet SketcherView.cpp
Frequently, when a WM_PAINT message is sent to your program, only part of the window needs to be redrawn. When Windows sends the WM_PAINT message to a window, it also defines an area in the client area of the window, and only this area needs to be redrawn. The CDC class provides a member function, RectVisible(), which determines whether a rectangle you supply to it as an argument overlaps the area that Windows requires to be redrawn. Here, you use this function to make sure that you draw only the elements that are in the area Windows wants redrawn, thus improving the performance of the application. You use the begin() and end() members of the document object in the for loop to iterate over all the elements in the list. The auto keyword determines the type of iter appropriately. Within the loop, you dereference iter and store it in the pElement variable. You retrieve the bounding rectangle for each element using the GetBoundRect() member of the object, and pass it to the RectVisible() function in the if statement. If the value returned by RectVisible() is true, you call the Draw() function for the current element pointed to by pElement. As a result, only elements that overlap the area that Windows has identified as invalid are drawn. Drawing on the screen is a relatively expensive operation in terms of time, so checking for just the elements that need to be redrawn, rather than drawing everything each time, improves performance considerably.
Adding an Element to the Document The last thing you need to do to have a working document in our program is to add the code to the OnLButtonUp() handler in the CSketcherView class to add the temporary element to the document:
Creating the Sketch Document
❘ 1013
void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point) { if(this == GetCapture()) ReleaseCapture(); // Stop capturing mouse messages // If there is an element, add it to the document if(m_pTempElement) { GetDocument()->AddElement(m_pTempElement); InvalidateRect(nullptr); // Redraw the current window m_pTempElement = nullptr; // Reset the element pointer } }
Of course, you must check that there really is an element before you add it to the document. The user might just have clicked the left mouse button without moving the mouse. After adding the element to the list in the document, you call InvalidateRect() to get the client area for the current view redrawn. The argument of 0 invalidates the whole of the client area in the view. Because of the way the rubber-banding process works, some elements may not be displayed properly if you don’t do this. If you draw a horizontal line, for instance, and then rubber-band a rectangle with the same color so that its top or bottom edge overlaps the line, the overlapped bit of line disappears. This is because the edge being drawn is XORed with the line underneath, so you get the background color back. You also reset the pointer m_pTempElement to avoid confusion when another element is created. Note that the statement deleting m_pTempElement has been removed.
Exercising the Document After saving all the modified fi les, you can build the latest version of Sketcher and execute it. You’ll now be able to produce art such as “The Happy Programmer,” shown in Figure 17-1.
FIGURE 17-1
1014
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
The program is now working more realistically. It stores a pointer to each element in the document object, so they’re all automatically redrawn as necessary. The program also does a proper cleanup of the document data when it’s deleted. There are still some limitations in the program that you can address. For instance: ➤
You can open another view window by using the Window ➪ New Window menu option in the program. This capability is built into an MDI application and opens a new view to an existing document, not a new document. If you draw in one window, however, the elements are not drawn in the other window. Elements never appear in windows other than the one in which they were drawn, unless the area they occupy needs to be redrawn for some other reason.
➤
You can draw only in the client area you can see. It would be nice to be able to scroll the view and draw over a bigger area.
➤
You can’t delete an element, so if you make a mistake, you either live with it or start over with a new document.
These are all quite serious deficiencies that, together, make the program fairly useless as it stands. You’ll overcome all of them before the end of this chapter.
IMPROVING THE VIEW The fi rst item that you can try to fi x is the updating of all the document windows that are displayed when an element is drawn. The problem arises because only the view in which an element is drawn knows about the new element. Each view is acting independently of the others, and there is no communication between them. You need to arrange for any view that adds an element to the document to let all the other views know about it, and they need to take the appropriate action.
Updating Multiple Views The document class conveniently contains a function, UpdateAllViews(), to help with this particular problem. This function essentially provides a means for the document to send a message to all its views. You just need to call it from the OnLButtonUp() function in the CSketcherView class whenever you have added a new element to the document: void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point) { if(this == GetCapture()) ReleaseCapture(); // Stop capturing mouse messages // If there is an element, add it to the document if(m_pTempElement) { GetDocument()->AddElement(m_pTempElement); GetDocument()->UpdateAllViews(nullptr, 0, m_pTempElement); // Tell the views m_pTempElement = nullptr; // Reset the element pointer } }
Improving the View
❘ 1015
When the m_pTempElement pointer is not nullptr, the specific action of the function has been extended to call the UpdateAllViews() member of your document class. This function communicates with the views by causing the OnUpdate() member function in each view to be called. The three arguments to UpdateAllViews() are described in Figure 17-2.
This argument is a pointer to the current view. It suppresses calling of the OnUpdate() member function for the view.
LPARAM is a 32-bit Windows type that can be used to pass information about the region to be updated in the client area
This argument is a pointer to an object that can provide information about the area in the region to be updated in the client area.
void UpdateAllView( CView* pSender, LPARAM IHint = OL, CObject* pHint = NULL ); These two argument values are passed on to the OnUpdate() functions in the views
FIGURE 17-2
The fi rst argument to the UpdateAllViews() function call is often the this pointer for the current view. This suppresses the call of the OnUpdate() function for the current view. This is a useful feature when the current view is already up to date. In the case of Sketcher, because you are rubberbanding, you want to get the current view redrawn as well, so by specifying the fi rst argument as 0 you get the OnUpdate() function called for all the views, including the current view. This removes the need to call InvalidateRect() as you did before. You don’t use the second argument to UpdateAllViews() here, but you do pass the pointer to the new element through the third argument. By passing a pointer to the new element, you allow the views to figure out which bit of their client area needs to be redrawn. To catch the information passed to the UpdateAllViews() function, you must add an override for the OnUpdate() member function to the view class. You can do this from the Class View through the Properties window for CSketcherView. As I’m sure you recall, you display the properties for a class by right- clicking the class name and selecting Properties from the pop -up. If you click the Overrides button in the Properties window, you’ll be able to fi nd OnUpdate in the list of functions you can override. Click the function name, then the < Add> OnUpdate option that shows in the drop -down list in the adjacent column. You’ll be able to edit the code for the
1016
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
OnUpdate() override you have added in the Editor pane. You only need to add the highlighted code
below to the function defi nition: void CSketcherView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint) { // Invalidate the area corresponding to the element pointed to // if there is one, otherwise invalidate the whole client area if(pHint) { InvalidateRect(static_cast(pHint)->GetBoundRect()); } else { InvalidateRect(nullptr); } }
Note that you must uncomment at least the name of the third parameter in the generated version of the function; otherwise, it won’t compile with the additional code here. The three arguments passed to the OnUpdate() function in the view class correspond to the arguments that you passed in the UpdateAllViews() function call. Thus, pHint contains the address of the new element. However, you can’t assume that this is always the case. The OnUpdate() function is also called when a view is fi rst created, but with a null pointer for the third argument. Therefore, the function checks that the pHint pointer isn’t nullptr and only then gets the bounding rectangle for the element passed as the third argument. It invalidates this area in the client area of the view by passing the rectangle to the InvalidateRect() function. This area is redrawn by the OnDraw() function in this view when the next WM_PAINT message is sent to the view. If the pHint pointer is nullptr, the whole client area is invalidated. You might be tempted to consider redrawing the new element in the OnUpdate() function. This isn’t a good idea. You should do permanent drawing only in response to the Windows WM_PAINT message. This means that the OnDraw() function in the view should be the only thing that’s initiating any drawing operations for document data. This ensures that the view is drawn correctly whenever Windows deems it necessary. If you build and execute Sketcher with the new modifications included, you should fi nd that all the views are updated to reflect the contents of the document.
Scrolling Views Adding scrolling to a view looks remarkably easy at fi rst sight; the water is, in fact, deeper and murkier than it at fi rst appears, but jump in anyway. The fi rst step is to change the base class for CSketcherView from CView to CScrollView. This new base class has the scrolling functionality built in, so you can alter the defi nition of the CSketcherView class to this: class CSketcherView: public CScrollView { // Class definition as before... };
Improving the View
❘ 1017
You must also modify two lines of code at the beginning of the SketcherView.cpp fi le that refer to the base class for CSketcherView. You need to replace CView with CScrollView as the base class: IMPLEMENT_DYNCREATE(CSketcherView, CScrollView) BEGIN_MESSAGE_MAP(CSketcherView, CScrollView)
However, this is still not quite enough. The new version of the view class needs to know some things about the area you are drawing on, such as the size and how far the view is to be scrolled when you use the scroller. This information has to be supplied before the view is fi rst drawn. You can put the code to do this in the OnInitialUpdate() function in the view class. You supply the information required by calling a function that is inherited from the CScrollView class: SetScrollSizes(). The arguments to this function are explained in Figure 17-3.
This defines the horizontal(cx) and vertical(cy) distances to scroll a page. This can be defined as: CSize Page(cx, cy); Default is 1/10 of the total area
This defines the horizontal(cx) and vertical(cy) distances to scroll a line. This can be defined as: CSize Line(cx, cy); Default is 1/10 of the total area
void SetScrollSizes( int MapMode, SIZE Total, const SIZE& Page = sizeDefault, const SIZE& Line = sizeDefault );
Can be any of: MM_TEXT MM_TWIPS MM_LOENGLISH MM_HIENGLISH MM_LOMETRIC MM_HIMETRIC
This is the total drawing area and can be defined as: CSize Total(cx, cy); where cx is the horizontal extent and cy is the vertical extent in logical units.
FIGURE 17-3
Scrolling a distance of one line occurs when you click on the up or down arrow on the scroll bar; a page scroll occurs when you click the scrollbar itself. You have an opportunity to change the mapping mode here. MM_LOENGLISH would be a good choice for the Sketcher application, but fi rst
1018
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
get scrolling working with the MM_TEXT mapping mode because there are still some difficulties to be uncovered. (Mapping modes were introduced in Chapter 16.) To add the code to call SetScrollSizes(), you need to override the default version of the OnInitialUpdate() function in the view. You access this in the same way as for the OnUpdate() function override — through the Properties window for the CSketcherView class. After you have added the override, just add the code to the function where indicated by the comment: void CSketcherView::OnInitialUpdate() { CScrollView::OnInitialUpdate(); // Define document size CSize DocSize(20000,20000); // Set mapping mode and document size SetScrollSizes(MM_TEXT, DocSize, CSize(500,500), CSize(50,50)); }
This maintains the mapping mode as MM_TEXT and defi nes the total extent that you can draw on as 20,000 pixels in each direction. It also specifies a page scroll increment as 500 pixels in each direction and a line scroll increment as 50 pixels, because the defaults would be too large. This is enough to get the scrolling mechanism working after a fashion. Build the program and execute it with these additions, and you’ll be able to draw a few elements and then scroll the view. However, although the window scrolls OK, if you try to draw more elements with the view scrolled, things don’t work as they should. The elements appear in a different position from where you draw them and they’re not displayed properly. What’s going on?
Logical Coordinates and Client Coordinates The problem is the coordinate systems you’re using — and that plural is deliberate. You’ve actually been using two coordinate systems in all the examples up to now, although you may not have noticed. As you saw in the previous chapter, when you call a function such as LineTo(), it assumes that the arguments passed are logical coordinates. The function is a member of the CDC class that defi nes a device context, and the device context has its own system of logical coordinates. The mapping mode, which is a property of the device context, determines what the unit of measurement is for the coordinates when you draw something. The coordinate data that you receive along with the mouse messages, on the other hand, has nothing to do with the device context or the CDC object — and outside of a device context, logical coordinates don’t apply. The points passed to the OnLButtonDown() and OnMouseMove() handlers have coordinates that are always in device units — that is, pixels — and are measured relative to the upper left corner of the client area. These are referred to as client coordinates. Similarly, when you call InvalidateRect(), the rectangle is assumed to be defi ned in terms of client coordinates. In MM_TEXT mode, the client coordinates and the logical coordinates in the device context are both in pixels, and so they’re the same — as long as you don’t scroll the window. In all the previous examples,
Improving the View
❘ 1019
there was no scrolling, so everything worked without any problems. With the latest version of Sketcher, it all works fine until you scroll the view, whereupon the logical coordinates origin (the 0,0 point) is moved by the scrolling mechanism, so it’s no longer in the same place as the client coordinates origin. The units for logical coordinates and client coordinates are the same here, but the origins for the two coordinates systems are different. This situation is illustrated in Figure 17-4. The left side shows the position in the client area where you draw, and the points that are the mouse positions defi ning the line. These are recorded in client coordinates. The right side shows where the line is actually drawn. Drawing is in logical coordinates, but you have FIGURE 17-4 been using client coordinate values. In the case of the scrolled window, the line appears displaced because the logical origin has been relocated. This means that you are actually using the wrong values to defi ne elements in the Sketcher program, and when you invalidate areas of the client area to get them redrawn, the rectangles passed to the function are also wrong — hence, the weird behavior of the program. With other mapping modes, it gets worse, because not only are the units of measurement in the two coordinate systems different, but the y axes also may be in opposite directions!
Dealing with Client Coordinates Consider what needs to be done to fi x the problem. There are two things you may have to address: ➤
You need to convert the client coordinates that you got with mouse messages to logical coordinates before you can use them to create elements.
➤
You need to convert a bounding rectangle that you created in logical coordinates back to client coordinates if you want to use it in a call to InvalidateRect().
These amount to making sure you always use logical coordinates when using device context functions, and always use client coordinates for other communications about the window. The functions you have to apply to do the conversions are associated with a device context, so you need to obtain a device context whenever you want to convert from logical to client coordinates or vice versa. You can use the coordinate conversion functions of the CDC class inherited by CClientDC to do the work. The new version of the OnLButtonDown() handler incorporating this is as follows: // Handler for left mouse button down message void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point) {
1020
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
CClientDC aDC(this); OnPrepareDC(&aDC); aDC.DPtoLP(&point); m_FirstPoint = point; SetCapture();
// // // // //
Create a device context Get origin adjusted Convert point to logical coordinates Record the cursor position Capture subsequent mouse messages
}
You obtain a device context for the current view by creating a CClientDC object and passing the pointer this to the constructor. The advantage of CClientDC is that it automatically releases the device context when the object goes out of scope. It’s important that device contexts are not retained, as there are a limited number available from Windows and you could run out of them. If you use CClientDC, you’re always safe. As you’re using CScrollView, the OnPrepareDC() member function inherited from that class must be called to set the origin for the logical coordinate system in the device context to correspond with the scrolled position. After you have set the origin by this call, you use the function DPtoLP(), which converts from Device Points to Logical Points, to convert the point value that’s passed to the handler to logical coordinates. You then store the converted point, ready for creating an element in the OnMouseMove() handler. The new code for the OnMouseMove() handler is as follows: void CSketcherView::OnMouseMove(UINT nFlags, CPoint point) { CClientDC aDC(this); // Device context for the current view OnPrepareDC(&aDC); // Get origin adjusted aDC.DPtoLP(&point); // Convert point to logical coordinates if((nFlags&MK_LBUTTON)&&(this==GetCapture())) { m_SecondPoint = point; // Save the current cursor position // Rest of the function as before... }
The code for the conversion of the point value passed to the handler is essentially the same as in the previous handler, and that’s all you need here for the moment. The last function that you must change is easy to overlook: the OnUpdate() function in the view class. This needs to be modified to the following: void CSketcherView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint) { // Invalidate the area corresponding to the element pointed to // if there is one, otherwise invalidate the whole client area if(pHint) { CClientDC aDC(this); // Create a device context OnPrepareDC(&aDC); // Get origin adjusted // Get the enclosing rectangle and convert to client coordinates CRect aRect = static_cast(pHint)->GetBoundRect(); aDC.LPtoDP(aRect); InvalidateRect(aRect); // Get the area redrawn }
Improving the View
❘ 1021
else { InvalidateRect(nullptr);
// Invalidate the client area
} }
The modification here creates a CClientDC object and uses the LPtoDP() function member to convert the rectangle for the area that’s to be redrawn to client coordinates. If you now compile and execute Sketcher with the modifications I have discussed and are lucky enough not to have introduced any typos, it will work correctly, regardless of the scroller position.
Using MM_LOENGLISH Mapping Mode Now, we will look into what you need to do to use the MM_LOENGLISH mapping mode. The advantage of using this mapping mode is that it provides drawings in logical units of 0.01 inches, which ensures that the drawing size is consistent on displays at different resolutions. This makes the application much more satisfactory from the user’s point of view. You can set the mapping mode in the call to SetScrollSizes() made from the OnInitialUpdate() function in the view class. You also need to specify the total drawing area: if you defi ne it as 3,000 by 3,000, this provides a drawing area of 30 inches by 30 inches, which should be adequate. The default scroll distances for a line and a page are satisfactory, so you don’t need to specify those. You can use the Class View to get to the OnInitialUpdate() function and then change it to the following: void CSketcherView::OnInitialUpdate(void) { CScrollView::OnInitialUpdate(); // Define document size as 30x30ins in MM_LOENGLISH CSize DocSize(3000,3000); // Set mapping mode and document size. SetScrollSizes(MM_LOENGLISH, DocSize); }
You just alter the arguments in the call to SetScrollSizes() for the mapping mode and the document size that you want. That’s all that’s necessary to enable the view to work in MM_LOENGLISH. Note that you are not limited to setting the mapping mode once and for all. You can change the mapping mode in a device context at any time and draw different parts of the image to be displayed using different mapping modes. A function, SetMapMode(), is used to do this, but I won’t be going into this any further here. You can get your application working just using MM_LOENGLISH. Whenever you create a CClientDC object for the view and call OnPrepareDC(), the device context that it owns has the mapping mode you’ve set in the OnInitialUpdate() function. That should do it for the program as it stands. If you rebuild Sketcher, you should have scrolling working, with support for multiple views.
1022
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
DELETING AND MOVING SHAPES Being able to delete shapes is a fundamental requirement in a drawing program. One question relating to this is how you’re going to select the element you want to delete. Of course, after you decide how you are going to select an element, your decision also applies to how you move an element, so you can treat moving and deleting elements as related problems. But fi rst, consider how you’re going to bring move and delete operations into the user interface for the program. A neat way of providing move and delete functions would be to have a pop -up context menu appear at the cursor position when you click the right mouse button. You could then include Move and Delete as items on the menu. A pop -up that works like this is a very handy tool that you can use in lots of different situations. How should the pop -up be used? The standard way that context menus work is that the user moves the mouse over a particular object and right- clicks it. This selects the object and pops up a menu containing a list of items relating to actions that can be performed on that object. This means that different objects can have different menus. You can see this in action in Developer Studio itself. When you right- click a class icon in Class View, you get a menu that ’s different from the one you get if you right- click the icon for a member function. The menu that appears is sensitive to the context of the cursor, hence the term “context menu.” You have two contexts to consider in Sketcher. You could right- click with the cursor over an element, or you could right- click when there is no element under the cursor. So how can you implement this functionality in the Sketcher application? You can do it simply by creating two menus: one for when you have an element under the cursor and one for when you don’t. You can check if there’s an element under the cursor when the user clicks the right mouse button. If there is an element under the cursor, you can highlight the element so that the user knows exactly which element the context pop-up is referring to. First, take a look at how you can create a pop -up at the cursor, and, after that works, you can come back to how to implement the details of the move and delete operations.
IMPLEMENTING A CONTEXT MENU You will need two context menu pop -ups: one for when there is an element under the mouse cursor, and another for when there isn’t. The first step is to create a menu containing Move and Delete as menu items that will apply to the element under the cursor. The other pop-up will contain a combination of menu items from the Element and Color menus. So change to Resource View and expand the list of resources. Right-click the Menu folder to bring up a context menu — another demonstration of what you are now trying to create in the Sketcher application. Select Insert Menu to create a new menu. This has a default ID IDR_MENU1 assigned, but you can change this. Select the name of the new menu in the Resource View and display the Properties window for the resource by pressing Alt+Enter (this is a shortcut to the View ➪ Other Windows ➪ Properties Window menu item). You can then edit the resource ID in the Properties window by clicking the value for the ID. You could change it to something more suitable, such as IDR_ELEMENT_MENU, in the right column. Note that the name for a menu resource must start with IDR. Pressing the Enter key saves the new name.
Implementing a Context Menu
You can now create a new item on the menu bar in the Editor pane. This can have any old caption because it won’t actually be seen by the user; the caption element will be OK. Now you can add the Move and Delete items to the element pop-up. The default IDs of ID_ELEMENT_MOVE and ID_ELEMENT_DELETE will do fine, but you can change them if you want in the Properties window for each item. Figure 17-5 shows how the new element menu looks.
❘ 1023
FIGURE 17-5
Save the menu and create another with the ID IDR_NOELEMENT_MENU; you can make the caption no element this time. The second menu will contain the list of available element types and colors, identical to the items on the Element and Color menus on the main menu bar, but here separated by a separator. The IDs you use for these items must be the same as the ones you applied to the IDR_SketcherTYPE menu. This is because the handler for a menu is associated with the menu ID. Menu items with the same ID use the same handlers, so the same handler is used for the Line menu item regardless of whether it’s invoked from the main menu pop -up or from the context menu. You have a shortcut that saves you from having to create all these menu items one by one. If you display the IDR_SketcherTYPE menu and extend the Element menu, you can select all the menu items by clicking the fi rst item and then clicking the last item while holding down the Shift key. You can then right- click the selection and select Copy from the pop -up or simply press Ctrl+C. If you then return to the IDR_NOELEMENT_MENU and right- click the fi rst item on the no element menu, you can insert the complete contents of the Element menu by selecting Paste from the pop -up or by pressing Ctrl+V. The copied menu items will have the same IDs as the originals. To insert the separator, just right- click the empty menu item and select Insert Separator from the pop -up. Repeat the process for the Color menu items and you’re done. Close the properties box and save the resource file. At the moment, all you have is the defi nition of the context menus in a resource fi le. It isn’t connected to the code in the Sketcher program. You now need to associate these menus and their IDs with the view class. You also must create command handlers for the menu items in the pop -up corresponding to the IDs ID_ELEMENT_MOVE and ID_ELEMENT_DELETE.
Associating a Menu with a Class Context menus are managed in an MFC program by an object of type CContextManager. The CSketcherApp class object already has a member of type CContextManager defi ned, and you can use this object to manage your context menus. Go to the CSketcherApp class in the Class View and go to the defi nition of the PreLoadState() member function. Add the following lines of code:
Available for download on Wrox.com
void CSketcherApp::PreLoadState() { /* BOOL bNameValid; CString strName; bNameValid = strName.LoadString(IDS_EDIT_MENU); ASSERT(bNameValid);
1024
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
GetContextMenuManager()->AddMenu(strName, IDR_POPUP_EDIT); */ GetContextMenuManager()->AddMenu(_T(“Element menu”), IDR_ELEMENT_MENU); GetContextMenuManager()->AddMenu(_T(“No element menu”), IDR_NOELEMENT_MENU); } Code snippet SketcherApp.cpp
NOTE I have commented out the original lines of code that were there, to show that they are no longer required. You should remove these lines because they display the Edit menu as a context menu, and you don’t want that to happen.
Calling the GetContextManager() function for the application object returns a pointer to the CContextManager object. The AddMenu() function adds the menu specified by the second argument to the CContextManager object. The fi rst argument specifies a name for the new menu. You can specify the name explicitly, as in the code here, or you can specify a resource ID for the string that is the name. Note how the text strings in the first argument to the AddMenu() function are enclosed within the _T() macro. This is because we are using the Unicode version of the MFC libraries. The _T() macro selects the correct type for the string, depending on whether or not the _UNICODE symbol is defined for the project. You may remember we left the Use Unicode libraries option checked when we first created the Sketcher project. You should always use the _T() macro when defining string literals in a Unicode program. When you are using the MFC Unicode libraries, you should also use the TCHAR type instead of the char type in your code, LPTSTR instead of char*, and LPCTSTR instead of const char*. A context menu can be displayed in response to a WM_CONTEXTMENU message, but the default code generated by the wizard calls the OnContextMenu() handler from the OnRButtonUp() handler. If you look at the CSketcherView class implementation, there should already be a defi nition of the OnRButtonUp() handler and the OnContextMenu() handler, and this is where you’ll put the code to display the new context menus you have just defi ned. Note that the default code for the OnRButtonUp() handler calls ClientToScreen() to convert the coordinates of the current cursor position to screen coordinates. You can then add the following code to the OnContextMenu() handler: void CSketcherView::OnContextMenu(CWnd* pWnd, CPoint point) { #ifndef SHARED_HANDLERS // theApp.GetContextMenuManager()->ShowPopupMenu(IDR_POPUP_EDIT, // point.x, point.y, this, TRUE); if(m_pSelected) theApp.GetContextMenuManager()->ShowPopupMenu(IDR_ELEMENT_MENU, point.x, point.y, this); else theApp.GetContextMenuManager()->ShowPopupMenu(IDR_NOELEMENT_MENU, point.x, point.y, this); #endif }
Implementing a Context Menu
❘ 1025
The fi rst line of code was already present, and I have commented it out to indicate that it is not required for Sketcher. You should remove this line because it displays the Edit menu as a context menu, and you don’t want that to happen. The new code chooses between the two context menus you have created based on whether or not the m_pSelected member is null. m_pSelected will store the address of the element under the mouse cursor, or nullptr if there isn’t one. You will add this member to the view class in a moment. Calling the ShowPopupMenu() function for the CContextMenuManager object displays the pop -up identified by the fi rst argument at the position in the client area specified by the second and third arguments. Add m_pSelected as a protected member of the CSketcherView class as type CElement*. You can initialize this member to nullptr in the view class constructor. Now, you can add the handlers for the items in the element pop -up menu. Return to the Resource View and double- click IDR_ELEMENT_MENU. Right- click the Move menu item and then select Add Event Handler from the pop -up. You can then specify the handler in the dialog for the Event Handler wizard. It’s a COMMAND handler and you create it in the CSketcherView class. Click the Add and Edit button to create the handler function. You can follow the same procedure to create the hander for the Delete menu item. You don’t have to do anything for the no element context menu, as you already have handlers written for it in the document class. These automatically take care of the messages from the pop -up items.
Identifying a Selected Element We will create a very simple mechanism for selecting an element. An element in a sketch will be selected whenever the mouse cursor is within the bounding rectangle for an element. For this to be effective, Sketcher must track at all times, whether or not there is an element under the cursor. To keep track of which element is under the cursor, you can add code to the OnMouseMove() handler in the CSketcherView class. This handler is called every time the mouse cursor moves, so all you have to do is add code to determine whether there’s an element under the current cursor position and set m_pSelected accordingly. The test for whether a particular element is under the cursor is simple: if the cursor position is within the bounding rectangle for an element, that element is under the cursor. Here’s how you can modify the OnMouseMove() handler to determine whether there’s an element under the cursor: void CSketcherView::OnMouseMove(UINT nFlags, CPoint point) { // Define a Device Context object for the view CClientDC aDC(this); // DC is for this view OnPrepareDC(&aDC); // Get origin adjusted aDC.DPtoLP(&point); // Convert point to logical coordinates if((nFlags & MK_LBUTTON) && (this == GetCapture())) { // Code as before... }
1026
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
else { // We are not creating an element, so select an element CSketcherDoc* pDoc=GetDocument(); // Get a pointer to the document m_pSelected = pDoc->FindElement(point); // Set selected element } }
The new code is very simple and is executed only when we are not creating a new element. The else clause in the if statement calls the FindElement() function for the document object that you’ll add shortly and stores the element address that is returned in m_pSelected. The FindElement() function has to search the document for the first element that has a bounding rectangle that encloses point. You can add FindElement() to the document class as a public member and implement it like this: // Finds the element under the point CElement* CSketcherDoc::FindElement(const CPoint& point)const { for(auto rIter = m_ElementList.rbegin() ; rIter != m_ElementList.rend() ; ++rIter) { if((*rIter)->GetBoundRect().PtInRect(point)) return *rIter; } return nullptr; }
You use a reverse-iterator to search the list from the end, which means the most recently created and, therefore, last-drawn element will be found fi rst. The PtInRect() member of the CRect class returns TRUE if the point you pass as the argument lies within the rectangle, and FALSE otherwise. Here, you use this function to test whether or not the point lies within any bounding rectangle for an element in the sketch. The address of the fi rst element for which this is true is returned. If no element in the sketch is found, the loop will end and nullptr will be returned. The code is now in a state where you can test the context menus.
Exercising the Pop-Ups You have added all the code you need to make the pop-ups operate, so you can build and execute Sketcher to try it out. When you right- click the mouse, or press Shift+F10, a context menu will be displayed. If there are no elements under the cursor, the second context pop -up appears, enabling you to change the element type and color. These options work because they generate exactly the same messages as the main menu options, and because you have already written handlers for them. If there is an element under the cursor, the fi rst context menu will appear with Move and Delete on it. It won’t do anything at the moment, as you’ve yet to implement the handlers for the messages it generates. Try right-button clicks outside the view window. Messages for these are not passed to the document view window in your application, so the pop-up is not displayed.
Highlighting Elements Ideally, the user will want to know which element is under the cursor before right- clicking to get the context menu. When you want to delete an element, you want to know which element you are
Implementing a Context Menu
❘ 1027
operating on. Equally, when you want to use the other context menu — to change color, for example — you need to be sure no element is under the cursor when you right- click the mouse. To show precisely which element is under the cursor, you need to highlight it in some way before a right-button click occurs. You can change the Draw() member function for an element to accommodate this. All you need to do is pass an extra argument to the Draw() function to indicate when the element should be highlighted. If you pass the address of the currently-selected element that you save in the m_pSelected member of the view to the Draw() function, you will be able to compare it to the this pointer to see if it is the current element. All highlights will work in the same way, so let’s take the CLine member as an example. You can add similar code to each of the classes for the other element types. Before you start changing CLine, you must fi rst amend the defi nition of the base class CElement:
Available for download on Wrox.com
class CElement : public CObject { protected: int m_PenWidth; // Pen width COLORREF m_Color; // Color of an element CRect m_EnclosingRect; // Rectangle enclosing an element public: virtual ~CElement(); // Virtual draw operation virtual void Draw(CDC* pDC, CElement* pElement=nullptr) {} CRect GetBoundRect() const; protected: CElement();
// Get the bounding rectangle for an element
// Here to prevent it being called
}; Code snippet Elements.h
The change is to add a second parameter to the virtual Draw() function. This is a pointer to an element. The reason for initializing the second parameter to nullptr is that this allows the use of the function with just one argument; the second will be supplied as nullptr by default. You need to modify the declaration of the Draw() function in each of the classes derived from CElement in exactly the same way. For example, you should change the CLine class definition to the following: class CLine : public CElement { public: CLine(void); // Function to display a line virtual void Draw(CDC* pDC, CElement* pElement=nullptr); // Constructor for a line object CLine(const CPoint& Start, const CPoint& End, COLORREF aColor);
1028
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
protected: CPoint m_StartPoint; CPoint m_EndPoint; CLine(void);
// Start point of line // End point of line // Default constructor - should not be used
};
The implementation for each of the Draw() functions for the classes derived from CElement needs to be extended in the same way. The function for the CLine class is as follows:
Available for download on Wrox.com
void CLine::Draw(CDC* pDC, CElement* pElement) { // Create a pen for this object and // initialize it to the object color and line width m_PenWidth CPen aPen; if(!aPen.CreatePen(PS_SOLID, m_PenWidth,this==pElement ? SELECT_COLOR : m_Color)) { // Pen creation failed. Abort the program AfxMessageBox(_T(“Pen creation failed drawing a line”), MB_OK); AfxAbort(); } CPen* pOldPen = pDC->SelectObject(&aPen);
// Select the pen
// Now draw the line pDC->MoveTo(m_StartPoint); pDC->LineTo(m_EndPoint); pDC->SelectObject(pOldPen);
// Restore the old pen
} Code snippet Elements.cpp
This is a very simple change. The third argument to the CreatePen() function is now a conditional expression. You choose the color for the pen based on whether or not pElement is the same as the current CLine element pointer, this. If it is, you choose SELECT_COLOR for the pen rather than the original color for the line. When you have updated the Draw() functions for all the subclasses of CElement, you can add the defi nition for SELECT_COLOR to the SketcherConstants.h fi le: // SketcherConstants.h : Definitions of constants //Definitions of constants Available for download on Wrox.com
#pragma once // Element type definitions enum ElementType{LINE, RECTANGLE, CIRCLE, CURVE}; // Color values for drawing const COLORREF BLACK = RGB(0,0,0); const COLORREF RED = RGB(255,0,0); const COLORREF GREEN = RGB(0,255,0); const COLORREF BLUE = RGB(0,0,255);
Implementing a Context Menu
❘ 1029
const COLORREF SELECT_COLOR = RGB(255,0,180); /////////////////////////////////// Code snippet SketcherConstants.h
You have nearly implemented the highlighting. The derived classes of the CElement class are now able to draw themselves as selected when required — you just need a mechanism to cause an element to be selected. So where should you create this? You can determine which element, if any, is under the cursor in the OnMouseMove() handler in the CSketcherView class, so that’s obviously the place to expedite the highlighting. The amendments to the OnMouseMove() handler are as follows: void CSketcherView::OnMouseMove(UINT nFlags, CPoint point) { // Define a Device Context object for the view CClientDC aDC(this); // DC is for this view OnPrepareDC(&aDC); // Get origin adjusted aDC.DPtoLP(&point); // Convert point to logical coordinates if((nFlags & MK_LBUTTON) && (this == GetCapture())) { // Code as before... } else { // We are not creating an element to do highlighting CSketcherDoc* pDoc=GetDocument(); // Get a pointer to the document CElement* pOldSelected(m_pSelected); m_pSelected = pDoc->FindElement(point); // Set selected element if(m_pSelected != pOldSelected) { if(m_pSelected) InvalidateRect(m_pSelected->GetBoundRect(), FALSE); if(pOldSelected) InvalidateRect(pOldSelected->GetBoundRect(), FALSE); pDoc->UpdateAllViews(nullptr); } } }
You must keep track of any previously highlighted element because, if there’s a new one, you must un-highlight the old one. To do this, you save the value of m_pSelected in pOldSelected before you check for a selected element. You then store the address returned by the FindElement() function for the document object in m_pSelected. If pOldSelected and m_pSelected are equal, then either they both contain the address of the same element or they are both zero. If they both contain a valid address, the element will already have been highlighted last time around. If they are both zero, nothing is highlighted and nothing needs to be done. Thus, in either case, you do nothing. Thus, you want to do something only when m_pSelected is not equal to pOldSelected. If m_pSelected is not nullptr, you need to get it redrawn, so you call InvalidateRect() with the fi rst argument as the bounding rectangle for the element, and the second argument as FALSE so as not to erase the background. If pOldSelected is also not null, you must un-highlight the old element by invalidating its bounding rectangle in the same way. Finally, you call UpdateAllViews() for the document object to get the views updated.
1030
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
Drawing Highlighted Elements You still need to arrange that the highlighted element is actually drawn highlighted. Somewhere, the m_pSelected pointer must be passed to the draw function for each element. The only place to do this is in the OnDraw() function in the view: void CSketcherView::OnDraw(CDC* pDC) { CSketcherDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); if (!pDoc) return; CElement* pElement(nullptr); for(auto iter = pDoc->begin() ; iter != pDoc->end() ; ++iter) { pElement = *iter; if(pDC->RectVisible(pElement->GetBoundRect())) // If the element is visible pElement->Draw(pDC, m_pSelected); // ...draw it } }
You need to change only one line. The Draw() function for an element has the second argument added to communicate the address of the element to be highlighted.
Exercising the Highlights This is all that’s required for the highlighting to work all the time. It wasn’t trivial but, on the other hand, it wasn’t terribly difficult. You can build and execute Sketcher to try it out. Any time there is an element under the cursor, the element is drawn in magenta. This makes it obvious which element the context menu is going to act on before you right- click the mouse, and means that you know in advance which context menu will be displayed.
Servicing the Menu Messages The next step is to provide code in the bodies of the handlers for the Move and Delete menu items that you added earlier. You can add the code for Delete fi rst, as that’s the simpler of the two.
Deleting an Element The code that you need in the OnElementDelete() handler in the CSketcherView class to delete the currently selected element is simple: void CSketcherView::OnElementDelete() { if(m_pSelected) { CSketcherDoc* pDoc = GetDocument();// pDoc->DeleteElement(m_pSelected); // pDoc->UpdateAllViews(nullptr); // m_pSelected = nullptr; // } }
Get the document pointer Delete the element Redraw all the views Reset selected element ptr
Implementing a Context Menu
❘ 1031
The code to delete an element is executed only if m_pSelected contains a valid address, indicating that there is an element to be deleted. You get a pointer to the document and call the function DeleteElement() for the document object; you’ll add this member to the CSketcherDoc class in a moment. When the element has been removed from the document, you call UpdateAllViews() to get all the views redrawn without the deleted element. Finally, you set m_pSelected to nullptr to indicate that there isn’t an element selected. You can add a declaration for DeleteElement() as a public member of the CSketcherDoc class: class CSketcherDoc : public CDocument { protected: // create from serialization only CSketcherDoc(); DECLARE_DYNCREATE(CSketcherDoc) // Attributes public: // Operations public: unsigned int GetElementType() const // Get the element type { return m_Element; } COLORREF GetElementColor() const // Get the element color { return m_Color; } void AddElement(CElement* pElement) // Add an element to the list { m_ElementList.push_back(pElement); } std::list::const_iterator begin() const { return m_ElementList.begin(); } std::list::const_iterator end() const { return m_ElementList.end(); } CElement* FindElement(const CPoint& point) const; void DeleteElement(CElement* pElement);
// Delete an element
// Rest of the class as before... };
It accepts as an argument a pointer to the element to be deleted and returns nothing. You can implement it in SketcherDoc.cpp as the following: // Delete an element from the sketch void CSketcherDoc::DeleteElement(CElement* pElement) { if(pElement) { m_ElementList.remove(pElement); // Remove the pointer from the list delete pElement; // Delete the element from the heap } }
You shouldn’t have any trouble understanding how this works. You use pElement with the remove() member of the list object to delete the pointer from the list, then you delete the element pointed to by the parameter pElement from the heap.
1032
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
That’s all you need in order to delete elements. You should now have a Sketcher program in which you can draw in multiple scrolled views, and delete any of the elements in your sketch from any of the views.
Moving an Element Moving the selected element is a bit more involved. As the element must move along with the mouse cursor, you must add code to the OnMouseMove() method to provide for this behavior. As this function is also used to draw elements, you need a mechanism for indicating when you’re in “move” mode. The easiest way to do this is to have a flag in the view class, which you can call m_MoveMode. If you make it of type BOOL, the Windows API Boolean type, you use the value TRUE for when move mode is on, and FALSE for when it’s off. Of course, you can also defi ne it as the C++ fundamental type, bool, with the values true and false. You’ll also have to keep track of the cursor during the move, so you can add another data member in the view for this. You can call it m_CursorPos, and it will be of type CPoint. Another thing you should provide for is the possibility of aborting a move. To do this, you must remember the fi rst position of the cursor when the move operation started, so you can move the element back when necessary. This is another member of type CPoint, and it is called m_FirstPos. Add the three new members to the protected section of the view class: class CSketcherView: public CScrollView { // Rest of the class as before... protected: CPoint m_FirstPoint; CPoint m_SecondPoint; CElement* m_pTempElement; CElement* m_pSelected; bool m_MoveMode; CPoint m_CursorPos; CPoint m_FirstPos;
// // // // // // //
First point recorded for an element Second point recorded for an element Pointer to temporary element Currently selected element Move element flag Cursor position Original position in a move
// Rest of the class as before... };
These must also be initialized in the constructor for CSketcherView, so modify it to the following: CSketcherView::CSketcherView() : m_FirstPoint(CPoint(0,0)) , m_SecondPoint(CPoint(0,0)) , m_pTempElement(nullptr) , m_pSelected(nullptr) , m_MoveMode(false) , m_CursorPos(CPoint(0,0)) , m_FirstPos(CPoint(0,0)) { // TODO: add construction code here }
Implementing a Context Menu
❘ 1033
The element move process starts when the Move menu item from the context menu is selected. Now, you can add the code to the message handler for the Move menu item to set up the conditions necessary for the operation: void CSketcherView::OnElementMove() { CClientDC aDC(this); OnPrepareDC(&aDC); // GetCursorPos(&m_CursorPos); // ScreenToClient(&m_CursorPos); // aDC.DPtoLP(&m_CursorPos); // m_FirstPos = m_CursorPos; // m_MoveMode = true; // }
Set up the device context Get cursor position in screen coords Convert to client coords Convert to logical Remember first position Start move mode
You are doing four things in this handler:
1.
Getting the coordinates of the current position of the cursor, because the move operation starts from this reference point.
2.
Converting the cursor position to logical coordinates, because your elements are defi ned in logical coordinates.
3. 4.
Remembering the initial cursor position in case the user wants to abort the move later. Setting the move mode on as a flag for the OnMouseMove() handler to recognize.
The GetCursorPos() function is a Windows API function that stores the current cursor position in m_CursorPos. Note that you pass a reference to this function. The cursor position is in screen coordinates (that is, coordinates measured in pixels relative to the upper-left corner of the screen). All operations with the cursor are in screen coordinates. You want the position in logical coordinates, so you must do the conversion in two steps. The ScreenToClient() function (which is an inherited member of the view class) converts from screen to client coordinates, and then you apply the DPtoLP() function member of the aDC object to the result to convert to logical coordinates. After saving the initial cursor position in m_FirstPos, you set m_MoveMode to true so that the OnMouseMove() handler can deal with moving the element. Now that you have set the move mode flag, it’s time to update the mouse move message handler to deal with moving an element.
Modifying the WM_MOUSEMOVE Handler Moving an element only occurs when move mode is on and the cursor is being moved. Therefore, all you need to do in OnMouseMove() is add code to handle moving an element in a block that gets executed only when m_MoveMode is TRUE. The new code to do this is as follows: void CSketcherView::OnMouseMove(UINT nFlags, CPoint point) { CClientDC aDC(this); // DC is for this view OnPrepareDC(&aDC); // Get origin adjusted aDC.DPtoLP(&point);
// Convert point to logical coordinates
1034
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
// If we are in move mode, move the selected element and return if(m_MoveMode) { MoveElement(aDC, point); // Move the element return; } // Rest of the mouse move handler as before... }
This addition doesn’t need much explaining, really, does it? The if statement verifies that you’re in move mode and then calls a function MoveElement(), which does what is necessary for the move. All you have to do now is implement this function. Add the declaration for MoveElement() as a protected member of the CSketcherView class by adding the following at the appropriate point in the class defi nition: void MoveElement(CClientDC& aDC, const CPoint& point); // Move an element
As always, you can also right- click the class name in Class View to do this, if you want to. The function needs access to the object encapsulating a device context for the view, aDC, and the current cursor position, point, so both of these are reference parameters. The implementation of the function in the SketcherView.cpp fi le is as follows: void CSketcherView::MoveElement(CClientDC& aDC, const CPoint& point) { CSize distance = point - m_CursorPos; // Get move distance m_CursorPos = point; // Set current point as 1st for // next time // If there is an element selected, move it if(m_pSelected) { aDC.SetROP2(R2_NOTXORPEN); m_pSelected->Draw(&aDC, m_pSelected); // Draw the element to erase it m_pSelected->Move(distance); // Now move the element m_pSelected->Draw(&aDC, m_pSelected); // Draw the moved element } }
The distance to move the element that is currently selected is stored locally as a CSize object, distance. The CSize class is specifically designed to represent a relative coordinate position and has two public data members, cx and cy, which correspond to the x and y increments. These are calculated as the difference between the current cursor position, stored in point, and the previous cursor position, saved in m_CursorPos. This uses the subtraction operator, which is overloaded in the CPoint class. The version you are using here returns a CSize object, but there is also a version that returns a CPoint object. You can usually operate on CSize and CPoint objects combined. You save the current cursor position in m_CursorPos for use the next time this function is called, which occurs if there is a further mouse move message during the current move operation. You implement moving an element in the view using the R2_NOTXORPEN drawing mode, because it’s easy and fast. This is exactly the same as what you have been using during the creation of
Implementing a Context Menu
❘ 1035
an element. You redraw the selected element in its current color (the selected color) to reset it to the background color, and then call the function Move() to relocate the element by the distance specified by distance. You’ll add this function to the element classes in a moment. When the element has moved itself, you simply use the Draw() function once more to display it highlighted at the new position. The color of the element will revert to normal when the move operation ends, as the OnLButtonUp() handler will redraw all the windows normally by calling UpdateAllViews().
Getting the Elements to Move Themselves Add the Move() function as a virtual member of the base class, CElement. Modify the class defi nition to the following: class CElement : public CObject { protected: int m_PenWidth; COLORREF m_Color; CRect m_EnclosingRect; public: virtual ~CElement();
// Pen width // Color of an element // Rectangle enclosing an element
// Virtual draw operation virtual void Draw(CDC* pDC, CElement* pElement=nullptr) {} virtual void Move(const CSize& aSize){} // Move an element CRect GetBoundRect(); protected: CElement();
// Get the bounding rectangle for an element
// Here to prevent it being called
};
As discussed earlier in relation to the Draw() member, although an implementation of the Move() function here has no meaning, you can’t make it a pure virtual function because of the requirements of serialization. You can now add a declaration for the Move() function as a public member of each of the classes derived from CElement. It is the same in each: virtual void Move(const CSize& aSize); // Function to move an element
Next, you can add the implementation of the Move() function in the CLine class to Elements.cpp: void CLine::Move(const CSize& aSize) { m_StartPoint += aSize; m_EndPoint += aSize; m_EnclosingRect += aSize; }
// Move the start point // and the end point // Move the enclosing rectangle
This is easy because of the overloaded += operators in the CPoint and CRect classes. They all work with CSize objects, so you just add the relative distance specified by aSize to the start and end points for the line and to the enclosing rectangle.
1036
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
Moving a CRectangle object is even easier: void CRectangle::Move(const CSize& aSize) { m_EnclosingRect+= aSize; // Move the rectangle }
Because the rectangle is defi ned by the m_EnclosingRect member, that’s all you need to move it. The Move() member of the CCircle class is identical: void CCircle::Move(const CSize& aSize) { m_EnclosingRect+= aSize; // Move rectangle defining the circle }
Moving a CCurve object is a little more complicated, because it’s defi ned by an arbitrary number of points. You can implement the function as follows: void CCurve::Move(const CSize& aSize) { m_EnclosingRect += aSize; // Move the rectangle // Now move all the points std::for_each(m_Points.begin(), m_Points.end(), [&aSize](CPoint& p){ p += aSize; }); }
So that you don’t forget about them, I have used an algorithm and a lambda expression here. You will need to add an #include directive for the algorithm header to Elements.cpp. There’s still not a lot to it. You fi rst move the enclosing rectangle stored in m_EnclosingRect, using the overloaded += operator for CRect objects. You then use the STL for_each algorithm to apply the lambda expression that is the third argument to all the points defi ning the curve. The capture clause for the lambda expression accesses the aSize parameter by reference. Of course, the lambda parameter must be a reference so the original points can be updated.
Dropping the Element Once you have clicked on the Move menu item, the element under the cursor will move around as you move the mouse. All that remains now is to add the capability to drop the element in position once the user has fi nished moving it, or to abort the whole move. To drop the element in its new position, the user clicks the left mouse button, so you can manage this operation in the OnLButtonDown() handler. To abort the operation, the user clicks the right mouse button — so you can add the code to the OnRButtonDown() handler to deal with this. You can take care of the left mouse button fi rst. You’ll have to provide for this as a special action when move mode is on. The changes are highlighted in the following: void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point) { CClientDC aDC(this); // Create a device context OnPrepareDC(&aDC); // Get origin adjusted
Implementing a Context Menu
aDC.DPtoLP(&point);
❘ 1037
// convert point to Logical
if(m_MoveMode) { // In moving mode, so drop the element m_MoveMode = false; // Kill move mode m_pSelected = nullptr; // De-select the element GetDocument()->UpdateAllViews(0); // Redraw all the views return; } m_FirstPoint = point; // Record the cursor position SetCapture(); // Capture subsequent mouse messages }
The code is pretty simple. You fi rst make sure that you’re in move mode. If this is the case, you just set the move mode flag back to false and then de-select the element. This is all that’s required because you’ve been tracking the element with the mouse, so it’s already in the right place. Finally, to tidy up all the views of the document, you call the document’s UpdateAllViews() function, causing all the views to be redrawn. Add a handler for the WM_RBUTTONDOWN message to CSketcherView using the Properties window for the class. The implementation for aborting a move must do two things. It must move the element back to where it was and turn off move mode. You can move the element back in the right-buttondown handler, but you must leave switching off move mode to the right-button-up handler to allow the context menu to be suppressed. The code to move the element back is as follows: void CSketcherView::OnRButtonDown(UINT nFlags, CPoint point) { if(m_MoveMode) { // In moving mode, so drop element back in original position CClientDC aDC(this); OnPrepareDC(&aDC); // Get origin adjusted MoveElement(aDC, m_FirstPos); // Move element to original position m_pSelected = nullptr; // De-select element GetDocument()->UpdateAllViews(nullptr); // Redraw all the views } }
You fi rst create a CClientDC object for use in the MoveElement() function. You then call the MoveElement() function to move the currently selected element the distance from the current cursor position to the original cursor position that we saved in m_FirstPos. After the element has been repositioned, you just deselect the element and get all the views redrawn. The last thing you must do is switch off move mode in the button-up handler: void CSketcherView::OnRButtonUp(UINT /* nFlags */, CPoint point) { if(m_MoveMode) { m_MoveMode = false; return; }
1038
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
ClientToScreen(&point); OnContextMenu(this, point); }
Now, the context menu gets displayed only when move mode is not in progress.
Exercising the Application Everything is now complete for the context pop -ups to work. If you build Sketcher, you can select the element type and color from one context menu, or if you are over an element, you can move that element or delete it from the other context menu.
DEALING WITH MASKED ELEMENTS There’s still a limitation that you might want to get over. If the element you want to move or delete is enclosed by the rectangle of another element that is drawn after the element you want, you won’t be able to highlight it because Sketcher always fi nds the outer element fi rst. The outer element completely masks the element it encloses. This is a result of the sequence of elements in the list. You could fi x this by adding a Send to Back item to the context menu that would move an element to the beginning of the list. Add a separator and a menu item to the element drop down in the IDR_ELEMENT_MENU resource, as shown in Figure 17- 6.
FIGURE 17-6
You can add a handler for the item to the view class through the Properties window for the CSketcherView class. It’s best to handle it in the view because that’s where you record the selected element. Select the Events toolbar button in the Properties window for the class and doubleclick the message ID ID_ELEMENT_SENDTOBACK. You’ll then be able to select COMMAND below and OnElementSendtoback in the right column. You can implement the handler as follows: void CSketcherView:: OnElementSendtoback() { GetDocument()->SendToBack(m_pSelected); // Move element to start of list }
You’ll get the document to do the work by passing the currently selected element pointer to a public function, SendToBack(), that you implement in the CSketcherDoc class. Add it to the class defi nition with a void return type and a parameter of type CElement*. You can implement this function as follows: void CSketcherDoc::SendToBack(CElement* pElement) { if(pElement) {
Extending CLR Sketcher
m_ElementList.remove(pElement); m_ElementList.push_front(pElement);
❘ 1039
// Remove the element from the list // Put it back at the beginning of the list
} }
After checking that the parameter is not null, you remove the element from the list by calling remove(). Of course, this does not delete the element from memory; it just removes the element pointer from the list. You then add the element pointer back at the beginning of the list, using the push_front() function. With the element moved to the beginning of the list, it cannot mask any of the others because you search for an element to highlight from the end. You will always fi nd one of the other elements fi rst if the applicable bounding rectangle encloses the current cursor position. The Send to Back menu option is always able to resolve any element masking problem in the view.
EXTENDING CLR SKETCHER It’s time to extend CLR Sketcher by adding a class encapsulating a complete sketch. You’ll also implement element highlighting and a context menu with the ability to move and delete elements. You can adopt a different approach to drawing elements in this version of Sketcher that will make the move element operation easy to implement. Before you start extending CLR Sketcher and working with the element classes, let’s explore another feature of the Graphics class that will be useful in the application.
Coordinate System Transformations The Graphics class contains functions that can move, rotate, and scale the entire drawing coordinate system. This is a very powerful capability that you can use in the element drawing operations in Sketcher. The following table describes the most useful functions that transform the coordinate system.
TRANSFORM FUNCTION
DESCRIPTION
TranslateTransform(
Translates the coordinate system origin by dx in the x-direction and dy in the y-direction.
float dx, float dy) RotateTransform( float angle) ScaleTransform( float scaleX,
Rotates the coordinate system about the origin by angle degrees. A positive value for angle represents rotation from the x-axis toward the y-axis, a clockwise rotation in other words. Scales the x-axis by multiplying by scaleX and scales the y-axis by multiplying by scaleY.
float scaleY) ResetTransform()
Resets the current transform state for a Graphics object so no transforms are in effect.
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
You will be using the TranslateTransform() function in the element drawing operations. To draw an element, you can translate the origin for the coordinate system to the position specified by the inherited position member of the Element class, draw the element relative to the origin (0,0) and then restore the coordinate system to its original state. This process is illustrated in Figure 17-7, which shows how you draw a circle.
The circle element is to be drawn at positon (dx, dy) Position (dx, dy) X-Axis
Y-Axis
dy dx height
width
The Drawing Process X-Axis moves the origin to (dx, dy)
g-⬎TranslateTransform (dx, dy);
Y-Axis
X-Axis Y-Axis
1040
ircle g-⬎DrawEllipse the c in (pen, 0, 0, width, height); s w g Dra e ori at th
Moves the origin back to (0, 0) g-⬎ResetTransform(); FIGURE 17-7
You can change the Draw() function in the Circle class to use the TranslateTransform() function: virtual void Draw(Graphics^ g) override { g->TranslateTransform(safe_cast(position.X), safe_cast(position.Y));
Extending CLR Sketcher
❘ 1041
g->DrawEllipse(pen, 0, 0, width,height); g->ResetTransform(); }
The TranslateTransform() function requires arguments of type float, so you cast the coordinates to this type. This is not absolutely necessary, but you will get warning messages from the compiler if you don’t, so it is a good idea to always put the casts in. To make the most efficient use of the TranslateTransform() function when drawing a line, you could change the Line class constructor to store the end point relative to position: Line(Color color, Point start, Point end) { pen = gcnew Pen(color); this->color = color; position = start; this->end = end - Size(start); boundRect = System::Drawing::Rectangle(Math::Min(position.X, end.X), Math::Min(position.Y, end.Y), Math::Abs(position.X - end.X), Math::Abs(position.Y - end.Y)); // Provide for lines that are horizontal or vertical if(boundRect.Width < 2) boundRect.Width = 2; if(boundRect.Height < 2) boundRect.Height = 2; }
The subtraction operator for the Point class enables you to subtract a Size object from a Point object. Here, you construct the Size object from the start object that is passed to the constructor. This results in the end member coordinates being relative to start. Change the Draw() function in the Line class to the following: virtual void Draw(Graphics^ g) override { g->TranslateTransform(safe_cast(position.X), safe_cast(position.Y)); g->DrawLine(pen, 0, 0, end.X, end.Y); g->ResetTransform(); }
The DrawLine() function now draws the line relative to 0,0 after the TranslateTransform() function moves the origin of the client area to position, the start point for the line. The Draw() function for the Rectangle class is very similar to that for the Circle class: virtual void Draw(Graphics^ g) override { g->TranslateTransform(safe_cast(position.X), safe_cast(position.Y)); g->DrawRectangle(pen, 0, 0, width, height); g->ResetTransform(); }
1042
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
The Draw() function for the Curve class is simpler than before: virtual void Draw(Graphics^ g) override { g->TranslateTransform(safe_cast(position.X), safe_cast(position.Y)); Point previous(0,0); for each(Point p in points) { g->DrawLine(pen, previous, p); previous = p; } g->ResetTransform(); }
The points in the points vector all have coordinates relative to position, so once you transfer the client area origin to position using the TranslateTransform() method, each line can be drawn directly by means of the points in the points vector. With the existing element classes updated, you are ready to defi ne the object that will encapsulate a sketch.
Defining a Sketch Class Using Solution Explorer, create a new header fi le with the name Sketch.h to hold the Sketch class; just right- click the Header Files folder and select from the pop -up. A sketch is an arbitrary sequence of elements of any type that has Element as a base class. An STL/CLR container looks a good bet to store the elements in a sketch, and this time you can use a list container. To allow any type of element to be stored in the list, you can defi ne the container class as type list<Element^>. Specifying that the container stores references of type Element^ will enable you to store references to objects of any type that is derived from Element. Initially, the Sketch class will need a constructor, an Add() function that adds a new element to a sketch, and a Draw() function to draw a sketch. Here’s what you need to put in Sketch.h: // Sketch.h // Defines a sketch Available for download on Wrox.com
#pragma once #include #include "Elements.h" using namespace System; using namespace cliext; namespace CLRSketcher { public ref class Sketch { private: list<Element^>^ elements; public: Sketch()
Extending CLR Sketcher
❘ 1043
{ elements = gcnew list<Element^>(); } // Add an element to the sketch Sketch^ operator+=(Element^ element) { elements->push_back(element); return this; } // Remove an element from the sketch Sketch^ operator-=(Element^ element) { elements->remove(element); return this; } void Draw(Graphics^ g) { for each(Element^ element in elements) element->Draw(g); } }; } code snippet Sketch.h
The #include directive for the header is necessary for accessing the STL/CLR list container template, and the #include for the Elements.h header enables you to reference the Element base class. The Sketch class is defined within the CLRSketcher namespace, so it can be referenced without qualification from anywhere within the CLRSketcher application. The elements container that the constructor creates stores the sketch. You add elements to a Sketch object by calling its += operator overload function, which calls the push_back() function for the elements container to store the new element. Drawing a sketch is very simple. The Draw() function for the Sketch object iterates over all the elements in the elements container and calls the Draw() function for each of them. To make the current Sketch object a member of the Form1 class, add a member of type Sketch^ with the name sketch to the class. You can add the initialization for sketch to the Form1 constructor:
Available for download on Wrox.com
Form1(void) : drawing(false), firstPoint(0), elementType(ElementType::LINE), color(Color::Black), tempElement(nullptr), sketch(gcnew Sketch()) { InitializeComponent(); // SetElementTypeButtonsState(); SetColorButtonsState(); // } Code snippet Form1.h
1044
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
Don’t forget to add an #include directive for the Sketch.h header at the beginning of the Form1.h header. Now that you have the Sketch class defi ned, you can modify the MouseUp event handler in the Form1 class to add elements to the sketch: private: System::Void Form1_MouseUp(System::Object^ System::Windows::Forms::MouseEventArgs^ { if(!drawing) return; if(tempElement) { sketch += tempElement; tempElement = nullptr; Invalidate(); } drawing = false; }
sender,
e)
The function uses the operator+=() function for the sketch object when tempElement is not nullptr. After resetting tempElement to nullptr, you call the Invalidate() function to redraw the form. The fi nal piece of the jigsaw in creating a sketch is the implementation of the Paint event handler to draw the sketch.
Drawing the Sketch in the Paint Event Handler Amazingly, you need only one extra line of code to draw an entire sketch: private: System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ { Graphics^ g = e->Graphics; sketch->Draw(g); if(tempElement != nullptr) tempElement->Draw(g); }
e)
You pass the Graphics object, g, to the Draw() function for the sketch object to draw the sketch. This will result in g being passed to the Draw() function for each element in the sketch to enable each element to draw itself. Finally, if tempElement is not nullptr, you call the Draw() function for that, too. With the Paint() event handler complete, you should have a working version of CLR Sketcher. A little later in this chapter, you will add the capability to pop up a context menu to allow elements to be moved as in the MFC version of Sketcher, but fi rst, you need to get the element highlighting mechanism working.
Implementing Element Highlighting You want to highlight an element when the mouse cursor is within the element’s bounding rectangle, just like the MFC version of Sketcher. You will implement the mechanism to accomplish this in CLR Sketcher in a slightly different way.
Extending CLR Sketcher
❘ 1045
The MouseMove event handler in Form1 is the prime mover in highlighting elements because it tracks the movement of the cursor, but highlighting should occur only when there is not a drawing operation in progress. The first thing you need to do, before you can modify the MouseMove handler, is to implement a way for an element to draw itself in a highlight color when it is under the cursor. Add highlighted as a public property of the Element class of type bool to record whether an element is highlighted. public: property bool highlighted;
You can also add a protected member to the Element class to specify the highlight color for elements: Color highlightColor;
Add a public constructor to the Element class to initialize the new members: Element() : highlightColor(Color::Magenta) {
highlighted = false;
}
You cannot initialize a property in the initialization list, so it must go in the body of the constructor. As you will see, you will need external access to the bounding rectangle for an element, so you can add a public property to the Element class to provide this: property System::Drawing::Rectangle bound { System::Drawing::Rectangle get() { return boundRect; } }
This makes boundRect accessible without endangering its integrity, because there is no set() function for the bound property. Now, you can change the implementation of the Draw() function in each of the classes derived from Element. Here’s how the function looks for the Line class: virtual void Draw(Graphics^ g) override { pen->Color = highlighted ? highlightColor : color; g->TranslateTransform(safe_cast(position.X), safe_cast(position.Y)); g->DrawLine(pen, 0, 0, end.X, end.Y); g->ResetTransform(); }
The extra statement sets the Color property for pen to highlightColor whenever highlighted is true, or to color otherwise. You can add the same statement to the Draw() function for the other element classes.
Finding the Element to Highlight To highlight an element, you must discover which element, if any, is under the cursor at any given time. It would be helpful if an element could tell you if a given point is within the bounding
1046
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
rectangle. This process will be the same for all types of element, so you can add a public function to the Element base class to implement this: bool Hit(Point p) { return boundRect.Contains(p); }
The Contains() member of the System::Drawing::Rectangle structure returns true if the Point argument lies within the rectangle, and false otherwise. There are two other versions of this function: one accepts two arguments of type int that specify the x and y coordinates of a point, and the other accepts an argument of type System::Drawing::Rectangle and returns true if the rectangle for which the function is called contains the rectangle passed as an argument. You can now add a public function to the Sketch class to determine if any element in the sketch is under the cursor: Element^ HitElement(Point p) { for (auto riter = elements->rbegin(); riter != elements->rend(); ++riter) { if((*riter)->Hit(p)) return *riter; } return nullptr; }
This function iterates over the elements in the sketch in reverse order, starting with the last one added to the container, and returns a reference to the fi rst element for which the Hit() function returns true. If the Hit() function does not return true for any of the elements in the sketch, the HitElement() function returns nullptr. This provides a simple way to detect whether or not there is an element under the cursor.
Highlighting the Element Add a private member, highlightedElement, of type Element^ to the Form1 class to record the currently highlighted element and initialize it to nullptr in the Form1 constructor. Because there’s such a lot of code in the Form1 class, it is best to do this by right- clicking the class name and selecting Add ➪ Add variable. . . from the pop -up. Now, you can add code to the MouseMove handler to make the highlighting work: private: System::Void Form1_MouseMove(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) { if(drawing) { if(tempElement) Invalidate(tempElement->bound); // Invalidate old element area switch(elementType) { case ElementType::LINE:
Extending CLR Sketcher
❘ 1047
tempElement = gcnew Line(color, firstPoint, e->Location); break; case ElementType::RECTANGLE: tempElement = gcnew Rectangle(color, firstPoint, e->Location); break; case ElementType::CIRCLE: tempElement = gcnew Circle(color, firstPoint, e->Location); break; case ElementType::CURVE: if(tempElement) safe_cast(tempElement)->Add(e->Location); else tempElement = gcnew Curve(color, firstPoint, e->Location); break; } Invalidate(tempElement->bound); Update();
// Invalidate new element area // Repaint
} else { // Find the element under the cursor - if any Element^ element(sketch->HitElement(e->Location)); if(highlightedElement == element) // If the old is same as the new return; // there’s nothing to do // Reset any existing highlighted element if(highlightedElement) { Invalidate(highlightedElement->bound); // Invalidate element area highlightedElement->highlighted = false; highlightedElement = nullptr; } // Find and set new highlighted element, if any highlightedElement = element; if(highlightedElement) { highlightedElement->highlighted = true; Invalidate(highlightedElement->bound); // Invalidate element area } Update(); // Send a paint message } }
There’s a change to the original code that is executed to improve performance when drawing is true. The old code redraws the entire sketch to highlight an element when, in fact, only the regions occupied by the un-highlighted element and the newly highlighted element need to be redrawn. The Invalidate() member of the System::Windows::Forms::Form class has an overloaded version that accepts an argument of type System::Drawing::Rectangle to add the rectangle specified by the argument to the currently invalidated region for the form. Thus, you can call Invalidate() repeatedly to accumulate a composite of rectangles to be redrawn. Calling Invalidate() with a rectangle argument does not redraw the form, but you can get the invalid region redrawn by calling the Update() function for the form. You must pass the enclosing rectangle
1048
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
for an element to Invalidate(), rather than the bounding rectangle, to get elements redrawn properly, because the shapes extend beyond the right and bottom edges of the bounding rectangle by the thickness of the pen. You will therefore have to adjust the existing bounding rectangles to take the pen width into account if this is to work properly; you’ll get to that in a moment. You now pass the bounding rectangle of the old temporary element to the Invalidate() function to get just that area added to the region to be repainted. When you have created a new temporary element, you add its bounding rectangle to the region to be repainted. You fi nally call Update() for the Form1 object to send a WM_Paint message. The effect of all this is that only the region occupied by the old and new temporary elements will be repainted, so the operation will be much faster than drawing the whole of the client area. If drawing is false, you execute the highlighting code in the else clause. First, you get a reference to a new element under the cursor by calling the HitElement() function for the sketch. If the return value is the same as highlightedElement, either they are both nullptr or both reference the same element — either way, there’s nothing further to be done. If you don’t return from the handler, you test for an existing highlighted element, and if there is one, you invalidate the rectangle it occupies and reset it by setting the value of its highlighted property to false. You then set highlightedElement back to nullptr. You store the element value in highlightedElement, and if this is not null, you set the highlighted member for the element to true and call Invalidate() with the bounding rectangle as the argument. Finally, you call Update() to redraw the invalid region. The highlight code will execute repeatedly for every movement of the mouse cursor, so it needs to be efficient.
Adjusting the Bounding Rectangle for an Element The existing bounding rectangles for the elements need to be inflated by the width of the pen. The pen width is available as a floating point value, as the Width property of a Pen object. Here’s how you can modify the Line class constructor to inflate the bounding rectangles appropriately: Line(Color color, Point start, Point end) { pen = gcnew Pen(color); this->color = color; position = start; this->end = end - Size(start); boundRect = System::Drawing::Rectangle(Math::Min(position.X, end.X), Math::Min(position.Y, end.Y), Math::Abs(position.X - end.X), Math::Abs(position.Y - end.Y)); int penWidth(safe_cast(pen->Width)); // Pen width as an integer boundRect.Inflate(penWidth, penWidth); }
There are just two additional statements. The fi rst converts the Width property value for the pen to an integer and stores it locally. The second calls Inflate() for the Rectangle object. This inflates the width of the rectangle in both directions by the fi rst argument value, and the height in both directions by the second argument value. Thus, both the width and height will increase by twice the
Extending CLR Sketcher
❘ 1049
pen width. Note that the statements that were there previously to ensure the width and height were greater than zero are no longer required, because both will always be at least twice the pen width. You can add the same two statements to the end of the constructors for the Rectangle and Circle classes. The Curve constructor should be changed to the following: Curve(Color color, Point p1, Point p2) { pen = gcnew Pen(color); this->color = color; points = gcnew vector(); position = p1; points->push_back(p2 - Size(position)); // Find the minimum and maximum coordinates int minX = p1.X < p2.X ? p1.X : p2.X; int minY = p1.Y < p2.Y ? p1.Y : p2.Y; int maxX = p1.X > p2.X ? p1.X : p2.X; int maxY = p1.Y > p2.Y ? p1.Y : p2.Y; int width = maxX - minX; int height = maxY - minY; boundRect = System::Drawing::Rectangle(minX, minY, width, height); int penWidth(safe_cast(pen->Width)); // Pen width as an integer boundRect.Inflate(penWidth, penWidth); }
You also need to change the Add() member of the Curve class: void Add(Point p) { points->push_back(p - Size(position)); // Modify the bounding rectangle to accommodate the new point int penWidth(safe_cast(pen->Width)); // Pen width as an integer boundRect.Inflate(-penWidth, -penWidth); // Reduce by the pen width if(p.X < boundRect.X) { boundRect.Width = boundRect.Right - p.X; boundRect.X = p.X; } else if(p.X > boundRect.Right) boundRect.Width = p.X - boundRect.Left; if(p.Y < boundRect.Y) { boundRect.Height = boundRect.Bottom - p.Y; boundRect.Y = p.Y; } else if(p.Y > boundRect.Bottom) boundRect.Height = p.Y - boundRect.Top; boundRect.Inflate(penWidth, penWidth); }
// Inflate by the pen width
1050
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
Once you have done this, you should fi nd CLR Sketcher works with element highlighting in effect.
Creating Context Menus You use the Design window to create context menus interactively by dragging a ContextMenuStrip control from the Toolbox window to the form. You need to display two different context menus, one for when there is an element under the cursor and one for when there isn’t, and you can arrange for both possibilities using a single context menu strip. First, drag a ContextMenuStrip control from the Toolbox window to the form in the Design window; this will have the default name ContextMenuStrip1, but you can change this if you want. To make the context menu strip display when the form is right- clicked, display the Properties window for the form and set the ContextMenuStrip property in the Behavior group by selecting ContextMenuStrip1 from the drop -down in the value column. The drop -down for the context menu is empty at present, and you are going to control what menu items it contains programmatically. When an element is under the cursor, the element-specific menu items should display, and when there is no element under the cursor, menu items equivalent to those from the Element and Color menus should display. First, add a Send to Back menu item, a Delete menu item, and a Move menu item to the drop-down for the context menu by typing the entries into the design window. Change the (name) properties for the items to sendToBackContextMenuItem, deleteContextMenuItem, and moveContextMenuItem. A menu item can belong to only one menu strip, so you need to add new ones to the context menu matching those in the Element and Color menus. Add a separator and then add menu items Line, Rectangle, Circle, and Curve, and Black, Red, Green, and Blue. Change the (name) property for each in the same way as the others, to lineContextMenuItem, rectangleContextMenuItem, and so on. Also change the (name) property for the separator to contextSeparator so you can refer to it easily. You need Click event handlers for all the menu items in the context menu, but the only new ones you need to create are for the Move, Delete, and Send to Back items; you can set the handlers for all the other menu items to be the same as the corresponding items in the Element and Color menus. You can control what displays in the drop -down for the context menu in the Opening event handler for the context menu strip, because this event handler is called before the drop -down displays. Open the Properties window for the context menu strip by right- clicking it and selecting Properties. Click the Events button in the Properties window and double- click the Opening event to add a handler. You are going to add different sets of menu items to the context menu strip, depending on whether or not an element is under the cursor; so how can you determine if anything is under the cursor? Well, the MouseMove handler for the form has already taken care of it. All you need to do is check to see whether the reference contained in the highlightElement member of the form is nullptr or not. You have added items to the drop-down for contextMenuStrip1 to add the code to create the objects in the Form1 class that encapsulates them, but you want to start with a clean slate
Extending CLR Sketcher
❘ 1051
for the drop -down menu when it is to display, so you can set it up the way you want. In the Opening event handler, you want to remove any existing menu items before you add back the specific menu items you want. A ContextMenuStrip object that encapsulates a context menu stores its drop -down menu items in an Items property that is of type ToolStripItemsCollection^. To remove existing items from the drop -down, simply call Clear() for the Items property. To add a menu item to the context menu, call the Add() function for the Items property with the menu item as the argument. The implementation of the Opening handler for the context menu strip is as follows: private: System::Void contextMenuStrip1_Opening(System::Object^
sender,
System::ComponentModel::CancelEventArgs^ e) { contextMenuStrip1->Items->Clear(); // Remove existing items if(highlightedElement) { contextMenuStrip1->Items->Add(moveContextMenuItem); contextMenuStrip1->Items->Add(deleteContextMenuItem); contextMenuStrip1->Items->Add(sendToBackContextMenuItem); } else { contextMenuStrip1->Items->Add(lineContextMenuItem); contextMenuStrip1->Items->Add(rectangleContextMenuItem); contextMenuStrip1->Items->Add(circleContextMenuItem); contextMenuStrip1->Items->Add(curveContextMenuItem); contextMenuStrip1->Items->Add(contextSeparator); contextMenuStrip1->Items->Add(blackContextMenuItem); contextMenuStrip1->Items->Add(redContextMenuItem); contextMenuStrip1->Items->Add(greenContextMenuItem); contextMenuStrip1->Items->Add(blueContextMenuItem); // Set checks for the menu items lineContextMenuItem->Checked = elementType == ElementType::LINE; rectangleContextMenuItem->Checked = elementType == ElementType::RECTANGLE; circleContextMenuItem->Checked = elementType == ElementType::CIRCLE; curveContextMenuItem->Checked = elementType == ElementType::CURVE; blackContextMenuItem->Checked = color == Color::Black; redContextMenuItem->Checked = color == Color::Red; greenContextMenuItem->Checked = color == Color::Green; blueContextMenuItem->Checked = color == Color::Blue; } }
After clearing the context menu, you add a set of items to it depending on whether or not highlightedElement is null. You set the checks for the element and color menu items in the same way you did for the main menu, by setting the Checked property for each of the menu items. All the element and color menu items in the context menu should now work, so try it out. All that remains is to implement the element-specific operations. Let’s do the easiest one fi rst.
1052
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
Implementing the Element Delete Operation The overloaded -= operator in the Sketch class already provides you with a way to delete an element. You can use the operator-=() function in the implementation of the Click event handler for the Delete menu item: private: System::Void deleteContextMenuItem_Click(System::Object^ sender, System::EventArgs^ e) { if(highlightedElement) { sketch -= highlightedElement; // Delete the highlighted element Invalidate(highlightedElement->bound); highlightedElement = nullptr; Update(); } }
As well as deleting the element from the sketch, the event handler also invalidates the region it occupies to remove it from the display, and resets highlightedElement back to nullptr.
Implementing the “Send to Back” Operation The Send to Back operation works much as in MFC Sketcher. You remove the highlighted element from the list in the Sketch object and add it to the end of the list. Here’s the implementation of the handler function: private: System::Void sendToBackContextMenuItem_Click(System::Object^ sender, System::EventArgs^ { if(highlightedElement) { sketch -= highlightedElement; // Delete the highlighted // element sketch->push_front(highlightedElement); // Then add it back to the // beginning highlightedElement->highlighted = false; Invalidate(highlightedElement->bound); highlightedElement = nullptr; Update(); } }
e)
You delete the highlighted element using the operator-=() function, and then add it back to the beginning of the list using the push_front() function that you will defi ne in the Sketch class in a moment. You reset the highlighted member of the element to false and highlightedElement to nullptr before calling Invalidate() for the form to redraw the region occupied by the previously highlighted element. You can implement push_front() as a public function in the Sketch class defi nition as follows:
Extending CLR Sketcher
❘ 1053
void push_front(Element^ element) { elements->push_front(element); }
Implementing the Element Move Operation You’ll move an element by dragging it with the left mouse button down. This implies that a move operation needs a different set of functions from normal to be carried out by the mouse event handlers, so a move will have to be modal, as in the MFC version of Sketcher. You can identify possible modes with an enum class that you can add following the ElementType enum in Form1.h: enum class Mode {Normal, Move};
This defi nes just two modes, but you could add more if you wanted to implement other modal operations. You can now add a private member of type Mode with the name mode to the Form1 class. Initialize the new variable to Mode::Normal in the Form1 constructor. The only thing the Click event handler for the Move menu item has to do is set mode to Mode::Move: private: System::Void moveContextMenuItem_Click(System::Object^
sender,
System::EventArgs^ e) { mode = Mode::Move; }
The mouse event handlers for the form must take care of moving an element. Change the code for the MouseDown handler so it sets drawing to true only when mode is Mode::Normal: private: System::Void Form1_MouseDown(System::Object^
sender,
System::Windows::Forms::MouseEventArgs^ e) { if(e->Button == System::Windows::Forms::MouseButtons::Left) { if(mode == Mode::Normal) { drawing = true; } firstPoint = e->Location; } }
This sets drawing to true only when mode has the value Mode::Normal. If mode has a different value, only the point is stored for use in the MouseMove event handler. The MouseMove event handler will not create elements when drawing is false, so this functionality is switched off for all modes except Mode::Normal. The MouseUp event handler will restore mode to Mode::Normal when a move operation is complete.
1054
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
Moving an Element Because you now draw all elements relative to a given position, you can move an element just by changing the inherited position member to reflect the new position and to adjust the location of the bounding rectangle in a similar way. You can add a public Move() function to the Element base class to take care of this: void Move(int dx, int dy) { position.Offset(dx, dy); boundRect.X += dx; boundRect.Y += dy; }
Calling the Offset() function for the Point object, position, adds dx and dy to the coordinates stored in the object. To adjust the location of boundRect, just add dx to the Rectangle object’s X field and dy to its Y field. The MouseMove event handler will expedite the moving process: private: System::Void Form1_MouseMove(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) { if(drawing) { if(tempElement) Invalidate(tempElement->Bound); // The old element region switch(elementType) { // Code to create a temporary element as before... } Invalidate(tempElement->bound); // The new element region Update(); } else if(mode == Mode::Normal) { // Code to highlight the element under the cursor as before } else if(mode == Mode::Move && e->Button == System::Windows::Forms::MouseButtons::Left) { // Move the highlighted element if(highlightedElement) { Invalidate(highlightedElement->bound); // Region before move highlightedElement->Move(e->X - firstPoint.X, e->Y - firstPoint.Y); firstPoint = e->Location; Invalidate(highlightedElement->bound); // Region after move Update(); } } }
This event handler now provides three different functions: it creates an element when drawing is true, it does element highlighting when drawing is false and mode is Mode::Normal, and it moves
Extending CLR Sketcher
❘ 1055
the currently highlighted element when mode is Mode::Move and the left mouse button is down. This means that moving an element in this version of Sketcher will work a little differently from in the native version. To move an element, you select Move from the context menu, then drag the element to its new position with the left button down. The new else if clause executes only when mode has the value Mode::Move and the left button is down. Without the button test, moving would occur if you moved the cursor after clicking the menu item. This would happen without a MouseDown event occurring, which would create some confusion, as firstPoint would not have been initialized. With the code as you have it here, the moving process is initiated when you click the menu item and release the left mouse button. You then press the left mouse button and drag the cursor to move the highlighted element. If there is a highlighted element, you invalidate its bounding rectangle before moving the element the distance from firstPoint to the current cursor location. You then update firstPoint to the current cursor position, ready for the next move increment, and invalidate the new region the highlighted element occupies. You finally call Update() to redraw the invalid regions of the form. The last part you must implement to complete the ability to move elements is in the MouseUp event handler: private: System::Void Form1_MouseUp(System::Object^ System::Windows::Forms::MouseEventArgs^ e) { if(!drawing) { mode = Mode::Normal; return; } if(tempElement) { sketch += tempElement; tempElement = nullptr; Invalidate(); } drawing = false; }
Releasing the left mouse button ends a move operation. The drawing variable is false when an element is being moved, and the only thing the handler has to do when this is the case is reset mode to Mode::Normal to end the move operation. With that done, you should now have a version of CLR Sketcher in which you can move and delete elements, as illustrated in Figure 17-8. The Send to Back facility ensures that any element can always be highlighted to be moved or deleted; if an element will not highlight,
FIGURE 17-8
sender,
1056
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
just Send to Back any elements enclosing the element you want to move or delete. In Chapter 19, you will implement the capability to save a sketch in a fi le.
SUMMARY In this chapter, you’ve seen how to apply STL collection classes to the problems of managing objects and managing pointers to objects. Collections are a real asset in programming for Windows, because the application data you store in a document often originates in an unstructured and unpredictable way, and you need to be able to traverse the data whenever a view needs to be updated. You have also seen how to create document data and manage it in a pointer list in the document, and, in the context of the Sketcher application, how the views and the document communicate with each other. You have improved the view capability in Sketcher in several ways. You’ve added scrolling to the views using the MFC class CScrollView, and you’ve introduced a pop -up at the cursor for moving and deleting elements. You have also implemented an element-highlighting feature to provide the user with feedback during the moving or deleting of elements. CLR Sketcher has most of the features of MFC Sketcher, including drawing operations and a context menu, but you implemented some things a little differently because the characteristics of the CLR are not the same as the characteristics of MFC. The coding effort was considerably less than for MFC Sketcher because of the automatic code generation provided by the Forms Designer capability. The downside of using Forms Designer is that you can’t control how the code is organized, and the Form1 class becomes rather unwieldy because of the sheer volume of code in the defi nition. However, the Class View is a great help in navigating around the class defi nition.
Summary
❘ 1057
EXERCISES You can download the source code for the examples in the book, and the solutions to the following exercises, from www.wrox.com.
1.
Implement the CCurve class so that points are added to the head of a list instead of the back of a vector.
2.
Implement the CCurve class in the Sketcher program using list of pointers instead of a vector of objects to represent a curve.
3.
Look up the CArray template collection class in Help, and use it to store points in the CCurve class in the Sketcher program.
4.
Add the capability in CLR Sketcher to change the color of an existing element. Make this capability available through the context menu.
1058
❘
CHAPTER 17
CREATING THE DOCUMENT AND IMPROVING THE VIEW
WHAT YOU LEARNED IN THIS CHAPTER TOPIC
CONCEPT
Windows coordinate systems
When you draw in a device context using the MFC, coordinates are in logical units that depend on the mapping mode set. Points in a window that are supplied along with Windows mouse messages are in client coordinates. The two coordinate systems are usually not the same.
Screen coordinates
Coordinates that define the position of the cursor are in screen coordinates that are measured in pixels relative to the upper-left corner of the screen.
Converting between coordinate systems
Functions to convert between client coordinates and logical coordinates in an MFC application are available in the CDC class.
WM_PAINT messages
Windows requests that a view be redrawn by sending a WM_PAINT message to your MFC application. This causes the OnDraw() member of the affected view to be called.
The OnDraw() function
You should always do any permanent drawing of a document in the OnDraw() member of the view class in your MFC application. This ensures that the window is drawn properly when that is required by Windows.
Drawing efficiently
You can make your OnDraw() implementation more efficient by calling the RectVisible() member of the CDC class to check whether an entity needs to be drawn.
Updating multiple views
To get multiple views updated when you change the document contents, you can call the UpdateAllViews() member of the document object. This causes the OnUpdate() member of each view to be called.
Updating efficiently
You can pass information to the UpdateAllViews() function to indicate which area in the view needs to be redrawn. This makes redrawing the views faster.
Context menus
You can display a context menu at the cursor position in an MFC application in response to a right mouse click. This menu is created as a normal pop - up.
Menus and toolbars in Windows Forms applications
You create menu strips, context menus, and toolbars in a Windows Forms application by dragging the appropriate component from the Toolbox window to the form in the Forms Designer window. You cause the context menu to be displayed on the form in response to a right click by setting the ContextMenuStrip property for the form.
Creating event handlers in Windows Forms applications
You create event handlers for a component in a Windows Forms application through the Properties window for the component. You can change the name of the event handler function that is created by changing the value of its (name) property.
Controlling what is displayed in a menu
Implementing the DropDownOpening event handler for a menu item enables you to modify the drop - down before it displays.
18
Working with Dialogs and Controls WHAT YOU WILL LEARN IN THIS CHAPTER:
➤
How to create dialog resources
➤
How to add controls to a dialog
➤
The basic varieties of controls available
➤
How to create a dialog class to manage a dialog
➤
How to program the creation of a dialog box, and how to get information back from the controls in it
➤
Modal and modeless dialogs
➤
How to implement and use direct data exchange and validation with controls
➤
How to implement view scaling
➤
How to add a status bar to an application
Dialogs and controls are basic tools for user communication in the Windows environment. In this chapter you’ll learn how to implement dialogs and controls by applying them to extend the Sketcher program.
UNDERSTANDING DIALOGS Of course, dialog boxes are not new to you. Most Windows programs of consequence use dialogs to manage some of their data input. You click a menu item and up pops a dialog box with various controls that you use for entering information. Just about everything that appears
1060
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
in a dialog box is a control. A dialog box is actually a window and, in fact, each of the controls in a dialog is also a specialized window. Come to think of it, most things you see on the screen under Windows are windows. There are two things needed to create and display a dialog box in an MFC program: the physical appearance of the dialog box, which is defi ned in a resource fi le, and a dialog class object, used to manage the operation of the dialog and its controls. MFC provides a class called CDialog for you to use after you have defi ned your dialog resource.
UNDERSTANDING CONTROLS Many different controls are available to you in Windows, and in most cases there’s flexibility to how they look and operate. Most of them fall into one of the six categories shown in the following table.
CONTROL TYPE
WHAT THEY DO
Static controls
These are used to provide titles or descriptive information.
Button controls
Buttons provide a single - click input mechanism. There are basically three flavors of button controls: simple push buttons, radio buttons (of which only one may be in a selected state at any one time), and checkboxes (of which several may be in a selected state at one time).
Scrollbars
Scrollbars are typically used to scroll text or images, either horizontally or vertically, within another control.
List boxes
These present a list of choices of which one or more selections can be in effect at one time.
Edit controls
Edit controls allow text input or editing of text that is displayed.
Combo boxes
Combo boxes present a list of choices from which you can select, combined with the option of entering text yourself.
A control may or may not be associated with a class object. Static controls don’t do anything directly, so an associated class object may seem superfluous; however, there’s an MFC class, CStatic, that provides functions to enable you to alter the appearance of static controls. Button controls can also be handled by the dialog object in many cases, but again, MFC does provide the CButton class for use in situations where you need a class object to manage a control. MFC also provides a full complement of classes to support the other controls. Because controls are windows, they are all derived from CWnd.
Creating a Dialog Resource
❘ 1061
COMMON CONTROLS The set of standard controls supported by MFC and the Resource editor are called common controls. Common controls include all the controls you have just seen, as well as other more complex ones such as the animate control, for example, which has the capability to play an AVI (Audio Video Interleaved) file, and the tree control, which can display a hierarchy of items in a tree. Another useful control in the set of common controls is the spin button. You can use this to increment or decrement values in an associated edit control. To go into all of the possible controls that you might use is beyond the scope of this book, so I’ll just take a few illustrative examples (including one that uses a spin button) and implement them in the Sketcher program.
CREATING A DIALOG RESOURCE Here’s a concrete example. You could add a dialog to Sketcher to provide a choice of pen widths for drawing elements. This ultimately involves modifying the current pen width in the document, as well as in the CElement class, and adding or modifying functions to manage pen widths. You’ll deal with all that, though, after you’ve gotten the dialog together. Display the Resource View, expand the resource tree for Sketcher by clicking 䉯 twice, and rightclick the Dialog folder in the tree; then click Insert Dialog from the pop -up to add a new dialog resource to Sketcher. This results in the Dialog Resource editor swinging into action and displaying the dialog in the Editor pane, and the Toolbox showing a list of controls that you can add; if the Toolbox is not displayed, click Toolbox in the right-hand sidebar or press Ctrl+Alt+X. The dialog has OK and Cancel button controls already in place. Adding more controls to the dialog is simplicity itself: you can just drag the control from the list in the Toolbox window to the position at which you want to place it in the dialog. Alternatively, you can click a control from the list to select it, and then click in the dialog where you want the control to be positioned. When it appears you’ll still be able to move it around to set its exact position, and you’ll also be able to resize it by dragging handles on the boundaries. The dialog has a default ID assigned, IDD_DIALOG1, but it would be better to have an ID that ’s a bit more meaningful. You can edit the ID by right- clicking the dialog name in the Resource View pane and selecting Properties from the pop -up; this displays the properties for the dialog node. Change the ID to something that relates to the purpose of the dialog, such as IDD_PENWIDTH_DLG. You can also display the properties for the dialog itself by right- clicking in the Dialog Editor pane and selecting from the pop -up. Here you can change the Caption property value that appears in the title bar of the dialog window to Set Pen Width.
1062
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
Adding Controls to a Dialog Box To provide a mechanism for entering a pen width, you can add controls to the basic dialog that is displayed initially until it looks like the one shown in Figure 18-1. The figure shows the grid that you can use to position controls. If the grid is not displayed, you can select the appropriate toolbar button to display it; the toolbar button toggles the grid on and off. Alternatively, you can display rules along the side and top of the dialog, which you can use to create FIGURE 18-1 guide lines, by clicking the Toggle Guides button. You create a horizontal guide by clicking in the appropriate rule where you want the guide to appear. Controls placed in contact with a guide will be attached to it and move with the guide. You can reposition a guide line by dragging the arrow for it along the rule. You can use one or more guides when positioning a control. You can toggle guides on and off by clicking the Toggle Guides toolbar button. The dialog shown has six radio buttons that provide the pen width options. These are enclosed within a group box with the caption Pen Widths. The group box serves to enclose the radio buttons and make them operate as a group, for which only one member of the group can be checked at any given time. Each radio button has an appropriate label to identify the pen width that is set when it is selected. There are also the default OK and Cancel buttons that close the dialog. Each of the controls in the dialog has its own set of properties that you can access and modify, just as for the dialog box itself. Let’s press on with putting the dialog together. The next step in the creation of the dialog shown in Figure 18 -1 is to add the group box. As I said, the group box serves to associate the radio buttons in a group from an operational standpoint, and to provide a caption and a boundary for the group of buttons. Where you need more than one set of radio buttons, a means of grouping them is essential if they are to work properly. You can select the button corresponding to the group box from the common controls palette by clicking it; then click the approximate position in the dialog box where you want to place the center of the group box. This places a group box of default size onto the dialog. You can then drag the borders of the group box to enlarge it to accommodate the six radio buttons that you add. To set the caption for the group box, type the caption you want while the group box is selected (in this case, type Pen Widths). The last step is to add the radio buttons. Select the radio button control by clicking it, and then clicking the position in the dialog where you want to place a radio button within the group box. Do the same for all six radio buttons. You can select each button by clicking it; then type in the caption to change it. You can also drag the border of the button to set its size, if necessary. To display the Properties window for a control, select it by right- clicking it; then select Properties from the pop -up. You can change the ID for each radio button in the Properties window for the control, in order to make the ID correspond better to its purpose: IDC_PENWIDTH0 for the one-pixel-width pen, IDC_PENWIDTH1 for the 0.01-inch-width pen, IDC_PENWIDTH2 for the 0.02-inch-pen, and so on.
Programming for a Dialog
❘ 1063
You can position individual controls by dragging them around with the mouse. You can also select a group of controls by selecting successive controls with the Shift key pressed, or by dragging the cursor with the left button pressed to create an enclosing rectangle. To align a group of controls, or to space them evenly horizontally or vertically, select the appropriate button from the Dialog Editor toolbar. If the Dialog Editor toolbar is not visible, you can show it by right- clicking in the toolbar area and selecting it from the list of toolbars that is displayed. You can also align controls in the dialog by selecting from the Format menu.
Testing the Dialog The dialog resource is now complete. You can test it by selecting the toolbar button that appears at the left end of the toolbar or by pressing Ctrl+T. This displays the dialog window with the basic operations of the controls available, so you can try clicking on the radio buttons. When you have a group of radio buttons, only one can be selected at a time. As you select one, any other that was previously selected is reset. Click either the OK or Cancel button, or even the close icon in the title bar of the dialog, to end the test. After you have saved the dialog resource, you’re ready to add some code to support it.
PROGRAMMING FOR A DIALOG There are two aspects to programming for a dialog: getting it displayed, and handling the effects of its controls. Before you can display the dialog corresponding to the resource you’ve just created, you must fi rst defi ne a dialog class for it. The Class Wizard helps with this.
Adding a Dialog Class Right- click in the Resource Editor pane for the dialog and then select Add Class from the pop -up to display the Class Wizard dialog. You’ll defi ne a new dialog class derived from the MFC class CDialog, so select that class name from the “Base class” drop -down list box, if it’s not already selected. You can enter the class name as CPenDialog in the Class name edit box. The Class Wizard dialog should look as shown in Figure 18-2. Click the Finish button to create the new class. The CDialog class is a window class (derived from the MFC class CWnd) that’s specifically for displaying and managing dialogs. The dialog resource that you have created
FIGURE 18-2
1064
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
automatically associates with an object of type CPenDialog because the IDD class member is initialized with the ID of the dialog resource: class CPenDialog : public CDialog { DECLARE_DYNAMIC(CPenDialog) public: CPenDialog(CWnd* pParent = NULL); virtual ~CPenDialog();
// standard constructor
// Dialog Data enum { IDD = IDD_PENWIDTH_DLG }; protected: virtual void DoDataExchange(CDataExchange* pDX);
// DDX/DDV support
DECLARE_MESSAGE_MAP() };
The boldfaced statement defi nes IDD as a symbolic name for the dialog ID in the enumeration. Incidentally, using an enumeration is one of two ways to get an initialized data member into a native C++ class defi nition. The other way is to defi ne a static const integral member of the class, so the Class Wizard could have used the following in the class defi nition: static const int IDD = IDD_PENWIDTH_DLG;
If you try putting an initial value for any regular non-static data member declaration in a class, it won’t compile. Having your own dialog class derived from CDialog means that you get all the functionality that that class provides. You can also customize the dialog class by adding data members and functions to suit your particular needs. You’ll often want to handle messages from controls within the dialog class, although you can also choose to handle them in a view or a document class if this is more convenient.
Modal and Modeless Dialogs There are two different types of dialogs, modal and modeless, and they work in completely different ways. While a modal dialog is displayed, all operations in the other windows in the application are suspended until the dialog box is closed, usually by the user clicking an OK or Cancel button. With a modeless dialog you can move the focus back and forth between the dialog window and other windows in your application just by clicking them, and you can continue to use the dialog at any time until you close it. The Class Wizard is an example of a modal dialog; the Properties window is modeless. You can create a modeless dialog box by calling the Create() member of the CDialog class in your dialog class constructor. You create a modal dialog box by creating an object on the stack from your dialog class and calling its DoModal() function.
Programming for a Dialog
❘ 1065
Displaying a Dialog Where you put the code to display a dialog in your program depends on the application. In the Sketcher program, it will be convenient to add a menu item that, when selected, results in the pen width dialog’s being displayed. You can put this item in the IDR_SketcherTYPE menu bar. As both the pen width and the drawing color are associated with a pen, you can rename the Color menu as Pen. You do this just by double- clicking the Color menu item in the Resource Editor pane to open its Properties window and changing the value of the Caption property to &Pen. When you add the Width menu item to the Pen menu, it would be a good idea to separate it from the colors in the menu. You can add a separator after the last color menu item by right- clicking the empty menu item and selecting the Insert Separator menu item from the pop-up. You can then enter the new Width item as the next menu item after the separator. The Width menu item ends with an ellipsis (three periods) to indicate that it displays a dialog; this is a standard Windows convention. Double- click the menu to display the menu properties for modification, as shown in Figure 18-3. The default ID, ID_PEN_WIDTH, is fi ne, so you FIGURE 18-3 don’t need to change that. You can add a status bar prompt for the menu item, and because you’ll also add a toolbar button, you can include text for the tooltip as well. Remember, you just put the tooltip text after the status bar prompt text, separated from it by \n. Here, the value for the Prompt property is “Change pen width\nShow pen width options.” You need to add toolbar buttons to both toolbars corresponding to the Width menu item. To add the toolbar button to the toolbar that is displayed when you check “Large icons” in the Customize dialog for the application toolbar, open the toolbar resource by extending the Toolbar folder in the Resource View and double - clicking IDR_MAINFRAME_256. You can add a toolbar button to represent a pen width. The one shown in Figure 18 - 4 tries to represent a pen drawing a line.
FIGURE 18-4
To associate the new button with the menu item that you just added, open the Properties box for the button and specify its ID as ID_PEN_WIDTH, the same as that for the menu item. You then need to repeat the process for the IDR_MAINFRAME toolbar.
1066
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
Code to Display the Dialog The code to display the dialog goes in the handler for the Pen ➪ Width menu item. So, in which class should you implement this handler? The CSketcherView class is a candidate for dealing with pen widths, but following the previous logic with colors and elements, it would be sensible to have the current pen width selection in the document, so the handler should go in the CSketcherDoc class. Right- click the Width menu item in the Resource View pane for the IDR_SketcherTYPE menu and select Add Event Handler from the pop -up. You can then create a function for the COMMAND message handler corresponding to ID_PEN_WIDTH in the CSketcherDoc class. Now edit this handler and enter the following code: // Handler for the pen width menu item void CSketcherDoc::OnPenWidth() { CPenDialog aDlg; // Create a local dialog object // Display the dialog as modal aDlg.DoModal(); }
There are just two statements in the handler at the moment. The fi rst creates a dialog object that is automatically associated with your dialog resource. You then display the dialog by calling the DoModal() function for the aDlg object. Because the handler creates a CPenDialog object, you must add a #include directive for PenDialog.h to the beginning of SketcherDoc.cpp (after the #include directives for stdafx.h and Sketcher.h); otherwise, you’ll get compilation errors when you build the program. After you’ve done that, you can build Sketcher and try out the dialog. It should appear when you click the pen-width toolbar button. Of course, if the dialog is to do anything, you still have to add the code to support the operation of the controls; to close the dialog, you can use either of the buttons or the close icon in the title bar.
Code to Close the Dialog The OK and Cancel buttons (and the close icon on the title bar) already close the dialog. The handlers to deal with the BN_CLICKED event handlers for the OK and Cancel button controls have been implemented for you. However, it’s useful to know how the action of closing the dialog is implemented, in case you want to do more before the dialog is fi nally closed, or if you are working with a modeless dialog. The CDialog class defi nes the OnOK() method that is called when you click the default OK button, which has IDOK as its ID. This function closes the dialog and causes the DoModal() method to return the ID of the default OK button, IDOK. The OnCancel() function is called when you click the default Cancel button in the dialog; this closes the dialog, and DoModal() returns the button ID, which is IDCANCEL. You can override either or both of these functions in your dialog class to do what you want. You just need to make sure you call the corresponding base class function at the end of your function implementation. You’ll probably remember by now that you can add an override class by clicking the override button in the Properties window for the class. For example, you could implement an override for the OnOK() function as follows:
Supporting the Dialog Controls
❘ 1067
void CPenDialog::OnOK() { // Your code for data validation or other actions... CDialog::OnOK();
// Close the dialog
}
In a complicated dialog, you might want to verify that the options selected, or the data that has been entered, is valid. You could put code here to check the state of the dialog and fi x up the data, or even to leave the dialog open if there are problems. Calling the OnOK()function defi ned in the base class closes the dialog and causes the DoModal() function to return IDOK. Thus, you can use the value returned from DoModal() to detect when the dialog was closed via the OK button. As I said, you can also override the OnCancel() function in a similar way if you need to do extra cleanup operations before the dialog closes. Be sure to call the base class method at the end of your function implementation. When you are using a modeless dialog you must implement the OnOK() and OnCancel() function overrides so that they call the inherited DestroyWindow() to terminate the dialog. In this case, you must not call the base class OnOK() or OnCancel() functions, because they do not destroy the dialog window, but merely render it invisible.
SUPPORTING THE DIALOG CONTROLS For the pen dialog you’ll store the selected pen width in a data member, m_PenWidth, of the CPenDialog class. You can either add the data member by right- clicking the CPenDialog class name and selecting from the context menu, or you can add it directly to the class defi nition as follows: class CPenDialog : public CDialog { // Construction public: CPenDialog(CWnd* pParent = NULL);
// standard constructor
// Dialog Data enum { IDD = IDD_PENWIDTH_DLG }; int m_PenWidth;
// Record the current pen width
// Plus the rest of the class definition.... };
NOTE If you do use the context menu for the class to add m_PenWidth, be sure to add a comment to the member variable definition. This is a good habit to get into, even when the member name looks self- explanatory.
1068
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
You’ll use the m_PenWidth data member to set as checked the radio button corresponding to the current pen width in the document. You’ll also arrange for the pen width selected in the dialog to be stored in this member, so that you can retrieve it when the dialog closes. At this point you could arrange to initialize m_PenWidth to 0 in the CPenDialog class constructor.
Initializing the Controls You can initialize the radio buttons by overriding the OnInitDialog() function defi ned in the base class, CDialog. This function is called in response to a WM_INITDIALOG message, which is sent during the execution of DoModal() just before the dialog box is displayed. You can add the function to the CPenDialog class by selecting OnInitDialog in the list of overrides in the Properties window for the CPenDialog class. The implementation for the new version of OnInitDialog() is: BOOL CPenDialog::OnInitDialog() { CDialog::OnInitDialog(); // Check the radio button corresponding to the pen width switch(m_PenWidth) { case 1: CheckDlgButton(IDC_PENWIDTH1,1); break; case 2: CheckDlgButton(IDC_PENWIDTH2,1); break; case 3: CheckDlgButton(IDC_PENWIDTH3,1); break; case 4: CheckDlgButton(IDC_PENWIDTH4,1); break; case 5: CheckDlgButton(IDC_PENWIDTH5,1); break; default: CheckDlgButton(IDC_PENWIDTH0,1); } return TRUE; // return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE }
You should leave the call to the base class function there because it does some essential setup for the dialog. The switch statement checks one of the radio buttons, depending on the value set in the m_PenWidth data member. This implies that you must arrange to set m_PenWidth to a suitable value before you execute DoModal() because the DoModal() function causes the WM_INITDIALOG message to be sent, resulting in your version of OnInitDialog() being called. The CheckDlgButton() function is inherited indirectly from CWnd through CDialog. The first argument identifies the button, and the second argument, of type UINT, sets its check status. If the second argument is 1, it checks the button corresponding to the ID you specify in the first argument. If the second argument is 0, the button is unchecked. This function works with both checkboxes and radio buttons.
Completing Dialog Operations
❘ 1069
Handling Radio Button Messages After the dialog box is displayed, every time you click one of the radio buttons a message is generated and sent to the application. To deal with these messages, you can add handlers to the CPenDialog class. Return to the dialog resource that you created, rightclick each of the radio buttons in turn, and select Add Event Handler from the pop -up to create a handler for the BN_CLICKED message. Figure 18-5 shows the event handler dialog window for the button that has IDC_PENWIDTH0 as its ID. Note that I have edited the name of the handler, as the default name was a little cumbersome. The implementations of the BN_CLICKED event handlers for all of these radio buttons FIGURE 18-5 are similar because each just sets the pen width in the dialog object. As an example, the handler for IDC_PENWIDTH0 is as follows: void CPenDialog::OnPenwidth0() { m_PenWidth = 0; }
You need to add the code for all six handlers to the CPenDialog class implementation, setting m_PenWidth to 1 in OnPenwidth1(), to 2 in OnPenwidth2(), and so on.
COMPLETING DIALOG OPERATIONS You must now modify the OnPenWidth() handler in CSketcherDoc to make the dialog effective. Add the following code to the function: // Handler for the pen width menu item void CSketcherDoc::OnPenWidth() { CPenDialog aDlg; // Create a local dialog object // Set the pen width in the dialog to that stored in the document aDlg.m_PenWidth = m_PenWidth; // Display the dialog as modal // When closed with OK, get the pen width if(aDlg.DoModal() == IDOK) { m_PenWidth = aDlg.m_PenWidth; } }
1070
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
The m_PenWidth member of the aDlg object is passed a pen width stored in the m_PenWidth member of the document; you still have to add this member to CSketcherDoc. The call of the DoModal() function now occurs in the condition of the if statement, which is true if the DoModal() function returns IDOK. In this case you retrieve the pen width stored in the aDlg object and store it in the m_PenWidth member of the document. If the dialog box is closed by means of the Cancel button, the escape key on the keyboard, or the close icon, IDOK won’t be returned by DoModal(), and the value of m_PenWidth in the document will not be changed. Note that even though the dialog box is closed when DoModal() returns a value, the aDlg object still exists, so you can call its member functions without any problem. The aDlg object is destroyed automatically, on return from OnPenWidth(). All that remains to do to support variable pen widths in your application is to update the affected classes: CSketcherDoc, CElement, and the four shape classes derived from CElement.
Adding Pen Widths to the Document You need to add the m_PenWidth member to the document class, and the GetPenWidth() function to allow external access to the value stored. You should add the following bolded statements to the CSketcherDoc class defi nition: class CSketcherDoc : public CDocument { // the rest as before... protected: // the rest as before... int m_PenWidth; // Operations public: // the rest as before... int GetPenWidth() const { return m_PenWidth; }
// Current pen width
// Get the current pen width
// the rest as before... };
Because it’s trivial, you can defi ne the GetPenWidth() function in the defi nition of the class and gain the benefit of its being implicitly inline. You still need to add initialization for m_PenWidth to the constructor for CSketcherDoc, so modify the constructor in SketcherDoc.cpp to initialize m_PenWidth to 0.
Adding Pen Widths to the Elements You have a little more to do to the CElement class and the shape classes that are derived from it. You already have a member m_PenWidth in CElement to store the width to be used when you are drawing an element, and you must extend each of the constructors for elements to accept a pen width as an argument, and set the member in the class accordingly. The GetBoundRect() function
Completing Dialog Operations
❘ 1071
in CElement must be altered to deal with a pen width of 0. You can modify the CElement class fi rst. The new version of the GetBoundRect() function in the CElement class is as follows: // Get the bounding rectangle for an element CRect CElement::GetBoundRect() const { CRect boundingRect(m_EnclosingRect); // Object to store bounding rectangle //Increase the bounding rectangle by the pen width int Offset = m_PenWidth == 0 ? 1 : m_PenWidth; // Width must be at least 1 boundingRect.InflateRect(Offset, Offset); return BoundingRect; }
You use the local variable Offset to ensure that you pass the InflateRect() function a value of 1 if the pen width is 0 (a pen width of 0 always draws a line one pixel wide), and that you pass the actual pen width in all other cases. Each of the constructors for CLine, CRectangle, CCircle, and CCurve must be modified to accept a pen width as an argument, and to store it in the inherited m_PenWidth member of the class. The declaration for the constructor in each class defi nition needs to be modified to add the extra parameter. For example, in the CLine class, the constructor declaration becomes: CLine(const CPoint& start, const CPoint& end, COLORREF aColor, int penWidth);
And the constructor implementation should be modified to this: CLine::CLine(const CPoint& start, const CPoint& end, COLORREF aColor, int penWidth) :m_StartPoint(start), m_EndPoint(end) { m_PenWidth = penWidth; // Set pen width m_Color = aColor; // Set line color // Define the enclosing rectangle m_EnclosingRect = CRect(Start, End); m_EnclosingRect.NormalizeRect(); }
You should modify each of the class defi nitions and constructors for the shapes in the same way, so that each initializes m_PenWidth with the value passed as the last argument.
Creating Elements in the View The last change you need to make is to the CreateElement() member of CSketcherView. Because you have added the pen width as an argument to the constructors for each of the shapes, you must update the calls to the constructors to reflect this. Change the defi nition of CSketcherView:: CreateElement() to the following: CElement* CSketcherView::CreateElement() { // Get a pointer to the document for this view
1072
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
CSketcherDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc);
// Verify the pointer is good
// Now select the element using the type stored in the document switch(pDoc->GetElementType()) { case RECTANGLE: return new CRectangle(m_FirstPoint, m_SecondPoint, pDoc->GetElementColor(), pDoc->GetPenWidth()); case CIRCLE: return new CCircle(m_FirstPoint, m_SecondPoint, pDoc->GetElementColor(), pDoc->GetPenWidth()); case CURVE: return new CCurve(m_FirstPoint, m_SecondPoint, pDoc->GetElementColor(), pDoc->GetPenWidth()); case LINE: return new CLine(m_FirstPoint, m_SecondPoint, pDoc->GetElementColor(), pDoc->GetPenWidth()); default: // Something’s gone wrong AfxMessageBox(_T(“Bad Element code”), MB_OK); AfxAbort(); return nullptr; } }
Each constructor call now passes the pen width as an argument. This is retrieved from the document with the GetPenWidth() function that you added to the document class.
Exercising the Dialog You can now build and run the latest version of Sketcher to see how the pen dialog works out. Selecting the Pen ➪ Width menu option or the associated toolbar button displays the dialog box so that you can select the pen width. The screen shown in Figure 18-6 is typical of what you might see when the Sketcher program is executing. Note that the dialog box is a completely separate window. You can drag it around to position it where you want. You can even drag it outside the Sketcher application window. FIGURE 18- 6
USING A SPIN BUTTON CONTROL Now you can move on to looking at how the spin button can help in the Sketcher application. The spin button is particularly useful when you want to constrain an input within a given integer range. It’s normally used in association with another control, called a buddy control, that displays the value that the spin button modifies. The associated control is usually an edit control, but it doesn’t have to be.
Using a Spin Button Control
❘ 1073
It would be nice to be able to draw at different viewing scales in Sketcher. If you had a way to change the scale, you could scale up whenever you wanted to fi ll in the fi ne detail in your masterpiece, and scale down again when working across the whole vista. You could apply the spin control to managing scaling in a document view. A drawing scale would be a view-specific property, and you would want the element drawing functions to take the current scale of a view into account. Altering the existing code to deal with view scaling requires rather more work than setting up the control, so fi rst look at how you create a spin button and make it work.
Adding the Scale Menu Item and Toolbar Button The fi rst step is to provide a means of displaying the scale dialog. Go to Resource View and open the IDR_SketcherTYPE menu. You are going to add a Scale menu item to the end of the View menu. Enter the caption for the unused menu item as Scale. . . . This item will bring up the scale dialog, so you end the caption with an ellipsis (three periods) to indicate that it displays a dialog. Next you can add a separator before the new menu item by right- clicking it and selecting Insert Separator from the pop-up. The menu should now look as shown in Figure 18-7.
FIGURE 18-7
You can also add toolbar buttons to both toolbars for this menu item. All you need to do is make sure that the ID for each new button is also set to ID_VIEW_SCALE.
Creating the Spin Button You’ve got the menu item; you better have a dialog to go with it. In Resource View, add a new dialog by right- clicking the Dialog folder on the tree and selecting Insert Dialog from the pop -up. Change the ID to IDD_SCALE_DLG and the Caption property to Set Drawing Scale. Click the spin control in the palette, and then click on the position in the dialog where you want it to be placed. Next, right- click the spin control to display its properties. Change its ID to something more meaningful than the default, such as IDC_SPIN_SCALE. Now take at look at the properties for the spin button. They are shown in Figure 18-8.
FIGURE 18-8
1074
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
The Arrow Keys property is already set to True, enabling you to operate the spin button by using arrow keys on the keyboard. You should also set to true the value for both the Set Buddy Integer property, which specifies the buddy control value as an integer, and Auto Buddy, which provides for automatic selection of the buddy control. The effect of the latter is that the control selected as the buddy is automatically the previous control defi ned in the dialog. At the moment this is the Cancel button, which is not exactly ideal, but you’ll see how to change this in a moment. The Alignment property determines how the spin button is displayed in relation to its buddy. You should set this to Right Align so that the spin button is attached to the right edge of its buddy control. Next, add an edit control at the left side of the spin button by selecting the edit control from the list in the toolbox pane and clicking in the dialog where you want it positioned. Change the ID for the edit control to IDC_SCALE. To make the contents of the edit control quite clear, you could add a static control just to the left of the edit control in the dialog and enter View Scale: as the caption. You can select all three controls by clicking them while holding down the Shift key. Pressing the F9 function key aligns the controls tidily, or you can use the Format menu.
The Controls’ Tab Sequence Controls in a dialog have what is called a tab sequence. This is the sequence in which the focus shifts from one control to the next when you press the tab key, determined initially by the sequence in which controls are added to the dialog. You can see the tab sequence for the current dialog box by selecting Format ➪ Tab Order from the main menu, or by pressing Ctrl+D. Figure 18-9 shows the tab order required for the spin control to work with its buddy control.
FIGURE 18- 9
If the tab order you see is different, you have to change it. You really want the edit control to precede the spin button in the tab sequence, so you need to select the controls by clicking them in the order in which you want them to be numbered: OK button; Cancel button; edit control; spin button; and fi nally the static control. So, the tab order will be as shown in Figure 18-9. Now the edit control is selected as the buddy to the spin button.
Generating the Scale Dialog Class After saving the resource fi le, you can right- click the dialog and select Add Class from the pop -up at the cursor. You’ll then be able to defi ne the new class associated with the dialog resource that you have created. You should name the class CScaleDialog and select the base class as CDialog. Clicking the Finish button adds the class to the Sketcher project. You need to add a variable to the dialog class that stores the value returned from the edit control, so right- click the CScaleDialog class name in the Class View and select Add ➪ Add Variable from the pop -up. The new data member of the class is a special kind, called a control variable, so fi rst check the “Control variable” box in the window for the Add Member Variable wizard. A control variable
Using a Spin Button Control
❘ 1075
is a variable that is to be associated with a control in the dialog. Select IDC_SCALE as the ID from the Control ID drop -down list and Value from the Category list box. Enter the variable name as m_Scale. You’ll be storing an integer scale value, so select int as the variable type. The Add Member Variable wizard displays edit boxes in which you can enter maximum and minimum values for the variable m_Scale. For our application, a minimum of 1 and a maximum of 8 would be good values. Note that this constraint applies only to the edit box; the spin control is independent of it. Figure 18-10 shows how the window for the Add Member Variable Wizard should look when you are done.
FIGURE 18-10
When you click the Finish button, the wizard takes care of entering the code necessary to support your new control variable. You will also need to access the spin control in the dialog, and you can use the Add Member Variable Wizard to create that, too. Right- click CScaleDialog in Class View once again and click the “Control variable” checkbox. Leave the Category selection as Control and select IDC_SPIN_SCALE as the control ID: this corresponds to the spin control. You can now enter the variable name as m_Spin and add a suitable comment. When you click the Finish button, the new variable will be added to the CScaleDialog class. The class defi nition you’ll end up with after the wizard has added the new members is as follows: class CScaleDialog : public CDialog { DECLARE_DYNAMIC(CScaleDialog) public: CScaleDialog(CWnd* pParent = NULL); virtual ~CScaleDialog();
// standard constructor
// Dialog Data enum { IDD = IDD_SCALE_DLG }; protected: virtual void DoDataExchange(CDataExchange* pDX); DECLARE_MESSAGE_MAP() public: // Stores the current drawing scale int m_Scale; // Spin control for view scale CSpinButtonCtrl m_Spin; };
// DDX/DDV support
1076
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
The interesting bits of the class defi nition are bolded. The class is associated with the dialog resource through the enum statement, initializing IDD with the ID of the resource. It contains the variable m_Scale, which is specified as a public member of the class, so you can set and retrieve its value in a CScaleDialog object directly. There’s also some special code in the implementation of the class to deal with the new m_Scale member. The m_Spin variable references the CSpinButtonCtrl object so you can call functions for the spin control.
Dialog Data Exchange and Validation A virtual function called DoDataExchange() has been included in the class by the Class Wizard. If you look in the ScaleDialog.cpp fi le, you’ll fi nd that the implementation looks like this: void CScaleDialog::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Text(pDX, IDC_SCALE, m_Scale); DDV_MinMaxInt(pDX, m_Scale, 1, 8); DDX_Control(pDX, IDC_SPIN_SCALE, m_Spin); }
This function is called by the framework to carry out the exchange of data between variables in a dialog and the dialog’s controls. This mechanism is called dialog data exchange, usually abbreviated to DDX . This is a powerful mechanism that can provide automatic transfer of information between a dialog and its controls in most circumstances, thus saving you the effort of programming to get the data yourself, as you did with the radio buttons in the pen width dialog. In the scale dialog, DDX handles data transfers between the edit control and the variable m_Scale in the CScaleDialog class. The variable pDX, passed to the DoDataExchange() function, controls the direction in which data is transferred. After the base class DoDataExchange() function is called, the DDX_Text() function is called. The latter actually moves data between the variable m_Scale and the edit control. The call to the DDV_MinMaxInt() function verifies that the value transferred is within the limits specified. This mechanism is called dialog data validation, or DDV. The DoDataExchange() function is called automatically before the dialog is displayed to pass the value stored in m_Scale to the edit control. When the dialog is closed with the OK button, it is automatically called again to pass the value in the control back to the variable m_Scale in the dialog object. All this is taken care of for you. You need only to ensure that the right value is stored in m_Scale before the dialog box is displayed, and arrange to collect the result when the dialog box closes.
Initializing the Dialog You’ll use the OnInitDialog() function to initialize the dialog, just as you did for the pen width dialog. This time you’ll use it to set up the spin control. You’ll initialize the m_Scale member a little later when you create the dialog in the handler for a Scale menu item, because it should be set to the value of the scale stored in the view. For now, add an override for the OnInitDialog() function to the CScaleDialog class, using the same mechanism you used for the previous dialog, and add code to initialize the spin control as follows:
Using a Spin Button Control
❘ 1077
BOOL CScaleDialog::OnInitDialog() { CDialog::OnInitDialog(); // If you have not checked the auto buddy option in // the spin control’s properties, you can set the buddy control here // Set the spin control range m_Spin.SetRange(1, 8); return TRUE;
// return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE
}
There is only one line of code to add. This sets the upper and lower limits for the spin button by calling the SetRange() member of the spin control object. Although you have set the range limits for the edit control, this doesn’t affect the spin control directly. If you don’t limit the values in the spin control here, you allow the spin control to insert values outside the limits in the edit control, and there will be an error message from the edit control. You can demonstrate this by commenting out the statement that calls SetRange() here and trying out Sketcher without it. If you want to set the buddy control using code, rather than by setting the value of Auto buddy in the spin button’s properties to True, the CSpinButtonCtrl class has a function member to do this. You need to add the statement mSpin.SetBuddy(GetDlgItem(IDC_SCALE));
at the point indicated by the comments.
NOTE You can also access controls in a dialog programmatically. The function GetDlgItem() is inherited from CWnd via CDialog, and you can use it to retrieve the address of any control from the ID you pass as the argument. Thus, calling GetDlgItem() with IDC_SPIN_SCALE as the argument would return the address of the spin control. As you saw earlier, a control is just a specialized window, so the pointer returned is of type CWnd*; you therefore have to cast it to the type appropriate to the particular control, which would be CSpinButtonCtrl* in this case.
Displaying the Spin Button The dialog is to be displayed when the Scale menu option (or its associated toolbar button) is selected, so you need to add a COMMAND event handler to the CSketcherView class corresponding to the ID_VIEW_SCALE message through the Properties window for the class. You can then add code as follows: void CSketcherView::OnViewScale() { CScaleDialog aDlg; aDlg.m_Scale = m_Scale; if(aDlg.DoModal() == IDOK)
// Create a dialog object // Pass the view scale to the dialog
1078
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
{ m_Scale = aDlg.m_Scale; InvalidateRect(0);
// Get the new scale // Invalidate the whole window
} }
You create the dialog as a modal dialog, just as you did the pen width dialog. Before the dialog box is displayed by the DoModal() function call, you store the scale value provided by the m_Scale member of CSketcherView in the dialog member with the same name; this ensures that the control displays the current scale value when the dialog is displayed. If the dialog is closed with the OK button, you store the new scale from the m_Scale member of the dialog object in the view member with the same name. Because you have changed the view scale, you need to get the view redrawn with the new scale value applied. The call to InvalidateRect() does this. Don’t forget to add an #include directive for ScaleDialog.h to SketcherView.cpp. Of course, you must not forget to add the m_Scale data member to the defi nition of CSketcherView, so add the following line at the end of the other data members in the class defi nition: int m_Scale;
// Current view scale
You should also modify the CSketcherView constructor to initialize m_Scale to 1. This results in a view always starting out with a scale of one to one. That’s all you need to get the scale dialog and its spin control operational. You can build and run Sketcher to give it a trial spin before you add the code to use a view scale factor in the drawing process.
USING THE SCALE FACTOR Scaling with Windows usually involves using one of the scalable mapping modes, MM_ISOTROPIC or MM_ANISOTROPIC. By using one of these mapping modes you can get Windows to do most of the work. Unfortunately, it’s not as simple as just changing the mapping mode, because neither is supported by CScrollView. If you can get around that, however, you’re home and dry. You’ll use MM_ANISOTROPIC for reasons that you’ll see in a moment, so let’s fi rst understand what’s involved in using this mapping mode.
Scalable Mapping Modes As I’ve said, there are two mapping modes that allow the mapping between logical coordinates and device coordinates to be altered, and these are the MM_ISOTROPIC and MM_ANISOTROPIC modes. The MM_ISOTROPIC mode has a property that forces the scaling factor for both the x- and y-axes to be the same, which has the advantage that your circles will always be circles. The disadvantage is that you can’t map a document to fit into a rectangle of a different aspect ratio. The MM_ANISOTROPIC mode, on the other hand, permits scaling of each axis independently. Because it ’s the more flexible mode of the two, you’ll use MM_ANISOTROPIC for scaling operations in Sketcher.
Using the Scale Factor
❘ 1079
The way in which logical coordinates are transformed to device coordinates is dependent on the following parameters, which you can set: PARAMETER
DESCRIPTION
Window Origin
The logical coordinates of the upper left corner of the window. You set this by calling the function CDC::SetWindowOrg().
Window Extent
The size of the window specified in logical coordinates. You set this by calling the function CDC::SetWindowExt().
Viewport Origin
The coordinates of the upper left corner of the window in device coordinates (pixels). You set this by calling the function CDC::SetViewportOrg().
Viewport Extent
The size of the window in device coordinates (pixels). You set this by calling the function CDC::SetViewportExt().
The viewport referred to here has no physical significance by itself; it serves only as a parameter for defi ning how coordinates are transformed from logical coordinates to device coordinates. Remember the following: ➤
Logical coordinates (also referred to as page coordinates) are determined by the mapping mode. For example, the MM_LOENGLISH mapping mode has logical coordinates in units of 0.01 inches, with the origin in the upper left corner of the client area, and the positive y-axis direction running from bottom to top. These are used by the device context drawing functions.
➤
Device coordinates (also referred to as client coordinates in a window) are measured in pixels in the case of a window, with the origin at the upper left corner of the client area, and with the positive y-axis direction from top to bottom. These are used outside a device context — for example, for defi ning the position of the cursor in mouse message handlers.
➤
Screen coordinates are measured in pixels and have the origin at the upper left corner of the screen, with the positive y-axis direction from top to bottom. These are used for getting or setting the cursor position.
The formulae used by Windows to convert from logical coordinates to device coordinates are:
xDevice⫽(xLogical⫺xWindowOrg)*
xViewportExt ⫹xViewportOrg xWindowExt
yDevice⫽(yLogical⫺yWindowOrg)*
yViewportExt ⫹yViewportOrg yWindowExt
1080
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
With coordinate systems other than those provided by the MM_ISOTROPIC and MM_ANISOTROPIC mapping modes, the window extent and the viewport extent are fi xed by the mapping mode and can’t be changed. Calling the functions SetWindowExt() or SetViewportExt() in the CDC object to change them has no effect, although you can still move the position of (0,0) in your logical reference frame by calling SetWindowOrg() or SetViewportOrg(). However, for a given document size that is expressed by the window extent in logical coordinate units, you can adjust the scale at which elements are displayed by setting the viewport extent appropriately. By using and setting the window and viewport extents, you can get the scaling done automatically.
Setting the Document Size You need to maintain the size of the document in logical units in the document object. You can add a protected data member, m_DocSize, to the CSketcherDoc class defi nition to store the size of the document: CSize m_DocSize;
// Document size
You will also want to access this data member from the view class, so add a public function to the CSketcherDoc class defi nition as follows: CSize GetDocSize() const { return m_DocSize; }
// Retrieve the document size
You must initialize the m_DocSize member in the constructor for the document; modify the implementation of CSketcherDoc() as follows: CSketcherDoc::CSketcherDoc() : m_Element(LINE) , m_Color(BLACK) , m_PenWidth(0) , m_DocSize(CSize(3000,3000)) { // TODO: add one-time construction code here }
You’ll be using notional MM_LOENGLISH coordinates, so you can treat the logical units as increments of 0.01 inches, and the value set gives you an area of 30 square inches to draw on.
Setting the Mapping Mode You can set the mapping mode to MM_ANISOTROPIC in an override for the inherited OnPrepareDC() function in the CSketcherView class. This function is always called for any WM_PAINT message, and you have arranged to call it when you draw temporary objects in the mouse message handlers; however, you have to do a little more than just set the mapping mode. You’ll need to create the function override in CSketcherView before you can add the code. Just open the Properties window for the CSketcherView class and click the Overrides toolbar button. You can then add the override by selecting OnPrepareDC from the list and clicking on OnPrepareDC
Using the Scale Factor
❘ 1081
in the adjacent column. You are now able to type the code directly into the Editor pane. The implementation of OnPrepareDC() is as follows: void CSketcherView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo) { CScrollView::OnPrepareDC(pDC, pInfo); CSketcherDoc* pDoc = GetDocument(); pDC->SetMapMode(MM_ANISOTROPIC); // Set the map mode CSize DocSize = pDoc->GetDocSize(); // Get the document size pDC->SetWindowExt(DocSize);
// Now set the window extent
// Get the number of pixels per inch in x and y int xLogPixels = pDC->GetDeviceCaps(LOGPIXELSX); int yLogPixels = pDC->GetDeviceCaps(LOGPIXELSY); // Calculate the viewport extent in x and y int xExtent = (DocSize.cx*m_Scale*xLogPixels)/100; int yExtent = (DocSize.cy*m_Scale*yLogPixels)/100; pDC->SetViewportExt(xExtent,yExtent); // Set viewport extent }
The override of the base class function is unusual here in that you have left in the call to CScrollView::OnPrepareDC() and added the modifications after it, rather than where the comment in the default code suggests. If the class was derived from CView, you would replace the call to the base class version because it does nothing, but in the case of CScrollView, this isn’t the case. You need the base class function to set some attributes before you set the mapping mode. Don’t make the mistake of calling the base class function at the end of the override version, though — if you do, scaling won’t work. The CDC member function GetDeviceCaps() supplies information about the device with which the device context is associated. You can get various kinds of information about the device, depending on the argument you pass to the function. In this case the arguments LOGPIXELSX and LOGPIXELSY return the number of pixels per logical inch in the x and y directions, respectively. These values are equivalent to 100 units in your logical coordinates. You use these values to calculate the x and y values for the viewport extent, which you store in the local variables xExtent and yExtent, respectively. The document extent along an axis in logical units, divided by 100, gives the document extent in inches. If this is multiplied by the number of logical pixels per inch for the device, you get the equivalent number of pixels for the extent. If you then use this value as the viewport extent, you get the elements displayed at a scale of one to one. If you simplify the equations for converting between device and logical coordinates by assuming that the window origin and the viewport origin are both (0,0), they become the following: xDevice⫽xLogical*
xViewportExt xWindowExt
yDevice⫽yLogical*
yViewportExt yWindowExt
1082
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
If you multiply the viewport extent values by the scale (stored in m_Scale), the elements are drawn according to the value of m_Scale. This logic is exactly represented by the expressions for the x and y viewport extents in your code. The simplified equations, with the scale included, are as follows: xDevice⫽xLogical*
xViewportExt*m_Scale xWindowExt
yDevice⫽yLogical*
yViewportExt*m_Scale yWindowExt
You should be able to see from this that a given pair of device coordinates varies in proportion to the scale value. The coordinates at a scale of three are three times the coordinates at a scale of one. Of course, as well as making elements larger, increasing the scale also moves them away from the origin. That’s all you need in order to scale the view. Unfortunately, at the moment scrolling won’t work with scaling, so you need to see what you can do about that.
Implementing Scrolling with Scaling CScrollView just won’t work with the MM_ANISOTROPIC mapping mode, so clearly you must use another mapping mode to set up the scrollbars. The easiest way to do this is to use MM_TEXT,
because in this case the units of logical coordinates are the same as the client coordinates — pixels, in other words. All you need to do, then, is figure out how many pixels are equivalent to the logical document extent for the scale at which you are drawing, which is easier than you might think. You can add a function to CSketcherView to take care of the scrollbars and implement it to work out the number of pixels corresponding to the logical document extent. Right- click the CSketcherView class name in Class View and add a public function, ResetScrollSizes(), with a void return type and no parameters. Add the code to the implementation, as follows: void CSketcherView::ResetScrollSizes(void) { CClientDC aDC(this); OnPrepareDC(&aDC); CSize DocSize = GetDocument()->GetDocSize(); aDC.LPtoDP(&DocSize); SetScrollSizes(MM_TEXT, DocSize); }
// // // //
Set Get Get Set
up the device context the document size the size in pixels up the scrollbars
After creating a local CClientDC object for the view, you call OnPrepareDC() to set up the MM_ANISOTROPIC mapping mode. Because this takes scaling into account, the LPtoDP() member of the aDC object converts the document size stored in the local variable DocSize to the correct number of pixels for the current logical document size and scale. The total document size in pixels defines how large the scrollbars must be in MM_TEXT mode — remember, MM_TEXT logical coordinates are in pixels. You can then get the SetScrollSizes() member of CScrollView to set up the scrollbars based on this by specifying MM_TEXT as the mapping mode.
Using the Scale Factor
❘ 1083
It may seem strange that you can change the mapping mode in this way, but it’s important to keep in mind that the mapping mode is nothing more than a defi nition of how logical coordinates are to be converted to device coordinates. Whatever mode (and therefore coordinate conversion algorithm) you’ve set up applies to all subsequent device context functions until you change it, and you can change it whenever you want. When you set a new mode, subsequent device context function calls just use the conversion algorithm defi ned by the new mode. You figure out how big the document is in pixels with MM_ANISOTROPIC because this is the only way you can get the scaling into the process; you then switch to MM_TEXT to set up the scrollbars because you need units for this in pixels for it to work properly. Simple really, when you know how.
Setting Up the Scrollbars You must set up the scrollbars initially for the view in the OnInitialUpdate() member of CSketcherView. Change the previous implementation of the function to the following: void CSketcherView::OnInitialUpdate() { ResetScrollSizes(); CScrollView::OnInitialUpdate(); }
// Set up the scrollbars
All you do is call the ResetScrollSizes() function that you just added to the view. This takes care of everything — well, almost. The CScrollView object needs an initial extent to be set in order for OnPrepareDC() to work properly, so you need to add one statement to the CSketcherView constructor: CSketcherView::CSketcherView() : m_FirstPoint(CPoint(0,0)) // , m_SecondPoint(CPoint(0,0)) // , m_pTempElement(NULL) // , m_pSelected(NULL) // , m_MoveMode(FALSE) // , m_CursorPos(CPoint(0,0)) // , m_FirstPos(CPoint(0,0)) // , m_Scale(1) // { SetScrollSizes(MM_TEXT, CSize(0,0)); }
Set 1st recorded point to 0,0 Set 2nd recorded point to 0,0 Set temporary element pointer to 0 No element selected initially Set move mode off Initialize as zero Initialize as zero Set scale to 1:1 // Set arbitrary scrollers
The additional statement just calls SetScrollSizes() with an arbitrary extent to get the scrollbars initialized before the view is drawn. When the view is drawn for the fi rst time, the ResetScrollSizes() function call in OnInitialUpdate()sets up the scrollbars properly. Of course, each time the view scale changes, you need to update the scrollbars before the view is redrawn. You can take care of this in the OnViewScale() handler in the CSketcherView class: void CSketcherView::OnViewScale() { CScaleDialog aDlg; aDlg.m_Scale = m_Scale; if(aDlg.DoModal() == IDOK)
// Create a dialog object // Pass the view scale to the dialog
1084
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
{ m_Scale = aDlg.m_Scale; ResetScrollSizes(); InvalidateRect(0);
// Get the new scale // Adjust scrolling to the new scale // Invalidate the whole window
} }
With the ResetScrollSizes() function, taking care of the scrollbars isn’t complicated. Everything is covered by the one additional line of code. Now you can build the project and run the application. You’ll see that the scrollbars work just as they should. Note that each view maintains its own scale factor, independently of the other views.
USING THE CTASKDIALOG CLASS The CTaskDialog class is a new feature of Visual C++ 2010 that enables you to create and display a wide range of message boxes and input dialogs programmatically. Although CTaskDialog is easy to use, it has a couple of limitations. First, it works only if your application is running under Windows Vista or later. If you want to use it, and also support earlier versions of Windows, you have to program the old-fashioned way as an alternative, so it doesn’t save you any effort. Second, you have much more flexibility in how your input dialogs look and work if you create dialog resources for them in the way you have seen. Because of these limitations, I’ll only discuss the class briefly, and we won’t be integrating it into Sketcher. To use the CTaskDialog class in a source fi le, you need an #include directive for afxTaskDialog.h. You can program to use this class, but only when it is supported, like this: if(CTaskDialog::IsSupported) { // Use CTaskDialog } else { // Use a dialog derived from CDialog or use AfxMessageBox() }
The static IsSupported() function in CTaskDialog returns TRUE if the operating system environment on which the application is running supports it.
Displaying a Task Dialog The simplest way to create a CTaskDialog dialog and display it is to call the static ShowDialog() member of the class. Because you have no explicit CTaskDialog object, you can’t do anything beyond the basic options offered by the ShowDialog() function, so this is primarily a more sophisticated alternative to calling AfxMessageBox(). The prototype of the ShowDialog() function is as follows: static INT_PTR ShowDialog( const CString& content,
// Content of the dialog
Using the CTaskDialog Class
❘ 1085
const CString& mainInstr, // Main instruction in the dialog const CString& title, // Title bar text int nIDCommandFirst, // String ID of the first command int nIDCommandLast, // String ID of the last command int buttons = TDCBF_YES_BUTTON|TDCBF_NO_BUTTON, // Buttons in the dialog int nOptions = TDF_ENABLE_HYPERLINKS|TDF_USE_COMMAND_LINKS, // Dialog options const CString& footer=_T(“”)); // Footer string in the dialog
The nIDCommandFirst and nIDCommandLast parameters specify a range of IDs that should identify entries in the string table resource in your application. For each valid ID, a command button will be created, and the string from the table will appear as the caption for the command button. You need to make sure that the value for the nIDCommandFirst parameter is greater than the IDs for any other buttons you may be using, such as IDOK and IDCANCEL. If you create the IDs through the resource editor for the string table, this should not be a problem. The buttons parameter specifies the common buttons that the user can click to close the dialog. In addition to the defaults, you can also use TDCBF_OK_BUTTON, TDCBF_CANCEL_BUTTON, TDCBF_RETRY_ BUTTON, and TDCBF_CLOSE_BUTTON. These are single-bit masks that identify the buttons. When you want to have two or more buttons displayed in the dialog, you OR them together. There are many standard options you can specify via the nOptions parameter, in addition to the default values shown. You can fi nd these in the documentation for the SetOptions() function in the dialog class. The integer value that is returned reflects the selection made by the user to close the dialog. This will be the ID for the control that was clicked to close the dialog. In the case of a command button being clicked, the ID will be the ID that identifies the entry in the string table. If a common button is clicked to close the dialog, the value returned will be one of IDYES, IDNO, IDOK, IDCANCEL, IDRETRY, or IDCLOSE. Here’s a code fragment showing how you might use the ShowDialog() function: CString content(_T("Modal Line Type Options")); CString mainInst( _T("Choose the line type you want to use or click Cancel to exit:")); CString title(_T("Line Type Chooser")); int result = CTaskDialog::ShowDialog( content, mainInst, title, IDS_SOLID_LINE, IDS_DASHED_LINE, TDCBF_CANCEL_BUTTON );
For this fragment to compile and execute successfully, you would need to have defi ned the symbols IDS_SOLID_LINE, IDS_DOTTED_LINE, IDS_DOTDASH_LINE, and IDS_DASHED_LINE with consecutive values. You can create new symbols by clicking on the string table resource on the Resources pane
1086
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
and pressing the insert key to add a new string. You will then be able to edit the Caption, ID, and Value properties for the new entry. The dialog that is displayed is shown in Figure 18 -11. To get the data from this dialog when it closes, you could use the following code: switch(result) { case IDS_SOLID_LINE: m_LineType = SOLID; break; case IDS_DOTTED_LINE: m_LineType = DOTTED; break; case IDS_DOTDASH_LINE: m_LineType = DOTDASH; break; case IDS_DASHED_LINE: m_LineType = DASHED; break; case IDCANCEL: break; default: CTaskDialog::ShowDialog( _T("Error Choosing Line Type"), _T("Invalid return from dialog!"), _T("Error"), 0, 0, TDCBF_OK_BUTTON ); break; }
FIGURE 18-11
This fragment sets the value of a member variable, m_LineType, depending on the value returned from the ShowDialog() function. The function returns the symbol value corresponding to the command or common button that was clicked to terminate the dialog. There’s a provision in the default case for displaying an error dialog using the ShowDialog() function. This supplies string arguments as literals rather than creating CString objects.
Creating CTaskDialog Objects When you create a CTaskDialog object using the class constructor, you have increased flexibility in what you can do with the dialog because you have functions available that enable you to customize the dialog object. There are two constructors that have parameters similar to those of the ShowDialog() function. One has parameters for specifying a range of IDs for command controls, and the other doesn’t. The prototype for the constructor without command control parameters is as follows:
Using the CTaskDialog Class
❘ 1087
CTaskDialog( const CString& content, // Content of the dialog const CString& mainInstr, // Main instruction in the dialog const CString& title, // Title bar text int buttons = TDCBF_OK_BUTTON|TDCBF_CANCEL_BUTTON, // Buttons in the dialog int nOptions = TDF_ENABLE_HYPERLINKS|TDF_USE_COMMAND_LINKS, // Dialog options const CString& footer=_T("") // Footer string in the dialog };
Note that the default button values are different from those of the ShowDialog() function. Here the defaults are for OK and Cancel buttons rather than Yes and No buttons. Here’s how you could use this constructor: CTaskDialog scaleDlg( _T("Choose the View Scale"), _T("Click on the view scale you want:"), _T("View Scale Selector") );
When you use this constructor, you typically customize the object by calling member functions before displaying the dialog. The more comprehensive constructor adds two parameters that specify the IDs for a range of command controls: CTaskDialog( const CString& content, // Content of the dialog const CString& mainInstr, // Main instruction in the dialog const CString& title, // Title bar text int nIDCommandFirst, // String ID of the first command int nIDCommandLast, // String ID of the last command int buttons, // Buttons in the dialog int nOptions = TDF_ENABLE_HYPERLINKS|TDF_USE_COMMAND_LINKS, // Dialog options const CString& footer=_T(“”) // Footer string in the dialog };
As you see, there are no default button values in this case. This constructor produces an object that encapsulates a dialog that is similar to the one produced by the ShowDialog() function.
Adding Radio Buttons You can add radio buttons to a CTaskDialog object using the AddRadioButton() function. Here’s how you could add radio buttons to the scaleDlg object that was created in the previous section: const int SCALE_1(1001), SCALE_2(1002), scaleDlg.AddRadioButton(SCALE_1, _T("View scaleDlg.AddRadioButton(SCALE_2, _T("View scaleDlg.AddRadioButton(SCALE_3, _T("View scaleDlg.AddRadioButton(SCALE_4, _T("View int result = scaleDlg.DoModal();
SCALE_3(1003), SCALE_4(1004); scale 1")); scale 2")); scale 3")); scale 4"));
1088
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
The fi rst statement defi nes four integer constants that you use to identify the radio buttons. The AddRadioButton() function calls add four radio buttons, each identified by the fi rst argument, with the annotation for each radio button specified by the second argument. Executing this fragment, following the statement in the previous section that creates scaleDlg, will display the dialog shown in Figure 18 -12. The fi rst radio button is selected by default. If you wanted to have a different radio button selected by default, you could call the SetDefaultRadioButton() function for the dialog before you display it. For example:
FIGURE 18-12
scaleDlg.SetDefaultRadioButton(SCALE_3);
The argument to the function is the ID of the button you want to set as the default selection. You can remove all the radio buttons from a CTaskDialog object by calling its RemoveAllRadioButtons() function. You would do this if you wanted to add a different set of radio buttons before you re-display the dialog.
Getting Output from Radio Buttons You can obtain the ID of the radio button that is selected when the dialog closes by calling the GetSelectedRadioButtonID() function for the dialog object. Here’s how you can get output from scaleDlg: if(scaleDlg.DoModal() == IDOK) { switch(scaleDlg.GetSelectedRadioButtonID()) { case SCALE_1: m_Scale = 1; break; case SCALE_2: m_Scale = 2; break; case SCALE_3: m_Scale = 3; break; case SCALE_4: m_Scale = 4; break; } }
You display the dialog by calling DoModal() for scaleDlg. You want to check the state of the radio buttons in the dialog only when the OK button is used to close the dialog, and the if statement verifies that the return from DoModal() corresponds to the OK button. The GetSelectedRadioButtonID() function returns the ID of the radio button that is selected, so the switch statement sets the value of the m_Scale member, depending on the ID that is returned.
Working with Status Bars
❘ 1089
The CTaskDialog class is a convenient and more flexible alternative to the AfxMessageBox() function, especially when you want to get user input in addition to communicating a message. There are also many more functions that the CTaskDialog class defi nes for manipulating and updating the dialog that I have described here. However, creating a dialog resource using the toolbox provides you with a much wider range of controls that you can use and gives you complete flexibility in how the dialog is laid out. Furthermore, when you do use the CTaskDialog class in an application that you want to run with Windows XP or earlier operating systems, you must call the IsSupported() function to verify that the class is supported before you attempt to use it, and you will need to program for an alternative dialog creation process when it isn’t. For these reasons, you will fi nd that most dialogs are best created as a dialog resource or through AfxMessageBox().
WORKING WITH STATUS BARS With each view now being scaled independently, there’s a real need to have some indication of what the current scale in a view is. A convenient way to do this would be to display the scale in the status bar for each view window. A status bar was created by default in the Sketcher main application window. Usually, the status bar appears at the bottom of an application window, below the horizontal scrollbar, although you can arrange for it to be at the top of the client area. The status bar is divided into segments called panes; the status bar in the main application window in Sketcher has four panes. The one on the left contains the text “Ready,” and the other three are the recessed areas on the right that are used as indicators to record when Caps Lock, Num Lock, and Scroll Lock are in effect. It’s possible for you to write to the status bar that the Application Wizard supplied by default, but you need access to the m_wndStatusBar member of the CMainFrame object for the application, as this represents it. As it’s a protected member of the class, you must either add a public member function to modify the status bar from outside the class, or add a member to return a reference to m_wndStatusBar. You may well have several views of the same sketch, each with its own view scale, so you really want to associate displaying the scale with each view. Our approach will be to give each child window its own status bar. The m_wndStatusBar object in CMainFrame is an instance of the CMFCStatusBar class. You can use the same class to implement your own status bars in the view windows.
Adding a Status Bar to a Frame The CMFCStatusBar class defi nes a control bar with multiple panes in which you can display information. We will be using this in a very simple way, but the CMFCStatusBar class provides a great deal of advanced capability, including the ability to display icons in status bar panes. Consult the Visual C++ 2010 documentation for the CMFCStatusBar class for more information on this. The fi rst step to using CMFCStatusBar is to add a data member for the status bar to the defi nition of CChildFrame, which is the frame window for a view. Add the following declaration to the public section of the class: CMFCStatusBar m_StatusBar;
// Status bar object
1090
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
NOTE Status bars should be part of the frame, not part of the view. You don’t want to be able to scroll the status bars or draw over them. They should just remain anchored to the bottom of the window. If you added a status bar to the view, it would appear inside the scrollbars and would be scrolled whenever you scrolled the view. Drawing over the part of the view containing the status bar would cause the bar to be redrawn, leading to an annoying flicker. Having the status bar as part of the frame avoids these problems.
Creating Status Bar Panes To create the panes in the status bar, you call the SetIndicators() function for the status bar object. This function requires two arguments, a const array of indicators of type UINT and the number of elements in the array. Each element in the array is a resource symbol that will be associated with a pane in the status bar, and each resource symbol must have an entry in the resource string table that will be the default text in the pane. There are standard resource symbols, such as ID_INDICATOR_CAPS and ID_INDICATOR_NUM, which are used to identify indicators for the Caps Lock and Num Lock keys respectively, and you can see these in use if you look at the implementation of the OnCreate() function in the CMainFrame class. They also appear in the string table resource. You can create your own indicator resource symbol by extending the String Table resource in Resource View: click the 䉯 symbol, double- click the String Table entry to display the table, and then press the insert key. If you right- click the new entry in the table and select Properties from the menu, you will be able to change the ID to ID_INDICATOR_SCALE and the caption to View Scale : 1. You should initialize the m_StatusBar data member just before the visible view window is displayed. So, using the Properties window for the CChildFrame class, you can add a function to the class that will be called in response to the WM_CREATE message that is sent to the application when the window is to be created. Add the following code to the OnCreate() handler: int CChildFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if(CMDIChildWndEx::OnCreate(lpCreateStruct) == -1) return -1; // Create the status bar m_StatusBar.Create(this); static UINT indicators[] = {ID_INDICATOR_SCALE}; m_StatusBar.SetIndicators(indicators, sizeof(indicators)/sizeof(UINT)); m_StatusBar.SetPaneStyle(0, SBPS_STRETCH); // Stretch the first pane return 0; }
The generated code isn’t bolded. There’s a call to the base class version of the OnCreate() function, which takes care of creating the defi nition of the view window. It’s important not to delete this function call; otherwise, the window is not created.
Working with Status Bars
❘ 1091
Calling the Create() function for the CMFCStatusBar object creates the status bar. You pass the this pointer for the current CChildFrame object to the Create() function, setting up a connection between the status bar and the window that owns it. The indicators that define the panes in the status bar are typically defi ned as an array of UINT elements. Here you are interested only in setting up a single pane, so you defi ne indicators as an array that is initialized just with the symbol for your status pane. When you want to add multiple panes to a status bar, you can separate them by including ID_SEPARATOR symbols between your symbols in the indicators array. You call the SetIndicators() function for the status bar object with the address of indicators as the fi rst argument and a count of the number of elements in the array as the second argument. This will create a single pane to the left in the status bar. The call to SetPaneStyle() for the fi rst pane causes this pane to be stretched as necessary, the fi rst argument being the pane index and the second a UINT value specifying the style or styles to be set. Without this, the sizing grip would not remain in the correct position when you resized the window. Only one pane can have the SBPS_STRETCH style. Other styles you can set for a pane are as follows: SBPS_NORMAL
No stretch, borders, or pop - out styles set.
SBPS_POPOUT
Border reversed, so text pops out.
SBPS_NOBORDERS
No 3D borders.
SBPS_DISABLED
No text drawn in the pane.
You just OR the styles together when you want to set more than one style for a pane.
Updating the Status Bar If you build and run the code now, the status bars appear, but they show only a scale factor of one, grayed out, no matter what scale factor is actually being used — not very useful. This is because there is no mechanism in place for updating the status bar. What you need to do is add code somewhere that changes the text in the status bar pane each time a different scale is chosen. The obvious place to do this is in the CSketcherView class because that’s where the current view scale is recorded. You can update a pane in the status bar by adding an UPDATE_COMMAND_UI handler that is associated with the indicator symbol for the pane. Right- click CSketcherView in Class View, and select Class Wizard from the pop -up. This will display the MFC Class Wizard dialog. As you can see, this enables you to create and edit a whole range of functions in a class and provides an alternative way to access any class member. If it is not already visible, select the Commands tab in the dialog: this enables you to add command handlers to the class. Select ID_INDICATOR_SCALE in the Object IDs pane; this ID identifies the status bar pane you want to update. Then, select UPDATE_COMMAND_UI in the Messages pane. The dialog should look like Figure 18-13.
1092
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
FIGURE 18-13
Click the Add Handler button to add the command handler to the class. Another dialog will be displayed that gives you the opportunity to change the handler function name to OnUpdateScale, which is a little more concise than the default. You can then click OK to close the dialog, and OK again to close the MFC Class Wizard dialog. All you need is to complete the defi nition for the handler that was added, so add the following code to the function in SketcherView.cpp: void CSketcherView::OnUpdateScale(CCmdUI *pCmdUI) { pCmdUI->Enable(); CString scaleStr; scaleStr.Format(_T(“ View Scale : %d”), m_Scale); pCmdUI->SetText(scaleStr); }
The parameter is a pointer to a CCmdUI object that encapsulates the status bar pane as a command target. You create a CString object and call its Format() function to generate the text string to be displayed in the pane. The fi rst argument to Format() is a format control string in which you
Using a List Box
❘ 1093
can embed conversion specifiers for subsequent arguments. The format string with the embedded specifiers is the same as for the C function, printf(). Each of the subsequent arguments is converted according to the corresponding format specifier in the fi rst argument, so there must be one format specifier in the format string for each argument after the fi rst. Here the %d specifier converts the value of m_Scale to a decimal string, and this is incorporated into the format string. Other common specifiers you can use are %f for floating point values and %s for string values. Calling SetText() for the CCmdUI object sets the string you supply as the argument in the status bar pane. Calling Enable() for the CCmdUI object causes the pane text to be displayed normally because the BOOL parameter has a default value of TRUE. An explicit argument of FALSE would make the pane text grayed out. That’s all you need for the status bar. If you build Sketcher again, you should have multiple, scrolled windows, each at different scales, with the scale displayed in the status bar in each view.
USING A LIST BOX Of course, you don’t have to use a spin button to set the scale. You could also use a list box control, for example. The logic for handling a scale factor would be exactly the same, and only the dialog box and the code to extract the value for the scale factor from it would change. If you want to try this out without messing up the development of the Sketcher program, you can copy the complete Sketcher project to another folder and make the modifications to the copy. Deleting part of a Class Wizard –managed program can be a bit messy, so it’s a useful experience to have before you really need to do it.
Removing the Scale Dialog You first need to delete the definition and implementation of CScaleDialog from the copy of the Sketcher project, as well as the resource for the scale dialog. To do this, go to the Solution Explorer pane, select ScaleDialog.cpp, and press the delete key. Click the Delete button in the dialog that is displayed, unless you want to keep the file; then select ScaleDialog.h and remove it from the project in the same way. Go to Resource View, expand the Dialog folder, click IDD_SCALE_DLG, and press the delete key to remove the dialog resource. Delete the #include directive for ScaleDialog.h from SketcherView.cpp. At this stage, all references to the original dialog class have been removed from the project. Are you all done yet? Almost. The IDs for the resources should have been deleted for you. To verify this, right- click Sketcher.rc in Resource View and select the Resource Symbols menu item from the pop -up; you can check that IDC_SCALE and IDC_SPIN_SCALE are no longer in the list. Of course, the OnViewScale() handler in the CSketcherView class still refers to CScaleDialog, so the Sketcher project won’t compile yet. You’ll fi x that when you have added the list box control. Select the Build ➪ Clean Solution menu item to remove any intermediate fi les from the project that may contain references to CScaleDialog. After that’s done, you can start recreating the dialog resource for entering a scale value.
1094
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
Creating a List Box Control Right-click the Dialog folder in Resource View, and add a new dialog with a suitable ID and caption. You could use the same ID as before, IDD_SCALE_DLG. Select the list box button in the list of controls, and click where you want the list box to be positioned in the dialog box. You can enlarge the list box and adjust its position in the dialog by dragging it appropriately. Right- click the list box and select Properties from the pop -up. You can set the ID to something suitable, such as IDC_SCALE_LIST, as shown in Figure 18-14. The Sort property will be True by default, so make sure you set it to False. This means that strings that you add to the list box are not automatically sorted. Instead, they’re appended to the end of the list in the box, and are displayed in the sequence in which you enter them. Because you will be using the position of the selected item in the list to indicate the scale, it’s important not to have the sequence changed. The list box has a vertical scrollbar for the list entries by FIGURE 18-14 default, and you can accept the defaults for the other properties. If you want to look into the effects of the other properties, you can click each of them in turn to display text at the bottom of the Properties window explaining what the property does. Now that the dialog is complete, you can save it, and you’re ready to create the class for the dialog.
Creating the Dialog Class Right-click the dialog and select Add Class from the pop-up. Again, you’ll be taken to the dialog to create a new class. Give the class an appropriate name, such as the one you used before, CScaleDialog, and select CDialog as the base class. If, when you click Finish, you get a message box saying that ScaleDialog.cpp already exists, you forgot to explicitly delete the .h and .cpp files. Go back and do that now, or rename the files if you want to keep them. Everything should then work as it’s supposed to. After you’ve completed that, all you need to do is add a public control variable called m_Scale to the class, corresponding to the list box ID, IDC_SCALE_LIST. The type should be int and the limits should be 0 and 7. Don’t forget to set the Category as Value; otherwise, you won’t be able to enter the limits. Because you have created it as a control variable, DDX is implemented for the m_Scale data member, and you will use the variable to store a zero-based index to one of the eight entries in the list box. You need to initialize the list box in the OnInitDialog() override in the CScaleDialog class, so add an override for this function using the Properties window for the class. Add code to the function as follows:
Using a List Box
❘ 1095
BOOL CScaleDialog::OnInitDialog() { CDialog::OnInitDialog(); CListBox* pListBox = static_cast(GetDlgItem(IDC_SCALE_LIST)); CString scaleStr; for(int i = 1 ; i AddString(scaleStr); } pListBox->SetCurSel(m_Scale); // Set current scale return TRUE;
// return TRUE unless you set the focus to a control // EXCEPTION: OCX Property Pages should return FALSE
}
The fi rst line that you have added obtains a pointer to the list box control by calling the GetDlgItem() member of the dialog class. This is inherited from the MFC class, CWnd. It returns a pointer of type CWnd*, so you cast this to type CListBox*, which is a pointer to the MFC class defi ning a list box. Using the pointer to the dialog’s CListBox object, you then use the AddString() member repeatedly in the for loop to add the lines defi ning the list of scale factors. Each entry in the list box is formed by a call to the Format() member of the scaleStr object. The statement following the for loop sets the currently selected entry in the list box to correspond to the value of m_Scale. These entries appear in the list box in the order in which you enter them, so that the dialog is displayed as shown in Figure 18-15.
FIGURE 18-15
Each entry in the list is associated with a zero-based index value that is automatically stored in the m_Scale member of CScaleDialog through the DDX mechanism. Thus, if you select the third entry in the list, m_Scale is set to 2.
Displaying the Dialog The dialog is displayed by the OnViewScale() handler that you added to CSketcherView in the previous version of Sketcher. You need only to amend this function to deal with the new dialog, using a list box, the code for it is as follows: void CSketcherView::OnViewScale() { CScaleDialog aDlg; aDlg.m_Scale = m_Scale - 1; if(aDlg.DoModal() == IDOK) { m_Scale = 1 + aDlg.m_Scale; ResetScrollSizes(); InvalidateRect(0); } }
// Create a dialog object // Pass the view scale to the dialog
// Get the new scale // Adjust scrolling to the new scale // Invalidate the whole window
1096
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
Because the index value for the entry selected from the list is zero -based, you must decrement the current scale value by one when setting the value in the CScaleDialog object. You also need to add one to the value obtained from the dialog to get the actual scale value to be stored in the view. The code to display this value in the view’s status bar is exactly as before. The rest of the code to handle scale factors is already complete and requires no changes. After you’ve added back the #include directive for ScaleDialog.h, you can build and execute this version of Sketcher to see the list box in action.
USING AN EDIT BOX CONTROL You could use an edit box control to add annotations to a sketch in Sketcher. You’ll need a new element type, CText, that corresponds to a text string, and an extra menu item to set a TEXT mode for creating elements. Because a text element needs only one reference point, you can create it in the OnLButtonDown() handler in the view class. You’ll also need a new item in the Element menu to set TEXT mode. You’ll add this text capability to Sketcher in the following sequence:
1. 2. 3. 4.
Create a dialog resource and its associated class with an edit box control for input. Add the new menu item. Add the code to open the dialog for creating an element. Add the support for a CText class.
Creating an Edit Box Resource Create a new dialog resource in Resource View by right- clicking the Dialog folder and selecting Insert Dialog from the pop -up. Change the ID for the new dialog to IDD_TEXT_DLG, and the caption text to Enter Text. To add an edit box, select the edit control icon from the list of controls and then click the position in the dialog where you want to place it. You can adjust the size of the edit control by dragging its borders, and you can alter its position in the dialog by dragging the whole thing around. You can display the properties for the edit box by right- clicking it and selecting Properties from the pop -up. You could fi rst change its ID to IDC_EDIT_TEXT, as shown in Figure 18-16. Some of the properties for this control are of interest at this point. First, select the Multiline property. Setting the value for this as True creates a multiline edit box in which the text you enter can span more than one line. This enables you to enter a long line of text that will still remain visible in its entirety in the edit box. The Align text property determines
FIGURE 18-16
Using an Edit Box Control
❘ 1097
how the text is to be positioned in the multiline edit box. The value Left is fi ne here, but you also have the options for Center and Right. If you were to change the value for the Want return property to True, pressing Enter on the keyboard while entering the text in the control would insert a return character into the text string. This enables you to analyze the string if you want to break it into multiple lines for display. You don’t want this effect, so leave the property value as False. In this state, pressing enter has the effect of selecting the default control (which is the OK button), so pressing enter closes the dialog. If you set the value of the Auto HScroll property to False, there is an automatic spill to the next line in the edit box when you reach the edge of the control while entering text. However, this is just for visibility in the edit box — it has no effect on the contents of the string. You could also change the value of the Auto VScroll property to True to allow text to continue beyond the number of lines that are visible in the control. If you set the Vertical Scroll property to True, the edit control will be supplied with a scrollbar that will allow you to scroll the text. When you’ve fi nished setting the properties for the edit box, close its Properties window. Make sure that the edit box is fi rst in the tab order by selecting the Format ➪ Tab Order menu item or by pressing Ctrl+D. You can then test the dialog by selecting the Test Dialog menu item or by pressing Ctrl+T. The dialog is shown in Figure 18-17.
FIGURE 18-17
You can even enter text into the dialog in test mode to see how it works. Clicking the OK or Cancel button closes the dialog.
Creating the Dialog Class After saving the dialog resource, you can create a suitable dialog class corresponding to the resource, which you could call CTextDialog. To do this, right- click the dialog in Resource View and select Add Class from the pop -up. The base class should be CDialog. Next you can add a control variable to the CTextDialog class by right- clicking the class name in Class View and selecting Add ➪ Add Variable from the pop -up. Select IDC_EDIT_TEXT as the control ID and Value as the category. Call the new variable m_TextString and leave its type as CString — you’ll take a look at this class after you’ve fi nished the dialog class. You can also specify a maximum length for it in the Max chars edit box, as shown in Figure 18-18.
FIGURE 18-18
1098
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
A length of 100 is more than adequate for your needs. The variable that you have added here is automatically updated from the data entered into the control by the DDX mechanism. You can click Finish to create the variable in the CTextDialog class, and close the Add Member Variable wizard.
The CString Class You have already used CString objects a couple of times in Sketcher, but there’s more to it than you have seen so far. The CString class provides a very convenient and easy-to -use mechanism for handling strings that you can use just about anywhere a string is required. To be more precise, you can use a CString object in place of strings of type const char*, which is the usual type for a character string in native C++, or of type LPCTSTR, which is a type that comes up frequently in Windows API functions. The CString class provides several overloaded operators, as shown in the following table, that make it easy to process strings. OPERATOR
USAGE
=
Copies one string to another, as in: str1 = str2; str1 = _T(“A normal string“);
Concatenates two or more strings, as in:
+
str1 = str2 + str3 + _T(” more“); +=
Appends a string to an existing CString object, as in: str1 += str2;
==
Compares two strings for equality, as in: if(str1 == str2)
// do something...
=
Tests if one string is greater than or equal to another.
The variables str1 and str2 in the preceding table are CString objects. CString objects automatically grow as necessary, such as when you add an additional string to the end of an existing object. For example: CString str = _T("A fool and your money "); str += _T("are soon partners.");
The fi rst statement declares and initializes the object str. The second statement appends an additional string to str, so the length of str automatically increases.
Using an Edit Box Control
❘ 1099
Adding the Text Menu Item Adding a new menu item should be easy by now. You just need to open the menu resource with the ID IDR_SketcherTYPE in Resource View by double- clicking it, and add a new menu item, Text, to the Element menu. The default ID, ID_ELEMENT_TEXT, that appears in the Properties window for the item is fi ne, so you can leave that as it is. You can add a prompt to be displayed on the status bar corresponding to the menu item, and because you’ll also want to add an additional toolbar button corresponding to this menu item, you can add a tooltip to the end of the prompt line, using \n to separate the prompt and the tooltip. Don’t forget the context menu. You can copy the menu item from IDR_SketcherTYPE. Right- click the Text menu item and select Copy from the pop -up. Open the menu IDR_NOELEMENT_MENU, rightclick the empty item at the bottom, and select Paste. All you need to do then, is drag the item to the appropriate position — above the separator — and save the resource fi le. Add the toolbar button to the IDR_MAINFRAME_256 toolbar and set its ID to the same as that for the menu item, ID_ELEMENT_TEXT. You can drag the new button so that it’s positioned at the end of the block defi ning the other types of elements. Do the same for the IDR_MAINFRAME toolbar that is used for small toolbar icons. When you’ve saved the resources, you can add an event handler for the new menu item. In the Class View pane, right- click CSketcherDoc and display its Properties window. Add a COMMAND handler for the ID_ELEMENT_TEXT ID and add code to it as follows: void CSketcherDoc::OnElementText() { m_Element = TEXT; }
Only one line of code is necessary to set the element type in the document to TEXT. You also need to add a function to check the menu item if it is the current mode, so add an UPDATE_COMMAND_UI handler corresponding to the ID_ELEMENT_TEXT ID, and implement the code for it as follows: void CSketcherDoc::OnUpdateElementText(CCmdUI* pCmdUI) { // Set checked if the current element is text pCmdUI->SetCheck(m_Element == TEXT); }
This operates in the same way as the other Element pop -up menu items. Of course, you could also have added both of these handlers through the Class Wizard dialog that you display by selecting Class Wizard from the context menu in Class View. You must also add an additional entry to the ElementType enum in the SketcherConstants.h header fi le: enum ElementType{LINE, RECTANGLE, CIRCLE, CURVE, TEXT};
The next step is to defi ne the CText class for an element of type TEXT.
1100
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
Defining a Text Element You can derive the class CText from the CElement class as follows: // Class defining a text object class CText: public CElement { public: // Function to display a text element virtual void Draw(CDC* pDC, CElement* pElement=0); // Constructor for a text element CText(const CString& aString, const CRect& rect, COLORREF aColor); virtual void Move(const CSize& size); // Move a text element protected: CString m_String; CText(){}
// Text to be displayed // Default constructor
};
I added this manually, but I’ll leave it to you to decide how you want to do it. This definition should go at the end of the Elements.h fi le, following the other element types. This class defi nition declares the virtual Draw() and Move() functions, as the other element classes do. The data member m_String of type CString stores the text to be displayed. The CText constructor declaration defi nes three parameters: the string to be displayed, the rectangle in which the string is to be displayed, and the color. The pen width doesn’t apply to an item of text, because the appearance is determined by the font. Although you do not need to pass a pen width as an argument to the constructor, the constructor needs to initialize the m_PenWidth member inherited from the base class because it is used in the computation of the bounding rectangle for the text.
Implementing the CText Class You have three functions to implement for the CText class: ➤
The constructor for a CText object
➤
The virtual Draw() function to display it
➤
The Move() function to support moving a text object by dragging it with the mouse
I added these directly to the Elements.cpp fi le.
The CText Constructor The constructor for a CText object needs to initialize the class and base class data members: // CText constructor CText::CText(const CString& aString, const CRect& rect, COLORREF aColor) { m_PenWidth = 1; // Set the pen width
Using an Edit Box Control
m_Color = aColor; m_String = aString;
❘ 1101
// Set the color for the text // Make a copy of the string
m_EnclosingRect = rect; m_EnclosingRect.NormalizeRect(); }
You set the pen width to 1, and store the color and the text string to be displayed. The second parameter is the rectangle in which the text is to be displayed, so this is stored as the enclosing rectangle.
Creating a Text Element After the element type has been set to TEXT, a text object should be created at the cursor position whenever you click the left mouse button and enter the text you want to display. You therefore need to display the dialog that permits text to be entered in the OnLButtonDown() handler, but only when the element type is TEXT. Add the following code to this handler in the CSketcherView class: void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point) { CClientDC aDC(this); // Create a device context OnPrepareDC(&aDC); // Get origin adjusted aDC.DPtoLP(&point); // convert point to Logical // In moving mode, so drop the element if(m_MoveMode) { m_MoveMode = false; // Kill move mode m_pSelected = nullptr; // De-select element GetDocument()->UpdateAllViews(nullptr); // Redraw all the views return; } CSketcherDoc* pDoc = GetDocument();// Get a document pointer if(pDoc->GetElementType() == TEXT) { CTextDialog aDlg; if(aDlg.DoModal() == IDOK) { // Exit OK so create a text element CSize textExtent = aDC.GetTextExtent(aDlg.m_TextString); CRect rect(point, textExtent); //Create enclosing rectangle CText* pTextElement = new CText( aDlg.m_TextString, rect, pDoc->GetElementColor()); // Add the element to the document pDoc->AddElement(pTextElement); // Get all views updated pDoc->UpdateAllViews(nullptr, 0, pTextElement); } return; } m_FirstPoint = point; SetCapture(); }
// Record the cursor position // Capture subsequent mouse messages
1102
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
The code to be added is bolded. It creates a CTextDialog object and then opens the dialog using the DoModal() function call. The m_TextString member of aDlg is automatically set to the string entered in the edit box, so you can use this data member to pass the string entered back to the CText constructor if the OK button is used to close the dialog. The color is obtained from the document using the GetElementColor()member that you have used previously. You also need to determine the rectangle that bounds the text. Because the size of the rectangle for the block of text depends on the font used in a device context, you use the GetTextExtent() function in the CClientDC object, aDC, to initialize the CSize object, textExtent, with the width and height of the text string in logical coordinates. You then use a CRect constructor that creates a rectangle from a point that is the top right corner of the rectangle, and a CSize object that specifies the width and height of the rectangle. The CText object is created on the heap because the list in the document maintains only pointers to the elements. You add the new element to the document by calling the AddElement() member of CSketcherDoc, with the pointer to the new text element as an argument. Finally, UpdateAllViews() is called with the fi rst argument 0, which specifies that all views are to be updated.
Drawing a CText Object Drawing text in a device context is different from drawing a geometric figure. The implementation of the Draw() function for a CText object is as follows: void CText::Draw(CDC* pDC, CElement* pElement) { COLORREF Color(m_Color); // Initialize with element color if(this==pElement) Color = SELECT_COLOR;
// Set selected color
// Set the text color and output the text pDC->SetTextColor(Color); pDC->DrawText(m_String, m_EnclosingRect, DT_CENTER|DT_VCENTER| DT_SINGLELINE|DT_NOCLIP); }
You don’t need a pen to display text. You just need to specify the text color using the SetTextColor() function member of the CDC object, and then use the DrawText() member to output the text string. This displays the string, specified by the fi rst argument, in the rectangle specified by the second argument, using the default font. The third argument specifies how the text is to be formatted. You specify the third argument as one or more standard formatting constants of type UINT combined together with logical ORs. Many of these constants correspond to the ones defi ned for use with the Windows API DrawText() function. The following table lists some of the more common constants:
Using an Edit Box Control
CONSTANT
MEANING
DT_LEFT
Text is aligned with the left of the rectangle.
DT_CENTER
Text is centered horizontally in the rectangle.
DT_RIGHT
Text is aligned with the right of the rectangle.
DT_TOP
Text is aligned with the top of the rectangle.
DT_VCENTER
Text is centered vertically in the rectangle. This is used only with DT_SINGLELINE.
DT_BOTTOM
❘ 1103
Text is aligned with the bottom of the rectangle. This is used only with DT_SINGLELINE.
DT_SINGLELINE
Text is displayed on a single line. CR and LF do not break the line.
DT_NOCLIP
Inhibits clipping of the text, and drawing is faster.
DT_WORDBREAK
The string will be broken between words if it would otherwise extend beyond the sides of the rectangle.
Because the TextOut() function doesn’t use a pen, it isn’t affected by setting the drawing mode of the device context. This means that the raster operations (ROP) method that you use to move the elements leaves temporary trails behind when applied to text. Remember that you used the SetROP2() function to specify the way in which the pen would logically combine with the background. By choosing R2_NOTXORPEN as the drawing mode, you could cause a previously drawn element to disappear by redrawing it — it would then revert to the background color and thus become invisible. Fonts aren’t drawn with a pen, so it won’t work properly with the text elements. You’ll see how to fix this problem a little later in this chapter.
Moving a CText Object The Move() function for a CText object is simple: void CText::Move(const CSize& size) { m_EnclosingRect += size; }
// Move the rectangle
All you need to do is alter the enclosing rectangle by the distance specified in the size parameter. To fi x the trails problem when moving a text element, you have to treat it as a special case in the MoveElement() function in the CSketcherView class. Providing an alternative move mechanism for text elements requires that you can discover when the m_pSelected pointer contains the address of a CText object. The typeid operator in native C++ that you met back in Chapter 2 can
help with this.
1104
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
By comparing the expression typeid(*m_pSelected) with typeid(CText), you can determine whether or not m_pSelected is pointing to a CText object. To be able to use the typeid operator in your code, you must change the Enable Run-Time Type Information property value for the project to Yes(/GR). You can open the project’s Properties dialog by pressing Alt+F7; you will fi nd the property to change by selecting Configuration Properties ➪ C/C++ ➪ Language in the left pane of the dialog. Here’s how you can update the MoveElement() function to eliminate the trails when moving text: void CSketcherView::MoveElement(CClientDC& aDC, CPoint& point) { CSize distance = point - m_CursorPos; // Get move distance m_CursorPos = point; // Set current point as 1st for next time // If there is an element, selected, move it if(m_pSelected) { // If the element is text use this method... if (typeid(*m_pSelected) == typeid(CText)) { CRect oldRect=m_pSelected->GetBoundRect(); aDC.LPtoDP(oldRect); m_pSelected->Move(distance); InvalidateRect(&oldRect); UpdateWindow(); m_pSelected->Draw(&aDC,m_pSelected);
// // // // // //
Get old bound rect Convert to client coords Move the element Invalidate combined area Redraw immediately Draw highlighted
return; } // ...otherwise, use this method aDC.SetROP2(R2_NOTXORPEN); m_pSelected->Draw(&aDC,m_pSelected); m_pSelected->Move(distance); m_pSelected->Draw(&aDC,m_pSelected);
// Draw the element to erase it // Now move the element // Draw the moved element
} }
When m_pSelected points to a text element, you obtain the bounding rectangle for the old element before moving the element to its new position, and convert the rectangle to client coordinates, because that’s what InvalidateRect() expects. After the move, you call InvalidateRect() to redraw the rectangle where the element used to be. You can see that the code for invalidating the rectangles that you must use for moving the text is a little less elegant than the ROP code that you use for all the other elements. It works, though, as you’ll see for yourself if you make this modification, and then build and run the application. Because you use the typeid operator for testing the type, you must add an #include directive for the typeinfo header to SketcherView.cpp. This header provides the defi nition for the type_info class, and you need this because the typeid operator returns a reference to a const type_info object.
Dialogs and Controls in CLR Sketcher
❘ 1105
For the program to compile successfully, you need to add a #include directive for TextDialog.h to the SketcherView.cpp fi le. You should now be able to produce annotated sketches using multiple scaled and scrolled views, such as the ones shown in Figure 18 -19.
FIGURE 18-19
You can see that the text does not scale along with the shapes. You could scale the text by adjusting the font size in the device context based on the scale. Of course, you would have to adjust the rectangle enclosing the text when you draw it, too.
DIALOGS AND CONTROLS IN CLR SKETCHER There is a full range of dialogs and controls available for use in a CLR program, but the procedure for adding them to an application is often different from what you have seen in the MFC version of Sketcher. Extending CLR Sketcher will allow you to try out some of the ways in which you can use dialogs and controls in a Windows Forms application. You will not necessarily end up with the GUI for CLR Sketcher that you would want in practice because you will inevitably duplicate some functions in a way that is not needed in the application.
1106
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
Adding a Dialog It would be useful to add a dialog that you can use to enter pen widths, to allow elements to be drawn with lines of different thicknesses. Your fi rst thought as to how to do this is likely to involve the Design window for the form — and indeed, for some predefi ned dialogs this is the place to start. However, when you want to create your own dialog from the ground up, the starting point is Solution Explorer. To add a dialog, you actually add a form to the project that you then modify to make it a dialog window. In Solution Explorer, right- click your project, click the Add menu item, then click New Item. In the dialog that displays, select the UI group in the left pane; then select Windows Form in the right pane. Enter the name as PenDialog.cs and click the OK button. The .cs extension identifies that the dialog is for a C++/CLI program. A new Design window will display showing the dialog. In the Properties window for the dialog, change the FormBorderStyle property in the Appearance group to FixedDialog and set the ControlBox, MinimizeBox, and MaximizeBox properties in the Window Style group to false. Change the value of the Text property to Set Pen Width. You now have a modal dialog for setting the pen width, so next you can add the controls you need to the dialog.
Customizing the Dialog You’ll use radio buttons to allow a pen width to be chosen, and place them within a group box that serves to keep them in a group in which only one radio button can be checked at one time. Drag a GroupBox control from the Containers group in the Toolbox window onto the dialog you have created. Change the value of the Text property for the group box to Select Pen Width and change the value of the (name) property to penWidthGroupBox. Adjust the size of the group box by dragging its border so that it fits nicely within the dialog frame. Drag six RadioButton controls from the Toolbox window to the group box and arrange them in a rectangular configuration as you did in the MFC Sketcher program. You’ll notice that alignment guides to help you position the controls display automatically for each RadioButton control after the fi rst. Change the text properties for the radio button controls to Pen Width 1 through Pen Width 6, and the corresponding (name) property values to penWidthButton1 through penWidthButton6. Change the value of the Checked property for penWidthButton1 to true. Next you can add two buttons to the dialog and change their Text properties to OK and Cancel, and the (name) properties to penWidthOK and penWidthCancel. Return to the dialog’s Properties window and set the AcceptButton and CancelButton property values to penWidthOK and penWidthCancel by selecting from the list in the values column. Verify that the DialogResult property values for the buttons are OK and Cancel; this will cause the appropriate DialogResult value to be returned when the dialog is closed by the user clicking one button or the other. You should now have a dialog in the Design window that looks similar to Figure 18-20.
FIGURE 18-20
Dialogs and Controls in CLR Sketcher
❘ 1107
The code for the dialog class is part of the project and is defi ned in the PenDialog.h header fi le. However, there are no instances of the PenDialog class anywhere at the moment, so to use the dialog you must create one. You will create a PenDialog object in the Form1 class; add an #include directive for PenDialog.h to Form1.h. Add a new private variable of type PenDialog^ to the Form1 class with the name penDialog and initialize it in the Form1 constructor to gcnew PenDialog(). You need a way to get information about which radio button is checked from the PenDialog object. One way to do this is to add a public property to the class to make the information available. It can be a read- only property, so you need only to implement get() for the property. The code that the Windows Form Designer generates is delimited by a #pragma region and a #pragma endregion directive, and you should not modify this manually or add in your own code. Add the following code to the PenDialog class defi nition immediately after the #pragma endregion directive: public: property float PenWidth { float get() { if(penWidthButton1->Checked) return 1.0f; if(penWidthButton2->Checked) return 2.0f; if(penWidthButton3->Checked) return 3.0f; if(penWidthButton4->Checked) return 4.0f; if(penWidthButton5->Checked) return 5.0f; return 6.0f; } }
The System::Drawing::Pen class defi nes a pen where the pen width is a floating-point value; hence, the PenWidth property is floating-point. Now that the dialog is complete, and you have a Form1 member that references a dialog object, you need a mechanism to open the dialog. Another toolbar button on the Form1 window is a good choice.
Displaying the Dialog You need a bitmap to display on the new toolbar button. Switch to the Resource View, right- click the Bitmap folder, and click Insert Bitmap. Set the Height and Width property values for the bitmap to 16, the Filename property value to penwidth.bmp, and the ID to IDB_PENWIDTH. You can now create a bitmap of your choosing to represent a line width; I’ll use a symbol that looks like a pen drawing a line, as in MFC Sketcher. In the Design window for Form1.h, add a separator to the toolbar, followed by a new toolbar button. Change the (name) property value to penWidthButton and the ToolTipText property value to Change pen width. Set the image for the new button to penwidth.bmp, and then create a Click
1108
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
event handler for the toolbar button by double- clicking the Click event in its Properties window. You can add the following code to the new event handler to display the pen dialog: private: System::Void penWidthButton _Click( System::Object^ sender, System::EventArgs^ { if(penDialog->ShowDialog() == System::Windows::Forms::DialogResult::OK) { // Get the pen width ... } }
e)
Calling ShowDialog() for the PenDialog object displays the dialog. Because it is a modal dialog, it remains visible until a button is clicked to close it. The ShowDialog() function returns a value that is an enumerator from the System::Windows::Forms::DialogResult enumeration. This enumeration defi nes the following enumerator values: None, OK, Cancel, Abort, Retry, Ignore, Yes, No
These provide for a variety of buttons being identified to close a dialog, and you can set any of these values from the drop -down list of values for the DialogResult property of a button. You must fully qualify the DialogResult type name here because the Form1 class has an inherited member with the name DialogResult. You need somewhere to record the current pen width in the Form1 class, so add a private penWidth variable of type float to the class and initialize it to 1.0f in the constructor. You can now replace the comment in the preceding Click event handler with the following: penWidth = penDialog->PenWidth;
This will set the current pen width to the value returned by the PenWidth property for the dialog. All that remains to do is to implement creating and drawing elements with a given pen width.
Creating Elements with a Pen Width This will involve modifying the constructor in each of the derived element classes. The modifications are essentially the same in each class, so I’ll just show how the Line class constructor changes, and you can do the rest. The changes to the Line class constructor to allow for different pen widths are as follows: Line(Color color, Point start, Point end, float penWidth) { pen = gcnew Pen(color, penWidth); this->color = color; position = start; this->end = end - Size(start); boundRect = System::Drawing::Rectangle(Math::Min(position.X, end.X), Math::Min(position.Y, end.Y), Math::Abs(position.X - end.X), Math::Abs(position.Y - end.Y));
Dialogs and Controls in CLR Sketcher
❘ 1109
boundRect.Inflate(safe_cast(penWidth), safe_cast(penWidth)); }
The only changes are to add an extra parameter to the Line constructor, to use the Pen constructor that accepts a second argument of type float that specifies the pen width, and to increase the size of the bounding rectangle by the penWidth argument value. Make the same change to the constructors for the other element classes. Because you have changed the parameter list for the element class constructors, you must change the MouseMove event handler that creates elements: private: System::Void Form1_MouseMove(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) { if(drawing) { if(tempElement) Invalidate(tempElement->Bound); // The old element region switch(elementType) { case ElementType::LINE: tempElement = gcnew Line(color, firstPoint, e->Location, penWidth); break; case ElementType::RECTANGLE: tempElement = gcnew Rectangle(color, firstPoint, e->Location, penWidth); break; case ElementType::CIRCLE: tempElement = gcnew Circle(color, firstPoint, e->Location, penWidth); break; case ElementType::CURVE: if(tempElement) safe_cast(tempElement)->Add(e->Location); else tempElement = gcnew Curve(color, firstPoint, e->Location, penWidth); break; } // Rest of the code as before... }
You should now have CLR Sketcher with pen widths fully working, as Figure 18 -21 illustrates.
Using a Combo Box Control You can add a combo box to the toolbar in the Form1 Design window to provide a way to enter a line style. ComboBox is an option in the list that displays when you click the down arrow for the toolbar item that adds new entries in the Design window. You can customize this option to do what you want.
FIGURE 18-21
1110
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
Set the DropDownStyle property to DropDownList; the effect of this is to only allow values to be selected from the list. A value of DropDown allows any value to be typed in the combo box. Change (name) to something more relevant, such as lineStyleComboBox. You want to add a specific set of items to be displayed in the combo box, and the Items property holds these. Select the Items property, click (Collection) in the value column, and select the ellipsis at the right to open the String Collection Editor dialog. You can then enter strings, as shown in Figure 18-22.
FIGURE 18-22
These are the entries that will be displayed in the drop -down for the combo box; they are indexed from 0 to 4. Because the default size for the combo box is not as wide as you need for the longest string, change the value of the Size property to 150,25. You can also change the FlatStyle property to Standard, so that the combo box will be more visible on the toolbar. You can set the ToolTipText property to Choose line style. At present the combo box will not show anything when it is fi rst displayed, but you can fi x that in the Form1 constructor. Add the following line of code to the constructor after the // TODO comment: lineStyleComboBox->SelectedIndex = 0;
The SelectedIndex property determines which of the entries in the Items collection is displayed in the combo box, and because the entries in the combo box Items property collection are indexed from 0, this causes the fi rst entry to be displayed initially. That will continue to be the entry displayed until you select a new entry from the combo box, so the combo box will always show the currently selected line width. You need somewhere to store the current line style, so add a private member, lineStyle, to the Form1 class of type System::Drawing::Drawing2D::DashStyle. This is an enum type used by the Pen class to specify a style for drawing lines. If you add a using directive for the System::Drawing::Drawing2D namespace, you can specify the type for lineStyle as just DashStyle. The DashStyle enum defi nes the members Solid, Dash, Dot, DashDot, DashDotDot, and Custom. The meaning for the fi rst five are obvious. The Custom style gives you complete flexibility in specifying a line style as virtually any sequence of line segments and spaces of differing lengths that you want. You specify how the line style is to appear by setting the DashPattern property for the Pen object. You specify the pattern with an array of float values that specify the lengths of the line segments and intervening spaces. Here’s an example: array^ pattern = {5.0f, 3.0f}; pen->DashPattern = pattern;
Dialogs and Controls in CLR Sketcher
❘ 1111
This specifies a dashed line for the Pen object pen, consisting of segments of length 5.0f separated by spaces of length 3.0f. The pattern you specify is repeated as often as necessary to draw a given line. Obviously, by specifying more elements in the array, you can defi ne patterns as complicated as you like. You need to know when an entry from the combo box is selected so that you can update the lineStyle member of the Form1 class. An easy way to arrange this is to add a handler for the SelectedIndexChanged event for the ComboBox object; add this handler through the Properties window and implement it like this: private: System::Void lineStyleComboBox_SelectedIndexChanged( System::Object^ sender, System::EventArgs^ e) { switch(lineStyleComboBox->SelectedIndex) { case 1: lineStyle = DashStyle::Dash; break; case 2: lineStyle = DashStyle::Dot; break; case 3: lineStyle = DashStyle::DashDot; break; case 4: lineStyle = DashStyle::DashDotDot; break; default: lineStyle = DashStyle::Solid; break; } }
This just sets the value of lineStyle to one of the DashStyle enum values based on the value of the SelectedIndex property for the combo box object. To draw elements with different line styles you must add another parameter of type DashStyle to each of the element constructors. First, add the following using directive to Elements.h: using namespace System::Drawing::Drawing2D;
Here’s how you update the Line class constructor to support drawing lines in different styles: Line(Color color, Point start, Point end, float penWidth, DashStyle style) { pen = gcnew Pen(color, penWidth); pen->DashStyle = style; // Rest of the code for the function as before... }
There’s just one new line of code, which sets the DashStyle property for the pen to the property passed as the last argument to the constructor. Change the other element class constructors in the same way.
1112
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
The last thing you must do is update the constructor calls in the mouse move event handler in the Form1 class: private: System::Void Form1_MouseMove(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) { if(drawing) { if(tempElement) Invalidate(tempElement->bound); // Invalidate old element area switch(elementType) { case ElementType::LINE: tempElement = gcnew Line(color, firstPoint, e->Location, penWidth, lineStyle); break; case ElementType::RECTANGLE: tempElement = gcnew Rectangle(color, firstPoint, e->Location, penWidth, lineStyle); break; case ElementType::CIRCLE: tempElement = gcnew Circle(color, firstPoint, e->Location, penWidth, lineStyle); break; case ElementType::CURVE: if(tempElement) safe_cast(tempElement)->Add(e->Location); else tempElement = gcnew Curve(color, firstPoint, e->Location, penWidth, lineStyle); break; } // Rest of the code for the function as before... }
If you recompile CLR Sketcher and run it again, the combo box will enable you to select a new line style for drawing elements. The line styles don’t really work for curves because of the way they are drawn — as a series of very short line segments. With the other elements, the line styles look better if you draw with a pen width greater than the default.
Creating Text Elements Drawing text in CLR Sketcher will be similar to drawing text in MFC Sketcher. Selecting a Text menu item or clicking a text toolbar button will set Text as the element drawing mode, and in this mode clicking anywhere on the form will display a modal dialog that allows some text to be entered; closing the dialog with the OK button will display the text at the cursor position. There are many ramifications to drawing text, but in the interest of keeping the book to a modest weight, I’ll limit this discussion to the basics.
Dialogs and Controls in CLR Sketcher
❘ 1113
Add a TEXT enumerator to the ElementType enum class, then add a Text menu item to the Elements menu, and a corresponding toolbar button in the Form1 Design window; you will need to create a bitmap resource for the button. Change the value of the (name) property for the menu item to textToolStripMenuItem if necessary, and create a Click event handler for it. You can also add a value for the ToolTipText property for the menu item and the toolbar button. You can create a bitmap for the toolbar button to indicate text mode, and select the Click event handler for the toolbar button to be the Click event handler for the menu item. You can implement the Click event handler like this: private: System::Void textToolStripMenuItem_Click( System::Object^ { elementType = ElementType::TEXT; SetElementTypeButtonState(); }
sender, System::EventArgs^
e)
The function just sets elementType to the ElementType enumerator that represents text drawing mode. Update the SetElementTypeButtonsState() function to check the text button. You will need to update the DropDownOpening event handler for the tool strip to ensure that the new text menu item gets checked. Don’t forget to add a Text menu item to the context menu, too. This will involve amending the handler for the Opening event for the context menu to add the Text menu item, and set it as checked when appropriate.
Drawing Text You need to look into several things before you can make CLR Sketcher create and draw text. Let ’s start with how you draw text. The Graphics class defi nes a DrawString() function for drawing text. There are several overloaded versions of this function, as described in the following table. FUNCTION
DESCRIPTION
DrawString(
Draws str at the position point using font, with the color determined by brush. Type Windows::Drawing::PointF is a point represented by coordinates of type float. You can use a Point object as an argument in any of the functions for a PointF parameter.
String^ str, Font^ font, Brush^ brush, PointF point) DrawString( String^ str, Font^ font,
Draws str at the position (X,Y) using font, with the color determined by brush. You can also use coordinate arguments of type int when you call this function.
Brush^ brush, float X, float Y)
continues
1114
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
(continued) DrawString( String^ str, Font^ font, Brush^ brush,
Draws str within the rectangle rect using font, with the color determined by brush. Type Windows::Drawing::RectangleF defines a rectangle with its position width and height specified by values of type float. You can use a Rectangle object as an argument in any of the functions for a RectangleF parameter.
RectangleF rect) DrawString( String^ str, Font^ font,
Draws str at the position point using font, with the color determined by brush and the formatting of the string specified by format. A StringFormat object determines the alignment and other formatting properties for a string.
Brush^ brush, PointF point, StringFormat^ format) DrawString( String^ str, Font^ font,
Draws str at the position (X,Y) using font, with the color determined by brush and the formatting of the string specified by format.
Brush^ brush, float X, float Y, StringFormat^ format) DrawString( String^ str, Font^ font,
Draws str within the rectangle rect using font, with the color determined by brush and the formatting of the string determined by format.
Brush^ brush, RectangleF rect, StringFormat^ format)
Clearly, you need to understand a bit about fonts and brushes in order to use the DrawString() function, so I’ll briefly introduce those before returning to how you can create and display Text elements.
Creating Fonts You specify the font to be used when you draw a string by a System::Drawing::Font object that defi nes the typeface, style, and size of the drawn characters. An object of type System::Drawing:: FontFamily defi nes a group of fonts with a given typeface, such as “ Arial” or “Times New Roman”. The System::Drawing::FontStyle enumeration defi nes possible font styles, which can be any of the following: Regular, Bold, Italic, Underline, and Strikeout. You can create a Font object like this:
Dialogs and Controls in CLR Sketcher
❘ 1115
FontFamily^ family = gcnew FontFamily(L"Arial"); System::Drawing::Font^ font = gcnew System::Drawing::Font(family, 10, FontStyle::Bold, GraphicsUnit::Point);
You create the FontFamily object by passing the name of the font to the constructor. The arguments to the Font constructor are the font family, the size of the font, the font style, and an enumerator from the GraphicUnit enumeration that defi nes the units for the font size. The possible enumerator values are as follows: World
The world coordinate system is the unit of measure.
Display
The unit of measure is that of the display device: pixels for monitors, and 1/100 inch for printers.
Pixel
A device pixel is the unit of measure.
Point
The unit of measure is a point (1/72 inch).
Inch
The unit of measure is the inch.
Document
The unit of measure is the document unit, which is 1/300 inch.
Millimeter
The millimeter is the unit of measure.
Thus, the previous code fragment defi nes a 10 -point bold Arial font. Note that the Font type name is fully qualified in the fragment because a form object in a Windows Forms application inherits a property with the name Font from the Form base class that identifies the default font for the form. Windows Forms applications support TrueType fonts primarily, so it is best not to choose Open Type fonts. If you attempt to use a font that is not supported, or the font is not installed on your computer, the Microsoft Sans Serif font will be used.
Creating Brushes A System::Drawing::Brush object determines the color used to draw a string with a given font; it is also used to specify the color and texture used to fi ll a shape. You can’t create a Brush object directly because Brush is an abstract class. You create brushes using the SolidBrush, the TextureBrush, and the LinearGradientBrush class types derived from Brush. A SolidBrush object is a brush of a single color, a TextureBrush object is a brush that uses an image to fi ll the interior of a shape, and a LinearGradientBrush is a brush defi ning a color gradient blend, usually between two colors, but also possibly among several colors. You will use a SolidBrush object to draw text, which you create like this: SolidBrush^ brush = gcnew SolidBrush(Color::Red);
This creates a solid brush that will draw text in red.
Choosing a Font You can store a reference to the current Font object, to be used when CLR Sketcher is creating Text elements, by adding a private variable of type System::Drawing::Font^, and with the name
1116
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
textFont, to the Form1 class. You must use the fully qualified type name to avoid confusion with the inherited property with the name Font. Initialize textFont in the Form1 constructor to Font, which is a form property specifying the default font for the form. Be sure to do this in the body of the constructor following the // TODO: comment; if you attempt to initialize it in the initialization list, the program will fail because Font is not defi ned at this point.
You can add a toolbar button to allow the user to select a font for entering text, perhaps a button with a bitmap representation of F for font. Give the button a suitable value for the (name) property, such as toolStripFontButton, and add a Click event handler for it. The Toolbox window has a standard dialog for choosing a font that will display all the fonts available on your system and allow selection of the font style and size. Select the Design window for Form1.h and drag a FontDialog control from the Toolbox window to the form. The Click event handler for the toolStripFontButton can display the font dialog: private: System::Void toolStripFontButton_Click( System::Object^ sender, System::EventArgs^ { if(fontDialog1->ShowDialog() == System::Windows::Forms::DialogResult::OK) { textFont = fontDialog1->Font; } }
e)
You display the dialog by calling its ShowDialog() function, as you did for the pen dialog. If the function returns DialogResult::OK, you know the dialog was closed with the OK button. In this case, you retrieve the chosen font from the Font property for the dialog object, and store it in the textFont variable in the Form1 class object. You can try this out if you want. The Font dialog will look like Figure 18-23. You need a class to represent a Text element, so let’s defi ne that next.
Defining the Text Element Class
FIGURE 18-23
The TextElement element type will have Element as a base class like the other element types, so you can call the Draw() function polymorphically. Basic information about the location and color of the text element will be recorded in the members inherited from the base class, but you need to add extra members to store the text and information relating to the font. Here’s the initial defi nition of the class: public ref class TextElement : Element { protected: String^ text; SolidBrush^ brush; Font^ font;
Dialogs and Controls in CLR Sketcher
❘ 1117
public: TextElement(Color color, Point p, String^ text, Font^ font) { this->color = color; brush = gcnew SolidBrush(color); position = p; this->text = text; this->font = font; int height = font->Height; // Height of text string int width = static_cast(font->Size*text->Length); // Width of string boundRect = System::Drawing::Rectangle(position, Size(width, height)); boundRect.Inflate(2,2); // Allow for descenders } virtual void Draw(Graphics^ g) override { brush->Color = highlighted ? highlightColor : color; g->TranslateTransform(safe_cast(position.X), safe_cast(position.Y)); g->DrawString(text, font, brush, Point(0,0)); g->ResetTransform(); } };
I chose the TextElement type name, rather than just Text, to avoid confusion with a member of the Form class with the name Text. A TextElement object has members to store the text string, the font, and the brush to be used to draw the text. Determining the bounding rectangle for a text element introduces a slight complication in that it depends on the point size of the font, and you have to figure out the width and height from the font size. The height is easy because the font object has a Height property that makes the font’s line spacing available, which is a good estimate for the height of the text string. The Size property returns the em size of the font (which is the width of the letter M), so you can get a generous estimate of the width of the rectangle the string will occupy by multiplying the em size by the number of characters in the string. I have inflated the rectangle by two because the line spacing doesn’t always result in a bounding rectangle that takes account of descenders such as in lowercase ps and qs.
Creating the Text Dialog You’ll want to enter text from the keyboard when you create a TextElement element, so you need a dialog to manage this. Go to the Solution Explorer window, and add a new form to CLR Sketcher by right- clicking the project name and clicking Add ➪ New Item from the menu. Enter the name as TextDialog. Change the Text property for the form to Create Text Element, and change the properties to make it a dialog as you did for PenDialog; this involves changing the FormBorderStyle property to FixedDialog and setting MaximizeBox, MinimizeBox, and ControlBox properties to false. The next step is to add buttons to close the dialog. Add an OK button and a Cancel button to the text dialog with the DialogResult property values set appropriately. Change the (name) property values to textOKButton and textCancelButton respectively. Set the AcceptButton property value for TextDialog to textOKButton and the CancelButton property value to textCancelButton.
1118
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
Using a Text Box A TextBox control allows a single line or multiple lines of text to be entered, so it certainly covers what you want to do here. Add a TextBox to the text dialog by dragging it from the Toolbox window to the dialog in the Design window. By default, a TextBox allows a single line of text to be entered, and you can stick with that here. When you want to allow multiple lines of input, you click the arrow to the right on the TextBox in the Design window, and click the checkbox to enable Multiline. The Text property for the TextBox provides access to the input and makes it available as a String object. Ideally, you want the text dialog to open with the focus on the box for entering text. Then you can enter the text immediately after the dialog opens, and press enter to close it as if you had selected the OK button. The control that has the focus initially is determined by the tab order of the controls on the dialog, which depends on the value of the TabIndex property for the controls. If you set the TabIndex property for the text box to 0 and the OK and Cancel buttons to 1 and 2 respectively, this will result in the text box having the focus when the dialog initially displays. The tab order also defi nes the sequence in which the focus changes when you press the tab key. You can display the tab order for the controls in the Design window for the dialog by selecting View ➪ Tab Order from the main menu; selecting the menu item again removes the display of the tab order. The textBox1 control will store the string you enter, but because this is a private member of the dialog object, it is not accessible directly. You must add a mechanism to retrieve the string from the dialog object. Another problem is that the textBox1 control will retain the text you enter and display it the next time the dialog is opened. You probably don’t want this to occur, so you need a way to reset the Text property of textBox1. You can add a public property to the TextDialog class that will deal with both difficulties. Add the following code to the TextDialog class defi nition, following the #pragma endregion directive: // Property to access the text in the edit box public: property String^ TextString { String^ get() {return textBox1->Text; } void set(String^ text) { textBox1->Text = text; } }
The get() function for the TextString property makes the string you enter available, and the set() function enables you to reset it. It would be nice if the text appeared in the edit box in the current font so that the user could tell whether it’s what he or she wants. This will provide an opportunity to cancel the text entry and change to another font. Add the following property to the TextDialog class, following the preceding one: // Set the edit box font public: property System::Drawing::Font^ TextFont { void set(System::Drawing::Font^ font) { textBox1->Font = font; } }
This is a set- only property that you can use to set the font for the edit box object textBox1.
Dialogs and Controls in CLR Sketcher
❘ 1119
You need an extra data member in the Form1 class to help you create text elements. Add an #include directive for TextDialog.h to Form1.h, then add a textDialog member of type TextDialog^. You can initialize it to gcnew TextDialog() in the initialization list for the Form1 class constructor.
Displaying the Dialog and Creating a Text Element The process for creating an element that is text is different from the process for geometric elements, so to understand the sequence of events in the code, let’s describe the interactive process. Text element mode is in effect when you select the Text menu item or toolbar button. To create an element, you click at the position on the form where you want the top left corner of the text string to be. This will display the text dialog; you type the text you want in the text box, and press Enter to close the dialog. The MouseMove handler is not involved in the process at all. Clicking the mouse to define the position of the element embodies the whole process. This implies that you must display the dialog and create the text element in the MouseDown event handler. Here’s the code to do that: private: System::Void Form1_MouseDown(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) { if(e->Button == System::Windows::Forms::MouseButtons::Left) { firstPoint = e->Location; if(mode == Mode::Normal) { if(elementType == ElementType::TEXT) { textDialog->TextString = L””; // Reset the text box string textDialog->TextFont = textFont; // Set the font for the edit box if(textDialog->ShowDialog() == System::Windows::Forms::DialogResult::OK) { tempElement = gcnew TextElement(color, firstPoint, textDialog->TextString, textFont); sketch += tempElement; Invalidate(tempElement->bound); // The text element region tempElement = nullptr; Update(); } drawing = false; } else { drawing = true; } } } }
You create text elements only when the elementType member of the form has the value ElementType::TEXT and the mode is Mode::Normal; the second condition is essential to avoid displaying the dialog when you are in move mode. When a text element is being created, the fi rst action is to reset the Text property for the textBox1 control to an empty string by setting it as
1120
❘
CHAPTER 18
WORKING WITH DIALOGS AND CONTROLS
the value for the TextString property for the dialog. You also set the font for the edit box to the value stored in the textFont member of the form. You display the dialog by calling ShowDialog() in the if condition expression. If the ShowDialog() function returns DialogResult::OK, you retrieve
the string from the dialog, use it to create the TextElement object, and add the object to the sketch. You then invalidate the region occupied by the new element and call Update() to display it. You also reset tempElement to nullptr when you are done with it. Finally, you set drawing to false to prevent the MouseMove handler from attempting to create an element.
FIGURE 18-24
If you recompile Sketcher, you should be able to create text elements using a font of your choice. Not only that, but you can move and delete them too. Figure 18 -24 shows Sketcher displaying text elements.
SUMMARY In this chapter you’ve seen several different dialogs using a variety of controls. Although you haven’t created dialogs involving several different controls at once, the mechanism for handling them is the same as what you have seen, because each control can operate independently of the others. Dialogs are a fundamental tool for managing user input in an application. They provide a way for you to manage the input of multiple related items of data. You can easily ensure that an application only receives valid data. Judicious choice of the controls in a dialog can force the user to choose from a specific set of options. You can also check the data after it has been entered in a dialog and prompt the user when it is not valid.
EXERCISES You can download the source code for the examples in the book and the solutions to the following exercises from www.wrox.com.
1.
Implement the scale dialog in MFC Sketcher using radio buttons.
2.
Implement the pen width dialog in MFC Sketcher using a list box.
3.
Implement the pen width dialog in MFC Sketcher as a combo box with the drop list type selected on the Styles tab in the Properties box. (The drop list type allows the user to select from a drop down list but not to enter alternative entries in the list.)
Summary
❘ 1121
WHAT YOU LEARNED IN THIS CHAPTER TOPIC
CONCEPT
Dialogs
A dialog involves two components: a resource defining the dialog box and its controls, and a class that is used to display and manage the dialog.
Extracting data from a dialog
Information can be extracted from controls in a dialog by means of the DDX mechanism. The data can be validated with the DDV mechanism. To use DDX/DDV you need only use the Add Control Variable option for the Add Member Variable wizard to define variables in the dialog class associated with the controls.
Modal dialogs
A modal dialog retains the focus in the application until the dialog box is closed. As long as a modal dialog is displayed, all other windows in an application are inactive.
Modeless dialogs
A modeless dialog allows the focus to switch from the dialog box to other windows in the application and back again. A modeless dialog can remain displayed as long as the application is executing, if required.
Common controls
Common Controls are a set of standard Windows controls supported by MFC and the resource editing capabilities of Developer Studio.
Adding controls
Although controls are usually associated with a dialog, you can add controls to any window.
Adding a new form in a Windows Forms application
You add a form to a Windows Forms application using the Solutions Explorer pane; you right- click the project name and select Add ➪ New Item from the menu.
Converting a form into a modal dialog
You convert a form into a modal dialog window by changing the value of the FormBorderStyle property to FixedDialog, and changing the values of the MaximizeBox, MinimizeBox, and ControlBox properties to false.
Adding controls to a dialog
You populate a dialog with controls by dragging them from the Toolbox window onto the dialog in the Design pane.
Displaying a dialog
You display a dialog by calling its ShowDialog() function.
The DialogResult property for a button
The value that you set for the DialogResult property for a button in a dialog determines the value returned by the ShowDialog() function when that button is used to close the dialog.
Using standard dialogs
The toolbox has several complete standard dialogs available, including a dialog for choosing a font.
Drawing test on a form
You can draw a string on a form by calling the DrawString() function for the Graphics object that encapsulates the drawing surface of a form. A Font object argument to the function determines the typeface, style, and size of the characters drawn, and a Brush object argument determines the color.
19
Storing and Printing Documents WHAT YOU WILL LEARN IN THIS CHAPTER:
➤
How serialization works
➤
How to make objects of a class serializable
➤
The role of a CArchive object in serialization
➤
How to implement serialization in your own classes
➤
How to implement serialization in the Sketcher application
➤
How printing works with MFC
➤
Which view class functions you can use to support printing
➤
What a CPrintInfo object contains and how it ’s used in the printing process
➤
How to implement multipage printing in the Sketcher application
With what you have accomplished so far in the Sketcher program, you can create a reasonably comprehensive document with views at various scales, but the information is transient because you have no means of saving a document. In this chapter, you’ll remedy that by seeing how you can store a document on disk. You’ll also investigate how you can output a document to a printer.
UNDERSTANDING SERIALIZATION A document in an MFC -based program is not a simple entity — it’s a class object that can be very complicated. It typically contains a variety of objects, each of which may contain other objects, each of which may contain still more objects . . . and that structure may continue for a number of levels.
1124
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
You want to be able to save a document in a file, but writing a class object to a fi le represents something of a problem because it isn’t the same as a basic data item like an integer or a character string. A basic data item consists of a known number of bytes, so to write it to a fi le only requires that the appropriate number of bytes be written. Conversely, if you know a value of type int was written to a fi le, to get it back, you just read the appropriate number of bytes. Writing objects is different. Even if you write away all the data members of an object, that’s not enough to be able to get the original object back. Class objects contain function members as well as data members, and all the members, both data and functions, have access specifiers; therefore, to record objects in an external fi le, the information written to the fi le must contain complete specifications of all the class structures involved. The read process must also be clever enough to synthesize the original objects completely from the data in the file. MFC supports a mechanism called serialization to help you to implement input from and output to disk of your class objects with a minimum of time and trouble. The basic idea behind serialization is that any class that’s serializable must take care of storing and retrieving itself. This means that for your classes to be serializable — in the case of the Sketcher application, this will include the CElement class and the shape classes you have derived from it — they must be able to write themselves to a file. This implies that for a class to be serializable, all the class types that are used to declare data members of the class must be serializable, too.
SERIALIZING A DOCUMENT This all sounds rather tricky, but the basic capability for serializing your document was built into the application by the Application Wizard right at the outset. The handlers for the File ➪ Save, File ➪ Save As, and File ➪ Open menu items all assume that you want serialization implemented for your document, and already contain the code to support it. Take a look at the parts of the defi nition and implementation of CSketcherDoc that relate to creating a document using serialization.
Serialization in the Document Class Definition The code in the defi nition of CSketcherDoc that enables serialization of a document object is shown shaded in the following fragment: class CSketcherDoc : public CDocument { protected: // create from serialization only CSketcherDoc(); DECLARE_DYNCREATE(CSketcherDoc) // Rest of the class... // Overrides public: virtual BOOL OnNewDocument(); virtual void Serialize(CArchive& ar); // Rest of the class... };
Serializing a Document
❘ 1125
There are three things here that relate to serializing a document object:
1. 2. 3.
The DECLARE_DYNCREATE() macro. The Serialize() member function. The default class constructor.
DECLARE_DYNCREATE() is a macro that enables objects of the CSketcherDoc class to be created dynamically by the application framework during the serialization input process. It’s matched by a complementary macro, IMPLEMENT_DYNCREATE(), in the class implementation. These macros apply only to classes derived from CObject, but as you will see shortly, they aren’t the only pair of macros that can be used in this context. For any class that you want to serialize, CObject must be a direct or indirect base because it adds the functionality that enables serialization to work. This is why the CElement class was derived from CObject. Almost all MFC classes are derived from CObject and, as such, are serializable.
NOTE The Hierarchy Chart in the Microsoft Foundation Class Reference for Visual C++ 2010 shows those classes, which aren’t derived from CObject. Note that CArchive is in this list.
The class defi nition also includes a declaration for a virtual function Serialize(). Every class that’s serializable must include this function. It’s called to perform both input and output serialization operations on the data members of the class. The object of type CArchive that’s passed as an argument to this function determines whether the operation that is to occur is input or output. You’ll explore this in more detail when considering the implementation of serialization for the document class. Note that the class explicitly defi nes a default constructor. This is also essential for serialization to work because the default constructor is used by the framework to synthesize an object when reading from a fi le, and the synthesized object is then filled out with the data from the fi le to set the values of the data members of the object.
Serialization in the Document Class Implementation There are two bits of the SketcherDoc.cpp fi le containing the implementation of CSketcherDoc that relate to serialization. The fi rst is the macro IMPLEMENT_DYNCREATE() that complements the DECLARE_DYNCREATE() macro: // SketcherDoc.cpp : implementation of the CSketcherDoc class // #include "stdafx.h" // SHARED_HANDLERS can be defined in an ATL project implementing preview, thumbnail // and search filter handlers and allows sharing of document code with that project. #ifndef SHARED_HANDLERS #include "Sketcher.h"
1126
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
#include "PenDialog.h" #endif #include "SketcherDoc.h" #include <propkey.h> #ifdef _DEBUG #define new DEBUG_NEW #endif // CSketcherDoc
IMPLEMENT_DYNCREATE(CSketcherDoc, CDocument) // Message maps and the rest of the file...
This macro defi nes the base class for CSketcherDoc as CDocument. This is required for the proper dynamic creation of a CSketcherDoc object, including members that are inherited from the base class.
The Serialize() Function The class implementation also includes the defi nition of the Serialize() function: void CSketcherDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO: add storing code here } else { // TODO: add loading code here } }
This function serializes the data members of the class. The argument passed to the function, ar, is a reference to an object of the CArchive class. The IsStoring() member of this class object returns TRUE if the operation is to store data members in a fi le, and FALSE if the operation is to read back data members from a previously stored document. Because the Application Wizard has no knowledge of what data your document contains, the process of writing and reading this information is up to you, as indicated by the comments. To understand how you do this, let’s look a little more closely at the CArchive class.
The CArchive Class The CArchive class is the engine that drives the serialization mechanism. It provides an MFC -based equivalent of the stream operations in C++ that you used for reading from the keyboard and writing to the screen in the console program examples. An object of the MFC class CArchive provides a
Serializing a Document
❘ 1127
mechanism for streaming your objects out to a file, or recovering them again as an input stream, automatically reconstituting the objects of your class in the process. A CArchive object has a CFile object associated with it that provides disk input/output capability for binary files, and provides the actual connection to the physical file. Within the serialization process, the CFile object takes care of all the specifics of the file input and output operations, and the CArchive object deals with the logic of structuring the object data to be written or reconstructing the objects from the information read. You need to worry about the details of the associated CFile object only if you are constructing your own CArchive object. With the document in Sketcher, the framework has already taken care of it and passes the CArchive object that it constructs, ar, to the Serialize() function in CSketcherDoc. You’ll be able to use the same object in each of the Serialize() functions you add to the shape classes when you implement serialization for them. The CArchive class overloads the extraction and insertion operators (>> and > elementCount; // retrieve the element count // Now retrieve all the elements and store in the list for(size_t i = 0 ; i < elementCount ; ++i) { ar >> pElement; m_ElementList.push_back(pElement); } } }
For three of the data members, you just use the extraction and insertion operators that are overloaded in the CArchive class. This works for the data member m_Color, even though its type is COLORREF, because type COLORREF is the same as type long. The m_Element member is of type ElementType, and the serialization process won’t handle this directly. However, an enum type is an integer type, so you can cast it to an integer for serialization, and then just deserialize it as an integer before casting the value back to ElementType. For the list of elements, m_ElementList, you fi rst store the count of the number of elements in the list in the archive, because you will need this to be able to read the elements back. You then write the elements from the list to the archive in the for loop. You use the m_Color >> m_EnclosingRect; } }
// the pen width // Store the color, // and the enclosing rectangle
// the pen width // the color // and the enclosing rectangle
Applying Serialization
❘ 1137
This function is of the same form as the one supplied for you in the CSketcherDoc class. All of the data members defined in CElement are supported by the overloaded extraction and insertion operators, and so, everything is done using those operators. Note that you must call the Serialize() member for the CObject class to ensure that the inherited data members are serialized. For the CLine class, you can code the function as: void CLine::Serialize(CArchive& ar) { CElement::Serialize(ar); if (ar.IsStoring()) { ar m_StartPoint >> m_EndPoint; }
// Call the base class function
// Store the line start point, // and the end point
// Retrieve the line start point, // and the end point
}
Again, the data members are all supported by the extraction and insertion operators of the CArchive object ar. You call the Serialize() member of the base class CElement to serialize its data members, and this calls the Serialize() member of CObject. You can see how the serialization process cascades through the class hierarchy. The Serialize() function member of the CRectangle class is simple: void CRectangle::Serialize(CArchive& ar) { CElement::Serialize(ar); // Call the base class function }
This only calls the direct base class Serialize() function because the class has no additional data members. The CCircle class doesn’t have additional data members beyond those inherited from CElement, either, so its Serialize() function also just calls the base class function: void CCircle::Serialize(CArchive& ar) { CElement::Serialize(ar); }
// Call the base class function
For the CCurve class, you have rather more work to do. The CCurve class uses a vector container to store the defi ning points, and because this is not directly serializable, you must
1138
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
take care of it yourself. Having serialized the document, this is not going to be terribly difficult. You can code the Serialize() function as follows: void CCurve::Serialize(CArchive& ar) { CElement::Serialize(ar); // // Serialize the vector of points if (ar.IsStoring()) { ar nPoints; // // Now retrieve the points CPoint point; for(size_t i = 0 ; i < nPoints ; ++i) { ar >> point; m_Points.push_back(point); } } }
Call the base class function
Store the point count ; ++i)
Stores number of points Retrieve the number of points
You fi rst call the base class Serialize() function to deal with serializing the inherited members of the class. Storing the contents of the vector is basically the same as the technique you used for serializing the document. You fi rst write the number of elements to the archive, then the elements themselves. The CPoint class is serializable, so it takes care of itself. Reading the points back is equally straightforward. You just store each object read from the archive in the vector, m_Points, in the for loop. The serialization process uses the no-arg constructor for the CCurve class to create the basic class object, so the vector member is created within this process. The last class for which you need to add an implementation of Serialize() to Elements.cpp is CText: void CText::Serialize(CArchive& ar) { CElement::Serialize(ar); if (ar.IsStoring()) { ar > m_String; } }
// Call the base class function
// Store the text string
// Retrieve the text string
Exercising Serialization
❘ 1139
After calling the base class function, you serialize the m_String data member using the insertion and extraction operators for ar. The CString class, although not derived from CObject, is still fully supported by CArchive with these overloaded operators.
EXERCISING SERIALIZATION That ’s all you have to do to implement the storing and retrieving of documents in the Sketcher program! The Save and Open menu options in the fi le menu are now fully operational without adding any more code. If you build and run Sketcher after incorporating the changes I ’ve discussed in this chapter, you’ll be able to save and restore fi les and be automatically prompted to save a modifi ed document when you try to close it or exit from the program, as shown in Figure 19 -2.
FIGURE 19 -2
The prompting works because of the SetModifiedFlag() calls that you added everywhere you updated the document. If you click the Yes button in the screen shown in Figure 19-2, you’ll see the File ➪ Save As dialog box shown in Figure 19-3.
1140
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
FIGURE 19 -3
This is the standard dialog box for this menu item under Windows. If yours looks a little different, it’s probably because you are using a different version of the Windows operating system from mine. The dialog is all fully working, supported by code supplied by the framework. The fi le name for the document has been generated from that assigned when the document was fi rst opened, and the fi le extension is automatically defi ned as .ske. The application now has full support for fi le operations on documents. Easy, wasn’t it?
PRINTING A DOCUMENT It’s time to take a look at how you can print a sketch. You already have a basic printing capability implemented in the Sketcher program, courtesy of the Application Wizard and the framework. The File ➪ Print, File ➪ Print Setup, and File ➪ Print Preview menu items all work. Selecting the File ➪ Print Preview menu item displays a window showing the current Sketcher document on a page, as shown in Figure 19- 4. Whatever is in the current document is placed on a single sheet of paper at the current view scale. If the document’s extent is beyond the boundary of the paper, the section of the document off the paper won’t be printed. If you select the Print button, this page is sent to your printer.
FIGURE 19 -4
Printing a Document
❘ 1141
As a basic capability that you get for free, it’s quite impressive, but it’s not adequate for our purposes. A typical document in our program may well not fit on a page, so you would either want to scale the document to fit, or, perhaps more conveniently, print the whole document over as many pages as necessary. You can add your own print processing code to extend the capability of the facilities provided by the framework, but to implement this, you fi rst need to understand how printing has been implemented in MFC.
The Printing Process Printing a document is controlled by the current view. The process is inevitably a bit messy because printing is inherently a messy business, and it potentially involves you in implementing your own versions of quite a number of inherited functions in your view class. Figure 19-5 shows the logic of the process and the functions involved. It also shows how the sequence of events is controlled by the framework and how printing a document involves calling five inherited members of your view class, which you may need to override. The CDC member functions shown on the left side of the diagram communicate with the printer device driver and are called automatically by the framework.
View Members
loop while there are more pages
CDC::StartPage()
5
CDC::EndPage()
7
CDC::EndDoc()
8
FIGURE 19 -5
OnPreparePrinting()
• Calculate page count • Call DoPreparePrinting()
2
OnBeginPrinting()
• Allocate GDI resources
4
OnPrepareDC()
6
OnPrint()
9
OnEndPrinting()
3
The Framework
CDC::StartDoc()
1
• Change viewpoint origin • Set DC attributes
• Print headers/footers • Print current page
• De-Allocate GDI resources
1142
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
The typical role of each of the functions in the current view during a print operation is specified in the notes alongside it. The sequence in which they are called is indicated by the numbers on the arrows. In practice, you don’t necessarily need to implement all of these functions, only those that you want to for your particular printing requirements. Typically, you’ll want at least to implement your own versions of OnPreparePrinting(), OnPrepareDC(), and OnPrint(). You’ll see an example of how these functions can be implemented in the context of the Sketcher program a little later in this chapter. The output of data to a printer is done in the same way as outputting data to the display — through a device context. The GDI calls that you use to output text or graphics are device-independent, so they work just as well for a printer as they do for a display. The only difference is the device that the CDC object applies to. The CDC functions in Figure 19-5 communicate with the device driver for the printer. If the document to be printed requires more than one printed page, the process loops back to call the OnPrepareDC() function for each successive new page, as determined by the EndPage() function. All the functions in your view class that are involved in the printing process are passed a pointer to an object of type CPrintInfo as an argument. This object provides a link between all the functions that manage the printing process, so take a look at the CPrintInfo class in more detail.
The CPrintInfo Class A CPrintInfo object has a fundamental role in the printing process because it stores information about the print job being executed and details of its status at any time. It also provides functions for accessing and manipulating this data. This object is the means by which information is passed from one view function to another during printing, and between the framework and your view functions. An object of the CPrintInfo class is created whenever you select the File ➪ Print or File ➪ Print Preview menu options. After being used by each of the functions in the current view that are involved in the printing process, it’s automatically deleted when the print operation ends. All the data members of CPrintInfo are public. The ones we are interested in for printing sketches are shown in the following table. MEMBER
USAGE
m_pPD
A pointer to the CPrintDialog object that displays the Print dialog box.
m_bDirect
This is set to TRUE by the framework if the print operation is to bypass the Print dialog box; otherwise, FALSE.
m_bPreview
A member of type BOOL that has the value TRUE if File ➪ Print Preview was selected; otherwise, FALSE.
m_bContinuePrinting
A member of type BOOL. If this is set to TRUE, the framework continues the printing loop shown in the diagram. If it ’s set to FALSE, the printing loop ends. You only need to set this variable if you don’t pass a page count for the print operation to the CPrintInfo object (using the SetMaxPage() member function). In this case, you’ll be responsible for signaling when you’re finished by setting this variable to FALSE. continues
Printing a Document
❘ 1143
(continued) MEMBER
USAGE
m_nCurPage
A value of type UINT that stores the page number of the current page. Pages are usually numbered starting from 1.
m_nNumPreviewPages
A value of type UINT that specifies the number of pages displayed in the Print Preview window. This can be 1 or 2.
m_lpUserData
This is of type LPVOID and stores a pointer to an object that you create. This allows you to create an object to store additional information about the printing operation and associate it with the CPrintInfo object.
m_rectDraw
A CRect object that defines the usable area of the page in logical coordinates.
m_strPageDesc
A CString object containing a format string used by the framework to display page numbers during print preview.
A CPrintInfo object has the public member functions shown in the following table. FUNCTION
DESCRIPTION
SetMinPage(UINT nMinPage)
The argument specifies the number of the first page of the document. There is no return value.
SetMaxPage(UINT nMaxPage)
The argument specifies the number of the last page of the document. There is no return value.
GetMinPage() const
Returns the number of the first page of the document as type UINT.
GetMaxPage() const
Returns the number of the last page of the document as type UINT.
GetFromPage() const
Returns the number of the first page of the document to be printed as type UINT. This value is set through the print dialog.
GetToPage() const
Returns the number of the last page of the document to be printed as type UINT. This value is set through the print dialog.
When you’re printing a document consisting of several pages, you need to figure out how many printed pages the document will occupy, and store this information in the CPrintInfo object to make it available to the framework. You can do this in your version of the OnPreparePrinting() member of the current view.
1144
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
To set the number of the first page in the document, you need to call the function SetMinPage() in the CPrintInfo object, which accepts the page number as an argument of type UINT. There’s no return value. To set the number of the last page in the document, you call the function SetMaxPage(), which also accepts the page number as an argument of type UINT and doesn’t return a value. If you later want to retrieve these values, you can call the GetMinPage() and GetMaxPage() functions for the CPrintInfo object. The page numbers that you supply are stored in the CPrintDialog object pointed to by the m_pPD member of CPrintInfo, and displayed in the dialog box that pops up when you select File ➪ Print... from the menu. The user is then able to specify the numbers of the fi rst and last pages that are to be printed. You can retrieve the page numbers entered by the user by calling the GetFromPage() and GetToPage() members of the CPrintInfo object. In each case, the value returned is of type UINT. The dialog automatically verifi es that the numbers of the fi rst and last pages to be printed are within the range you supplied by specifying the minimum and maximum pages of the document. You now know what functions you can implement in the view class to manage printing for yourself, with the framework doing most of the work. You also know what information is available through the CPrintInfo object that is passed to the functions concerned with printing. You’ll get a much clearer understanding of the detailed mechanics of printing if you implement a basic multipage print capability for Sketcher documents.
IMPLEMENTING MULTIPAGE PRINTING You use the MM_LOENGLISH mapping mode in the Sketcher program to set things up and then switch to MM_ANISOTROPIC. This means that the unit of measure for the shapes and the view extent is one hundredth of an inch. Of course, with the unit of size being a fi xed physical measure, ideally, you want to print objects at their actual size. With the document size specified as 3000 by 3000 units, you can create documents up to 30 inches square, which spreads over quite a few sheets of paper if you fi ll the whole area. It requires a little more effort to work out the number of pages necessary to print a sketch than with a typical text document because in most instances, you’ll need a two -dimensional array of pages to print a complete sketch document. To avoid overcomplicating the problem, assume that you’re printing on a normal sheet of paper (either A4 size or 8 1/2 by 11 inches) and that you are printing in portrait orientation (which means the long edge is vertical). With either paper size, you’ll print the document in a central portion of the paper measuring 7.5 inches by 10 inches. With these assumptions, you don’t need to worry about the actual paper size; you just need to chop the document into 750 by 1000 – unit chunks, where a unit is 0.01 inches. For a document larger than one page, you’ll divide up the document as illustrated in the example in Figure 19- 6.
Implementing Multipage Printing
❘ 1145
Number of widths 4 Page 2
Number of heights 2
Page 1
Page 3
Page 4
10 inches
Wrox Press
Page 5
Page 6
Page 7
Page 8 7.5 inches
FIGURE 19 - 6
As you can see, you’ll be numbering the pages row-wise, so in this case, pages 1 to 4 are in the fi rst row and pages 5 to 8 are in the second. A sketch occupying the maximum size of 30⫻30 inches will print on 12 pages.
Getting the Overall Document Size To figure out how many pages a particular document occupies, you need to know how big the sketch is, and for this, you want the rectangle that encloses everything in the document. You can do this easily by adding a function GetDocExtent() to the document class, CSketcherDoc. Add the following declaration to the public interface for CSketcherDoc: CRect GetDocExtent();
// Get the bounding rectangle for the whole document
The implementation is no great problem. The code for it is: // Get the rectangle enclosing the entire document CRect CSketcherDoc::GetDocExtent() { CRect docExtent(0,0,1,1); // Initial document extent for(auto iter = m_ElementList.begin() ; iter != m_ElementList.end() ; ++iter) docExtent.UnionRect(docExtent, (*iter)->GetBoundRect()); docExtent.NormalizeRect(); return docExtent; }
You can add this function defi nition to the SketcherDoc.cpp fi le, or simply add the code if you used the Add ➪ Add Function capability from the pop -up in Class View. The process loops through
1146
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
every element in the document, getting the bounding rectangle for each element and combining it with docExtent. The UnionRect() member of the CRect class calculates the smallest rectangle that contains the two rectangles passed as arguments, and puts that value in the CRect object for which the function is called. Therefore, docExtent keeps increasing in size until all the elements are contained within it. Note that you initialize DocExtent with (0,0,1,1) because the UnionRect() function doesn’t work properly with rectangles that have zero height or width.
Storing Print Data The OnPreparePrinting() function in the view class is called by the application framework to enable you to initialize the printing process for your document. The basic initialization that’s required is to provide information about how many pages are in the document for the print dialog that displays. You’ll need to store information about the pages that your document requires, so you can use it later in the other view functions involved in the printing process. You’ll originate this in the OnPreparePrinting() member of the view class, too, store it in an object of your own class that you’ll defi ne for this purpose, and store a pointer to the object in the CPrintInfo object that the framework makes available. This approach is primarily to show you how this mechanism works; in many cases, you’ll fi nd it easier just to store the data in your view object. You’ll need to store the number of pages running the width of the document, m_nWidths, and the number of rows of pages down the length of the document, m_nLengths. You’ll also store the upper-left corner of the rectangle enclosing the document data as a CPoint object, m_DocRefPoint, because you’ll use this when you work out the position of a page to be printed from its page number. You can store the fi le name for the document in a CString object, m_DocTitle, so that you can add it as a title to each page. It will also be useful to record the size of the printable area within the page. The defi nition of the class to accommodate these is: #pragma once class CPrintData { public: UINT printWidth; UINT printLength; UINT m_nWidths; UINT m_nLengths; CPoint m_DocRefPoint; CString m_DocTitle; // Constructor CPrintData(): printWidth(750) , printLength(1000) {}
// // // // // //
Printable page width - units 0.01 inches Printable page length - units 0.01 inches Page count for the width of the document Page count for the length of the document Top-left corner of the document contents The name of the document
// 7.5 inches // 10 inches
};
The constructor sets default values for the printable area that corresponds to an A4 page with halfinch margins all round. You can change this to suit your environment, and, of course, you can change the values programmatically in a CPrintData object.
Implementing Multipage Printing
❘ 1147
You can add a new header fi le with the name PrintData.h to the project by right- clicking the Header Files folder in the Solution Explorer pane and then selecting Add ➪ New Item from the pop up. You can now enter the class defi nition in the new fi le. You don’t need an implementation fi le for this class. Because an object of this class is only going to be used transiently, you don’t need to use CObject as a base or to consider any other complication. The printing process starts with a call to the view class member OnPreparePrinting(), so check out how you should implement that.
Preparing to Print The Application Wizard added versions of OnPreparePrinting(), OnBeginPrinting(), and OnEndPrinting() to CSketcherView at the outset. The base code provided for OnPreparePrinting() calls DoPreparePrinting() in the return statement, as you can see: BOOL CSketcherView::OnPreparePrinting(CPrintInfo* pInfo) { // default preparation return DoPreparePrinting(pInfo); }
The DoPreparePrinting() function displays the Print dialog box using information about the number of pages to be printed that’s defi ned in the CPrintInfo object. Whenever possible, you should calculate the number of pages to be printed and store it in the CPrintInfo object before this call occurs. Of course, in many circumstances, you may need information from the device context for the printer before you can do this — when you’re printing a document where the number of pages is going to be affected by the size of the font to be used, for example — in which case, it won’t be possible to get the page count before you call OnPreparePrinting(). In this case, you can compute the number of pages in the OnBeginPrinting() member, which receives a pointer to the device context as an argument. This function is called by the framework after OnPreparePrinting(), so the information entered in the Print dialog box is available. This means that you can also take account of the paper size selected by the user in the Print dialog box. Assume that the page size is large enough to accommodate a 7.5-inch by 10 -inch area to draw the document data, so you can calculate the number of pages in OnPreparePrinting(). The code for it is: BOOL CSketcherView::OnPreparePrinting(CPrintInfo* pInfo) { CPrintData*p(new CPrintData); // Create a print data object CSketcherDoc* pDoc = GetDocument(); // Get a document pointer CRect docExtent = pDoc->GetDocExtent(); // Get the whole document area // Save the reference point for the whole document p->m_DocRefPoint = CPoint(docExtent.left, docExtent.top); // Get the name of the document file and save it p->m_DocTitle = pDoc->GetTitle(); // Calculate how many printed page widths are required
1148
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
// to accommodate the width of the document p->m_nWidths = static_cast(ceil( static_cast<double>(docExtent.Width())/p->printWidth)); // Calculate how many printed page lengths are required // to accommodate the document length p->m_nLengths = static_cast( ceil(static_cast<double>(docExtent.Height())/p->printLength)); // Set the first page number as 1 and // set the last page number as the total number of pages pInfo->SetMinPage(1); pInfo->SetMaxPage(p->m_nWidths*p->m_nLengths); pInfo->m_lpUserData = p; // Store address of PrintData object return DoPreparePrinting(pInfo); }
You fi rst create a CPrintData object on the heap and store its address locally in the pointer. After getting a pointer to the document, you get the rectangle enclosing all of the elements in the document by calling the function GetDocExtent() that you added to the document class earlier in this chapter. You then store the corner of this rectangle in the m_DocRefPoint member of the CPrintData object, and put the name of the fi le that contains the document in m_DocTitle. The next two lines of code calculate the number of pages across the width of the document, and the number of pages required to cover the length. The number of pages to cover the width is computed by dividing the width of the document by the width of the print area of a page and rounding up to the next highest integer using the ceil() library function that is defi ned in the cmath header. For example, ceil(2.1) returns 3.0, ceil(2.9) also returns 3.0, and ceil(-2.1) returns -2.0. A similar calculation to that for the number of pages across the width of a document produces the number to cover the length. The product of these two values is the total number of pages to be printed, and this is the value that you’ll supply for the maximum page number. The last step is to store the address of the CPrintData object in the m_lpUserData member of the pInfo object. Don’t forget to add a #include directive for PrintData.h to the SketcherView.cpp fi le.
Cleaning Up after Printing Because you created the CPrintData object on the heap, you must ensure that it’s deleted when you’re done with it. You do this by adding code to the OnEndPrinting() function: void CSketcherView::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* pInfo) { // Delete our print data object delete static_cast(pInfo->m_lpUserData); }
That’s all that’s necessary for this function in the Sketcher program, but in some cases, you’ll need to do more. Your one-time fi nal cleanup should be done here. Make sure that you remove
Implementing Multipage Printing
❘ 1149
the comment delimiters (/* */) from the second parameter name; otherwise, your function won’t compile. The default implementation comments out the parameter names because you may not need to refer to them in your code. Because you use the pInfo parameter, you must uncomment it; otherwise, the compiler reports it as undefi ned. You don’t need to add anything to the OnBeginPrinting() function in the Sketcher program, but you’d need to add code to allocate any GDI resources, such as pens, if they were required throughout the printing process. You would then delete these as part of the clean-up process in OnEndPrinting().
Preparing the Device Context At the moment, the Sketcher program calls OnPrepareDC(), which sets up the mapping mode as MM_ANISOTROPIC to take account of the scaling factor. You must make some additional changes so that the device context is properly prepared in the case of printing: void CSketcherView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo) { int scale = m_Scale; // Store the scale locally if(pDC->IsPrinting()) scale = 1; // If we are printing, set scale to 1 CScrollView::OnPrepareDC(pDC, pInfo); CSketcherDoc* pDoc = GetDocument(); pDC->SetMapMode(MM_ANISOTROPIC); CSize DocSize = pDoc->GetDocSize(); pDC->SetWindowExt(DocSize);
// Set the map mode // Get the document size // Now set the window extent
// Get the number of pixels per inch in x and y int xLogPixels = pDC->GetDeviceCaps(LOGPIXELSX); int yLogPixels = pDC->GetDeviceCaps(LOGPIXELSY); // Calculate the viewport extent in x and y int xExtent = (DocSize.cx*scale*xLogPixels)/100; int yExtent = (DocSize.cy*scale*yLogPixels)/100; pDC->SetViewportExt(xExtent, yExtent);
// Set viewport extent
}
This function is called by the framework for output to the printer as well as to the screen. You should make sure that a scale of 1 is used to set the mapping from logical coordinates to device coordinates when you’re printing. If you left everything as it was, the output would be at the current view scale, but you’d need to take account of the scale when calculating how many pages you needed, and how you set the origin for each page. You determine whether or not you have a printer device context by calling the IsPrinting() member of the current CDC object, which returns TRUE if you are printing. All you need to do when you have a printer device context is set the scale to 1. Of course, you must change the statements lower down that use the scale value so that they use the local variable scale rather than the m_Scale member of the view. The values returned by the calls to GetDeviceCaps() with the arguments LOGPIXELSX and LOGPIXELSY return the number of logical points per inch in the x and
1150
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
y directions for your printer when you’re printing, and the equivalent values for your display when you’re drawing to the screen, so this automatically adapts the viewport extent to suit the device to which you’re sending the output.
Printing the Document You can write the data to the printer device context in the OnPrint() function. This is called once for each page to be printed. You need to add an override for this function to CSketcherView, using the Properties window for the class. Select OnPrint from the list of overrides and then click OnPrint in the right column. You can obtain the page number of the current page from the m_nCurPage member of the CPrintInfo object and use this value to work out the coordinates of the position in the document that corresponds to the upper-left corner of the current page. The way to do this is best understood using an example, so imagine that you are printing page seven of an eight-page document, as illustrated in Figure 19-7.
DocRefPoint m_nWidths: Number of widths 4
printWidth 750 printLength 1000
Page 1
Page 2
Page 3
Page 4
Page 7
Page 8
printLength*((m_nCurpage ⴚ 1)/m_nWidths) 1000*((7–1)/4) 1000*(6/4) 1000*1 1000
xOrg, yOrg
Page 6
1000 units
Page 5
xOrg ⴝ DocRefPoint.x ⴙ 1500 yOrg ⴝ DocRefPoint.y ⴙ 1000
Number of lengths 2
m_nCurPage 7
750 units printWidth*((m_nCurpage ⴚ 1)%m_nWidths) 750*(6%4) 750*2 1500 FIGURE 19 -7
You can get an index to the horizontal position of the page by decrementing the page number by 1 and taking the remainder after dividing by the number of page widths required for the width of the printed area of the document. Multiplying the result by printWidth produces the x coordinate of the upper-left corner of the page, relative to the upper-left corner of the rectangle enclosing the elements in the document. Similarly, you can determine the index to the vertical position of the document by dividing the current page number reduced by 1 by the number of page widths required for the horizontal width of the document. By multiplying the remainder by printLength, you get the relative y coordinate of the upper-left corner of the page. You can express this in the following statements:
Implementing Multipage Printing
❘ 1151
CPrintData* p(static_cast(pInfo->m_lpUserData)); int xOrg = p->m_DocRefPoint.x + p->printWidth* ((pInfo->m_nCurPage - 1)%(p->m_nWidths)); int yOrg = p->m_DocRefPoint.y + p->printLength* ((pInfo->m_nCurPage - 1)/(p->m_nWidths));
It would be nice to print the fi le name of the document at the top of each page and, perhaps, a page number at the bottom, but you want to be sure you don’t print the document data over the fi le name and page number. You also want to center the printed area on the page. You can do this by moving the origin of the coordinate system in the printer device context after you have printed the fi le name. This is illustrated in Figure 19-8.
m_rectDraw.right
Page Origin
This distance is yOffset given by - (m_rectDraw.bottom-printLength)/2 printLength
m_rectDraw.bottom
File Name
printWidth
This distance is xOffset given by - (m_rectDraw.right-printWidth)/2
The Printed Page The page origin in the document maps to here
Document origin DocRefPoint xOrg, yOrg
The Document
FIGURE 19 -8
1152
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
Figure 19-8 illustrates the correspondence between the printed page area in the device context and the page to be printed in the reference frame of the document data. Remember that these are in logical coordinates — the equivalent of MM_LOENGLISH in Sketcher — so y is increasingly negative from top to bottom. The page shows the expressions for the offsets from the page origin for the printWidth by printLength area where you are going to print the page. You want to print the information from the document in the dashed area shown on the page, so you need to map the xOrg, yOrg point in the document to the position shown in the printed page, which is displaced from the page origin by the offset values xOffset and yOffset. By default, the origin in the coordinate system that you use to defi ne elements in the document is mapped to the origin of the device context, but you can change this. The CDC object provides a SetWindowOrg() function for this purpose. This enables you to defi ne a point in the document’s logical coordinate system that you want to correspond to the origin in the device context. It’s important to save the old origin that’s returned from the SetWindowOrg() function as a CPoint object. You must restore the old origin when you’ve fi nished drawing the current page; otherwise, the m_rectDraw member of the CPrintInfo object is not set up correctly when you come to print the next page. The point in the document that you want to map to the origin of the page has the coordinates xOrg-xOffset,yOrg-yOffset. This may not be easy to visualize, but remember that by setting the
window origin, you’re defi ning the point that maps to the viewport origin. If you think about it, you should see that the xOrg, yOrg point in the document is where you want it on the page. The complete code for printing a page of the document is: // Print a page of the document void CSketcherView::OnPrint(CDC* pDC, CPrintInfo* pInfo) { CPrintData* p(static_cast(pInfo->m_lpUserData)); // Output the document file name pDC->SetTextAlign(TA_CENTER); // Center the following text pDC->TextOut(pInfo->m_rectDraw.right/2, 20, p->m_DocTitle); CString str; str.Format(_T(“Page %u”) , pInfo->m_nCurPage); pDC->TextOut(pInfo->m_rectDraw.right/2, pInfo->m_rectDraw.bottom-20, str); pDC->SetTextAlign(TA_LEFT); // Left justify text // Calculate the origin point for the current page int xOrg = p->m_DocRefPoint.x + p->printWidth*((pInfo->m_nCurPage - 1)%(p->m_nWidths)); int yOrg = p->m_DocRefPoint.y + p->printLength*((pInfo->m_nCurPage - 1)/(p->m_nWidths)); // Calculate offsets to center drawing area on page as positive values int xOffset = (pInfo->m_rectDraw.right - p->printWidth)/2; int yOffset = (pInfo->m_rectDraw.bottom - p->printLength)/2; // Change window origin to correspond to current page & save old origin CPoint OldOrg = pDC->SetWindowOrg(xOrg-xOffset, yOrg-yOffset); // Define a clip rectangle the size of the printed area
Implementing Multipage Printing
❘ 1153
pDC->IntersectClipRect(xOrg, yOrg, xOrg+p->printWidth, yOrg+p->printLength); OnDraw(pDC); pDC->SelectClipRgn(nullptr); pDC->SetWindowOrg(OldOrg);
// Draw the whole document // Remove the clip rectangle // Restore old window origin
}
The fi rst step is to initialize the local pointer, p, with the address of the CPrintData object that is stored in the m_lpUserData member of the object to which pInfo points. You then output the fi le name that you squirreled away in the CPrintData object. The SetTextAlign() function member of the CDC object allows you to defi ne the alignment of subsequent text output in relation to the reference point you supply for the text string in the TextOut() function. The alignment is determined by the constant passed as an argument to the function. You have three possibilities for specifying the alignment of the text, as shown in the following table.
CONSTANT
ALIGNMENT
TA_LEFT
The point is at the left of the bounding rectangle for the text, so the text is to the right of the point specified. This is default alignment.
TA_RIGHT
The point is at the right of the bounding rectangle for the text, so the text is to the left of the point specified.
TA_CENTER
The point is at the center of the bounding rectangle for the text.
You defi ne the x coordinate of the fi le name on the page as half the page width, and the y coordinate as 20 units, which is 0.2 inches, from the top of the page. After outputting the name of the document fi le as centered text, you output the page number centered at the bottom of the page. You use the Format() member of the CString class to format the page number stored in the m_nCurPage member of the CPrintInfo object. This is positioned 20 units up from the bottom of the page. You then reset the text alignment to the default setting, TA_LEFT. The SetTextAlign() function also allows you to change the position of the text vertically by OR-ing a second flag with the justification flag. The second flag can be any of those shown in the following table. CONSTANT
ALIGNMENT
TA_TOP
Aligns the top of the rectangle bounding the text with the point defining the position of the text. This is the default.
TA_BOTTOM
Aligns the bottom of the rectangle bounding the text with the point defining the position of the text.
TA_BASELINE
Aligns the baseline of the font used for the text with the point defining the position of the text.
1154
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
The next action in OnPrint() uses the method that I discussed for mapping an area of the document to the current page. You get the document drawn on the page by calling the OnDraw() function that is used to display the document in the view. This potentially draws the entire document, but you can restrict what appears on the page by defi ning a clip rectangle. A clip rectangle encloses a rectangular area in the device context within which output appears. Output is suppressed outside of the clip rectangle. It’s also possible to defi ne irregularly shaped areas for clipping, called regions. The initial default clipping area defi ned in the print device context is the page boundary. You define a clip rectangle that corresponds to the printWidth by printLength area centered in the page. This ensures that you draw only in this area and the file name and page number will not be overwritten. After the current page has been drawn by the OnDraw() function call, you call SelectClipRgn() with a NULL argument to remove the clip rectangle. If you don’t do this, output of the document title is suppressed on all pages after the fi rst, because it lies outside the clip rectangle that would otherwise remain in effect in the print process until the next time IntersectClipRect() gets called. Your fi nal action is to call SetWindowOrg() again to restore the window origin to its original location, as discussed earlier in this chapter.
Getting a Printout of the Document To get your fi rst printed Sketcher document, you just need to build the project and execute the program (once you’ve fi xed any typos). If you try File ➪ Print Preview, you should get something similar to the window shown in Figure 19-9.
FIGURE 19 - 9
Serialization and Printing in CLR Sketcher
❘ 1155
You get print preview functionality completely for free. The framework uses the code that you’ve supplied for the normal multipage printing operation to produce page images in the Print Preview window. What you see in the Print Preview window should be exactly the same as appears on the printed page.
SERIALIZATION AND PRINTING IN CLR SKETCHER As you know, serialization is the process of writing objects to a stream, and deserialization is the reverse: reconstructing objects from a stream. The .NET Framework offers several different ways to serialize and deserialize your C++/CLI class objects. XML serialization serializes objects into an XML stream that conforms to a particular XML schema. You can also serialize objects into XML streams that conform to the Simple Object Access Protocol (SOAP) specification, and this is referred to as SOAP serialization. A discussion of XML and SOAP is beyond the scope of this book, not because it’s difficult — it isn’t — but because to cover it adequately requires more pages than I can possibly include in this book. I’ll therefore show you how to use the third and, perhaps, simplest form of serialization provided by the .NET Framework, binary serialization. You’ll also investigate how you can print sketches from CLR Sketcher. With the help given by the Form Designer, this is going to be easy.
Understanding Binary Serialization Before you get into the specifics of serializing a sketch, let’s get an overview of what’s involved in binary serialization of your class objects. For binary serialization of your class objects to be possible, you have to make your classes serializable. You can make a ref class or value class serializable by marking it with the Serializable attribute, like this: [Serializable] public ref class MyClass { // Class definition... };
Of course, in practice, there may be a little more to it. For serialization to work with MyClass, all the class fields must be serializable, too, and often, there are fields that are not serializable by default, or fields that it does not make sense to serialize because the data stored will not be valid when it is deserialized, or fields that you just don’t want serialized for security reasons. In this case, special measures are necessary to take care of the non-serializable class members.
Dealing with Fields That Are Not Serializable Where a class has members that are not serializable, you can mark them with the NonSerialized attribute to prevent them from being serialized. For example: [Serializable] public ref class MyClass {
1156
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
public: [NonSerialized]int value; // Rest of the class definition... };
Here, the serialization process will not attempt to serialize the value member of a MyClass object. For this to compile, you need a using declaration for the System::Runtime::Serialization namespace. Although marking a data member as non-serializable avoids the possibility of the serialization process failing with types you cannot serialize, it does not solve the problem of serializing the objects unless the non-serialized data can be happily omitted when you come to reconstruct the object when it is deserialized. You will typically want to take some special action in relation to the non-serialized fields. You use the OnSerializing attribute to mark a class function that you want to be called when the serialization of an object begins. You can also identify a function that is to be called after an object has been serialized, by marking it with the OnSerialized attribute. This gives you an opportunity to do something about the non-serialized fields. For example: [Serializable] public ref class MyClass { public: [NonSerialized] ProblemType anObject; [OnSerializing] void FixNonSerializedData(StreamingContext context) { // Code to do what is necessary for anObject before serialization... } [OnSerialized] void PostSerialization(StreamingContext context) { // Code to do what is necessary after serialization... } // Rest of the class definition... };
When a MyClass object is serialized, the FixNonSerializedData() function will be called before the object is serialized, and the code in here will deal with the problem of anObject not being serialized, perhaps by allowing some other data to be serialized that will enable anObject to be reconstructed when the MyClass object is deserialized. The PostSerialization() function will be called after a MyClass object has been serialized. The function that you mark with either the OnSerializing or the OnSerialized attribute must have a void return type and a parameter of type StreamingContext. The StreamingContext object that is passed to the function when it is called is a struct containing information about the source and destination, but you won’t need to use this in CLR Sketcher.
Serialization and Printing in CLR Sketcher
❘ 1157
You can also arrange for a member function to be called after an object is deserialized. The OnDeserializing and OnDeserialized attributes identify functions that are to be called before or after deserialization, respectively. For example: [Serializable] public ref class MyClass { public: [NonSerialized] ProblemType anObject; [OnSerializing] void FixNonSerializedData(StreamingContext context) { // Code to do what is necessary for anObject before serialization... } [OnSerialized] void PostSerialization(StreamingContext context) { // Code to do what is necessary after serialization... } [OnDeserializing] void PreSerialization(StreamingContext context) { // Code to do stuff before deserialization... } [OnDeserialized] void ReconstructObject(StreamingContext context) { // Code to reconstruct anObject... } // Rest of the class definition... };
The PreSerialization() function will be called immediately before the deserialization process for a MyClass object starts. The ReconstructObject() function will be executed after the deserialization process for the object is complete, so this function will have the responsibility for setting up anObject appropriately. Functions that you mark with the OnDeserializing or OnDeserialized attributes must also have a void return type and a parameter of type StreamingContext. To summarize, typically, you can prepare a class to allow objects of that class type to be serialized through the following five steps:
1. 2.
Mark the class to be serialized with the Serializable attribute. Identify any data members that cannot or should not be serialized and mark them with NonSerialized attributes.
1158
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
3.
Add a public function with a return type of void and a single parameter of type StreamingContext to deal with the non-serializable fields when an object is serialized, and mark the function with the OnSerializing attribute.
4.
Add a public function with a return type of void and a single parameter of type StreamingContext to deal with the non-serialized fields when an object is deserialized, and mark the function with the OnDeSerialized attribute.
5.
Add a using declaration for the System::Runtime::Serialization namespace to the header fi le containing the class.
Serializing an Object Serializing an object means writing it to a stream, so you fi rst must defi ne the stream that is the destination for the data defi ning the object. A stream is represented by a System::IO::Stream class, which is an abstract ref class type. A stream can be any source or destination for data that is a sequence of bytes; a fi le, a TCP/IP socket, and a pipe that allows data to be passed between two processes are all examples of streams. You will usually want to serialize your objects to a fi le, and the System::IO::File class contains static functions for creating objects that encapsulate fi les. You use the File::Open() function to create a new fi le or open an existing fi le for reading and/or writing. The Open() function returns a reference of type FileStream^ to an object that encapsulates the file. Because FileStream is a type that is derived from Stream, you can store the reference that the Open() function returns in a variable of type Stream^. The Open() function comes in three overloaded versions, and the version you will be using has the following form: FileStream^ Open(String^ path, FileMode mode)
The path parameter is the path to the file that you want to open and can be a full path to the fi le, or just the fi le name. If you just specify an argument that is just a fi le name, the fi le will be assumed to be in the current directory. The mode parameter controls whether the fi le is created if it does not exist, and whether the data can be overwritten if the fi le does exist. The mode argument can be any of the FileMode enumeration values described in the following table. FILEMODE ENUMERATOR
DESCRIPTION
CreateNew
Requests that a new file specified by path be created. If the file already exists, an exception of type System::IO::IOException is thrown. You use this when you are writing a new file.
Truncate
Requests that an existing file specified by path be opened and its contents discarded by truncating the size of the file to zero bytes. You use this when you are writing an existing file.
Create
Specifies that if the file specified by path does not exist, it should be created, and if the file does exist, it should be overwritten. You use this when you are writing a file. continues
Serialization and Printing in CLR Sketcher
❘ 1159
(continued) FILEMODE ENUMERATOR
DESCRIPTION
Open
Specifies that the existing file specified by path should be opened. If the file does not exist, an exception of type System::IO::FileNotFoundException is thrown. You use this when you are reading a file.
OpenOrCreate
Specifies that the file specified by path should be opened if it exists, and created if it doesn’t. You can use this to read or write a file, depending on the access argument.
Append
The file specified by path is opened if it exists and the file position set to the end of the file; if the file does not exist, it will be created. You use this to append data to an existing file or to write a new file.
Thus, you could create a stream encapsulating a fi le in the current directory that you can write with the following statement: Stream^ stream = File::Open(L"sketch.dat", FileMode::Create);
The fi le sketch.dat will be created in the current directory if it does not exist; if it exists, the contents will be overwritten. The static OpenWrite() function in the File class will open the existing fi le that you specify by the string argument with write access, and return a FileStream^ reference to the stream you use to write the fi le. To serialize an object to a fi le encapsulated by a FileStream object that you have created, use an object of type System::Runtime::Serialization::Formatters::Binary::BinaryFormatter, which you can create as follows: BinaryFormatter^ formatter = gcnew BinaryFormatter();
You need a using declaration for System::Runtime::Serialization::Formatters::Binary if this statement is to compile. The formatter object has a Serialize() function member that you use to serialize an object to a stream. The fi rst argument to the function is a reference to the stream that is the destination for the data, and the second argument is a reference to the object to be serialized to the stream. Thus, you can write a sketch object to stream with the following statement: formatter->Serialize(stream, sketch);
You read an object from a stream using the Deserialize() function for a BinaryFormatter object: Sketch^ sketch = safe_cast<Sketch^>(formatter->Deserialize(stream));
The argument to the Deserialize() function is a reference to the stream that is to be read. The function returns the object read from the stream as type Object^, so you must cast it to the appropriate type.
1160
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
Serializing a Sketch You have to do two things to allow sketches to be serialized in the CLR Sketcher application: make the Sketch class serializable and add code to enable the File menu items and toolbar buttons to support saving and retrieving sketches.
Making the Sketch Class Serializable Add a using directive for System::Runtime::Serialization to the Sketch.h header fi le. While you are about it, you can copy the directive to the Elements.h header because you will need it there, too. Add the Serializable attribute immediately before the Sketch class defi nition to specify that the class is serializable: [Serializable] public ref class Sketch { // Class definition as before... };
Although this indicates that the class is serializable, in fact, it is not. Serialization will fail because the STL/CLR container classes are not serializable by default. You must specify that the elements class member is not serializable, like this: [Serializable] public ref class Sketch { private: [NonSerialized] list<Element^>^ elements; // Rest of the class definition as before... };
The class really is serializable now, but not in a useful way, because none of the elements in the sketch will get written to the fi le. You must provide an alternative repository for the elements in the list<Element^> container that is serializable to get the sketch elements written to the fi le. Fortunately, a regular C++/CLI array is serializable, and even more fortunately, the list container has a to_array() function that returns the entire contents of the container as an array. You can therefore add a public function to the class with the OnSerializing attribute that will copy the contents of the elements container to an array, and that you can arrange to be called before serialization begins. You can also add a public function with the OnDeserialized attribute that will recreate the elements container when the array containing the elements is deserialized. Here are the changes to the Sketch class that will accommodate that: [Serializable] public ref class Sketch { private: [NonSerialized] list<Element^>^ elements;
Serialization and Printing in CLR Sketcher
❘ 1161
array<Element^>^ elementArray; public: Sketch() : elementArray(nullptr) { elements = gcnew list<Element^>(); } [OnSerializing] void ListToArray(StreamingContext context) { elementArray = elements->to_array(); } [OnDeserialized] void ArrayToList(StreamingContext context) { elements = gcnew list<Element^>(elementArray); elementArray = nullptr; } // Rest of the class definition as before... };
You have a new private data member, elementArray, that holds all the elements in the sketch when it is serialized to a file. You initialize this to nullptr in the constructor. When a Sketch object is serialized, the ListToArray() function will be called fi rst, so this function transfers the contents of the list container to the elementArray array before serialization of the object takes place. The Sketch object containing the elementArray object is then written to the fi le. When a Sketch object is deserialized, the object is recreated containing the elementArray member, after which the ArrayToList() function is called to restore the contents of the elements container from the array. The array is no longer required, so you set it to nullptr in the function. So far, so good, but you are not quite there yet. For a sketch to be serializable, all the elements in the sketch must be serializable, too, and there’s the small problem of the Curve class that has an STL/CLR container as a member. Pen and Brush objects are not serializable, so something must be done about those before a sketch can be serialized. First, though, you can add the Serializable attribute to the Element class and all its subclasses. You can also mark the pen member of the Element class with the NonSerialized attribute.
Dealing with Pen and Brush Objects You can’t serialize/deserialize a Pen or Brush object, but if you serialize the attributes of the pen or brush, you will be able to reconstruct it. You can add some members to the Element base class to store the attributes you need: [Serializable] public ref class Element abstract { protected: Point position;
1162
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
Color color; System::Drawing::Rectangle boundRect; Color highlightColor; float penWidth; DashStyle dashStyle; [NonSerialized] Pen^ pen; // Rest of the class as before... };
I have shown all the additions for serialization as bold. There are two extra protected members of the class that store the pen width and the DashStyle property value for the element. Pen objects are not used by the TextElement class, so you should only reconstruct the pen member of the Element class in the classes that defi ne geometric shapes. You can add the following public functions to the Line, Rectangle, and Circle classes: [OnSerializing] void SavePenAttributes(StreamingContext context) { penWidth = pen->Width; dashStyle = pen->DashStyle; } [OnDeserialized] void CreatePen(StreamingContext context) { pen = gcnew Pen(color, penWidth); pen->DashStyle = dashStyle; }
The SavePenAttributes() function will be called immediately before an object is serialized to fi ll out the penWidth and dashStyle members ready for serialization. The CreatePen() function will be called after an object has been deserialized, and it will re- create the pen object from the attributes saved by the serialization process. In the TextElement class, you should mark the brush member with the NonSerialized attribute. You then must provide for reconstructing this object after deserializing a TextElement object. The changes to the class defi nition for this are as follows: [Serializable] public ref class TextElement : Element { protected: String^ text; Font^ font; [NonSerialized] SolidBrush^ brush; public: [OnDeserialized]
Serialization and Printing in CLR Sketcher
❘ 1163
void CreateBrush(StreamingContext context) { brush = gcnew SolidBrush(color); } // Rest of the class as before... };
The CreateBrush() function is called to reconstruct the brush member after an object is deserialized. No additional information is required to reconstruct the SolidBrush object, because it only needs to have the color member available, and that is serializable, anyway.
Making the Curve Class Serializable The Curve class needs to deal with the inherited pen member, and it must also take care of the vector container that isn’t serializable. You can pull the same trick with the container in the Curve class as you did with the Sketch class container, so amend the class defi nition to the following: [Serializable] public ref class Curve : Element { private: [NonSerialized] vector^ points; array^ pointsArray; public: Curve(Color color, Point p1, Point p2, float penWidth) : pointsArray(nullptr) { // Code for the constructor as before... } [OnSerializing] void SavePenAndVectorData(StreamingContext context) { penWidth = pen->Width; dashStyle = pen->DashStyle; pointsArray = points->to_array(); } [OnDeserialized] void RestorePenAndVectorData(StreamingContext context) { pen = gcnew Pen(color, penWidth); pen->DashStyle = dashStyle; points = gcnew vector(pointsArray); pointsArray = nullptr; } // Rest of the class definition as before... };
It should be easy to see how this works. The points member is now non-serialized, but you have a new member of the class, pointsArray, that will store the points in a serializable form.
1164
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
The SavePenAndVectorData() function that is called immediately prior to serialization of an object sets up the penWidth, dashStyle, and pointsArray members ready for serialization. The RestorePenAndVectorData() function restores the pen and the vector container after a Curve object has been deserialized. Don’t forget to add the using declaration for the System::Runtime::Serialization namespace to Sketch.h and Elements.h.
Implementing File Operations for a Sketch The menu items and toolbar buttons for fi le operations are already in place in CLR Sketcher. If you double- click the Click event property in the Properties window for the File ➪ Save, File ➪ Save As..., and File ➪ Open menu items, you’ll put the event handlers in place for all of them. Then just select the appropriate event handler from the drop-down list of values for the Click event for each of the toolbar buttons. All you have to do now is supply the code to make them do what you want.
Creating Dialogs for File Operations The Toolbox has standard dialogs for opening and saving fi les and you can use both of these. Drag an OpenFileDialog and a SaveFileDialog from the Toolbox to the Design window for Form1. You can change the (name) property values to saveFileDialog and openFileDialog. Change the values for the Title properties for both dialogs to whatever you want displayed in the title bar. You can also change the FileName property in the saveFileDialog to sketch; this is the default fi le name that will be displayed when the dialog is fi rst used. It’s a good idea to defi ne a folder that will hold your sketches, so create one now; you could use something like C:\CLR Sketches. You can specify the default directory as the value for the InitialDirectory property for both dialogs. Both dialogs have a Filter property that specifies the fi lters for the list of fi les that are displayed by the dialogs. The DefaultExt property value specifies a default extension for fi les, so set this to ske. You can set the value for the Filter property for both dialogs to “CLR Sketches|*.ske| All files|*.*“.
The default value of 1 for the FilterIndex property determines that the first file filter applies by default. If you want the second file filter to apply, set the value for FilterIndex to 2. Verify that the values for the ValidateNames and OverwritePrompt properties for the saveFileDialog have default values of true; this results in the user being prompted when an existing sketch is about to be overwritten.
Recording the Saved State of a Sketch Before you get into implementing the event handler for the File ➪ Save menu item, let’s consider what the logic is going to be. When you click the File ➪ Save menu item, what happens depends on whether the current sketch has been saved before. If the sketch has never been saved, you want the fi le save dialog to be displayed; if the sketch has been saved previously, you just want to write the sketch to the fi le without displaying the dialog. To allow this to work, you need a way to record whether or not the sketch has been saved. One way to do this is to add a public member of type bool with the name saved to the Sketch class. You can initialize it to true in the Sketch class constructor because you don’t want to save an empty sketch. Any change to a sketch should result in saved being set to false, and it should be set to true whenever the sketch is saved. The Form1_ MouseUp() handler adds new elements to the sketch, so this needs to be updated: private: System::Void Form1_MouseUp(System::Object^ System::Windows::Forms::MouseEventArgs^ e)
sender,
Serialization and Printing in CLR Sketcher
❘ 1165
{ if(!drawing) { mode = Mode::Normal; return; } if(tempElement) { sketch += tempElement; sketch->saved = false; tempElement = nullptr; Invalidate(); } drawing = false;
// Sketch has changed so mark it not saved
}
The Form1_MouseDown() handler adds the TextElement object to the sketch, so saved should be set to false there: private: System::Void Form1_MouseDown(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) { if(e->Button == System::Windows::Forms::MouseButtons::Left) { firstPoint = e->Location; if(mode == Mode::Normal) { if(elementType == ElementType::TEXT) { textDialog->TextString = L""; // Reset the text box string textDialog->TextFont = textFont; // Set the font for the edit box if(textDialog->ShowDialog() == System::Windows::Forms::DialogResult::OK) { tempElement = gcnew TextElement(color, firstPoint, textDialog->TextString, textFont); sketch += tempElement; sketch->saved = false; // Sketch has changed so mark it not saved Invalidate(tempElement->bound); // The text element region tempElement = nullptr; Update(); } drawing = false; } else { drawing = true; } } } }
1166
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
The Form1_MouseMove() function changes the sketch, so you must set saved to false in here, too: private: System::Void Form1_MouseMove(System::Object^ sender, System::Windows::Forms::MouseEventArgs^ e) { if(drawing) { // Code as before... } else if(mode == Mode::Normal) { // Code as before... } else if(mode == Mode::Move && e->Button == System::Windows::Forms::MouseButtons::Left) { // Move the highlighted element if(highlightedElement) { sketch->saved = false; // Mark sketch as not saved Invalidate(highlightedElement->bound); // Region before move highlightedElement->Move(e->X - firstPoint.X, e->Y - firstPoint.Y); firstPoint = e->Location; Invalidate(highlightedElement->bound); // Region after move Update(); } } }
The handler for the Delete context menu item changes the sketch, so that has to be updated: private: System::Void deleteContextMenuItem_Click(System::Object^ sender System::EventArgs^ e) { if(highlightedElement) { sketch->saved = false; // Mark sketch as not saved sketch -= highlightedElement; // Delete the highlighted element Invalidate(highlightedElement->bound); highlightedElement = nullptr; Update(); } }
Finally, the sendToBack() handler needs to be changed: private: System::Void sendToBackContextMenuItem_Click(System::Object^ sender, System::EventArgs^ e) { if(highlightedElement) { sketch->saved = false; // Mark sketch as not saved // Rest of the code as before... } }
❘ 1167
Serialization and Printing in CLR Sketcher
That’s covered the saved state of the sketch. Now, you can implement the process of saving it.
Saving a Sketch You need a BinaryFormatter object to save a sketch, and a good place to keep it is in the Form1 class. Add a using declaration for the System::Runtime::Serialization::Formatters::Binary namespace to Form1.h, and add a new private member, formatter, of type BinaryFormatter^, to the Form1 class. You can initialize it to gcnew BinaryFormatter() in the initialization list for the Form1 class constructor. Before you implement the Click event handler for the File ➪ Save menu item, add a using declaration for the System::IO namespace to the Form1.h header fi le. Add a private String^ member, sketchFilePath, to the Form1 class to store the file path for a sketch, and initialize this to nullptr in the Form1 constructor. You can add the following code to implement the Click event handler for save operations: private: System::Void saveToolStripMenuItem_Click( System::Object^ { SaveSketch(); }
sender, System::EventArgs^
e)
This just calls a helper function that you will add shortly. Here’s how you can implement the handler for the Save As... menu item using another helper function: private: System::Void saveAsToolStripMenuItem_Click(System::Object^ System::EventArgs^ e) { SaveSketchAs(); }
sender,
The reason for using helper functions in this way is because the process for dealing with a Save operation can use the functionality that is required for a Save As... operation. Add the SaveSketch() function as a private member of the Form1 class and implement it like this: void SaveSketch(void) { if(sketch->saved) return;
// Nothing to do because the sketch was not modified
if(sketchFilePath == nullptr) { // File has not yet been saved before, so show the save dialog SaveSketchAs(); } else { // File has been saved before, so just save it using the same name Stream^ stream = File::OpenWrite(sketchFilePath);
1168
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
formatter->Serialize(stream, sketch); stream->Close(); } }
There are two courses of action, depending on whether the sketch has been saved previously. If the sketch is unchanged, there’s nothing to do, so the function returns. If saved indicates that the sketch needs to be saved, there are two further possibilities:
1.
The sketch has never been saved previously, which is indicated by sketchFilePath having the value nullptr. In this case, you call SaveSketchAs() to use the save dialog to save the sketch.
2.
The sketch has been saved before, in which case, sketchFilePath will refer to a valid path. In this case, you can save it using the existing fi le path.
To save the sketch when it has been saved previously, you obtain a reference to a FileStream object that you can use to serialize the sketch by calling the static OpenWrite() function that is defi ned in the File class. The argument to OpenWrite() is the existing fi le path for the sketch. You then serialize the sketch by calling the Serialize() function for the BinaryFormatter object. Finally, you call Close() for the stream to close the stream and release the resources. You can add the SaveSketchAs() function to the Form1 class as a private function implemented like this: // Saves the current sketch void SaveSketchAs(void) { if(saveFileDialog->ShowDialog() == System::Windows::Forms::DialogResult::OK) { Stream^ stream = File::Open(saveFileDialog->FileName, FileMode::Create); if(stream != nullptr) { formatter->Serialize(stream, sketch); stream->Close(); sketchFilePath = saveFileDialog->FileName; sketch->saved = true; } else { MessageBox::Show(L”Failed to create sketch file stream!”); } } }
You display the fi le save dialog by calling its ShowDialog() function in the if condition. If the OK button closes the dialog, the user wants to complete the save operation, so you call the static Open() function to create a FileStream object for the fi le using the fi le name provided by the FileName property for the dialog object. You serialize the sketch to the fi le by calling the Serialize() function for the BinaryFormatter object, and close the stream. You save the fi le path from the dialog for use next time around, and set the saved member of the Sketch object to true.
Serialization and Printing in CLR Sketcher
❘ 1169
If calling the Open() function in the File class fails, nullptr is stored in stream. In this case, you use the System::Windows::Forms::MessageBox class to display a message. The MessageBox class Show() function provides you with the opportunity to display a wide range of message boxes. You will see more of the possibilities later in this chapter.
Retrieving a Sketch from a File Retrieving a sketch from a fi le is a little more complicated, because you have to consider the possibility for the current sketch to be overwritten. If there is a current sketch that has not been saved, you should give the user an opportunity to save it before reading the new sketch from a file. The Click handler for the File ➪ Open menu item deals with reading a sketch from a fi le. You can implement it like this: private: System::Void openToolStripMenuItem_Click( System::Object^ sender, System::EventArgs^ e) { Stream^ stream; if(!sketch->saved) { String^ message = L”The current sketch is not saved.\nSave the current sketch?”; String^ caption = L”Sketch Not Saved”; MessageBoxButtons buttons = MessageBoxButtons::YesNo; // Displays the MessageBox to warn about unsaved sketch if (MessageBox::Show(this, message, caption, buttons) == System::Windows::Forms::DialogResult::Yes) { SaveSketch(); } } // Now open a new sketch if(openFileDialog->ShowDialog() == System::Windows::Forms::DialogResult::OK) { stream = openFileDialog->OpenFile(); if(stream != nullptr) { sketch = safe_cast<Sketch^>(formatter->Deserialize(stream)); stream->Close(); sketch->saved = true; sketchFilePath = openFileDialog->FileName; Invalidate(); } } }
If the saved member of the current Sketch object is false, the current sketch has not been saved. Therefore, you display a message box offering the user the opportunity to save the current sketch. This version of the Show() function in the MessageBox class accepts four arguments: a reference to the current window, the message to be displayed, the caption for the
1170
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
message box, and a MessageBoxButtons value that determines the buttons to be included in the message box. MessageBoxButtons is an enumeration that includes the members OK, OKCancel, AbortRetryIgnore, YesNoCancel, YesNo, and RetryCancel. If the value returned from the Show() function in the MessageBox class corresponds to the Yes button being clicked, you call the SaveSketch() function to allow it to save the sketch. The OpenFileDialog class provides an OpenFile() function that returns a reference to the stream encapsulating the fi le selected in the dialog. You deserialize the sketch from the fi le by calling the Deserialize() function for the formatter object, and close the stream. The sketch is obviously in a file, so you set the saved member of the sketch to true. You store the file name in sketchFilePath for use by subsequent save operations, and call Invalidate() to get the form repainted to display the sketch you have just loaded.
Implementing the File New Operation The File ➪ New menu item should create a new empty sketch, but only after checking that the current sketch has been saved. Add a Click event handler for the menu item and implement it like this: private: System::Void newToolStripMenuItem_Click( System::Object^ { SaveSketchCheck(); sketch = gcnew Sketch(); sketchFilePath = nullptr; Invalidate(); Update(); }
sender, System::EventArgs^
e)
The implementation is very simple. You call SaveSketchCheck() to check whether the current sketch needs to be saved and provide the opportunity for the user to do so. You then create a new Sketch object, rest the sketchFilePath member, and redraw the client area by calling Invalidate(), then Update(). Add SaveSketchCheck() as a private member of the Form1 class and implement it like this: void SaveSketchCheck() { if(!sketch->saved) { String^ message = L”The current sketch is not saved.\nSave the current sketch?”; String^ caption = L”Sketch Not Saved”; MessageBoxButtons buttons = MessageBoxButtons::YesNo; // Displays the MessageBox to warn about unsaved sketch if ( MessageBox::Show(this, message, caption, buttons) == System::Windows::Forms::DialogResult::Yes) { SaveSketch(); } } }
❘ 1171
Serialization and Printing in CLR Sketcher
Dealing with Closing the Form At the moment, you can close the application by clicking on the close icon in the top-right corner of the form. This will close the application and lose the current sketch, if there is one. You really want to provide the possibility of saving the sketch before the application shuts down. Add a FormClosing event handler for the form by displaying the Property window for the form and double- clicking the FormClosing event. You can implement the event handler like this: private: System::Void Form1_FormClosing( System::Object^ sender, System::Windows::Forms::FormClosingEventArgs^ { SaveSketchCheck(); }
e)
You just call SaveSketchCheck() to provide for the sketch being saved when necessary. That’s all there is to it.
Supporting the Toolbar Buttons The toolbar buttons for saving and opening fi les and creating a new sketch do nothing at the moment. However, to make them work is trivial. All you have to do is to select the Click event handler for the File menu item corresponding to a button as the event handler for the button. You now have a version of CLR Sketcher with saving and retrieving sketches fully operational.
Printing a Sketch You have a head start on printing a sketch because the Toolbox provides five components that support printing operations, including a page setup dialog and print and print preview dialogs. To print a sketch, you create an instance of the PrintDocument component, implement a PrintPage event handler for the PrintDocument, and call the Print function for the PrintDocument object to actually print the sketch. Of course, you also need to create Click event handlers for the menu items that are involved and display a few dialogs along the way, but let’s start with the PrintDocument component.
Using the PrintDocument Component Drag a PrintDocument component from the Toolbox window to the form in the Design window. This adds a PrintDocument member to the Form1 class. If you display the Properties window for the PrintDocument object, you can change the value of its (name) property to printDocument. Click the Events button and double- click the PrintPage event to create a handler for it. The PrintPageEventArgs^ parameter has a Graphics property that supplies a Graphics object that you can use to draw the sketch ready for printing, like this: private: System::Void printDocument_PrintPage( System::Object^ sender, System::Drawing::Printing::PrintPageEventArgs^ { sketch->Draw(e->Graphics); }
e)
1172
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
It couldn’t be much easier, really, could it?
Implementing the Print Operation You need a print dialog to allow the user to select the printer and initiate printing, so drag a PrintDialog component from the Toolbox window to the form and change the (name) property value to printDialog. To associate the printDocument object with the dialog, select printDocument as the value of the Document property for the dialog from the drop - down list in the value column. Add a Click event handler for the File ➪ Print menu item, and set this handler as the handler for the toolbar button for printing. All you have to do now is add code to the handler to display the dialog to allow printing: private: System::Void printToolStripMenuItem_Click( System::Object^ sender, System::EventArgs^ { if(printDialog->ShowDialog() == System::Windows::Forms::DialogResult::OK) printDocument->Print(); }
e)
You display the dialog and, if the value returned from ShowDialog() is DialogResult::OK, you call the Print() function for the printDocument object to print the sketch. That’s it! You added two lines of code here plus one line of code in the PrintPage event handler, and a basic printing capability for sketches is working. Displaying the print dialog allows the user to choose the printer and change preferences for the print job before printing starts. Of course, you don’t have to display the dialog to print the sketch. If you wanted to print the sketch immediately without displaying the dialog, you could just call the Print() function for the PrintDocument object.
SUMMARY In this chapter, you have implemented the capability to store a document on disk in a form that allows you to read it back and reconstruct its constituent objects using the serialization processes supported by MFC and the CLR. You have also implemented the capability to print sketches in the native C++ and C++/CLI versions of the Sketcher application. If you are comfortable with how serialization and printing work in the two versions of Sketcher, you should have little difficulty with implementing serialization and printing in any MFC or Windows Forms application.
Summary
❘ 1173
EXERCISES You can download the source code for the examples in the book and the solutions to the following exercises from www.wrox.com.
1.
Update the code in the OnPrint() function in Sketcher so that the page number is printed at the bottom of each page of the document in the form “Page n of m,” where n is the current page number and m is the total number of pages.
2.
As a further enhancement to the CText class in Sketcher, change the implementation so that scaling works properly with text. (Hint : Look up the CreatePointFont() function in the online help.)
3.
Modify CLR Sketcher so that the user can choose a custom color for elements. (Hint : This will involve adding a new menu item and toolbar button. Look in the Toolbox for help with custom color selection.)
4.
Add a line type option to CLR Sketcher for a line style pattern that is two dashes, then two dots, then one dash, then one dot.
1174
❘
CHAPTER 19 STORING AND PRINTING DOCUMENTS
WHAT YOU LEARNED IN THIS CHAPTER TOPIC
CONCEPT
Serialization
Serialization is the process of transferring objects to a file. Deserialization reconstructs object from data in a file.
MFC Serialization
To serialize objects in an MFC application, you must identify the class as serializable. To do this you use the DECLARE_SERIALIZABLE() macro in the class definition and the IMPLEMENT_SERIALIZABLE() macro in the file containing the class implementation.
Serializable classes in an MFC application.
For a class to be serializable in an MFC application, it must have CObject as a base class, it must implement a no-arg constructor and implement the Serialize() function as a class member.
C++/CLI Serialization
To identify a C++/CLI class as serializable you must mark in with the Serialiable attribute.
Serializing C++/CLI classes
For a class to be serializable in a C++/CLI application, you must: ➤
mark any fields that cannot or should not be serialized with the NonSerialized attribute,
➤
add a public member function to deal with non-serializable fields when serializing an object that has a void return type and a single parameter of type StreamingContext and mark it with the OnSerializing attribute,
➤
add a public member function to handle non-serialized fields when deserializing an object that has a void return type and a single argument of type StreamingContext and mark the function with the OnDeserialized attribute,
➤
add a using declaration for the System::Runtime::Serialization namespace to each header file containing serializable classes.
Printing with the MFC
To provide the ability to print a document in an MFC application, you must implement your versions of the OnPreparePrinting(), OnBeginPrinting(), OnPrepareDC(), OnPrint() and OnEndPrinting() functions in the document view class.
The CPrintInfo object
A CPrintInfo object is created by the MFC framework to store information about the printing process. You can store the address of your own class object that contains print information in a pointer in the CPrintInfo object.
Printing in a C++/CLI application
To implement printing in a Windows Forms application, add a PrintDocument component to the form and implement the handler for the PrintPage event to print the form.
Implementing the printing userinterface in a C++/ CLI application
To implement the printing UI in a Windows Forms application, add a PrintDialog component to the form and display it in the Click handler for the menu item/toolbar button that initiates printing. When the print dialog is closed using the OK button, call the Print() function for the PrintDocument object to print the form.
20
Writing Your Own DLLs WHAT YOU WILL LEARN IN THIS CHAPTER:
➤
DLLs and how they work
➤
When you should consider implementing a DLL
➤
What varieties of DLLs are possible and what they are used for
➤
How to extend MFC using a DLL
➤
How to define what is accessible in a DLL
➤
How to access the contents of a DLL in your programs
Chapter 9 discussed how a C++/CLI class library is stored in a .dll fi le. Dynamic link libraries (DLLs) are also used extensively with native C++ applications. A complete discussion of DLLs in native C++ applications is outside the scope of a beginner’s book, but they are important enough to justify including an introductory chapter on them.
UNDERSTANDING DLLS Almost all programming languages support libraries of standard code modules for commonly used elements such as functions. In native C++, you’ve been using lots of functions that are stored in standard libraries, such as the ceil() function that you used in the previous chapter, which is declared in the cmath header. The code for this function is stored in a library fi le with the extension .lib, and when the executable module for the Sketcher program was created, the linker retrieved the code for this standard function from the library fi le and integrated a copy of it into the .exe fi le for the Sketcher program. If you write another program and use the same function, it will also have its own copy of the ceil() function. The ceil() function is statically linked to each application and is an integral part of each executable module, as illustrated in Figure 20 -1.
1176
❘
CHAPTER 20
WRITING YOUR OWN DLLS
Static Library
Copy added to each program during linkedit Library function
ProgramA.exe
ProgramB.exe
ProgramC.exe
Library function
Library function Library function
FIGURE 20 -1
Although this is a very convenient way of using a standard function with minimal effort on your part, it does have its disadvantages as a way for several concurrently executing programs to make use of the same function in the Windows environment. A statically linked standard function being used by more than one program concurrently is duplicated in memory for each program using it. This may not seem to matter much for the ceil() function, but some functions — input and output, for instance — are invariably common to most programs and are likely to occupy sizable chunks of memory. Having these statically linked would be extremely inefficient. Another consideration is that a standard function from a static library may be linked into hundreds of programs in your system, so identical copies of the code for them will be occupying disk space in the .exe fi le for each program. For these reasons, an additional library facility is supported by Windows for standard functions. It’s called a dynamic link library, and it’s usually abbreviated to DLL . This allows one copy of a function to be shared among several concurrently executing programs and avoids the need to incorporate a copy of the code for a library function into the executable module for a program that uses it.
Understanding DLLs
❘ 1177
How DLLs Work A dynamic link library is a fi le containing a collection of modules that can be used by any number of different programs. The fi le for a DLL usually has the extension .dll, but this isn’t obligatory. When naming a DLL, you can assign any extension you like, but this can affect how it’s handled by Windows. Windows automatically loads dynamic link libraries that have the extension .dll. If they have some other extension, you will need to load them explicitly by adding code to do this to your program. Windows itself uses the extension .exe for some of its DLLs. You have likely seen the extensions .vbx (Visual Basic Extension) and .ocx (OLE Custom Extension), which are applied to DLLs containing specific kinds of controls. You might imagine that you have a choice about whether or not you use dynamic link libraries in your program, but you don’t. The Win32 API is used by every Windows program, and the API is implemented in a set of DLLs. DLLs are fundamental to Windows programming. Connecting a function in a DLL to a program is different from the process used with a statically linked library, where the code is incorporated once and for all when the program is linked to generate the executable module. A function in a DLL is connected only to a program that uses it when the application is run, and this is done each time the program is executed, as Figure 20 -2 illustrates.
2. .dll loaded Library.dll
4. ProgramB loaded
ProgramC.exe ProgramB.exe ProgramA.exe
1. ProgramA loaded 6. ProgramC loaded
ProgramA
ProgramB
ProgramC
Library.dll
5. linkage to dll function
Function
3. linkage to dll function 7. linkage to dll function
Computer Memory FIGURE 20 -2
1178
❘
CHAPTER 20
WRITING YOUR OWN DLLS
Figure 20-2 shows the sequence of events when three programs that use a function in a DLL are started successively and then all execute concurrently. No code from the DLL is included in the executable module of any of the programs. When one of the programs is executed, the program is loaded into memory, and if the DLL it uses isn’t already present, it, too, is loaded separately. The appropriate links between the program and the DLL are then established. If, when a program is loaded, the DLL is already there, all that needs to be done is to link the program to the required function in the DLL. Note particularly that when your program calls a function or uses a class that is defined in a DLL, Windows will automatically load the DLL into memory. Any program subsequently loaded into memory that uses the same DLL can use any of the capabilities provided by the same copy of the DLL, because Windows recognizes that the library is already in memory and just establishes the links between it and the program. Windows keeps track of how many programs are using each DLL that is resident in memory, so that the library remains in memory as long as at least one program is still using it. When a DLL is no longer used by any executing program, Windows automatically deletes it from memory. MFC is provided in the form of a number of DLLs that your program can link to dynamically, as well as a library that your program can link to statically. By default, the Application Wizard generates programs that link dynamically to the DLL form of MFC. Having a function stored in a DLL introduces the possibility of changing the function without affecting the programs that use it. As long as the interface to the function in the DLL remains the same, the programs can use a new version of the function quite happily, without the need for recompiling or relinking them. Unfortunately, this also has a downside: it’s easy to end up using the wrong version of a DLL with a program. This can be a particular problem with applications that install DLLs in the Windows System folder. Some commercial applications arbitrarily write the DLLs associated with the program to this folder without regard for the possibility of a DLL with the same name being overwritten. This can interfere with other applications that you have already installed and, in the worst case, can render them inoperable.
NOTE DLLs are not the only means of sharing classes or functions among several applications. COM (Component Object Model) objects provide another, more powerful way for you to create reusable software components. Using COM can be quite complicated, but the ATL (Active Template Library), which is a template-based class library, can make COM programming easier. There is quite a lot to learn if you want to use COM and/or ATL; however, both topics are outside the scope of this book.
Runtime Dynamic Linking The DLL that you’ll create in this chapter is automatically loaded into memory when the program that uses it is loaded into memory for execution. This is referred to as load-time dynamic linking or early binding, because the links to the functions used are established as soon as the program and DLL have been loaded into memory. This kind of operation was illustrated in Figure 20 -2; however, this isn’t the only choice available. It’s also possible to cause a DLL to be loaded after execution of a program has started. This is called runtime dynamic linking or late binding. The sequence of operations that occurs with late binding is illustrated in Figure 20 -3.
Understanding DLLs
Library1.dll
❘ 1179
3. Library2.dll loaded at the request of the program.
Library2.dll Library3.dll 1. Program is loaded but no DLL is loaded. The program may use any one of the three DLLs.
Program.exe
Program
Library2.dll 2. At this point, the program determines that Library2.dll is required and causes it to be loaded.
Function
4. The program obtains the address of a function from the DLL and uses it to call the function.
Computer Memory FIGURE 20 -3
Runtime dynamic linking enables a program to defer linking of a DLL until it’s certain that the functions in a DLL are required. This enables you to write a program that can choose to load one or more of a number of DLLs based upon input to the program, so that only those functions that are necessary are actually loaded into memory. In some circumstances, this can drastically reduce the amount of memory required to run a program. A program implemented to use runtime dynamic linking calls the Windows API function LoadLibrary() to load the DLL when it’s required. The address of a function within the DLL can then be obtained via the function GetProcAddress(). When the program no longer has a need to
1180
❘
CHAPTER 20
WRITING YOUR OWN DLLS
use the DLL, it can detach itself from the DLL by calling the FreeLibrary() function. If no other program is using the DLL, it will be deleted from memory. I won’t be going into further details of how this works in this book.
Contents of a DLL A DLL isn’t limited to storing code for functions. You can also put other global entities such as classes, global variables, and resources into a DLL, including such things as bitmaps and fonts. The Solitaire game that comes with Windows uses a dynamic link library called Cards.dll, which contains all the bitmap images of the cards and functions to manipulate them. If you wanted to write your own card game, you could conceivably use this DLL as a base and save yourself the trouble of creating all the bitmaps needed to represent the cards. Of course, to use it, you would need to know specifically which functions and resources are included in the DLL. You can also define static global variables in a DLL, including C++ class objects, so that these can be accessed by programs using it. The constructors for global static class objects are called automatically when such objects are created. You should note that each program using a DLL gets its own copy of any static global objects defined in the DLL, even though they may not necessarily be used by a program. For global class objects, this involves the overhead of calling a constructor for each. You should, therefore, avoid introducing such objects into a DLL unless they are absolutely essential.
The DLL Interface You can’t access just anything that’s contained in a DLL. Only items that have been specifically identified as exported from a DLL are visible to the outside world. Functions, classes, global static variables, and resources can all be exported from a DLL, and those that are make up the interface to it. Anything that isn’t exported can’t be accessed from the outside. You’ll see how to export items from a DLL later in this chapter.
The DllMain() Function Even though a DLL isn’t executable as an independent program, it does contain a special variety of the main() function, called DllMain(). This is called by Windows when the DLL is fi rst loaded into memory to allow the DLL to do any necessary initialization before its contents are used. Windows will also call DllMain() just before it removes the DLL from memory to enable the DLL to clean up after itself if necessary. There are also other circumstances in which DllMain() is called, but these situations are outside the scope of this book.
DLL Varieties There are three different kinds of DLL that you can build with Visual C++ 2010 using MFC: an MFC extension DLL, a regular DLL with MFC statically linked, and a regular DLL with MFC dynamically linked.
MFC Extension DLL You build this kind of DLL whenever it’s going to include classes derived from the MFC. Your derived classes in the DLL effectively extend the MFC. The MFC must be accessible in the
Deciding What to Put in a DLL
❘ 1181
environment in which your DLL is used, so all the MFC classes are available together with your derived classes — hence the name “MFC extension DLL.” However, to derive your own classes from the MFC isn’t the only reason to use an MFC extension DLL. If you’re writing a DLL that includes functions that pass pointers to MFC class objects to functions in a program using it, or that receive such pointers from functions in the program, you must create it as an MFC extension DLL. Accesses to classes in the MFC by an extension DLL are always resolved dynamically by a link to the shared version of MFC that is itself implemented in DLLs. An extension DLL is created with the shared DLL version of the MFC, so when you use an extension DLL, the shared version of MFC must be available. An MFC extension DLL can be used by a normal Application Wizard– generated application. It requires the option Use MFC in a Shared DLL to be selected under the General set of properties for the project, which you access through the Project ➪ Properties menu option. This is the default selection with an Application Wizard – generated program. Because of the fundamental nature of the shared version of the MFC in an extension DLL, an MFC extension DLL can’t be used by programs that are statically linked to MFC.
Regular DLL — Statically Linked to MFC This is a DLL that uses MFC classes linked statically. Use of the DLL doesn’t require MFC to be available in the environment in which it is used, because the code for all the classes it uses is incorporated into the DLL. This bulks up the size of the DLL, but the big advantage is that this kind of DLL can be used by any Win32 program, regardless of whether or not it uses MFC.
Regular DLL — Dynamically Linked to MFC This is a DLL that uses dynamically linked classes from MFC but doesn’t add classes of its own. This kind of DLL can be used by any Win32 program, regardless of whether it uses MFC itself; however, use of the DLL does require the MFC to be available in the environment. You can use the Application Wizard to build all three types of DLL that use MFC. You can also create a project for a DLL that doesn’t involve MFC at all, by creating a Win32 project type using the Win32 Project template and selecting DLL in the application settings for the project.
DECIDING WHAT TO PUT IN A DLL How do you decide when you should use a DLL? In most cases, the use of a DLL provides a solution to a particular kind of programming problem, so if you have the problem, a DLL can be the answer. The common denominator is often sharing code among a number of programs, but there are other instances in which a DLL provides advantages. The kinds of circumstances in which putting code or resources in a DLL is a very convenient and efficient approach include the following: ➤
You have a set of functions or resources on which you want to standardize and that you will use in several different programs. The DLL is a particularly good solution for managing these, especially if some of the programs using your standard facilities are likely to be executing concurrently.
➤
You have a complex application that involves several programs and a lot of code, but that has sets of functions or resources that may be shared among several of the programs in the
1182
❘
CHAPTER 20
WRITING YOUR OWN DLLS
application. Using a DLL for common functionality or common resources enables you to manage and develop these with a great deal of independence from the program modules that use them, and can simplify program maintenance. ➤
You have developed a set of standard application- oriented classes derived from MFC that you anticipate using in several programs. By packaging the implementation of these classes in an extension DLL, you can make using them in several programs very straightforward, and, in the process, provide the possibility of being able to improve the internals of the classes without affecting the applications that use them.
➤
You have developed a brilliant set of functions that provide an easy-to -use but amazingly powerful toolkit for an application area that just about everybody wants to dabble in. You can readily package your functions in a regular DLL and distribute them in this form.
There are also other circumstances in which you may choose to use DLLs, such as when you want to be able to dynamically load and unload libraries, or to select different modules at run time. You could even use them to make the development and updating of your applications easier generally. The best way of understanding how to use a DLL is to create one and try it out. Let’s do that now.
WRITING DLLS This section examines two aspects of writing a DLL: how you actually write a DLL, and how you defi ne what’s to be accessible in the DLL to programs that use it. As a practical example of writing a DLL, you’ll create an extension DLL to add a set of application classes to the MFC. You’ll then extend this DLL by adding variables available to programs using it.
Writing and Using an Extension DLL You can create an MFC extension DLL to contain the shape classes for the Sketcher application. Although this will not bring any major advantages to the program, it demonstrates how you can write an extension DLL without involving yourself in the overhead of entering a lot of new code. The starting point is the Application Wizard, so create a new project by pressing Ctrl+Shift+N and choosing the project type as MFC and the template as MFC DLL. This selection identifies that you are creating a project for an MFC -based DLL with the name ExtDLLExample. Click the OK button and select Application Settings in the next window displayed. The window looks as shown in Figure 20 - 4.
FIGURE 20 -4
Writing DLLs
❘ 1183
Here, you can see three radio buttons corresponding to the three types of MFC -based DLL that I discussed earlier. You should choose the third option, as shown in the figure. The two checkboxes below the fi rst group of three radio buttons enable you to include code to support Automation and Windows sockets in the DLL. These are both advanced capabilities within a Windows program, so you don’t need either of them here. Automation provides the potential for hosting objects created and managed by one application inside another. Windows sockets provide classes and functionality to enable your program to communicate over a network, but you won’t be getting into this as it’s beyond the scope of the book. You can click the Finish button and complete creation of the project. Now that the MFC DLL wizard has done its stuff, you can look into the code that has been generated on your behalf. If you look at the contents of the project in the Solution Explorer pane, you’ll see that the MFC DLL wizard has generated several files, including a .txt file that contains a description of the other files. You can read what they’re all for in the .txt file, but the two shown in the following table are the ones of immediate interest.
FILE NAME
CONTENTS
dllmain.cpp
This contains the function DllMain() and is the primary source file for the DLL.
ExtDLLExample.def
The information in this file is used during compilation. It contains the name of the DLL, and you can also add to it the definitions of those items in the DLL that are to be accessible to a program using the DLL. You’ll use an alternative and somewhat easier way of identifying such items in the example.
When your DLL is loaded, the fi rst thing that happens is that DllMain() is executed, so perhaps you should take a look at that fi rst.
Understanding DllMain() If you look at the contents of dllmain.cpp, you will see that the MFC DLL wizard has generated a version of DllMain() for you, as shown here: extern "C" int APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { // Remove this if you use lpReserved UNREFERENCED_PARAMETER(lpReserved); if (dwReason == DLL_PROCESS_ATTACH) { TRACE0("EXTDLLEXAMPLE.DLL Initializing!\n"); // Extension DLL one-time initialization if (!AfxInitExtensionModule(ExtDLLExampleDLL, hInstance)) return 0;
1184
❘
CHAPTER 20
WRITING YOUR OWN DLLS
// Insert this DLL into the resource chain // NOTE: If this Extension DLL is being implicitly linked to by // an MFC Regular DLL (such as an ActiveX Control) // instead of an MFC application, then you will want to // remove this line from DllMain and put it in a separate // function exported from this Extension DLL. The Regular DLL // that uses this Extension DLL should then explicitly call that // function to initialize this Extension DLL. Otherwise, // the CDynLinkLibrary object will not be attached to the // Regular DLL's resource chain, and serious problems will // result. new CDynLinkLibrary(ExtDLLExampleDLL); } else if (dwReason == DLL_PROCESS_DETACH) { TRACE0("EXTDLLEXAMPLE.DLL Terminating!\n"); // Terminate the library before destructors are called AfxTermExtensionModule(ExtDLLExampleDLL); } return 1; // ok }
There are three arguments passed to DllMain() when it is called. The fi rst argument, hInstance, is a handle that has been created by Windows to identify the DLL. Every task under Windows has an instance handle that identifies it uniquely. The second argument, dwReason, indicates the reason why DllMain() is being called. You can see this argument being tested in the if statements in DllMain(). The fi rst if tests for the value DLL_PROCESS_ATTACH, which indicates that a program is about to use the DLL, and the second if tests for the value DLL_PROCESS_DETACH, which indicates that a program has fi nished using the DLL. The third argument is a pointer that’s reserved for use by Windows, so you can ignore it. When the DLL is fi rst used by a program, it’s loaded into memory, and the DllMain() function is executed with the argument dwReason set to DLL_PROCESS_ATTACH. This results in the Windows API function AfxInitExtensionModule() being called to initialize the DLL and an object of the class CDynLinkLibrary created on the heap. Windows uses objects of this class to manage extension DLLs. If you need to add initialization of your own, you can add it to the end of this block. Any cleanup you require for your DLL can be added to the block for the second if statement.
Adding Classes to the Extension DLL You will use the DLL to contain the implementation of the Sketcher shape classes. Move the fi les Elements.h and Elements.cpp from the folder containing the source for Sketcher to the folder containing the DLL. Be sure that you move rather than copy the fi les. Because the DLL is going to supply the shape classes for Sketcher, you don’t want to leave them in the source code for Sketcher. You’ll also need to remove Elements.cpp from the Sketcher project once you have copied it to the DLL project. To do this, open the Sketcher project, highlight Elements.cpp in the Solution Explorer pane by clicking the fi le, and then press Delete. If you don’t do this, the compiler complains that it
Writing DLLs
❘ 1185
can’t fi nd the fi le when you try to compile the project. Follow the same procedure to get rid of Elements.h from the Header Files folder in the Solution Explorer pane. You now need to add Elements.h and Elements.cpp to the extension DLL project, so open the ExtDLLExample project, select the menu option Project ➪ Add Existing Item, and choose the fi les Elements.cpp and Elements.h from the list box in the dialog box, as shown in Figure 20 -5. You can add multiple fi les in a single step by holding down the control key while you select from the list of fi les in the Add Existing FIGURE 20 -5 Item dialog box. You should eventually see the fi les you have added in the Solution Explorer pane and all the shape classes displayed in the Class View pane for the project. The shape classes use the constants VERSION_NUMBER and SELECT_COLOR that you have defi ned in the fi le SketcherConstants.h, so copy the defi nitions for these from the SketcherConstants.h fi le in the Sketcher project folder to the Elements.cpp fi le in the folder containing the DLL; you can place them immediately after the #include directives. Note that the VERSION_NUMBER and SELECT_COLOR variables are used exclusively by the shape classes, so you can delete them from the SketcherConstants.h fi le used in the Sketcher program or just comment them out.
Exporting Classes from the Extension DLL The collection of entities in a DLL that can be accessed by a program that is using it is referred to as the interface to the DLL. The process of making an object part of the interface to a DLL is referred to as exporting the object, and the process of accessing an object that is exported from a DLL is described as importing the object. The names of the classes defi ned in the DLL that are to be accessible in programs that use it must be identified in some way so that the appropriate links can be established between a program and the DLL. As you saw earlier, one way of doing this is by adding information to the .def fi le for the DLL. This involves adding what are called decorated names to the DLL and associating each decorated name with a unique identifying numeric value called an ordinal. A decorated name for an object is a name generated by the compiler, which adds an additional string to the name you gave to the object. This additional string provides information about the type of the object or, in the case of a function (for example), information about the types of the parameters to the function. Among other things, it ensures that everything has a unique identifier and enables the linker to distinguish overloaded functions from each other. Obtaining decorated names and assigning ordinals to export items from a DLL is a lot of work and isn’t the best or the easiest approach. A much easier way to identify the classes that you want to
1186
❘
CHAPTER 20
WRITING YOUR OWN DLLS
export from the DLL is to modify the class defi nitions in Elements.h to include the MFC macro AFX_EXT_CLASS before each class name, as shown in the following for the CLine class: // Class defining a line object class AFX_EXT_CLASS CLine: public CElement { // Details of the class definition as before... };
The AFX_EXT_CLASS macro indicates that the class is to be exported from the DLL. This has the effect of making the complete class available to any program using the DLL and automatically allows access to any of the data and functions in the public interface of the class. AFX_EXT_CLASS is defi ned as __declspec(dllexport) when it appears within an extension DLL and __declspec(dllimport) otherwise. This means that you can use the same macro for exporting entities from a DLL and importing entities into an application. However, the dllexport and dllimport attributes provide a more general mechanism for exporting and importing entities
in a DLL that you can use regardless of whether or not your DLL is an MFC extension, so I will use these in the example, rather than AFX_EXT_CLASS, which is available only with MFC. Here’s how you can use __declspec(dllexport) to export the CLine class from the DLL: // Macro to make the code more readable #define SKETCHER_API __declspec(dllexport) // Class defining a line object class SKETCHER_API CLine: public CElement { // Details of the class definition as before... };
You need to add SKETCHER_API to all the other shape classes, including the base class CElement, in exactly the way as with the CLine class. Why is it necessary to export CElement from the DLL? After all, programs create only objects of the classes derived from CElement, and not objects of the class CElement itself. One reason is that you have declared public members of CElement that form part of the interface to the derived shape classes, and that are almost certainly going to be required by programs using the DLL. If you don’t export the CElement class, functions such as GetBoundRect()will not be available. Another reason is that you create variables of type CElement or CElement* in Sketcher, so you need the class defi nition available for such a defi nition to compile. You have done everything necessary to add the shape classes to the DLL. All that remains is for you to compile and link the project to create the DLL.
Building a DLL You build the DLL in exactly the same way that you build any other project — by using the Build ➪ Build Solution menu option or selecting the corresponding toolbar button. The output produced is somewhat different, though. You can see the fi les that are produced in the debug subfolder of the project folder for a Debug build, or in the release subfolder for a Release build. The executable code
Writing DLLs
❘ 1187
for the DLL is contained in the fi le ExtDLLExample.dll. This fi le needs to be available to execute a program that uses the DLL. The fi le ExtDLLExample.lib is an import library fi le that contains the defi nitions of the items that are exported from the DLL, and it must be available to the linker when a program using the DLL is linked.
NOTE If you find the DLL build fails because Elements.cpp contains an #include directive for Sketcher.h, just remove it. On my system, the Class Wizard added the #include directive for Sketcher.h when creating the code for the CElement class, but it is not required.
Importing Classes from a DLL You now have no information in the Sketcher program on the shape classes because you moved the fi les containing the class defi nitions and implementations to the DLL project. However, the compiler still must know where the shape classes are coming from in order to compile the code for the Sketcher program. The Sketcher application needs to include a header fi le that defi nes the classes that are to be imported from the DLL. It must also identify the classes as external to the project by using the __declspec(dllimport) qualification for the class names in the class defi nitions. An easy way to do this is to modify the fi le Elements.h in the DLL project so that it can be used in the Sketcher project as well. What you need is for the class defi nitions in Elements.h to be qualified with __declspec(dllexport) when they are in the DLL project and qualified with __declspec(dllimport) when they are in the Sketcher project. You can achieve this by replacing the #define macro for the SKETCHER_API symbol in Elements.h with the following: #ifdef ELEMENT_EXPORTS #define SKETCHER_API __declspec(dllexport) #else #define SKETCHER_API __declspec(dllimport) #endif
If the symbol ELEMENT_EXPORTS is defined, then SKETCHER_API will be replaced by __declspec(dllexport); otherwise, it will be replaced by __declspec(dllimport). ELEMENT_EXPORTS is not defined in Sketcher, so using the header in Sketcher will do what you want. All you need is to define ELEMENT_EXPORTS in the ExtDLLExample project. You can do this by adding the following to the stdafx.h header in the DLL project, immediately following the #pragma once directive: #define ELEMENT_EXPORTS
Now you can rebuild the DLL project.
Using the Extension DLL in Sketcher Now that you have copied the new version of the Elements.h fi le to the project, you should add it back into the project by right- clicking the Header Files folder in the Solution Explorer pane and selecting Add ➪ Existing Item from the context menu.
1188
❘
CHAPTER 20
WRITING YOUR OWN DLLS
When you rebuild the Sketcher application, the linker needs to have the ExtDLLExample.lib fi le identified as a dependency for the project, because this file contains information about the contents of the DLL. Right- click Sketcher in the Solution Explorer pane and select Properties from the pop -up. You can then expand the Linker folder and select Input in the left pane of the Properties window. Then enter the name of the .lib fi le as an additional dependency, as shown in Figure 20 - 6.
FIGURE 20 - 6
Figure 20 - 6 shows the entry for the debug version of Sketcher. The .lib fi le for the DLL is in the Debug folder within the DLL project folder. If you create a release version of Sketcher, you’ll also need the release version of the DLL available to the linker and its .lib fi le, so you’ll have to enter the fully qualified name of the .lib fi le for the release version of the DLL, corresponding to the release version of Sketcher. The fi le to which the properties apply is selected in the Configuration drop -down list box in the Properties window. You have only one external dependency, but you can enter several when necessary by clicking the button to the right of the text box for input. Because the full path to the .lib fi le has been entered here, the linker will know not only that ExtDLLExample.lib is an external dependency, but also where it is. For the Sketcher application to compile and link correctly, the .lib fi le for the DLL must be available to the application. When you execute Sketcher, the .dll fi le is required. To enable Windows to fi nd and load a DLL for a program when it executes, it’s usual to place the DLL in your Windows System32 folder. If it’s not in this folder, Windows searches the folder containing the executable (Sketcher.exe in this case). If it isn’t there, you’ll get an error message. To avoid
Writing DLLs
❘ 1189
cluttering up your System32 folder unnecessarily, you can copy ExtDllExample.dll from the Debug folder of the DLL project to the Debug folder for Sketcher. If you now build the Sketcher application once more, you’ll fi nd there is a problem. In particular, the Serialize() member of CSketcherDoc causes a LNK2001 linker error. The problem is with the operator>>() member of the CArchive class. There is a version of this function defi ned in the CArchive class that supports type CObject, and, in the original version of Sketcher, the compiler was happy to use this to deserialize CElement objects before storing them in the list container. With the CElement class now imported from a DLL, this no longer works. However, you can get around this quite easily. You can change the code in the Serialize() function that reads shape objects to the following: size_t elementCount(0); // Count of number of elements CObject* pElement(nullptr); // Element pointer ar >> elementCount; // retrieve the element count // Now retrieve all the elements and store in the list for(size_t i = 0 ; i < elementCount ; ++i) { ar >> pElement; m_ElementList.push_back(static_cast(pElement)); }
Now, you store the address of the shape object that is to be deserialized as type CObject*. You then cast this to type CElement* before storing it in the list. With these changes, the Serialize() function will compile and work as before. Sketcher should now execute exactly as before, except that now it uses the shape classes in the DLL you have created.
Files Required to Use a DLL From what you have just seen in the context of using the DLL you created in the Sketcher program, you can conclude that three fi les must be available to use a DLL in a program, as shown in the following table. EXTENSION
CONTENTS
.h
Defines those items that are exported from a DLL and enables the compiler to deal properly with references to such items in the source code of a program using the DLL. The .h file needs to be added to the source code for the program using the DLL.
.lib
Defines the items exported by a DLL in a form, which enables the linker to deal with references to exported items when linking a program that uses a DLL.
.dll
Contains the executable code for the DLL, which is loaded by Windows when a program using the DLL is executed.
If you plan to distribute program code in the form of a DLL for use by other programmers, you need to distribute all three fi les in the package. For applications that already use the DLL, only the .dll fi le is required to go along with the .exe fi le that uses it.
1190
❘
CHAPTER 20
WRITING YOUR OWN DLLS
SUMMARY In this chapter, you’ve learned the basics of how to construct and use a DLL.
EXERCISE You can download the source code for the examples in the book and the solutions to the following exercise from www.wrox.com.
1.
This is the last time you’ll be amending this version of the Sketcher program, so try the following. Using the DLL we’ve just created, implement a Sketcher document viewer — in other words, a program that simply opens a document created by Sketcher and displays the whole thing in a window at once. You need not worry about editing, scrolling, or printing, but you will have to work out the scaling required to make a big picture fit in a little window!
Summary
❘ 1191
WHAT YOU LEARNED IN THIS CHAPTER TOPIC
CONCEPT
Dynamic link libraries
Dynamic link libraries provide a means of linking to standard functions dynamically when a program executes, rather than incorporating them into the executable module for a program.
MFC DLLs
An Application Wizard – generated program links to a version of MFC stored in DLLs by default.
Using DLLs
A single copy of a DLL in memory can be used by several programs executing concurrently.
Extension DLLs
An extension DLL is so called because it extends the set of classes in MFC. An extension DLL must be used if you want to export MFC- based classes or objects of MFC classes from a DLL. An extension DLL can also export ordinary functions and global variables.
Regular DLLs
A regular DLL can be used if you want to export only ordinary functions or global variables that aren’t instances of MFC classes.
Exporting classes from a DLL
You can export classes from an extension DLL by using the keyword AFX_EXT_CLASS before the class name in the DLL.
Importing classes from a DLL
Using the AFX_EXT_CLASS keyword, you can import the classes exported from an extension DLL by using the .h file from the DLL that contains the class definitions.
INDEX
Symbols + (addition) operator, 459–463 && (AND) operator, 131
= (assignment) operator, 45 overloading, 454–459 \ (backslash), 64 { } (curly braces), 44, 129 -- (decrement) operator, 540 >> (extraction) operator, 61 ( ) (function call) operator, 465–466 ++ (increment) operator, 74 overloading, 540 \n (newline), 65 ! (NOT) operator, 132 | | (OR) operator, 131–132 : : (scope resolution operator), 26 ; (semicolon), 44–50 / / (slashes), 41 [ ] (subscript) operator, 516 \t (tab), 64–65 ~ (tilde), 436 % operator, 491 >), 61
1206
F f( ) function, 313
fields initonly, 429–431
serializable, 1155–1156 File New, 1170 fi le operations, dialogs, 1164 FileMode enumeration, 1158–1159 fi les DLLs, 1191 program fi les, naming, 509 sketches, 1169–1170 fill( ) function, 725 fi ltering lists, 682–685 fi nalizers in reference classes, 625–628 find( ) function, 523–524, 705, 725–726 FindElement( ) function, 1026 find_first_not_of( ) function, 532 find_first_of( ) member function, 525–528 findIndex( ) function, 848–849 find_last_of( ) member function, 525–528 FixNonSerializedData( ) function, 1156 float data type, 58 floating-point loop counters, 150 floating-point numbers, 56–58 reading from keyboard, 61 floating windows, 11 folders, project folders, 13 Font object, 1114–1115 FontFamily object, 1115 fonts creating, 1114–1115 selecting, 1115–1116 for each loop, 161–163 for loop, 140–142, 324 counters, multiple, 143–144 indefi nite, 144–146 nested, 507 variations on, 142–150 format specifiers, 107 formatting, output, 63–64 C++/CLI, 108–109 left-aligned, 64 forms, closing, 1171 forward iterators, 648 frames, status bars, 1089–1093 free store, 201–205
freopen_s( ) function – functions
debugging controlling, 785–786 functions for checking, 784–785 output, 786–793 freopen_s( ) function, 790 friend classes, 571–572 friend function, 386–388 C++/CLI, 406 friend function, 451, 569–571 friend keyword, 386–388 front_inserter( ) function, 719–720 FunA( ) function, 802 FunB( ) function, 802 FunC( ) function, 802 function adapters (STL), 650–651 function members, 366 adding, 497–503 static, 401 function objects, 723–724 STL, 650 templates, 486 _FUNCTION_ preprocessor macro, 773 function templates, 314–316 defi ning, 479 functional header, 650 functional notation, 51 enumerators and, 60 functions abort( ), 768–769 accessor get( ), 416 set( ), 416 accumulate( ), 674–675, 716 action expressed, 44 Add( ), 479 AddElement( ), 1010 AddMenu( ), 1024 AddRadioButton( ), 1087–1088 addresses, returning, 279–280 AddString( ), 1095 AfxMessageBox( ), 974–975, 1084–1085 analyzing numbers, 326–330 append( ), 516, 769 Arc( ), 953 Area( ), 358 arguments, 252 accepting variable number, 275–277 omitting, 302–303 passing by pointers, 262–263
passing by value, 260–261 references as, 267–270 arrays multidimensional, 266–267 passing, 263–267 pointer notation, 264–265 ArrayToList( ), 1161 Assert( ), 799 assert( ), 768–769 AssertValid( ), 989 assign( ), 663, 679 assignment operator, 455 returning reference, 455 at( ), 660 average( ), 263–264 begin( ), 654, 659, 1012 BeginPaint( ), 830 BinarySearch( ), 224 blanks elimination from string, 322 body, 255 BoxSurface( ), 388 C++/CLI programming, variable number of arguments, 289–290 calculator implementation, 319–322 calls call stack, 775–776 overloading call operator, 465–466 capacity( ), 655 CBox( ), 374 Ceil( ), 1177–1178 CGlassBox, 575–576 Char: :IsLetter( ), 162 Char: :ToLower( ), 158 Char: :ToUpper( ), 158 CheckDlgButton( ), 1068 checking free store, 784–785 in classes, 366 Clear( ), 219 clear( ), 661, 671, 679, 1011 ClientToScreen( ), 1024 combine_each( ), 868 Compare( ), 239–240, 391–392 CompareTo( ), 338, 602 Console: :Read( ), 158 Console: :ReadKey( ), 159 Console: :Write( ), 158 Contains( ), 1046 ContinueRouting( ), 922 Create( ), 838
1207
functions – functions
functions (continued) CreateBrush( ), 1163 CreateCompatibleBitmap( ), 855 CreateElement( ), 988, 1071–1072 CreatePen( ), 955, 1028, 1162 CreateSolidBrush( ), 957–958 CreateWindow( ), 820, 827 CreateWindowsEx( ), 861 _CrtDumpMemoryLeaks( ), 785 _CrtMemCheckpoint( ), 784 _CrtMemDifference( ), 784 _CrtMemDumpAllObjectsSince( ), 785 _CrtMemDumpStatistics( ), 784–785 _CrtSetDbgFlag( ), 786 _CrtSetReportFile( ), 786–787 _CrtSetReportMode( ), 786–787 c_str( ), 519 DDX_Text( ), 1076 DeleteElement( ), 1031, 1132 deleteEntry( ), 713 DeleteObject( ), 956 Deserialize( ), 1159 DestroyWindow( ), 1067 DisplayMessage( ), 443 DllMain( ), 1182, 1185–1186
DLLs, 1179 DoDataExchange( ), 1076 DoIt( ), 797 DoModal( ), 1064, 1068, 1078 DoPreparePrinting( ), 1147 Draw( ), 972–973, 1100 DrawSet( ), 854 DrawSetParallel( ), 866–867 DrawString( ), 1113 DrawText( ), 1102 eatspaces( ), 322 Ellipse( ), 952–954 empty( ), 514, 656, 678 Enable( ), 922, 1093 end( ), 654 EndsWith( ), 240–243 EqualRect( ), 360 Equals( ), 596 erase( ), 662, 679 expr( ), 320, 322–325
expression evaluation, 322–325 extract( ), 333–336, 348–349 f( ), 313 fill( ), 725
1208
find( ), 523–524, 705, 725–726 FindElement( ), 1026 find_first_not_of( ), 532 findIndex( ), 848–849 FixNonSerializedData( ), 1156 freopen_s( ), 790
friend, 386–388 friend, 451, 569–571 front_inserter( ), 719–720 FunA( ), 802 FunB( ), 802 FunC( ), 802 generate( ), 733 generic, 337–343 GetBoundRect( ), 976 GetCapture( ), 992 GetClientRect( ), 830 GetContextManager( ), 1024 GetCursorPos( ), 1033 GetDeviceCaps( ), 1081, 1149–1150 GetDocExtent( ), 1145 GetDocument( ), 894, 949, 989, 1132 GetFrequency( ), 860 GetFromPage( ), 1143 GetHeight( ), 498 GetLength( ), 498 getline( ), 175, 331, 510, 515, 531 GetMaxPage( ), 1143 GetMessage( ), 823–824 GetMinPage( ), 1143 getName( ), 772 getNameLength( ), 773 getPerson( ), 713 GetProcAddress( ), 1181 GetSelectedRadioButtonID( ), 1088–1089 GetStockObject( ), 819 GetToPage( ), 1143 GetWidth( ), 498 global, adding, 503–504 headers, 253–254 Hit( ), 1046 ignore( ), 712 Indent( ), 796 IndexOf( ), 240–243 IndexOfAny( ), 242 Inflate( ), 1048–1050 InflateRect( ), 360 inherited, 573–574 InitializeComponent( ), 930–931
functions – functions
InitInstance( ), 837, 890, 896 initPerson( ), 665
inline, 372–373 defi ning, 498 inline keyword, 373 input_names( ), 36 insert( ), 518, 661 instantiation, 315 Invalidate( ), 1006, 1047 InvalidateRect( ), 967–968, 1013 Invoke( ), 614, 616–618 IsDigit( ), 348 isdigit( ), 327 IsLower( ), 158 IsNullOrEmpty( ), 344 IsPrinting( ), 1149–1150 IsStoring( ), 1126 IsSupported( ), 1084 IteratePoint( ), 851–852, 853 Join( ), 235 LastIndexOf( ), 241 LineTo( ), 951–952, 1018–1019 listEntries( ), 714 listInfo( ), 658 listnames, 514 listRecords( ), 700 listVector( ), 733 LoadLibrary( ), 1181 LoadStdProfileSettings( ), 896 lock( ), 865–867 LPtoDP( ), 1021 main( ), 36, 44 (See also main( ) function) make_pair( ), 703 Max( ), 478, 479 max( ), 310 MaxElement( ), 338 max_size( ), 656 member functions, 370–372 at( ), 516 class templates, 479–481 const, 393–394 data( ), 519 defi nition outside class, 394–395 find_first_of( ), 525–528 find_last_of( ), 525–528 getline( ), 667 length( ), 510 positioning defi nition, 372 swap( ), 518
menu messages, 915–916 coding, 916–920 merge( ), 681 Move( ), 1100, 1103–1105 MoveElement( ), 1034, 1103–1105, 1132 MoveRect( ), 358 MoveTo( ), 950 names, 252 need for, 253 new, 611–612 Next( ), 221 NormalizeRect( ), 977 number( ), 326, 347–348 Offset( ), 1054 OnAppAbout( ), 890 OnBeginPrinting( ), 1147 OnCancel( ), 1066 OnCreate( ), 1090–1091 OnDraw( ), 949–950, 967 OnEndPrinting( ), 1147, 1148 OnInitDialog( ), 1068, 1076 OnInitialUpdate( ), 1021, 1083–1084 OnLButtonDown( ), 963 OnLButtonUp( ), 963 OnMouseMove( ), 963 OnPrepareDC( ), 1020, 1080–1081, 1142, 1149–1150 OnPreparePrinting( ), 1142, 1147 OnPrint( ), 1142, 1150 OnViewScale( ), 1083–1084 OpenWrite( ), 1159 operator>, 447, 450–452 operator, defi ning, 447 operator( ), 447–448 operator, 452–454 C++/CLI, 533 CString class, 1098 disallowed operators, 447 implementing overloaded operator, 447–450 increment/decrement, 540 operator> function, 447 reference classes, 540–543 value classes, 534–539 safe_cast, 110 scope resolution, 94–96 sizeof, 190–192 static_cast, 590 typeid, 81 unary, 82 options, setting, 27–28 OR (| |) operator, 131–132 organizing code, 508–509
1220
out_of_range exception, 516, 660 output C++/CLI, to command line, 105 to command line, 62–63 conditional operators, 134–135 Debug class, 794–795 controlling, 797–799 destination, 795–796 indenting, 796 device context, 947 formatting, 63–64 left-aligned, 64 free store debugging, 786–793 Trace class, 794–795 controlling, 797–799 destination, 795–796 indenting, 796 Windows GDI, 946 Output( ) function, 579–580 output iterators, 648 output statements, 45 output stream iterators, 720–723 overloading constructors, 378 functions, 310–312 reference types for parameters, 313 when to, 313–314 operators, 489–491 + (addition), 459–463 = (assignment) operator, 454–459 > (greater than) operator, 452–454 C++/CLI, 533 CString class, 1098 disallowed operators, 447 implementing overloaded operator, 447–450 increment/decrement, 540 operator> function, 447 reference classes, 540–543 value classes, 534–539 override keyword, 567
P PadLeft( ) function, 238 PadRight( ) function, 238
page coordinates, mapping mode, 1079 Paint( ) event handler
drawing in, 1044 implementing, 1006
parallel execution/serial execution – PPL (Parallel Patterns Library)
parallel execution/serial execution, switching between, 861–862 parallel loop iterations, 845 parallel processing, 843–844 algorithms, 844 parallel_for, 844, 845–846, 866–867 parallel_for_each, 844, 846–849 parallel_invoke, 844, 849, 857–859 example, 850–864 parallel_for algorithm, 844, 845–846 Mandelbrot set, 866–867 terminating operations, 848–849 parallel_for_each algorithm, 844, 846–849 parallel_invoke algorithm, 844, 849 Mandelbrot set, 857–859 parameters const modifier, 270–271 destructors, 436 empty, 254 functions initialization, 302–303 reference parameters, 448 initialization, 269 insert( ) function, 518 multiple, class templates, 483–485 reference lvalue, 470 rvalue, 271–273 applying, 470–472 values, assigning default, 378–380 void keyword, 254 WinMain( ) function, 816 parenthesized substrings, extracting, 348–349 pbuffer pointer, 199 pdata pointer, 194 pen drawing, CLR, 999–1001 Pen object, serialization, 1161–1163 Pen properties, 999–1000 pen styles for drawing, 955 brushes, 957 pen widths, 1070–1071 element creation, 1108–1109 PenDialog( ) function, 1107 pfun( ) function, 296 plotters, Windows GDI, 946 pmessage member, 439, 470 pointers arithmetic, 194–196 array names as, 196–198
arrays and, 194–201 arrays of, 188–190 to char, 186 class objects, 576–578 to classes, 402–403 base classes, 576–578 derived classes, 577–578 to constants, 192–194 declaring, 181–182 dereferencing, 195 to functions, 295–296 as arguments, 299–301 arrays of, 301 declaring, 296–299 incrementing/decrementing, 195 indirection operator, 182 initializing, 183–190, 202 interior, 244–247 lpfnWndProc member, 818 multidimensional arrays and, 200 notation, 200–201 to objects, 401–404 overview, 181 passing arguments by, 262–263 pointer notation, 264–265 pbuffer, 199 pdata, 194 pstart, 246 reasons to use, 183 RECT object, 361 returning from functions, 277–280 struct, 361–362 struct members, 362 this, 390–392 using, 184–186 vector containers, storing, 669–671 PolyLine( ) function, 978–979 polymorphism, 363, 576 in loop body, 606 Pop( ) function, 592, 594, 599 pop-ups, 1026 pop_back( ) function, 661 pop_front( ) function, 671 positioning member function defi nitions, 372 variable declarations, 92 PostSerialization( ) function, 1156 power( ) function, 256 PPL (Parallel Patterns Library), 844
1221
PPL (Parallel Patterns Library) – queue container
PPL (Parallel Patterns Library) (continued) critical_section class, 865 templates, 844 #pragma once directive, 495, 504 precedence of operators, changing, 447 precompiled header fi les, 894 prefi x C, 364 prefi xes in Windows programs, 813–814 PreLoadState( ) function, 1023 preprocessor directives, 43 PreSerialization( ) function, 1157 Print dialog box, displaying, 1147 printArea( ) function, 465, 486 PrintDocument component, 1171 printers, Windows GDI, 946 printf( ) function, 1093 printing aligning text, 1153 cleanup, 1148–1149 device context, 1149–1150 document size, 1145–1146 documents, 1140–1144, 1150–1154 implementing, 1172 multipage, implementing, 1144–1155 paper size, 1144 process of, 1141–1144 sketches, PrintDocument component, 1171 storing print data, 1146–1147 priority queue containers, 689–694 private class members, 382–385 base classes, access, 556–558 private keyword, 382 Product( ) function, 621 product( ) function, 297 program bugs, 757 program fi les, naming, 509 program statements, 44–46 program windows creating, 820–821 initializing, 821–822 specifying, 817–820 programming with strings, 176–177 Windows, 5, 7–9 (See Windows) project folder, 13 projects, 13 console CLR, 24–26
1222
empty, 20–24 defi ning, 14–17 properties, 39 Solution Explorer, 17 source fi les, 39 Win32 console application, 14–17 properties Arrow Keys, 1074 Character Set, 39 classes, 416–429 indexed, 416, 423–428 scalar, 416 DialogResult, 1117 displaying, 39 DisplayStyle, 940 Multline, 1096 Pen, 999–1000 reserved names, 429 scalar defi ning, 416–419 trivial, 419–423 SelectedIndex, 1110 static, 429 toolbar buttons, editing, 925–926 Windows Forms, modifying, 931–932 property keyword, 416 Property Manager tab, 17, 41 protected class members, declaring, 562–564 protected keyword, 562–564 prototypes of functions, 256–259 proverb variable, 162 pstart pointer, 246 PtInRect( ) function, 1026 public keyword, 364, 374 public section, copy constructor, 443 pure virtual functions, 580–581 Push( ) function, 592, 594 push_back( ) function, 517, 653, 660 push_front( ) function, 671
Q query string, 517
queue containers, 685–688 priority containers, 689–694 reference class object storage, 740–744 queue header, 647 queue container, 647
radio buttons – rvalues
R radio buttons CTaskDialog class, 1087–1089
messages, 1069 random access iterators, 649 Random object, 221 randomValues( ) function, 733 rbegin( ) function, 655 Read( ) function, 109–110
reading key presses, 159–160 from stdin, 510 to string objects, 510 ReadKey( ) function, 109–110, 160 ReadLine( ) function, 109–110 ReconstructObject( ) function, 1157 recording document changes, 1131–1133 RECT object, 967 pointers to, 361 RECT structure, 360 Rectangle( ) function, 978–979 rectangles bounding rectangles, 975–977, 1048–1050 drawing CLR, 1001 windows and, 978–979 normalized, 977 RectVisible( ) function, 1012 recursion lambda expressions, 735–736 loops and, 285 using, 288–289 recursive functions, 285–289, 373 ref class keyword, 413 reference class types, 412–415 assignment operator (=), 543–544 copy constructor, 415 derived reference classes, 599–602 overloading operators, 540–543 reference classes destructors, 625–628 fi nalizers, 625–628 reference parameters functions, 448 lvalue, 470 rvalue, 271–273 applying, 470–472 reference types, parameters, overloaded functions, 313
references as arguments to a function, 267–270 to class objects, 404–406 introduction, 206 lvalue declaring, 207–208 initializing, 207–208 members, 355 returning from functions, 280–283 rvalue defi ning, 208 initializing, 208 tracking references, 244 virtual functions, 578–580 RegisterClass( ) function, 820 RegisterClassEx( ) function, 820 reinterpret_cast( ) keyword, 80 ReleaseCapture( ) function, 992 remainders, calculating, 72–73 RemoveAllRadioButtons( )
function, 1088 RemoveElement( ) function, 341 remove_if( ) function, 681 rend( ) function, 655
repeating blocks, loops, 139–142 Replace( ) function, 344 replace( ) function, 519, 725 reserve( ) function, 653, 659 reserved property names, 429 Reset( ) function, 457 ResetScrollSizes( ) function, 1082 resize( ) function, 656 Resource View tab, 17, 1061 RestorePenAndVectorData( )
function, 1164 result variable, 255 return statement, 128, 255–256
reverse-iterators, 1026 rhs, 74 ROP (Raster Operation), 984–985 trails, 1103 Run( ) function, 897 run( ) function, 870 runtime dynamic linking, 1180–1182 RUNTIME_CLASS( ) macro, 896 rvalues, 88–89, 271–273 applying, 470–472 defi ning, 208 initializing, 208
1223
safe_cast operator – SetRegistryKey( ) function
S safe_cast operator, 110 SavePenAndVectorData( ) function, 1164 SavePenAttributes( ) function, 1162 SaveSketch( ) function, 1167 SaveSketchCheck( ) function, 1170
scalable mapping modes, 1078–1080 scalar properties, 416 defi ning, 416–419 trivial, 419–423 Scale menu item, 1073 scaling, 1078 scrolling, implementing, 1082–1084 scope automatic variables, 89–92 global, 92 namespaces, 98 resolution, 26 variables, 255 scope resolution operator, 94–96 screen coordinates, pixels, 1079 ScreenToClient( ) function, 1033 scrollbars, 1060 setup, 1083–1084 scrolling, implementing, scaling and, 1082–1084 views in Sketcher, 1016–1021 SDI (Single Document Interface), 876 applications, creating, 882–886 view class, 893–894 searching array elements, 224 null-terminated strings, 213–215 one-dimensional arrays, 224–227 separator characters, 532 strings, 240–243, 523–533 any of a set of characters, 242–243 security, private keyword, 382 SelectClipRgn( ) function, 1154 SelectedIndex property, 1110 SelectObject( ) function, 958 SelectStockObject( ) function, 958 semantic errors, 757 semicolon (;), 44 struct, 354 variable declarations, 50 Send to Back operation, 1052 SendToBack( ) function, 1038–1039
1224
sentence string, 510
separator characters, searching for, 532 sequence containers (STL), 651–652 STL/CLR library, 737–745 vector containers, creation, 652–655 serial and parallel execution, switching between, 861–862 Serializable attribute, 1155 serialization applying, 1131–1139 binary, 1155–1159 Brush object, 1161–1163 classes Curve, 1163–1164 documents, 1125–1128 element classes, 1135–1139 implementing, 1131 macros, 1129 shape classes, 1136–1139 Sketch, 1160–1161 documents, 1133–1135 CSketcherDoc, 1124–1125 fields, 1155–1156 introduction, 1123–1124 objects, 1158–1159 Pen object, 1161–1163 process, 1129–1130 Sketcher, 1139–1140 sketches, 1160–1171 Serialize( ) function, 1125–1130, 1159 set( ) accessor function, 416 set header fi le, 647 SetCapture( ) function, 992 SetCheck( ) function, 922 SetCompatibleTextRenderingDefault( )
function, 932 SetDefaultRadioButton( ) function, 1088 SetElementTypeButtonState( )
function, 941, 1113 SetIndicators( ) function, 1090–1091 setiosflags( ) function, 523 SetMaxPage( ) function, 1143 SetMinPage( ) function, 1143 SetModifiedFlag( ) function, 1131–1132 SetPixel( ) function, 856 SetRadio( ) function, 922 SetRange( ) function, 1077 SetRegistryKey( ) function, 896
SetROP2( ) function – societal classes
SetROP2( ) function, 984–985 SetScrollSizes( ) function, 1017, 1021,
1083–1084 set container, 647 SetText( ) function, 922, 1093 SetTextAlign( ) function, 1153 SetTextColor( ) function, 1102 setValues( ) function, 733 SetViewportOrg( ) function, 1080 setw( ) function, 64, 144 SetWindowExt( ) function, 1080 SetWindowOrg( ) function, 1152
shape classes, serialization, 1136–1139 shapes, drawing deleting, 1022 moving, 1022 sharing case, 137–139 sharing memory, between variables, 444–446 shift operators, 86–88 short data type, 57 short keyword, 52 ShowDialog( ) function, 1084–1085, 1108, 1172 ShowIt( ) function, 439 showit( ) function, 302 showPerson( ) function, 666 ShowPopupMenu( ) function, 1025 ShowVolume( ) function, 572–573 ShowWindow( ) function, 821–822 signed char data type, 57 signed keyword, 55 size( ) function, 656 size of vector container, 655–660 Size parameter, constant expressions, 485 sizeof operator, 190–192 Sketch class defi ning, 1042–1044 serializable, 1160–1161 Sketcher project CDC class, 950–958 classes for elements, 968 coding menu message functions, 916–920 context menu, 1022–1038 controls, 1060–1061 creating, 897–899 dialogs, 1059–1060 creating resources, 1061–1063 displaying, 1065–1067
exercising, 1072 task dialogs, 1084–1088 DLLs, 1184–1191 drawing mouse and, 965–984 mouse messages, 962–965 edit box controls, 1096–1105 event handlers, 936–937 extending, 909–910 graphics, drawing in practice, 959–960 list boxes, 1093–1096 masked elements, 1038–1039 menu message functions, 915–916 menu messages, 914–915 menu resources, 910–913 message categories, 907–908 message handling, 908–909 message maps, 903–907 mouse messages, capturing, 992–993 OnDraw( ) function, 949–950, 956, 990 pen widths, 1070 printing documents, 1140–1143 multipage printing, 1143–1155 scalable mapping modes, 1078–1080 serialization, 1123–1124 applying, 1131–1138 documents, 1124–1131 shapes deleting, 1022 moving, 1022 sketch document, 1009–1014 sketches, storing, 1009 spin button control, 1072–1078 status bars, 1089–1092 toolbar buttons, 924–928 user interface, message handlers to update, 920–924 views scrolling, 1016–1020 updating multiple, 1014–1016 sketches printing, 1142–1143 PrintDocument component, 1171 recording saved state, 1164–1167 retrieving from fi les, 1169–1170 saving, 1167–1169 serialization, 1160–1171 slashes (/ /), 41 societal classes, 364
1225
Solution Explorer window – strcpy_s( ) function
Solution Explorer window, 11 Class View tab, 17 projects, 17 Property Manager tab, 17 Resource View tab, 17 solutions, building, 18–19 Sort( ) function, 222 sort( ) function, 522, 650, 668–669 sorting arrays associated, 223–224 one-dimensional, 222–224 vector elements, 668–669 words from text, 528–533 source code, Win32, 17–18 source fi les, adding to projects, 39 spaces, removing from strings, 344 spin button controls, 1072–1073 controls tab sequence, 1074 creating, 1073–1074 displaying, 1077–1078 splice( ) function, 680 sqrt( ) function, 980–981 stack containers, 694–697 stack header, 648 stack container, 648 Standard C++ Library, 10 standard library, std, 43 StartsWith( ) function, 240–243 StartTimer( ) function, 860 statement blocks, 47 statements assignment statements, 45 break, 136, 148 continue, 146–148 critical sections, 864–865 goto, 139 if, 123–124, 304 extended, 126–127 nested, 124–128 if-else, 128–130 output, 45 program statements, 44–46 return, 128, 255–256 switch, 135–138 static constructors, 431, 603 controls, 1060 data members, 397–400
1226
function members, 401 global variables, DLLs, 1182 properties, 429 variables, 96, 283–285 static keyword, 429 static_cast keyword, 79 static_cast operator, 590 status bars frames, 1089–1093 Mandelbrot set, 861 panes, 1090–1091 updating, 1091–1093 std, 43 stdin, 510 stdout, 510 stepping over in debugging, 777–780 STL/CLR library, 736 containers, 737 associative containers, 745–752 sequence containers, 737–745 STL (standard template library) algorithms, 649–650 containers, 646–647 array containers, 697–701 associative containers, 701–715 double-ended queue containers, 671–675 headers, 646–647 list containers, 675–685 queue containers, 685–688 range of, 651 sequence containers, 651–652 stack containers, 694–697 function adapters, 650–651 function objects, 650 introduction, 645–646 iterators, 648–649 StopTimer( ) function, 860 storage duration, 89–96 objects class objects in vectors, 663–668 map containers, 703–704 pointers in a vector, 669–671 print data, 1146–1147 scope, 89–96 strcat( ) function, 210, 775 strcmp( ) function, 212–213 strcpy( ) function, 212, 774 strcpy_s( ) function, 212, 439, 553, 774
stream input data incorrect, debugging – term( ) function
stream input data incorrect, debugging, 758 string class, 512 string object creating, 510–512 reading text into, 510 replace( ) function, 519 stdout, 510 strings, 233–234 access, 516–520 appending, 516 blanks, eliminating, 322 characters, for each loop, 161–163 combined, 512 comparing, 239–240, 520–523 concatenation, 512–515 creating, 513–514 empty, reading, 515 handling, character arrays, 174–177 input, character arrays, 175–177 joining, 234–237, 513–514 library functions, 208–215 modifying, 237–239, 516–520 multiple, storing, 179–180 null-terminated comparing, 212–213 copying, 211–212 joining, 210–211 length, 209 searching, 213–215 PadLeft( ) function, 238 PadRight( ) function, 238 programming with, 176–177 query, 517 searching, 240–243, 523–533 sentence, 510 spaces removing, 344 trimming, 237 substrings, 516–517 extracting, 333–336, 348–349 Trim( ) function, 238 TrimEnd( ) function, 238 TrimStart( ) function, 238 strings container class, 646 strlen( ) function, 209, 439 Stroustrup, Bjarne, 364 strspn( ) function, 213–215 strstr( ) function, 214 struct type, 354, 406 C versus C++, 363
defi ning, 354–355 global scope, 358 incrementing, 359 initializing, 355 IntelliSense and, 359–360 members, accessing, 355–359, 362 pointers, 361–362 RECT, 360 semicolon, 354 structures, unions in, 446 subscript ([ ]) operator, 516 substr( ) function, 516, 532 Substring( ) function, 349 substrings, 516–517 extracting, 333–336, 348–349 Sum( ) function, 621 sum( ) function, 276 sumarray( ) function, 301 swap( ) function, 518, 662 swapping, 523 switch statement, 135–138 synonyms for data types, 59 syntactic errors, 757 System namespace, 105
T tab (\t), 64–65 task_group object, 869–873 tasks, 869–873 template keyword, 314, 479 templates class templates, 477–478 combinable, 867–869 defi ning, 478–481 instances, 483 member functions, 479–481 multiple parameters, 483–485 object creation, 481–483 documents, MFC, 878–879 function objects, 486, 723–724 function templates, 314–316 defi ning, 479 lambda expressions, 730–734 PPL, 844 program size and, 316 temporary elements, 971–972 term( ) function, 321, 324 value returned, 325–326, 346–347 1227
terminology – unions
terminology, C++, 365 testing for empty vectors, 656 dialogs, 1063 extended classes, 780–783 key presses, 160 text alignment, 1153 drawing, 1113–1115 fonts, selecting, 1115–1116 rendering, 932 sorting words from, 528–533 text dialog, 1117 text elements CLR Sketcher, 1112–1120 creating, 1101–1102, 1119–1120 defi ning, 1100 text member, 476 TextBox control, 1118–1119 TextElement, 1116–1117 TextOut( ) function, 1103, 1153 this pointer, 390–392 ThisClass class, 618–621 throw exception, C++/CLI, 336 throwing exceptions, 304–306 tilde (~), 436 timer high-resolution, 860 Mandelbrot set, 859 defi ning, 859–860 using, 862–864 _tmain( ) function, 18 Toggle Guides button, 1062 toolbars, 810 adding, 938–942 buttons, 924–928 opening fi les, 1171 properties, editing, 925–926 saving fi les, 1171 tooltips, 927–928 checkmarks, 12 dockable, 12–13 list, 12 options, 12 tooltips, 927–928 ToString( ) function, 237, 410–411, 423, 534 Trace class, 793–803 assertions, 799 output
1228
controlling, 797–799 destination, 795–796 generation, 794–795 indenting, 796 using, 800–803 trace output in Windows Forms applications, 803–804 tracepoints, setting, 763 TraceSwitch reference class objects, 797 TraceTest class, 802 tracking handles, creating, 216–217 tracking references, 244 transform( ) function, 726–728 TranslateTransform( ) function, 1040–1041 treble( ) function, 278–279 TriggerEvents( ) function, 622 Trim( ) function, 237–238 TrimEnd( ) function, 238 TrimStart( ) function, 238 truth tables, 82 try block, 307 memory allocation, 309 nested, 306–307 try_lock( ) function, 865–867 two-dimensional arrays, C++, 178 type arguments, 693 explicit, 339 type casting, 78–79 old-style casting, 80–81 type conversion, 71, 78–79 assignments and, 79 explicit type conversion, 71, 79–80 implicit type conversion, 78 typedef keyword, 59 typeid operator, 81 typeinfo header, #include directive, 318 typename keyword, 314–315, 338, 628
U unary minus, 67 unary operators, 82 unconditional branching, 139 unhandled exceptions, 758 Unicode libraries, 21 Unindent( ) function, 796 union keyword, 444–445 UnionRect( ) function, 1146 unions
unique( ) function – vector containers
anonymous, 446 classes and, 446 defi ning, 444–445 members, referencing, 445 structures and, 446 unique( ) function, 680 unlock( ) function, 865–867 unsigned char data type, 57 unsigned int data type, 57 unsigned long data type, 58 unsigned long long data type, 58 unsigned short data type, 57 update handlers, 923–924 UpdateAllViews( ) function, 1014, 1015 UPDATE_COMMAND_UI messages, 914 user-defi ned types, 454 user interface, message handlers and, 920–924 using declarations, 43, 97, 258 using directives, 97 using namespace statement, 26
V value class types, 407–412 operators, overloading, 534–539 values boxing/unboxing, 594–595 comparing, 121–123 converting type, 71 lvalues, 88–89, 470 declaring, 207–208 initializing, 207–208 named objects, 472–477 members, incrementing, 355–356 parameters, assigning default, 378–380 passing by to functions, 260–261 returning from functions, pointers, 277–280 rvalues, 88–89, 271–273 applying, 470–472 defi ning, 208 initializing, 208 term( ) function, 325–326, 346–347 variables assigning new, 45 changing, 767 initializing, 51–52 specific sets, 59–61
values vector, 654 variables addresses, 182 automatic, 89–92 bool, 523 Boolean, 56 capturing specific (lambda expressions), 730 const, 70 count, 533 data exchange, 1076 declaring, 44–45, 50–51 positioning declarations, 92 double, 326 dval, 445 fundamental data types, 363 global, 92–96 static, DLLs, 1182 incrementing, 73 index, 325 initializing, 51–52 hexadecimal constants, 54 new operator, 202 integer, 52–53 MAX, 330 memory, sharing, 444–446 modifying, 73–74 naming, 49–50 newline, 66 proverb, 162 result, 255 scope, 255 static, 96, 283–285 values assigning new, 45 changing, 767 debugging and, 765–766 specific sets, 59–61 viewing in Edit window, 767 .vbx extension (Visual Basic Extension), 1179 vector containers capacity, 655–660 class objects, storing, 663–668 creating, 652–655 elements accessing, 660–661 inserting/deleting, 661–663 sorting, 668–669 empty, testing for, 656
1229
vector containers – Windows
vector containers (continued) handle storage, 737–740 pointers, storing, 669–671 size, 655–660 sizing, 656 vector header fi le, 646 vectors, values, 654 vector container, 646 versions of C++, 5 Viewport Extent parameter, 1079 Viewport Origin parameter, 1079 views MFC, 877 Sketcher scrolling, 1016–1021 updating, 1014–1016 virtual destructors, 586–590 virtual functions, 572–576 calling, 576 pure virtual functions, 580–581 references, 578–580 virtual keyword, 574–576 virtual machine, 2 visibility specifiers for classes and interfaces, 606–607 void keyword, 254 Volume( ) function, 371, 452, 574 volume debugging, 770 VOLUMEDEBUG, 770
W wait( ) function, 870 wchar_t data type, 54, 57 wcslen( ) function, 209 what( ) function, 309 where keyword, 338 while loop, 150–152, 307, 322
whitespace, 46–47 wide character type, 54 Win32 console application, 14–17 debugging application, 19–20 executing program, 20 source code, 17–18 Win32 Application Wizard dialog box, 15 Win32 Debug, 760 Window Extent parameter, 1079
1230
Window Origin parameter, 1079 WindowProc( ) function, 811, 814, 828, 829
complete function, 831–832 switch statement, 829 Windows applications creating MFC application, 28–30 MFC classes, 879–880 operating system and, 810 data types, 812–813 forms, 840–841 GDI (Graphical Device Interface), 946–948 mapping modes, 947–948 messages, 811, 822–826, 907 client area, drawing, 829–831 decoding, 829 functions, 827–832 message loop, 822–824 multitasking, 824–826 non-queued, 822 queued, 822 programming, 5, 7–9 event-driven programs, 811 notation, 813–814 overview, 808 prefi xes, 813–814 program organization, 834–835 program structure, 814–833 program window creation, 820–821 program window initialization, 821–822 program window specification, 817–820 sample program, 833 windows, 808–810 borders, 809 client area, 809, 946 close button, 809 DestroyWindow( ) function, 1067 docking, 11 drawing in, 945–946 client area, 967–968 Editor window, 11 elements, 809 floating, 11 maximize button, 809 MDI child window, 809 MDI parent window, 809 minimize button, 809 program window
Windows API – wstring object
creating, 820–821 initializing, 821–822 specifying, 817–820 Solution Explorer, 11 title bar, 809 toolbar, 810 unlocking from position, 11 Windows API, 808, 811–812 BeginPaint( ) function, 830 CreateWindow( ) function, 820 GetClientRect( ), 830 RegisterClass( ) function, 820 RegisterClassEx( ) function, 820 Windows Forms applications C++/CLI, 929–932 creating, 31–32 GUI, 808 trace output, 803–804 C++/CLI, 928–929 properties, modifying, 931–932 WinMain( ) function, 814–827 arguments, 816 complete function, 826–827 messages, 822–826 functions, 827–832 parameters, 816 program windows creating, 820–821
initializing, 821–822 specifying, 817–820 WindowProc( ) function, 828 Wizards Add Member Function, 497–498 Add Member Variable Wizard, 1074–1075 Application Wizards, 812 Class Wizard, 904, 970–971, 1063–1064 Event Handler, 915–916 MFC Class Wizard, 1091 wmain( ) function, 48 WM_CONTEXTMENU message, 1024 WM_LBUTTONDOWN message, 963 WM_LBUTTONUP message, 963 WM_LBUTTONUP message, 990 WM_MOUSEMOVE message, 963, 1033–1035 WM_PAINT message, 945–946 WNDCLASSEX, 817–818 WndProc( ) function, 811 WORD data type, 813 words, sorting from text, 528–533 words array, 531 wrapping lambda expressions, 734–736 Write( ) function, 105, 343, 794 WriteIf( ) function, 794 WriteLine( ) function, 105, 411, 535, 794 WriteLineIf( ) function, 794 writing C++ applications, 3–4 wstring object, 511
1231
Related Wrox Books Beginning ASP.NET 4: in C# and VB ISBN: 978-0-470-50221-1 This introductory book offers helpful examples in a step-by-step format and has code examples written in both C# and Visual Basic. With this book, a web site example takes you through the processes of building basic ASP.NET web pages, adding features with pre-built server controls, designing consistent pages, displaying data, and more.
Beginning Visual C# 2010 ISBN: 978-0-470-50226-6 Using this book, you will first cover the fundamentals such as variables, flow control, and object-oriented programming and gradually build your skills for web and Windows programming, Windows forms, and data access. Step-by-step directions walk you through processes and invite you to “Try it Out” at every stage. By the end, you’ll be able to write useful programming code following the steps you’ve learned in this thorough, practical book. If you’ve always wanted to master Visual C# programming, this book is the perfect one-stop resource.
Professional ASP.NET 4: in C# and VB ISBN: 978-0-470-50220-4 Written by three highly recognized and regarded ASP.NET experts, this book provides all-encompassing coverage on ASP.NET 4 and offers a unique approach of featuring examples in both C# and VB. After a fast-paced refresher on essentials such as server controls, the book delves into expert coverage of all the latest capabilities of ASP.NET 4. You’ll learn site navigation, personalization, membership, role management, security, and more.
Professional C++ ISBN: 978-0-7645-7484-9 This code-intensive, practical guide teaches all facets of C++ development, including effective application design, testing, and debugging. You’ll learn simple, powerful techniques used by C++ professionals, little-known features that will make your life easier, and reusable coding patterns that will bring your basic C++ skills to the professional level.
Professional C# 4 and .NET 4 ISBN: 978-0-470-50225-9 After a quick refresher on C# basics, the author dream team moves on to provide you with details of language and framework features including LINQ, LINQ to SQL, LINQ to XML, WCF, WPF, Workflow, and Generics. Coverage also spans ASP.NET programming with C#, working in Visual Studio 2010 with C#, and more. With this book, you’ll quickly get up to date on all the newest capabilities of C# 4.
Professional Visual Basic 2010 and .NET 4 ISBN: 978-0-470-50224-2 If you’ve already covered the basics and want to dive deep into VB and .NET topics that professional programmers use most, this is your guide. You’ll explore all the new features of Visual Basic 2010 as well as all the essential functions that you need, including .NET features such as LINQ to SQL, LINQ to XML, WCF, and more. Plus, you’ll examine exception handling and debugging, Visual Studio features, and ASP.NET web programming.
Professional Visual Studio 2010 ISBN: 978-0-470-54865-3 Written by an author team of veteran programmers and developers, this book gets you quickly up to speed on what you can expect from Visual Studio 2010. Packed with helpful examples, this comprehensive guide examines the features of Visual Studio 2010 and walks you through every facet of the Integrated Development Environment (IDE), from common tasks and functions to its powerful tools.
Visual Basic 2010 Programmer’s Reference ISBN: 978-0-470-49983-2 Visual Basic 2010 Programmer’s Reference is a language tutorial and a reference guide to the 2010 release of Visual Basic. The tutorial provides basic material suitable for beginners but also includes in-depth content for more advanced developers
Build real-world applications as you dive into C++ development By following author Ivor Horton’s accessible tutorial approach and detailed examples you can quickly become an effective C++ programmer. Thoroughly updated for the 2010 release, this book introduces you to the latest development environment and teaches you how to build real-world applications using Visual C++. With this book by your side, you are well on your way to writing applications in both versions of C++ and becoming a successful C++ programmer.
wrox.com
Ivor Horton’s Beginning Visual C++ 2010: •
Teaches the essentials of C++ programming using both of the C++ language technologies supported by Visual C++ 2010
•
Shares techniques for finding errors in C++ programs and explains general debugging principles
•
Discusses the structure and essential elements that are present in every Windows® application
•
Demonstrates how to develop native Windows applications using the Microsoft Foundation Classes
•
Guides you through designing and creating substantial Windows applications in both C++ and C++/CLI
•
Features numerous working examples and exercises that help build programming skills
Ivor Horton is one of the preeminent authors of tutorials on the Java, C and C++ programming languages. He is widely known for his unique tutorial style, which is readily accessible to both novice and experienced programmers. Horton is also a systems consultant in private practice. He previously taught programming for more than 25 years. Wrox Beginning guides are crafted to make learning programming languages and technologies easier than you think, providing a structured, tutorial format that will guide you through all the techniques involved.
Programming / C++ Development
$54.99 USA $65.99 CAN
Programmer Forums Join our Programmer to Programmer forums to ask and answer programming questions about this book, join discussions on the hottest topics in the industry, and connect with fellow programmers from around the world.
Code Downloads Take advantage of free code samples from this book, as well as code samples from hundreds of other books, all ready to use.
Read More Find articles, ebooks, sample chapters, and tables of contents for hundreds of books, and more reference resources on programming topics that matter to you.