00 9721 CD-FM
11/13/00
12:11 PM
Page i
C++Builder™ 5 Developer’s Guide Jarrod Hollingworth Dan Butterfield Bob Swart Jamie Allsop
201 West 103rd St., Indianapolis, Indiana, 46290 USA
00 9721 CD-FM
11/13/00
12:11 PM
Page ii
C++Builder™ 5 Developer’s Guide
ASSOCIATE PUBLISHER
Copyright
ACQUISITIONS EDITOR
©
2001 by Sams Publishing
All rights reserved. No part of this book shall be reproduced, stored in a retrieval system, or transmitted by any means, electronic, mechanical, photocopying, recording, or otherwise, without written permission from the publisher. No patent liability is assumed with respect to the use of the information contained herein. Although every precaution has been taken in the preparation of this book, the publisher and author assume no responsibility for errors or omissions. Nor is any liability assumed for damages resulting from the use of the information contained herein. International Standard Book Number: 0-672-31972-1 Library of Congress Catalog Card Number: 00-102818 Printed in the United States of America First Printing: November 2000 03
02
01
00
Michael Stephens
Carol Ackerman
DEVELOPMENT EDITOR Robyn Thomas
MANAGING EDITOR Matt Purcell
PROJECT EDITOR Andrew Beaster
INDEXERS Sandy Henselmeier Eric Schroeder
PROOFREADERS 4
3
2
1
Candice Hightower Jessica McCarty
Trademarks
TECHNICAL EDITORS
All terms mentioned in this book that are known to be trademarks or service marks have been appropriately capitalized. Sams Publishing cannot attest to the accuracy of this information. Use of a term in this book should not be regarded as affecting the validity of any trademark or service mark.
Peter Nunn Paul Strickland John Thomas Eamonn Wallace
Warning and Disclaimer
TEAM COORDINATOR
Every effort has been made to make this book as complete and as accurate as possible, but no warranty or fitness is implied. The information provided is on an “as is” basis. The authors and the publisher shall have neither liability nor responsibility to any person or entity with respect to any loss or damages arising from the information contained in this book or from the use of the CD or programs accompanying it.
Pamalee Nelson
MEDIA DEVELOPERS William Eland Matt Bates
INTERIOR DESIGNER Anne Jones
COVER DESIGNER Anne Jones
LAYOUT TECHNICIANS Ayanna Lacey Heather Hiatt Miller Stacey Richwine-DeRome
00 9721 CD-FM
11/13/00
12:11 PM
Page iii
Overview Introduction
1
PART I
C++Builder 5 Essentials
1
Introduction to C++Builder
2
C++Builder Projects and More on the IDE
3
Programming in C++Builder
4
Advanced Programming with C++Builder
5
User Interface Principles and Techniques
6
Compiling and Optimizing Your Application
7
Debugging Your Application
8
Using VCL Components
9
Creating Custom Components
9
95
335
389
437 499
Creating Property and Component Editors
11
More Custom Component Techniques
577
701
Communications, Database, and Web Programming
12
Communications Programming
13
Web Server Programming
14
Database Programming
PART III
147 225
10
PART II
53
775
813
861
Interfaces and Distributed Computing
15
DLLs and Plug-Ins
925
16
COM Programming
17
Going Distributed: DCOM
18
One Step Ahead: COM+
19
Multi-Tier Distributed Applications with MIDAS 3
20
Distributed Applications with CORBA
21
Microsoft Office Integration 1195
22
Using ActiveX Techniques
967 1023 1057
1233
1173
1121
00 9721 CD-FM
11/13/00
12:11 PM
PART IV
Page iv
Advanced Topics
23
Data Presentation with C++Builder
24
Using the Win32 API
25
Multimedia Techniques
26
Advanced Graphics with DirectX and OpenGL
PART V
1279
1351 1457
C++Builder Application Deployment
27
Creating Help Files and Documentation
28
Software Distribution 1593
29
Software Installation and Updates
PART VI
Knowledge Base
30
Tips, Tricks, and How Tos 1675
31
Real-World Examples 1809
PART VII A
Appendix Information Sources 1825
1625
1561
1505
00 9721 CD-FM
11/13/00
12:11 PM
Page v
CONTENTS
Contents Introduction
Part I 1
1
C++Builder 5 Essentials Introduction to C++Builder 9 C++Builder Basics ................................................................................10 Hello World! A Basic Start ..............................................................10 The VCL, Forms, and Components..................................................12 The Component Palette ....................................................................14 Your First Real Program ..................................................................17 How to Get Around ..........................................................................23 What’s New in C++Builder 5 ................................................................24 Web Programming............................................................................25 Distributed Applications ..................................................................26 Team Development ..........................................................................26 Application Localization ..................................................................26 Debugging ........................................................................................27 Database Application Development ................................................27 Developer Productivity ....................................................................27 Companion Tools CD-ROM ............................................................28 Upgrading and Compatibility Issues ....................................................28 Upgrading C++Builder from an Earlier Version ..............................28 Using Existing Projects in C++Builder 5 ........................................29 Creating Projects Compatible with Previous Versions of C++Builder ....................................................................................29 Solving Other Project Upgrading Issues ..........................................30 Migration from Delphi ..........................................................................30 Comments ........................................................................................31 Variables ..........................................................................................31 Constants ..........................................................................................32 Operators ..........................................................................................33 Controlling Program Flow................................................................36 Functions and Procedures ................................................................38 Classes ..............................................................................................40 Preprocessor Directives ....................................................................42 Types of Files ..................................................................................43 Advantages and Disadvantages of C++Builder 5..................................44 Visual Reality: True Rapid Application Development ....................44 Keeping Up with the Joneses: The C++ Standard ..........................46 Choosing the Right Development Environment ..............................47 C++Builder Advantages and Disadvantages Conclusion ................47
v
00 9721 CD-FM
vi
11/13/00
12:11 PM
Page vi
C++BUILDER 5 DEVELOPER’S GUIDE Preparation for Kylix ............................................................................48 Similarities Between Kylix and C++Builder ..................................48 Differences Between Kylix and C++Builder ..................................49 Porting C++Builder Projects to Kylix..............................................50 So When?..........................................................................................50 Summary ................................................................................................51 2
C++Builder Projects and More on the IDE 53 Understanding C++Builder Projects......................................................54 Files Used in C++Builder Projects ..................................................54 Project Manager................................................................................57 Using the Object Repository..................................................................58 Adding Items to the Object Repository............................................58 Using Items in the Object Repository ..............................................63 Sharing Items Within a Project ........................................................63 Customizing the Object Repository ................................................64 Creating and Adding a Wizard to the Object Repository ................64 Understanding and Using Packages ......................................................67 Considerations when Using Packages..............................................70 The C++Builder Runtime Packages ................................................72 Using Tdump ....................................................................................75 Introducing New IDE Features in C++Builder 5 ..................................76 Property Categories in the Object Inspector ....................................76 Images in Drop-Down Lists in the Object Inspector ......................78 The XML Project File Format..........................................................80 Forms—Save as Text ........................................................................82 The Node-Level Options ..................................................................83 The New To-Do List ........................................................................87 The Console Wizard ........................................................................90 Summary ................................................................................................93
3
Programming in C++Builder 95 Coding Style to Improve Readability ....................................................96 The Use of Short and Simple Code ................................................97 The Use of Code Layout ................................................................98 The Use of Sensible Naming ........................................................104 The Use of Code Constructs ........................................................111 The Use of Comments ..................................................................112 A Final Note on Improving the Readability of Code ....................117 Better Programming Practices in C++Builder ....................................117 Use a String Class Instead of char* ..............................................117 Understand References and Use Them Where Appropriate ..........118 Avoid Using Global Variables ........................................................121
00 9721 CD-FM
11/13/00
12:11 PM
Page vii
CONTENTS Understand and Use const in Your Code ......................................127 Be Familiar with the Principles of Exceptions ..............................129 Use new and delete to Manage Memory ......................................134 Understand and Use C++-Style Casts ............................................138 Know When to Use the Preprocessor ............................................141 Learn About and Use the C++ Standard Library ..........................143 Further Reading ..................................................................................144 Summary ..............................................................................................146 4
Advanced Programming with C++Builder 147 Introducing the Standard C++ Library and Templates........................148 Understanding C++ Templates ......................................................148 Exploring the Standard C++ Library Features ..............................152 Coming to Grips with Containers and Iterators ............................153 Using the Standard Algorithms ......................................................161 Closing Thoughts on the SCL ........................................................162 Using Smart Pointers and Strong Containers ......................................163 The Heap Versus the Stack ............................................................163 Pointers ..........................................................................................163 A Strong Container ........................................................................168 Pitfalls ............................................................................................170 Smart Pointers and Strong Containers Summary ..........................170 Implementing an Advanced Exception Handler..................................171 Reviewing the Strategy ..................................................................171 Reviewing the Advantages ............................................................172 Replacing the Compiler’s Default Exception Handler ..................172 Adding Project-Specific Information to the Class ........................174 The Exception Handler’s Source Code ..........................................174 Advanced Exception Handler Summary ........................................193 Creating Multithreaded Applications ..................................................193 Understanding Multitasking ..........................................................194 Understanding Multithreading........................................................194 Creating a Thread Using API Calls................................................195 Understanding the TThread Object ................................................199 Understanding the Main VCL Thread............................................205 Establishing Priorities ....................................................................208 Timing Threads ..............................................................................211 Synchronizing Threads ..................................................................213 Introducing Design Patterns ................................................................218 Understanding the Recurring Nature of Patterns ..........................218 Recurring Patterns in Software Design ..........................................218 Design Patterns as a Vocabulary ....................................................219 Design Pattern Format....................................................................219
vii
00 9721 CD-FM
viii
11/13/00
12:11 PM
Page viii
C++BUILDER 5 DEVELOPER’S GUIDE Design Pattern Classification ........................................................220 Parting Thoughts About Design Patterns ......................................222 Summary ..............................................................................................222 5
User Interface Principles and Techniques 225 User Interface Guidelines ....................................................................226 The Example Projects Used in This Chapter ......................................229 Introducing the MiniCalculator Project..........................................230 Enhancing Usability by Providing Feedback to the User ..................231 Using TProgressBar and TCGauge ..................................................232 Using the Cursor ............................................................................233 Using TStatusBar ..........................................................................234 Using Hints ....................................................................................243 Enhancing Usability Through Input Focus Control ............................264 Responding to Input ......................................................................264 Moving Input Focus ......................................................................270 Enhancing Usability Through Appearance..........................................272 Using Symbols on Their Own with Buttons ..................................274 Using Symbols in Addition to Text ................................................275 Using Color to Provide Visual Clues ............................................280 Using Shaped Controls ..................................................................280 Enhancing Usability by Allowing Customization of the User Interface ....................................................................................283 Docking ..........................................................................................284 Resizing ..........................................................................................289 Using TControlBar ........................................................................299 Controlling Visibility ......................................................................313 Customizing the Client Area of an MDI Parent Form ..................317 Enhancing Usability by Remembering the User’s Preferences ..........317 Coping with Differing Screen Conditions ..........................................328 Coping with Different Screen Resolutions ....................................328 Coping with Different Font Sizes ..................................................328 Coping with Different Color Depths..............................................328 Coping with Complexity in the Implementation of the User Interface ....................................................................................329 Using Action Lists ..........................................................................329 Sharing Event Handlers..................................................................331 Summary ..............................................................................................334
6
Compiling and Optimizing Your Application 335 Understanding How the Compiler Works............................................336 Speeding Up Compile Times ..............................................................338 Precompiled Headers......................................................................338 Other Techniques for Speeding Up Compile Times ......................340
00 9721 CD-FM
11/13/00
12:11 PM
Page ix
CONTENTS Exploring the C++Builder 5 Compiler and Linker Enhancements ....341 Background Compilation................................................................342 Miscellaneous Compiler Enhancements ........................................343 New Linker Enhancements ............................................................343 Optimizing: An Introduction................................................................344 Optimizing for Execution Speed ........................................................346 Crozzle Solver Application Example ............................................347 Exponential Timings ......................................................................349 Project Options for Execution Speed ............................................351 Detecting Bottlenecks ....................................................................352 Optimizing the Design and Algorithms..........................................358 Exploring Techniques for Streamlining Code ................................365 Techniques for Streamlining Data..................................................379 Hand-Tuning Assembly Code ........................................................381 Using External Optimization..........................................................384 Execution Speed Optimization Summary ......................................385 Optimizing Other Aspects of Your Application ..................................385 Optimizing Program Size ..............................................................385 Final Optimization Aspects ............................................................386 Summary ..............................................................................................387 7
Debugging Your Application 389 Debugging Overview ..........................................................................390 Project Guidelines ..........................................................................391 Programming Guidelines................................................................392 The Debugging Task ......................................................................393 Basic Debugging Techniques ..............................................................394 Outputting Debug Information ......................................................395 Using Assertions ............................................................................400 Implementing a Global Exception Handler....................................402 Other Basic Debugging Issues ......................................................403 Using the C++Builder Interactive Debugger ......................................404 Advanced Breakpoints....................................................................405 New Breakpoint Features in C++Builder 5....................................409 C++Builder Debugging Views ......................................................409 Watches, Evaluating, and Modifying ............................................415 The Debug Inspector ......................................................................416 CodeGuard ..........................................................................................417 Enabling and Configuring CodeGuard ..........................................418 Using CodeGuard ..........................................................................419 Examining CodeGuard Errors and Their Causes ..........................421
ix
00 9721 CD-FM
x
11/13/00
12:11 PM
Page x
C++BUILDER 5 DEVELOPER’S GUIDE Advanced Debugging ..........................................................................426 Locating the Source of Access Violations......................................426 Attaching to a Running Process ....................................................428 Using Just-In-Time Debugging ......................................................428 Remote Debugging ........................................................................429 Debugging DLLs ............................................................................431 Looking at Other Debugging Tools................................................431 Testing..................................................................................................433 Testing Stages and Techniques ......................................................433 Testing Tips ....................................................................................435 Summary ..............................................................................................436 8
Using VCL Components 437 VCL Overview ....................................................................................438 It All Starts at TObject ..................................................................439 Building on Existing Objects ........................................................440 Using the VCL................................................................................442 The C++ Extensions ......................................................................445 The Streaming Mechanism ..................................................................452 Advanced Streaming Requirements ..............................................453 Streaming Unpublished Properties ................................................454 Common Control Updates ..................................................................458 Common Control Dynamic Link Library ......................................459 C++Builder Common Control Updates ........................................461 Miscellaneous VCL Enhancements ....................................................464 New Help Hint and Menu Features................................................464 Registry Access ..............................................................................465 VCL Documentation Enhancements ..............................................465 New TApplicationEvents Component ..........................................466 TIcon Enhancements ......................................................................466 Other Miscellaneous VCL Enhancements......................................466 Extending the VCL—More than Just a TStringList ..........................466 Using TStringList as a Container ................................................467 Storing Non-VCL Objects..............................................................467 Linking Strings to Objects of the Same Type ................................469 Creating a Chain of Events ............................................................479 Sorting the Lists..............................................................................480 Making Improvements....................................................................481 Advanced Custom Draw Events ..........................................................482 The TTreeView Component ............................................................483 The TListView Component ............................................................483 The TToolBar Component ..............................................................484 Advanced Custom Draw Events Example ....................................484
00 9721 CD-FM
11/13/00
12:11 PM
Page xi
CONTENTS Control Panel Applet Wizard Components..........................................484 The Basics of an Applet ................................................................484 Making Use of Third-Party Components ............................................495 Third-Party Component Advantages and Disadvantages ..............495 Where to Look for More C++Builder Resources ..........................496 Summary ..............................................................................................497 9
Creating Custom Components 499 Why Create Custom Components ......................................................500 Understanding Component Writing ....................................................501 Why Build Upon an Existing Component?....................................501 Designing Custom Components ....................................................503 Using the VCL Chart......................................................................504 Writing Non-Visual Components ........................................................504 Properties ........................................................................................504 Events ............................................................................................514 Methods ..........................................................................................517 Creating Component Exceptions....................................................519 The namespace ................................................................................521 Responding to Messages ................................................................522 Designtime Versus Runtime ..........................................................524 Linking Components ......................................................................526 Linking Events Between Components ..........................................529 Writing Visual Components ................................................................532 Where to Begin ..............................................................................532 TCanvas ..........................................................................................532 Using Graphics in Components ....................................................534 Responding to Mouse Messages ....................................................538 Putting It All Together ....................................................................539 Modifying Windowed Components ..............................................547 Creating Custom Data-Aware Components ........................................561 Making the Control Read-Only......................................................561 Establishing the Link......................................................................562 Using the OnDataChange Event ......................................................565 Changing to a Data-Editing Control ..............................................567 Working Toward a Dataset Update ................................................568 Adding a Final Message ................................................................571 Registering Components......................................................................571 Summary ..............................................................................................574
10
Creating Property and Component Editors 577 Creating Custom Property Editors ......................................................580 The GetAttributes() Method ......................................................593 The GetValue() Method ................................................................593
xi
00 9721 CD-FM
xii
11/13/00
12:11 PM
Page xii
C++BUILDER 5 DEVELOPER’S GUIDE The SetValue() Method ................................................................594 The Edit() Method ........................................................................595 The GetValues() Method ..............................................................600 Using the TPropertyEditor Properties ..........................................600 Considerations when Choosing a Suitable Property Editor ..........601 Properties and Exceptions....................................................................602 Registering Custom Property Editors ..................................................604 Obtaining a TTypeInfo* (PTypeInfo) from an Existing Property and Class for a Non-VCL Type ....................................606 Obtaining a TTypeInfo* (PTypeInfo) for a Non-VCL Type by Manual Creation ............................................................614 How to Obtain a TTypeInfo* for a Non-VCL Type ......................615 Rules for Overriding Property Editors ..........................................616 Using Images in Property Editors........................................................617 The ListMeasureWidth() Method..................................................622 The ListMeasureHeight() Method................................................623 The ListDrawValue() Method ......................................................624 The PropDrawValue() Method ......................................................629 The PropDrawName() Method ........................................................630 Installing Editor-Only Packages ..........................................................632 Using Linked Image Lists in Property Editors....................................634 The GetAttributes() Method ......................................................640 The GetComponentImageList() Method ........................................640 The GetValues() Method ..............................................................641 The ListMeasureWidth() and ListMeasureHeight() Methods ......641 The ListDrawValue() Method ......................................................643 The PropDrawValue() Method ......................................................645 Other Considerations when Rendering Images..............................647 Linking to a Parent’s TCustomImageList ......................................647 A Generalized Solution for ImageIndex Properties ......................652 Creating Custom Component Editors..................................................658 The Edit() Method ........................................................................664 The EditProperty() Method ........................................................668 The GetVerbCount() Method ........................................................670 The GetVerb() Method ..................................................................670 The PrepareItem() Method ..........................................................670 The ExecuteVerb() Method ..........................................................677 The Copy() Method ........................................................................678 Registering Component Editors ..........................................................680 Using Predefined Images in Custom Property and Component Editors............................................................................680 Adding Resource Files to Packages ..............................................682 Using Resources in Property and Component Editors ..................682
00 9721 CD-FM
11/13/00
12:11 PM
Page xiii
CONTENTS Registering Property Categories in Custom Components ..................687 Understanding Categories and Category Creation ........................688 Registering a Property or Properties in a Category........................690 Summary ..............................................................................................698 11
Part II 12
More Custom Component Techniques 701 Miscellaneous Considerations for Custom Components ....................702 Displaying a Class Property’s Published Properties in the Object Inspector ....................................................................702 Using Namespaces in Event Parameter Lists ................................703 Considerations when Determining an Event’s Parameter List ......704 Overriding DYNAMIC Functions........................................................709 Handling Messages in Custom Components..................................711 Using Windows Callback Functions in Components ....................726 Considerations when Choosing Fundamental Property Types ......736 Allowing for Designtime and Runtime Component Use ..............742 Frames..................................................................................................744 What Exactly Is a Frame? ..............................................................744 The TCustomFrame Class ................................................................744 Working with Frames at Designtime..............................................746 Working with Frames at Runtime ..................................................747 Creating a TFrame Descendant Class..............................................747 Inheriting from a TFrame Descendant Class ..................................751 Reusing Frames ..............................................................................752 Closing Remarks on Frames ..........................................................754 Component Distribution and Related Issues ......................................754 Packaging Components ..................................................................754 Where Distributed Files Should Be Placed....................................760 Naming Packages and Package Units ............................................762 Naming Components ......................................................................764 Distributing only a Designtime-Only Package ..............................764 Distributing Components for Different Versions of C++Builder ..................................................................................767 Creating Component Palette Bitmaps ............................................770 Using Guidelines in the Design of Components for Distribution ..................................................................................771 Taking a Look at Other Distribution Issues ..................................772 Summary ..............................................................................................772
Communications, Database, and Web Programming Communications Programming 775 Serial Communication ........................................................................776 Protocol ..........................................................................................777
xiii
00 9721 CD-FM
xiv
11/13/00
12:11 PM
Page xiv
C++BUILDER 5 DEVELOPER’S GUIDE State Machines................................................................................780 Architecture ....................................................................................781 Thread Synchronization Techniques ..............................................782 Buffering ........................................................................................782 Concluding Remarks on Serial Communications ..........................783 Internet Protocols—SMTP, FTP, HTTP, POP3 ..................................784 Tour of Component Tabs ................................................................784 An Example Chat Server................................................................785 An Example Chat Client ................................................................790 An Example Email Application......................................................793 An Example HTTP Server ............................................................802 Example FTP Client Software........................................................805 Summary ..............................................................................................811 13
Web Server Programming 813 Web Modules ......................................................................................814 Web Server Application Wizard ..........................................................814 CGI ................................................................................................815 WinCGI ..........................................................................................815 ISAPI/NSAPI..................................................................................815 CGI or ISAPI? ................................................................................815 WebBroker Support Components ........................................................816 TWebDispatcher ..............................................................................817 TWebModule ....................................................................................817 TWebResponse ..................................................................................819 TWebRequest....................................................................................820 Web Servers ........................................................................................820 WebBroker Producing Components ....................................................824 TPageProducer ................................................................................824 TDataSetPageProducer ..................................................................828 TDataSetTableProducer ................................................................831 TQueryTableProducer ....................................................................833 Web Application Wizards ....................................................................837 Maintaining State ................................................................................838 Fat URLs ........................................................................................838 Cookies ..........................................................................................839 Hidden Fields..................................................................................839 Web Security........................................................................................842 Secure Sockets Layer ....................................................................843 Authorization ..................................................................................843 Securing a Web Application ..........................................................845 HTML and XML ................................................................................851 XML ..............................................................................................851
00 9721 CD-FM
11/13/00
12:11 PM
Page xv
CONTENTS InternetExpress ....................................................................................853 Customer Orders ............................................................................853 TMidasPageProducer ......................................................................853 Deployment ....................................................................................855 Final Master-Detail ........................................................................856 Web Page Design Issues ................................................................858 Summary ..............................................................................................860 14
Database Programming 861 Architecture Models for Database Applications..................................862 The Borland Database Engine........................................................862 BDE Native (Single-Tier) ..............................................................863 BDE/SQL Links (Client/Server) ....................................................864 Distributed (Multitier) ....................................................................864 Data Access Methods ....................................................................865 Native Components ........................................................................866 ODBC Using the BDE ..................................................................866 ODBC Using Native Components ................................................867 ADO (ActiveX) ..............................................................................868 Embedded SQL ..............................................................................868 Native API ......................................................................................869 Database Architectures—Conclusion ............................................869 Sources for More Information on Database Architectures ............870 Structured Query Language (SQL)......................................................871 Tables and Indices ..........................................................................871 Parameters ......................................................................................872 insert, update, delete, and select ..............................................873 Aggregate Functions ......................................................................875 More Information on SQL..............................................................875 ADO Express Components for C++Builder........................................875 ADO Versus BDE ..........................................................................876 Component Overview ....................................................................877 Database Connections ....................................................................878 Accessing Datasets ........................................................................880 Accessing a Dataset with TADOTable..............................................880 Using TADOCommand for Dataset Access..........................................884 Managing Transactions ..................................................................884 Using Component Events ..............................................................884 Creating Generic Database Applications........................................886 Performance Optimizations ............................................................887 Error Handling Issues ....................................................................888 Multitier Applications and ADO ....................................................889 ADO Express Components—Conclusion ......................................889
xv
00 9721 CD-FM
xvi
11/13/00
12:11 PM
Page xvi
C++BUILDER 5 DEVELOPER’S GUIDE Data Acquisition Architectures ............................................................889 The Basic Choices ..........................................................................890 Fetching Data from Many Sources ................................................891 Data Module Designer ........................................................................893 What Are Data Modules? ..............................................................893 Why Use a Data Module? ..............................................................893 How Do I Use a Data Module in My Applications, DLLs, and Distributed Objects?..............................................................895 What Goes in a Data Module? ......................................................896 How Can I Add Properties to My Data Module? ..........................897 How to Use the Data Module Designer ........................................897 Advanced Concepts ........................................................................900 Form Inheritance with Data Modules ............................................900 Handling Uneven Form Inheritance with Data Modules ..............901 How to Avoid Dependence on Specific User Interfaces ................902 How to Work with Application-Specific and Framework Components in Data Modules ....................................................903 Data Modules in Packages ............................................................905 Data Module Designer—Conclusions............................................905 InterBase Express ................................................................................905 Bug Tracker Database Schema ......................................................906 Database Rules ..............................................................................909 Generators, Triggers, and Stored Procedures ................................909 Bug Tracker Implementation..........................................................911 Bug Tracker Wrap Up ....................................................................920 Summary ..............................................................................................921
Part III 15
Interfaces and Distributed Computing DLLs and Plug-Ins 925 Using the DLL Wizard ........................................................................926 Writing and Using DLLs ....................................................................927 Linking DLLs Statically ................................................................928 Importing Functions from a Dynamically Linked DLL ................931 Exporting Classes ..........................................................................936 Using Packages Versus DLLs ..............................................................939 Using SDI Forms in a DLL ................................................................942 Using MDI Child Forms in DLLs and Packages ................................943 Using MDI Child Forms in DLLs..................................................943 Using MDI Child Forms in Packages ............................................948 Using Microsoft Visual C++ DLLs with C++Builder ........................951 Using C++Builder DLLs with Microsoft Visual C++ ........................952 Writing Plug-Ins ..................................................................................953
00 9721 CD-FM
11/13/00
12:11 PM
Page xvii
CONTENTS Anatomy of a Plug-In ....................................................................954 TIBCB5PlugInBase Class ................................................................959 TBCB5PluginManager ......................................................................963 Wrapping Up Plug-Ins....................................................................965 Summary ..............................................................................................965 16
COM Programming 967 Understanding COM Servers and Clients ..........................................968 Outgoing Interfaces and Event Sinks Revisited ..................................969 Writing the COM Server ....................................................................970 Choosing the Server Type ..............................................................971 Choosing a Threading Model ........................................................972 Creating the Server ........................................................................973 Adding a COM Object ..................................................................974 Dissecting the Generated Code ......................................................978 Writing Method Bodies ..................................................................983 Adding Better Error Support ..........................................................985 Implementing a Method That Fires an Event ................................988 Implementing a Custom Interface ..................................................989 Firing Custom Events ....................................................................993 Writing the Proxy/Stub DLL ..............................................................999 Writing the COM Client ....................................................................1006 Importing the Type Library ..........................................................1007 Looking at the Generated C++ Constructions..............................1008 Creating and Using the COM Server Object ..............................1011 Catching Dispinterface-Based Events ..........................................1013 Querying for the Custom Interface ..............................................1016 Writing the Custom Interface-Based Event Sink ........................1017 Recommended Readings....................................................................1021 Summary ............................................................................................1022
17
Going Distributed: DCOM 1023 What Is DCOM? ................................................................................1024 Windows OS Family and DCOM ................................................1025 The DCOMCnfg Utility Tool ............................................................1025 Global Security Settings ..............................................................1026 Per-Server Security Settings ........................................................1028 Field Testing DCOM ........................................................................1031 Creating the Server Application ..................................................1031 Creating the Client Application....................................................1033 Configuring Launch and Access Permissions ..............................1036 Configuring Identity ....................................................................1037 Running the Example ..................................................................1037
xvii
00 9721 CD-FM
xviii
11/13/00
12:11 PM
Page xviii
C++BUILDER 5 DEVELOPER’S GUIDE Programming Security ......................................................................1038 CoInitializeSecurity Function Parameters ..............................1038 Using CoInitializeSecurity ......................................................1040 Understanding DLL Clients and Security ....................................1042 Implementing Programmatic Access Control ..............................1042 Implementing Interface-Wide Security ........................................1044 Using the Blanket ........................................................................1046 Summary ............................................................................................1054 18
One Step Ahead: COM+ 1057 Introducing COM+ ............................................................................1058 COM+ Applications ....................................................................1058 The COM+ Catalog ......................................................................1059 Using COM+ Services ......................................................................1060 Loosely Coupled Events ..............................................................1060 Transactions..................................................................................1062 Synchronization ............................................................................1065 Security ........................................................................................1066 Queued Components ....................................................................1066 Load Balancing ............................................................................1066 Developing and Using COM+ Events ..............................................1067 Creating the COM+ Event Object................................................1067 Installing the Event in a COM+ Application ..............................1069 Creating the Publisher ..................................................................1073 Creating Subscribers ....................................................................1074 Configuring Subscribers ..............................................................1078 Creating a Persistent Subscription................................................1080 Creating a Transient Subscription ................................................1083 Developing and Using COM+ Transactional Objects ......................1093 Creating the Transactional Objects for the Business Layer ........1095 Developing Compensated Resource Managers ............................1105 Creating the Client........................................................................1118 Summary ............................................................................................1119
19
Multi-Tier Distributed Applications with MIDAS 3 1121 Introduction to MIDAS......................................................................1122 MIDAS Clients and Servers ..............................................................1125 Creating a Simple MIDAS Server................................................1125 Examining MIDAS Server Registration ......................................1129 Creating a MIDAS Client ............................................................1130 Using the Briefcase Model ..........................................................1134 Using ApplyUpdates ....................................................................1138 Implementing Error Handling ......................................................1138
00 9721 CD-FM
11/13/00
12:11 PM
Page xix
CONTENTS Demonstrating Reconcile Errors ..................................................1142 Accessing the Server Remotely....................................................1143 Creating a MIDAS Master-Detail Server ....................................1144 Exporting Master-Detail DataSets................................................1147 Creating a MIDAS Master-Detail Client......................................1148 Using Nested Tables ....................................................................1149 Understanding MIDAS Bandwidth Bottlenecks ..........................1152 MIDAS 3 Enhancements ..................................................................1154 TDataSetProvider ........................................................................1154 IProvider Versus IAppServer ......................................................1155 Stateless Data Broker ..................................................................1155 InternetExpress Applications........................................................1161 WebConnection Component ..........................................................1163 Object Pooling ..............................................................................1164 Socket Server................................................................................1166 Object Broker ..............................................................................1169 Deployment ..................................................................................1170 Summary ............................................................................................1171 20
Distributed Applications with CORBA 1173 Introduction to CORBA ....................................................................1174 How CORBA Works..........................................................................1175 Static and Dynamic ......................................................................1175 Always or On-Demand ................................................................1175 Flat or Hierarchical ......................................................................1176 Who Is the Server and Who Is the Client ....................................1176 The Object Request Broker ..........................................................1176 The Basic Object Adapter ............................................................1177 The Portable Object Adapter ........................................................1177 CORBA Versus COM ..................................................................1177 Visibroker Components ....................................................................1177 The Smart Agent ..........................................................................1178 The Object Activation Daemon ....................................................1178 The Console..................................................................................1178 The Interface Definition Language....................................................1178 The interface Keyword ..............................................................1179 The attribute Keyword ..............................................................1180 Methods ........................................................................................1180 Type Definitions ..........................................................................1180 Exceptions ....................................................................................1180 Inheritance ....................................................................................1181 Modules ........................................................................................1181
xix
00 9721 CD-FM
xx
11/13/00
12:11 PM
Page xx
C++BUILDER 5 DEVELOPER’S GUIDE What’s New in C++Builder 5 ............................................................1182 C++Builder Support for CORBA ......................................................1183 Environment Options....................................................................1183 Debugger Options ........................................................................1184 Project Options ............................................................................1184 The CORBA Server Wizard ........................................................1185 The CORBA Client Wizard..........................................................1186 CORBA IDL File Wizard ............................................................1186 CORBA Object Implementation Wizard......................................1187 The Project Updates Dialog ........................................................1188 The Use CORBA Object Wizard..................................................1189 Differences Between C++Builder 4 and C++Builder 5 ..............1190 Implementation Models ....................................................................1191 Inheritance ....................................................................................1192 Virtual Implementation Inheritance..............................................1192 Delegation Model (Tie) ................................................................1192 Poor Man’s CORBA ..........................................................................1193 Summary ............................................................................................1193 21
Microsoft Office Integration 1195 Overview of Integration with Microsoft Office ................................1196 How to Integrate ................................................................................1197 Using TOleContainer ..................................................................1197 Using Automation ........................................................................1199 Using Variants and Automation Objects ......................................1201 Guarding Against Macro Viruses with Automation ....................1203 Using Word Basic ........................................................................1203 Integrating with Word ........................................................................1204 Collections ....................................................................................1204 The Application Object ................................................................1204 Working with Documents ............................................................1205 Getting Text from Word................................................................1209 Putting Objects into Word Documents ........................................1212 Integrating with Excel........................................................................1216 Obtaining the Application Object ................................................1216 Working with Workbooks ............................................................1216 Putting Cells into Excel Worksheets ............................................1219 Getting Cells from Excel..............................................................1221 Using C++Builder 5’s Server Components ......................................1221 The WordApplication and WordDocument Components ..............1223 Vocabulary Revisited ....................................................................1223 Final Thoughts About ATL and OleServers ................................1228 Going Further ....................................................................................1228
00 9721 CD-FM
11/13/00
12:11 PM
Page xxi
CONTENTS Word ............................................................................................1229 Excel ............................................................................................1229 Other Office Applications ............................................................1230 Summary ............................................................................................1232 22
Part IV 23
Using ActiveX Techniques 1233 Understanding Active Server Objects................................................1234 A Step-by-Step Example: Creating Response and Request Objects Via the Active Server Object Wizard ............1235 ASP Session, Server, and Application Objects ............................1243 ASP Objects and WebBroker Support..........................................1243 Redeployment of Active Server Objects ......................................1244 Debugging Active Server Objects ................................................1245 Introducing ActiveForms ..................................................................1246 Building an ActiveForm ....................................................................1246 Deploying an ActiveForm for Use in Internet Explorer....................1249 Setting Options for an ActiveForm ..............................................1249 Connecting to an ActiveForm ......................................................1252 Creating a Data-Aware ActiveForm ..................................................1254 Working with CAB Files and Packages ......................................1257 ActiveForm Updates ....................................................................1258 OCCACHE and Downloaded Program Files ....................................1258 Creating ActiveForms as MIDAS Clients ........................................1259 Using an ActiveForm in Delphi ........................................................1261 Creating Component Templates with ActiveForms ..........................1263 Shell Programming ............................................................................1264 Shell Basics ..................................................................................1264 Retrieving a Folder’s Contents ....................................................1267 Transferring Shell Objects............................................................1269 Summary ............................................................................................1275 Advanced Topics Data Presentation with C++Builder 1279 Presenting Data in Reports ................................................................1280 Understanding the Value of a Report ..........................................1280 Using QuickReport to Produce Reports ......................................1281 Understanding the Philosophy of the Custom Viewer ................1281 QuickReport Custom Viewer Summary ......................................1292 Printing Text and Graphics ................................................................1292 Printing Text ................................................................................1292 Printing Graphics..........................................................................1303
xxi
00 9721 CD-FM
xxii
11/13/00
12:11 PM
Page xxii
C++BUILDER 5 DEVELOPER’S GUIDE Using Advanced Printing Techniques................................................1308 Determining Printer Resolution....................................................1308 Determining the Printable Paper Size ..........................................1308 Determining the Physical Sizes....................................................1309 Determining Printer Drawing Capabilities ..................................1310 How to Print with a Rotated Font ................................................1311 Getting Access to Printer Settings and Setup ..............................1312 How to Get the Default Printer Name..........................................1313 How to Set the Default Printer ....................................................1314 Resetting TPrinter ......................................................................1318 General Information About Accessing DEVMODE Using TPrinter............................................................................1318 Using PRINTER_INFO_2 ................................................................1319 Other Paper-Related Functions ....................................................1325 Working with Jobs........................................................................1334 How to Catch the Pressing of the Print Screen Button ................1336 Printing a Form ............................................................................1338 Creating a Print Preview ..............................................................1338 Using Printer-Related Conversion Routines ................................1339 Other Printer-Related Information ..............................................1341 Creating Charts with the TChart Component....................................1342 Getting Started with TeeChart ......................................................1342 Altering Chart Appearance at Runtime ........................................1344 Interacting with Charts ................................................................1345 Creating Charts Dynamically ......................................................1347 Printing Charts..............................................................................1348 Upgrading to TeeChart Pro ..........................................................1349 Summary ............................................................................................1350 24
Using the Win32 API 1351 Win32 API Versus Win32 Middleware..............................................1352 Brief History of Windows and the API..............................................1353 Win32 API Functional Areas ............................................................1356 Windows Management ................................................................1358 System Services............................................................................1360 Graphical Device Interface ..........................................................1363 Multimedia Services ....................................................................1364 Common Controls and Dialogs ....................................................1366 Shell Features ..............................................................................1369 International Features ..................................................................1370 Network Services..........................................................................1371
00 9721 CD-FM
11/13/00
12:11 PM
Page xxiii
CONTENTS Anatomy and Operation of a Windows Program ..............................1372 WinMain() ....................................................................................1372 Window Handles ..........................................................................1374 Windows Messages ......................................................................1374 Real-World Examples Using the API ..............................................1377 Launching an Application Within a Program ..............................1378 Fundamental File I/O....................................................................1382 Using the Magic of Shell..............................................................1393 Implementing Multimedia Services ............................................1406 Using Globally Unique Identifiers (GUIDs)................................1410 Determining System Information ................................................1411 Flashing a Notification ................................................................1423 Adding System Support................................................................1427 Animating Effects ........................................................................1429 Shaping Your Applications ..........................................................1432 Writing Control Panel Applets the Old-Fashioned Way ..............1442 Summary ............................................................................................1455 25
Multimedia Techniques 1457 The Graphical Device Interface (GDI) ..............................................1458 The Windows API and the Device Context..................................1459 Understanding TCanvas: The C++Builder Interface ....................1459 Customizing a Drawing................................................................1462 An Analogue Clock Example ......................................................1464 Image Support....................................................................................1465 The Windows Bitmap Object ......................................................1466 The TBitmap Class ........................................................................1466 JPEG Images ................................................................................1467 GIF Images ..................................................................................1468 PNG Images..................................................................................1469 Image Processing ..............................................................................1472 Displaying and Obtaining Image Information ............................1473 Accessing Individual Pixel Values Using TCanvas->Pixels ........1474 Image Generation ........................................................................1475 Fast Access to Pixel Value Using ScanLine ................................1477 Point Operations: Thresholding and Color/Grayscale Inversion ....................................................................................1478 Global Operation: Histogram Equalization..................................1480 Geometrical Transformation: Zoom ............................................1483 Spatial Operation: Smoothing and Edge Detection ....................1487
xxiii
00 9721 CD-FM
xxiv
11/13/00
12:11 PM
Page xxiv
C++BUILDER 5 DEVELOPER’S GUIDE Audio Files, Video Files, and CD Music ..........................................1489 The Media Control Interface ........................................................1489 The Waveform Audio Interface ....................................................1496 Concluding Remarks on the Waveform Audio Interface ............1504 Summary ............................................................................................1504 26
Advanced Graphics with DirectX and OpenGL 1505 Introduction to OpenGL ....................................................................1506 OpenGL Versus Direct3D ............................................................1507 The OpenGL Command Structure ..............................................1507 Drawing Loops in C++Builder Using OnIdle() ..........................1507 Using OpenGL ..................................................................................1508 Stage 1: OpenGL Initialization ....................................................1508 Stage 2: Setting Up a Rendering Environment with Lighting and Shading ................................................................1515 Stage 3: 3D Transformations........................................................1518 Stage 4: Drawing Primitives ........................................................1520 Stage 5: Flipping Surfaces............................................................1530 An Example OpenGL Program ....................................................1530 OpenGL Conclusion ....................................................................1531 OpenGL References ....................................................................1531 Introduction to DirectX......................................................................1532 COM Basis of the DirectX API....................................................1532 Non-Object DirectX Functions ....................................................1533 Using DirectDraw ..............................................................................1533 Initialization of a DirectDraw Object ..........................................1533 Adjusting Display Settings for DirectDraw ................................1535 Drawing Surfaces ........................................................................1537 Using the GDI on DirectDraw Surfaces ......................................1539 Loading Bitmaps into a Surface ..................................................1541 A DirectDraw Example Program ................................................1547 DirectDraw Conclusion ................................................................1547 Using DirectSound ............................................................................1547 Initializing a DirectSound Object ................................................1548 Creating a Secondary Buffer ........................................................1549 A DirectSound Example Program—A Multiple Sound Player ..1556 Taking DirectX Further......................................................................1556 DirectX References............................................................................1557 Summary ............................................................................................1557
00 9721 CD-FM
11/13/00
12:11 PM
Page xxv
CONTENTS
Part V
C++Builder Application Deployment
27
Creating Help Files and Documentation 1561 Technical Writing 101—Ten Quick Steps to Better Writing ............1562 Types of Documentation....................................................................1563 Strategies for Online Documentation ................................................1564 Approaches to Help ......................................................................1565 Help Formats......................................................................................1568 WinHelp Format Help Files: The Windows Standard ......................1570 Help-Authoring Tools ..................................................................1571 Context-Sensitive Help ................................................................1572 The MS Help Workshop ..............................................................1573 Adding What’s This? Help to C++Builder ..................................1579 Expanding Your Help Project Using Advanced Features ............1580 Microsoft HTML Help Files..............................................................1581 Help Properties and Methods in the VCL ........................................1583 Help Properties ............................................................................1583 Help Methods ..............................................................................1583 Events ..........................................................................................1587 Resources for the Help Author ..........................................................1587 Books ............................................................................................1587 Help-Authoring Tools Available on the Internet ..........................1588 Summary ............................................................................................1592
28
Software Distribution 1593 Language Internationalization and Localization ..............................1594 Overview of Language Internationalization ................................1594 The Localize Application ............................................................1595 Things to Remember ....................................................................1601 Resource DLL Wizard ......................................................................1602 How Does It Work? ......................................................................1602 How to Create a Resource DLL ..................................................1602 How to Test It ..............................................................................1605 Other Files and Programs to Ship......................................................1606 Your Application Files..................................................................1606 Distribution Steps ........................................................................1609 Copyrighting and Software Licensing ..............................................1609 Copyrighting ................................................................................1609 Software Licensing ......................................................................1611 Software Protection............................................................................1611 Protecting Your Application ........................................................1612 Protecting Your Application with Third-Party Components ........1612
xxv
00 9721 CD-FM
xxvi
11/13/00
12:11 PM
Page xxvi
C++BUILDER 5 DEVELOPER’S GUIDE Protecting Your Application with Other Types of Components ..............................................................................1614 Some Final Thoughts on Software Protection ............................1615 Shareware ..........................................................................................1615 Shareware and How to Protect It..................................................1616 Implementing Shareware Protection Methods ............................1618 Shareware Methods in a Nutshell ................................................1618 Distribution and Marketing Via the Internet......................................1618 Web Sites ......................................................................................1619 Customer Support ........................................................................1619 Advertising ..................................................................................1619 Free Banner Ads ..........................................................................1620 Accepting Credit Cards and Providing Unlock Codes ................1621 Internet Marketing Hints and Tips ..............................................1622 Summary ............................................................................................1623 29
Software Installation and Updates 1625 Install and Uninstall ..........................................................................1626 Installation Program Creators ......................................................1626 Install Maker ................................................................................1627 Uninstalling ..................................................................................1630 CAB and INF Files ............................................................................1632 About CAB Files ..........................................................................1632 About INF Files............................................................................1635 About Internet Packages ..............................................................1640 Versions, Updates, and Patches ........................................................1643 Versions ........................................................................................1643 Updates for Application Improvements ......................................1644 Patches ..........................................................................................1646 Patch Maker..................................................................................1647 Some Tips on Updates and Patches..............................................1649 Version Control and TeamSource ......................................................1649 Who Should Use TeamSource? ....................................................1650 Why Should TeamSource Be Used? ............................................1650 When Should TeamSource Be Used? ..........................................1651 Where Can TeamSource Be Used? ..............................................1651 How Is TeamSource Used? ..........................................................1651 TeamSource Windows ..................................................................1657 Version Controllers ......................................................................1660 Bookmarks....................................................................................1661 Locks ............................................................................................1661
00 9721 CD-FM
11/13/00
12:11 PM
Page xxvii
CONTENTS Using InstallShield Express ..............................................................1662 Installing InstallShield..................................................................1663 Getting Started in InstallShield ....................................................1663 Testing ..........................................................................................1671 Summary ............................................................................................1671
Part VI 30
Knowledge Base Tips, Tricks, and How Tos 1675 Making the Enter Key Simulate the Tab Key....................................1676 The Solution ................................................................................1676 Explanation of the Code ..............................................................1677 Some Pitfalls ................................................................................1681 Tab Key Simulation Wrap Up ......................................................1681 Determining the OS Version ..............................................................1682 The Solution ................................................................................1682 The Code ......................................................................................1683 Determining the OS Version Wrap Up ........................................1684 Programming Using Floating-Point Numbers ..................................1684 Background ..................................................................................1685 Working with Numbers ................................................................1686 Performing Addition and Subtraction ..........................................1687 Performing Formulaic Chains of Arithmetic................................1691 Comparing Data............................................................................1691 Floating-Point Numbers—Closing Remark ................................1692 Implementing Splash Screens............................................................1692 The WinMain() Function ..............................................................1692 Creating a Splash Screen..............................................................1694 Preventing More than One Instance of an Application from Running ..................................................................................1696 The Solution ................................................................................1696 The Code ......................................................................................1697 Conclusion ....................................................................................1702 Working with Drag-and-Drop............................................................1702 The Solution ................................................................................1702 The Code ......................................................................................1702 How Does It Work? ......................................................................1705 Wrapping Up Drag-and-Drop ......................................................1707 Capturing the Screen..........................................................................1707 How Windows Handles Windows ................................................1707 The Solution ................................................................................1708 Capturing Screens Wrap Up ........................................................1712
xxvii
00 9721 CD-FM
11/13/00
xxviii
12:11 PM
Page xxviii
C++BUILDER 5 DEVELOPER’S GUIDE Implementing a TJoyStick Component ............................................1713 Creating a Windows System Monitor-Like Application ..................1726 A Look at Windows System Resources ......................................1726 The Solution ................................................................................1728 System Monitor Wrap Up ............................................................1735 Examining a Soundex Application ....................................................1735 The Solution ................................................................................1736 Using Tree View Components ..........................................................1743 Tree View Basics ..........................................................................1744 Adding Nodes ..............................................................................1744 Using Glyphs ................................................................................1747 Navigating Tree Views ................................................................1749 Accessing Nodes ..........................................................................1750 Finding Nodes ..............................................................................1751 Displaying Node Counts ..............................................................1752 Moving Nodes Up and Down ......................................................1754 Implementing Drag-and-Drop ......................................................1755 Modifying a Node ........................................................................1757 Deleting a Node............................................................................1759 Supporting Undo and Redo Deletion ..........................................1760 Save a Tree (For the Morning After)............................................1763 TTree Wrap Up ............................................................................1763 Implementing an Icon Extraction Utility ..........................................1764 Creating a Windows Explorer-Like Application ..............................1773 Investigating Windows Shell Functions and Interfaces................1773 The Solution ................................................................................1774 Windows Explorer Wrap Up ........................................................1781 Working with NT Services ................................................................1781 The SendMsg Program ................................................................1781 The Stickums Service ..................................................................1784 The Stickem Client Program ........................................................1788 Windows Services Wrap Up ........................................................1789 Using Cryptography ..........................................................................1789 The Solution ................................................................................1790 File Encoding................................................................................1793 File Decryption ............................................................................1799 Creating a World Daylight Clock ......................................................1801 Special Supplement for Non-Believers ........................................1807 Summary ............................................................................................1808
00 9721 CD-FM
11/13/00
12:11 PM
Page xxix
CONTENTS 31
Part VII A
A Real-World Example 1809 Examining a World Wave Statistics Program ....................................1810 Examining the Source Code ..............................................................1811 Header File math.h ......................................................................1812 Header File mapunit.h ................................................................1812 Header File wavedata.h ..............................................................1813 Source Code File about.cpp ........................................................1813 Examining the Code for TMainUnit ..................................................1814 Making Some Improvements ............................................................1819 Summary ............................................................................................1821
Appendix Information Sources 1825 Borland Web Sites..............................................................................1826 Borland Community Web Site......................................................1826 Borland Main Web Site ................................................................1827 CodeCentral ......................................................................................1828 Discussion Lists and Forums ............................................................1830 Web-Based Forums ......................................................................1830 Email Discussion Lists ................................................................1831 Newsgroups........................................................................................1831 Web Sites ..........................................................................................1833 How-Tos and Technical Articles ..................................................1834 Component Repositories ..............................................................1834 Books and Magazines ........................................................................1835 C++Builder Books........................................................................1836 General C++ Books ......................................................................1837 Magazines ....................................................................................1837 Conferences and User Groups ..........................................................1838 Index
1839
xxix
00 9721 CD-FM
11/13/00
12:11 PM
Page xxx
About the Authors Major Authors Jarrod Hollingworth Jarrod has been professionally programming since 1993. He is now running his own business, Backslash (http://www.backslash.com.au), developing software applications for the Internet and key business sectors and working as a software development consultant. He has a solid background in C/C++ programming in the telecommunications industry and assisted in the development of the world’s first live operator–answered GSM (digital mobile) short-messaging system. Starting in 1985 as a self-taught hobbyist programmer in BASIC and Assembly, he moved to Pascal and C/C++ through completion of a bachelor of science degree in computing at Deakin University in Australia. His professional roles in software development have ranged from programmer to software department manager. With several years of experience in C++Builder and Delphi and having worked on project teams using Microsoft Visual C++, he believes that with few exceptions C++Builder is the best tool for developing Windows applications. Jarrod lives in Melbourne, Australia, with his wife, Linda. His other major interests include traveling and cycling. Jarrod can be contacted at
[email protected].
Dan Butterfield Dan is currently writing mathematical modeling and geographical information system (GIS) software for environmental applications in the Aquatic Environments Research Centre (AERC) at the University of Reading in the UK. As sole software developer for the research center, Dan works closely with colleagues to provide implementations of ground-breaking water quality modeling and data visualization software. INCA (Integrated Nitrogen in Catchments), the latest incarnation of an original nitrogen model, is in use worldwide and is undergoing continual development. Dan started programming in BASIC at age 12 and has programmed professionally in REXX, Fortran, Pascal, C, and now C++. All of Dan’s development work over the past two years has been exclusively with Borland C++Builder.
Bob Swart Bob Swart (aka Dr. Bob—http://www.drbob42.com) is a senior technical consultant using Delphi, C++Builder, and JBuilder (and soon Kylix) for TAS Advanced Technologies (http://www.tas-at.com) in Best, The Netherlands. Bob is a freelance technical author for
00 9721 CD-FM
11/13/00
12:11 PM
Page xxxi
xxxi
The Delphi Magazine, Delphi Developer, UK-BUG NewsLetter, and SDGN Magazine. He is a coauthor of The Revolutionary Guide to Delphi 2, Delphi 4 Unleashed, and C++Builder 4 Unleashed. Bob has spoken at Borland conferences all over the world.
Jamie Allsop Jamie lives in Northern Ireland. Mostly self-taught, he has used a variety of languages, but C++ is his language of choice. He has used C++Builder since it was first released, and it is his preferred development tool. He has a degree in electronic engineering and is currently doing research. He also runs his own software company, Shiying, with his wife and has developed components for communication and real-time DSP applications. Other interests include football (the using-feet kind) and badminton.
Contributing Authors Rob Allen Rob lives in London, UK. He has a degree in electronic engineering but has spent the last five years programming with Borland C++ 4.5 and 5 and C++Builder 3. Rob has experience writing test and measurement tools for the mobile phone industry.
Khalid Almannai Khalid lives in Bahrain. He started programming with Borland C++ 4.5, and his knowledge of programming languages includes C, C++, BASIC, and Delphi. He has two years of experience with C++Builder.
Drew Avis Drew Avis is a technical writer specializing in online documentation. He lives in Merrickville, Ontario, and currently works for Nortel Networks. Drew has worked for small and large software companies, and he has developed and taught a technical writing course for engineers at the University of Calgary. In his spare time Drew brews beer, plays hockey, and works on his homebrew recipe software StrangeBrew.
Jay Banks Jay lives in the UK. He has been a programming enthusiast since he was 11 and has programming experience in a number of areas, including accounting, machine control, security, components, games, educational, and databases. His C++Builder experience is varied.
00 9721 CD-FM
11/13/00
12:11 PM
Page xxxii
Eduardo Bezerra Eduardo lives in Rio de Janeiro, Brazil. He has been programming for 19 years and has extensive experience in C++ development. Eduardo started programming with Borland Turbo C and has worked with all Borland C/C++ compilers up to C++Builder. He owns a small company specializing in consulting services and development for business areas such as the Internet and telecommunications. He has been working with COM for four years, integrating systems and creating solutions for distributed environments.
Phillip H. Blanton II Phillip attended the University of Northern Colorado, where he studied physics and computer science. He has been programming personal computers with Borland tools since 1987. Over the past 13 years he has worked in a number of technical roles, including network administrator, IS department director, database application developer, and network security specialist. He is currently a senior software engineer with TurboPower Software in Colorado Springs. He lives in the mountains surrounding Woodland Park, Colorado, with his wife Mary Ann, two daughters Daly and Sydney, and Harley the Wonder Dog. You can reach him via email at
[email protected].
Joe Bonavita Joe lives in Connecticut and has been programming for 14 years, three of those years in C++Builder. Joe started programming when he was 14 on a Commodore 64 and is a selftaught programmer. He graduated to the PC, then to BASIC, Assembler, and C++. Joe started using C++Builder beginning with version 1. Thanks to his wife, who gives him the support needed to keep moving forward in the ever-changing field of programming, he is learning other languages and contributing to books such as this one. Joe believes that books like this are very beneficial to self-starters.
J. Alan Brogan Alan lives in Kerry, Ireland. He has been programming and teaching for more than 10 years and is currently leading a team developing software within the telecommunications industry. Alan has used other languages but prefers the simplicity and elegance of C and C++. He has always preferred Borland’s IDEs and still uses Turbo C, but he spends most of his programming day with C++Builder.
00 9721 CD-FM
11/13/00
12:11 PM
Page xxxiii
Mark Cashman Mark is a professional software developer with experience in a variety of industries, ranging from insurance to manufacturing, to geographic information systems for bulk product delivery. His specialties include computer/human interaction, relational databases, component-based software, object-oriented development, and Web site/Web software development, including his own site, The Temporal Doorway, whose C++Builder section can be found at http://www.temporaldoorway.com/programming/index.htm. He has been a methodology consultant, a developer, and a manager and is currently Senior Software Developer for VTechnologies, LLC, a vendor of shipping-oriented middleware. Mark is a member of Borland TeamB, an association of official but unpaid newsgroup support people, selected for their knowledge of Borland products and willingness to help other developers. He is also an awardwinning artist, a published author of science fiction (currently working on his third novel) and nonfiction (including several articles for C++Builder Developer’s Journal), a composer of computer-based fusion music, and a rock climber with several first ascents to his credit.
Damon Chandler Damon has been programming for more than 15 years. Mostly self-taught, he followed a common path to C++ programming: GW-BASIC, Pascal, then C++. Damon is an engineer by trade and a programmer at heart. He is currently working in the Visual Communications Lab at Cornell University, where his research primarily centers around wavelet image compression. C++Builder is his tool of choice, and he has recently been made a member of TeamB.
Jeppe Cramon Jeppe lives in Copenhagen, Denmark. He has been programming for seven years, starting out on Pascal, then moving on to C++, Visual Basic, Java, and C#. Jeppe has been using C++Builder the last four years and has recently started working with XML, ASP, DHTML, COM, and .NET programming. Jeppe holds an engineering degree in electronics and computer science. Besides running a small consulting firm, Jeppe also contributes to the Bytamin-C Web site (http://www.bytamin-c.com) and has developed a free WinZip clone—Jzip—and an arcade game—B.U.G.S.—for OS/2 and Windows. Jeppe is also an enthusiastic skier and drummer in his sparse spare time.
Mark Davey Mark lives in Hampshire, England. He has been writing software professionally since his teens and has been using C++Builder for two years. He has been mostly involved in writing realtime embedded control systems for broadcasting applications with leading UK broadcast engineering companies and is currently developing radio communication systems. Mark also has a
00 9721 CD-FM
11/13/00
12:11 PM
Page xxxiv
private enterprise in data logging systems for the motorsport industry, developing exclusively with C++Builder. He is deeply interested in music, art, and technology. He is shortly to be married to Sarah.
Paul Gustavson Paul lives in Virginia and is a senior systems engineer for Synetics, Inc. (http://www. a U.S.-based company providing knowledge management, systems engineering, and enterprise management services. Paul supports a wide variety of advanced distributive simulation efforts within the DoD community and is an experienced software developer specializing in C++Builder. He also is a partner in SimVentions (http://www.simventions.com), an upstart company developing and leveraging existing technologies and techniques to create innovative applications and solutions that engage the mind and further knowledge. Paul has been developing applications for the Windows environment since the introduction of Windows 3.1 (1993), and he has published and presented over a dozen technical papers for the Simulation Interoperability Standards Organization (http://www.sisostds.org). synetics.com),
John MacSween John graduated from Glasgow University with an honours degree in naval architecture and ocean engineering. He presently works for Henry Abram & Sons Ltd, a project cargo and heavy lift shipping company in Glasgow, Scotland. John has been programming since the age of 14 and has used BASIC, Fortran, and C++. He has been programming in C++Builder for approximately one and a half years and uses it to create various in-house engineering applications. John may be contacted at
[email protected] or http://www.redrival.com/mandtsoftware/.
Stéphane Mahaux Stéphane runs his own software consulting company, Hyperian Development Solutions (http://www.hyperian.ab.ca), in Edmonton, Canada. He started programming before the Windows 3 era and has been programming professionally for more than 10 years. He has been using C++Builder since version 1. Stéphane also has lived in France, where he managed a business specializing in database tools. He wrote a monthly column in Point DBF magazine about database programming in various development tools.
William Morrison Hooked on Borland products since being introduced to Turbo Pascal 7 in high school, Bill has been programming professionally with C++Builder and Delphi for more than three years. Bill has written predictive dialers and standard business applications using C++Builder and is
00 9721 CD-FM
11/13/00
12:11 PM
Page xxxv
experienced with ISAPIs, APIs, and C/S database design. He assists new C++Builder and Delphi users through the Undernet IRC help channels and can be seen occasionally contributing on the various Borland Usenet groups. Bill is currently vice president of Software Development for CXC Consulting (http://www.cxcca.com), located in sunny Southern California. Bill wishes to thank Jason Wharton (IBObjects) and Jeff Overcash (IBX) for their assistance with the InterBase sections in this book, showing just what a great group of programmers InterBase has supporting it.
Ionel Munoz Ionel lives in Quebec. His current occupation is as a senior software analyst at EXFO, a leading manufacturer of fiber optics test equipment. His job involves developing architectures and frameworks, most of them based on COM/DCOM. Ionel started programming in Turbo Pascal in 1990 and C++ in 1992, and he has used C++Builder since version 1. He has developed applications in many areas, including games, components, Java applets, client/server databases, data acquisition systems, and multimedia. He has been developing with COM since 1995 and is experienced in using ATL in both VC++ and C++Builder. He has contributed to Borland’s CodeCentral source code database, with several COM-related examples. His interests beyond computer science are traditional karate, books, and music. Ionel has a wife, Sylvia, and a daughter, Marietta.
Jean Pariseau Jean works as a chef at a prestigious New England country club while attending college at the Community College of Rhode Island, where he’s about to receive his associates degree in engineering. Jean has been programming for about 14 years, starting on a Commodore 64 and working up through the Amiga platform and now Windows-based machines. Mostly self taught, Jean works with C++, Pascal/Delphi, Assembler, Lisp, and Java. Jean’s programming interests revolve around computer algebra, numerical methods, computational fluid dynamics, and compiler design.
Pete Pedersen Pete lives in Logan, Utah, and has been programming professionally for over 30 years. He has used Borland’s tools for about eight years, and C++Builder since version 1. During his career, he has been an international consultant and the founder of a software company (BlueLine Software) and has written articles for trade publications. He is currently employed by Spiricon, a leader in laser diagnostics. When not working, he enjoys spending time with his family. He and his nine-year-old son, Cameron, are collaborating on a programming book for kids, using C++Builder.
00 9721 CD-FM
11/13/00
12:11 PM
Page xxxvi
Ruurd F. Pels Ruurd works in the Netherlands as an IT consultant for TAS Advanced Technologies B.V. His main area of expertise is software engineering, particularly in C++. He is a regular contributor to Dr. Bob’s C++Builder Gate (http://www.drbob42.com/Cbuilder/) and occasionally publishes reviews and articles. He can be reached at
[email protected]. Ruurd is married to Angelien and the father of two sons, Ruben and Daniël.
Sean Rock Sean lives in Bolton, UK. He is originally from San Diego, California, but has lived in the UK for the past five and a half years. Sean started programming in college using Pascal and has taught himself Delphi and C/C++. Sean uses computer programming primarily as a hobby but does take on freelance work for local companies. He has been using C++Builder for about two years and just recently started the Component Writer’s Guide Web site, dedicated to writing VCL components, which can be found at http://www.componentwriter.co.uk.
Simon Rutley-Frayne Simon lives in Devon, UK. He began programming with C++Builder just over two years ago and soon after started Casimo Software (http://www.casimosoftware.co.uk), developing shareware software and components. Simon runs his own computer consultancy business and is the Components/Applications and Books editor of TheBits (http://www.thebits.org), where he is responsible for component and book reviews and updates.
Vikash Shah Vikash lives in Middlesex, England. Since earning his degree in mathematics and computer science, he has gained three years professional C++ experience, including two years with C++Builder.
Malcolm Smith Malcolm lives in Australia. He is a self-taught programmer working as an analyst programmer, developing customized MIS solutions for the printing industry, and device drivers for monitoring and surveillance equipment in the security industry. He also runs his own business, MJ Freelancing, developing encryption, anti-piracy, and other tools and components.
00 9721 CD-FM
11/13/00
12:11 PM
Page xxxvii
Keith Turnbull II A graduate of Michigan State University, Keith has been in network programming since 1991. He has worked on a range of products, from online games to insurance enrollment software. He is currently the vice president of client/server development for Intercanvas Design, Inc. Keith lives in Des Moines, Iowa, with his wife, two dogs, three cats, and four computers.
Chris Winters Chris lives in Richmond, Virginia. He has been programming for six years, two years professionally. Although fluent in C/C++, Chris has used BASIC, HTML, Java, Assembly, MS Access, and Delphi. He started with Turbo C++ (DOS) and has been using C++Builder since version 1. Chris currently works for PSINet, one of the world’s largest and most experienced providers of IP-based communications services for business. He has two kids, Lauren Ashley and Collin Mason. He also writes system utilities and shareware programs and experiments with new Win32 APIs. You can visit him at http://www.encomsysware.com.
William Woodbury Bill has been programming since he was 12. He has used a variety of languages, including ADA, BASIC, Pascal, Java, C/C++, and Assembly, and he currently uses C++Builder. He has written graphics applications, games, and networking applications.
Siu-Fan Wu Siu-Fan obtained his bachelor of science degree in physics and his Ph.D. in electronic and electrical engineering from the Univerity of Surrey in the UK. He worked for the Canon Research Centre (UK) as a research scientist on sound field visualization and then as a lecturer at Singapore Polytechnic. Currently he is a lecturer at the Hong Kong Institute of Vocational Education, teaching mainly microprocessors and instrumentation. His research interests include image processing, compression, and retrieval. He has been programming with Borland C and C++Builder for about nine years.
Yoto Yotov Yoto Yotov is currently studying in Montreal. Often working with C++Builder, he has continuously explored it since its first appearance. He has published articles in various technical journals and Web sites.
Zexiang Wu Zexiang Wu has been programming with Borland C++ for ten years. Her recent work has focused on interface programming. Away from work, she enjoys backpacking, classical and folk dancing, and she especially enjoys sampling food from different countries; her favorites are from southern China where she originates.
00 9721 CD-FM
11/13/00
12:11 PM
Page xxxviii
Dedications Jarrod Hollingworth I dedicate this book to my wife, Linda. Throughout the many long days and late nights, her love, support, understanding, and acceptance of my constant phrase “another 15 minutes” (an hour and a half in the real world) have made this book possible.
Dan Butterfield To my family.
Bob Swart For Yvonne, Erik, and Natasha.
Jamie Allsop To Wenmay (Wu Ze Xiang), who never lost faith.
00 9721 CD-FM
11/13/00
12:11 PM
Page xxxix
Acknowledgments Jarrod Hollingworth First I’d like to thank Dan Butterfield. From concept to completion, Dan shared the author-side organizational tasks with me. With more than 30 contributing authors, these tasks were very demanding, and the time and effort that Dan invested in them is extremely appreciated. I’m sure that Dan would agree that it was quite a challenge! This book has opened my eyes to the fact that the publishing process is very involved indeed. As the acquisitions editor, Carol Ackerman took the book onboard and managed the manuscript submissions and the overall schedule. Robyn Thomas was the development editor for this book. Her eagle eye for quality and content-related issues ensured that the book as a whole is more than the sum of its parts. It was a pleasure working with both Carol and Robyn. I’d also like to thank technical editors Paul Strickland, John Thomas, Peter Nunn, and Eamonn Wallace for their attention to detail, copy editors Gene Redding and Pat Kinyon (who, with their superior knowledge of English, improved the grammar in just about every paragraph), project editor Andrew Beaster, and all other staff at Macmillan. For their assistance with various issues in writing my topics, I’d like to thank Charlie Calvert, Michael Swindell, Tim Del Chiaro, John Wiegley, Lee Cantey, Maurice Barnum, David Marancik, Karen Giles, John Kaster, and David Intersimone at Borland and Kent Reisdorph and Per Larsen at Turbopower Software Company. Finally I’d like to thank each and every author in this book, but in particular Bob Swart, Jamie Allsop, Ionel Munoz, and Vikash Shah, who showed exceptional commitment and enthusiasm. With such a large breadth of experience, each author has donated a piece of his knowledge to make this book an invaluable resource for C++Builder developers.
Dan Butterfield I would like to thank (in no particular order): The directors of the AERC, Professor Paul Whitehead and Dr. Penny Johnes, for their patience and understanding on those occasions when book deadlines interfered with my day job. My colleagues and friends, for their interest and support, but especially Brian Cox, Andrew Wade, Hannah Prior, Steve Gurney and of course Cam Sans, who helped keep me sane. Matt Roberts and Andrew Burns for invaluable Web site help and advice. Erika Meller and Heather Browning for their image manipulation and graphic design skills. Nicola Tuson and Joan Bachelor for helping out with online chat transcriptions.
00 9721 CD-FM
11/13/00
12:11 PM
Page xl
Everyone at Macmillan, but especially Carol Ackerman, Robyn Thomas, Andrew Beaster, and Paul Strickland, who all had to work incredibly hard to make this book a reality. Finally, a huge thank you to Jarrod for getting this whole thing started (and for all those late night/early morning Internet chats ;o), and to all the authors involved—your commitment and patience astounded me daily. Thanks guys!
Bob Swart I need to thank Yvonne for putting up with me writing yet another set of chapters. The phrase “almost done” slowly lost its meaning to her. Also, thanks to Jarrod and Dan for putting this book together—without you, it wouldn’t be the same.
Jamie Allsop It goes without saying that I am eternally grateful to both Jarrod and Dan. I want to thank you for taking on a task that few can comprehend the enormity of. Because you believed that a book could be written, I (and the other authors) also believed it could be done. I would also like to thank you for your continued encouragement throughout the project; it made the long hours I spent writing and coding worth it and even helped get some chapters finished. Thank you for helping me realize a dream. I want to thank all the authors of the book; if any one of you had not written the sections you did, the book would never have made it to maturity. I would like to thank all the editors at Sams for the excellent work that they put into the book to ensure it is of high quality. I especially would like to thank development editor Robyn Thomas for all her heading suggestions and comments that greatly helped to shape the sections that I wrote. I would also like to thank Paul E. Strickland, one of the technical editors, for his comments and suggestions. Gene Redding and Pat Kinyon also deserve a mention for the excellent work they put into copy editing the chapters I was involved with. Your attention to detail is incredible. I would like to thank my family and friends for their support, patience, and understanding throughout the time I have been involved in the book, notably my twin brother Kristin, my mum Norah, my dad Terry, and my friends Hong and Kendo. Finally, and most importantly, I would like to thank my wife, Wenmay, who supported and cared for me throughout the writing of the book. She continually encouraged me and didn’t mind when I had to put the book first; believe me, that happened very often! Without her, my contribution would surely have been much smaller.
00 9721 CD-FM
11/13/00
12:11 PM
Page xli
Tell Us What You Think! As the reader of this book, you are our most important critic and commentator. We value your opinion and want to know what we’re doing right, what we could do better, what areas you’d like to see us publish in, and any other words of wisdom you’re willing to pass our way. As an Associate publisher for Sams, I welcome your comments. You can fax, email, or write me directly to let me know what you did or didn’t like about this book—as well as what we can do to make our books stronger. Please note that I cannot help you with technical problems related to the topic of this book, and that due to the high volume of mail I receive, I might not be able to reply to every message. When you write, please be sure to include this book’s title and author as well as your name and phone or fax number. I will carefully review your comments and share them with the author and editors who worked on the book. Fax: (317) 581-4770 Email:
[email protected] Mail:
Michael Stephens Sams Publishing 201 West 103rd Street Indianapolis, IN 46290 USA
00 9721 CD-FM
11/13/00
12:11 PM
Page xlii
01 9721 intro
11/13/00
9:40 AM
Page 1
Introduction This has been an extremely interesting project on which to work. Our goal seemed simple—to produce a new book for C++Builder 5 that covered not only material new to version 5, but also techniques and topics not covered anywhere else. The book was born on “The Bits” (http://www.thebits.org) C++Builder technical discussion list with an email from Jarrod Hollingworth in mid-November 1999: Well I’ve just returned from the Australia & New Zealand BorCon99 and it was fantastic. They showed a lot of great enhancements to all products and the tutorials and sessions were very informative…great stuff. I don’t think that it’s likely that there will be a C++Builder 5 Unleashed book… This revelation was a disappointment to everyone subscribed to The Bits list because not only have the Unleashed books proved invaluable resources, but they are often the only readily available C++Builder reference books (the bundled Borland “Teach Yourself …” and “Developer’s Guide” manuals aside). It gradually became clear that several list subscribers had been considering writing articles on their particular areas of expertise, and so the project was born. Our first task was to decide what kind of book we should write and what topics the C++Builder community would like to see in it. To this end, the original Web site (dubbed “The C++Builder Book Writers’ Guild”) was set up, complete with online survey. We advertised the survey on the most active C++Builder discussion lists and newsgroups. The results from this survey and from online discussions (which can be found on the book’s Web site at http://www.bcb5book.force9.co.uk) helped shape the book. So, here we are with the final product. The book has been written by 34 authors from all over the world, in countries including Australia, Bahrain, Brazil, Canada, Denmark, Hong Kong, Ireland, the Netherlands, the UK, and the U.S. Each author contributed topics in his area of expertise and managed this task using email, online chats, and the Web site (and the occasional telephone call). All of the authors have full professional and personal lives and have somehow managed to work on the book in the little spare time that they have. The majority of the authors have little or no professional writing experience, though you may recognize the names of several authors from other C++Builder and Delphi books, as TeamB newsgroup members, or hosts of C++Builder-related Web sites. It has, of course, been very hard work. At the time of writing, the project organizers have collectively spent well over 600 hours, sent over 2,000 emails, and received over 3,500 emails on organizational matters alone. It has been an incredibly rewarding experience, and hopefully, the brief history of the project given here will persuade you that there is nothing magical about
01 9721 intro
2
11/13/00
9:40 AM
Page 2
C++BUILDER 5 DEVELOPER’S GUIDE
technical book writing—you can do it, too. We hope that we have created a book that users of all versions of C++Builder will find useful in the development of all types of applications. We have worked hard to include topics and techniques not covered in any other book and have done our best to cover the majority of features new to C++Builder 5. Above all, we hope that, through the unique way in which the book has been written, we have adhered to the C++Builder/Delphi ethos: sharing of knowledge.
Important Things to Note Because of the way this book was written, with so many authors and over a relatively short time period, there are bound to be differences in writing styles between, and perhaps even within, chapters. We (the authors and a team of editors at Sams) have all worked hard to reduce the impact of this, but it is bound to be obvious in places. As with all technical books, and despite our best efforts, it is inevitable that there will be the occasional error in the text and accompanying program code. In light of this, we shall maintain a list of errata on the book Web site at http://www.bcb5book.force9.co.uk and on the Sams Web site at http://www.samspublishing.com. You can also email bug reports and general queries/comments to
[email protected].
The Companion CD-ROM The companion CD-ROM contains all of the example code and C++Builder projects from the book. The code is organized by chapter and can be accessed from a custom-built interface. Some of the chapter folders also contain README.TXT files that provide important information about the projects in that chapter. These include chapters 13, 15, 16, 18, and 19. There are also many freeware, shareware, demo and trial components, and applications that should be of interest to C++Builder users. These may also be viewed from the interface to the CD-ROM. To ensure that the CD-ROM list of contents is up to date, it is supplied as a README.TXT file in the CD-ROM’s root directory.
A Word of Thanks During the course of the project, the list of contributing authors changed regularly as other commitments took priority and topics were added or altered. We’d like to take this opportunity to thank everyone who volunteered to help but were unable to see the book through to completion. We are particularly grateful to Rick Malik, the creator (and host) of the original book Web site, who put in a lot of time and effort during the initial stages of the project.
01 9721 intro
11/13/00
9:40 AM
Page 3
INTRODUCTION
Who Should Read This Book? This book is not a C++ primer, nor is it a tutorial on using C++Builder, the application. Rather, it is a guide to using C++Builder to create better, larger, more complex applications, to help expand your current C++Builder skills, and to investigate the features new to C++Builder 5. If you already have experience developing applications with C++Builder, are looking to upgrade from version 4 to version 5, or want to build on your current knowledge, this book is for you. That said, there is a natural, rapid progression through most of the chapters and through the book as a whole. This should even make the book useful to C++Builder beginners as, although the book was originally intended to be for intermediate/advanced readers only, the final product has proved to be accessible to readers of all levels despite the advanced nature of the topics.
C++Builder System Requirements C++Builder 5 Developer’s Guide is written, for the most part, for users of C++Builder version 5. Most of the text and example code, however, is applicable to C++Builder version 4 as well. Table I.1 shows the applicability of various versions of C++Builder. TABLE I.1 Percentage of This Book That Is Applicable to Different Versions of C++Builder C++Builder Version
Applicability
C++Builder 5 Enterprise
100%
C++Builder 5 Professional
94%
C++Builder 5 Standard
77%
C++Builder 4 Enterprise
84%
C++Builder 4 Professional
79%
Although most of the code in the book should work with C++Builder version 4 (except version 5 specifics, of course), a lot of the C++Builder project files on the CD-ROM that accompanies this book will be in version 5 format. Because this format is incompatible with C++Builder version 4, it will be necessary for version 4 users to create new projects, insert code from the companion CD-ROM, and add forms and properties as appropriate. The minimum system requirements for C++Builder 5 Enterprise are as follows: • Intel Pentium 90 or higher (P166 recommended) • Microsoft Windows 2000, Windows 95, 98, or NT4.0 with Service Pack 3 or later
3
01 9721 intro
4
11/13/00
9:40 AM
Page 4
C++BUILDER 5 DEVELOPER’S GUIDE
• 32MB RAM (64MB recommended) • Hard disk space: 253MB for compact install, 388MB for full install • CD-ROM drive • VGA or higher resolution monitor • Mouse or other pointing device
How This Book Is Organized This book is organized into seven parts. The first five parts have been arranged with natural progression in mind. These cover topics ranging from C++Builder and C++ techniques through communications, database, Web and distributed programming, to advanced, more general programming topics such as OpenGL programming and software installation/distribution. The last two parts contain C++Builder hints, tips, how-tos, an example real-world application, and other recommended sources of C++Builder information. The parts of the book are as follows: • Part I: “C++Builder 5 Essentials”—This part, consisting of Chapters 1–11, contains everything you need to know to make the best use of C++Builder 5 when developing applications. This includes an introduction to C++Builder and the Integrated Development Environment (Chapters 1 and 2); advice on C++ programming and software development with C++Builder (Chapters 3–5); compiling, optimizing, and debugging considerations (Chapters 6 and 7); and comprehensive information on using and writing VCL components (Chapters 8–11). • Part II: “Communications, Database, and Web Programming”—This part, consisting of Chapters 12–14, covers many aspects of communications, database, and Web programming. This includes serial communications and Internet protocols (Chapter 12); WebBroker, InternetExpress, and XML programming (Chapter 13); and database programming, particularly ADO Express, InterBase Express, the new Data Module Designer, and a discussion of database architecture options (Chapter 14). • Part III: “Interfaces and Distributed Computing”—This part, consisting of Chapters 15–22, provides detailed information on all aspects of interfaces and distributed computing. This includes using and writing DLLs, C++Builder packages, and plug-ins (Chapter 15); COM, DCOM, and COM+ programming (Chapters 16–18); MIDAS 3 (Chapter 19); CORBA (Chapter 20); integration with Microsoft Office, particularly Word and Excel (Chapter 21); and ActiveX programming (Chapter 22). • Part IV: “Advanced Topics”—This part, consisting of Chapters 23–26, covers advanced topics generally not found in C++Builder reference books. These include advanced printing and data presentation techniques (Chapter 23); a comprehensive look
01 9721 intro
11/13/00
9:40 AM
Page 5
INTRODUCTION
at the use of the Win32 API (Chapter 24); image processing, graphics (GDI, GIFs, JPEGs, and so on), and sound (WAV, MP3, and so on) with C++Builder (Chapter 25); and a look at advanced graphics programming with DirectX and OpenGL (Chapter 26). • Part V: “C++Builder Application Deployment”—This part, consisting of Chapters 27–29, contains more information not generally found in C++Builder books. This includes techniques and advice for creating standard Windows and HTML help files (Chapter 27); software distribution considerations, with a particular emphasis on shareware (Chapter 28); and techniques for software installation and updates, including version control with TeamSource (Chapter 29). • Part VI: “Knowledge Base”—This part, Chapters 30 and 31, contains a set of C++Builder hints, tips, and how-tos (Chapter 30); and an example real-world application (Chapter 31). • Part VII: “Appendix”—This appendix contains a comprehensive list of C++Builder resources, including Web sites (especially the Borland community Web site and CodeCentral), newsgroups, discussion lists, forums, books, magazines, and user groups.
Conventions Used in This Book This section describes the important typographic terminology and command conventions used in this book. Features in this book include the following:
NOTE Notes give you comments and asides about the topic at hand, as well as full explanations of certain topics.
TIP Tips provide great shortcuts and hints on how to use C++Builder 5 more effectively.
CAUTION These warn you against making your life miserable and tell you how to avoid the pitfalls in programming.
5
01 9721 intro
6
11/13/00
9:40 AM
Page 6
C++BUILDER 5 DEVELOPER’S GUIDE
In addition, you’ll find various typographic conventions throughout this book: • Commands, variables, directories, and files appear in text in a special monospaced
font.
• Placeholders in syntax descriptions appear in a monospaced italic typeface. This indicates that you will replace the placeholder with the actual filename, parameter, or other element that it represents.
02 Part 1
11/13/00
9:55 AM
Page 7
PART
C++Builder 5 Essentials
I
IN THIS PART 1 Introduction to C++Builder
9
2 C++Builder Projects and More on the IDE 3 Programming in C++Builder
53
95
4 Advanced Programming with C++Builder 5 User Interface Principles and Techniques
147 225
6 Compiling and Optimizing Your Application 7 Debugging Your Application 8 Using VCL Components
335
389
437
9 Creating Custom Components
499
10 Creating Property and Component Editors 11 More Custom Component Techniques
701
577
02 Part 1
11/13/00
9:55 AM
Page 8
03 9721 CH01
11/13/00
9:42 AM
Page 9
Introduction to C++Builder Chris Winters Khalid Almannai Jarrod Hollingworth Vikash Shah
IN THIS CHAPTER • C++Builder Basics • What’s New in C++Builder 5 • Upgrading and Compatibility Issues • Migration from Delphi • Advantages and Disadvantages of C++Builder 5 • Preparation for Kylix
CHAPTER
1
03 9721 CH01
10
11/13/00
9:42 AM
Page 10
C++Builder 5 Essentials PART I
Throughout this chapter we will introduce you to Borland C++Builder, one of the leading development environments for creating Internet, desktop, client/server, and distributed applications. It combines the ease of a RAD environment with the power and performance of ANSI C++. C++Builder is the tool of choice for hobbyist programmers, development teams in small and medium-size companies, and large teams in major corporations alike.
NOTE For more information on the features and benefits of C++Builder, see the “Features & Benefits” and “New C++Builder Users” links on the C++Builder Web site at http://www.borland.com/bcppbuilder/.
Seasoned C++Builder 5 programmers might choose to skip this chapter. For the rest of you, we will show the basics of C++Builder, migrating to C++Builder from other development environments, the advantages and disadvantages of C++Builder in comparison with other development environments, and an overview of what’s new in version 5 of C++Builder. On a slightly different note, we introduce you to Kylix, the code name for Borland’s upcoming development environment for the Linux operating system, one of the most anticipated development products in recent years. Kylix is basically C++Builder and Delphi for Linux, allowing you to create Linux applications as easily as you can create Windows applications with C++Builder and Delphi. Most of what you have learned and will learn with C++Builder and much of your existing code can be applied to Kylix.
C++Builder Basics In this section we will describe the basics of C++Builder and develop a simple application. By reading over the basics, you will see that C++Builder is quite easy to use.
Hello World! A Basic Start You just opened up your copy of C++Builder, threw the manuals aside, installed it, and are ready to go—right? We will go through the basics of C++Builder, projects, the VCL (Visual Component Library), Object Inspector, and more. If you are a beginner and need to learn really quickly, this chapter is for you! If you are an advanced developer and you are ready to burn more knowledge into your head, skip this chapter and move on to the real meat-and-potatoes. Developers used to do command-line programming using great products such as Turbo C++. Since Windows dominated the operating system market, compilers moved very quickly toward
03 9721 CH01
11/13/00
9:42 AM
Page 11
Introduction to C++Builder CHAPTER 1
With the introduction of OWL (ObjectWindows Library), things started getting easier. OWL contained all the programming headaches and made it easier to develop applications. The Borland C++ 3.1 compiler contained OWL, and Windows development started getting a little easier. After some time, Borland introduced Borland 5.0 compiler, which OWL and the IDE improved. Shortly after the success of Borland C++ 5.0, Delphi appeared on the market. Delphi was the visual Pascal language. Soon after the release, Borland released C++Builder, which was developed from Delphi. C++Builder used the same technology and from that point, Windows development became a lot easier than before. Delphi contained advanced RAD (Rapid Application Development) technologies, and C++Builder inherited this technology and became one of the best compilers in the world today. Compared to Visual C++, developing applications is faster and can be easily ported to other platforms. For the end user and the future developer, C++Builder has become one of the most popular compilers in the world. It is easy to use, and you can quickly learn how to navigate. Visual Basic developers will see that Visual Basic’s component manipulation is similar to C++Builder’s. Also, the IDE is somewhat similar and easy to navigate around. You do have to learn C++, which takes several years to master. However, Borland C++Builder can aid in that learning. Borland/Inprise has made it easy—the VCL and its class functions do the hard work for you. If you want to move to the fast-paced world of RAD development, C++Builder is the way to go. OK, let’s get started. First, consult your manuals and make sure C++Builder is installed properly on your system. Then, go ahead and start it up.
NOTE I will not go into great detail about each part of C++Builder, because that’s a whole book in itself! Plus, you can access the manuals or online help for installation and operation. What I will do is explain some basics to get you started building simple applications.
1 INTRODUCTION TO C++BUILDER
Windows programming. When Borland introduced Borland C++ Compiler 3.1, the world started moving into Windows development. However, it was still hard to grasp, and you still had to do all the hard work. Sometimes it would take three to five pages just to display a window, because of all the code to catch the messages to manipulate the window, draw the window, and perform other techniques within the Windows environment.
11
03 9721 CH01
12
11/13/00
9:42 AM
Page 12
C++Builder 5 Essentials PART I
When C++Builder has fully loaded, you will see the three main windows for the programming environment. This is called the Integrated Development Environment (IDE). The IDE has everything you need in one place, as you can see in Figure 1.1. The IDE is the programming work area for your project. Here you will manipulate controls or components, type in code, configure C++Builder, and maybe set properties of the project. Speedbar
Object Inspector
Main Menu
Component Palette
Form
Code Editor
FIGURE 1.1 The C++Builder IDE.
The VCL, Forms, and Components The Visual Component Library (VCL) is a repository of the components used to create C++Builder applications. A component is an object you use to make up the program, such as a check box, a drive combo box, or a graphical image. These components are chosen by leftclicking and placing them in your work area. VCL components are code that is compiled to perform certain operations, eliminating even more code that you might have to type. You can also add or write your own components, which is discussed in upcoming chapters. The components that make up the VCL remove most of the hard work for you. These components are located in the Component Palette. See the section “The Component Palette,” later in this chapter.
03 9721 CH01
11/13/00
9:42 AM
Page 13
Introduction to C++Builder CHAPTER 1
The Object Inspector also has an Events tab, where you can create events to indicate user interaction with the component. These events perform other actions that you define.
The Form A form is a visible window that is, in most instances, the user interface of your application. When you create a new application in C++Builder, a blank form is created automatically. To build the user interface you simply add visual components to the form, then position and size them accordingly. You can also add non-visual components to a form, such as timers. These appear as a simple component icon at designtime but are not visible at runtime. By default, when a user runs your application the form will be displayed in the center of the screen. You can alter the initial position of the form and other settings by changing the form properties in the Object Inspector.
The Speedbar The Speedbar provides quick access to the menu functions most commonly used within C++Builder, such as Run, Step Through, View Unit, View Form, and Add to Project. You can customize the Speedbar to your liking by adding the functions you prefer and removing those you don’t use. Customizing the IDE Toolbars One of the main features of the IDE is its capability to be customized. You can add or remove any speed button to or from any toolbar except for the Component Palette, which contains components, not commands. The C++Builder toolbars are as follows: • Standard • View • Debug • Custom • Component Palette To customize a C++Builder toolbar, follow these steps: 1. Right-click anywhere on the toolbar. 2. Choose Customize.
1 INTRODUCTION TO C++BUILDER
All components have properties, which can be manipulated by code or C++Builder. A component’s properties determine how it performs, how it looks, what functionalities it has, and so forth. You can modify the properties of a component with the Object Inspector, which is the panel at the left of the screen under the main menu when the Properties tab is selected (refer to Figure 1.1). You can also change property settings in code, but I would not recommend that unless you get really familiar with C++Builder and the VCL.
13
03 9721 CH01
14
11/13/00
9:42 AM
Page 14
C++Builder 5 Essentials PART I
3. The Toolbars Customize dialog will be displayed, as shown in Figure 1.2. 4. Click on the Commands page. 5. Now you can drag and drop any command you want to the toolbar. 6. To remove any speed button from the toolbar, just drag it outside the toolbar.
FIGURE 1.2 The Toolbars Customize dialog.
The Component Palette The Component Palette, located under the main menu, is an inventory of all the components in the VCL. These components are grouped by categories, the names of which are displayed on the tabs above the components. To pick a component, click it with your left mouse button, then click again on the form to place the component where you want it. As indicated previously, you can modify the component’s properties with the Object Inspector or by changing the code.
Events and Event Handlers In your first lesson in learning C++Builder, we will place a simple button on the form, set an event for the button, and run the program. You see buttons on almost every Windows application. It is a simple object that enables a user to trigger an event. The Button component is located under the Standard tab of the Component Palette. Its icon looks like a standard button with OK in the middle. To place a Button component on the form, left-click the icon one time; then, left-click again on the center of the form. Figure 1.3 shows the form with the button. Voila! You now have placed a button on the form, and C++Builder has created an instance of that button within the code. At the moment the button isn’t very useful because it doesn’t do anything. If you were to compile and run the application nothing would happen when you click on the button. Typically though, when a user presses the button you want to perform some
03 9721 CH01
11/13/00
9:42 AM
Page 15
Introduction to C++Builder CHAPTER 1
FIGURE 1.3 A Button component added to the form.
When the button is clicked at runtime, an event is generated. For your application to respond to this event you need to create an event handler. An event handler is simply a function that is automatically called when its event occurs. C++Builder creates the outline for the OnClick event handler, the event that occurs when the user clicks the button, when you double-click on the Button component at designtime. You can do the same thing by selecting the Button and then double-clicking in the OnClick field on the Events tab in the Object Inspector. Once the outline for the event handler is created, you can then add code to perform the necessary action that should occur when the button is clicked. The outline for the OnClick event handler is shown in the following code. void __fastcall TForm1::Button1Click(TObject *Sender) { }
If you right-click with the mouse in the Code Editor and choose Open Source/Header File, you will see the following code: //-------------------------------------------------------------------------#ifndef Unit1 #define Unit1 //-------------------------------------------------------------------------#include
#include #include <StdCtrls.hpp> #include //-------------------------------------------------------------------------class TForm1 : public TForm
1 INTRODUCTION TO C++BUILDER
action, such as saving information that the user may have entered, displaying a message to the user, and so on.
15
03 9721 CH01
16
11/13/00
9:42 AM
Page 16
C++Builder 5 Essentials PART I { __published: // IDE-managed Components TButton *Button1; void __fastcall Button1Click(TObject *Sender); private: // User declarations public: // User declarations __fastcall TForm1(TComponent* Owner); }; //-------------------------------------------------------------------------extern PACKAGE TForm1 *Form1; //-------------------------------------------------------------------------#endif
This code is generated automatically within C++Builder. I put it here for clarity to show you what C++Builder can do for you automatically. Now we’ll add code to display a message when the button is clicked, that the program is going to end, and then we’ll terminate the program. In the code editor, click on Unit1.cpp to move back to the Button component’s event. Type the following code inside the event that you just created: void __fastcall TForm1::Button1Click(TObject *Sender) { ShowMessage(“Hello world! This is a test application! Press OK”); Close(); }
This code can probably be understood even by a beginner. When the program runs, the user clicks on the button and an event is triggered. Our OnClick event handler will display a dialog box with our friendly message. When the user closes the dialog box our program then terminates due to the Close() method that we called.
Let’s Run It and See! From the Speedbar, click the green arrow that looks like the Play button of a tape player. On the main menu, click Run and choose Run. (As a shortcut, you could press F9 instead.) When you press the Run button, C++Builder will start to compile and execute the program. It then waits until you press the button. When you do so, the dialog appears revealing the message. The program exits when you press OK. After viewing your program, let’s close out our current project. From the main menu, choose File, New Application. C++Builder will ask if you want to save the application project; answer No. Let’s write a real program that can do something.
03 9721 CH01
11/13/00
9:42 AM
Page 17
Introduction to C++Builder CHAPTER 1
17
Your First Real Program
1
Choose File, New Application from the main menu. C++Builder will create a new project and generate code to create an empty form. Now we’re ready to create an application with as little code as possible.
INTRODUCTION TO C++BUILDER
On the Component Palette, select the Additional tab and then the Image component. If you aren’t sure, its icon has the sky, a hill, and water at the foot of the hill. If you put your mouse over each icon, a helper will tell you what it is. After selecting the Image component by clicking it, move your mouse to the form and click one time to place the component on the form. You will not see much, but a square outline will appear. This component displays graphical images. Under the Properties tab in the Object Inspector, go to the stretch attribute and select True. From the Component Palette, go to the Dialogs tab and choose the OpenDialog component. If necessary, scroll through the Component Palette with the left and right arrows. The OpenDialog component looks like an open yellow folder. Select it with your left mouse button and place it anywhere in the top right corner of the form. The component now is part of your form and is used for displaying a dialog box in which you can choose files. It can also open the files that you pick. Now we want to set an attribute to this component. Go to the Object Inspector and look for the attribute named Filter, located under the Properties tab. Enter the following text into the edit box for that attribute: BMP files|*.bmp
From the Standard tab of the Component Palette, select two Button components. Instead of placing one Button component at a time, you can place multiple Button components by holding down the Shift key and pressing the Button icon from the Component Palette. Click on the form and a Button component appears. Click on the form again and another Button component appears. Now select the white arrow on the left side of the Component Palette. This is the Object Selector in which you can navigate and select your components or objects on your form. Because we are done placing buttons, we now want to be able to select other components. Click one time on the Button1 component on the form. This selects the button itself. We now want to modify the attributes of that particular component. In the Object Inspector, click inside the Caption attribute and replace the existing text with the words Get Picture. The Caption attribute is where the button displays information to the user. Delete the existing text if needed. This gives your button a new caption to users.
03 9721 CH01
18
11/13/00
9:42 AM
Page 18
C++Builder 5 Essentials PART I
Go to the Win32 tab in the Component Palette and select the status bar. It looks like a gray bar with a grip. Hold your mouse over the components a couple of seconds to find the component named StatusBar. A status bar is usually located at the bottom of most Windows applications and displays the status of an application. Place this on the form. You will see that the component falls to the bottom of the form. Now double-click on the Button component on the form (Object Inspector will display Button1) to set an event. Enter the following code: void __fastcall TForm1::Button1Click(TObject *Sender) { if(OpenDialog1->Execute()) Image1->Picture->LoadFromFile(OpenDialog1->FileName); StatusBar1->SimpleText = OpenDialog1->FileName; }
Let’s pause to explain what’s going on. The window you now see is the Source Code window. It can always be accessed by the Project Manager or by the Object Inspector. The Source Code Edit window is where you will type in source code for your program. Now let’s set focus on our form. To access our form, we can use the Speedbar tool button named Toggle Form/Unit. It looks like a form and a piece of paper with arrows pointing toward the form on both sides. If you hold your cursor over this button, it will display Toggle Form/Unit (F12). Another way to toggle between forms and code is with the F12 key. Press F12 now to set focus on the form. Click one time on Button2 (Object Inspector will display Button2) on the form and set the caption to Close. Double-click on that button and enter the following code: void __fastcall TForm1::Button2Click(TObject *Sender) { Close(); }
Now focus on your form again by moving the source code editor out of the way and clicking on your form. Doesn’t look like much, does it? Soon you will see how much C++Builder has done for you with a little bit of code! Before running this application, arrange your buttons and other components nicely to give a clean look. Use the white arrow selector on the left side of the Component Palette to select any component. You will not be able to move the StatusBar component at the bottom of the form, so you can skip that one. Press the green Run arrow, or choose Run, Run from the main menu. C++Builder should compile the program if you did not have any typos. Your program should appear with two buttons with the captions Get Picture and Close. Choose the Get Picture button.
03 9721 CH01
11/13/00
9:42 AM
Page 19
Introduction to C++Builder CHAPTER 1
After selecting the file, press OK. You should see a picture of the Windows Setup bitmap. At the bottom of the form, the name of the file will be in the status bar. You have written your first application that really works, and with as little code as possible! Let’s explain what really happened, so that you can get a feel for the components. First you placed an Image component on the form. This component enables you to display .BMP files within your program. With this component, C++Builder takes all the grunt work out for you. If you wanted to display a graphic file in Windows, you would first need pages and pages of code just to load the palette of colors the BMP has. You would also have to perform error checking against the BMP structure it fills in, error check to make sure it’s a .BMP file, set the resolution, and much more, just to display the picture itself. C++Builder has already done this for you. All you have to do is drop the component on the form and set its properties. You then set the stretch property, which stretches the picture to match the size of the component you placed on the form. For larger pictures, resize the component using your mouse. After dropping the Image component on the form, you also placed an OpenDialog box. The OpenDialog component lets the user select a file to open under a directory. In this case, we used the OpenDialog component to select the filename of the image to open. The Image component determines if it is a graphic file, reads it in, and displays its contents. We set the Filter property on this component to find all files that end in the .BMP extension, which describes the file type we are opening. It will also display this file type in the OpenDialog box as the type to open. The | is a separator to tell C++Builder there’s another parameter coming for this type of property. *.BMP tells the OpenDialog box to open only files that end in .BMP. If you want to see how this looks or change the filter settings during designtime, go to Form1 and select the OpenDialog component by clicking it. Then go to the Object Inspector. In the Filter section, double-click within the text area and a table will come up that displays what I just described. After that you placed two Button components. We also set the captions of the Button components, which identifies what the buttons do. Then we set events for the Button components. You can also set other events with the Object Inspector. The Object Inspector has an Events tab that lists the events a component has. To set off a particular event, simply double-click in the text area of that event. If you were to write this whole program using barebones code, you would produce at least 20 pages of code. The idea of this chapter is to show you the basics and to demonstrate how quickly you can use C++Builder.
1 INTRODUCTION TO C++BUILDER
An Open dialog box will appear asking for a file with the .BMP extension. Go to your Windows directory under your C: drive and select SETUP.BMP or another file that has a .BMP extension.
19
03 9721 CH01
20
11/13/00
9:42 AM
Page 20
C++Builder 5 Essentials PART I
Let’s write another program using different components. This will give another example of RAD technology within C++Builder.
NOTE Go ahead and close the project. You do not have to save it. Choose File, Close All from the main menu and answer No.
From the main menu, select File, New Application. A new project will be created. Let’s save this project by selecting File, Save Project As from the main menu. Give the form’s source code a name. Change the default Unit1.cpp to Mainfrm.cpp. After saving the form’s source code, the project source code will appear. By default, it is Name it Proj.bpr.
Project1.pbr.
Place two ListBox components on the form. The list boxes are in the Standard tab in the Component Palette. Don’t worry about placing them in a specified location, but do align them next to each other. C++Builder creates them as ListBox1 and ListBox2. Drop an EditBox and two Buttons below the list boxes. They are also located in the Standard tab in the Component Palette. Align them any way you want. C++Builder creates the buttons as Button1 and Button2. It also creates an Editbox named Edit1. Select Button1. In the Object Inspector, select the Caption property. Change the caption to ADD. Select Button2. In the Object Inspector, select the Caption property. Change the caption to REMOVE. Select Edit1. In the Object Inspector, select the Text property. Remove the string within the Text property. Drop a Label component under the Edit1 edit box. In the Object Inspector, select the Caption property. Enter Friends’ Names. To select the form itself, click anywhere on the form, but not on any component. You can also do this inside the Object Inspector by selecting Form1 in the drop-down box. Select the Events tab in the Object Inspector and look for the OnCreate event. Double-click inside the property setting for this event. C++Builder will now create the following code for the event handler: void __fastcall TForm1::FormCreate(TObject *Sender) { }
03 9721 CH01
11/13/00
9:42 AM
Page 21
Introduction to C++Builder CHAPTER 1
void __fastcall TForm1::FormCreate(TObject *Sender) { ListBox1->Items->Add(“David Sexton”); ListBox1->Items->Add(“Randy Kelly”); ListBox1->Items->Add(“John Kirksey”); ListBox1->Items->Add(“Bob Martling”); }
Switch back to the form and double-click on Button1. This is the button with the Add caption. An event will be created. Type the following inside the event handler: void __fastcall TForm1::Button1Click(TObject *Sender) { String GetListItem = ListBox1->Items->Strings[ListBox1->ItemIndex]; ListBox2->Items->Add(GetListItem); }
Switch back to the form, and double-click on Button2. This is the button with the Remove caption. C++Builder will create an event. Type the following inside the event handler: void __fastcall TForm1::Button2Click(TObject *Sender) { ListBox2->Items->Delete(ListBox2->ItemIndex); }
Switch back to the form and select the Edit1 edit box. From the Object Inspector, choose the Events tab. Find the OnKeyPress event. Double-click in the empty area for this event, and the event handler will be created by C++Builder. Type the following code in the event handler: void __fastcall TForm1::Edit1KeyPress(TObject *Sender, char &Key) { if (Key==13) { ListBox2->Items->Add(Edit1->Text); ListBox1->Items->Add(Edit1->Text); } }
Now let’s save the project we are working on. Remember to always save your work! Select File, Save All from the main menu. After saving the project, let’s make the project. Press Ctrl+F9 or choose Project, Make Proj.exe. This will create the executable. If there are any errors, check for typos.
1 INTRODUCTION TO C++BUILDER
You need to place code inside this event. It is triggered when the form is created at runtime. This means that when you run this program, Windows will create the form and execute any code within the event. Type the following inside the event handler:
21
03 9721 CH01
22
11/13/00
9:42 AM
Page 22
C++Builder 5 Essentials PART I
Let’s run it and see what happens. Press the green arrow to run the application we just created. You can also choose Run from the main menu as well. I will explain how the code works in a minute. The application should appear as a regular window with two list boxes with names in them. There should also be two buttons on the form. The edit box (which is our Edit1 component) will have the cursor in it ready for us to type. Enter your friends’ names. As soon as you press Enter, each name will be added not only to the first list box but also to the second list box. Select one of the names and press the Add button. The name you selected will appear in the next list box beside it. If you press Remove, the name will disappear from the second list box only. Select the names and add them to the other list box. Then remove the names. As you see, we added an event for FormCreate. This event executes after the form’s creation. In this event you will see that there are some strings to be added inside ListBox1. The strings are added to the items of the list box. The next event we set is under Button1. We created a string from the String class named which equals the item that was selected by the user. How did this event know that the item was selected? It didn’t. It read the item’s index. If there was no selection, it would be null. The next line adds the string from the index of ListBox1. GetListItem,
Button2’s
event is smaller than the first. It gets the index of the item inside ListBox2 and
deletes it. For our third event, we used an OnKeyPress for the Edit1 edit box. When a user enters data and presses a key, the event is triggered, executing code inside it. This particular event scans for the Enter key, which is equal to 13. Of course, we could have used the VK_ENTER value that C++Builder defines as the Enter key. The if statement checks the passed parameter of Key to see if this is true. If it is, the code inside the if statement executes and adds the string within the edit box to both list boxes. We created three events, and the code is pretty small. We also put several components on the form without any code at all. We have a working program with minimal code. As you can see, you built this application in just a few minutes. Once you get used to the Component Palette, the Object Inspector, and some operations within the IDE, you’ll be on your way to learning the ins and outs of C++Builder. If you compare the time required to develop applications using other compilers, such as Visual C++ or Microsoft Foundation Classes (MFC), you will see that C++Builder is far superior to the others.
03 9721 CH01
11/13/00
9:42 AM
Page 23
Introduction to C++Builder CHAPTER 1
How to Get Around This section gives the answers to some commonly asked questions about the C++Builder compiler. • How do I look at a project’s source code and other source code for each form? You can do this by using the Project Manager and the Project menu item in the main menu. Project, View Source will display the main entry source for the starting application. If you want to view source files from other forms, includes, or resource files, use the Project Manager. To bring up the Project Manager, select View, Project Manager or press Ctrl+Alt+F11. • How do I change properties on a component? You can do this with the Object Inspector. To bring it up, press F11 or choose View, Object Inspector. In the Object Inspector, select your component and the properties will be loaded within the Object Inspector. From there you can set events and change properties. • I am trying to arrange my components precisely, but I am having difficulty. Can I have more control over the alignment? To move a component to the exact location you want, press Control and use the arrow keys to move the component. This will give you the exact pixel alignment you need within C++Builder. • I just compiled and ran my application, but now it seems to be locked up. A bunch of weird windows popped up and I do not know what to do. How can I stop this madness? You can get C++Builder to reset the program. Press Ctrl+F2 or select Run, Program Reset. This will kill your program completely and take away those nasty whatever-theyare-windows, and you’ll be back to your code. • I compiled my first program, but I want to create my own icon and include it in my program. How do I do this? First, use the trusty and wonderful Image Editor. Open it by selecting Tools, Image Editor in the main menu. Simply create a new icon from there and save it. Then, select Project, Options. The Project Options dialog will appear. Select the Application tab and press the Load Icon button. Locate your icon and press OK. Then you will have to rebuild your project. Compiling or making your project will not do it. You will have to rebuild all by choosing Project, Build All Projects from the main menu. After that, your application will contain your new icon!
1 INTRODUCTION TO C++BUILDER
Explore the menu items under C++Builder. The online help will also guide you through the menu items, Object Inspector, and some other options within the IDE.
23
03 9721 CH01
24
11/13/00
9:42 AM
Page 24
C++Builder 5 Essentials PART I
• Every time I compile an application, my form has the name Form1. How do I change this? Remember the Object Inspector? We talked about this for setting properties for components. You can also use it to set properties for your forms. Use the Caption property attribute to change a form’s title. Try experimenting with the Object Inspector for different results! • I am tired of choosing the menu items for something simple. Isn’t there an easier way? Yes—it’s called the Speedbar, and it is located right above the Object Inspector (by default). If you want, for example, to create a new application object, press the button with the image of a white piece of paper. If you do not know what those buttons are, hold your cursor over one for a couple of seconds, and a helper will appear to tell you what the button is. • I have all my components in place and do not want to move them. Sometimes I accidentally move them by mistake. Is there a way I can keep these components still? Yes, you can do this by choosing Edit, Lock Controls from the main menu. This will lock all controls on the form.
What’s New in C++Builder 5 As with the staggered releases of new versions of Borland C++Builder and Borland Delphi in the past, C++Builder 5 introduces new features first seen in Delphi 5 and then adds some more. There are many new features and enhancements in the areas of Web programming, distributed application development, team development, application localization, debugging, database application development, and developer productivity, among others. Most of the new features and enhancements are covered in more detail throughout this book. C++Builder 5 is available in three versions: Standard (Std), Professional (Pro) , and Enterprise (Ent). Standard has the fewest features but is still a powerful development environment for Windows programming and includes over 85 components for RAD programming, the awardwinning compiler, advanced debugger, and more. The Professional version has over 150 components and adds features including the new CodeGuard™ tool, multiprocess debugging, and standard database functionality. The Enterprise version has over 200 components, including Internet Express, CORBA development, Microsoft SQL Server 7 and Oracle 8i support, MIDAS development, a full suite of internationalization tools, TeamSource version control manager, and more. Missing from C++Builder 5 is Merant (formerly Intersolv) PVCS Version Control.
03 9721 CH01
11/13/00
9:42 AM
Page 25
Introduction to C++Builder CHAPTER 1
NOTE
You can also see information on the new features in the “What’s New” section of the C++Builder online help.
The features listed in the following sections are available in the Professional and Enterprise versions of C++Builder 5 and not in the Standard version, except where noted. In the remainder of the book, we will not make a distinction of which versions of C++Builder the new features apply to. Consult the full-feature matrix if necessary.
Web Programming Most people know that one of C++Builder’s strengths is in developing Web and Internet applications. In C++Builder 5, Borland has added the Active Server Page (ASP) Application Wizard, Internet Express (Ent), a new Web browser component, and WebBroker enhancements. The new Internet Express (Ent) allows you to build “thin” clients for the Web by presenting data from MIDAS servers and back-end databases using XML and HTML 4. Thin clients are smaller in size than regular (or “fat”) clients. They do not access the database directly and therefore do not require the Borland Database Engine (BDE) to be installed on the client machine. WebBroker is now available in the Professional version of C++Builder 5 (previously Enterprise only) and is now able to preview with HTML 4 support. The last main Web programming addition to C++Builder 5 is the new Web browser component, which allows you to integrate HTML browsing into your applications. This seemingly simple feature can be used in many ways, such as to • Create your own customized Web browser. For instance, develop one for your company that allows users to view Web pages only within the company’s intranet. • Create the user interface for your program in HTML. This has several benefits. The user interface can now be created by nonprogrammers. If there are Web page developers in your organization, they can assist in application development. Additionally, you can dynamically customize the user interface for a particular user’s level of experience simply by loading a different HTML file. The user interface can also be customized for a particular client. Shipping a new version of the user interface is as easy as downloading an HTML file, something that browsers do extremely well.
1 INTRODUCTION TO C++BUILDER
The full-feature matrix highlighting all of the new features in each version (Standard, Professional, Enterprise) of C++Builder 5 is available from the “Feature List” link on the C++Builder Web site at http://www.borland.com/bcppbuilder/.
25
03 9721 CH01
26
11/13/00
9:42 AM
Page 26
C++Builder 5 Essentials PART I
• Integrate your help files using HTML, making the creation of help files as simple as creating Web pages. Several products, such as Microsoft Encarta and Microsoft Office 2000, use a Web browser control in these ways. America Online (AOL) and CompuServe use Web browser controls to create their customized browser applications.
Distributed Applications CORBA (Ent) now supports the VisiBroker ORB v4.00 and CORBA 2.3–compliant clients and servers. Other new CORBA features include the Portable Object Adapter and Objects By Value. There are enhancements to the Visual TypeLibrary Editor, Interface Repository, CORBA Wizards, and more. MIDAS (Ent) now has XML data packet support, a stateless DataBroker, a new Web Connection component, server object pooling, and provider options.
Team Development A great new feature in C++Builder 5 is TeamSource. It is included in the Enterprise version and can be purchased separately for the Professional version. TeamSource is a front-end version control manager that allows parallel development of projects. Developers work on their own local copies of the project. In most cases, TeamSource automatically handles the merging of changes. A visual compare utility simplifies the task of resolving conflicts. TeamSource tracks version history and allows a particular state of the project to be bookmarked. The actual back-end file versioning is performed by separate version control software. C++Builder 5 (Ent) includes the Borland ZLib back-end version control software and also supports an interface to Merant PVCS (purchased separately). Plug-ins can be written for TeamSource to support other back-end version control software.
Application Localization New to C++Builder 5 Enterprise are the Translation Suite, Translation Repository, RC Translator, and DFM Translator. These features can also be purchased separately for C++Builder Professional. They allow you to internationalize or localize your applications for new languages and cultures, simultaneously developing your application for multiple locales. The Resource DLL Wizard has been enhanced to allow you to translate your applications to new languages easily.
03 9721 CH01
11/13/00
9:42 AM
Page 27
Introduction to C++Builder CHAPTER 1
27
Debugging
1
Perhaps one of the best new features that most C++Builder programmers can benefit from is CodeGuard. CodeGuard is a runtime error-detection tool that can locate and diagnose memory and resource leaks. In addition, some other new debugging features are an FPU/MMX View and breakpoint actions (all versions) and groups (all) for programmable breakpoint control and managing multiple breakpoints at once.
INTRODUCTION TO C++BUILDER
Database Application Development There are several new database features in C++Builder 5, including the DataModule Designer, InterBase Express, and ADO Express. The DataModule Designer, first seen in Delphi 5, allows you to see and design the parent-child relationships among data-access components in the tree view and entity relationship dependencies (including master-detail) in the Data Diagram view. With InterBase Express, you can develop high-performance database applications using InterBase that don’t require the Borland Database Engine (BDE). It comes with the new SQL monitor for advanced data access debugging. ADOExpress is a new set of components that allows you to access relational and non-relational databases using Microsoft’s ActiveX Data Object (ADO) and OLEDB technology. Using ADO, you can access standard databases, email, file systems, spreadsheets, and other information. ADOExpress applications don’t require the BDE. In addition to these new features, C++Builder 5 now has InterBase 5.6, MS SQL Server 7, and Oracle 8i support.
Developer Productivity There are some great new features in the area of developer productivity. Multithreaded background compilation (all) allows you to continue working while the compiler is doing its stuff. Custom Desktop Settings (all), with auto-debug mode switching, allows you to arrange and save different IDE desktops just the way you want. You can set a separate layout for debugging applications and, as you switch in and out of debugging, the desktop changes your layout automatically. An integrated to-do list manager lets you add simple task reminders with a comment and priority to your projects. A to-do item can appear in your code as a reminder to come back to something that needs to be finished. There’s a new to-do list view to manage those items. Several wizards have been added, including a Windows 2000 Client Logo Application Wizard (all), Console Application Wizard (all), Control Panel Applet Wizard, and simple C and C++ Application Wizards (all).
03 9721 CH01
28
11/13/00
9:42 AM
Page 28
C++Builder 5 Essentials PART I
There is also enhanced Microsoft Visual C++ support, including Visual C++ 6.0 projects, Microsoft Foundation Class (MFC) 6.0, and Active Template Library (ATL) 3.0 support. There are many more developer-productivity features and enhancements new to C++Builder 5, including per-file project options override (all), property categories (all), and property images (all) in the Object Inspector; programmable code editor key mappings (all); new components (all); and the capability to import automation servers as components.
Companion Tools CD-ROM The C++Builder 5 companion tools CD-ROM (Pro/Ent) contains more than 40 useful thirdparty tools. Some are full versions, and some are shareware and trial versions. Some of the full and free tool highlights are • Debug Server 1.1 from Elitedevelopments is a COM/DCOM-based debugging output manager that lets you centrally collect, manage, and display debug messages from your applications. • Morfit’s 3D Engine SDK 3.0 and WorldBuilder 3.1 allow you to incorporate 3D graphics into your applications and develop arcade-quality games. • GExperts is an open-source product that contains dozens of IDE enhancements to speed development. Several “Experts” are included to navigate, search, and transform your code. GExperts also provides shortcuts for common programming tasks, quick access to information relevant to C++Builder developers, and extensive customizing of the IDE.
Upgrading and Compatibility Issues C++Builder 5 was officially released on March 22, 2000. At the time of the writing of this book, there are several known compatibility problems in C++Builder 5 with existing projects and third-party tools, and there are unresolved bugs. An update (patch) is not yet available. The C++Builder 5 bug list can be found on the Borland C++Builder Web site via the “Updates and Patches” and then “Investigate Bugs” links at http://www.borland.com/bcppbuilder/. Sign up for the C++Builder tech alert from the Borland Web site to get the latest information on known issues and update releases. You should also read the README.TXT files in the root folder of the C++Builder installation CD-ROM for known issues at the time of release.
Upgrading C++Builder from an Earlier Version C++Builder 5 can coexist with earlier versions of C++Builder on your machine, but shared applications such as the BDE will be upgraded to the latest version. C++Builder 5 can also coexist with Delphi installations. The same general rules apply.
03 9721 CH01
11/13/00
9:42 AM
Page 29
Introduction to C++Builder CHAPTER 1
Using Existing Projects in C++Builder 5 When you load a project created in a previous version of C++Builder, the project is automatically converted to work with C++Builder 5. The project file is updated to XML format, and the earlier VCL library versions listed in it are updated to list C++Builder 5 libraries. Some slight changes to the USE* clauses in the main project file source are also made. You may have written version-specific code into your projects or used third-party components that do. Changes to properties and methods in the VCL between different C++Builder versions have typically necessitated the use of #ifdef VERXXX clauses to compile the correct usage. If any of the version-specific code is testing explicitly for C++Builder 4 or earlier versions without a “catch-all” for forward compatibility, you may need to update the clauses. For example, if the code tested #ifdef VER125 for C++Builder 4–specific code, you will need to add either an #ifdef VER130/#endif or an #else catch-all to specify the C++Builder 5–specific code. If you don’t have the source code, usually because it is a third-party component or package, then you will need to contact the vendor to get an update.
Creating Projects Compatible with Previous Versions of C++Builder If you need to create projects that can be used with earlier versions of C++Builder, there are several areas in which care must be taken. In particular, you cannot use new features in forms and project source code, such as new properties, methods, or events. There are several new project, compiler, and linker options that should not be used. The help file contains detailed information on the new features added to C++Builder 5. In C++Builder 5, form files are stored as text rather than binary as in previous versions. To save a form as binary, right-click on the form and uncheck Text DFM. If you want all forms created in the future to be saved as binary, uncheck the New Forms as Text option on the Preferences page of Tools, Environment Options. As shown in detail in Chapter 2, “C++Builder Projects and More on the IDE,” project files are now saved in XML format. The project file in previous versions of C++Builder used a makefile format. You can export a makefile format project file by using the Project, Export Makefile option. You will need to rename the makefile with a .bpr extension and make several changes
1 INTRODUCTION TO C++BUILDER
If you have no use for the previous version of C++Builder, then you should uninstall the previous version before installing C++Builder 5. You should also read the INSTALL.TXT and README.TXT files in the root folder of the installation CD-ROM for further installation and upgrading notes.
29
03 9721 CH01
30
11/13/00
9:42 AM
Page 30
C++Builder 5 Essentials PART I
to the options listed in the file, including the C++Builder version number, set in the VERSION option, and VCL file versions listed in the LIBRARIES, SPARELIBS, PACKAGES, and CFLAG1 options. Converting the project file to previous versions is a bit involved; there are other changes that may be necessary. The to-do list is a new feature in C++Builder 5. However, the in-source to-do list items and global project to-do file require no changes for support in previous versions. If you see the message Error reading symbol file when opening a C++Builder 4 project, you should rebuild the application. C++Builder 4 symbol files are not compatible with C++Builder 5.
Solving Other Project Upgrading Issues There are several new method parameters, data values, classes, and VCL behaviors in C++Builder 5 that you should be aware of. Some of the main changes are listed here. •
TPoint is no longer a structure. The initialization syntax has changed from using braces ({ and }) to formally typecasting two coordinates as a TPoint. See “TPoint, compatibility issues” in the online help.
•
TPropertyEditor
•
TComponentList
•
The sprintf() method of the AnsiString class now overwrites the current contents of the string rather than append to the existing string. A new cat_sprintf() method provides the old behavior.
•
TCppWebBrowser
TPoint
The parameter list for the TPropertyEditor constructor has changed to TPropertyEditor(const di_IFormDesigner ADesigner, int AropCount.
There is a new class named TComponentList to store and maintain a list of components. It is defined in the include file cntnrs.hpp. See the online help for information on how to use TComponentList.
AnsiString sprintf
and THTML The TCppWebBrowser component on the Internet page replaces the THTML component from Netmasters. See the online help for upgrading notes.
• MIDAS Several changes have been made to MIDAS. See the online help for additional information. There are many more upgrading issues in C++Builder 5. The “Upgrading to Borland C++Builder 5” book on the Contents tab of the online help contains detailed information.
Migration from Delphi This section is not to teach you C++, but to get you started with programming in C++Builder, if you know how to program in Delphi. The first thing you should note when programming in C++Builder is that it is case sensitive. It may look like a hard thing to do if you have been programming in Delphi for a long time, but you will get used to it.
03 9721 CH01
11/13/00
9:42 AM
Page 31
Introduction to C++Builder CHAPTER 1
1
CAUTION
Here we will compare Delphi and C++Builder in the major programming areas. At the end of this discussion, you should be able to translate most of your Delphi code to C++Builder without any trouble.
Comments The first thing you need to know is how to enter comments in C++Builder. Table 1.1 compares Delphi to the C++Builder way of creating comments. Delphi-to-C++Builder Comments Comparison
Delphi
C++Builder
{ My comments in Delphi }
/* My comments in C++Builder */
(* Another comment in Delphi *) // One line of comments
// One line of comments
Variables As in Delphi, in C++Builder you must declare the variable type before you can use it. Table 1.2 compares Delphi variables with those of C++Builder. The table also shows the number of bytes that each type requires. TABLE 1.2
Delphi-to-C++Builder Variables Comparison
Type
Size (bytes)
Delphi
C++Builder
Signed integer
1 2 4 8
ShortInt
char
SmallInt
short, short int
Integer, LongInt
int, long
Int64
__int64
INTRODUCTION TO C++BUILDER
Unlike Delphi, the code in C++ is case sensitive.
TABLE 1.1
31
03 9721 CH01
32
11/13/00
9:42 AM
Page 32
C++Builder 5 Essentials PART I
TABLE 1.2
Continued
Type
Size (bytes)
Delphi
C++Builder
Unsigned integer
1 2 4
Byte
BYTE, unsigned short
Floating point
Variant
Word
unsigned short
Cardinal, LongWord
unsigned long
4 8
Single
float
Double
double
10 16
Extended
long double
Variant,
Variant,
OleVariant,
OleVariant, VARIANT
TVarData
Character Dynamic string Null-terminated string Null-terminated wide string Dynamic 2-byte string Pointer Boolean
1 2 -
Char
char
WideChar
WCHAR
AnsiString
AnsiString
PChar
char *
-
PWideChar
LPCWSTR
-
WideString
WideString
8 1
Pointer
Void *
Boolean
bool
The table covers most variables. For more information, refer to the C++Builder help.
Constants There are two ways to declare a constant in C++Builder. The old way is to use the preprocessor directive #define like this: #define myconstant
100
The new and safer way of defining constants is by using the const keyword, as follows: const int myconstant = 100; myconstant
is again declared as a constant, but this time it’s declared as an integer.
03 9721 CH01
11/13/00
9:42 AM
Page 33
Introduction to C++Builder CHAPTER 1
Operators
1
Delphi-to-C++Builder Assignment Operators Comparison
Operator
Operator Type
Delphi
C++Builder
Assignment
Assign Add then Assign Subtract then Assign Multiply then Assign Divide then Assign Modulus then Assign Bitwise And then Assign Bitwise Or then Assign Bitwise Xor then Assign Bitwise Shl then Assign Bitwise Shr then Assign Equal Not equal Greater than Greater than or equal to Less than Less than or equal to Add Subtract Multiply Divide float values Divide integer values Modulus And Or Not
:= None None None None None None None None None None = <> > >= < <= + * / div mod and or not
= += -= *= /= %= &= |= ^= <<= >>= == != > >= < <= + * / / % && || !
Comparison
Arithmetic
Logical
INTRODUCTION TO C++BUILDER
This section explains C++Builder operators, and how you can convert Delphi operators to C++Builder. Table 1.3 lists operators types in Delphi and their counterparts in C++Builder. TABLE 1.3
33
03 9721 CH01
34
11/13/00
9:42 AM
Page 34
C++Builder 5 Essentials PART I
TABLE 1.3
Continued
Operator
Operator Type
Delphi
C++Builder
Bitwise
and or xor not Shift to the left Shift to the right Deceleration Dereferencing Address of variable References Class declaration Structure declaration Scope resolution Direct access Indirect access Increment Decrement
And Or Xor Not Shl Shr ^Type pointer^ @Variable var class record . . None Inc() Dec()
& | ^ ~ << >> Type* *Pointer &Variable Type& class struct :: . -> ++ --
String quoting
‘
“
Pointers
Class
Others
Assignment Operators In C++ you can combine different operators with the assign operator (+=). For example, the following x = x + 2;
can be written as follows: x += 2;
Increment and Decrement Operators In C++ you can increment or decrement variables using prefix or postfix methods. The following is an example of how to use both: x = ++y; // prefix x = y++; // postfix
03 9721 CH01
11/13/00
9:42 AM
Page 35
Introduction to C++Builder CHAPTER 1
int x=5, y=5; x = ++y; // at this point x=y=6 // … later on x = y++; // now x=6 and y=7
Conditional Operators The conditional operator (?:) is the only operator in C++ that takes three expressions and returns a value. It is written as follows: (expression1) ? (expression2) = (expression3)
The operator returns expression2 as a value when expression1 is true. Otherwise it returns expression3. For example return ((a >b)? c : d);
This can be written using an if-else statement as follows: if (a>b) return (c); else return (d);
Pointer Operators The indirection (*) operator is used to declare pointers. The same symbol is also used to dereference a pointer, and in this case it’s called a dereference operator. Dereferencing a pointer is the way to retrieve the value it is pointing to. The compiler is clever enough to tell the difference. Look at the following example: int x, y = 8; int* ptr = &y; // ptr declared and initialized to hold the address of y x = *ptr; // dereferencing ptr.
Here is how you used to do the same thing in Delphi: var x,y : Integer; ptr : ^Integer; begin y:=8; ptr := @y; x := ptr^; end;
1 INTRODUCTION TO C++BUILDER
The first statement tells the compiler to increment y first, then assign x to the new incremented value of y. The second statement is the opposite. It first assigns x to y then increments y. The following example should clarify:
35
03 9721 CH01
36
11/13/00
9:42 AM
Page 36
C++Builder 5 Essentials PART I
The (*) and (&) operators in C++ are equivalent to the (^) and (@) operators in Delphi, respectively. The (&) is called the address-of operator; it is used to retrieve the address of a variable in memory. The (&) operator is also used to declare references. A reference is just a special case of a pointer, and you can treat it as a regular object. References are very useful when used to pass parameters to functions. In Delphi, parameters passed by reference are also called variable parameters. In this case, the (&) reference operator in C++ is equivalent to the var keyword in Delphi.
The new and delete Operators You can always declare variables like this: char buffer[255];
is now allocated on the stack, and this type of variable is called a local variable. There are two problems with local variables. First, they are destroyed when the function returns. Second, the memory size that can be allocated on the stack is limited.
buffer
You can solve these problems by allocating memory from the heap, as follows: char* buffer; buffer = new char;
Or you can write it in one line, such as char* buffer = new char;
Now you have a variable that can be used everywhere in your program with the size you want. The only thing you need to remember is that any memory allocated needs to be freed. This is done with the delete operator, as follows: delete buffer;
Class Operators There are two ways to access data members and member functions of a class, by using direct or indirect access. See the “Classes” section, later in this chapter, for more detail.
Controlling Program Flow As in Delphi, C++Builder has several structures for conditional branching and looping. These structures are •
if-else
•
switch
statements
statements
03 9721 CH01
11/13/00
9:42 AM
Page 37
Introduction to C++Builder CHAPTER 1 for
loops
•
while
1
and do-while loops
C++Builder also has break and continue commands, which are quite similar to the ones in Delphi to break or continue the flow of execution. Table 1.4 shows examples of how to use conditional branching and looping structures in Delphi and their counterparts in C++Builder. TABLE 1.4 Statement if-else
switch
for
Delphi-to-C++Builder Conditional and Loop Statement Comparison
Delphi
C++Builder
if (i = val) then
if (i == val)
begin statement1; ...; end;
{ statement1; ...;}
if (i = val1) then
if (i == val1)
begin statement1; ...; end
{ statement1; ...;}
else if (i = val2) then
else if (i == val2)
begin statement1; ...; end
{ statement1; ...;}
else
else
begin statement1; ...; end;
{ statement1; ...;}
case <Expression> of
switch (<Expression>){
val1 :
case Val1:
statement;
statement; break;
val2 :
case Val2:
statement;
statement; break;
…
…
else statement;
default : statement;
end;
} // end switch
// end case
for i := val1 to val2
do
for(i=val1;i<=val2;i+=inc)
begin statement1; ...; end;
{ statement1; ...;}
// Increment by 1 only.
// Increment by any value // depends on inc value.
for i:=val1 downto val2 do
for(i=val1;i>=val2;i-=dec)
begin statement1; ...; end;
{ statement1; ...;}
// Decrement by 1 only.
// Decrement by any value // depends on dec value.
INTRODUCTION TO C++BUILDER
•
37
03 9721 CH01
38
11/13/00
9:42 AM
Page 38
C++Builder 5 Essentials PART I
TABLE 1.4
Continued
Statement
Delphi
C++Builder
while
while i = val do
while (i == val)
begin statement1; ...; end;
{ statement1; ...; }
repeat
do
statement1;
{
… …
statement1;
…
until (i = val)
} while
do-while
(i != val);
The following are some tips on how to use these statements and commands: • You need to place a break keyword at the end of each case statement to stop execution. Failing to do so will cause the execution of the statements that come under later case statements until it reaches the break statement or the end of switch. This seems odd, but it has its usefulness sometimes. • The selector expression in the switch statement won’t accept non-ordinal types such as strings. • The for(;;) loop is equivalent to a while(true) or while(1) loop. All are infinite loops. To quit such loops, use the break keyword.
Functions and Procedures As in Delphi, a function in C++Builder must be declared (prototyped) before it can be used. Unlike Delphi, C++Builder has no special keywords that are used to declare functions such as function and procedure. Table 1.5 shows examples of function declarations in both Delphi and C++Builder. TABLE 1.5
Delphi-to-C++Builder Function Declaration Comparison
Delphi
C++Builder
procedure Add;
void Add();
procedure Add(x, y: Integer);
void Add(int x, y);
function Add: Integer;
int Add();
function Add (x, y: Integer): Integer;
int Add (int x, y);
03 9721 CH01
11/13/00
9:42 AM
Page 39
Introduction to C++Builder CHAPTER 1
The main() function takes two parameters, argc and argv, and returns an integer value. It is unlike other functions in that it can’t be called directly; it is called automatically when the program starts running. Functions in C++ use an opening brace ({) to begin and a closing brace (}) to end. These braces are equivalent to Delphi begin and end keywords. In Figure 1.4 you can see that C++Builder uses the return keyword to return values. It will also cause the termination of the called function (main() in this case). You need to be careful here. Unlike Delphi’s Result keyword, return will terminate the function immediately, so any statements coming after it will be ignored.
FIGURE 1.4 The C++Builder code editor window for a new console application.
1 INTRODUCTION TO C++BUILDER
Any C or C++ program must have a main() function that will be the entry point of the program. For GUI applications it is called WinMain(). main() functions just like any other function: It takes parameters and returns values. Figure 1.4 shows the main() function when you create a new console application. Note that you can create a console application by choosing File, New, Console Wizard.
39
03 9721 CH01
40
11/13/00
9:42 AM
Page 40
C++Builder 5 Essentials PART I
Classes Delphi and C++Builder have the same way of controlling the access of functions. They hide these functions in structures called classes. Collecting functions in a class to accomplish a specific task is called encapsulation. From C, C++ inherited the struct keyword, which is almost identical to class. The only difference between structures and classes is that structure data members and member functions are public by default, whereas a class’s default is private. As in Delphi, a class in C++Builder has the following features: • Constructors and destructors • Access control to data members and member functions • A pointer called this, which is equivalent to self in Delphi One of the most powerful features of classes in C++ is that they can be built using multiple inheritance. Multiple inheritance is when a class is derived from two or more base classes. In Delphi (actually Pascal), classes are derived from a single base class. Let’s look at some of the class syntax for various types of inheritance: • No inheritance class MyClass { // no inheritance. // Private by default so anything declared here is private. private: // Private declaration protected: // Protected declaration public: // Public declaration }; // MyClass is terminated by semicolon.
• Single inheritance class MyClass: BaseClass1 {...};
• Multiple inheritance class MyClass: BaseClass1, BaseClass2, {...};
BaseClass3
Constructors and Destructors Every class has a constructor and a destructor. If the programmer does not write a specific constructor or destructor for the class, the compiler will create default ones.
03 9721 CH01
11/13/00
9:42 AM
Page 41
Introduction to C++Builder CHAPTER 1
A class can have more than one constructor, but it has only one destructor. Constructors and destructors are just like any other member functions of the class, but they have some special features: • They don’t return values (not even void). • The constructor takes the name of the class, and the destructor takes the name of the class preceded by the ~ symbol. For example class A { Public: A(); // Constructor ~A(); // Destructor }
• Constructors can take parameters as needed, but destructors take none. • They can’t be called as a normal function.
Accessing Data Members and Member Functions Assume you have the following simple class: class Rabbit { Private int speed=20; };
Later, you can declare it in your program using one of the following methods: Rabbit Rabbit1; Rabbit* Rabbit2 = new Rabbit; Rabbit1 is created on the stack, and Rabbit2 is created on the heap. You can access the speed data member for both of them using the direct access operator, as follows: Rabbit1.speed=30; (*Rabbit2).speed=30;
To make it easier to access data members of classes such as Rabbit2, C++ introduced the arrow operator (->). The above statement can be written as Rabbit2->speed=30;
The this Pointer Like the self pointer in Delphi, this is a hidden data member in all C++Builder classes. The following is an example of how to use the this pointer:
1 INTRODUCTION TO C++BUILDER
The constructor’s main purpose is to allocate space for the class and to initialize class data members. The destructor’s job is to free the allocated memory.
41
03 9721 CH01
42
11/13/00
9:42 AM
Page 42
C++Builder 5 Essentials PART I __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { TLabel *Label1 = new TLabel(this); Label1->Parent = this; Label1->Caption = this->Width; // which is equivalent to Form1->Width; }
Preprocessor Directives Preprocessor directives are special instructions for the compiler. They are specified on a line that starts with the # symbol. Preprocessor directives are usually placed at the beginning of a unit. Many directives can be placed just about anywhere in the unit, but certain directives must be placed at a particular position within the unit, such as the #pragma argsused directive, which must be placed before the definition of the function to which it applies. We will look at the #define and #include preprocessor directives.
The #define Directive As shown in the “Constants” section, earlier in this chapter, the preprocessor command #define can be used to declare constants. The #define command is also used to create macros. Macros are just like any other function and can have parameters as follows: #define MAX(A,B) ((A)>(B)?(A): (B) #define MIN(A,B) ((A)>(B)?(B): (A)
You use them as follows: int x = y = max
x, y, max ; 5; 6; = MAX(x,y); // Remember that C++Builder is case sensitive.
The #include Directive Before we discuss the #include directive, you need to know the use of a header file. C++ uses a header file as an interface to source code files. Most of the time all declarations of constants, variables, and functions should be placed in the header files, and only the implementation part should be kept in the .CPP source file. During compilation, the #include directives will be replaced with the header file to which the directive is pointing. The syntax can take one of the following forms: #include #include “headerfile.hpp”
03 9721 CH01
11/13/00
9:42 AM
Page 43
Introduction to C++Builder CHAPTER 1
Types of Files Table 1.6 compares Delphi file types to those in C++Builder. TABLE 1.6
Delphi-to-C++Builder File Type Comparison
Delphi
C++Builder
Description
DPR
BPR
PAS
CPP
DFM
DFM
PAS
H
RES
RES
DCU
OBJ
BPG
BPG
DPK
BPK
None
BPI
BPL
BPL
The Builder project. Each unit has a source code file plus one main project file. A binary file that describes the form and all its components. A header file. A compiled binary resource file. Compiled binary object. Combination of projects in one group file. Source file listing the units used in the package. Import library file created for each package file. The runtime library. It’s like Windows DLL with special Builder features. Static library files. Backup files. A text file that contains information for which files the Builder needs to compile and link for this project. A text file that contains information used to perform low-level debugging tasks. Turbo Debugger Symbol—contains debugging information.
or HPP
None
LIB
~*
~*
None
MAK
MAP
MAP
None
TDS
The following points should help to clarify the file type comparisons in Table 1.6. • The first five types are created on the first Save of the project. • The CPP and HPP files are equivalent to the implementation and interface sections, respectively, of the PAS file.
1 INTRODUCTION TO C++BUILDER
The first statement tells the compiler to search for the headers only in the include files, which are set in Project, Options, Directories/Conditionals. In the second statement, the compiler will start searching for the headers in the current directory. Then it will continue searching in the include files.
43
03 9721 CH01
44
11/13/00
9:42 AM
Page 44
C++Builder 5 Essentials PART I
• The OBJ files are created when selecting Project, Compile Unit. The same type of file will be created when compiling a package. • The BPK file will be created when you save a package file. •
BPI, BPL,
and LIB files are created when a package is compiled or installed.
• In C++Builder 4 and C++Builder 5, MAK has been replaced by BPR. If you try to open MAK file in C++Builder 4 or higher, the IDE will convert it to a BPR file. • The TDS file contains debugging information. This file will get larger when the Project, Options, Compiler, Debug Information menu item is enabled.
Advantages and Disadvantages of C++Builder 5 In this section, we discuss some of the features of C++Builder that could affect a software engineering project. It would be fair to assume that the majority of this book’s readers will have settled upon C++Builder as their chosen development platform. The intention here is to highlight some of those features that make C++Builder such a useful tool, while bringing the reader’s attention to other issues that could cause difficulties.
Visual Reality: True Rapid Application Development For C++ programmers who require a truly visual environment for designing applications with rich, professional-looking user interfaces, the C++Builder IDE with the accompanying Visual Component Library is the only comprehensive, integrated solution. C++Builder’s array of project templates helps get a new program up and running in under a minute, and there is no faster way to start Windows programming in C++. The Object Repository provides an invaluable way to reuse code for productivity and consistency. Even a moderately experienced user will be able to create a simple multifile text editor with menus, toolbars (that can dock or float), and other basic features, all in half an hour. Not only is this astoundingly productive, it can be lots of fun. Being able to see how your application looks while you are designing it reduces the need for trial and error. C++Builder 5 adds some new features to those seen in previous versions that make visual development even easier. For example, the TFrame component allows you to work with independent, reusable form-like windows that can then be placed within forms or even on the Component Palette for later use. Improvements to the Object Inspector include enhanced editors for TColor and TCursor properties, as well as property and event lists that are now categorized to allow infrequently used sets of items to be hidden. This makes it easier to find what you are looking for. On the left side of Figure 1.5, the TCursor property editor in the Object Inspector is shown at work in a project containing two frames that are being used within the main form.
03 9721 CH01
11/13/00
9:42 AM
Page 45
Introduction to C++Builder CHAPTER 1
45
1 INTRODUCTION TO C++BUILDER
FIGURE 1.5 Some of the new visual development enhancements in C++Builder 5.
Microsoft’s Visual C++, although “visual” in name, does not offer this WYSIWYG method of development. This is perhaps the most significant advantage of C++Builder over Visual C++. Visual C++ users have two realistic alternatives for Windows application development. One is to develop programs with the supplied resource editor and the Microsoft Foundation Classes (MFC) library. The other is to develop back-end code as a dynamic link library (DLL) to be used by a graphical user interface (GUI) created in another development environment better suited to visual programming, for example Microsoft’s own Visual Basic language. The first option does not offer a great deal of visual interaction. While the use of the resource editor allows dialog boxes to be laid out in a way similar to C++Builder’s form designer, the variety of ways in which controls can be manipulated at designtime in C++Builder is far more powerful. In fact, C++Builder also comes with the MFC, which is included for those rare times when it would be preferred, though the main reason for its inclusion is for compatibility with existing MFC code. (Visual C++ does not provide a migration path from C++Builder.) Furthermore, only standard controls are catered to by the resource editor, while C++Builder’s system can support the thousands of additional VCL components that are available. The MFC is a very “thin” wrapper around the Windows Application Programming Interface (API) and, although it is more complete in its coverage of the API than the VCL, it does not provide the same high level of built-in functionality and places the burden on the programmer.
03 9721 CH01
46
11/13/00
9:42 AM
Page 46
C++Builder 5 Essentials PART I
The other option actually does present certain benefits in that, from an architectural point of view, the separation of an application’s “engine” from its user interface can provide flexibility and expandability. Nevertheless, the capability of C++Builder to serve well as both a C++ compiler and a visual GUI designer supports its claim to be the most productive of the two systems. A well-engineered program written in C++Builder can support the GUI separation idea equally well.
Keeping Up with the Joneses: The C++ Standard For many years now, Borland has been several strides ahead of the pack in ensuring that its compiler is up to date with the latest ANSI/ISO standards. C++Builder 5—built upon the version 5.5 model of the Borland 32-bit C++ compiler (BCC32)—is no exception. The current details of the language are formalized in the latest ANSI/ISO specification. It differs from the previous version of the standard in several respects, notably in the areas of C++ templates and namespaces. This may not mean a great deal in small, monolithic applications developed prototypically and largely within the confines of the form designer. However, for a serious application that has been designed with scalability in mind, a compiler as rigorous and standard-compliant as BCC32 is crucial to the long-term success of a project. As a consequence of Borland’s policy on the issue of continual improvements to its back-end compiler, however, some developers will find that existing programs that correctly compile under previous versions of C++Builder will fail to do so under version 5. This problem is likely to manifest itself only in cases where heavy use is made of a feature whose treatment by the compiler has undergone a particularly major change, such as templates. I speak from first-hand experience, having spent time converting such an application created in C++Builder 3 to run under version 4 and then again to version 5. Although this particular application has been exposed as being badly written in some places—a good thing to be aware of as a developer and certainly food for thought—it did set my project back. As a keen software engineer and one who is learning all the time, I found the experience to be a positive one. Unfortunately, in such circumstances, project management and the all-important customer may not be quite so enthused. The moral of this story is that the long-term benefits of the compiler can be greatly appreciated, but one cautious eye needs to stay on the here and now. C++Builder also extends the compiler to support proprietary (that is, non-portable) extensions to the C++ language in order to support the VCL. While in many circumstances you may choose to avoid them for reasons of portability, this is another string in the bow of the compiler. For example, the __closure keyword, which supports the concept of a member-function pointer for an instance and not a class, can be useful, because there is no clean way to achieve a similar effect within the scope of the current C++ specification. See the “keyword extensions” reference on the “__closure” page in the online help for more information.
03 9721 CH01
11/13/00
9:42 AM
Page 47
Introduction to C++Builder CHAPTER 1
47
Choosing the Right Development Environment
1
Whatever claims are made by compiler vendors, no one platform is perfect. There will always be supporters who back one side and find only fault with the other. As a piece of software evolves over time, it is inevitable that problems will arise that will reveal flaws in the chosen development tool. This is a fact of life for a developer.
INTRODUCTION TO C++BUILDER
C++Builder 5 is a development tool, not a panacea for all our programming troubles. Of course, this is not exclusive to C++Builder, and there are areas of software development to which it is ideally suited and superior to its competitors and other to which it is not. Having said this, there is no disputing that C++Builder 5 is a robust and intuitive development environment that can be an immensely powerful and productive solution. For those developers who go the Microsoft route, there may be very pressing reasons for doing so—or the dominant reason may simply be that Borland is not Microsoft. By the time you read this, it is expected that Borland will have ported C++Builder to run under the Linux operating system. The potential to write multiplatform applications is sure to interest many people and sway them toward the product. See the section “Preparation for Kylix,” later in this chapter.
C++Builder Advantages and Disadvantages Conclusion It is possible to draw some useful conclusions from this discussion. While the following lists are by no means exhaustive, we can identify some particular advantages and disadvantages to C++Builder. We have not focused on database or Internet features, which are also key selling points of C++Builder, but they do appear in the following list. A summary of the advantages of C++Builder 5 is provided in the following list: • It is a powerful form designer. • It is suitable for both traditional C++ programming and rapid user interface development, because it integrates an ANSI/ISO–compliant compiler with a truly visual development environment. • Its comprehensive Visual Component Library can be supplemented through acquisition of third-party components (including Delphi components) and by creating your own. • Proprietary C++ extensions support additional programming constructs. • Project templates and the Object Repository allow for quick project setup and reuse. • Easy-to-use database support is provided through data-aware components and the Borland Database Engine. • It provides powerful support for Internet development.
03 9721 CH01
48
11/13/00
9:43 AM
Page 48
C++Builder 5 Essentials PART I
• Migration from Visual C++ to C++Builder is relatively straightforward. • It provides a possible route for porting applications to Linux. A summary of the disadvantages of C++Builder 5 is provided in the following list: • The form designer’s ease of use can encourage a haphazard program structure if its design is not in place early on. • Due to changes in the compiler, it might not be worth upgrading from earlier versions if a program is going to require major changes to compile under version 5. • Migration from C++Builder to Visual C++ is usually very difficult. • Choosing a Borland product over Microsoft may have long-term consequences due to Microsoft’s general domination of the industry.
Preparation for Kylix Kylix is the internal code name for Borland’s development environment for the Linux operating system. (The official product name has not yet been announced at the time this is being written.) In basic terms, it is “Delphi and C++Builder for Linux.” With Kylix you can develop applications for Linux systems as easily as you can currently develop Windows applications with C++Builder. It will be possible to port many of your existing C++Builder applications to Linux. There are many similarities between Kylix and C++Builder (and Delphi), but there are also some important differences, mostly brought about by the differences between the Windows operating system and the Linux operating system. We’ll take a look at some of these to alert you to new possibilities with C++Builder. If you are considering porting your applications to Linux, you should be aware of the similarities and differences between Kylix and C++Builder. When I was in the Boy Scouts, we had a simple motto: “Be Prepared!” I still think that it is a great motto, and in the game of software development it’s something that we should keep in mind. Note that what is presented in the remainder of this section is based on information from public announcements and discussions of Kylix available at the time of writing this book. The information here should be considered speculative, because the product is not yet complete, and most of the features and specific details have not been publicly released.
Similarities Between Kylix and C++Builder The basic framework for Kylix will be very similar to C++Builder in most respects. Apart from the general differences between the Windows user interface and the various Linux user interfaces available, the IDE in both products will be almost identical. There will be the Object
03 9721 CH01
11/13/00
9:43 AM
Page 49
Introduction to C++Builder CHAPTER 1
FIGURE 1.6 The Kylix IDE, displayed in Linux using the K Desktop Environment.
Just as C++Builder shares a lot of common functionality and look-and-feel with Delphi, my guess is that the addition of Kylix into the pool will result in common core features being shared among all three products. As new features are implemented in any one of the products, those features will be available in the others in the next release. In the C++Builder version of Kylix, you’ll still be writing C++ code just as you do today. You’ll hardly notice the difference. After all, C++ is C++.
Differences Between Kylix and C++Builder Because Linux is not Windows, there are some features in C++Builder that we won’t see in Kylix, at least initially. These Windows-specific technologies include COM, DCOM, COM+, ActiveX, and Win32 API calls. A few years ago, Microsoft, Digital, and another large company were developing DCOM for UNIX servers. If it’s not available already, then it’s only a matter
1 INTRODUCTION TO C++BUILDER
Inspector, a class browser, various component tabs in the Component Palette, and a code editor. You will be able to reuse what you’ve already learned with C++Builder. Figure 1.6 is a very early (pre-release) screenshot of a Delphi project loaded into Kylix. The K Desktop Environment (KDE) of Linux is being used. Note the similarities to the C++Builder IDE.
49
03 9721 CH01
50
11/13/00
9:43 AM
Page 50
C++Builder 5 Essentials PART I
of time before it is. It is possible that SOAP (Simple Object Access Protocol) support will be included at some stage. I’m guessing that many of the Win32 API calls will have an equivalent in Linux, either transparently using a system API layer or specific to Linux. The latter would make porting of applications between the two more difficult. The VCL for Linux will be named the Borland Component Library for Cross Platform (CLX for short, pronounced “clicks”). The various CLX components will be grouped into logical units such as visual components, data access components, and so on. The GUI components will be built on TrollTech’s Qt framework. Qt is a cross-platform C++ GUI application framework that was selected by Borland as the most suitable framework to use in the Linux environment. Qt is installed with the KDE but must be installed separately when using Gnome or other Linux desktops. CLX will appear in the Windows versions of C++Builder and Delphi to facilitate easier porting of applications between Linux and Windows. However, for performance reasons, the VCL should still be used if the target operating system for the application is Windows. Kylix will include a new thin database access layer, likely to be called DBExpress, as a replacement for the Borland Database Engine (BDE). The current discussion suggests that it will support a limited set of underlying databases and require only a single DLL to be installed with the client.
Porting C++Builder Projects to Kylix Porting C++Builder projects to Kylix will depend on the features that are implemented in Kylix. The previous sections should give a general feel of the types of porting issues you’re likely to find. Porting most applications will definitely be possible. It would be great if it were possible without source code modifications but, realistically, many applications will require at least some basic tweaking.
So When? Based on current discussions, the Delphi version of Kylix will be released in late 2000. By the time you’re reading this, it may already be released, and you will know more about it than I do! The C++Builder version? Current discussions put that at early 2001.
03 9721 CH01
11/13/00
9:43 AM
Page 51
Introduction to C++Builder CHAPTER 1
51
Summary
1
In this chapter, you learned the basics of C++Builder for beginners and those moving from Delphi. If this was your first look at C++Builder, you should now be able to see its enormous potential for developing Windows applications. You were also introduced to some of the new features and enhancements in C++Builder 5 and some of the upgrading and compatibility issues when migrating existing projects and third-party tools. Finally, you learned a little about Kylix, Borland’s upcoming product for software development on the Linux platform.
INTRODUCTION TO C++BUILDER
Several of the concepts discussed in this chapter will be covered in detail in the following chapters, and many new concepts will be introduced.
03 9721 CH01
11/13/00
9:43 AM
Page 52
04 9721 CH02
11/13/00
9:40 AM
Page 53
C++Builder Projects and More on the IDE Jamie Allsop Yoto Yotov Jarrod Hollingworth Khalid Almannai Dan Butterfield
IN THIS CHAPTER • Understanding C++Builder Projects • Using the Object Repository • Understanding and Using Packages • Introducing New IDE Features in C++Builder 5
CHAPTER
2
04 9721 CH02
54
11/13/00
9:40 AM
Page 54
C++Builder 5 Essentials PART I
In Chapter 1, “Introduction to C++Builder,” we introduced you to the IDE and projects, both integral parts of C++Builder. In this chapter we will build on Chapter 1. First we will look at projects in more detail, focusing on the various files included in a project and the Project Manager. Then we will show you how to use the Object Repository to store and reuse code and other program elements that you’ve developed. We also will discuss Packages, a special type of dynamic link library (DLL), and how they provide functionality in the IDE and in your applications. Finally, we will look at several great new features in the C++Builder 5 IDE.
Understanding C++Builder Projects C++Builder makes programming easier than ever. The Integrated Development Environment (IDE), introduced in Chapter 1, provides two-way development by integrating a graphical Form Designer and a Code Editor, allowing you to develop applications from two angles. Using the ClassExplorer, Code Insight, and standard features of the Form Designer, C++Builder can even write a lot of the mundane code for you! You can use C++Builder to develop applications such as those in the following list: • Windows or console applications • Client/server applications • Dynamic link libraries (DLLs) • Custom components and Packages • Component Object Model (COM) and ActiveX controls All these applications are created using projects. A project is a collection of C++ source files, form files, and other file types that together define the application. C++Builder uses a special project file to store the structure of the project and to remember various project options that change the way the application is built.
Files Used in C++Builder Projects C++Builder projects consist of many different types of files. Some files are created automatically by C++Builder, such as when a new project is started or when new items are added to an existing project. Other files are created by the developer, and there are other files that are created when the application is compiled. These files can be categorized as follows: • Main project files • Form files • Package files
04 9721 CH02
11/13/00
9:40 AM
Page 55
C++Builder Projects and More on the IDE CHAPTER 2
55
• The desktop options file • Backup files
Main Project Files When you create most projects, three main files are automatically created. They are shown in the following list: • C++Builder project file ProjectName.bpr • Main project source file ProjectName.cpp • Main project resource file ProjectName.res
By default your project is named Project1. You can change the name of the project when you first save it by renaming the Project1.bpr file in the Save Project As dialog that appears. You should be careful naming the various files in your project, using the same name for the main project files and other unit files can become confusing, and in some cases can hinder the ability to view the files in the Code Editor. The C++Builder Project File The project file is a text file that contains the project options settings and the rules to build the project. This file has changed somewhat through the different versions of C++Builder. In C++Builder 1 through 4, the file was in a Makefile format. In C++Builder 1 the project file actually had the extension .mak to signify this. In C++Builder 5 the file changed to the Extensible Markup Language (XML) format. For more information on the new XML project file format, see “The XML Project File Format,” later in this chapter. The Main Project Source File This file contains the application entry (startup) code, macros such as USEUNIT and USEFORM that define various files that are built with the application, and little else. C++Builder automatically maintains the macros throughout development. In a standard Windows application, the main project source file contains the WinMain() function as the application entry-point. In other types of applications, this function may be named DllEntryPoint() or simply main(). Unlike most other auto-generated source files, this file has no corresponding header (.h) file. You’ll seldom need to change the main project source file except to execute a function as the application starts, such as displaying a splash screen while the application is initializing. The Project Resource File This file contains the application’s icon, the application version number, and other information. Not all application types have a project resource file.
C++BUILDER PROJECTS
The files are initially created internally for you to start using. The files will not be created on disk until you save the project.
2
04 9721 CH02
56
11/13/00
9:40 AM
Page 56
C++Builder 5 Essentials PART I
Resource files in general store images, icons, and cursors for your project. To create these items, you typically use a tool like the Image Editor provided by C++Builder. Resource files can also contain strings and other objects. For a more in-depth look at storing images in a resource file, see “Using Predefined Images in Custom Property and Component Editors” in Chapter 10, “Creating Property and Component Editors.”
Form Files For each Form you create, C++Builder generates the following files: • The Form layout file UnitName.dfm • The Form source file UnitName.cpp • The Form header file UnitName.h By default the unit name is Unit1 for the first Form created, Unit2 for the second, and so on. You can rename the Form files when saving the project. The extension .dfm stands for Delphi Form, a reminder that C++Builder is based partially on Borland’s Delphi product. The .dfm file contains values that represent the graphical part of the Form, such as component locations, font styles, and so on. In C++Builder versions 1 through 4, the .dfm file is saved in a binary format. It can be viewed as text in these versions by right-clicking on the Form and then selecting View As Text. In C++Builder 5, the .dfm file is saved in a text format by default, but it can be saved in binary format if required. For more information see the “Forms—Save as Text” section, later in this chapter. You can edit the .dfm Form files if you want, but you rarely need to do so. The .cpp file and its associate .h header file are created with the .dfm file each time you create a new Form. The .h file contains the C++ class definition for the Form. The .cpp file contains the event handler functions for the Form and for the components that you add to the Form. In simple applications, most of the code that you write will be placed in the Form’s .cpp file. To view the .cpp and .h files, do the following: 1. If your project is not open, select File, Open. 2. Select View, Units, then choose the unit file of your Form and click OK or press Enter. The .cpp file for the Form will be displayed in the Code Editor. 3. To view the header file, right-click in the Form’s .cpp file displayed in the Code Editor and choose Open Source/Header File.
The Package Files Packages are simply dynamic link libraries (DLLs) that can be shared among many C++Builder applications. They allow you to share classes, components, and data between applications. For example, the most frequently used C++Builder components reside in a
04 9721 CH02
11/13/00
9:40 AM
Page 57
C++Builder Projects and More on the IDE CHAPTER 2
57
Package called VCL50. Most applications created in C++Builder share some common code from this Package, provided in the Package file vcl50.bpl. The following are specific Package files: • The Package Project Options (.bpk) file. Consider this file to be like a .bpr project file, but applicable only for Packages. This file is created if you choose File, New, Package. • The Borland Package Library (.bpl) file.
• The Borland Package Import Library (.bpi) file. Each time you compile the Package source file, a .bpi file will be created. Again, its base name matches the Package source base name. See the “Understanding and Using Packages” section later in this chapter for more information on Packages.
The Desktop Options File The desktop options file for a project stores the arrangement of the various windows open in the IDE and the files open in the Code Editor. The next time the project is opened, these settings will be restored. The Desktop options filename has the format ProjectName.dsk and is saved in the same folder as the project. C++Builder can be set to save the file automatically when you close the project. From the menu, select Tools, Environment Options. This will take you to the Environment Options dialog. Select the Preferences tab and check AutoSave Options, Project Desktop.
Backup Files C++Builder will create a backup file for each of your project’s .bpr, .dfm, .cpp, and .h files each time you save your project, except for the first save. All backup file extensions are prefixed with the ~ symbol; thus, the .bpr file extension will become .~bp, .cpp will become .~cp, and so on.
Project Manager The Project Manager displays the file structure of a project or project group. The Project Manager can be viewed by selecting View, Project Manager from the C++Builder menu. Figure 2.1 shows an example of the Project Manager dialog.
2 C++BUILDER PROJECTS
This is the resulting runtime library for the Package. It is like a DLL, except that it contains C++Builder-specific features. Its base name matches the Package source base name.
04 9721 CH02
58
11/13/00
9:40 AM
Page 58
C++Builder 5 Essentials PART I
FIGURE 2.1 The Project Manager window for C++Builder.
A project group is a collection of projects. Sometimes you need to create more than one project for an application. For example, an application could have a VCL project, a DLL project, and a console project. The information about the project group is stored in a project group file in the same folder as the project. It has the filename ProjectName.bpg. The main functions of the Project Manager are shown in the following list: • Adding or removing projects • Adding or removing forms or units • Drag-and-drop capability • Selecting a project • Compiling or building all your projects
Using the Object Repository One of C++Builder’s most powerful project management tools is the Object Repository. It lets you share and reuse forms, projects, data modules, project templates, and wizards. For example, if you need to develop a series of applications that share a standard About dialog box, you can store the dialog box in the Object Repository. This allows you to add the dialog to all projects quickly, saving time and avoiding the need to copy or redesign the dialog each time.
Adding Items to the Object Repository Let’s take as our first example the standard About dialog box described previously, or the About box for short. We will use as a base the standard About box available in C++Builder. To design and test it, we first need to create a simple application that will act as the host of the
04 9721 CH02
11/13/00
9:40 AM
Page 59
C++Builder Projects and More on the IDE CHAPTER 2
59
About box. Select File, New Application. To add the default About box to the application, select File, New, Forms, About Box. We want to customize this default About box by changing the captions of the labels to appropriate values at runtime. Figure 2.2 shows how we want our standard About box to look. It contains a logo, the correct name and version of the product that is using the About box, and a comment. We’ve kept the default logo, but it too can be customized if desired.
2 C++BUILDER PROJECTS
FIGURE 2.2 Our standard About dialog box.
If you followed the previous steps to create a new About box, you’ll notice that, in addition to the customized label captions, we have deleted the copyright label. We have done this simply to demonstrate that you can make whatever changes you want. The copyright label can be removed by selecting Copyright Label in the Form Designer and pressing Delete. Name the About box StdAboutBox by selecting About Box in the Form Designer and entering this text next to the Name property in the Object Inspector. Save the entire C++Builder project by selecting File, Save All. In the Save As dialog, change the name of the About Box unit from Unit2.cpp to StdAboutBox.cpp. Change the name of the application’s main form unit from Unit1.cpp to AboutTestForm.cpp, and change the name of the project source file to AboutTestProj.cpp. To set the label captions of the About box at runtime, add some code to the OnCreate event handler of the About box. With the About box selected in the Form Designer, click on the Events tab in the Object Inspector and double-click next to the OnCreate event. Add the code in Listing 2.1 to the TStdAboutBox::FormCreate() event handler function that appears in the Code Editor. LISTING 2.1
The OnCreate Event Handler Function for TStdAboutBox
void __fastcall TStdAboutBox::FormCreate(TObject *Sender) { struct TransArray { WORD LanguageID, CharacterSet;
04 9721 CH02
60
11/13/00
9:40 AM
Page 60
C++Builder 5 Essentials PART I
LISTING 2.1
Continued
}; DWORD VerInfo, VerSize; HANDLE MemHandle; LPVOID MemPtr, BufferPtr; UINT BufferLength; TransArray *Array; char QueryBlock[40]; // Get the product name and version from the // applications version information. String Path(Application->ExeName); VerSize = GetFileVersionInfoSize(Path.c_str(), &VerInfo); if (VerSize > 0) { MemHandle = GlobalAlloc(GMEM_MOVEABLE, VerSize); MemPtr = GlobalLock(MemHandle); GetFileVersionInfo(Path.c_str(), VerInfo, VerSize, MemPtr); VerQueryValue(MemPtr, “\\VarFileInfo\\Translation”, &BufferPtr, &BufferLength); Array = (TransArray *)BufferPtr; // Get the product name. wsprintf(QueryBlock, “\\StringFileInfo\\%04x%04x\\ProductName”, Array[0].LanguageID, Array[0].CharacterSet); VerQueryValue(MemPtr, QueryBlock, &BufferPtr, &BufferLength); // Set the product name caption. ProductName->Caption = (char *)BufferPtr; // Get the product version. wsprintf(QueryBlock, “\\StringFileInfo\\%04x%04x\\ProductVersion”, Array[0].LanguageID, Array[0].CharacterSet); VerQueryValue(MemPtr, QueryBlock, &BufferPtr, &BufferLength); // Set the version caption. Version->Caption = (char *)BufferPtr; GlobalUnlock(MemPtr); GlobalFree(MemHandle); } else { ProductName->Caption = “”; Version->Caption = “”; } Comments->Caption = “Thank you for trying this fabulous product.\n” “We hope that you have enjoyed using it.”; }
This code retrieves the product name and product version from the project’s version information. To add version information to the project, select Project, Options, then click on the
04 9721 CH02
11/13/00
9:40 AM
Page 61
C++Builder Projects and More on the IDE CHAPTER 2
61
Version Info tab and check the Include Version Information in Project option. Scroll down to the ProductName field in Key/Value table and enter a product name such as My Demo Product. By default the project starts with version 1.0.0.0. Click OK to close the Project Options dialog. Later, when the About box is added to any project, the correct version information for that project will be displayed without the need to change any code in the About box. In Listing 2.1, a long caption has been assigned to the Comments label. To display this caption, select the Comments label in the Form Designer, click on the Properties tab in the Object Inspector, and change the Width and Height properties to 265 and 35, respectively. Set AutoSize to false. The creation of our standard About box is now complete.
void __fastcall TForm1::Button1Click(TObject *Sender) { StdAboutBox->ShowModal(); }
The complete AboutTestForm.cpp unit is shown in Listing 2.2. LISTING 2.2
The Complete AboutTestForm.cpp Unit
//--------------------------------------------------------------------------#include #pragma hdrstop #include “AboutTestFOrm.h” #include “StdAboutBoxForm.h” //--------------------------------------------------------------------------#pragma package(smart_init) #pragma resource “*.dfm” TForm1 *Form1; //--------------------------------------------------------------------------__fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } //---------------------------------------------------------------------------
C++BUILDER PROJECTS
To test the About box, we need to display it from the test application to which we added it. To do this, click on the AboutTestForm.cpp tab in the Code Editor. Add the line #include “StdAboutBoxForm.h” immediately after the #include “AboutTestForm.h” line at the top of the unit. Add a Button component to the middle of the test Form in the Form Designer, and create an OnClick event handler for the button by double-clicking the button. In the event handler, add the following code.
2
04 9721 CH02
62
11/13/00
9:40 AM
Page 62
C++Builder 5 Essentials PART I
LISTING 2.2
Continued
void __fastcall TForm1::Button1Click(TObject *Sender) { StdAboutBox->ShowModal(); } //---------------------------------------------------------------------------
This code will display our About box when the button on the test Form is clicked at runtime. Using ShowModal(), the About box must be closed before the user can return to the test Form. Save all changes to the project by selecting File, Save All. You can find the complete example, with the project filename AboutTestProj.bpr, in the ORAboutBox folder on the CD-ROM that accompanies this book. Now compile and run the application by selecting Run, Run or pressing F9. When you click on the button, the About box is displayed. You should see an About box similar to that shown in Figure 2.2. Click OK to close the About box and return to the test Form. Close the test Form to stop the application. Now that our About box has been tested and is working correctly, we can add it to the Object Repository. Items added to the Object Repository appear in the File, New dialog. First, rightclick on the About box in the Form Designer and select Add To Repository from the pop-up menu. Specify a title and description. In the Page combo box, select an appropriate page of the Object Repository to add the About box to, such as Forms. Enter your name in the Author field and, if you like, specify an icon to display for this Form. Click OK to save the About box to the Object Repository. At this point, if you select File, New and click on the Forms tab, you should see the standard About box you just created.
FIGURE 2.3 The Add To Repository dialog.
In addition to forms, entire projects can be added to the Object Repository. This is done by selecting Add To Repository from the Project menu.
04 9721 CH02
11/13/00
9:40 AM
Page 63
C++Builder Projects and More on the IDE CHAPTER 2
63
NOTE Before adding items, make sure you save the current form or project. Unsaved items cannot be added to the Object Repository. If you forget to save the item before adding to the Object Repository, C++Builder will prompt you to do so. If an item with the same name already exists in the Object Repository, C++Builder will prompt you to replace it.
2 To access any of the items previously added to the Object Repository, choose File, New from the main menu. As you can see, the Object Repository has various pages, each of which contains reusable items.
TIP The items shown on each tab of the Object Repository List can be displayed in various formats. Right-click on the Object Repository dialog to see the list of available view styles. For example, you can select View Details to display the name, description, modified date and time, and author of each item in a tabular format.
At the bottom of each page, you’ll find three radio buttons: Copy, Inherit, and Use. These buttons demonstrate the power and flexibility of the Object Repository. The Copy option creates a duplicate of an item you select. Changes made to the duplicate will not affect the Object Repository. In the same way, changes to the original item will not affect the duplicate. The Inherit option is similar to Copy. The difference is that the new object is derived from the original. Therefore, changes in the Object Repository original will be reflected in your copy. Finally, the Use option opens and edits the selected Object Repository item directly. Using items directly is not recommended unless you’re dealing with data modules.
Sharing Items Within a Project If you display the File, New dialog and a project is currently open, you’ll find a tab with the name as your project. This tab lists all forms and data modules available in your project. The Object Repository allows you to inherit from existing forms. This feature is particularly helpful if you have to create several similar forms a project.
C++BUILDER PROJECTS
Using Items in the Object Repository
04 9721 CH02
64
11/13/00
9:40 AM
Page 64
C++Builder 5 Essentials PART I
Customizing the Object Repository You can organize items and pages displayed in the Object Repository using the Object Repository Options dialog box. To display it, select Tools, Repository or right-click the File, New dialog and choose Properties from the context menu.
FIGURE 2.4 The Object Repository dialog.
The first list box contains a list of all available pages. With the arrow buttons, you can change the position of the selected page. In addition, you could add, remove, or delete pages with the appropriate buttons on the right.
NOTE Only empty pages can be removed. Before deleting a page, remove all objects it contains.
The second list box contains all items in the selected page. This time only two buttons are available: one to delete the object and another to edit its description. Depending on the object type, the following options may be available: • New Form Opens the specified default form when you click File, New Form. • Main Form Opens the specified default main form when a new project is created. • New Project Opens the specified default new project when you click File, New Application.
Creating and Adding a Wizard to the Object Repository Wizards (also known as experts) are simple applications that lead you through various dialog boxes and help you create projects or code snippets or simply display information. C++Builder
04 9721 CH02
11/13/00
9:40 AM
Page 65
C++Builder Projects and More on the IDE CHAPTER 2
65
comes with a variety of wizards, such as the Console Wizard and the MFC Wizard, which you may have used before. We’ve seen in the previous pages how to create and add forms and projects to the Object Repository. Obviously, wizards require closer integration with the C++Builder IDE. They do this using the Open Tools API TIExpert interface.
NOTE
To create a wizard, you must define a descendant of the TIExpert class. Each of the TIExpert member functions will then gather information about the new wizard. These member functions are: GetName(), GetAuthor(), GetComment(), GetPage(), GetGlyph(), GetStyle(), GetState(), GetIDString(), GetMenuText(), and Execute(), as well as a constructor and a destructor. Again, a complete description can be found in the OTA source files. Wizards can be compiled in a package (described in the next section, “Understanding and Using Packages”) or in a DLL. I’ll use a Package, which is easier to install and manage. The first step is to create a new Package from the Object Repository by selecting File, New, and then selecting Package from the New tab. Edit the Package’s source file, Package1.cpp, adding the #include <ExptIntf.hpp> line, TWizard class, TWizard::Execute() function, and namespace sections shown in Listing 2.3. You can find the complete Package, with the filename MyWizard.bpk, in the ORWizard folder on the CD-ROM that accompanies this book. LISTING 2.3
Source Code for MyWizard.cpp
//--------------------------------------------------------------------------#include #include <ExptIntf.hpp> #pragma hdrstop USERES(“MyWizard.res”); USEPACKAGE(“vcl50.bpi”); //--------------------------------------------------------------------------#pragma package(smart_init) //---------------------------------------------------------------------------
2 C++BUILDER PROJECTS
The Open Tools API (OTA) is composed of eight units, each of which provides interfaces for interaction with the IDE. Although the OTA is not officially documented, the best way to master it is to look at its source code, found in the \Source\ToolsAPI folder where C++Builder was installed.
04 9721 CH02
66
11/13/00
9:40 AM
Page 66
C++Builder 5 Essentials PART I
LISTING 2.3
Continued
// Package source. class __declspec(delphiclass) TWizard : public TIExpert { public: String __stdcall GetName() {return “My Wizard”;}; String __stdcall GetAuthor() {return “Yoto Yotov”;}; String __stdcall GetComment() {return “A sample wizard”;}; String __stdcall GetPage() {return “New”;}; HICON __stdcall GetGlyph() {return hIcon;}; TExpertStyle __stdcall GetStyle() {return esProject;}; TExpertState __stdcall GetState() {return TExpertState() << esEnabled;}; String __stdcall GetMenuText() {return “Wizard”;}; String __stdcall GetIDString() {return “Yotov.Wizard”;}; virtual void __stdcall Execute(void); __fastcall TWizard() {hIcon = LoadIcon(NULL, IDI_APPLICATION);}; virtual __fastcall ~TWizard() {}; protected: HICON hIcon; }; void __stdcall TWizard::Execute() { Application->MessageBox(“Wizard is available!”, “Congratulations!”, MB_OK); } #pragma argsused int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*) { return 1; } //--------------------------------------------------------------------------namespace Mywizard
04 9721 CH02
11/13/00
9:40 AM
Page 67
C++Builder Projects and More on the IDE CHAPTER 2
LISTING 2.3
67
Continued
{ void __fastcall PACKAGE Register() { RegisterLibraryExpert(new TWizard()); } }
This code implements the required TIExpert member functions; for instance, the GetName() function sets the name of our wizard in the Object Repository to My Wizard, and the GetPage() function sets the page that it will appear on to New.
Now, open the File, New dialog and admire the wizard you’ve just created. It will appear with the name My Wizard. Of course, more coding is required in the Execute() member function to make your expert useful. I leave that to you.
Understanding and Using Packages This section discusses Packages and how they are used both in applications and by the IDE. The term Package refers specifically to a source module with the .bpk extension (Borland Package) and more generally to a Borland Package Library (BPL) file (.bpl extension) that is created when the Package is built. A BPL file is very similar to a dynamic link library (DLL) and is used for the same purpose, to dynamically link executable code to an application. For more information on the similarities and differences between a Borland Package Library and a dynamic link library, refer to Chapter 15, “DLLs and Plug-Ins,” which covers this topic in more depth. A Package can be one of three types: designtime-only, runtime-only, or dual designtime/ runtime. Essentially, the only difference between these Packages is that a designtime Package can be installed into the IDE, and a runtime Package cannot. Runtime Packages can be used only at runtime. The dual Package can be used in either situation and is often used for convenience when initially developing components. Using Packages as a method for deploying components makes up the bulk of the “Distributing Components and Other Issues” section in Chapter 11, “More Custom Component Techniques.” A Package consists of two sections: a Contains section and a Requires section. Files that appear in the Contains section are those that the Package contains, which are compiled and linked when the Package itself is compiled and linked. These files are part of the Package.
C++BUILDER PROJECTS
To add our wizard to the Object Repository, we need to compile and install it. Display the Package Editor by selecting View, Toggle Form/Unit or pressing F12 while in the Code Editor. Click the Compile button in the Package Editor, and when the compile is finished press OK. To install the Package, simply click the Install button in the Package Editor. A message box will appear to tell you that the Package has been installed.
2
04 9721 CH02
68
11/13/00
9:40 AM
Page 68
C++Builder 5 Essentials PART I
Import files appearing in the Requires section are references to runtime Packages that this Package must access to function correctly. This mechanism will be explained more clearly later in this section. When a Package is built, all the files in the Contains section, where appropriate, are compiled and linked, and object files for each are created. In addition, a BPL file is generated, as are a Borland Package Import (BPI) file (.bpi extension) and a static library (LIB) file (.lib extension). To not generate a BPI file, select the Linker tab in the Project, Options dialog, and uncheck the Generate Import Library option in the Linking group. To not generate a LIB file, uncheck the Generate .lib File option. You always need to generate a BPI file, which is used by the IDE during linking, so that the executable can use the respective BPL file at runtime. This is true except in a designtime-only Package. Therefore, the same types of files are produced for all Package types. The difference is only apparent in their use. Figure 2.5 shows the structure of a Package and the files that are produced upon a successful build. Package
PackageName. bpk
Files Created on a Successful Build
Contains PackageName.bpl PackageName.cpp ContainedUnit1
PackageName.bpi PackageName.lib
ContainedUnit2
• • • ContainedUnitn
ContainedUnit1.obj ContainedUnit2.obj
• • • ContainedUnitn.obj
Requires vcl50.bpi
FIGURE 2.5 The structure and output of a Package.
Note that in Figure 2.5, the contained units are implied to be C++ translation units. Hence, for each translation unit, an object file is produced when the Package is compiled and linked successfully. Other files can also be added to a Package’s Contains section, most notably resource files and object files. These files are commonly needed when a Package is used to Package components. For more details on this, refer to Chapter 11. The Requires section in Figure 2.5 includes the import file vcl50.bpi. This indicates that the Package requires executable code contained by vcl50.bpl (the core VCL runtime BPL).
04 9721 CH02
11/13/00
9:40 AM
Page 69
C++Builder Projects and More on the IDE CHAPTER 2
69
Placing the vcl50.bpi import file in the Package’s Requires section allows the linker to resolve any external references to the vcl50.bpl runtime library. Nearly all the Packages that you create will use the core VCL, in which case you will need this import file in the Requires section of nearly all the Packages you create.
For an application to link dynamically to a .bpl file, the linker must be able to resolve references to the functions and data contained by the .bpl file during linking. Such a reference is referred to as an external reference because it refers to something external to the current application. To resolve an external reference, the linker searches for an import record for the function called by the application. The import record is contained in the corresponding Borland Package Import (BPI) file (.bpi extension) for the Package. For every function that is exported from the Package (basically any function that is declared in a unit contained by the Package), there is an entry that states the internal name of the function and the name of the module that contains the function. In this case that is the name of the Package’s BPL file (more information than this is presented, but it is not relevant to this discussion). This information is copied by the linker to the application’s executable file. This creates a dynamic link to the function that will be resolved by Windows each time the application is executed. It does this by searching all the files on the system path (for example, files in the Windows system directory) for the file named by the import record when the application is loaded into memory. If it finds the required BPL file, that also is loaded into memory. The external reference is then referenced to the correct location within the BPL file. If the required BPL is already loaded into memory, then all the better. The overhead for this operation is not incurred, and the BPL is shared by the applications using it. The function or data can then be used by the application. It should be clear that the BPL and BPI files produced by a runtime Package are used to support dynamic linking of the code that the Package exports. The LIB file is used to support static linking of the code that the Package exports. Essentially, the LIB file contains the OBJ files of the units contained by the Package itself and is in fact an object library. When a function is required from the Package, the appropriate OBJ from the LIB file is copied to the target executable. Each such executable therefore has its own copy. Table 2.1 summarizes the purpose of the files produced by a Package when it is successfully built.
2 C++BUILDER PROJECTS
In Figure 2.5 we can see that three files are produced when a Package is built. The purpose of the BPL file depends on whether the Package is a designtime Package or a runtime Package. If it is a dual Package, the functionality of both Package types is available. With designtime Packages, the BPL file is used to install the Package into the IDE. This is done by selecting Install Packages from the Components menu and browsing for the designtime Package .bpl file. With runtime Packages, the BPL file is used specifically to allow applications to link dynamically to the functions and data that the Package contains at runtime.
04 9721 CH02
70
11/13/00
9:40 AM
Page 70
C++Builder 5 Essentials PART I
TABLE 2.1
Package Files Created on a Successful Build
Extension
Description
Type
Purpose
.bpl
Borland Package Library (BPL)
Dynamically linkable library
Contains executable code of the Package and exports the functions and data of the Package. A runtime library accessed by applica tions that are dynamically linked to it. A designtime library that can be installed into the IDE to make new components or editors available at designtime.
.bpi
.lib
Borland Import Library (BPI) Static Library File (LIB)
Import library
Object library
Contains import records for the functions and data exported by the corresponding BPL file required for dynamic linking to the BPL file. A static library containing the object files of the units contained by the Package. Used to statically link exported functions and data to a target application.
Table 2.1 shows that, in order to use a runtime BPL, the corresponding BPI file must be available at link time for the external references to the BPL to be resolved. Figure 2.6 illustrates the relationship between the files produced by a Package and an application that uses the Package. When you compile and link a project, you can choose whether the project is dynamically linked or statically linked to the Packages it requires. By default, dynamic linking is used. To change this setting for the project, uncheck the Build with Runtime Packages option on the Project Options, Packages page. It is also possible to choose units from a Package that you want to be statically linked to a given unit within a project. To do this, add the #pragma link “unitname” directive, generally near the top of the unit, where unitname is the name of the unit that you want to be statically linked to the unit within your project. This results in the linker copying the required object file from the Package’s .lib file.
Considerations when Using Packages For a class to be properly imported and exported from a Package, the PACKAGE macro must be used after the class keyword in the class definition, as shown in the following code:
04 9721 CH02
11/13/00
9:40 AM
Page 71
C++Builder Projects and More on the IDE CHAPTER 2
71
class PACKAGE TMyComponent : public TComponent { // Component class definition here. };
The PACKAGE macro must also be given in the declaration of the Register() function for the component. This is shown in the following code:
Linking
Runtime
Dynamically Link
Borland Package Import (BPI) External Reference
Import Record
Target Application
Borland Package Library (BPL) Dynamically Links to a Single Copy of the BPL
Target Application Statically Linked Executable Code
External Reference
Object file
Static Library File (LIB) Statically Link
FIGURE 2.6 The Package–application relationship.
Of course, this applies to component class definitions. If you use the Component Wizard to create the component, C++Builder will insert the PACKAGE macro for you. To ensure that functions and data are properly imported and exported from a unit contained in a Package, the #pragma package(smart_init) directive should be placed in the unit’s source file, typically after any #include statements but before any source code. Failing to do so will
2 C++BUILDER PROJECTS
namespace Newcomponent { void __fastcall PACKAGE Register() { TComponentClass classes[1] = {__classid(TMyComponent)}; RegisterComponents(“Samples”, classes, 0); } }
04 9721 CH02
72
11/13/00
9:40 AM
Page 72
C++Builder 5 Essentials PART I
not prevent the unit from compiling, but it will prevent the Package from statically linking. The purpose of the directive is to ensure that the packaged units are initialized in the order determined by their dependencies. The #pragma package(smart_init,weak) directive is also available. This is used when you do not want a particular unit to be contained in the BPL file. Instead, the unit is placed in the BPI file. When the unit is required, it is copied from the BPI file and statically linked to the target application. Such a unit is said to be weakly packaged and is used to eliminate conflicts among Packages that may depend on the same external library.
The C++Builder Runtime Packages C++Builder comes with a variety of runtime Packages that contain the core VCL as well as other libraries and components. These Packages are listed in Table 2.2, as are the units that are exported from each Package. TABLE 2.2
The C++Builder Runtime Packages
Runtime Package
Units Exported from Package
Runtime Functionality for:
dss50.bpl
Mxarrays, Mxbutton, Mxcommon, Mxconsts, Mxdb, Mxdcube, Mxdsql, Mxgraph, Mxgrid, Mxpbar, Mxpivsrc, Mxqedcom, Mxqparse, Mxstore, Mxtables
Decision Cube Components
ibevnt50.bpl
Ibconsts, Ibctrls, Ibevnts, Ibproc32
InterBase Events
inet50.bpl
Copyprsr, Httpapp, Webconst
inetdb50.bpl
Dbweb, Dsprod
Internet Components Internet Components
nmfast50.bpl
Nmconst, Nmdaytim, Nmecho, Nmextstr, Nmfngr, Nmftp, Nmhttp, Nmmsg, Nmnntp, Nmpop3, Nms_huge, Nms_stream, Nmsmtp, Nmstrm, Nmtime, Nmudp, Nmurl, Nmuue, Psock
FastNet Internet Components
qrpt50.bpl
Qr3const, Qrabout, Qrcomped, Qrctrls, Qrdatawz, Qrenved, Qrexpbld, Qrexport, Qrexpr, Qrexpred,
QReport Components
04 9721 CH02
11/13/00
9:40 AM
Page 73
C++Builder Projects and More on the IDE CHAPTER 2
TABLE 2.2
73
Continued
Runtime Package
Units Exported from Package
Runtime Functionality for:
Qrextra, Qrlabled, Qrlablwz, Qrprev, Qrprgres, Qrprnsu, Qrprntr, Qrwizard, Quickrpt Arrowcha, Brushdlg, Bubblech, Chart, Ganttch, Pendlg, Series, Tecanvas, Teeabout, Teeconst, Teefunci, Teengine, Teeprevi, Teeprocs, Teeshape, Teexport
TChart
teedb50.bpl
Dbchart
teeqr50.bpl
Qrtee
teeui50.bpl
Areaedit, Arrowedi, Axisincr, Axmaxmin, Baredit, Custedit, Dbeditch, Editchar, Flineedi, Ganttedi, Iedi3d, Iediaxis, Iedigene, Iedilege, Iedipage, Iedipane, Iediperi, Iediseri, Ieditcha, Iedititl, Iediwall, Pieedit, Shapeedi, Teegally, Teelisb, Teepoedi, Teestore
(Database) TChart (QReport) TChart (User Interface)
vcl50.bpl
Activex, Actnlist, Axctrls, Buttons, Classes, Clipbrd, Comconst, Comctrls, Commdlg, Comobj, Comstrs, Consts, Contnrs, Controls, Dialogs, Dsgnintf, Dsgnwnds, Editintf, Exptintf, Extctrls, Extdlgs,
2 C++BUILDER PROJECTS
tee50.bpl
TChart
Standard, Additional, System, Win32 and Dialog Components
04 9721 CH02
74
11/13/00
9:40 AM
Page 74
C++Builder 5 Essentials PART I
TABLE 2.2
Continued
Runtime Package
Units Exported from Package
Runtime Functionality for:
Fileintf, Flatsb, Forms, Graphics, Grids, Imglist, IniFiles, Istreams, Libhelp, Libintf, Mapi, Mask, Masks, Math, Menus, Mtx, Multimon, Oleconst, Olectnrs, Olectrls, Oleserver, Printers, Proxies, Registry, Scktcomp, Stdactns, Stdctrls, Stdvcl, Svcmgr, Syncobjs, Sysconst, System, Sysutils, Toolintf, Toolsapi, Toolwin, Typinfo, Vclcom, Virtin vclado50.bpl
Adoconst, Adodb, Adoint, Oledb
ADO Components
vclbde50.bpl
Bde, Bdeconst, Dbinpreq, Dbpwdlg, Dbtables, Smintf
vcldb50.bpl
Db, Dbactns, Dbcgrids, Dbcommon, Dbconsts, Dbctrls, Dbgrids, Dblogdlg, Dbolectl
Borland Database Engine Data Access and Data Controls Components
vcldbx50.bpl
Dblookup, Report, Rsconsts
TDBLookup Components
vclib50.bpl
Ib, Ibbatchmove, Ibblob, Ibcustomdataset, Ibdatabase, Ibdatabaseinfo, Ibevents, Ibexternals, Ibheader, Ibinstall, Ibintf, Ibquery, Ibservices, Ibsql, Ibsqlmonitor, Ibstoredproc, Ibtable, Ibupdatesql, Ibutils
InterBase Components
vclie50.bpl
Mshtml, Shdocvw
vcljpg50.bpl
Jconsts, Jpeg
Internet Explorer JPEG Functionality
04 9721 CH02
11/13/00
9:40 AM
Page 75
C++Builder Projects and More on the IDE CHAPTER 2
TABLE 2.2
75
Continued
Runtime Package
Units Exported from Package
Runtime Functionality for:
Corbacon, Corbaobj, Corbardm, Corbcnst, Corbastd, Databkr, Dbclient, Dsintf, Mconnect, Midas, Midascon, Midconst, Mtsrdm, Objbrkr, Orbpas, Provider, Sconnect
MIDAS Components
vclsmp50.bpl
Calender, Diroutln, Gauges, Spin
Sample Components
vclx50.bpl
Checklst, Colorgrd, Ddeman, Filectrl, Mplayer, Outline, Tabnotbk, Tabs
Win 3.1 and System Components
webmid50.bpl
Compprod, Miditems, Midprod, Scrptmgr, Pagitems, Wbmconst, Webcomp, Xmlbrokr
Internet Express Components
vclmid50.bpl
2
Using Tdump The tdump utility that comes with C++Builder (located in the Bin directory) can be used for examining the contents of BPL, BPI, and LIB files. In fact, any executable or library file can be examined with tdump. Open a DOS window and enter the following line, using the appropriate filenames in place of the names in italic text: tdump FileToBeExamined OutputFileName.dmp
When you press Enter, the file OutputFileName.dmp will contain a text representation of FileToBeExamined. This is a useful utility with which you should be familiar. To see only the exports from a BPL, use the -ee command-line option like this:
C++BUILDER PROJECTS
There are 22 Packages shown in Table 2.2, of which 19 are available in the Professional version of C++Builder. The ADO Package (vclado50.bpl), MIDAS Package (vclmid50.bpl), and Internet Express Package (webmid50.bpl) are available only in the Enterprise edition. This table can be used as a reference if you need to know which Package to add to your Requires section in a Package.
04 9721 CH02
76
11/13/00
9:40 AM
Page 76
C++Builder 5 Essentials PART I tdump –ee FileToBeExamined.bpl OutputFileName.dmp
To see only the units imported to a BPL, use the -em command line option like this: tdump –em FileToBeExamined.bpl OutputFileName.dmp
You can see the different options available simply by typing tdump and pressing the Enter key.
Introducing New IDE Features in C++Builder 5 C++Builder 5 contains many new features in the IDE. We’ll look at several of the most useful additions, including property categories and property images in the Object Inspector, the new Extensible Markup Language (XML) project file format, how form files are now saved in a text format, node-level project options, the project wide and source-code level To-Do list, and the new Console Wizard.
Property Categories in the Object Inspector In Chapter 1 we described how the IDE’s Object Inspector can be used to display and edit component properties and event handler functions. C++Builder 5 introduces the idea of property categories. All properties (including events that are also properties) can now be arranged by category in the Object Inspector, as well as the normal alphabetical listing. The purpose of categories is to allow the logical grouping of related properties. A property may belong to more than one category, if appropriate. Additionally, it is possible to hide a property from the Object Inspector by hiding its category. This is referred to as filtering. Throughout the following sections, we will show you how to use property categories to group related properties in the Object Inspector window. The information applies equally to events. For information on how to register categories for properties in custom components, see the section “Registering Property Categories in Custom Components” in Chapter 10.
Using Property Categories By default the Object Inspector displays properties alphabetically. To view them by category, right-click on the working area of the Object Inspector and select Arrange, By Category. The property categories will now be displayed in the Object Inspector. These can be expanded or collapsed by clicking the + (expand) or - (collapse) icon (by default they are initially collapsed). This action is persistent: If a category is expanded and the properties are viewed by name before viewing them again by category, the category remains expanded. If another component is selected and it has properties in the same category, those will also be shown expanded. To filter which property categories are displayed in the Object Inspector, right-click again and select View. A category is checked or unchecked: Unchecked categories are hidden. Selecting View, All automatically checks all categories, selecting View, None unchecks all cat-
04 9721 CH02
11/13/00
9:40 AM
Page 77
C++Builder Projects and More on the IDE CHAPTER 2
77
egories (and therefore hides them), and selecting View, Toggle toggles the state of the categories. That is, checked categories become unchecked and vice versa. Changing the value of a property that occurs in more than one category results in the property’s value being changed consistently in all categories.
Using the Predefined Property Categories There are 13 predefined property (and event) categories, 12 of which are used by the VCL in C++Builder. These are described in Table 2.3, with the kind of property contained in each category and example properties. TABLE 2.3
Category Specification
Action
Contains properties that are managed by actions and whose behavior is related to runtime functionality. The Hint and Checked properties of TMenuItem are in this category. Contains properties that manage the data shown by a component. This category is not currently used by the VCL. It was originally used for the Text, EditMask, and Tag properties, but these can now be found in the Localizable, Localizable, and Miscellaneous categories, respectively. Contains properties whose behavior is related to database operations. The DatabaseName, MasterSource, and OnCalcFields properties of TTable are in this category. Contains properties that are related to drag-and-drop or docking operations. The OnDragOver and DockSite properties of TForm are in this category. Contains properties (and events) that are related to help, hint, or assistance operations. The OnHint and HelpContext properties of TStatusBar are in this category. Contains properties that are related to control input to the component. The OnKeyPress, OnClick, and Enabled properties of TForm are in this category. Contains properties that are related to the layout and visual display of a control at designtime. The OnResize and Width properties of TForm are in this category. Contains properties that are now obsolete. The Ctl3D and OldCreateOrder properties of TForm are in this category. Contains properties that are related to the linking of one component to another. The PopupMenu property of TForm and the DataSource property of TDBGrid are in this category.
Data
Database
Drag, Drop, and Docking Help and Hints
Layout
Legacy Linkage
C++BUILDER PROJECTS
Category Name
Input
2
Property Categories in C++Builder
04 9721 CH02
78
11/13/00
9:40 AM
Page 78
C++Builder 5 Essentials PART I
TABLE 2.3
Continued
Category Name
Category Specification
Locale
Contains properties that are related to international locales or compliance with international locale operating systems. The BiDiMode and ParentBiDiMode properties of TForm are in this category. Contains properties that are subject to possible change, depending on where the application is deployed. The BiDiMode, Hint, and Font properties of TForm are in this category. Contains properties that have not been categorized, do not fit into any category, or do not require categorization. The Tag and Name properties of TForm are in this category. Contains properties that are related to the layout and visual display of the control at runtime. The BorderStyle, Color, and Width prop erties of TForm are in this category.
Localizable
Miscellaneous
Visual
The table of categories is a useful indication of how categories are used by the VCL. A real appreciation of which properties can be found in which category will be gained only through a continued use of categories in the Object Inspector.
Images in Drop-Down Lists in the Object Inspector C++Builder 5 extends the TPropertyEditor class to include six new functions (see Chapter 10 for more information) that support the rendering of images in drop-down lists in the Object Inspector. This allows the Object Inspector to present a more intuitive interface for suitable properties. Property editors that overload these functions are shown in Table 2.4, along with VCL properties that are registered to use them. Properties that are registered to use these property editors will display the appropriate images associated with each property value. TABLE 2.4
Property Editors with Built-In Support for Images
Property Editor Class
VCL Property
TBrushStyleProperty
TBrush::Style
TColorProperty
TColor
TCursorProperty
TCursor
TFontNameProperty
TFont::Name
TPenStyleProperty
TPen::Style
TComponentImageIndexPropertyEditor
TImageIndex
TListViewColumnImageIndexPropertyEditor
TListColumn:: ImageIndex
04 9721 CH02
11/13/00
9:40 AM
Page 79
C++Builder Projects and More on the IDE CHAPTER 2
TABLE 2.4
79
Continued
Property Editor Class
VCL Property
TMenuItemImageIndexPropertyEditor
TMenuItem:: ImageIndex
TPersistentImageIndexPropertyEditor
TImageIndex
Properties registered using the TComponentImageIndexPropertyEditor, TListViewColumnImageIndexPropertyEditor, TMenuItemImageIndexPropertyEditor,
and property editors will show images only when the associated property of type TImageList is set to a TCustomImageList-descendant component containing the required images. TPersistentImageIndexPropertyEditor
The TComponentImageIndexPropertyEditor is for TImageIndex properties in components, descendants of TComponent, with a parent component that has a TCustomImageList with the name Images. This property editor will be used for custom components that meet these criteria. If a different name is required for the TCustomImageList property in the parent component, a new property editor must be created that is derived from this one. Two things make this impractical. First, it is not possible to include the header of the $(BCB)\Include\Vcl\stdreg.hpp file where these four TImageIndex property editors are declared. This is because of missing header files included in the unit. Second, the rendering code is not correct for large images and is incorrectly offset. A better approach is to override this property editor, as is shown in Chapter 10. The TListViewColumnImageIndexPropertyEditor and TMenuItemImageIndexPropertyEditor property editors are for TImageIndex properties with a parent component that has a TCustomImageList with the name SmallImages. The TPersistentImageIndexPropertyEditor is for TImageIndex properties in classes that are descendants of TPersistent (but not TComponent) with a parent component that has a TCustomImageList with the name Images. This property editor will be used for classes that meet these criteria. Again, if a different name is required for the TCustomImageList property in the parent component, a new property editor must be created that is derived from this one. The best approach is to override this property editor as shown in Chapter 10.
2 C++BUILDER PROJECTS
If the global variable FontNamePropertyDisplayFontNames ($(BCB)\Include\Vcl\dsgnintf. hpp) is set to false (the default) when using the TFontNameProperty property editor, the font names are not shown in the Object Inspector using the actual font style of the font. Rather, the default Microsoft Sans Serif font is used. This was done because the rendering of such images can be slow on low-end machines or those with many fonts installed. To enable the images, this value must be set to true. This time delay is incurred only the first time the property is displayed, so it is acceptable on most machines. Setting FontNamePropertyDisplayFontNames to true is a non-trivial exercise requiring the use of the Open Tools API. An alternative solution is to install a property editor that overrides TFontNameProperty. This approach is shown in Chapter 10.
04 9721 CH02
80
11/13/00
9:40 AM
Page 80
C++Builder 5 Essentials PART I
Figure 2.7 shows how the ImageIndex property of TMenuItem can be used in conjunction with a TImageList component such that images contained in TImageList are displayed in the dropdown list of this property. To make the image list available, the Images property in the TMainMenu component must be set to (in this case) ImageList1.
FIGURE 2.7 ImageIndex
images in a TMenuItem component.
Figure 2.8 illustrates several property editors that display images in the Object Inspector using a TShape component, added as an example to the form in Figure 2.7. For more information on how to use drop-down list images in custom components, refer to the relevant section in Chapter 10, which also displays a class hierarchy for TPropertyEditor derived classes.
The XML Project File Format In C++Builder 5, project files (*.bpr and *.bpk) have changed from a Makefile format to the Extensible Markup Language (XML) format. You can view and edit the new XML project file in the IDE using Project, Edit Option Source. In previous versions of C++Builder, select Project, View Makefile. For compatibility with existing projects, the Makefile format can be formatted using a template file and exported. Simply select Project, Export Makefile. Alternatively, you can select Export Makefile for the particular project node from the context menu in the Project Manager. You can also export the Makefile using the BPR2MAK.EXE command-line tool.
04 9721 CH02
11/13/00
9:40 AM
Page 81
C++Builder Projects and More on the IDE CHAPTER 2
81
2 C++BUILDER PROJECTS
FIGURE 2.8 Drop-down list images in a TShape component.
When an existing project in the Makefile format is loaded into C++Builder 5, the project file format is automatically changed to the new XML format. A dialog box appears to inform you of this conversion. Listing 2.4 is a simplified version of the project file for the Doodle example program that shipped with C++Builder 4. It is in the original Makefile format. Listing 2.5 shows the same thing in the new XML format as it ships with C++Builder 5. Both have been greatly simplified for illustration purposes and are not working project files. LISTING 2.4
The Simplified Doodle Project File in C++Builder 4 Makefile Format
PROJECT = doodle.exe OBJFILES = doodle.obj main.obj palette.obj CFLAG1 = -I$(BCB)\include;$(BCB)\include\vcl -Od -Hc -H=$(BCB)\lib\vcl40.csm \ ➥-w -Ve -r- -k -y -v -vi- -D$(SYSDEFINES);$(USERDEFINES) -c -b- -w-par \ -w-inl -Vx -tW [Version Info] MajorVer=1 MinorVer=0
04 9721 CH02
82
11/13/00
9:40 AM
Page 82
C++Builder 5 Essentials PART I
LISTING 2.5
The Simplified Doodle Project File in C++Builder 5 XML Format
<MACROS> [Version Info] MajorVer=1 MinorVer=0
To most people it doesn’t matter whether the project file is in Makefile or XML format. The project file is built automatically according to the units that make up the project and the settings in the Project, Project Options dialog. If you use the MAKE command-line utility to compile your projects, you will need to export the project file as a Makefile in order to keep using the MAKE utility.
Forms—Save as Text This new feature of C++Builder version 5 saves forms as text rather than in binary form, as in previous versions. This is a default option, however, so forms may be saved as binary if so desired. Right-click on any project form to get the context menu. The Text DFM option is checked by default, as shown in Figure 2.9. When the form unit or project is saved, the form will be stored as plain text in its DFM file. An example text DFM file is shown in Listing 2.6. LISTING 2.6
An Example DFM File Saved as Text
object Form1: TForm1 Left = 192 Top = 107 Width = 311 Height = 158 Caption = ‘Text Form’ Color = clBtnFace Font.Charset = DEFAULT_CHARSET
04 9721 CH02
11/13/00
9:40 AM
Page 83
C++Builder Projects and More on the IDE CHAPTER 2
LISTING 2.6
83
Continued
Unchecking the Text DFM option of a form will cause the form to be saved as binary. It’s possible to make this the default for all new forms by unchecking the New Forms as Text option on the Preferences tab of the Environment Options dialog box, shown in Figure 2.10. This is reached by selecting Tools, Environment Options from the main C++Builder menu. Two example projects are provided in the TextForms folder on the CD-ROM that accompanies this book. FormText.bpr uses the Text DFM option to save the form as text. FormBinary.bpr does not use the Text DFM option and saves the form in binary format.
The Node-Level Options With the new Node-Level Options feature, you can override some of the project options for specific nodes (such as source files) of the project. There is a set of global project options and there also are local options for each of the nodes in the project, which default to the same as the global options.
2 C++BUILDER PROJECTS
Font.Color = clWindowText Font.Height = -11 Font.Name = ‘MS Sans Serif’ Font.Style = [] OldCreateOrder = False PixelsPerInch = 96 TextHeight = 13 object Label1: TLabel Left = 13 Top = 24 Width = 277 Height = 20 Caption = ‘This form is saved as text (default)’ Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -16 Font.Name = ‘MS Sans Serif’ Font.Style = [fsBold] ParentFont = False end object Button1: TButton Left = 114 Top = 72 Width = 75 Height = 25 Caption = ‘OK’ TabOrder = 0 OnClick = Button1Click end end
04 9721 CH02
84
11/13/00
9:40 AM
Page 84
C++Builder 5 Essentials PART I
FIGURE 2.9 The Text DFM (save as text) option on a form’s context menu.
FIGURE 2.10 The Preferences tab of the Environment Options dialog box.
Setting Global and Node-Level Options The global options can be set in the Project Options dialog, accessible from the Project, Options menu item. They are similar in appearance and function to the project options in previous versions of C++Builder. Options set here become the default for all nodes in the project.
04 9721 CH02
11/13/00
9:40 AM
Page 85
C++Builder Projects and More on the IDE CHAPTER 2
85
We’ll take a look at how this can be used with the Doodle example application that is installed with C++Builder 5. Let’s assume that we want to disable code optimization by default for all units in the project, but that we want to enable speed optimization for the main.cpp unit. The global Project Options dialog for the Doodle application with code optimization disabled by default is shown in Figure 2.11.
2 C++BUILDER PROJECTS
FIGURE 2.11 The global Project Options dialog for the Doodle application with code optimization disabled.
To override the default project options, you can set local options for a particular node through the Local Options dialog. This is accessible via the context menu for the node in the Project Manager. Figure 2.12 shows the Project Manager for the Doodle application, along with the context menu for the main.cpp node. The Local Options menu item is selected.
FIGURE 2.12 The Project Manager and Local Options context menu item for the main.cpp node of the Doodle application.
The Local Options dialog for this node is shown in Figure 2.13 with speed optimization enabled, thus overriding the default project option. Notice that, when you set local settings,
04 9721 CH02
86
11/13/00
9:40 AM
Page 86
C++Builder 5 Essentials PART I
they are highlighted with a different color in the dialog to indicate that they override the default project settings.
FIGURE 2.13 The Local Options dialog and settings for the main.cpp node.
Once a node’s settings are overridden in the Local Options dialog, that node is marked in the Project Manager with a red tick. Figure 2.14 shows how the main.cpp node is marked to indicate that some of the settings have been overridden.
FIGURE 2.14 The main.cpp node is marked to indicate settings have been overridden.
Reverting Node-Level Options To revert a specific locally set option to the project default, select Revert from the context menu of the option in question. To revert all locally set options on a particular page of the Project Options dialog, select Revert from the context menu of the tab. To revert all locally set options for the entire node, select Revert All from the context menu of the Right Click to Revert label. When you’re done, click OK.
04 9721 CH02
11/13/00
9:40 AM
Page 87
C++Builder Projects and More on the IDE CHAPTER 2
87
Uses for Node-Level Options The capability to override project options on a node-by-node basis allows you greater flexibility in the way that the project is built. You probably won’t need to use this feature very often, but it can be valuable in particular circumstances. Some likely instances in which you would override the default project options include the following: • To set size optimization as the default for your project, except for certain speed-critical units set to use speed optimization. • To use a different version of an include file with a particular unit by setting a different include path. • To disable particular warnings for a unit to avoid warning messages for code that is valid. • To use a different C++ language compliance for a unit that was supplied or developed according to a different standard. These are just some of the possible examples. There are undoubtedly many more cases that require node-level options override.
The New To-Do List A great enhancementin C++Builder 5 is the To-Do List. A to-do item can be added globally to the project or locally within your code to associate the item with a particular unit or section of code. A to-do list is an essential item for every programmer. Just about every project has some form of to-do list, whether it is part of the project management software or handwritten. The new To-Do List feature in C++Builder 5 brings that right into the code, making it easier for you to document a task in the first place and faster to find it later on. The Crozzle project (discussed in more detail in Chapter 6, “Compiling and Optimizing Your Application”) will be used to demonstrate the features of the To-Do List in the following examples. You can find it in the folder for Chapter 6 on the CD-ROM that accompanies this book. To-do items can be assigned a Priority (from 0 to 5), an Owner (a person or a group, as appropriate), and a Category. This helps in the overall management of the list, which can grow to a considerable number of items in even the most modest project.
The To-Do List View The To-Do List view is accessed from the View, To-Do List menu item in the C++Builder IDE. It lists three types of to-do items. Global items are shown in normal text. Local items in the open and active unit are shown in bold, and those in other units are shown in gray. The module name for local to-do items is shown in the To-Do List view.
C++BUILDER PROJECTS
• To use special conditional defines to modify the behavior of a unit.
2
04 9721 CH02
88
11/13/00
9:40 AM
Page 88
C++Builder 5 Essentials PART I
Figure 2.15 shows the To-Do List view for the Crozzle project, displaying several list items. In this section we will look at general item management through the use of the To-Do List view. The specific details and use of global and to-do items in particular are shown in following sections.
FIGURE 2.15 Crozzle project to-do list items displayed in the To-Do List view.
In the To-Do List view, most tasks are performed through the context menu. You can add global items, delete and edit any item, sort items on any field, and filter them by Category, Owner, Type, and Status. The to-do list can be copied to the Clipboard as text or to an HTML table for use in other documentation or software (for example, paste it into your project development intranet Web page). The HTML table properties are configurable from the Table Properties context menu item. To mark a to-do item as complete, simply check its Status check box. It will remain in the list until deleted.
Global To-Do List Items Global to-do items are stored in the file ProjectName.todo, which is automatically created and added to your project when a global to-do item is added. Examples of global items include • Milestone date reminders for a project • A list of features to be implemented • A list of features to add in the next version • Bugs that have not yet been tracked to a specific piece of code If you load the CrozzleProj.todo file of the Crozzle project into a text editor (such as Notepad), you will see a list of to-do list items enclosed in braces ({}). They contain various fields and a description. These form the global to-do list for the project. Use the To-Do List view in the C++Builder IDE to manage them.
04 9721 CH02
11/13/00
9:40 AM
Page 89
C++Builder Projects and More on the IDE CHAPTER 2
89
To add a global list item to the project, right-click and select Add from the context menu in the To-Do List view. Enter appropriate values for the description, priority, owner, and category and click OK. All previously used values for Owner and Category in the current project can be selected from the drop-down lists.
Local To-Do List Items A local to-do item appears as a special comment within your code. It is automatically added to your source code when you add a method to an object using the Add Method dialog from the context menu of the Class Explorer.
• A bug note • A note that a piece of code needs improvements • To mark code that looks suspicious • To help find your place later • To indicate changes that need to be made to user manuals as the result of a code change Local to-do list items are simply declared as C++ style comments within your code in a particular format. They are similar in appearance to the global to-do list items in the projectname.todo file. Listing 2.7 shows a portion of the Crozzle project code that contains a local to-do list item. LISTING 2.7
A Local To-Do List Item in CrozzleSolve.cpp
//--------------------------------------------------------------------------void __fastcall TCrozzleForm::HelpAboutItemClick(TObject *Sender) { TAboutForm *AboutForm = new TAboutForm(Application); /* TODO 5 -oJarrod -cEnhancement : Position the about box in centre of ➥crozzle window. */ AboutForm->ShowModal(); }
The comment is constructed with a number of fields. Some are optional and are not included if the value was not given. A priority of zero is not shown in the comment. The comment format is /* status [priority] [-o owner] [-c category ] :
description
*/
2 C++BUILDER PROJECTS
You can also add to-do list items manually. When doing so, you specify where in the code they are to be placed. They can be added just about anywhere within the code (including header files) except at the top of a unit above the include file declarations. Some practical uses for local to-do items include
04 9721 CH02
90
11/13/00
9:40 AM
Page 90
C++Builder 5 Essentials PART I
LISTING 2.7
Continued
or // status [priority] [-o owner] [-c category] : description
The easiest way to add a local to-do item is to select Add To-Do Item from the context menu by right-clicking in the appropriate place in the Code Editor or by pressing Shift+Ctrl+T to insert an item at the cursor. Items can also be entered manually by typing them into the Code Editor in the correct format (the code is continually parsed, and they will be picked up automatically).
CAUTION Because the text from the to-do item description is entered into a comment within your code, make sure that you don’t use the /* or */ character combinations in the description, Doing so will cause the To-Do List view, Class Explorer, Code Insight, and the compiler to interpret the comment incorrectly.
From the To-Do List view, you can jump straight to the code of a local to-do item by doubleclicking it or selecting Open from the context menu. When a local item is marked as Complete (through the To-Do List view), the comment remains in the code. The status word TODO in the comment is changed to DONE. You might want to leave completed items in the code to keep a history of them. To delete a pending or completed to-do item, either select Delete from the context menu in the To-Do List view or delete the comment in the code. Experiment with the To-Do List feature and make a point of using it diligently. Don’t let the list get out of date!
The Console Wizard Several new applications wizards are included with C++Builder version 5, one of these being the Console Wizard. This wizard provides a few simple options to make it easier to build console applications, which are Win32 applications without the usual graphical user interface.
Starting the Console Wizard The Console Wizard, as with all application wizards, may be started by selecting File, New from the C++Builder main menu. Select the Console Wizard icon from the New Items form, as shown in Figure 2.16.
04 9721 CH02
11/13/00
9:40 AM
Page 91
C++Builder Projects and More on the IDE CHAPTER 2
91
2 The new application Console Wizard icon.
The options dialog will then be displayed. These options are • Source Type Specifies whether the source language will be C or C++. • Use VCL Allows use of VCL components. This option is available only if C++ is specified as the source language. • Multi Threaded Specifies multiple execution threads. This option is automatically selected if Use VCL is checked. • Console Application Checking this option will cause a console window to be created for the application. It is also possible to specify the location of an existing source code file, which will then be automatically loaded. The options dialog is shown in Figure 2.17.
FIGURE 2.17 The Console Wizard options dialog.
When the options are set and you click OK, a skeleton function is created in the Code Editor. For example, checking only the C++ and Console Application options causes the main() skeleton function, shown in Listing 2.8, to be created.
C++BUILDER PROJECTS
FIGURE 2.16
04 9721 CH02
92
11/13/00
9:40 AM
Page 92
C++Builder 5 Essentials PART I
LISTING 2.8
Skeleton Code for a Console Application
//------------------------------------------------------------#pragma hdrstop //------------------------------------------------------------#pragma argsused int main(int argc, char* argv[]) { return 0; } //-------------------------------------------------------------
A Simple Console Application This brief example uses the C Source Type option with Console Application checked to generate a console window. Adding the code to the main() skeleton function as shown in Listing 2.9 will generate the output shown in Figure 2.18 when run. You can find the complete project, with the project filename MyConsole.bpr, in the Console folder on the CD-ROM that accompanies this book. LISTING 2.9
The Simple Console Application Code in MyConsoleCode.c
//------------------------------------------------------------#pragma hdrstop #include <stdio.h> #include //------------------------------------------------------------#pragma argsused int main(int argc, char* argv[]) { printf(“Hello World!\n”); getch(); return 0; } //-------------------------------------------------------------
04 9721 CH02
11/13/00
9:40 AM
Page 93
C++Builder Projects and More on the IDE CHAPTER 2
93
2 Simple console application output.
Summary In this chapter we have looked at C++Builder projects in detail, learned how to reuse projects and other program elements using the Object Repository, and gained an understanding of what Packages are and how they can be used. We also looked at several new IDE features in C++Builder 5. The IDE is very powerful and is a great aid to productivity, and the features explained in this chapter will make development easier for you. We’ll make use of some of these features in later chapters.
C++BUILDER PROJECTS
FIGURE 2.18
04 9721 CH02
11/13/00
9:40 AM
Page 94
05 9721 CH03
11/13/00
9:49 AM
Page 95
Programming in C++Builder Jamie Allsop
IN THIS CHAPTER • Coding Style to Improve Readability • Better Programming Practices in C++Builder • Further Reading
CHAPTER
3
05 9721 CH03
96
11/13/00
9:49 AM
Page 96
C++Builder 5 Essentials PART I
This chapter introduces important concepts concerned with how code is written in C++Builder. The first section concentrates on how the readability of code may be improved. Optimizing the readability of code is very important. More readable code is easier to understand and maintain, and this can have a big effect on the cost of a software project. It is more than just making code look pretty on a page; it is about augmenting the code’s description such that its purpose, intent, and behavior are adequately reflected. Adopting a coding style and applying it consistently is probably the easiest way to achieve more readable code. However, some styles offer greater improvement than others, and this section looks at a variety of approaches to several considerations in an effort to give the reader some insight into which styles they should adopt. More importantly, it offers suggestions as to why certain styles are possibly more effective than others as well as warnings on the application of certain styles. Some techniques in moderation are very effective but when overused will decrease readability. Situations such as this are highlighted. The fundamental elements of coding style are examined in turn, and guidelines are presented on how each element can affect readability, along with suggestions on how each element can be used to best effect. Where appropriate, effective use of the IDE is also covered. The second section covers a variety of topics concerning the writing of C++ code in C++Builder applications. A wide range of topics is covered, from the use of new and delete to the use of const in programs. The main purpose of the section is to highlight areas of coding that are often misunderstood or misused. Often, certain concepts are not fully appreciated, particularly by programmers whose background is not in C++. However, the main target audience is those whose background is in C++. They will find explanations of specific issues concerning the use of C++ in C++Builder and should therefore benefit from the topics covered. Some topics begin with more straightforward material before moving the discussion to advanced concepts. Also, many of the topics address issues that arise as a result of the Object Pascal heritage of the VCL (Visual Component Library). Finally, the last section, “Further Reading,” lists the references that appear throughout the chapter, along with a brief description of the reference itself. You are encouraged to seek out these references; reading them should prove very beneficial.
Coding Style to Improve Readability This section looks at some of the issues in improving the readability of code and offers possible methods of achieving this. Regardless of which styles and methods are chosen, the one thing that should be remembered is that you must be consistent. Consistency is very important (note that code in this section is written in different styles to illustrate points in the text).
05 9721 CH03
11/13/00
9:49 AM
Page 97
Programming in C++Builder CHAPTER 3
97
The Use of Short and Simple Code It may be obvious, but always try to keep any given block of code short and simple. This achieves two things. First, it means that your code is ultimately the culmination of many smaller pieces of code, each of which serves a specific and understandable role. This means that at different parts of the code the complexity of the code is governed only by the level of abstraction, not by the amount of code present. Consider the two functions in Listing 3.1. LISTING 3.1
Code Complexity and Level of Abstraction
#include double GetMaximumValue(const std::vector<double>& Vector) throw(std::out_of_range) { double Maximum = Vector.at(0);
return Maximum; } void NormalizeVector(std::vector<double>& Vector) { if(!Vector.empty()) { double Maximum = GetMaximumValue(Vector); for(int i=0; i
Both of the above functions are similar in terms of the complexity of the code that they contain, but the second function, NormalizeVector, performs in total a more complex task, because the level of abstraction is higher.
3 PROGRAMMING IN C++BUILDER
for(int i=0; i Maximum) { Maximum = Vector[i]; } }
05 9721 CH03
98
11/13/00
9:49 AM
Page 98
C++Builder 5 Essentials PART I
TIP Use the pre-increment operator instead of the post-increment operator—for example, in Listing 3.1, ++i is used in preference to i++. Using either of the increment operators will increment i, but the post-increment version will return the old value of i. Since this is not required, the extra processing time is wasted. Typically, the postincrement operator is implemented in terms of the pre-increment operator. The pre-increment operator is therefore faster than the post-increment operator. For built-in types such as int, there is little difference in speed, but for user-defined types this may not be the case. Regardless, if the post-increment operator is repeatedly used unnecessarily, there will be a cost in terms of performance.
Second, it means that when you are reading code you are never too far away from local variable declarations and function parameters (which contain type information). To this end, if you find yourself writing large pieces of code that perform several tasks, consider how the code could be separated into smaller conceptual blocks and rewritten accordingly.
The Use of Code Layout The easiest way to improve the layout of your code is to make sure that braces ({}) are placed on their own line and that matching pairs line up. Code inside braces should then be indented by a predetermined amount that is used consistently throughout the program. Typically, two to four spaces are used as a suitable indent, though some use more and some use only one. (Beware of too many spaces; this can actually degrade readability.)
TIP Make sure the Use Tab Character setting on the General tab in Tools, Editor Options is unchecked. This ensures that spaces are inserted for tabs. In general, avoid using Tab; use spaces instead.
Why is this so helpful? First, it allows fast visual scanning of the code. The code can be broken quickly into its constituent functional blocks, and each block’s nature can be seen from the line starting the block. Second, the scope of the code is very clear. It is obvious which variables are in or out of scope at any given time, and this can be helpful in tracking down problem code. This is an important feature of this approach. Remember that the scope of any given block includes the block header statement (such as a for statement or a function header), and the functional part of the block is the code contained by the braces. Aligning the braces with the block header statement and indenting the functional code show this logical relationship explicitly.
05 9721 CH03
11/13/00
9:49 AM
Page 99
Programming in C++Builder CHAPTER 3
99
The reason this layout style is so effective is that it is easier to match a pair of braces than it is to match an end brace with a keyword (or other permutations), due to the lack of symbolic similarity. It is also easy to maintain, because each opening brace must have a corresponding ending brace in the same column.
TIP The IDE allows you to indent and unindent selected blocks of code using, respectively, CTRL+SHIFT+I and CTRL+SHIFT+U. The number of spaces that the editor indents or unindents the selected code is determined by the value of the Block indent setting on the General tab in Tools, Editor Options. Setting this value to 1 (one) offers the greatest flexibility.
When code is laid out as previously described—braces on their own line, matching pairs of braces lined up, and code within the braces indented—it can be read easily. Loop constructs can be marked as blocks, and nested constructs become very clear. No room is left for ambiguity. One instance in which this can be particularly useful is in the use of nested if-else statements. The following code is unclear: #include // We have : int A <= int B <= int C if(A + B > C) if((A==B) || (B==C)) if((A==B) && (B==C)) std::cout << “This is an EQUILATERAL triangle”; else if( (A*A + B*B) == C*C ) std::cout << “This is a RIGHT-ANGLED ISOCELES triangle”; else std::cout << “This is an ISOCELES triangle”; else if( (A*A + B*B) == C*C )std::cout << “This is a RIGHT-ANGLED triangle”; else std::cout << “This is a TRIANGLE”; else std::cout << “This is NOT triangle”;
3 PROGRAMMING IN C++BUILDER
Indenting both the braces and the code contained in them together is not as effective, because the braces are hard to pick out in the code. For single blocks of code this is not such a problem, but for nested blocks it can become confusing. That said, some still prefer this approach. For an alternative discussion of this, please refer to A Practical Handbook of Software Construction by McConnell, 1993, p. 399. Be aware that most of the discussion presented in the book refers to languages that use keywords to show control block structures, such as begin and end, and the considerations involved are therefore somewhat different, though the distinction is not considered by the text. Tread cautiously.
05 9721 CH03
11/13/00
100
9:49 AM
Page 100
C++Builder 5 Essentials PART I
Its meaning is straightforward when written as follows: #include // We have : int A <= int B <= int C if(A + B > C) { if((A==B) || (B==C)) { if((A==B) && (B==C)) { std::cout << “This is an EQUILATERAL triangle”; } else if( (A*A + B*B) == C*C ) { std::cout << “This is a RIGHT-ANGLED ISOCELES triangle”; } else { std::cout << “This is an ISOCELES triangle”; } } else if( (A*A + B*B) == C*C ) { std::cout << “This is a RIGHT-ANGLED ISOCELES triangle”; } else std::cout << “This is a TRIANGLE”; } else std::cout << “This is NOT triangle”;
Always consider if there is a better, clearer way to write code, especially if you find yourself writing large nested if and if-else statements. If you are writing a large if-else block, consider replacing it with a switch statement. This may not always be possible. In the case of large nested if statements, try restructuring the code into consecutive if-else statements. In a similar vein, try to keep lines of code short. This makes it easier to read in the editor window and also makes it easier to print out.
TIP Often when code is printed, some code lines are too long for the page onto which they are printed. When this occurs, one of two things happens: The lines are wrapped to the start of the next line or the lines are simply truncated. Both are unsatisfactory and degrade the readability of the code.
05 9721 CH03
11/13/00
9:49 AM
Page 101
Programming in C++Builder CHAPTER 3
101
The best way to prevent this is to ensure that excessively long lines of code either are avoided or that they are carefully broken to multiple lines. When you are writing code in the Code Editor, the right margin can be used as a guide to the width of the printable page that you use. Change the right margin setting on the Display tab in the Tools, Editor Options menu so that the value represents the absolute printable right margin. In other words, set it so that characters that appear after this margin either are not printed or are wrapped to the next line (if Wrap Lines is checked in the File, Print menu). This will depend on the page size used and the value of the left margin setting (also in the File, Print menu). For A4 paper and a left margin setting of 0, the right margin should be set to 94. This means that code lines that extend past this line will not be printed as they appear on the screen. To ensure that the right margin is visible in the Code Editor, make sure that the Visible Right Margin setting on the Display tab in the Tools, Editor Options menu is also checked.
For heavily nested loops or selection constructs, decrease your indent size if it is large or redesign the code so that some of the work is carried out by functions. switch
statements can be written differently. For example
switch(Key) { case ‘a’ : // a very long line of code that disappears ... break; case ‘b’ : // another very long piece of code ... break; default : // Value not required – default processing break; }
This can be rewritten as switch(Key) { case ‘a’ : // a very long line of code that can now all be seen break; case ‘b’
3 PROGRAMMING IN C++BUILDER
Common reasons why code lines can become excessively long include heavily nested loops or selection constructs, use of switch statements with complex code inside, trying to write for and if statements on the same line, long function parameter lists, long Boolean expressions inside if statements, and string concatenation using the + operator.
05 9721 CH03
11/13/00
102
9:49 AM
Page 102
C++Builder 5 Essentials PART I : // another very long piece of code that can also be seen break; default : // Value not required – default processing break; }
With for and if statements all on one line, place the code executable part of the statement on a separate line. For example for(int i=0; i<10; ++i) // long code that disappears ...
This can be replaced by the following: for(int i=0; i<10; ++i) { // long piece of code that no longer disappears }
The if statement can be similarly written. For example if( Key == ‘a’ || Key == ‘A’ ) // long code that disappears ...
This can be replaced with if( Key == ‘a’ || Key == ‘A’ ) { // long piece of code that no longer disappears }
In fact, it is better practice to write such one-line statements in this way, because it allows the debugger to trace into the line of code that is to be executed. Long Boolean expressions inside if statements should be written on several lines, such as in the following: if(Key == VK_UP || Key == VK_DOWN || Key == VK_LEFT... { // Code here }
This is better written as follows: if(Key == || Key || Key || Key || Key || Key {
VK_UP == VK_DOWN == VK_LEFT == VK_RIGHT == VK_HOME == VK_END)
05 9721 CH03
11/13/00
9:49 AM
Page 103
Programming in C++Builder CHAPTER 3
103
// Code here }
This code raises an important point concerning the placement of the || or similar operator. The reason for placing it on the left side of each line is that we read from left to right. Placing it on the right makes the reading both slower and less natural, and it distorts the emphasis of the expression. Placing the operator on the right side of each line in the expression to show that there is more after the line is unnecessary, because people tend to read code by scanning blocks, not by reading individual lines as a computer does. Long function parameter lists can be dealt with similarly, by placing each parameter on a new line. For example void DrawBoxWithMessage(const AnsiString &Message, int...
This can be rewritten as shown in Listing 3.2. LISTING 3.2
Writing Long Function Parameter Lists
It is important to place the comma at the end of each line. Placing the comma at the start of each line makes the code more difficult to read, because you would not expect to encounter a comma in this position in normal written text. The comma is for the compiler to separate the parameters and has no other meaning; its use should not unduly confuse someone reading the program. Note that the positioning of the comma is in contrast to the previous discussion of operator placement. The same approach can be taken with long string concatenations: AnsiString FilePath = “”; AnsiString FileName = “TestFile”; FilePath = “C:\\RootDirectory” + “\\” + “Branch\\Leaf” + ...
This can be rewritten as shown in Listing 3.3. LISTING 3.3
Writing Long String Concatenations
AnsiString FilePath = “”; AnsiString FileName = “TestFile”; FilePath = “C:\\RootDirectory” + “\\”
3 PROGRAMMING IN C++BUILDER
void DrawBoxWithMessage(const AnsiString &Message, int Top, int Left, int Height, int Width);
05 9721 CH03
11/13/00
104
9:49 AM
Page 104
C++Builder 5 Essentials PART I
LISTING 3.3
Continued + “Branch\\Leaf” + FileName + ”.txt”;
The code in Listing 3.2 and Listing 3.3 is very clear, but it may not be very easy to maintain due to the amount of indentation required. For those who find adding lots of spaces difficult (some people really do!), an alternative approach is to use the standard indent for each of the following lines in such an expression. For example, if you are using a three-space indent, then the code from Listing 3.2 would become void DrawBoxWithMessage(const AnsiString &Message, int Top, int Left, int Height, int Width);
This degrades readability but results in code that is easier to maintain, a rather dubious tradeoff, but sometimes a reasonable one.
TIP You can save time writing code by using the code templates. These are accessed from the editor by pressing Ctrl+J and then using the up and down arrow keys or the mouse to select a template. However, remember that in order to maintain consistency with your own coding style and to get the best use of code templates, you should customize the templates that C++Builder provides. To edit code templates, go to Tools, Editor Options and select the Code Insight tab. Note that the | character is used to indicate where the cursor will be placed initially when the template is pasted into the editor. Code templates can also be edited manually in the $(BCB)\Bin\Bcb.dci file (where $(BCB) is the C++Builder 5 installation directory).
The Use of Sensible Naming One of the best ways to improve the readability of code is to use sensible naming for variables, types, and functions. Type names include those used for naming actual types; for example int is the type name for an integer, TFont is the type name for the VCL’s font class. Variable names are the names of variables that are declared to be of a specific type, for example, in the code int NumberOfPages; NumberOfPages
is the name of a variable of type int.
05 9721 CH03
11/13/00
9:49 AM
Page 105
Programming in C++Builder CHAPTER 3
105
Function names are given to functions to describe what they do. We’ll consider variable names first, though most of what is said about variable names applies equally to type names and function names.
Choosing Variable Names to Indicate Purpose Generally, you want to choose a name that reflects a variable’s nature and purpose. If possible, it should also suggest what type the variable is. For example, String EmployeeName; is better than String S;. When the variable pops up later in code, you will have no idea what S is for. For example, is it the number of pages in a book or a string representing a person’s name? You also won’t know what it is—for example, an int, a double, a String? EmployeeName is obvious: It is a variable that holds the name of an employee, and it is most likely a string. Using descriptive names such as this is easy to do and makes everyone’s life much easier when the code is read at a later date. Every time a new variable is declared, ask the question, “What is the variable’s purpose?” Summarize the answer to the question and you have a sensible variable name.
String String String String
EmployeeName; employee_name; Employee_Name; employeeName;
A disadvantage of using underscores is that variable names can quickly become very long. These are okay technically, but they start to make code look cluttered and also increase the symbolic appearance of variable names. Some suggest that all variable names should start with a lowercase letter, such as the employeeName example. This is often done to separate variable names from type names, which would typically start with a capital letter. Others start a variable name with a lowercase letter only when it is a temporary variable, such as a temp variable in a swap function. A compromise should be found so that a variable name is meaningful but also concise. That said, the meaningfulness of a variable name is most important.
Modifying Variable Names to Indicate Type In general, knowing the purpose of a variable is often more important to the understanding of a piece of code than knowing what type the variable is. However, there are times when you may want to remind yourself of the variable type because special rules might apply. A simple example to illustrate this is given in Listing 3.4.
3 PROGRAMMING IN C++BUILDER
When naming a variable, a word or short phrase is often used. Using a capital letter to start each word in the variable name is a popular method of making the name clear and easy to read. Others prefer all lowercase with underscores to separate words and, still others, a mixture of both. To illustrate, the previous declaration could be written as any of the following:
05 9721 CH03
11/13/00
106
9:49 AM
Page 106
C++Builder 5 Essentials PART I
LISTING 3.4
Illustrating the Need to Be Aware of a Variable’s Type
int Sum = 0; int* Numbers = new int[20]; for(int i=0; i<20; ++i) { Numbers[i] = i*i; Sum += Numbers[i]; } double Average = Sum/20; // Sum is an int, needs cast as a double
The answer to the code in Listing 3.4 should be 123.5, but the value in Average will be 123. This is because Sum is an int, and when you divide by 20 (treated as an int), you get an int result that is assigned to Average.
TIP Try to declare variables just before they are used; this helps ensure that variables are created only if they are actually needed. If code contains conditional statements (or throws an exception), it may be that some of the variables declared are not used. It is sensible not to incur the cost of creating and destroying such variables unless they are used. Be wary, though, of placing declarations inside loops, unless that is what is intended. It is also a good idea to initialize variables when they are declared. On a similar note, never mix pointer and non-pointer declarations on the same line. Doing so is confusing at best. It is preferable to have variables declared as Type VariableName;.
Hence, a pointer to an int should be written as int* pointerToInt;
But if we write int* pointerToInt, isThisAPointerToInt;
then PointerToInt is a pointer to an int and isThisAPointerToInt is not a pointer to an int, it is an int. Actually, this is what the declaration is saying: int *pointerToInt, notPointerToInt;
This is still not clear, and the declaration is no longer written in the Type VariableName format. The solution is to write the declarations on separate lines. The ambiguity then disappears. int* pointerToInt = 0; int notPointerToInt;
05 9721 CH03
11/13/00
9:49 AM
Page 107
Programming in C++Builder CHAPTER 3
107
You should ensure that pointers are explicitly initialized either to NULL (0) or to some valid memory location. This prevents the accidental use of wild pointers (those that point to an undefined memory location).
To obtain the expected result of 123.5 for Average in Listing 3.4, the last line of the code snippet should be as follows: double Average = static_cast<double>(Sum)/20; // Performs as expected
This results in Sum being cast as a double before being divided by 20 (treated as a double). Average is now assigned the double value 123.5.
TIP Make sure you are familiar with the four types of C++ casts, and always use C++-style casting in preference to C-style casting. C++-style casts are more visible in code and give an indication of the nature of the cast taking place.
Sometimes type information may be required. One method of adding type information to a variable name is to use a letter (or letters) as a symbol at the start or at the end of each variable name. For example, you might use b for bool, s for a string, and so on. A problem with this approach is that type information can be added unnecessarily to too many different types of variables. The emphasis is then invariably placed on the type information and not on the variable’s purpose. Also, there are many more possible types for a variable than there are letters of the alphabet. As a result, such symbols can themselves become confusing and complex. An infamous example of such a convention is the Hungarian Notation commonly seen in Win32 API code.
NOTE The Hungarian Notation has its roots in Win API programming and advocates adding symbols to all variables to indicate their type. Since there aren’t enough letters for all types, oddities in the notation are common. For example, Boolean variables are pre-
PROGRAMMING IN C++BUILDER
This example is somewhat trivial, and adding type information to the variable name would be a bit like using a sledgehammer to crack a nut. A more sensible solution is probably to declare Sum as a double.
3
05 9721 CH03
11/13/00
108
9:49 AM
Page 108
C++Builder 5 Essentials PART I
fixed with an f, strings with an sz, pointers with p, and so on. A complete list is not appropriate here. The notation is infamous because many feel it creates more problems than it solves. These arise mostly from inconsistencies (such as the variable wParam being a 32-bit unsigned integer, not a 16-bit unsigned integer as the prefix implies) and names that are difficult to read. A perusal of the Win32 API help files should reveal some examples of the notation. Heavily typed notations are dangerous because they place the emphasis of a variable name on the variable’s type, which often does not tell you much about the variable.
When reading such code, it is not always easy for the reader to mentally strip away the type codes, and this can decrease the code’s overall readability. If you want prefixed (or even appended) type symbols, a compromise is to restrict the use of added symbols to only a few specific types.
Modifying Variable Names to Indicate Characteristics or Restrictions A variable’s name can also be used to convey information regarding some characteristic that a variable may have or to point out some restriction that may be applicable. It is important to know when a variable is a pointer, because pointers can easily wreak havoc in a program. For example, if you have a pointer to an int and accidentally add 5 to it without dereferencing it first, you have a problem. It is good practice to use a pointer only when no other type can be used, such as a reference (which are implicitly dereferenced). It is also sometimes important to know when a variable is static, when a variable is a class data member, or when a variable is a function parameter. Suitable symbols that could be prefixed would be p_ for a pointer, s_ for a static variable, d_ or m_ for a class data member, and a_ for a function parameter. (An a_ symbol is used to differentiate a function parameter from a pointer and should be read as “where the parameter a_x represents the argument passed to the function.” The use of a_ then becomes reasonable.) An underscore is often used to separate a prefix from the variable name proper. This makes it easier to strip away the prefix when reading the code. If a separating underscore is used with an information symbol, then it is possible to append the symbol to the end of the variable name. This has the advantage of allowing the variable name to be read more naturally. For example, when reading s_NumberOfObjects, you would probably say “this is a static variable that holds the number of objects,” whereas reading NumberOfObjects_s, you might say “this variable holds the number of objects and is static.” This places the emphasis on the purpose of the variable (to store the number of objects). Which you prefer is probably a matter of personal choice. A problem with all such symbols occurs when more than one is applicable; then the syntax is not so tidy. Solutions such as always prefixing p_ for pointers and appending the other symbols can solve most of these problems, but not all.
05 9721 CH03
11/13/00
9:49 AM
Page 109
Programming in C++Builder CHAPTER 3
109
Another common situation that often receives special attention is the naming of variables whose values do not change, in other words constants. Such variables are often named using all capital letters. When you declare variables of certain classes, it is often sensible to include the class name (without any prefixed letter, for example T or C, if it is present; this is explained shortly) as part of the variable name. This is often done by prefixing some additional information to the name, or it can be as crude as appending a number to the class name as the C++Builder IDE does, though generally a little more consideration should be applied. For example, consider the following variable declarations. TComboBox* CountryComboBox; TLabel* NameLabel; String BookTitle;
In the case of TComboBox and TLabel, it is appropriate to include the class name as part of the variable name. However, in the case of BookTitle, it is fairly obvious that it is a string, and adding the word String to the variable name perhaps does more harm than good.
Choosing Type Names As was mentioned earlier, the naming of types should be approached in a fashion similar to the naming of variables. However, some conventions need to be considered. For classes, convention says that if the class derives from TObject, then the class name should be prefixed with the letter T. This lets the user of the class know that it is a VCL-style class, and it is consistent with the naming of other VCL classes. This has a beneficial side effect. Variables declared of the class can use the class name without the prefix, making it obvious what the variable is. Non-VCL (that is, normal C++) classes can also use a prefix, but it is wise perhaps to use a prefix other than T, such as C to indicate that the normal C++ object model applies and reinforce that the class does not descend from TObject. This distinction can be important: VCL-style classes must be created dynamically; non-VCL classes do not have this restriction. The naming of other types, such as enums, Sets, structs, and unions, can be handled in a similar fashion. By convention, C++Builder prefixes a T to enumeration and Set names, though some may prefer not to follow this convention, which has no specific meaning. Avoid using an E as a prefix; C++Builder uses this for its exception classes.
3 PROGRAMMING IN C++BUILDER
Of special note in C++Builder is the naming of private member variables, which have a corresponding __property declaration. By convention, such variables are prefixed with the letter F (for Field). You should follow this convention and avoid using a capital F prefix for other purposes. The purpose of a prefix in this case is to allow the property name to be used unchanged (it is not necessary to think of a similar-sounding variant name).
05 9721 CH03
11/13/00
110
9:49 AM
Page 110
C++Builder 5 Essentials PART I
Because enums and Sets are commonly used in C++Builder, it is worth mentioning some points specifically related to their naming. A Set is a template class declared in $(BCB)\Include\VCL\sysset.h as template ➥ class RTL_DELPHIRETURN Set;
Ignoring RTL_DELPHIRETURN, which is present for VCL compatibility, we can see that a Set template takes three parameters: a type parameter and two range-bounding parameters. Hence, a Set could be declared as Set CapitalLetterMask;
If a Set is to be used more than once, a typedef is normally used to simplify its representation, as seen here: typedef Set TCapitalLetterMask; // Later in the code TCapitalLetterMask CapitalLetterMask; Sets
are often used to implement masks (as in this case), hence the use of the word Mask in the names used in the previous code. An enumeration is typically used to implement the contents of a Set. For example, the following definitions can be found in $(BCB)\Include\VCL\graphics.hpp:
enum TFontStyle { fsBold, fsItalic, fsUnderline, fsStrikeOut }; typedef Set TFontStyles;
Most enums used by the VCL are to facilitate the use of Sets. For convenience they are declared at file scope. Simply including the file allows easy access to the Set. This means that the potential for a name collision is high. To avoid such collisions, the “initials” of the enum name are prefixed to each of the values that the enum can take. This minimizes the chance of a name collision. This is good practice and should be used in your own code. For example, the initials of TFontStyle (excluding the T) are fs, which is prefixed to each of the enums. Another method that can be used to prevent name collisions is to place enums and typedefs inside the class definitions that use them. This means that such enums and typedefs, when called from outside the class, must be qualified by the class namespace. The same can be applied to const values. Using a typedef in certain situations (such as this one) can improve readability. A typedef can also improve readability in the declaration of function pointers (and particularly __closures, i.e. events, discussed in Chapter 9, “Creating Custom Components”). Beyond situations such as these, typedefs should be used sparingly because you are actually hiding the type of the variable. Using typedefs too much will result in confusion.
05 9721 CH03
11/13/00
9:49 AM
Page 111
Programming in C++Builder CHAPTER 3
111
Choosing Function Names Function names should be precise and describe what the function does. If it is not possible to describe precisely what a function does, then perhaps the function itself should be changed. Different kinds of functions should be named in slightly different ways. A function that does not return a value (a void function) or returns only function success information—in other words, those that return no data—should generally be named using an object verb name, such as CreateForm(), DisplayBitmap(), and OpenCommPort(). A member function of this type often does not require a qualifying object, because the object that calls the function generally can fulfill that role. If a function does return a data value, then the function should be named to reflect the nature of the return value, such as GetAverage(), log10(), and ReadIntervalTimeOut(). Some prefer to make the first word in a function name lowercase. This is a matter of personal preference, but consistency should be maintained.
Adhering to Naming Conventions How variables, types, and functions are named is an extensive topic, encompassing a myriad of concerns. Mentioning every convention that could be used is impossible. That being the case, you should endeavor to adhere to the following guidelines: • Name a variable or a function such that its purpose is obvious. If this means more typing, then so be it. • Name a type so that its intended use is obvious. Try not to use a name that is an obvious choice for a variable of the type. Prefixing a letter symbol can help. • Be consistent! With practice, even the most obscure naming system can be understood if it is consistently applied.
The Use of Code Constructs One of the best ways to improve the readability of code is through the appropriate use of code constructs—using the right tool at the right time for the right job. Therefore it is important to understand when to use const (and when not to), when to use references instead of pointers, which loop statement is most appropriate, whether to use multiple if-else statements or a single switch statement, when to represent something with a class and when not to, when to throw an exception and when to catch it, how to write an exception specification, and so on.
3 PROGRAMMING IN C++BUILDER
In general, function names can be longer than variable names, and if you need to write a long function name, you should not be overly concerned. However, take care to ensure that a long function name is not the product of a poorly designed function that tries to perform too many poorly related operations. In fact, you should endeavor to write functions that perform a single well-defined task, and the name of the function should reflect that. If you must write a function that does more than one thing, the name should make that obvious.
05 9721 CH03
11/13/00
112
9:49 AM
Page 112
C++Builder 5 Essentials PART I
Most of these are design issues, but some are simple to add to code and can improve not only readability but also robustness. The use of const and references (along with other coding issues) is discussed in the next section. This is an area of programming in which you can continually improve. The bottom line is this: If you know what you are doing when you write code, and you are doing it for the right reasons and in the right way, then your code will be easier to follow. This is because what the reader expects to happen will happen. If it doesn’t, the reader will become confused by the code, which is to be avoided.
The Use of Comments The main purpose of comments is to allow the annotation of the code to improve readability. Judicious use of comments does just that, but care needs to be taken not to make the code untidy by putting comments here and there without any real strategy. Comments can be applied throughout implementation code to annotate specific areas where confusion might arise. If you do this, use only C++-style comments beginning with //. This prevents comments from being finished prematurely by the unexpected occurrence of a C-style comment end, */.
Using Comments to Document Code How comments are added to implementation code is important. If it is done poorly, the effect can be to make code even more unclear. If a few guidelines are followed, comments can improve the readability of code. It is important not to interrupt the layout of the code. Comments should be separated from the code, either with space or a differing style (such as italic) or by using a different color. A comment should be indented when code is indented. This is particularly important if the comments discuss the code’s functionality. Comments written in this way can be scanned independently from the code, and the code can be scanned independently from the comments. Some programmers prefer not to separate a comment line from a code line, because they feel that use of color and style is sufficient to separate the comments from the code.
TIP By modifying the settings on the Colors tab in the Tools, Editor Options menu, it is possible to remove or highlight comments as required. To hide a comment from view in the editor window, simply change the color of the comment so that it matches the editor window background color; in the Defaults setting this is white. To highlight a comment, try changing the background color to blue and setting the foreground color to white. You can experiment to see what you like most.
05 9721 CH03
11/13/00
9:49 AM
Page 113
Programming in C++Builder CHAPTER 3
113
If a comment is specifically related to a single line of code and room permits, it is better to add the comment on the same line following the code. If several lines require such comments, an effort should be made to ensure that each comment starts at the same column in the editor. The advantage of such comments is that they do not interfere with the code’s layout. For example, code could be commented as follows: double GetMaximumValue(const std::vector<double>& Vector) ➥ throw(std::out_of_range) { // Initialize Maximum, if the vector is empty an // std::out_of_range exception will be thrown double Maximum = Vector.at(0); for(int i=0; i Maximum) { Maximum = Vector[i]; } }
++i) // // // //
If the ith element is greater than the current value in Maximum then set Maximum equal to its value
The comments for the if statement are superfluous, but they have been added to illustrate how comments can be laid out. Comments can also be appended to the closing brace of loop and selection statements: }//end if }//end for }// ! for }//end for(int i=0; i
The last example might be too much sometimes, but it can be useful when writing code or when a loop or selection construct spans more than one page. Comments are very useful when documenting code. They are particularly useful for summarizing important information about a function’s operational characteristics. There are many ways to present such information, but the important thing is to use one method consistently. If there is something important to say about a function, the information should appear in the header file that declares it and in the implementation file that implements it. Whoever maintains the code needs as much information as the user of the code. You should always have a brief summary of a function’s purpose above the function implementation and at the function’s declaration. You should also list any requirements that must be met for the function to operate as expected (sometimes referred to as preconditions) and any promises that the function makes to the user (sometimes referred to as postconditions). Any condition that results in undefined
PROGRAMMING IN C++BUILDER
return Maximum; }
3
05 9721 CH03
11/13/00
114
9:49 AM
Page 114
C++Builder 5 Essentials PART I
behavior should be explicitly stated and included in any list of requirements. Information presented in this way can be thought of as a contract for the function—if you do this, then the function promises to do that. Only enough information as is necessary needs to be documented. For short, simple functions, little needs to be written. Conversely, large complex functions may require more lengthy comments. If this is so, two considerations must be kept in mind. Users of a function don’t need to know of implementation issues internal to the function or how a function affects any private or protected data (assuming the function is a class member function). Such information should not appear in a header file. Also remember that comments are most useful when they are close to where they are directed in the implementation. It may be that some of the description is too detailed and some of the information given would be better placed elsewhere. An example of commenting a function in the implementation file would be as follows: //-----------------------------------------------------------// // // PURPOSE : Returns the maximum value present in a vector // of doubles // // REQUIRES : The vector passed is not empty // // PROMISE : The function will return the value of the // largest element // //-----------------------------------------------------------// double GetMaximumValue(const std::vector<double>& Vector) ➥ throw(std::out_of_range) { double Maximum = Vector.at(0); for(int i=0; i Maximum) { Maximum = Vector[i]; } } return Maximum; }
Similarly, the header file could look like this: double GetMaximumValue(const std::vector<double>& Vector) ➥ throw(std::out_of_range) // PURPOSE : Returns the maximum value in a vector of doubles
05 9721 CH03
11/13/00
9:49 AM
Page 115
Programming in C++Builder CHAPTER 3
115
// REQUIRES : The vector passed is not empty // PROMISE : Returns the value of the largest element
Remember that the header files in a program are often the only up-to-date documentation available for an interface, and as such they should be clear and accurately maintained (kept upto-date). If this is not done, comments can quickly become useless. If code is changed, then always change any comments that relate to that code. If you find yourself having to write extensive comments to explain a particularly tricky piece of code, it may be that the code itself should be changed.
Using Comments to Ignore Code Another use of comments of particular note to C++Builder is the commenting out of the names of unused parameters in IDE-generated event handlers. It is common that some or all of the parameters are not used by the function. Commenting out the parameter names does not alter the function signature, but it does show explicitly which of the parameters are actually used. If parameters are written on the same line, then C-style comments must be used to achieve this. The parameter list can be rearranged to allow commenting with C++-style comments, but the result looks untidy. Therefore, caution must be exercised. The following code snippet of an event handler for a TButton MouseUp event is shown for illustration:
TIP The MouseUp event handler uses the sprintf() AnsiString member function to modify the Caption property of Label1. This works because the sprintf() member function returns the AnsiString (*this) by reference. In general, however, a property should be modified only by assigning a new value to it using the assignment operator.
It is possible to delete the unused parameter names, but this can make the function header confusing, especially if you decide to use one of the parameters at a later date. A solution is to comment out the parameter name and then replace the commented out comma or closing bracket before the comment, as follows:
PROGRAMMING IN C++BUILDER
void __fastcall TMainForm::Button1MouseUp(TObject* /*Sender*/, TMouseButton /*Button*/, TShiftState /*Shift*/, int X, int Y) { // Display the cursor position within Button1 Label1->Caption.sprintf(“%d,%d”, X, Y); }
3
05 9721 CH03
11/13/00
116
9:49 AM
Page 116
C++Builder 5 Essentials PART I void __fastcall TMainForm::Button1MouseUp(TObject* ,//Sender, TMouseButton ,//Button, TShiftState ,//Shift, int X, int Y) { // Display the cursor position within Button1 Label1->Caption.sprintf(“%d,%d”, X, Y); }
This is effective and easily maintainable. If you want to use the parameter later, simply delete the // and the extra comma or extra closing bracket. An alternative is as follows: void __fastcall TMainForm::Button1MouseUp(TObject* ,//Sender TMouseButton ,//Button TShiftState ,//Shift int X, int Y) { // Display the cursor position within Button1 Label1->Caption.sprintf(“%d,%d”, X, Y); }
This differs only in the removal of the redundant commas from the end of each comment. You may find this is an improvement, because a comma (or bracket) at the end of a comment could be a distraction.
Using Comments to Improve Appearance A final use of comments is to help improve the overall appearance of the code as it appears on the screen or when it is printed. This is done by placing boxes around headings, placing divider lines between functions, and so on. When using comments in this way, care should be taken not to obscure the code within a forest of * characters or other such symbols. You must also be consistent for this to be useful. C++Builder automatically places a divider line between blocks of code that it generates. This helps improve the appearance of the code, because there is additional visual separation. It is good practice to do this with your own code.
TIP C++Builder 5 lets you change the default divider line that it places between sections of code that it generates. You can do so by editing the following entry in the BCB.BCF file in the $(BCB)\BIN folder. If the entry does not exist, you will have to create it. As you can see, the format is similar to an *.ini file.
05 9721 CH03
11/13/00
9:49 AM
Page 117
Programming in C++Builder CHAPTER 3
117
[Code Formatting] Divider Line=//---- My Custom Divider Line ----//
The divider line should begin as a comment line, with //. A good method of adding the line you want is to first write it in the Code Editor, then cut and paste it to the BCB.BCF file. That way you know exactly what it will look like. This helps IDE-generated code look consistent with your own code. It is a good idea to add this divider line to your code templates.
A Final Note on Improving the Readability of Code Ultimately, the best descriptor of the code’s purpose and how it operates is the code itself. By improving the code’s readability, you enhance this description.
Better Programming Practices in C++Builder
Use a String Class Instead of char* Say goodbye to char* for string manipulation. Use either the string class provided by the C++ Standard Library or the VCL’s native string class AnsiString (which has been conveniently typedefed to String). You also could use both. You can access the C++ Standard Library’s string class by including the statement #include <string>
at the top of your code. If portability is a goal, this is the string class to use. Otherwise, use AnsiString, which has the advantage that it is the string representation used throughout the VCL. This allows your code to work seamlessly with the VCL. You should endeavor to become familiar with the methods that AnsiString offers. Because strings are required so often, this will pay itself back in terms of improved use and efficiency. For circumstances in which an old-style char* string is required, such as to pass a parameter to a Win32 API call, both string classes offer the c_str() member function, which returns such a string. In addition, the AnsiString class also offers the popular old-style sprintf() and printf()functions (for concatenating strings) as member functions. It offers two varieties of each: a standard version and a cat_ version. The versions differ in that the cat_ version adds the concatenated string to the existing AnsiString, and the standard version replaces any existing contents of the AnsiString. The difference between the sprintf() and printf() member
3 PROGRAMMING IN C++BUILDER
This section looks at some ways to improve how you write C++ code in C++Builder. Entire books are devoted to better C++ programming, and you are encouraged to read such texts to deepen your understanding of C++. A list of suggested reading is given at the end of this chapter. The topics discussed here are those that have particular relevance to C++Builder and those that are often misunderstood or misused by those new to C++Builder.
05 9721 CH03
11/13/00
118
9:49 AM
Page 118
C++Builder 5 Essentials PART I
functions is that sprintf() returns a reference to the AnsiString, and printf() returns the length of the final formatted string (or the length of the appended string, in the case of cat_printf). The function declarations are int __cdecl int __cdecl AnsiString& AnsiString&
printf(const char* format, ...); cat_printf(const char* format, ...); __cdecl sprintf(const char* format, ...); __cdecl cat_sprintf(const char* format, ...);
These member functions ultimately call vprintf() and cat_vprintf() in their implementation. These member functions take a va_list as their second parameter, as opposed to a variable argument list. This would require the addition of the #include <stdarg.h> statement in your code. The function declarations are int __cdecl vprintf(const char* format, va_list paramList); int __cdecl cat_vprintf(const char* format, va_list paramList);
The respective printf() and sprintf() functions perform the same task, differing only in their return types. As a result, this is the only criterion that is required when deciding which of the two to use.
CAUTION Note that the printf() and sprintf() AnsiString member functions in C++Builder version 4 are the same as the cat_printf() and cat_sprintf() functions in version 5, not the printf() and sprintf() AnsiString member functions. Care should be taken when converting code between the two versions.
Understand References and Use Them Where Appropriate References are often misunderstood and therefore are not used as often as they should be. Often it is possible to replace pointers with references, making the code more intuitive and easier to maintain. This section looks at some of the features of references and when they are most appropriately used. The reason for the abundance of pointer parameters in the VCL in C++Builder is also discussed. A reference always refers to only one object, its referent, and it cannot be re-bound to refer to a different object (“object” in this context includes all types). A reference must be initialized on creation, and a reference cannot refer to nothing (NULL). Pointers, on the other hand, can point to nothing (NULL), can be re-bound, and do not require initialization on creation. A reference should be considered an alternative name for an object, whereas a pointer should be considered an object in itself. Anything that is done to a reference is also done to its referent and vice versa. This is because a reference is just an alternative name for the referent; they are the same thing. We can see therefore that references, unlike pointers, are implicitly dereferenced.
05 9721 CH03
11/13/00
9:49 AM
Page 119
Programming in C++Builder CHAPTER 3
119
The following code shows how a reference can be declared: int X = 12; // Declare and initialize int X to 12 int& Y = X; // Declare a reference to an int, i.e. Y, and // initialize it to refer to X
If we change the value of Y or X, we also change the value of X or Y, respectively, because X and Y are two names for the same thing. Another example of declaring a reference to a dynamically allocated variable is TBook* Book1 = new TBook(); // Declare and create a TBook object TBook& Book2 = *Book1;
// // // //
Declare a TBook reference, i.e. Book2, and initialize it to refer to the object pointed by Book1
The object pointed to by Book1 is the referent of the reference Book2.
void swap(int& X, int& Y) { int temp; temp = X; X = Y; Y = temp; }
This function would be called as follows: int Number1 = 12; int Number2 = 68; Swap(Number1, Number2); // Number1 == 68 and Number2 == 12 Number1 and Number2 are passed by reference to swap, and therefore X and Y become alternative names for Number1 and Number2, respectively, within the function. What happens to X also happens to Number1 and what happens to Y also happens to Number2. A predefined type such as an int should be passed by reference only when the purpose is to change its value; otherwise, it is generally more efficient to pass by value. The same cannot be said for user-defined types (classes, structs, and so on). Rather than pass such types to functions by value, it is more efficient to pass such types by const reference or, if the type is to be changed, then by non-const reference or pointer. For example:
3 PROGRAMMING IN C++BUILDER
One of the most important uses for references is the passing of user-defined types as parameters to functions. A parameter to a function can be passed by reference by making the parameter a reference and calling the function as if it were passed by value. For example, the following function is the typical swap function for two ints:
05 9721 CH03
11/13/00
120
9:49 AM
Page 120
C++Builder 5 Essentials PART I void DisplayMessage(const AnsiString& message) { //Display message. // message is an alias for the AnsiString argument passed // to the function. No copy is made and the const qualifier // states that the function will not (cannot) modify message }
is better than: void DisplayMessage(AnsiString message) { //Display message. // message is a copy of the AnsiString argument passed }
The first function is better for two reasons. First, the AnsiString parameter is passed by reference. This means that when the function is called, the AnsiString used as the calling argument is used, because only a reference is used by the function. The copy constructor of AnsiString does not need to be invoked (as it would be on entering the second function), and neither does the destructor, as it would be at the end of the second function when message goes out of scope. Second, the const keyword is used in the first function to signify that the function will not modify message through message. Both functions are called in the same way: AnsiString Message = “Hello!”; DisplayMessage(Message);
However, the first is safer and faster. Note that the calling code need not be directly affected. Functions can also return references, which has the side effect of the function becoming an lvalue (a value that can appear on the left side of an expression) for the referent. This also allows operators to be written that appear on the left side of an expression, such as the subscript operator. For example, given the Book class, an ArrayOfBooks class can be defined as follows: class Book { public: Book(); int NumberOfPages; }; class ArrayOfBooks { private: static const unsigned NumberOfBooks = 100; public:
05 9721 CH03
11/13/00
9:49 AM
Page 121
Programming in C++Builder CHAPTER 3 Book&
121
operator[] (unsigned i);
};
In this case, an instance of ArrayOfBooks can be used just like a normal array. Elements accessed using the subscript operator can be assigned to and read from, such as in the following: ArrayOfBooks ShelfOfBooks; unsigned PageCount = 0; ShelfOfBooks[0].NumberOfPages = 45; // A short book! PageCount += ShelfOfBooks[0].NumberOfPages; //PageCount = 45
This is possible because the value returned by the operator is the actual referent, not a copy of the referent. Generally we can say that references are preferred to pointers because they are safer (they can’t be re-bound and don’t require testing for NULL because they must refer to something). Also, they don’t require explicit dereferencing, making code more intuitive.
Avoid Using Global Variables Unless it is absolutely necessary, don’t use global variables in your code. Apart from polluting the global namespace (and increasing the chance of a name collision), it increases the dependencies between translation units that use the variables. This makes code difficult to maintain and minimizes the ease with which translation units can be used in other programs. The fact that variables are declared elsewhere also makes code difficult to understand. One of the first things any astute C++Builder programmer will notice is the global form pointers present in every form unit. This might give the impression that using global variables is OK; after all, C++Builder does it. However, C++Builder does this for a reason, which we will discuss at the end of this section. For now, we will examine some of the alternatives to declaring global variables.
3 PROGRAMMING IN C++BUILDER
What then of the pointers used in C++Builder’s VCL? The reason behind the extensive use of pointers in the VCL is that the VCL is written in Object Pascal, which uses Object Pascal references. An Object Pascal reference is closer to a C++ pointer than a C++ reference. This has the side effect that, when the VCL is used with C++, pointers have to be used as replacements for Object Pascal references. This is because an Object Pascal reference (unlike a C++ reference) can be set to NULL and can be re-bound. In some cases it is possible to use reference parameters instead of pointer parameters, but because all VCL-based objects are dynamically allocated on free store and therefore are referred to through pointers, the pointers must be dereferenced first. Because the VCL relies on some of the features of Object Pascal references, pointers are used for object parameter passing and returning. Remember that a pointer parameter is passed by value, so the passed pointer will not be affected by the function. You can prevent modification of the object pointed to by using the const modifier.
05 9721 CH03
11/13/00
122
9:49 AM
Page 122
C++Builder 5 Essentials PART I
Let’s assume that global variables are a must. How can we use global variables without incurring some of the side effects that they produce? The answer is that we use something that acts like a global variable but is not one. We use a class with a member function that returns a value of or reference to (whichever is appropriate) a static variable that represents our global variable. Depending on the purpose of our global variables (for example, global to a program or global to a library), we may or may not need access to the variables through static member functions. In other words, it may be possible to instantiate an object of the class that contains the static variables when they are required. We consider first the case where we do require access to the static variables (representing our global variables) through static member functions. We commonly refer to this kind of class as a module. With a module of global variables, we improve our representation of the variables by placing them into a class, making them private static variables, and using static getters and setters to access them (for more information, see Large-Scale C++ Software Design by Lakos, 1996, p. 69). This prevents pollution of the global namespace and gives a certain degree of control over how the global variables are accessed. Typically, the class would be named Global. Hence, two global variables declared as int Number; double Average;
could be replaced by class Global { private: static int Number; static double Average; //PRIVATE CONSTRUCTOR Global(); //not implemented, instantiation not possible public: // SETTERS static void setNumber(int NewNumber) { Number = NewNumber; } static void setAverage(double NewAverage) { Average = NewAverage; } // GETTERS static int getNumber() { return Number; } static double getAverage() { return Average; } };
Accessing Number is now done through Global::getNumber() and Global::setNumber(). Average is accessed similarly. The class Global is effectively a module that can be accessed throughout the program and does not need to be instantiated (because the member data and functions are static).
05 9721 CH03
11/13/00
9:49 AM
Page 123
Programming in C++Builder CHAPTER 3
123
Often such an implementation is not required, and it is possible to create a class with a global point of access that is constructed only when first accessed. This has the benefit of allowing control over the order of initialization of the variables (objects must be constructed before first use). The method used is to place the required variables inside a class that cannot be directly instantiated, but accessed only through a static member function that returns a reference to the class. This ensures that the class containing the variables is constructed on first use and is constructed only once. This approach is often referred to as the Singleton pattern (for more information, see Design Patterns: Elements of Reusable Object-Orientated Software by Gamma et al., 1995, p. 127). Patterns are a way of representing recurring problems and their solutions in object-based programs. For more on patterns, see Chapter 4, “Advanced Programming with C++Builder.” The basic code required to create a Singleton (as such a class is commonly referred to) is as follows: class Singleton { public: static Singleton& Instance();
An implementation of Instance is Singleton& Singleton::Instance() { static Singleton* NewSingleton = new Singleton(); return *NewSingleton; }
The initial call to Instance will create a new Singleton and return a reference to it. Subsequent calls will simply return a reference. However, the destructor of the Singleton will not be called; the object is simply abandoned on free store. If there is important processing that must be executed in the destructor, then the following implementation will ensure that the Singleton is destructed: Singleton& Singleton::Instance() { static Singleton NewSingleton; return NewSingleton; }
This implementation causes its own problem. It is possible for another static object to access the Singleton after it has been destroyed. One solution to this problem is the nifty counter technique (for more information, see C++ FAQs Second Edition, Cline et al., 1999, p. 235, and
PROGRAMMING IN C++BUILDER
protected: Singleton(); // Not Implemented, Instantiation not possible };
3
05 9721 CH03
11/13/00
124
9:49 AM
Page 124
C++Builder 5 Essentials PART I
Large-Scale C++ Software Design, Lakos, 1996, p. 537), in which a static counter is used to control when each object is created and destroyed. If you find the need for this technique, perhaps a re-think of the code would also be helpful. It may be that a slight redesign could remove the dependency. It should now be clear that static variables are like global variables and can almost always be used in place of global variables. Remember, though, that ultimately global variables should be avoided.
Understand How C++Builder Uses Global Variables What then of the global form pointer variables in C++Builder? Essentially, global form pointer variables are present to allow the use of non-modal forms. Such forms require a global point of access for as long as the form exists, and it is convenient for the IDE to automatically create one when the form is made. The default operation of the IDE is to add newly created forms to the auto-create list, which adds the line Application->CreateForm(__classid(TFormX), &FormX);
(where X is a number) to the WinMain function in the project .cpp file. Modal forms do not require this, because the ShowModal() method returns after the forms are closed, making it possible to delete them in the same scope as they were created in. General guidelines on the use of forms can therefore be given.
TIP You can uncheck the Auto Create Forms option on the Forms property page in the Tools, Environment Options menu to change the behavior of the IDE so that forms are not automatically added to the auto-create list. When this is done, forms are instead added to the available list.
First, determine if a form is to be a Modal form or a Non-Modal form. If the form is Modal, then it is possible to create and destroy the form in the same scope. This being the case, the global form pointer variable is not required, and the form should not be auto-created. Remove the Application->CreateForm entry from WinMain either by deleting it or by removing it from the auto-create list on the Forms page in the Project, Options menu. Next, either delete or comment out the form pointer variable from the .h and .cpp files, and state explicitly in the header file that the form is Modal and should be used only with the ShowModal() method. That is, in the .cpp file remove TFormX* FormX;
and from the .h file, remove
05 9721 CH03
11/13/00
9:49 AM
Page 125
Programming in C++Builder CHAPTER 3
125
extern PACKAGE TFormX* FormX;
Add a comment such as the following: // This form is MODAL and should only called with the ShowModal() method.
To use the form, simply write TFormX* FormX = new TFormX(0); try { FormX->ShowModal(); } __finally { delete FormX; }
Because you most likely do not want the form pointer to point elsewhere, you could declare the pointer as const:
TFormX(this); FormX->ShowModal(); delete FormX;
The use of a try/__finally block ensures that the code is exception-safe. An alternative to these examples is to use the Standard Library’s auto_ptr class template: auto_ptr FormX(new TFormX(0)); FormX->ShowModal();
Whichever technique you use, you are guaranteed that if the code terminates prematurely because an exception is thrown, FormX will be destructed automatically. With the first technique this happens in the __finally block; with the second it occurs when auto_ptr goes out of scope. The second technique can be further enhanced by making the auto_ptr const, since generally it is not required that the auto_ptr lose ownership of the pointer, as in the following code. (For more information, see Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions by Sutter, 2000, p. 158.) const auto_ptr FormX(new TFormX(0)); FormX->ShowModal();
3 PROGRAMMING IN C++BUILDER
TFormX* const FormX = new TFormX(0); try { FormX->ShowModal(); } __finally { delete FormX; }
05 9721 CH03
11/13/00
126
9:49 AM
Page 126
C++Builder 5 Essentials PART I
Of particular note in the code snippets is that 0 (NULL) is passed as the argument to the AOwner parameter of FormX. This is because we handle the destruction of the form ourselves.
TIP Using auto_ptr is an effective way of managing the memory of VCL-based objects. It is exception-safe and easy to use. For a VCL object that takes an owner parameter in its constructor, you can simply pass 0, because you know that the object will be deleted when the auto_ptr goes out of scope.
If the form is Non-Modal, you must decide only whether or not you want it auto-created. If you don’t, you must ensure that it is removed from WinMain. When you want it created later, you can use the form’s global pointer and the new operator. Show the form using the Show() method. Remember that you cannot delete Modal forms, because Show() returns when the form is shown, not when it is closed. Therefore, it may still be in use. For example, if the form is auto-created, write FormX->Show();
Otherwise create and show it this way: FormX = new TFormX(this); FormX->Show();
It is important not to create the form again after it has been auto-created, because this will overwrite the reference to the auto-created form. This means that the auto-created instance can no longer be accessed by the application and can result in the application crashing when an attempt to dereference the global pointer is made. It is advisable therefore to check the value of the pointer for equality with NULL (0) before creation: if(FormX == 0) FormX = new TFormX(this); FormX->Show();
This is possible because the form pointer is a global variable that is guaranteed to be initialized to zero. Using this technique ensures that the global pointer will always point to a valid location and will not be overwritten. As an aside to this topic, the practice of declaring variables or functions as static so that they have scope only within the translation unit in which they are declared is deprecated. Instead, such variables and functions should be placed in an unnamed namespace. (For more information, see ANSI/ISO C++ Professional Programmer’s Handbook: The Complete Language by Kalev, 1999, p. 157.)
05 9721 CH03
11/13/00
9:49 AM
Page 127
Programming in C++Builder CHAPTER 3
127
Understand and Use const in Your Code The const keyword should be used as a matter of course, not as an optional extra. Declaring a variable const allows attempted changes to the variable to be detected at compile time (resulting in an error) and also indicates the programmer’s intention not to modify the given variable. Moreover, not using the const keyword indicates the programmer’s intention to modify a given variable. The const keyword can be used in a variety of ways. First, it can be used to declare a variable as a constant: const double PI = 3.141592654;
This is the C++ way to declare constant variables. Do not use #define statements. Note that const variables must be initialized. The following shows the possible permutations for declaring const variables. Pointer and reference declarations are read from right to left, as the following examples show: int Y = 12; const int X = Y;
// X equals Y which equals 12, therefore X = 12 // X cannot be changed, but Y can
// In the next declaration the pointer itself is constant // The int pointed to by P, i.e. Y can be // changed through P but P itself cannot change
// The next two declarations are the same: const int* P = &Y; int const* P = &Y;
// The int pointed to by P, i.e. // Y cannot be changed through P
// The next two declarations are the same: const int* const P = &Y; P int const* const P = &Y;
// Neither P, nor what it points to, // i.e. Y can be changed through P
// The next two declarations are the same: const int& R = Y R int const& R = Y
// The int referred to by R, i.e. // Y cannot be changed through R
After reviewing the previous examples, it is helpful to reiterate how const is used with pointer declarations. As stated previously, a pointer declaration is read from right to left, so that in int * const the const refers to the *. Hence, the pointer is constant, though the int it points to can be changed. With int const * the const refers to the int. In this case the int itself is constant, though the pointer to it is not. Finally, with int const * const, both the int and
PROGRAMMING IN C++BUILDER
int* const P = &Y;
3
05 9721 CH03
11/13/00
128
9:49 AM
Page 128
C++Builder 5 Essentials PART I
the * are constant. Also remember that int const and const * const is the same as int cosnt * const.
int
are the same, so const
int
If you want to declare a literal string of chars, declare it as one of the following: const char* const LiteralString = “Hello World”; char const * const LiteralString = “Hello World”;
Both of the previous strings and the pointers to them are constant. Function parameters should be declared as const in this fashion when it is appropriate, such as when the intention of the function is not to modify the argument that is passed to the function. For example, the following function states that it will not modify the arguments passed to it: double GetAverage(const double* ArrayOfDouble, int LengthOfArray) { double Sum = 0; for(int i=0; i
Another way of thinking about this is to assume that if the const keyword is not used for a parameter, then it must be the intention of the function to modify that parameter’s argument, unless the parameter is pass-by-value (a copy of the parameter is used, not the parameter itself). Notice that declaring int LengthOfArray as a const is inappropriate, because this is pass-by-value. LengthOfArray is a copy, and declaring it as a const has no effect on the argument passed to the function. Similarly, ArrayOfDouble is declared as follows: const double* ArrayOfDouble
not const double* const ArrayOfDouble
Because the pointer itself is a copy, only the data that it points to needs to be made const. The return type of a function can also be const. Generally it is not appropriate to declare types returned by value as const, except in the case of requiring the call of a const-overloaded member function. Reference and pointer return types are suitable for returning as consts. Member functions can be declared const. A const member function is one that does not modify the this object (*this). Hence, it can call other member functions inside its function body only if they are also const. To declare a member function const, place the const keyword at
05 9721 CH03
11/13/00
9:49 AM
Page 129
Programming in C++Builder CHAPTER 3
129
the end of the function declaration and in the function definition at the same place. Generally, all getter member functions should be const, because they do not modify *this. For example class Book { private: int NumberOfPages; public: Book(); int GetNumberOfPages() const; };
The definition of GetNumberOfPages() could be int Book::GetNumberOfPages() const { return NumberOfPages; }
class ArrayOfBooks { public: Book& operator[] (unsigned i); const Book& operator[] (unsigned i) const; };
The ArrayOfBooks class can use the [] operator on both const and non-const Books. For example, if an ArrayOfBooks object is passed to a function by reference to const, it would be illegal for the array to be assigned to using the [] operator. This is because the value indexed by i would be a const reference, and the const state of the passed array would be preserved. Remember, know what const is and use it whenever you can.
Be Familiar with the Principles of Exceptions An exception is a mechanism for handling runtime errors in a program. There are several approaches that can be taken to handling runtime errors, such as returning error codes, setting global error flags, and exiting the program. In many circumstances, an exception is the only appropriate method that can be employed effectively, such as when an error occurs in a constructor. (For more information, see ANSI/ISO C++ Professional Programmer’s Handbook: The Complete Language by Kalev, 1999, p. 113.)
3 PROGRAMMING IN C++BUILDER
The final area in which const is commonly encountered is when operators are overloaded by a class and access to both const and non-const variables is required. For example, if a class ArrayOfBooks is created to contain Book objects, it is sensible to assume that the [] operator will be overloaded (so that the class acts like an array). However, the question of whether or not the [] operator will be used with const or non-const objects must be considered. The solution is to const-overload the operator, as the following code indicates:
05 9721 CH03
11/13/00
130
9:49 AM
Page 130
C++Builder 5 Essentials PART I
Exceptions will commonly be encountered in two forms in C++Builder programs: C++ exceptions and VCL exceptions. Generally the principles involved with both are the same, though there are some differences. C++ uses three keywords to support exceptions; try, catch, and throw. C++Builder extends its exception support to include the __finally keyword. The try, catch, and __finally keywords are used as headers to blocks of code (that is, code that is enclosed between braces). Also, for every try block there must always be one or more catch blocks or a single __finally block.
The try Keyword The try keyword is used in one of two possible ways. The first and simplest is as a simple block header, to create a try block within a function. The second is as a function block header, to create a function try block, either by placing the try keyword in front of the function’s first opening brace or, in the case of constructors, in front of the colon that signifies the start of the initializer list.
NOTE C++Builder does not currently support function try blocks. However, because it makes a real difference only with constructors and even then has little impact on their use, it is unlikely that its omission will have any effect. For those who are interested, it will be supported in version 6 of the compiler.
The catch Keyword Normally, at least one catch block will immediately follow any try block (or function try block). A catch block will always appear as the catch keyword followed by parentheses containing a single exception type specification with an optional variable name. Such a catch block (commonly referred to as an exception handler) can catch only an exception whose type exactly matches the exception type specified by the catch block. However, a catch block can be specified to catch all exceptions by using the catch all ellipses exception type specifier, catch(...). A typical try/catch scenario is as follows: try { // Code that may throw an exception } catch(exception1& e) { // Handler code for exception1 type exceptions
05 9721 CH03
11/13/00
9:49 AM
Page 131
Programming in C++Builder CHAPTER 3
131
} catch(exception2& e) { // Handler code for exception2 type exceptions } catch(...) { // Handler code for any exception not already caught }
The __finally Keyword The last of these, __finally, has been added to allow the possibility of performing cleanup operations or ensuring certain code is executed regardless of whether an exception is thrown. This works because code placed inside a __finally block will always execute, even when an exception is thrown in the corresponding try block. This allows code to be written that is exception-safe and will work properly in the presence of exceptions. A typical try/__finally scenario is try { // Code that may throw an exception
It should be noted that try/catch and try/__finally constructs can be nested inside other try/catch and try/__finally constructs.
The throw Keyword The throw keyword is used in one of two ways. The first is to throw (or rethrow) an exception, and the second is to allow the specification of the type of exceptions that a function may throw. In the first case (to throw or rethrow an exception), the throw keyword is followed optionally by parentheses containing a single exception variable (often an object) or simply the single exception variable after a space, similar to a return statement. When no such exception variable is used, the throw keyword stands on its own. Then its behavior depends on its placement. When placed inside a catch block, the throw statement rethrows the exception currently being handled. When placed elsewhere, such as when there is no exception to rethrow, it causes terminate() to be called, ultimately ending the program. It is not possible to use throw to rethrow an exception in VCL code. The second use of the throw keyword is to allow the specification of the exceptions that a function may throw. The syntax for the keyword is throw(<exception_type_list>)
PROGRAMMING IN C++BUILDER
} __finally { // Code here is always executed, even if // an exception is thrown in the preceding // try block }
3
05 9721 CH03
11/13/00
132
9:49 AM
Page 132
C++Builder 5 Essentials PART I
The exception_type_list is optional and when excluded indicates that the function will not throw any exceptions. When included, it takes the form of one or more exception types separated by commas. The exception types listed are the only exceptions the function may throw.
Unhandled and Unexpected Exceptions In addition to the three keywords described, C++ offers mechanisms to deal with thrown exceptions that are not handled by the program and exceptions that are thrown but are not expected. This might include an exception that is thrown inside a function with an incompatible exception specification. When an exception is thrown but not handled, terminate() is called. This calls the default terminate handler function, which by default calls abort(). This default behavior should be avoided because abort() does not ensure that local object destructors are called. To prevent terminate() being called as a result of an uncaught exception, the entire program can be wrapped inside a try/catch(...) block in WinMain() (or main() for command-line programs). This ensures that any exception will eventually be caught. If terminate() is called, you can modify its default behavior by specifying your own terminate handler function. Simply pass the name of your terminate handler function as an argument to the std::set_ terminate() function. The <stdexcept> header file must be included. For example, given a function declared as void TerminateHandler();
The code required to ensure that this handler is called in place of the basic terminate() handler is #include <stdexcept> std::set_terminate(TerminateHandler);
When an exception is thrown that is not expected, then unexpected() is called. Its default behavior is to call terminate(). Again, the opportunity exists to define your own function to handle this occurrence. To do so, call std::set_unexpected(), passing the function handler name as an argument. The <stdexcept> header file must be included.
Using Exceptions This brings the discussion to consideration of the exceptions that can and should be thrown by a function and where such exceptions should be caught. This should be decided when you are designing your code, not after it has already been written. To this end, you must consider several things when you write a piece of code. Some of the topics are very complex, and it is beyond the scope of this book to cover all the issues involved. Instead, check the “Further Reading” section at the end of this chapter for more information.
05 9721 CH03
11/13/00
9:49 AM
Page 133
Programming in C++Builder CHAPTER 3
133
You must consider if the code you have written could throw one or more exceptions. If so, you must then consider if it is appropriate to catch one or more of the exceptions in the current scope or let one or more of them propagate to an exception handler outside the current scope. If you do not want one or more of the exceptions to propagate outside the current scope, then you must place the code in a try block and follow it with the one or more appropriate catch blocks to catch any desired exceptions (or all exceptions, using a catch-all block). To this end, you should be aware of the exceptions built into the language itself, the C++ Standard Library, and the VCL and be aware of when they may be thrown. For example, if new fails to allocate enough memory, std::bad_alloc is thrown. Throw an exception in a function only when it is appropriate to do so, when the function cannot meet its promise. (See the section “The Use of Comments,” earlier in this chapter, for a discussion of a function’s promise. Also see C++ FAQs, Second Edition, Cline et al., 1999, p. 137.) You should catch an exception only when you know what to do with it, and you should always catch an exception by reference. (For more information, see More Effective C++: 35 New Ways to Improve Your Programs and Designs by Meyers, 1996, p. 68.) VCL exceptions cannot be caught by value. Also, it may not be possible to fully recover from an exception, in which case the handler should perform any possible cleanup and then rethrow the exception.
You should ensure that you write exception-safe code that works properly in the presence of exceptions. For example, simple code such as this is not exception safe: TFormX* const FormX = new TFormX(0); FormX->ShowModal(); delete FormX;
If an exception is thrown between the creation and deletion of the form, the form will never be deleted, so the code does not work properly in the presence of exceptions. For an exceptionsafe alternative, see the section “Avoid Global Variables,” earlier in this chapter. If you are writing container classes, endeavor to write code that is exception-neutral—code that propagates all exceptions to the caller of the function that contains the code. (For more information, see Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions by Sutter, 2000, p. 25.) Never throw an exception from a destructor, because the destructor may have been called as a result of stack unwinding after a previous exception was called. This calls terminate(). Destructors should have an exception specification of throw().
PROGRAMMING IN C++BUILDER
You should understand when and how to use exception specifications for functions and be wary of the possibility of writing an incorrect specification. This will result in unexpected() being called if an unspecified exception is thrown inside a function and it is not handled within that function.
3
05 9721 CH03
11/13/00
134
9:49 AM
Page 134
C++Builder 5 Essentials PART I
A Final Note on Exceptions Finally, you should appreciate the differences between VCL and C++ exceptions. VCL exceptions allow operating system exceptions to be handled as well as exceptions generated from within the program. Such exceptions must be caught by reference. VCL exceptions generated from within the program cannot be caught by value. An advantage of VCL exceptions is that they can be thrown and caught within the IDE.
Use new and delete to Manage Memory The VCL requires that all classes that inherit from TObject are created dynamically in free store. Free store is often referred to as the heap, but free store is the correct term when applied to memory allocated and deallocated by new and delete. The term heap should be reserved for the memory allocated and deallocated by malloc() and free(). (For more information, see Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions by Sutter, 2000, p. 142.) This means a lot of calls to new and delete in C++Builder programs, so it is important to understand a few things about how new and delete work.
CAUTION A Non-Plain Old Data (Non-POD) object is essentially any but the most trivial of classes. Such objects must have their memory allocated by using new; the C equivalent malloc() will not suffice (its behavior is undefined) and be subsequently deallocated with delete, not free(). The new and delete operators ensure that, in addition to the allocation/deallocation of memory, the object’s constructor and destructor, respectively, are called. The new operator also returns a pointer that is suitable to the object created, not merely a void pointer that must be cast to the required type. new and delete call operator new/operator delete, respectively, to allocate/deallocate memory, and these can be overloaded for specific classes. This allows the customization of memory allocation/deallocation behavior. This is not possible with malloc() and free(). (For more information, see ANSI/ISO C++ Professional Programmer’s Handbook: The Complete Language by Kalev, 1999, p. 221.)
A successful call to new allocates sufficient memory in free store (using operator new) calls the object’s constructor and returns a pointer of the type pointer-to-the-object-type-created. A correctly initialized object is the result. Subsequently calling delete calls the object’s destructor and deallocates the memory obtained previously by calling new.
05 9721 CH03
11/13/00
9:49 AM
Page 135
Programming in C++Builder CHAPTER 3
135
CAUTION Never call a VCL object’s Free() method to destroy a VCL object. Always use delete. This ensures that the object’s destructor is called and that the memory allocated previously with new is freed. Free() does not guarantee this, and it is bad practice to use it.
If the call to new is unsuccessful, a std::bad_alloc exception is thrown. Note that the bad_alloc exception is defined in the standard library file . Hence, you must include #include in your program, and it is in the std namespace. It does not return NULL. Therefore, you should not check the return pointer for equality with NULL. The program should be prepared to catch the std::bad_alloc exception and, if the function that calls new does not catch the exception, it should pass the exception outside the function, so that calling code has the opportunity to catch it. Either of the following would be appropriate:
or void CreateObject(TMyObject* MyObject) throw(std::bad_alloc) { MyObject = new TMyObject(); }
The use of exceptions allows the code that handles the error to be centralized, which leads to safer code that is more intuitive. The throw keyword added to the function header is called an exception specification. The effect of its inclusion in the function header is to specify which exceptions the function may throw. For more explanation refer to the section “Be Familiar with the Principles of Exceptions,” earlier in this chapter. In the case of the first CreateObject() function, a throw() exception specifier is used to indicate that no exception will be thrown by the function. This is acceptable, because the only exception that may be thrown, std::bad_alloc, is caught and dealt with by the function itself. In the case of the second implementation of CreateObject(), the exception specifier throw(std::bad_alloc) is used
3 PROGRAMMING IN C++BUILDER
void CreateObject(TMyObject* MyObject) throw() { try { MyObject = new TMyObject(); } catch(std::bad_alloc) { //Print a message “Not enough memory for MyObject”; // Deal with the problem // or exit gracefully } }
05 9721 CH03
11/13/00
136
9:49 AM
Page 136
C++Builder 5 Essentials PART I
to indicate that the only exception that the function can throw is std::bad_alloc. This should be caught and handled by one of the calling routines. There is also the possibility of writing your own out-of-memory function handler to deal with failed memory allocation. To set a function as a handler for out-of-memory conditions when using new, call the set_new_handler() function (also defined in ), passing as a parameter the name of the function you will use as the out-of-memory handler. For example, if you write a function (non-member or static member) called OutOfMemory to handle such occurrences, the necessary code would be #include void OutOfMemory() { // Try to free some memory // if there is now enough memory then this // function will NOT be called next time // else either install a new handler or throw an exception } // Somewhere in the main code, near the start write: std::set_new_handler(OutOfMemory);
This code requires some explanation, because the sequence of events that occurs when new fails dictates how the OutOfMemory function should be written. If new fails to allocate enough memory, then OutOfMemory is called. OutOfMemory tries to free some memory (how this is done will be discussed later); new will then try again to allocate the required memory. If it is successful, we are finished. If it is unsuccessful, the process just described will be repeated. In fact, it will repeat infinitely until either enough memory is allocated or the OutOfMemory function terminates the process. To terminate the process, the OutOfMemory function can do several things. It can throw an exception (such as std::bad_alloc()), it can install a different memory handler that can then try to make more memory available, it can assign NULL to set_new_handler (std::set_new_handler(0)), or it can exit the program (not recommended). If a new handler is installed, then this series of events will occur for the new handler (which is called on the subsequent failed attempt). If the handler is set to NULL (0), then no handler will be called, and the exception std::bad_alloc() will be thrown. Making more memory available is dependent on the design of the program and where the memory shortage arises from. If the program keeps a lot of memory tied up for performance reasons but does not always require it to be available at all times, then such memory can be freed if a shortage occurs. Identifying such memory is the difficult part. If there is no such memory usage in the program, then the shortage will be a result of factors external to the program, such as other memory-intensive software or physical limitations. There is nothing that
05 9721 CH03
11/13/00
9:49 AM
Page 137
Programming in C++Builder CHAPTER 3
137
can be done about physical limitations, but it is possible to warn the user of a memory shortage so that memory-intensive software can be shut down, thereby freeing additional memory. The trick is to give an advance warning before all the memory is used up. One approach is to pre-allocate a quantity of memory at the beginning of the program. If new fails to allocate enough memory, then this memory can be freed. The user is warned that memory is low and told to try to free more memory for the application. Assuming that the pre-allocated block was large enough, the program should be able to continue operating as normal if the user has freed additional memory. This preemptive approach is simple to implement and reasonably effective. There are other approaches, and the reader is directed to the books in the “Further Reading” section for more information. It is important to note that if you want to allocate raw memory only, then operator new and should be used instead of the new and delete operators. (For more information, see More Effective C++: 35 New Ways to Improve Your Programs and Designs by Meyers, 1996, p. 38.) This is useful for situations in which, for example, a structure needs to be allocated dynamically, and the size of the structure is determined through a function call before the dynamic allocation. This is a common occurrence in Win32 API programming: operator delete
DWORD StructureSize = APIFunctionToGetSize(SomeParameter);
3
WIN32STRUCTURE* PointerToStructure; PointerToStructure = static_cast<WIN32STRUCTURE*>(operator new(StructureSize)); // Do something with the structure operator delete(PointerToStructure);
PROGRAMMING IN C++BUILDER
It is clear that the use of malloc() and free() should not be required. Finally, we will discuss the use of new and delete in dynamically allocating and deallocating arrays. Arrays are allocated and deallocated using operator new[] and operator delete[], respectively. They are separate operators from operator new and operator delete. When new is used to create an array of objects, it first allocates the memory for the objects (using operator new[]) and then each object is initialized by calling its default constructor. Deleting an array using delete performs the opposite task: It calls the destructor for each object and then deallocates the memory (using operator delete[]) for the array. So that delete knows to call operator delete[] instead of operator delete, a [] is placed between the delete keyword and the pointer to the array to be deleted: delete [] SomeArray;
Allocating a single-dimensional array is straightforward. The following format is used: TBook* ArrayOfBooks = new TBook[NumberOfBooks];
Deleting such an array is also straightforward. However, remember that the correct form of delete must be used—delete []. For example
05 9721 CH03
11/13/00
138
9:49 AM
Page 138
C++Builder 5 Essentials PART I delete [] ArrayOfBooks;
Remember that [] tells the compiler that the pointer is to an array, as opposed to simply a pointer to a single element of a given type. If an array is to be deleted, it is essential that delete [] be used, not delete. If delete is used erroneously, then at best only the first element of the array will be deleted. We know that when an array of objects is created, the default constructor is used. This means that you will want to ensure that you have defined the default constructor to suit your needs. Remember that a compiler-generated default constructor does not initialize the classes’ data members. Also, you will probably want to overload the assignment operator (=) so that you can safely assign object values to the array objects. A twodimensional array can be created using code such as the following: TBook** ShelvesOfBooks = new TBook*[NumberOfShelves]; for(int i=0; i
To delete such an array use the following: for(int i=0; i
One thing remains unsaid: If you want to have an array of objects, a better approach is to create a vector of objects using the vector template from the STL. It allows any constructor to be used and also handles memory allocation and deallocation automatically. It will also reallocate memory if there is a memory shortage. This means that the use of the C library function realloc() is also no longer required. For more information on the vector template class, refer to the “Introduction to the Standard C++ Library and Templates” section in Chapter 4. Placement new (allocation at a predetermined memory location) and nothrow new (does not throw an exception on failure, returns NULL instead) have not been discussed, because they are beyond the scope of this section. However, if more information is required on either of these, please refer to the “Further Reading” section.
Understand and Use C++-Style Casts There are four C++ casts. They are outlined in Table 3.1.
05 9721 CH03
11/13/00
9:49 AM
Page 139
Programming in C++Builder CHAPTER 3
TABLE 3.1
139
C++-Style Casts
Cast
General Purpose
static_cast(exp)
Used to perform casts such as an int to a double. T and exp may be a pointer, a reference, an arithmetic type (such as int), or an enum type. You cannot cast from one type to another, such as from a pointer to an arithmetic. Used to perform casting down or across an inheritance hierarchy. For example, if class X inherits from class O, then a pointer to class O can be cast to a pointer to class X, provided the conversion is valid.
dynamic_cast(exp)
T may be void*.
a pointer or a reference to a defined class type or
may be a pointer or a reference. For a conversion from a base class to a derived class to be possible, the base class must contain at least one virtual function; in other words, it must be polymorphic. One important feature of dynamic_cast is that if a conversion between pointers is not possible, a NULL pointer is returned; if a conversion between references is not possible, a std::bad_cast exception is thrown (include the header file ). As a result, the conversion can be checked for success. This is the only cast that can affect the const or volatile nature of an expression. It can be either cast off or cast on. This is the only thing const_cast is used for. For example, if you want to pass a pointer to const data to a functionthat only takes a pointer to non-const data, and you know the data will not be modified, you could pass the pointer by const_casting it. exp
and exp must be of the same type except for their const or volatile factors. Used to perform unsafe orimplmentation-dependent casts. This cast should be used only when nothing else will do. This is because it allows you to re-interpret the expression as a completely different type, such as to cast a float* to an int*. It is commonly used to cast between function pointers. If you find yourself needing to use T
reinterpret_cast(exp)
PROGRAMMING IN C++BUILDER
const_cast(exp)
3
05 9721 CH03
11/13/00
140
9:49 AM
Page 140
C++Builder 5 Essentials PART I
TABLE 3.1
Continued
Cast
General Purpose reinterpret_cast,
decide carefully if the approach you are taking is the right one, and remember to document clearly your intention (and possibly your reasons for this approach). T must be a pointer, a reference, an arithmetic type, a pointer to a function, or a pointer to a member function. A pointer can be cast to an integral type and vice versa. The casts most likely to be of use are static_cast (for trivial type conversions such as int to double) and dynamic_cast. An example of using static_cast can be found in the last line of the following code: int Sum = 0; int* Numbers = new int[20]; for(int i=0; i<20; ++i) { Numbers[i] = i*i; Sum += Numbers[i]; } double Average = static_cast<double>(Sum)/20;
The astute among you will recognize this as the code from Listing 3.4 earlier in this chapter. One of the times when dynamic_cast is commonly used in C++Builder is to dynamic_cast or TComponent* Owner, to ensure that Sender or Owner is of a desired class, such as TForm. For example, if a component is placed on a form, it may be necessary to distinguish if it was placed directly or was perhaps placed on a Panel component. To carry out such a test, the following code is required: TObject* Sender
TForm* OwnerForm = dynamic_cast(Owner); if(OwnerForm) { //Perform processing since OwnerForm != NULL, i.e. 0 }
First a pointer of the required type is declared, and then it is set equal to the result of the dynamic_cast. If the cast is unsuccessful, the pointer will point to the required type and can be used for accessing that type. If it fails, it will point to NULL, and hence can be used to evaluate a Boolean expression. Sender can be similarly used. The situations that require such casting are many and varied. What is important is to understand what it is that you want to achieve and make your intention and reasoning clear.
05 9721 CH03
11/13/00
9:49 AM
Page 141
Programming in C++Builder CHAPTER 3
141
Each of the C++ casts performs a specific task and should be restricted for use only where appropriate. The C++ casts are also easily seen in code, making it more readable.
Know When to Use the Preprocessor It is not appropriate to use the preprocessor for defining constants or for creating function macros. Instead, you should use const variables or enum types for constants and use an inline function (or inline template function) to replace a function macro. Consider also that a function macro may not be appropriate anyway (in which case the inline equivalent would not be required). For example, the constant π can be defined as const double PI = 3.141592654;
If you wanted to place this inside a class definition, then you would write class Circle { public: static const double PI; // This is only a declaration };
3
const double Circle::PI = 3.141592654; // This is the constant definition // and initialization
Note that the class constant is made static so that only one copy of the constant exists for the class. Also notice that the constant is initialized in the implementation file (typically after the include directive for the header file that contains the class definition). The exception to this is the initialization of integral types, char, short, long, unsigned, and int. These can be initialized directly in the class definition. When a group of related constants is required, an enum is a sensible choice: enum LanguagesSupported { English, Chinese, Japanese, French };
Sometimes an enum is used to declare an integer constant on its own: enum { LENGTH = 255 };
Such declarations are sometimes seen inside class definitions. A static ration (like that for PI) is a more correct approach. Replacing a function macro is also easily achieved. Given the macro #define cubeX(x) ( (x)*(x)*(x) )
the following inline function equivalent can be written: inline double cubeX(double x) { return x*x*x; }
const
variable decla-
PROGRAMMING IN C++BUILDER
In the implementation (*.cpp) file, you would define and initialize the constant by writing
05 9721 CH03
11/13/00
142
9:49 AM
Page 142
C++Builder 5 Essentials PART I
Notice that this function takes a double as an argument. If an int were passed as a parameter, it would have to be cast to a double. Because we want the behavior of the function to be similar to that of the macro, we should avoid this necessity. This can be achieved in one of two ways: Either overload the function or make it a function template. In this case, overloading the function is the better of the two choices, because a function template would imply that the function could be used for classes as well, which would most likely be inappropriate. Therefore, an int version of the inline function could be written as inline int cubeX(int x) { return x*x*x; }
Generally, we want to avoid using #define for constants and function macros. #define should be used when writing include guards. Remember that include guards are written in the header file to ensure that a header already included is not included again. For example, a typical header file in C++Builder will look like this: #ifndef Unit1H #define Unit1H
// Is Unit1H not already defined? // If not then we reach this line and define it
// Header file code placed here... #endif
// End of if Unit1H not defined
This code ensures that the code between #ifndef and #endif will be included only once. It is a good idea to follow some convention when choosing suitable defines for header files. C++Builder uses an uppercase H after the header filename. If you write your own translation units, you should follow this convention. Of course, you can use a different naming convention, such as pre-pending INCLUDED_ to the header filename, but you should be consistent throughout a project. Using include guards prevents a header file from being included more than once, but it must still be processed to see if it is to be included.
TIP When you follow the IDE naming convention for include guards (appending an ‘H’ to the end of the header filename), the IDE treats the translation unit as a set, and it will appear as such in the Project Manager. If you do not want your .cpp and .h files to be treated in this way, do not use IDE-style include guards.
It has been shown that for very large projects (or more generally, projects with large, dense include graphs), this can have a significant effect on compile times. (For more information, see Large-Scale C++ Software Design by Lakos, 1996, p. 82.) Therefore, it is worth wrapping all include statements in an include guard to prevent the unnecessary inclusion of a file that has been already defined. For example, if Unit1 from the previous code snippet also included
05 9721 CH03
11/13/00
9:49 AM
Page 143
Programming in C++Builder CHAPTER 3
143
ModalUnit1, ModalUnit2,
and ModalUnit3, which are dialog forms used by other parts of the program, their include statements could be wrapped inside an include guard as follows:
#ifndef Unit1H #define Unit1H
// Is Unit1H not already defined? // If not then we reach this line and define it
#ifndef ModalUnit1H #include “ModalUnit1.h” #endif
// Is ModalUnit1H not already defined? // No then include it // End of if Unit1H not defined
#ifndef ModalUnit2H #include “ModalUnit2.h” #endif #ifndef ModalUnit3H #include “ModalUnit3.h” #endif // Header file code placed here... #endif
// End of if Unit1H not defined
TIP Note that the Project Manager in C++Builder 5 has been improved to include an expandable list of header file dependencies for each source file included in a project. Simply click on the node beside the source filename to either expand or collapse the list. Note that the header file dependency lists are based on the source file’s .obj file, hence the file must be compiled at least once to use this feature. Also note that the list could be out of date if changes are made without recompilation.
Know when using the preprocessor will benefit the program and when it won’t. Use it carefully and only when necessary.
Learn About and Use the C++ Standard Library The C++ Standard Library, including the Standard Template Library (STL), is a constituent part of ANSI/ISO C++, just as the definition for bool is. You can save a lot of unnecessary coding by learning to use its features in your programs. The Standard Library has an advan-
PROGRAMMING IN C++BUILDER
This is not pretty but it is effective. Remember that you must ensure that the names you define for include guards must not match any name that appears elsewhere in your program. The define statement will ensure that it is replaced with nothing, which could cause havoc. That is why a naming convention must be agreed upon and adhered to.
3
05 9721 CH03
11/13/00
144
9:49 AM
Page 144
C++Builder 5 Essentials PART I
tage over homegrown code in that it has been thoroughly tested and is fast, and it is the standard, so portability is a big bonus. Standard Library features are summarized in the following list: • Exceptions, such as bad_alloc, bad_cast, bad_typeid, and bad_exception • Utilities, such as min(), max(), auto_ptr, and numeric_limits • Input and output streams, such as istream and ostream • Containers, such as vector • Algorithms, such as sort() • Function objects (functors), such as equal_to() • Iterators • Strings, such as string • Numerics, such as complex • Special containers, such as queue and stack • Internationalization support Nearly everything in the Standard Library is a template, and most of the library consists of the STL, so it is very flexible. For example, the vector template class can be used to store any kind of data of the same type. As a result, it is a direct replacement for arrays in C++ and should be used in preference to arrays whenever possible. For more information about the STL, refer to the “Introduction to the Standard C++ Library and Templates” section in Chapter 4.
Further Reading This section lists some texts that cover the material presented in this chapter much more thoroughly, and they should be a first source for more information about C++ programming. The books are listed in alphabetical order according to the author’s name. A brief summary of the contents of each book is also given. • Cline, M., Lomow, G., and Girou, M. (1999). C++ FAQs Second Edition. AddisonWesley Longman, Inc. This text is based on the online C++ FAQ at http://www.cerfnet.com/~mpcline/c++but it offers much more than the online FAQ. It covers topics from basic to very advanced and is well written, having evolved over several years. This book offers very good value and makes an excellent reference, a great choice for a second book on C++. faq-lite/,
• Gamma, E., Helm, R., Johnson, R., and Vlissides, J. (1995). Design Patterns: Elements of Reusable Object-Orientated Software. Addison-Wesley Longman, Inc.
05 9721 CH03
11/13/00
9:49 AM
Page 145
Programming in C++Builder CHAPTER 3
145
This is the pioneering text on design patterns. Examples are given in C++ (and Smalltalk), making it particularly useful. This book should help you to approach objectbased programming in a different way. • Horton, I. (1998). Beginning C++: The Complete Language. Wrox Press. This text has complete coverage of ANSI/ISO C++. It is not compiler specific and is very up to date. It is precise and includes information not found in other books. It makes a very good first book on C++. • Kalev, D. (1999). ANSI/ISO C++ Professional Programmer’s Handbook: The Complete Language. Que Corporation. This is a relatively recent text with many interesting insights into the C++ standard. It is a handy reference for many of the more advanced topics in C++. It focuses a lot on why certain features are present, offering an insight rarely found in many similar books. • Lakos, J. (1996). Large-Scale C++ Software Design. Addison-Wesley Longman, Inc. Despite the title, this text is essential reading for anyone involved in any but the most trivial C++ projects. Divided into three parts covering basics, physical design, and logical design, the book has many guidelines and principles scattered throughout (collated at the end for ease of reference) that make reading it instantly productive.
This text offers a very thorough treatment of how code is written. It is a definitive guide to this subject and should be read by anyone who really wants to examine and understand the merits of the different ways code is written. It does not present one particular method of writing code but rather comments on a variety of techniques. • Meyers, S. (1998). Effective C++ Second Edition: 50 Specific Ways to Improve Your Programs and Designs. Addison-Wesley Longman, Inc. Perhaps one of the most famous C++ books, it is written in an easy-to-read style, its size belies the great wealth of information it contains. • Meyers, S. (1996). More Effective C++: 35 New Ways to Improve Your Programs and Designs. Addison-Wesley Longman, Inc. The follow-up text to Effective C++ offers more pearls of wisdom to be digested by the avid programmer. Meyers’ books are definitely a must read. • Sutter, H. (2000). Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions. Addison-Wesley Longman, Inc. As the title suggests, this is an advanced text based on the “C++ Guru of the Week” series, which can be found on the comp.lang.c++.moderated newsgroup. It covers many topics and is highly informative. Once several of the previous texts have been digested, thisbook’s true value can be appreciated.
PROGRAMMING IN C++BUILDER
• McConnell, S. C. (1993). Code Complete: A Practical Handbook of Software Construction. Microsoft Press.
3
05 9721 CH03
11/13/00
146
9:49 AM
Page 146
C++Builder 5 Essentials PART I
Summary This chapter has covered a lot of ground. The main points to remember from the chapter are as follows: • Use a definite style when writing code and apply it consistently. This makes the code clearer and more maintainable. • Ensure that commenting in code is up to date and of suitable detail. • Try to understand why code is written the way it is and what consequences might arise from other approaches. • Try to improve your knowledge of C++ and the C++Builder IDE. Both will greatly improve your productivity.
06 9721 CH04
11/13/00
9:48 AM
Page 147
Advanced Programming with C++Builder Stéphane Mahaux Yoto Yotov Vikash Shah
IN THIS CHAPTER • Introducing the Standard C++ Library and Templates • Using Smart Pointers and Strong Containers • Implementing an Advanced Exception Handler • Creating Multithreaded Applications • Introducing Design Patterns
CHAPTER
4
06 9721 CH04
11/13/00
148
9:48 AM
Page 148
C++Builder 5 Essentials PART I
In the previous chapters, you had the chance to immerse yourself in the world of C++Builder programming. It is now time to push your exploration even further by discovering more advanced programming topics. In this chapter, we’ll take a look at one of the most powerful set of tools at the disposal of C++ programmers: the Standard Template Library. You’ll then explore the use of smart pointers and strong containers, as well as the implementation of advanced exception handlers. Finally, we’ll introduce the concepts of multithreading and design patterns.
Introducing the Standard C++ Library and Templates The Standard C++ Library, often referred to as the Standard Library, Standard Template Library, SCL, or STL, is a standardized set of generic classes and functions that provides a powerful framework for C++ programming.
NOTE Historically speaking, the Standard Template Library (STL) is only a subset of the current Standard C++ Library (SCL) that defines the container and algorithm parts. The standard string class, streams, and valarray type are technically separate from the STL. However, in practice it is common to use the terms synonymously.
Although at first glance it can seem somewhat daunting, the SCL should be considered a mustknow topic for any serious C++ programmer. When used appropriately, it can lead to tight, efficient, and flexible solutions to most common programming problems.
Understanding C++ Templates The SCL uses the C++ templates feature to implement most of its capabilities. C++ templates are a relatively new and important addition to the ANSI/ISO C++ specification. They are normally considered an advanced language topic, and a thorough discussion is both unnecessary in the context of this chapter and beyond the scope of this book. However, to appreciate and understand the SCL, a basic grounding is desirable. This section is for readers who are unfamiliar with this language feature. A template may be thought of as a type of cookie cutter, except that templates are used to stamp out a variety of functions and classes. When you write a template, you define to the compiler a family of functions or classes that you want to make available to your program. You indicate in very general terms what these functions or classes have in common; what they do and how they do it.
06 9721 CH04
11/13/00
9:48 AM
Page 149
Advanced Programming with C++Builder CHAPTER 4
149
Let’s take a simple example. Suppose we need a function called largest() that takes an array of integers along with the number of array elements and returns the largest value in that array. Such a function might look like this: int largest(int values[], int numvalues) { int lrg = values[0]; for(int i=1; ilrg) lrg = values[i]; } return lrg; }
This function is quite restrictive—it only allows us to work with arrays of integers. Suppose we later decide that we also need to support a float. We would need to write a new function, even though there would hardly be any difference in the way they work. In this instance, it would be no big deal. We could copy and paste the int version and replace ints with floats—job pretty much done. I think you know what’s coming next! As with any example, things are not always as straightforward in practice. And what if we need to support yet another type at a later time? Templates to the rescue! Listing 4.1 illustrates an alternative solution. LISTING 4.1
A Function Template to Find the Largest Value in Any Array
Although this looks almost like the original function, the template keyword puts us straight. What we have is not a function at all, but a template. The template specifies that T is to be considered an alias for “any type you like.” Assuming you leave the compiler’s template handling options as the default (that is, leave the External option unchecked in the C++ page of the Project Options dialog), the compiler will use the template if possible to generate any function that you use in your program that has not been explicitly defined and that has the characteristics exhibited in the template. To do this, the compiler must have seen the entire template specification before the function call is made. Therefore, unlike with functions or classes in which the usual practice is to place declarations in header files and implementation code in source
4 ADVANCED PROGRAMMING WITH C++BUILDER
template T largest(T values[], int numvalues) { T lrg = values[0]; for(int i=1; ilrg) lrg = values[i]; } return lrg; }
06 9721 CH04
11/13/00
150
9:48 AM
Page 150
C++Builder 5 Essentials PART I
files, the implementation code for templates should appear with the declaration in a header file that can be included from whichever files require the template. I normally refer to such a template as a “function template” (with the emphasis on “template”) while I speak of a specific compiler-generated function as being a “templatized function” (emphasis on “function”) or a “specialized function.” To illustrate the use of the largest template, let’s assume we have placed it in a header file called largest.h, which we include in the C++ console program shown in Listing 4.2. LISTING 4.2 An Example Console Program Demonstrating the Use of the largest Function Template #include “largest.h” #include <string> #include int main(void) { int ivalues[3] = { 2, 6, 3 }; float fvalues[5] = { 0.2, 0.5, 0.3, 0.6, 0.4 }; std::string svalues[3] = { “Standard”, “C++”, “Library” }; std::cout << largest(ivalues, 3) << std::endl << largest(fvalues, 5) << std::endl << largest(svalues, 3); return 0; }
In this code we include the SCL headers <string> and and then declare that we are using a namespace std so that the standard identifiers string (the standard character string type) and cout (a global object providing us with a way to output to a console window) are visible to our program. We then declare three arrays of different types and output the largest values from each (6, 0.6, and “Standard”) on separate lines using three different versions of a function called largest(), based on int, float, and string types, respectively. Each function will be generated by the compiler based on our template.
NOTE SCL container classes are defined in the namespace std. To reference these classes, you have two choices. The first one is to use the :: scope resolution operator: std::cout << 23;
06 9721 CH04
11/13/00
9:48 AM
Page 151
Advanced Programming with C++Builder CHAPTER 4
151
The second one is the using namespace directive, which brings the specified namespace or name into the current scope. using namespace std; cout << 23;
We’ll further describe the namespace std later in this chapter.
Templatized functions need to have parameters of the specified template type, so that the correct version of the function can be determined from the arguments passed in a function call. In the largest example shown in Listing 4.2, for instance, by calling largest() and passing in an array of string, the compiler knows that it needs to use our template by replacing the T (see Listing 4.1) with string. Class templates, on the other hand, do not have this requirement. Instead, the template must be used by explicitly telling the compiler which template type to use when constructing a particular class object instance. As an example, the SCL offers a list class template. To instantiate a list object called mystrings to store string objects, you would declare list<string> mystrings;
Class templates are altogether more powerful than function templates. In fact, function templates are mostly useful when designed to work with template types that are themselves templatized class. There is a lot more to be said about templates but, to wind up this section, a few closing comments are called for.
• There is no specific limit to the number of template type specifiers that can be declared for a template. You can specify several type aliases inside the angle brackets using commas, and even give defaults using = much in the same way as you would write a function parameter list. More than two specifiers without default values, though, and a template will probably never be used, due to being just too cumbersome. • Templates, when used extensively, can lead to severe productivity losses during development. This is because they place a huge burden on the compiler, meaning that compilation can be slow. Also, the code for a particular version of a templatized function or class is duplicated in the object files of all source units that use that function or class. This causes a great deal of code bloat that can affect link times also. Furthermore, the linker
4 ADVANCED PROGRAMMING WITH C++BUILDER
• I oversimplified the “any type you like” definition of the template type specifier. In the largest function template, T is constrained to be any type that supports all of the operations that the template expects to be legal—specifically, any type that does not support the assignment operator = and the greater-than operator > will cause offense to the compiler when it attempts to generate an appropriate templatized function.
06 9721 CH04
11/13/00
152
9:48 AM
Page 152
C++Builder 5 Essentials PART I
faces the task of resolving all of the duplicated information. It has been known for template-rich code to fail to link at all. Ultimately, the purpose of this introduction has not been to provide a grounding in the design of templates, but rather to break you in gently to one of the most confusing and often misunderstood realms of the C++ language. I hope you have had your appetite whetted for templates and become a template cynic like all good C++ programmers should be. Templates are elegant in theory but devilish in practice. This will stand you in good stead for learning about the SCL, from which there is no getting away.
Exploring the Standard C++ Library Features It is possible to divide the SCL into a number of areas or concepts, which are summarized in the following sections. Due to space restrictions, this chapter covers only containers, iterators, and algorithms, and only briefly at that. The reason for giving these areas special attention is that there is a place for these features in a wide range of situations, and newcomers to the library should find that they learn techniques that are immediately applicable. Adaptors are useful to know about, but once you understand containers, the C++Builder Help system is an adequate resource. The string class is an important one to know about. Although C++Builder provides a VCLcompatible string implementation—AnsiString—the string class should be used instead if code portability is needed. To find the string reference in the online help files, go to Help, C++Builder Help, (Index tab), basic_string. The valarray and bitset classes are useful only in a narrow range of applications. Finally, the subject of I/O streams is omitted. This can be a vital tool in most data processing problems, but due to the vastness of this area of the library, it would be impossible to do the topic justice in this chapter.
Containers Containers are class templates that make it easy to work with collections of like objects. They can be customized to work with almost any type of element (providing the elements’ type meets some basic preconditions), and the variety of containers ranges from linked-lists to indexed arrays and associative arrays that manage key-value pairs.
Iterators Iterators are objects that contain pointers to individual elements within a container and that support the notion of the ordering of elements within containers. Each container type has the capability to generate iterator objects appropriate to its own implementation.
06 9721 CH04
11/13/00
9:48 AM
Page 153
Advanced Programming with C++Builder CHAPTER 4
153
Adaptors Adaptors are class templates that work like extremely limited but simple-to-use containers, although in actual fact they provide only an alternative interface to one of the real container types (such as the list container that we previously described). Examples of adaptors include stacks and queues that support only basic push() and pop() interaction.
Algorithms Algorithms are function templates that can be applied to containers to perform a commonly required operation such as finding a particular value, sorting the elements, or removing duplicates. There are more than 60 algorithms in the library. string string is the SCL’s character string implementation. It is a special case of the more generic basic_string templatized class that is defined to work with normal 8-bit character quantities, such as char values. valarray is an often forgotten class template that can be used in mathematical situations. It is unusually limited in some ways, but in certain cases it is the container best suited to working with numeric types. valarray
bitset bitset is a handy class for working with binary values. It provides member functions for setting, testing, and flipping bits, and can also convert a value given as a string of 0 and 1 characters.
I/O Streams
Coming to Grips with Containers and Iterators The saying goes that every story has a beginning, a middle, and an end. Within a story, every chapter has a beginning, a middle, and an end all of its own. We may want to bookmark a particular position in this story, and sometimes we might even bookmark two pages in order to mark out a section. This is a pretty good way to think of a container. In this analogy, the storybook would be the container, its pages would be the contained elements, and the bookmarks would be iterators, as shown in Figure 4.1.
4 ADVANCED PROGRAMMING WITH C++BUILDER
I/O stream describes a family of classes and class templates that deal with streaming. Streams offer a flexible and type-safe way of controlling the transfer of data between a number of media. When we talk about streams, we are speaking generally about file streams (for streaming to and from a physical disc), string streams (for streaming character-like data between storage buffers), or other types of stream.
06 9721 CH04
11/13/00
154
9:48 AM
Page 154
C++Builder 5 Essentials PART I The best bit
container theBook
In the beginning…
theBook.begin() bestBit The End
theBook.end()
Iterators
FIGURE 4.1 A storybook analogy.
It’s high time to start introducing the common container types. We’ll look at the list, vector, deque, set, and map containers, in that order. These names appear in the C++Builder 5 Help system Index tab, and full details of the class template interfaces are easy to find. What follows in this chapter is therefore designed to be only an appetizer; the main course awaits on the Help menu. By reading on, however, you should find yourself in a position to make decisions about when and how to use the features of the SCL, and how to make sense of the documentation that came with C++Builder. First, a preamble. Each of these containers can be made accessible by placing its name within a #include directive, such as #include <list>
As we touched upon earlier, all SCL classes and functions are declared to be in namespace std. Therefore, to declare a list of int, for example, you need to use the following syntax: std::list my_int_list;
The list template declaration under Help, C++Builder Online Help, Index tab, List specifies that two template arguments can be provided: template > class list;
The second class argument, Allocator, is defaulted to an instance of allocator, another SCL templatized class. This class determines how the memory for elements is allocated or freed by the list template. It is not particularly interesting to understand this, but just be aware that there are little hidden extras scattered throughout the SCL that you will normally ignore— don’t let them confuse you.
06 9721 CH04
11/13/00
9:48 AM
Page 155
Advanced Programming with C++Builder CHAPTER 4
155
NOTE The std namespace (any namespace, in fact) can be made current with the using declaration, such as using std::list, or the using directive, such as using namespace std. The latter will make visible all such SCL identifiers. However, to avoid any confusion and to prevent accidental scope changes via #include chains, it is recommended to always explicitly qualify identifiers. To make the following text more readable though, I will drop the std::.
I mentioned previously that there are several different types of containers available for different situations. There are also various types of iterators, because each container type provides its own custom iterator type. Every container provides the following nested types: •
iterator The class of iterator designed for traversing and modifying elements of the particular container.
•
Like iterator, except that this type does not allow modifications to be made to the element that the iterator points to. const_iterator
NOTE
All types of iterators behave like traditional C pointers in that they overload at least the member access operator ->, the dereference operator *, and the increment operator ++. If i is an iterator that points to a contained item, then *i references that item, i->x accesses a member x of that item, and i++ makes the iterator point to the next item. Every container also provides member functions called begin and end. The begin function returns an iterator object (or const_iterator object if the container is considered to be const) that lets us access the first element in the container. The end function returns an iterator not to the last element but to some imaginary element that we don’t much care about; the value of the iterator is known as the past-the-end value, and all we need to know is that we get there by iterating past the last element.
4 ADVANCED PROGRAMMING WITH C++BUILDER
The SCL departs somewhat from normal object-oriented practices in that there is no common base class that determines the similarities in the interface of different containers. Instead, the SCL provides a formal specification of what rules the containers must abide by in order to be fully SCL-compatible. In the main, the SCL is not a particularly object-oriented library. It uses the features of the C++ language that it needs to accomplish its goals in a way that maximizes its applicability across problem domains and minimizes efficiency concerns.
06 9721 CH04
11/13/00
156
9:48 AM
Page 156
C++Builder 5 Essentials PART I
To demonstrate a way in which we can iterate over elements in a container, I have chosen to use a std::list, introduced earlier, by way of example. This choice is quite arbitrary, since all container types support common features such as iteration. Remember that to use the std::list templatized class in source code, you must insert a #include <list> directive. We can use a for loop like so: for(std::list::iterator i = lst.begin(); i != lst.end(); i++) { std::cout << *i << std::endl; }
The list, vector, and deque Containers It is convenient to discuss these three types together because there are a number of important similarities between them. The list container is used to represent a doubly linked list structure. It is at its most efficient when inserting or erasing elements. A list is also a good choice for a collection that needs to be regularly sorted (though we’ll see later that if new elements must always be inserted in sort order, a set is better). The list template supports bidirectional iterators. A bidirectional iterator is a type that permits traversal from one element to the next adjacent element in both the forward and reverse directions. The list::iterator class thus overloads operator ++ and operator--to enable this. The vector type is a random-access container, which means you can access elements by an index into the container using the square-bracket operator, rather like a conventional array. It supports random-access iterators. Like the bidirectional iterators used when working with list containers, the increment and decrement operators are provided. These iterators also support pointer-style arithmetic. Insert and erase operations are possible, but in terms of speed this is not a vector’s strong point. A vector is appropriate when the number of elements it must hold doesn’t change too often. What do you get if you cross a list with a vector? You get a double-ended queue, or deque (pronounced “deck”). The capabilities of a deque are almost a union of those of a list and a vector. Like a list, insert and erase operations are efficient, particularly at the front or the back, though deques aren’t as good at performing these in the middle of the container. However, like vectors, the iterators it uses are of the random-access variety; thus, elements can be accessed by index. All three of the above types support the following member functions for adding new items into the container or removing existing items. For the purposes of the following examples, first let’s declare one of the following typedefs so that the identifier Container refers to a container of ints. The example code will successfully
06 9721 CH04
11/13/00
9:48 AM
Page 157
Advanced Programming with C++Builder CHAPTER 4
157
compile with any one of these typedefs declared, because the functions that are called are provided by all three container types. Due to the simplicity of the examples, there is nothing to choose between them, so your first experience of choosing a container will be really easy! So which will it be? typedef std::list Container;
or typedef std::vector Container;
or typedef std::deque Container;
Let’s begin, starting with some element-insertion functions. void push_back(const T& x); void push_front(const T& x);
The push_back() and push_front() functions are used to insert elements at the back or front of the container. The inserted value is a copy of x. (Do not forget that you can find the description and syntax of these functions in the C++Builder Online Help files.) The following code shows how to instantiate a Container of integers and insert some values. The comments show the order of elements in the Container after each step. Container ints; // construct empty Container object ints.push_back(1); // { 1 } ints.push_back(2); // { 1, 2 } ints.push_front(3); // { 3, 1, 2 }
The insert() function can be used to insert an element elsewhere in the list.
takes an iterator argument pos that points to the element before which the new element is to be inserted. The function returns a new iterator that points to the newly added element. insert()
The code below continues from the previous example and uses insert() to push values into the middle of the Container. // Get an iterator that points to the first item. Container::iterator pos = ints.begin(); advance(pos, 1); // advance the iterator to the next item // Insert two new elements. pos = ints.insert(pos, 4); // { 3, 4, 1, 2 } ints.insert(pos, 5); // { 3, 5, 4, 1, 2 }
4 ADVANCED PROGRAMMING WITH C++BUILDER
iterator insert(iterator pos, const T& x);
06 9721 CH04
11/13/00
158
9:48 AM
Page 158
C++Builder 5 Essentials PART I
NOTE Note that if you declare a typedef for a container type, such as typedef std::list Container as we have done here for the container member function examples, you can also refer to a nested type such as the container’s iterator type by qualifying it with typedef. Instead of referring to the iterator class std::list::iterator, you can simply use Container::iterator. Not only is this easier to read, but it makes the code easier to maintain, such as if you choose to use a different container type in place of the list.
The following functions can be used to remove elements in a similar way: void pop_back(void); void pop_front(void);
The pop_back() and pop_front()functions remove the element at the back or the front of the list. Continuing our example code, we can remove the front and back elements like so: ints.pop_front(); // { 5, 4, 1, 2 } ints.pop_back(); // { 5, 4, 1 }
We can also erase elements from the middle of the Container with the erase() function. iterator erase(iterator pos);
The erase() function removes an element given by pos. The iterator that is returned points to the next element. The following code shows how to remove the second element in the Container and change the value of the next element. pos = ints.begin(); advance(pos, 1); pos = ints.erase(pos); // { 5, 1 } *pos = 6; // { 5, 6 }
These are just some of the most commonly used functions, and they are presented here to demonstrate how to perform basic manipulation.
The set and map Containers I hinted earlier that the list, vector, and deque class templates had something in common that set them apart from the set and map types. There are differences and similarities with all containers, in truth, but one of the first questions I ask myself when setting about choosing an appropriate container is this: Do I want to ensure that the elements in my container are always unique, fast to locate, or maintained in ascending order?
06 9721 CH04
11/13/00
9:48 AM
Page 159
Advanced Programming with C++Builder CHAPTER 4
159
If the answer is “Yes, most definitely,” I use either a set or a map. If the answer is “No thanks, I like the sound of ‘fast to locate’ but the other things don’t fit my requirements,” then I go for one of the former choices. A set can be thought of as a list whose elements are always sorted. When new elements are inserted into a set, they always get placed in their rightful position in sort order. It is not possible to insert them in any other way. When you try to insert a value, an iterator can be supplied as a hint to where it should go in order to make the operation quicker, but it will end up in one place regardless. Because of this strict ordering, tests for inclusion of a specific value are fast. A map is an associative container that holds key-value pairs. Recall that in order to use any templatized class, such as map, you need to first specialize that class; in other words, you need to tell the compiler which types of object your particular instance of that class should work with. In the case of a map, you need to specify two types—the type of key that the map uses to index its elements and the type of values it holds. is another SCL class template that contains two public data members, called first and that are of two types specified in the template specialization. A map’s elements are instances of pairs specialized with the same types as the map, in which the first member is the key and the second member is the value. The keys in a map are always unique. At most one of the pair elements will have a first member of any given value. Furthermore, much like a set, the pair elements always remain in sort order (based on the pair’s key). A map uses bidirectional iterators that point to pair elements. If you have such an iterator, i for instance, to get the key or value from it you would use i->first or i->second. pair
second,
The way that both set and map maintain ordering is determined by one of the template parameters. The set template is declared like so:
The second parameter, Compare, is a class that is used to compare two values. The default argument is the SCL’s less class template specialized on the type passed in the first parameter (whose alias is Key). The less template simply uses the less-than operator (<) and is a special type of template that produces an object known as a function object (and also known as a functional or functor). It is declared like so: template struct less : binary_function { bool operator () (const T& a, const T& b) const; }; template bool less::operator () (const T& a, const T& b) const
4 ADVANCED PROGRAMMING WITH C++BUILDER
template , class Allocator = allocator > class set ;
06 9721 CH04
11/13/00
160
9:48 AM
Page 160
C++Builder 5 Essentials PART I { return (a
Function object classes use an overloaded parenthesis operator so that instances can be used syntactically as if they were functions. They are a more powerful replacement for oldfashioned function pointers. When we look at algorithms shortly, we’ll see that function objects are used in many situations. To declare a set of ints that are sorted in descending order instead of ascending order, we could use the SCL greater template: std::set > intset;
To insert values into sets or maps, we use the insert() member function. To search for a specific value using its key, we use find. pair insert(const value_type& x); iterator find(const key_type& x);
is a typedef for the type of the key. In the case of a set, value_type and key_type are identical. For a map, value_type is the key/value pair type. An easy way to create a pair object on-the-fly is with the make_pair function template, which takes the two required pair values as arguments and returns an appropriately typed pair object containing those values. insert() returns a pair itself, of type pair. If the pair being inserted is already found in the set or map, then the returned pair will contain the past-the-end iterator and the Boolean value false. If it is not found and the insertion is valid, the returned pair will contain an iterator to the inserted pair, and the Boolean value will be true. Another method of adding a value to a map that is easier to read is with the overloaded square bracket operator. Both methods are shown in the example code in Listing 4.3. The find() function should be quite self-explanatory by now. It returns an iterator to the element whose key matches x, or the past-the-end iterator if it’s not found. key_type
LISTING 4.3
Using a map Container
typedef std::map<std::string, std::string> ssmap; ssmap days; days.insert(make_pair(std::string(“Mon”), std::string(“Monday”))); days[“Tues”] = “Wednesday”; // What?!! Can’t be right, surely! days[“Tues”] = “Tuesday”; // Right, soon fixed that! // Have we added Thursday yet? ssmap::iterator i = days.find(“Thurs”); if(i == days.end()) { // Only add an entry for Thursday if not already there.
06 9721 CH04
11/13/00
9:48 AM
Page 161
Advanced Programming with C++Builder CHAPTER 4
LISTING 4.3
161
Continued
// (Okay, we know there can’t already be an entry really!) days[“Thurs”] = “Thursday”; } std::cout << “Enter day abbreviation: “; char input[32]; std::cin >> input; if(days.find(input) != days.end()) { std::cout << “Full name is “ << days[input] << std::endl; } else { std::cout << “Sorry, I don’t know that day.” << std::endl; }
There is a lot more that can be said about all of the containers that we have discussed, but by now you should have a basic working knowledge of them.
Using the Standard Algorithms I have explained how each container type has its own strengths and weaknesses. The class interfaces listed in the Help files are a good guide, too. The list template, for example, includes a sort member function for sorting elements, but the vector template does not. The reason is that a list is pretty good when it comes to sorting and has its own nippy sort routine. A vector, on the other hand, isn’t geared to sorting its elements. But it’s perfectly reasonable to want to sort a vector, and the SCL does provide an easy way to do so.
Many algorithms take two iterators that define a range of elements in a container over which the algorithm will be applied. A range, by convention, is assumed to include the first iterator and every element that follows up to but not including the second iterator (it is assumed but not verified that the second iterator does appear sequentially after the first). Consequently, the begin() and end() member functions of a container correctly define a range consisting of all elements in the container. In the previous section we introduced the idea of a function object. Many algorithms take a function object as an argument.
4 ADVANCED PROGRAMMING WITH C++BUILDER
Algorithms are non-member function templates that are used to do the everyday things you might want to do with a container. One such algorithm is sort, which can be used to sort a range of (or all) the elements inside a container. It would be inefficient to use it to sort an entire list, because list provides its own function that is much quicker. It would be a complete waste of time trying to sort one of the containers that are always sorted, such as set or map. But under other circumstances it is the easiest way to sort elements.
06 9721 CH04
11/13/00
162
9:48 AM
Page 162
C++Builder 5 Essentials PART I
Let’s consider a specific algorithm as an example. We’ll go with the more complex of the two versions of sort, which has three parameters. Coincidentally, they happen to include two iterators and a function object. Who said my articles were contrived?! The sort algorithm looks like this: template void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);
The parameters first and last are used to specify the range of elements to sort. The comp parameter requires a function object that compares two elements in the container and returns true if the first element should appear before the second when sorted. Otherwise, it returns false. Suppose we have a container declared as follows: std::deque<string> strings;
We can use the sort algorithm to sort strings based on their lengths, for example, so that the longer the string, the nearer to the front of the list it appears. We need to define a function object like this: struct length_compare { bool operator () (const string& a, const string& b) const { return a.length() > b.length(); } };
This struct defines a function object that when invoked takes two strings and returns true if the first is longer than the second. To sort the entire strings container, we can call std::sort, constructing and passing in a length_compare object on-the-fly: std::sort(strings.begin(), strings.end(), length_compare());
The container will now be sorted. To see this in action, build the project SCLProg.bpr, which can be found in the SCLProg folder on the CD-ROM that accompanies this book. The application prompts the user for strings to push into the deque, then sorts the deque using a length_compare function object. The program also demonstrates how to use the for_each algorithm to write the sorted list back to the form, or to a file on disc. Numerous algorithms exist for performing these kinds of tasks. A full list of algorithms provided by the Standard C++ Library is available from the online Help index (look under “algorithms”).
Closing Thoughts on the SCL I have been barely able to scratch the surface of the SCL. This demonstrates that the features that are provided encompass a very wide range of possible applications.
06 9721 CH04
11/13/00
9:48 AM
Page 163
Advanced Programming with C++Builder CHAPTER 4
163
It is quite a relief to know that the supplied online Help files form a reasonably comprehensive guide. There are some good explanatory sections and discussions to be found among all the class listings and other reference material. After reading this chapter, if you feel tempted to dive into the documentation with new-found faith, I suspect you’ll have gained a lot from this.
Using Smart Pointers and Strong Containers This section deals with allocating memory on the heap and having it freed automatically. In other words, it is a method that can significantly reduce the chance of memory leak. What’s more, it will make your code easier to read and maintain. This method is not specific to C++Builder; it will work with any compiler that supports templates. This is an important and ubiquitous subject, and every C++ programmer should benefit from it.
The Heap Versus the Stack Simply put, the stack is the memory managed by the compiler as part of the program’s environment. As a programmer, the feature I like most about the stack is that I never have to worry about freeing it, even if an exception is thrown. It will always be unwound automatically as the program exits the current scope. That is, the compiler will always free the memory allocated on the stack automatically. The heap, on the other hand, is the memory managed by the program. It provides great flexibility, but keeping track of it all can be a challenge and a half, because unlike the stack, it is the responsibility of the programmer to free all of it. When some of it is not freed, the program is said to leak memory.
Pointers
Interestingly, pointers are usually allocated on the stack and are therefore destroyed automatically. There is a way to take advantage of this, as we shall see.
Strong Pointers There is a simple method of tying the heap to the stack. This means the heap will be freed automatically when the stack is unwound. Guaranteed! Here is how it works. Instead of using a regular pointer to reference the memory you allocated, use a strong pointer. It consists of a simple templatized class whose destructor will free the heap memory. So when the strong pointer is automatically destroyed as the stack is unwound, the heap memory referenced will
4 ADVANCED PROGRAMMING WITH C++BUILDER
In C++, when a programmer allocates memory on the heap, pointers are used to reference it and work with it. Unfortunately, a pointer doesn’t provide an automatic means of freeing the memory it references. It is solely the responsibility of the programmer to free every byte allocated. In today’s complex programs, that’s easier said than done.
06 9721 CH04
11/13/00
164
9:48 AM
Page 164
C++Builder 5 Essentials PART I
be freed as well. Also, override the dereference operator so the class will behave just like a regular raw pointer. Because the strong pointer is templatized, it can reference any type of object, so all angles are covered. Listing 4.4 illustrates the heart of this class, called auto_ptr. LISTING 4.4
The Heart of Class auto_ptr
auto_ptr<X>::auto_ptr( X* p ) { the_p = p ; } auto_ptr<X>::~auto_ptr() { delete the_p ; } X* auto_ptr<X>::operator ->() { return the_p ; } X& auto_ptr<X>::operator *() { return *the_p ; }
This really is a simple class, which makes it even more of a gem. Here is an example of how to use it: auto_ptr myLabel( new TLabel(this) ) ; // Strong pointer myLabel->Parent = this ; myLabel->Caption = “This is a label” ;
NOTE A small reminder: auto_ptr, like other SCL classes, is part of the namespace std. To compile the previous code, insert using namespace std; in the beginning of your code or add the std:: preface. Also, make sure to include the memory.h file, found in the C++Builder Include folder.
The first important thing to notice about this code is that I didn’t have to free the label. The second is that the strong pointer is used just like a regular pointer. The third is that because strong pointers can hold only pointers, the template type refers only to the object referenced.
06 9721 CH04
11/13/00
9:48 AM
Page 165
Advanced Programming with C++Builder CHAPTER 4
165
A strong pointer is said to own the memory or object it references. It is important to realize that, because the object you created cannot outlive the strong pointer. The strong pointer must have a scope big enough for the purpose of the object.
TIP The auto_ptr class is easiest to use when the object or memory you allocated on the heap is needed for a period of time equal to the life of the strong pointer referencing it—in other words, when both the strong pointer and the memory it references can be destroyed automatically at the end of the scope. Of course, you can also use it if the memory has a smaller life span than the scope. In this case, you simply tell the strong pointer to free the memory using reset(). This is very useful when you need to make your code exception-safe.
Only one strong pointer can own the memory referenced. This makes sense, because the memory can be freed only once. As a result, when one strong pointer is assigned to another, ownership is transferred instead of shared. This may seem strange, because programmers usually equate assigning with copying, but here the strong pointer being assigned is modified. For instance auto_ptr label1( new TLabel(this) ) ; auto_ptr label2 ; label2 = label1 ; // Ownership transferred
After this code has executed, label2 owns and references the TLabel object, while label1 owns nothing and points to NULL.
void TForm1::Button1Click( TObject* Sender ) { static int counter; auto_ptr myLabel( new TLabel(this) ) ; // Strong pointer myLabel->Parent = this ; myLabel->Name = “Label” + String(++counter) ; myLabel->Top = 10 ; myLabel->Left = 10 ; myLabel->Caption = “This is a label named “ + myLabel->Name ; }
Upon execution, it is disappointing that the label does not appear on the form. That is because it was destroyed by the strong pointer when the stack was unwound at the end of the method.
4 ADVANCED PROGRAMMING WITH C++BUILDER
Let’s take the first example and put it in the context of C++Builder. Create a new application, drop a button, and attach the following OnClick event handler to it:
06 9721 CH04
11/13/00
166
9:48 AM
Page 166
C++Builder 5 Essentials PART I
To work as expected, the strong pointer must have a class scope, just as a regular pointer would. Also, to make the whole process exception-safe, only strong pointers should be used. Here is the new OnClick event handler: // In the header file: class Tform1 : public Tform { // ... private: auto_ptr classLabel ; // ... }; // In the source file: void TForm1::Button1Click( TObject* Sender ) { static int counter = 0 ; auto_ptr myLabel( new TLabel(this) ) ; // Strong pointer myLabel->Parent = this ; myLabel->Name = “Label” + String(++counter) ; myLabel->Top = 10 ; myLabel->Left = 10 ; myLabel->Caption = “This is a label named “ + myLabel->Name ; classLabel = myLabel ; // Transfer to class scope }
Every time the button is clicked, a new TLabel will be created. When it is assigned to classLabel, this strong pointer will first free the object it previously owned, then take ownership of the new label. No leak! You may be surprised to learn that this templatized class is already part of the C++ standard. And because C++Builder is fully compliant with the standard, you already have it. You can find it in the memory.h header file, which was installed in the product’s Include folder. Have a look; it’s not too complex, and you will see all the methods available. That is the good news. The bad news is that the standards committee stopped there, failing to provide a complete solution. In fact, the auto_ptr class barely made it into the standard. However, two more classes are presented that should satisfy most of your needs.
CAUTION The auto_ptr class includes a method called get(), which must be used with extreme care. It returns a direct reference to the memory it owns.
06 9721 CH04
11/13/00
9:48 AM
Page 167
Advanced Programming with C++Builder CHAPTER 4
167
An alarm should have gone off in your head. It was previously explained that one and only one strong pointer could own the memory allocated on the heap. But here is a method that could be used to create another strong pointer referencing and owning the same memory. It could also be used to free the memory the strong pointer owns. This is probably the reason the auto_ptr class was controversial and was accepted in the standard only at the last minute. You see, there is no way around this. There has to be a way of passing a raw pointer to API calls requiring it. When you use the get() method, be absolutely sure it is only for function calls that will positively not free that memory. Or if it does, then it is your responsibility to call the release() method before the strong pointer is used, or before it runs out of scope and tries to free that memory again.
Smart Pointers Smart pointers build on strong pointers by allowing multiple smart pointers to reference the same object. This is accomplished by having all the smart pointers share an SCL set referencing each other, so the last smart pointer to be destroyed will know to free the memory. We won’t go into the implementation details here; suffice it to say that it works like a charm. The implication that is important to realize here is that the memory allocated is not married to the scope of the first smart pointer to own it. Its life span can change every time the application is run, depending on when the last smart pointer owning it is destroyed. This templatized class is used pretty well like the auto_ptr class. However, its definition does imply one major difference. When assigning one smart pointer to another, ownership is indeed shared.
The Caution issued previously about using the strong pointer’s get() method is just as valid with smart pointers. It has the same potential for bringing havoc and must be used with the same extreme care.
Furthermore, the SmartPointer class provided on the CD-ROM accompanying this book is thread safe (look for files SmartPointer.cpp and SmartPointer.hpp). This makes the SmartPointer class absolutely invaluable when writing multithreaded code. Imagine how complicated it used to be for a programmer to know when to free memory shared between threads,
ADVANCED PROGRAMMING WITH C++BUILDER
CAUTION
4
06 9721 CH04
11/13/00
168
9:48 AM
Page 168
C++Builder 5 Essentials PART I
given that he could not know which thread would terminate last. Using this class, that nightmare can usually be ignored. Apart from the differences mentioned, it is used just like the auto_ptr class. Here is a quick example: SmartPointer label1( new TLabel(this) ) ; SmartPointer label2 ; label2 = label1 ; // Ownership is shared
After the code has executed, label1 and label2 both own and reference the object. Either smart pointer can be destroyed first.
A Strong Container Managing dynamic objects as a group is another very common task. Any C++ programmer who has used the Standard C++ Library (SCL) would find it hard to work without it. Creating a strong, leak-proof container similar to the SCL’s vector container is more involved than creating the strong pointer class. That is because the functionality of the vector must be duplicated. The companion CD-ROM contains such a container, called StrongVector. Look for files StrongVector.cpp and StrongVector.hpp. What’s more, the SCL’s vector and StrongVector work exactly the same, so there is no learning curve from one to the other. The SCL is reviewed at the beginning of this chapter. Listings 4.5 and 4.6 show a simple example of the SCL’s vector and StrongVector. LISTING 4.5
Using the SCL Vector
public: typedef vector LABELS ; LABELS labels ;
TForm1::TForm1( TComponent* Owner ) { labels.push_back( new TLabel(this) ) ; } TForm1::~TForm1() { LABELS::iterator i ; i = labels.begin() ; while( i++ < labels.end() ) delete *i ; }
06 9721 CH04
11/13/00
9:48 AM
Page 169
Advanced Programming with C++Builder CHAPTER 4
LISTING 4.6
169
Using StrongVector
public: typedef StrongVector LABELS ; LABELS labels ; TForm1::TForm1( TComponent* Owner ) { auto_ptr label(new TLabel(this) ) ; labels.push_back( label ) ; }
This comparison shows the differences in their use. First, notice that when declaring the strong container and strong pointer, the template type is not a pointer. This is because the two are limited to owning pointers, so they will make the types into pointers automatically. Second, a strong pointer is used to transfer the label to the strong vector. This is to ensure that the strong chain is not broken. Finally, the memory does not have to be freed explicitly with StrongVector. There is a lot more to the SCL than vectors. Duplicating the whole library would be a huge task. Luckily, it’s not one that needs to be done. The power of the SCL can be harnessed, provided the objects are first assigned to a StrongVector for safekeeping. After that, the regular (raw) pointers can be managed using any of the SCL’s containers. Listing 4.7 provides an example. LISTING 4.7
Using Other SCL Containers Through StrongVector
public: typedef StrongVector CONTAINER ; CONTAINER container ;
TForm1::TForm1( TComponent* Owner ) { CONTAINER::iterator i ; for( int ctr=0; ctr < 10; ++ctr ) { // Assign labels to a strong container auto_ptr label(new TLabel(this) ) ; container.push_back( label ) ;
ADVANCED PROGRAMMING WITH C++BUILDER
typedef set LABELS ; LABELS labels ;
4
06 9721 CH04
11/13/00
170
9:48 AM
Page 170
C++Builder 5 Essentials PART I
LISTING 4.7
Continued
// Now, any SCL container can be safely used i = container.end() ; labels. push_back( *i ) ; } }
Pitfalls You have just read how wonderful strong and smart pointers are, but it is important to realize that these are not regular pointers. Also, there are pitfalls to watch out for. • Never initialize a strong/smart pointer with a reference to the stack. Imagine the mess when it tries to free that memory. • Never initialize a strong/smart pointer using the this pointer. You would end up automatically deleting your own object and then some. • Never pass a strong pointer as a parameter. Two unwanted side effects will result. The first is that the strong pointer used as a parameter will transfer ownership to the instance in the function or method. The second is that the memory will be freed upon completion of that function or method. This is not a problem with smart pointers. • Never pass a dereferenced smart pointer to another thread. That would break the chain. If the first thread finishes first, it will free the memory, and the second thread’s reference will then be invalid. • Using strong/smart pointers means you lose inherited polymorphism. In other words, auto_ptr cannot be used in place of auto_ptr.
Smart Pointers and Strong Containers Summary Using the classes introduced here, the trend is reversed. You can write more stable applications without taking more time. Using strong pointers/vectors means the days of littering your code with try...catch statements are gone. And when using the SmartPointer class in threads, you don’t have to make all the threads know about the other ones sharing memory with them. The strong memory management paradigm saves you time writing exception-safe code, and it saves you time not looking for memory leaks. Code maintenance is an important cost factor. That is why code readability is noteworthy. It is a side effect of the fewer lines of code required to make the application exception safe. You will not escape the try and catch statements, but it will not be to manage the heap. As great as the auto_ptr, StrongPointer, and StrongVector classes are, it is important to realize that they are not appropriate in all situations. Learn their pitfalls so they will not trap
06 9721 CH04
11/13/00
9:48 AM
Page 171
Advanced Programming with C++Builder CHAPTER 4
171
you. For instance, even though a smart pointer can be safely passed as a parameter, you should consider the cost of doing so frequently.
NOTE For more information on this topic, the following two articles are well worth reading. • Roubal, E. “Templatized Managed/Smart Pointers,” C++Report, February 1998 Vol10/No2:23–28 • Milewski, B. “Strong Pointers and Resource Management in C++,” C++Report, September 1998 Vol10/No8:23–27
When I first read Roubal’s article about smart pointers, it was a revelation! I hope you will benefit from this object-oriented feature as much as I did.
Implementing an Advanced Exception Handler Exception handling, exception handler, and exception safety are all related but different. Exception handling is a mechanism for capturing, propagating, and dealing with error conditions in an application. Sometimes this method can backfire, so programmers must also learn about exception safety. These topics are fundamental to the language and are covered in Chapter 3, “Programming in C++Builder,” as well as in books teaching C++ fundamentals. In this section, it is assumed that the reader is familiar with the concepts of exception handling.
Reviewing the Strategy Let’s begin with a quick overview of exception handling strategy when using the exception handler presented here. First, make the program’s code exception safe. This means that an exception will not make the program unstable or leak resources. Be especially careful with constructors, loops, and memory allocation. Second, check WinMain() for exception safety. A C++Builder program can fire an event only during message processing. Message processing starts with the call to Application.Run(), so the exception handler will not be called for exceptions occurring in WinMain(). That code must be contained in a try or catch statement. All C++Builder skeletons do this automatically. To view the skeleton of your current application, go to Project, View Source.
4 ADVANCED PROGRAMMING WITH C++BUILDER
An exception handler is an application-wide unit or class that helps deal with a thrown exception. This section presents a class for C++Builder programmers to report an exception. As a bonus, this class will automatically catch and report exceptions not handled directly in the program.
06 9721 CH04
11/13/00
172
9:48 AM
Page 172
C++Builder 5 Essentials PART I
Third, catch exceptions to resolve situations that require special attention, such as when you don’t want a message to be displayed to the user or you want to resolve the problem then and there. Don’t worry about other situations; that is the advantage of the exception model. Finally, replace the compiler’s default exception handler with the one described a little later in this discussion. It is a class called ErrorHandler.
NOTE In the previous chapter, you learned how to use the try, catch, and __finally keywords. If you’re still unfamiliar with the principles of exceptions, now is the time to go back and carefully review them. You’ll soon learn how to replace the default exception handler, but without basic knowledge on exceptions and exception handling, you’ll be unable to follow.
Reviewing the Advantages This approach provides a number of major advantages over the default one: • It logs all available information into a file for later review by the support staff. • It takes a snapshot of the state of the whole system. This will include information about the exception, the object that threw it, the active form, the application, and the Windows operating system. • It includes form-specific information. This is very important because it provides context to the exception. Showing which action the user took at the time of the exception is one of the most important clues for finding where in the program the error occurred. • It provides an opportunity to address application-specific requirements. • It improves integration with the application. For instance, the error message can be displayed in a custom form, or it may be logged but not displayed at all. • It allows reporting and logging of messages that are not exceptions.
Replacing the Compiler’s Default Exception Handler We can call our ErrorHandler class in the catch block of all the try and catch statements in the application. But what we really want is a ubiquitous exception handler that will trap even uncaught exceptions. After all, most exceptions are unexpected. In other words, we want to tell C++Builder to use our ErrorHandler class whenever an exception reaches it.
06 9721 CH04
11/13/00
9:48 AM
Page 173
Advanced Programming with C++Builder CHAPTER 4
173
Understanding the Technique C++Builder does have a default exception handler. However, it is extremely basic; all it does is display the error message in a modal message box. Luckily, the VCL’s engineers have provided a simple and easy way of replacing it: Just assign a handler to TApplication::OnException. From that point on, the provided handler can call the ErrorHandler class to process all uncaught exceptions. I’ll provide you an example later in this chapter.
MDI Child Forms For MDI child forms (those whose FormStyle property is fsMDIChild), nothing needs to be done. That is because the handler knows where to get the information it requires, including the active MDI child form.
SDI or Modal Forms A form of any style except fsMDIChild should assign its own exception handler. Although this is not required, it will allow the handler to include very useful form-specific information not available otherwise. It is done in three steps: 1. Create the form’s exception handler and have it call ErrorHandler::LogEntry(). Use the this pointer as the last parameter so the method can include information about the form. See ErrorHandler::DefaultExceptionHandler() in Listing 4.12, later in this chapter, for an example. 2. In the form’s OnActivate event handler, use the technique described earlier to make the method created in step 1 the new default handler. As a result, the VCL will use it to handle uncaught exceptions as long as the form is active. See the ErrorHandler’s constructor in Listing 4.9 for an example.
If deriving forms from a base form, this process needs to be done only once in the base form. Consider creating a base form from which all other forms in the project will be derived. I’ll stop here to explain why such measures are necessary. replaces the default compiler exception handler. One of the parameters of this function is form, which is passed to the ErrorHandler::LogEntry() method. The form variable is used to track form-specific information. As you’ll see later in this chapter, form is set by default to the active MDI child form. That’s why information about SDI or modal forms will not be displayed in our log file. By following the three steps described previously, you’ll be able to address this issue. ErrorHandler::DefaultExceptionHandler()
4 ADVANCED PROGRAMMING WITH C++BUILDER
3. In the form’s OnDeactivate event handler, restore the previous exception handler or set TApplication::OnException to NULL. If no handler is assigned, C++Builder will revert to the original default handler.
06 9721 CH04
11/13/00
174
9:48 AM
Page 174
C++Builder 5 Essentials PART I
Other Classes For all classes that are not forms—such as TDataModule or your own classes—nothing needs to be done. These classes are assumed to be in the context of the active form, and its handler will be used.
Adding Project-Specific Information to the Class Because there is only one instance of this class per application, modifications could be made to it directly. However, deriving from it will result in better reusability across projects and good use of the object-oriented model. The only important point to remember when deriving your own class from this one is to instantiate it rather then the base class. More on that in the next section.
The Exception Handler’s Source Code We are finally ready to examine the ErrorHandler class. Each method will be described, followed by its listing. In order to conserve space, the header file and some less important methods for C-style structured exceptions are not listed here. For the full source code, please refer to the EHandler.cpp and EHandler.h files on the companion CD-ROM.
TIP Go through this source code carefully. The API calls and techniques it uses could be useful in other parts of your program. For instance, method LogObjectState() shows how to get the DataSource property without casting the control.
The source code in Listing 4.8 should look familiar. It is the same as in any new C++Builder form. Even though this class is not a form, it will perform a lot of its work using the VCL. The last line, however, is different and worthy of special attention. First, ErrorHandler is the name of the class about to be defined. Second, the errorHandler_G variable is not a pointer but an actual instantiation of the exception handling class. Because the variable is declared as external in the header, the exception handler will be accessible globally, immediately upon program launch. Remember that global objects are instantiated before any functions or methods are called, even WinMain().
06 9721 CH04
11/13/00
9:48 AM
Page 175
Advanced Programming with C++Builder CHAPTER 4
LISTING 4.8
175
Class Instantiation
#include #pragma hdrstop #include “EHandler.h” //--------------------------------------------------------------------------#pragma package(smart_init) //--------------------------------------------------------------------------ErrorHandler errorHandler_G ; //---------------------------------------------------------------------------
Three important things happen in the constructor (see Listing 4.9). First, it is called when the errorHandler_G variable is automatically instantiated at program startup. You can use the Run, Trace to Next Source Line menu command (or Shift+F7) to follow line-by-line the execution of your program. Second, it opens the log file (OpenLogFile() is implemented in Listing 4.11), so we are sure it will be available when an error needs to be logged. Finally, it replaces the compiler’s default exception handler with itself. In other words, simply adding this unit to a project will automatically and effortlessly result in the use of this exception handler. LISTING 4.9
The Constructor Replaces the Compiler’s Exception Handler
// Replace compiler’s default exception handler // with our own default handler.
4 ADVANCED PROGRAMMING WITH C++BUILDER
__fastcall ErrorHandler::ErrorHandler( void ) : ERROR_TITLE ( “<<< ERROR INFORMATION >>>” ), MESSAGE_TITLE( “<<< MESSAGE >>>” ), SYSTEM_TITLE ( “< SYSTEM STATE >” ), PROGRAM_TITLE( “< APPLICATION STATE >” ), OBJECT_TITLE ( “< OBJECT THROWING EXCEPTION >” ), FORM_TITLE ( “< FORM >” ), DETAIL_TITLE ( “< ERROR DETAILS >” ), DATA_TITLE ( “< DATA >” ), log ( NULL ), log_External ( NULL ) /****************************************************************************\ * ACCESS: Public * \****************************************************************************/ { // Open the log file OpenLogFile() ;
06 9721 CH04
11/13/00
176
9:48 AM
Page 176
C++Builder 5 Essentials PART I
LISTING 4.9
Continued
previousExceptionHandler = Application->OnException ; Application->OnException = DefaultExceptionHandler ; }//Constructor() //---------------------------------------------------------------------------
Listing 4.10 shows the destructor. It first verifies that it was not replaced as the default exception handler. That is because it does not make sense to have two exception handlers. It also reverts to the compiler’s handler, in case this is not the end of the program. Finally, it frees the class’ resources. LISTING 4.10
The Destructor
__fastcall ErrorHandler::~ErrorHandler() { // Warn if an error MAY have occurred in ErrorHandler:: // DefaultExceptionHandler() // or if somebody else tried to steal the show. if( Application->OnException == NULL ) WriteToLog( “***Class ErrorHandler was disabled. An error “ “may have occured in it.***” ) ; else if( Application->OnException != DefaultExceptionHandler ) WriteToLog( “***The OnException event points to another “ “handler. There may be a conflict.***” ) ; // Restore previous default exception handler Application->OnException = previousExceptionHandler ; // Free resources delete log ; delete log_External ; }//Destructor //---------------------------------------------------------------------------
The methods in Listing 4.11 deal with opening the log file. What is interesting in OpenLogFile() is that the file buffer (declared privately in the header) is opened for both input and output. However, this class only appends information to the log. The sole purpose of opening it for reading as well is to provide a way for the application to display the log. In doing so, this class • Keeps controls of the log file • Is assured the application has read-only access to it • Minimizes resource use by sharing a single file buffer between the two streams
06 9721 CH04
11/13/00
9:48 AM
Page 177
Advanced Programming with C++Builder CHAPTER 4
177
When using this buffer-sharing technique, special care must be taken to make the file buffer outlive the streams depending on it. In this case, the streams are freed in the destructor, while the file buffer is freed when the class’ stack is unwound at the end of the destructor. LISTING 4.11
Opening a Log File
void __fastcall ErrorHandler::OpenLogFile( void ) /****************************************************************************\ * ACCESS: Protected * \****************************************************************************/ { // Open the log file // Note: The file will be closed automatically // when the buffer object is destroyed. // The buffer is guaranteed to outlive the streams // since it is allocated on the stack // while the streams are on the heap. sharedFileBuffer.open( GetLogFileName().c_str(), ios_base::in | ios_base::out | ios_base::app ) ; log = new ostream( &sharedFileBuffer ) ; log_External = new istream( &sharedFileBuffer ) ; }//OpenLogFile() //---------------------------------------------------------------------------
return logFileName ; }//GetLogFileName() //--------------------------------------------------------------------------istream* __fastcall ErrorHandler::GetLogFileStream( void ) /****************************************************************************\ * ACCESS: Public * \****************************************************************************/ { return log_External ; }//GetLogFileStream() //---------------------------------------------------------------------------
4 ADVANCED PROGRAMMING WITH C++BUILDER
String __fastcall ErrorHandler::GetLogFileName( void ) /****************************************************************************\ * ACCESS: Public * \****************************************************************************/ { String exeName( Application->ExeName ) ; int extStart = exeName.Pos( “.” ) ; String logFileName( exeName.SubString( 0, extStart - 1 ) ) ; logFileName += “.log” ;
06 9721 CH04
11/13/00
178
9:48 AM
Page 178
C++Builder 5 Essentials PART I
Listing 4.12 contains the new default exception handler that was assigned in the constructor. All it does is call LogEntry(), which does the actual work. You will find it in Listing 4.13. As you can see, the method knows which MDI child form is active. If the program does not use MDI, no form information will be logged unless either the exception is caught directly or the default handler is replaced for each form as described earlier. The method also temporarily reverts to the default exception handler. This is in case the handler itself throws an exception. LISTING 4.12
The New DefaultExceptionHandler
void __fastcall ErrorHandler::DefaultExceptionHandler( TObject *sender, Exception* exception ) /****************************************************************************\ * ACCESS: Public * \****************************************************************************/ { // Restore compiler’s default exception handler Application->OnException = NULL ; // Handle passed exception LogEntry( sender, exception, EXCEPTION_UNHANDLED, Application->MainForm-> ActiveMDIChild ) ; // Use our own handler again. Application->OnException = DefaultExceptionHandler ; }//DefaultExceptionHandler() //---------------------------------------------------------------------------
Method LogEntry() is where the work is actually performed. From its few parameters, it manages to extrapolate quite a bit of information. And because it is a public member, it can be called directly, but usually from within a catch block. Looking up LogEntry() in Listing 4.13, you will see that the first thing it does is inform the user of the problem. Only the exception message is displayed. Other information is superfluous to the user. Please note that the doDisplay parameter defaults to true. The method then checks whether this is a fatal error. Next, the actual entry is appended to the log file. Several private and protected member methods are called to acquire and log information about the system’s state. Of course, this is one class that must truly be exception safe, so all this is contained within a try/finally statement. Let’s understand that exception safe does not mean exception free. That is why DefaultExceptionHandler() temporarily reverts to the compiler’s default handler.
06 9721 CH04
11/13/00
9:48 AM
Page 179
Advanced Programming with C++Builder CHAPTER 4
LISTING 4.13
179
Logging an Exception
void __fastcall ErrorHandler::LogEntry( TObject* const sender, Exception* exception, Severity severity, TForm* const form, const bool doDisplay ) /****************************************************************************\ * ACCESS: Public * \****************************************************************************/ { if( doDisplay ) Show( exception ) ; if( severity == EXCEPTION_FATAL ) WriteToLog( “***FATAL EXCEPTION - Attempting to log system’s “ “state***” ) ; //***Make entry*** if( form ) form->Cursor = crHourGlass ; try { WriteToLog( “ “ if( severity != WriteToLog( else WriteToLog(
) ; MESSAGE_ONLY ) ERROR_TITLE ) ; MESSAGE_TITLE ) ;
if( severity == EXCEPTION_FATAL ) Application->Terminate() ; }//LogEntry() //---------------------------------------------------------------------------
4 ADVANCED PROGRAMMING WITH C++BUILDER
WriteToLog( “Handler”, ”VCL Exception” ) ; LogExceptionState( exception ) ; LogObjectState( sender ) ; LogFormState( form ) ; LogProgramState( severity ) ; LogWindowsState() ; }//try __finally { // Always reset the cursor if( form ) form->Cursor = crDefault ; }//finally
06 9721 CH04
11/13/00
180
9:48 AM
Page 180
C++Builder 5 Essentials PART I
There is an overloaded version of LogEntry() that may be used to add an entry to the log without throwing an exception. It may be an error detected using another mechanism (a return code, for example). Alternatively, it may not be an error at all, but simply a special situation that needs to be tracked. This overloaded version is in Listing 4.14. LISTING 4.14
Logging a Message
void __fastcall ErrorHandler::LogEntry( String message, Severity severity, TForm* form, const bool doDisplay ) /****************************************************************************\ * ACCESS: Public * \****************************************************************************/ { if( doDisplay ) Show( message ) ; //***Make entry*** if( form ) form->Cursor = crHourGlass ; try { WriteToLog( “ “ if( severity != WriteToLog( else WriteToLog(
) ; MESSAGE_ONLY ) ERROR_TITLE ) ; MESSAGE_TITLE ) ;
WriteToLog( message ) ; LogFormState( form ) ; LogProgramState( severity ) ; LogWindowsState() ; }//try __finally { // Always reset the cursor if possible if( form ) form->Cursor = crDefault ; }//finally }//LogEntry() //---------------------------------------------------------------------------
Listing 4.15 shows how to extract all the information contained in the exception object. The process is straightforward; just cast to the most-derived type of interest and get any special
06 9721 CH04
11/13/00
9:48 AM
Page 181
Advanced Programming with C++Builder CHAPTER 4
181
information it contains. Notice that an exception of type EDBEngineError can contain nested messages. LISTING 4.15
Extracting Exception Information
void __fastcall ErrorHandler::LogExceptionState( Exception* exception ) /****************************************************************************\ * ACCESS: Private * \****************************************************************************/ { if( ! exception ) return ; WriteToLog( “ERROR”, exception->Message ) ; WriteToLog( “Exception type”, exception->ClassName() ) ; // EDBEngineError EOleException* OleException ; EOleSysError* OleSysError ; EDBEngineError* BDEException = dynamic_cast<EDBEngineError*> (exception) ; if( BDEException ) { TDBError* error_ptr ; AnsiString errorCount_String( BDEException->ErrorCount ) ; AnsiString subscript_String, category_String, errorCode_String, errorSubCode_String, nativeErrorCode_String ; WriteToLog( DETAIL_TITLE ) ;
if( error_ptr ) { subscript_String = subscript + 1 ; category_String = error_ptr->Category ; errorCode_String = error_ptr->ErrorCode ; errorSubCode_String = error_ptr->SubCode ; nativeErrorCode_String = (int)error_ptr->NativeError ;
4 ADVANCED PROGRAMMING WITH C++BUILDER
// Get nested messages for( int subscript = 0; subscript < BDEException->ErrorCount; subscript++ ) { error_ptr = BDEException->Errors[subscript] ;
06 9721 CH04
11/13/00
182
9:48 AM
Page 182
C++Builder 5 Essentials PART I
LISTING 4.15
Continued
WriteToLog( “DBE Error “, subscript_String + “ of “ + errorCount_String ) ; WriteToLog( “-> Message”, error_ptr->Message ) ; WriteToLog( “-> Category”, category_String ) ; WriteToLog( “-> Error Code”, errorCode_String ) ; WriteToLog( “-> Sub Code”, errorSubCode_String ) ; if( error_ptr->NativeError ) WriteToLog( “-> Native Code”, nativeErrorCode_String ) ; }//if else WriteToLog( “DBE Error”, “* Unavailable *” ) ; }//for }//if EDBEngineError // EOleException else if( (OleException = dynamic_cast<EOleException*>(exception)) != 0 ) { WriteToLog( DETAIL_TITLE ) ; WriteToLog( “OLE Error Code”, OleException->ErrorCode ) ; WriteToLog( “OLE Source App.”, OleException->Source ) ; WriteToLog( “Refer To File”, OleException->HelpFile ) ; }//if EOleException // EOleSysError else if( (OleSysError = dynamic_cast<EOleSysError*>(exception)) != 0 ) { WriteToLog( DETAIL_TITLE ) ; WriteToLog( “OLE API Error”, OleSysError->ErrorCode ) ; }//if EOleSysError }//LogExceptionState() //---------------------------------------------------------------------------
Listing 4.16 shows how to extract information about the state of the Windows operating system. A lot of details are included to make sure an important clue will not be left out when it is needed. There is a mixture of Windows API calls and VCL calls, but all are straightforward. LISTING 4.16
Extracting Operating System Information
void __fastcall ErrorHandler::LogWindowsState( void ) /****************************************************************************\ * ACCESS: Private * \****************************************************************************/
06 9721 CH04
11/13/00
9:48 AM
Page 183
Advanced Programming with C++Builder CHAPTER 4
LISTING 4.16
183
Continued
{ // Title WriteToLog( SYSTEM_TITLE ) ; // Last MS-Windows error code & message for this thread AnsiString currentThreadID( (int)GetCurrentThreadId() ) ; WriteToLog( “Thread ID”, currentThreadID ) ; int lastWin32Error_int = (int)GetLastError() ; AnsiString lastWin32Error_String( lastWin32Error_int ) ; if( lastWin32Error_int ) { char* buffer ;//LPVOID buffer ; if( FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, lastWin32Error_int, MAKELANGID (LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) & buffer, 0, NULL ) ) { lastWin32Error_String += “ - “ ; lastWin32Error_String += buffer ; }//if message LocalFree( buffer ) ; SetLastError( 0 ) ; // Reset Win32 thread’s error code }//if Win32 error WriteToLog( “Win32 Error”, lastWin32Error_String ) ; // Current Time WriteToLog( “Date & Time”, Now() ) ;
// Windows User Name bufferSize = sizeof( buffer1 ) ; success = GetUserName( buffer1, &bufferSize ) ; buffer = success ? buffer1 : “N/A” ; WriteToLog( “Windows User”, buffer ) ; // Current path String path( ExpandFileName( “.” ) ) ; WriteToLog( “Current Path”, path ) ;
4 ADVANCED PROGRAMMING WITH C++BUILDER
// Computer Name char buffer1[ MAX_COMPUTERNAME_LENGTH + 1 ] ; DWORD bufferSize = sizeof( buffer1 ) ; BOOL success = GetComputerName( buffer1, &bufferSize ) ; String buffer = success ? buffer1 : “N/A” ; WriteToLog( “Computer Name”, buffer ) ;
06 9721 CH04
11/13/00
184
9:48 AM
Page 184
C++Builder 5 Essentials PART I
LISTING 4.16
Continued
// System Info SYSTEM_INFO systemInfo ; GetSystemInfo( &systemInfo ) ; switch( systemInfo.dwProcessorType ) { case PROCESSOR_INTEL_386: WriteToLog( “Processor”, “Intel 386” ) ; break ; case PROCESSOR_INTEL_486: WriteToLog( “Processor”, “Intel 486” ) ; break ; case PROCESSOR_INTEL_PENTIUM: WriteToLog( “Processor”, “Intel Pentium” ) ; break ; case PROCESSOR_MIPS_R4000: WriteToLog( “Processor”, “MIPS” ) ; break ; case PROCESSOR_ALPHA_21064: WriteToLog( “Processor”, “Alpha” ) ; break ; default: WriteToLog( “Processor”, “Unknown” ) ; break ; }//switch char addressBuffer[35] ; // ultoa() returns up to 33 bytes AnsiString addressString( ultoa( (unsigned long)systemInfo. lpMinimumApplicationAddress, addressBuffer, 16 ) ) ; WriteToLog( “Min Address”, addressString ) ; addressString = ultoa( (unsigned long)systemInfo. lpMaximumApplicationAddress, addressBuffer, 16 ) ; WriteToLog( “Max Address”, addressString ) ; // Memory status MEMORYSTATUS memoryStatus ; const unsigned long OneK = 1024 ; const unsigned long OneMeg = OneK * OneK ; char valueBuffer[20] ; // itoa() returns up to 17 bytes memoryStatus.dwLength = sizeof( memoryStatus ) ; GlobalMemoryStatus( & memoryStatus ) ;
06 9721 CH04
11/13/00
9:48 AM
Page 185
Advanced Programming with C++Builder CHAPTER 4
LISTING 4.16
185
Continued
AnsiString valueString( itoa( (int)memoryStatus.dwMemoryLoad, valueBuffer, 10 ) ) ; WriteToLog( “Mem Load”, valueString + “%” ) ; valueString = itoa( (int)(memoryStatus.dwTotalPhys / OneK), valueBuffer, 10 ) ; WriteToLog( “Total Phys Mem”, valueString + “ KB” ) ; valueString = itoa( (int)(memoryStatus.dwAvailPhys / OneK), valueBuffer, 10 ) ; WriteToLog( “Avail Phys Mem”, valueString + “ KB” ) ; valueString = itoa( (int)(memoryStatus.dwTotalVirtual / OneMeg), valueBuffer, 10 ) ; WriteToLog( “Total Virt Mem”, valueString + “ MB” ) ; valueString = itoa( (int)(memoryStatus.dwAvailVirtual / OneMeg), valueBuffer, 10 ) ; WriteToLog( “Avail Virt Mem”, valueString + “ MB” ) ; valueString = itoa( (int)(memoryStatus.dwTotalPageFile / OneMeg), valueBuffer, 10 ) ; WriteToLog( “Total Page Mem”, valueString + “ MB” ) ; valueString = itoa( (int)(memoryStatus.dwAvailPageFile / OneMeg), valueBuffer, 10 ) ; WriteToLog( “Avail Page Mem”, valueString + “ MB” ) ; }//LogWindowsState() //---------------------------------------------------------------------------
Getting information about the state of the program is shown in Listing 4.17. Only basic information can be included because this is a generic method. Typically, the value of all global variables should be included in the log. Please refer to the earlier discussion in the section “Adding Project-Specific Information to the Class.” Extracting Program-Specific Information
void __fastcall ErrorHandler::LogProgramState( Severity severity ) /****************************************************************************\ * ACCESS: Protected * \****************************************************************************/ { WriteToLog( PROGRAM_TITLE ) ; // Severity switch( severity ) { case MESSAGE_ONLY: WriteToLog( “Severity”, “System message only” ) ; break ;
ADVANCED PROGRAMMING WITH C++BUILDER
LISTING 4.17
4
06 9721 CH04
11/13/00
186
9:48 AM
Page 186
C++Builder 5 Essentials PART I
LISTING 4.17
Continued
case EXCEPTION_HANDLED: WriteToLog( “Severity”, “Exception handled” ) ; break ; case EXCEPTION_UNHANDLED: WriteToLog( “Severity”, ”Unhandled exception” ) ; break ; case EXCEPTION_FATAL: WriteToLog( “Severity”, “* FATAL *” ) ; break ; default: WriteToLog( “Severity”, “* Unknown *” ) ; break ; }//switch // Get version info from EXE bool rc ; DWORD handle ; DWORD size = GetFileVersionInfoSize( Application->ExeName.c_str(), &handle ) ; if( size ) { void* buffer = new char[size] ; try { rc = GetFileVersionInfo( Application->ExeName.c_str(), handle, size, buffer ) ; if( rc ) { char* valuePointer ; unsigned int valueSize ; rc = VerQueryValue( buffer, TEXT(“\\StringFileInfo\\040904E4\\” “FileVersion”), (void**)&valuePointer, &valueSize ) ; if( rc ) { AnsiString value( valuePointer, valueSize ) ; WriteToLog( “Version”, value ); }//if VerQueryValue() }//if GetFileVersionInfo() }//try __finally {
06 9721 CH04
11/13/00
9:48 AM
Page 187
Advanced Programming with C++Builder CHAPTER 4
LISTING 4.17
187
Continued
delete buffer ; }//finally }//if GetFileVersionInfoSize() // Compiler used switch( __BORLANDC__ ) { case 0x520: WriteToLog( “Compiler”, “C++Builder 1” ) ; break ; case 0x530: WriteToLog( “Compiler”, “C++Builder 3” ) ; break ; case 0x540: WriteToLog( “Compiler”, “C++Builder 4” ) ; break ; case 0x550: WriteToLog( “Compiler”, “C++Builder 5” ) ; break ;
Providing information about the object that threw the exception is vital because it will tell what the user was doing when the exception was thrown. The LogObjectState() method in Listing 4.18 shows how it can be done. The first part gets the most important properties of common VCL base classes. The second part acquires detailed information about the object if it is data aware (including the value of all the fields of the attached dataset). This is accomplished by searching for a DataSource property using the same technique that the IDE’s Object Inspector uses. This is a very useful technique because it does not require casting, which means the type of the object does not have to be known. It is possible that this object will not be the actual culprit, but another object related to it. For instance, if the exception was thrown while manipulating another object in the OnClick event handler of a button, DefaultExceptionHandler() will supply a pointer to the button control. Keep this in mind when reading a log file.
4 ADVANCED PROGRAMMING WITH C++BUILDER
default: char compiler[50] ; sprintf( compiler, “Borland C++ %x”, __BORLANDC__ ) ; WriteToLog( “Compiler”, compiler ) ; }//switch }//LogProgramState() //---------------------------------------------------------------------------
06 9721 CH04
11/13/00
188
9:48 AM
Page 188
C++Builder 5 Essentials PART I
To customize this method, please refer to the earlier discussion in the section “Adding ProjectSpecific Information to the Class.” Note that function typeid() will return only the RTTI information of C++Builder objects. For Delphi objects, only the static type is returned, which is usually TObject. LISTING 4.18
Extracting Detailed Information About the Sender
void __fastcall ErrorHandler::LogObjectState( TObject* sender ) /****************************************************************************\ * ACCESS: Protected * \****************************************************************************/ { if( ! sender ) return ; TDataSet* dataSetReferenced = NULL ; WriteToLog( OBJECT_TITLE ) ; WriteToLog( “Calling Obj”, sender->ClassName() ) ; WriteToLog( “-> BCB Class”, typeid(sender).name() ) ; char addressBuffer[35] ; // ultoa() returns up to 33 bytes AnsiString addressString ; addressString = ultoa( (unsigned long)sender, addressBuffer, 16 ) ; WriteToLog( “-> Address”, addressString ) ; // Is it also TComponent? TComponent* component = dynamic_cast( sender ) ; if( component ) { WriteToLog( “Component”, “(“ + component->ClassName() + “)” ) ; WriteToLog( “-> Class”, typeid(component).name() ) ; TControl* owner = dynamic_cast( component->Owner ) ; if( owner ) WriteToLog( “-> Owner”, owner->Name ) ; // Is it also TControl? control = dynamic_cast( sender ) ; if( control ) { WriteToLog( “-> Name”, control->Name ) ; // Is this a data-aware control? // If so, get the dataset referred to.
06 9721 CH04
11/13/00
9:48 AM
Page 189
Advanced Programming with C++Builder CHAPTER 4
LISTING 4.18
189
Continued PPropInfo propInfo = GetPropInfo( PTypeInfo( control->ClassInfo() ), “DataSource” ) ; if( propInfo ) { TDataSource* dataSourceProperty = (TDataSource*)GetOrdProp( control, propInfo ) ; if( dataSourceProperty ) dataSetReferenced = dataSourceProperty->DataSet ; }//if data-aware control // Is it also TWinControl? TWinControl* winControl = dynamic_cast( sender ) ; if( winControl ) { String showing = winControl->Showing ? “True” : “False” ; WriteToLog( “-> Showing”, showing ) ;
// Is it also TPageControl? TPageControl* pageControl = dynamic_cast( sender ) ; if( pageControl ) { WriteToLog( “-> Tab”, pageControl->ActivePage->Caption ) ; }//if TPageControl }//if TWinControl }//if TControl }//if TComponent
WriteToLog( “DataSet”, dataSet->Name ) ; WriteToLog( “-> Owner”, dataSet->Owner->Name ) ; WriteToLog( “-> Active”, dataSet->Active ? “True” : “False” ) ; String state; switch( dataSet->State ) {
ADVANCED PROGRAMMING WITH C++BUILDER
// Get [referenced] dataset TDataSet* dataSet = dataSetReferenced ? dataSetReferenced : dynamic_cast( sender ) ; if( dataSet ) { WriteToLog( DATA_TITLE ) ;
4
06 9721 CH04
11/13/00
190
9:48 AM
Page 190
C++Builder 5 Essentials PART I
LISTING 4.18
Continued case dsInactive: state = “Inactive” ; break ; case dsBrowse: state = “Browsing” ; break ; case dsEdit: state = “Editing” ; break ; case dsInsert: state = “Inserting” ; break ; case dsCalcFields: state = “Updating calc. fields” ; break ; case dsFilter: state = “Filtering” ; break ;
default: state = “Internal processing” ; break ; }//switch WriteToLog( “-> State”, state ) ; // Show data fields TFields* fields = dataSet->Fields ; int count = fields->Count ; WriteToLog( String(“-> FIELDS: (“) + String(count) + “)” ) ; for( int ctr=0; fields && (ctr < count); ++ctr ) { WriteToLog( fields->Fields[ctr]->FieldName, fields->Fields[ctr]->AsString ) ; }//for fields }//if dataSet }//LogObjectState() //---------------------------------------------------------------------------
06 9721 CH04
11/13/00
9:48 AM
Page 191
Advanced Programming with C++Builder CHAPTER 4
191
In large projects, the information gathered by LogObjectState() can be too specific to determine where in the program to start looking for the error. LogFormState(), shown in Listing 4.19, includes very basic but extremely useful information that provides context to the error. LISTING 4.19
Providing Context to the Error
void __fastcall ErrorHandler::LogFormState( TForm* form ) /****************************************************************************\ * ACCESS: Protected * \****************************************************************************/ { if( ! form ) return ; WriteToLog( FORM_TITLE ) ; WriteToLog( “Form”, form->Name ) ; if( form->ActiveControl ) WriteToLog( “-> Active Cntrl”, form->ActiveControl->Name ) ; if( form->Owner && (form->Owner != Application) ) WriteToLog( “-> Owner”, form->Owner->Name ) ; else WriteToLog( “-> Owner”, ”Application” ) ; if( form->Parent ) WriteToLog( “-> Parent”, form->Parent->Name ) ; }//LogFormState() //---------------------------------------------------------------------------
Listing 4.20 contains two overloaded versions of the Show() method, one for each version of the LogEntry() method. One displays exception messages, and the other is used for regular string messages. These methods may easily be overridden to provide better integration with the application. For instance, it may be desirable to use a different window or a status bar.
ADVANCED PROGRAMMING WITH C++BUILDER
The class now acquired a lot of information for the log file. But what about the user? He must be informed of the problem, but in a manner that will not be overwhelming. A simple modal window is used to display a brief message.
4
06 9721 CH04
11/13/00
192
9:48 AM
Page 192
C++Builder 5 Essentials PART I
LISTING 4.20
Providing Feedback to the User
void __fastcall ErrorHandler::Show( Exception* exception ) /****************************************************************************\ * ACCESS: Protected * \****************************************************************************/ { MessageBeep( MB_ICONEXCLAMATION ) ; Application->ShowException( exception ) ; }//Show() //--------------------------------------------------------------------------void __fastcall ErrorHandler::Show( String message ) /****************************************************************************\ * ACCESS: Protected * \****************************************************************************/ { MessageBeep( MB_ICONEXCLAMATION ) ; MessageBox( NULL, message.c_str(), “Attention”, MB_OK + MB_ICONERROR ) ; }//Show() //---------------------------------------------------------------------------
All the Get...State() methods use WriteToLog() to add to the log file. There are two overloaded versions. The first blindly logs what it is passed. The second justifies a label and its value before logging them. As you can see in Listing 4.21, there is not much to it. Because the stream used only allows appending, we don’t even have to do any positioning. LISTING 4.21
Appending to the Log File
void __fastcall ErrorHandler::WriteToLog( String text ) /****************************************************************************\ * ACCESS: Protected * \****************************************************************************/ { *log << text.c_str() << endl ; }//WriteToLog() //--------------------------------------------------------------------------void __fastcall ErrorHandler::WriteToLog( String name, String value ) /****************************************************************************\ * ACCESS: Protected * \****************************************************************************/
06 9721 CH04
11/13/00
9:48 AM
Page 193
Advanced Programming with C++Builder CHAPTER 4
LISTING 4.21
193
Continued
{ String text; const int MAX_NAME = 15 ; name.Trim() ; if( name.Length() > MAX_NAME) text = AnsiString::StringOfChar( ‘ ‘, M
AX_NAME – name.Length() ) ;
else text = SetLength( MAX_NAME ) ; // Applied to local copy only. text += name ; text += “: “ ; text += value.Trim() ; WriteToLog( text ) ; }//WriteToLog() //---------------------------------------------------------------------------
To see our ErrorHandler class in action, start a new MDI application (go to File, New and select MDI Application from the Projects tab). Choose a directory for your new project. Then, go to Project, Add to Project and locate the EHandler.cpp file found on the companion CD. Your application is now ready to log error messages.
Advanced Exception Handler Summary
The handler presented here is not adequate for all types of applications. Nevertheless, many of the techniques used are certainly transferable. An exception handler is a constant work in progress. As you read this, it is likely that the source code given here will have evolved to meet new situations and incorporate suggestions from fellow developers.
Creating Multithreaded Applications For Scrabble players, “multitasking” and “multithreading” might be a great opportunity to earn points. For developers, these terms are often sources of confusion and unnecessary headaches. I should emphasize unnecessary here because, once explained, they become part of the obvious programming concepts.
4 ADVANCED PROGRAMMING WITH C++BUILDER
While the compiler’s default exception handler may be adequate for simple and small programs, it is much too crude for most projects. The capability to gather information about the system’s state at a given time and store it for later review is paramount in successfully supporting a product.
06 9721 CH04
11/13/00
194
9:48 AM
Page 194
C++Builder 5 Essentials PART I
Understanding Multitasking To put it simple, multitasking is the capability of the operating system to run multiple programs at the same time. Unconsciously you’ve been using this capability while switching from your Microsoft Word document to Windows Explorer. Although multitasking may seem characteristic of graphical operating systems such as Windows or Linux, earlier computers also used multitasking to some extent. For example, UNIX allows you to run multiple programs in the background. Under Windows 3.x, applications used cooperative multitasking. Cooperative means that a program has control over the CPU and, before switching to another application, this program must finish processing data. This type of multitasking has a serious drawback: If an application stops responding, the entire operating system will hang. 32-bit versions of Windows solved this problem by introducing preemptive multitasking. A simple dictionary definition will help you understand its meaning: “preemptive: done before somebody else has had an opportunity to act.” In other words, to allow task-switching, 32-bit versions of Windows suspend the current application, whether it’s ready to lose control or not.
NOTE Cooperative multitasking is also called “nonpreemptive multitasking” for obvious reasons. Unlike preemptive multitasking, the operating system is unable to suspend an application that has stopped responding.
Understanding Multithreading Multithreading is the capability of a program to run multiple tasks (threads) at the same time. Most Windows applications use only one thread, the primary thread. A primary thread takes care of child windows creation and message processing. All secondary threads are used to perform background operations: loading large files, looking for information, performing mathematical calculations. In the following pages, we’ll cover the different aspects of creating multithreaded applications.
CAUTION Throughout their learning process, young children tend to repeat words they have overheard here and there, simply to prove their knowledge or to resemble “big people.” A similar situation occurs with programmers. Some developers tend to overuse programming techniques they’ve learned.
06 9721 CH04
11/13/00
9:48 AM
Page 195
Advanced Programming with C++Builder CHAPTER 4
195
Do not use separate threads in your application unless you’re dealing with lengthy background operations. Sometimes, with small code readjustments, you can simply avoid the use of threads. Why complicate your work? That said, multithreaded applications offer multiple advantages, as we’ll see later in this chapter.
Creating a Thread Using API Calls You can create a new thread from another one by calling the CreateThread() API function. The CreateThread() parameters specify, among others, the security attributes, the creation flags, and the thread function: HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );
The SECURITY_ATTRIBUTES structure determines whether other processes can modify the object and whether a child process can inherit a handle to this object. If lpThreadAttributes is NULL, the thread gets the default security descriptor. The dwCreationFlags parameter specifies the thread creation flags. If its value is CREATE_ SUSPENDED, the thread will not run until you call the ResumeThread() function. Set this value to 0 to run the thread immediately after creation.
The most crucial parameter is the starting function, also known as thread function. is the address of the function that accepts one parameter and returns a DWORD exit code: lpStartAddress
DWORD WINAPI ThreadFunc(LPVOID);
4 ADVANCED PROGRAMMING WITH C++BUILDER
The lpThreadId parameter points to an empty DWORD that will receive the thread identifier. Under Windows NT/2000, if this parameter is NULL, the thread identifier is simply not returned. Windows 9x requires a DWORD variable. To ensure complete compatibility with the current operating system, do not use the NULL value.
06 9721 CH04
11/13/00
196
9:48 AM
Page 196
C++Builder 5 Essentials PART I
TIP In a sense, the starting function can be compared to the main() or WinMain() function in a C++ program. ThreadFunc() is the main entry point for your thread.
Finally, dwStackSize and lpParameter specify the size of the stack (in bytes) and the parameter passed to the thread, respectively.
TIP CreateThread(), as many other API calls, contains a large list of arguments more or
less complex. In the beginning, understanding all aspects of this function can be disorienting. A simple trick to overcome this problem is to first look at the arguments that may be zeroed. For example, in almost all parameters of CreateThread() except for lpStartAddress and lpThreadId, you can safely specify 0. Once you fully understand these two arguments, you can always go back and further explore the CreateThread() function.
With the previous explanations and a little help from the Win32 Programmer’s Reference help file, we should now be able to write a simple multithreaded application. Our example project should contain two buttons: Start and Stop. When the user clicks on the Start button, he resumes the newly created thread. This thread should draw random ellipses and rectangles on the form. By clicking on the Stop button, the user should be able to suspend the thread. Take a look at Listing 4.22. Don’t forget that you can find the complete source code in the ThreadAPI folder of the companion CD. LISTING 4.22
ThreadFormUnit.cpp
#include #pragma hdrstop #include “ThreadFormUnit.h” #pragma package(smart_init) #pragma resource “*.dfm” TThreadForm *ThreadForm; HANDLE Thread;
06 9721 CH04
11/13/00
9:48 AM
Page 197
Advanced Programming with C++Builder CHAPTER 4
LISTING 4.22
197
Continued
DWORD WINAPI ThreadFunc(LPVOID Param) { HANDLE MainWnd(Param); RECT R; GetClientRect(MainWnd, &R); const MaxWidth = R.right - R.left; const MaxHeight = R.bottom - R.top; int X1, Y1, X2, Y2, R1, G1, B1; bool IsEllipse; while(true) { HDC DC = GetDC(MainWnd); X1 Y1 X2 Y2
= = = =
rand() rand() rand() rand()
% % % %
MaxWidth; MaxHeight; MaxWidth; MaxHeight;
R1 = rand() & 255; G1 = rand() & 255; B1 = rand() & 255; IsEllipse = rand() & 1;
if(IsEllipse) Ellipse(DC, X1, Y1, X2, Y2); else Rectangle(DC, X1, Y1, X2, Y2); ReleaseDC(MainWnd, DC); DeleteObject(Brush); } } __fastcall TThreadForm::TThreadForm(TComponent* Owner) : TForm(Owner) { randomize();
4 ADVANCED PROGRAMMING WITH C++BUILDER
HBRUSH Brush = CreateSolidBrush( RGB(R1, G1, B1)); SelectObject(DC, Brush);
06 9721 CH04
11/13/00
198
9:48 AM
Page 198
C++Builder 5 Essentials PART I
LISTING 4.22
Continued
DWORD Id; Thread = CreateThread(0, 0, ThreadFunc, ThreadForm->Handle, CREATE_SUSPENDED, &Id); if(!Thread) { ShowMessage(“Error! Cannot create thread.”); Application->Terminate(); } } void __fastcall TThreadForm::StartClick(TObject *) { ResumeThread(Thread); Start->Enabled = false; Stop->Enabled = true; } void __fastcall TThreadForm::StopClick(TObject *) { SuspendThread(Thread); Stop->Enabled = false; Start->Enabled = true; }
NOTE As you can see in Listing 4.22, the code uses API functions almost exclusively. One of the reasons is that you must avoid accessing VCL properties and methods from secondary threads. I will describe why and provide a solution in the next section.
The code listing is long, but it is easy to understand. In the form constructor, we create a suspended thread using the CreateThread() function and check whether the new thread is valid or not. The Start and Stop buttons use the ResumeThread() and SuspendThread() API functions to modify the thread state. Finally, the thread function draws random shapes on the form’s canvas. (Notice how the window handle is passed to ThreadFunc().) Figure 4.2 shows the project.
06 9721 CH04
11/13/00
9:48 AM
Page 199
Advanced Programming with C++Builder CHAPTER 4
199
FIGURE 4.2 The ThreadAPI project.
NOTE Another efficient way to start a new thread is the _beginthread() routine defined in process.h (you can find process.h in the C++Builder Include folder): unsigned long _beginthread(void (_USERENTRY *__start)(void *), unsigned __stksize, void *__arg);
Because it requires fewer parameters, this function is commonly used in multithreaded applications.
Understanding the TThread Object
LISTING 4.23
TThread Class
class DELPHICLASS TThread; class PASCALIMPLEMENTATION TThread : public System::TObject { typedef System::TObject inherited; private: unsigned FHandle; unsigned FThreadID;
4 ADVANCED PROGRAMMING WITH C++BUILDER
C++Builder encapsulates Windows thread objects into the TThread object. Creating a new thread is basically a matter of creating a new instance of a TThread descendant. Listing 4.23 contains the definition of the TThread abstract class.
06 9721 CH04
11/13/00
200
9:48 AM
Page 200
C++Builder 5 Essentials PART I
LISTING 4.23
Continued
bool FTerminated; bool FSuspended; bool FFreeOnTerminate; bool FFinished; int FReturnValue; TNotifyEvent FOnTerminate; TThreadMethod FMethod; System::TObject* FSynchronizeException; void __fastcall CallOnTerminate(void); TThreadPriority __fastcall GetPriority(void); void __fastcall SetPriority(TThreadPriority Value); void __fastcall SetSuspended(bool Value); protected: virtual void __fastcall DoTerminate(void); virtual void __fastcall Execute(void) = 0 ; void __fastcall Synchronize(TThreadMethod Method); __property int ReturnValue = {read=FReturnValue, write=FReturnValue, nodefault}; __property bool Terminated = {read=FTerminated, nodefault}; public: __fastcall TThread(bool CreateSuspended); __fastcall virtual ~TThread(void); void __fastcall Resume(void); void __fastcall Suspend(void); void __fastcall Terminate(void); unsigned __fastcall WaitFor(void); __property bool FreeOnTerminate = {read=FFreeOnTerminate, write= FFreeOnTerminate, nodefault}; __property unsigned Handle = {read=FHandle, nodefault}; __property TThreadPriority Priority = {read=GetPriority, write= SetPriority, nodefault}; __property bool Suspended = {read=FSuspended, write=SetSuspended, nodefault}; __property unsigned ThreadID = {read=FThreadID, nodefault}; __property TNotifyEvent OnTerminate = {read=FOnTerminate, write= FOnTerminate}; };
If you’re wondering how to create a TThread descendant, the answer is simple. Open the File, New dialog and select Thread Object from the Object Repository. C++Builder will prompt you for the class name of the new descendant. Enter TRandomThread and click on OK.
06 9721 CH04
11/13/00
9:48 AM
Page 201
Advanced Programming with C++Builder CHAPTER 4
201
C++Builder will create automatically a new source file containing the TRandomThread object: #include #pragma hdrstop #include “Unit2.h” #pragma package(smart_init) __fastcall TRandomThread::TRandomThread(bool CreateSuspended) : TThread(CreateSuspended) { } void __fastcall TRandomThread::Execute() { }
The Execute() method contains the code that will be executed when the thread runs. In other words, Execute() replaces our thread function. Also notice that the constructor of your object contains a CreateSuspended parameter. Just like the CREATE_SUSPENDED flag, when CreateSuspended is true, you must first call the Resume() method; otherwise, Execute() won’t be called. In Tables 4.1 and 4.2, I’ve summarized the most common properties and methods of the TThread class. TABLE 4.1
TThread Properties
Description
FreeOnTerminate
Determines whether the thread object is automatically destroyed when the thread terminates. Provides access to the thread’s handle. Use this value when calling API functions. Specifies the thread’s scheduling priority. Set this priority to a higher or lower value when needed. Determines the value returned to other threads when the current thread object finishes. Specifies whether the thread is suspended or not. Determines whether the thread is about to be terminated. Determinesthe thread’s identifier.
Handle Priority ReturnValue Suspended Terminated ThreadID
4 ADVANCED PROGRAMMING WITH C++BUILDER
Property
06 9721 CH04
11/13/00
202
9:48 AM
Page 202
C++Builder 5 Essentials PART I
TABLE 4.2
TThread Methods
Method
Description
DoTerminate()
Calls the OnTerminate event handler without terminating the thread. Contains the code to be executed when the thread runs. Resumes a suspended thread. Pauses a running thread. Executes a call within the VCL primary thread.
Execute() Resume() Suspend() Synchronize() Terminate() WaitFor()
Signals the thread to terminate. Waits for a thread to terminate.
As an example project, I would like to go back to the random shapes program (refer to Listing 4.22). This time, we’ll try to use VCL objects exclusively. You’ve already created a TRandomThread object, so we’ll use this object as the secondary thread of our application. The first step is to add the main unit’s include file to the new thread unit. Select File, Include Unit Hdr and then select ThreadFormUnit. There’s not much to put in the TRandomThread constructor, except for the random numbers generator: __fastcall TRandomThread::TRandomThread(bool CreateSuspended) : TThread(CreateSuspended) { randomize(); }
Now let’s take care of the core part of our thread: the Execute() method. We no longer need to determine the form size using the GetClientRect() API function. We could simply read the ClientWidth and ClientHeight properties: const MaxWidth = ThreadForm->ClientWidth; const MaxHeight = ThreadForm->ClientHeight; int X1, Y1, X2, Y2, R1, G1, B1; bool IsEllipse;
The TCanvas object, with which you’re probably familiar, could greatly simplify the drawing process. There is a small problem: The VCL does not allow multiple threads to access the same graphic object simultaneously. Therefore, you must use the Lock() and Unlock() methods to make sure that other threads do not access the TCanvas while you’re drawing:
06 9721 CH04
11/13/00
9:48 AM
Page 203
Advanced Programming with C++Builder CHAPTER 4
203
while(true) { ThreadForm->Canvas->Lock(); X1 Y1 X2 Y2
= = = =
rand() rand() rand() rand()
% % % %
MaxWidth; MaxHeight; MaxWidth; MaxHeight;
R1 = rand() & 255; G1 = rand() & 255; B1 = rand() & 255; IsEllipse = rand() & 1; ThreadForm->Canvas->Brush->Color = TColor(RGB(R1, G1, B1)); if(IsEllipse) ThreadForm->Canvas->Ellipse(X1, Y1, X2, Y2); else ThreadForm->Canvas->Rectangle(X1, Y1, X2, Y2); ThreadForm->Canvas->Unlock(); }
This puts an end to the thread object code. Let’s take a look now at the main unit. In the form constructor, we create a new instance of TRandomThread: TRandomThread* Thread;
ADVANCED PROGRAMMING WITH C++BUILDER
__fastcall TThreadForm::TThreadForm(TComponent* ) : TForm(Owner) { Thread = new TRandomThread(true); if(!Thread) { ShowMessage(“Error! Cannot create thread.”); Application->Terminate(); } }
4
06 9721 CH04
11/13/00
204
9:48 AM
Page 204
C++Builder 5 Essentials PART I
The Start button calls the Resume() method: void __fastcall TThreadForm::StartClick(TObject *) { Thread->Resume(); Start->Enabled = false; Stop->Enabled = true; }
The Stop button calls the Suspend() method: void __fastcall TThreadForm::StopClick(TObject *) { Thread->Suspend(); Stop->Enabled = false; Start->Enabled = true; }
TIP The C++Builder IDE provides a Threads debug window containing the list of available threads: their ID, state, location, and status. To display this window, choose View, Debug Windows, Threads from the C++Builder menu or press Ctrl+Alt+T.
The thread is automatically terminated when the Execute() function finishes executing or when the application is closed. To ensure that memory occupied by your thread object is freed on termination, always insert the following in the Execute() method: FreeOnTerminate = true;
Sometimes, however, you may need to terminate a thread by code. To do so, you could use the Terminate() method. Terminate() tells the thread to terminate by setting the Terminated property to true. It is important to understand that Terminate() does not exit the thread by itself. You must periodically check in the Execute() method whether Terminated is true. For example, to be able to terminate our TRandomThread object, add the following line: while(true) { if(Terminated) break;
has the advantage of allowing you to do the cleaning up by yourself, thus giving you more control over the thread termination. Unfortunately, if the thread stops responding, calling Terminate() will be useless. Terminate()
06 9721 CH04
11/13/00
9:48 AM
Page 205
Advanced Programming with C++Builder CHAPTER 4
205
The TerminateThread() API function is a more radical way to cause a thread to exit. TerminateThread() instantly closes the current thread without freeing memory occupied by the thread object. You should use this function only in extreme cases, when no other options are left. The TerminateThread() syntax is simple. Here is an example: TerminateThread((HANDLE)Thread->Handle, false);
Understanding the Main VCL Thread Properties and methods of VCL objects are not necessarily thread safe. This means that when accessing properties and methods, you may use memory that is not protected from other threads. Therefore, the main VCL thread should be the only thread to have control over the VCL.
NOTE The main VCL thread is the primary thread of your application. It handles and processes Windows messages received by VCL controls.
NOTE Graphic objects are exceptions to the thread-safe rule. As we’ve seen previously, by using the Lock() and Unlock() methods, other threads can be prevented from drawing on the canvas.
void __fastcall Synchronize(TThreadMethod &Method);
Consider the example of a thread displaying increasing values in a Label component. Obviously, we’ll use a for loop in the Execute() method. But how will we change the Label’s caption? By synchronizing with the VCL. Listing 4.24 contains the source code of the TLabelThread object, and Figure 4.3 shows the results.
4 ADVANCED PROGRAMMING WITH C++BUILDER
To allow threads to access VCL objects, TThread provides the Synchronize() method. Synchronize() performs actions contained in a routine as if they were executed from the main VCL thread:
06 9721 CH04
11/13/00
206
9:48 AM
Page 206
C++Builder 5 Essentials PART I
LISTING 4.24
TLabelThread Thread Object
#include #pragma hdrstop #include “ThreadFormUnit.h” #pragma package(smart_init) #include class TLabelThread : public TThread { private: protected: int Num; void __fastcall Execute(); void __fastcall DisplayLabel(); public: __fastcall TLabelThread(bool CreateSuspended); }; //----------------------------------------------__fastcall TLabelThread::TLabelThread(bool CreateSuspended) : TThread(CreateSuspended) { } void __fastcall TLabelThread::DisplayLabel() { ThreadForm->Label->Caption = Num; } void __fastcall TLabelThread::Execute() { FreeOnTerminate = true; for(Num = 0; Num <= 1000; Num++) { if(Terminated) break; Synchronize (DisplayLabel); } }
06 9721 CH04
11/13/00
9:48 AM
Page 207
Advanced Programming with C++Builder CHAPTER 4
207
TIP As opposed to the TRandomThread example, where we had an endless loop, in this project the thread is terminated when the value of 1000 is reached. By handling the OnTerminate event of TLabelThread, you can determine when the thread is about to exit: void __fastcall TThreadForm::bStartClick(TObject *) { Thread = new TLabelThread(false); Thread->OnTerminate = OnTerminate; bStart->Enabled = false; } void __fastcall TThreadForm::OnTerminate(TObject *) { bStart->Enabled = true; }
In fact, you can use OnTerminate as a replacement for the Synchronize() method. If your thread has actions to perform before exiting, OnTerminate will allow you to access VCL properties and methods from within the main unit. Consider the previous example where we enabled the bStart button in the OnTerminate event handler. To accomplish the same thing directly from the thread object, we would have written a far more complex code: // void __fastcall EnableButton();
void __fastcall TLabelThread::Execute() { // ... if(Terminated) { Synchronize(EnableButton); // ... }
4 ADVANCED PROGRAMMING WITH C++BUILDER
void __fastcall TLabelThread::EnableButton() { ThreadForm->bStart->Enabled = true; }
06 9721 CH04
11/13/00
208
9:48 AM
Page 208
C++Builder 5 Essentials PART I
FIGURE 4.3 The LabelThread project.
Establishing Priorities In an application using multiple threads, it is important to know which threads will have a higher priority and run first. Table 4.3 describes all possible priorities levels. TABLE 4.3
Thread Priorities
Priority Level
Description
THREAD_PRIORITY_TIME_CRITICAL
15 points above normal 2 points above normal 1 point above normal Normal 1 point below normal 2 points below normal 15 points below normal
THREAD_PRIORITY_HIGHEST THREAD_PRIORITY_ABOVE_NORMAL THREAD_PRIORITY_NORMAL THREAD_PRIORITY_BELOW_NORMAL THREAD_PRIORITY_LOWEST THREAD_PRIORITY_IDLE
All threads are created using the THREAD_PRIORITY_NORMAL. Once a thread has been created, you can adjust the priority level higher or lower using the SetThreadPriority() function. A general rule is that a thread dealing with the user interface should have a higher priority to make sure that the application remains responsive to the user’s actions. Background threads are usually set to THREAD_PRIORITY_BELOW_NORMAL or THREAD_PRIORITY_LOWEST so that they can be terminated when necessary.
NOTE Priority levels are commonly called relative scheduling priorities because they are relative to other threads in the same process.
06 9721 CH04
11/13/00
9:48 AM
Page 209
Advanced Programming with C++Builder CHAPTER 4
209
The TThread object provides a Priority property, which determines the thread priority level. Its possible values are tpTimeCritical tpHighest tpHigher tpNormal tpLower tpLowest tpIdle
As you can see, they closely match the priority levels we previously described. If you’re still not convinced of the importance of thread priorities, take a look at the following example. Start a new application and add two progress bars (Max property set to 5000) and a Start button. We will try to increment the position of the progress bars using threads of different priorities. Listing 4.25 contains the source code of the TPriorityThread thread object. LISTING 4.25
TPriorityThread Thread Object
#include #pragma hdrstop #include “PriorityThreadUnit.h” #include “ThreadFormUnit.h” #pragma package(smart_init)
void __fastcall TPriorityThread::DisplayProgress() { if(First) ThreadForm->ProgressBar1->Position++; else ThreadForm->ProgressBar2->Position++; } void __fastcall TPriorityThread::Execute() {
4 ADVANCED PROGRAMMING WITH C++BUILDER
__fastcall TPriorityThread::TPriorityThread(bool Temp) : TThread(false) { First = Temp; }
06 9721 CH04
11/13/00
210
9:48 AM
Page 210
C++Builder 5 Essentials PART I
LISTING 4.25
Continued
FreeOnTerminate = true; for(Num = 0; Num <= 5000; Num++) { if(Terminated) break; Synchronize (DisplayProgress); } }
Notice that I slightly modified the TPriorityThread constructor. The Temp boolean variable (which replaces CreateSuspended) will indicate which progress bar should be accessed. The main unit contains only the code for the Start button OnClick handler: void __fastcall TThreadForm::bStartClick(TObject *) { TPriorityThread *First; First = new TPriorityThread (true); First->Priority = tpLowest; TPriorityThread *Second; Second = new TPriorityThread(false); Second->Priority = tpLowest; bStart->Enabled = false; }
Run the program and click on the Start button. Both progress bars should reach the end at approximately the same time, as shown in Figure 4.4. Now set the priority of the first thread to tpLower. Any difference? See the result in Figure 4.5.
FIGURE 4.4 Threads with same priority.
06 9721 CH04
11/13/00
9:48 AM
Page 211
Advanced Programming with C++Builder CHAPTER 4
211
FIGURE 4.5 Threads with different priorities.
Timing Threads Sometimes when developing it is useful to time sections of code. The basic principle is to record the system time before and after the code and subtract the start time from the end time to calculate the elapsed time. For general applications this can be done with the Win32 API function GetTickCount(). This is illustrated in Listing 4.26. LISTING 4.26
Timing Code with GetTickCount()
int Start = GetTickCount(); // ... Form1->Canvas->Lock(); for(int x = 0; x <= 100000; x++) Form1->Canvas->TextOut(10, 10, x); Form1->Canvas->Unlock(); // ... int Total = GetTickCount() - Start; ShowMessage(FloatToStr(Total / 1000.0) + “ sec”);
4
Unfortunately, because of the preemptive behavior of Windows, threads are often interrupted. For this reason, you can’t rely on GetTickCount() to retrieve the thread execution time. However, Windows provides the GetThreadTimes() API function, which helps you time your threads: BOOL GetThreadTimes( HANDLE hThread, LPFILETIME lpCreationTime, LPFILETIME lpExitTime, LPFILETIME lpKernelTime, LPFILETIME lpUserTime );
ADVANCED PROGRAMMING WITH C++BUILDER
A similar example is also shown in the “Manual Code Timing” section of Chapter 6, “Compiling and Optimizing Your Application,” using clock() instead of GetTickCount(). There are also other functions that can be used to time code.
06 9721 CH04
11/13/00
212
9:48 AM
Page 212
C++Builder 5 Essentials PART I
CAUTION GetThreadTimes() is available only under Windows NT/2000.
As you can see, GetThreadTimes() uses the FILETIME structure. Before performing arithmetic operations, you must first store the user time information in a LARGE_INTEGER. Then, by subtraction of the 64-bit QuadPart members of the LARGE_INTEGER structure, you could obtain the number of 100 nanoseconds that your code takes to execute. Listing 4.27 illustrates this. LISTING 4.27
GetThreadTimes() Example
FILETIME CreationTime, ExitTime, KernelTime; union { LARGE_INTEGER iUT; FILETIME fUT; } UserTimeS, UserTimeE; GetThreadTimes((HANDLE)Handle, &CreationTime, &ExitTime, &KernelTime, &UserTimeS.fUT); // ... Form1->Canvas->Lock(); for(int x = 0; x <= 100000; x++) Form1->Canvas->TextOut(10, 10, x); Form1->Canvas->Unlock(); // ... GetThreadTimes((HANDLE)Handle, &CreationTime, &ExitTime, &KernelTime, &UserTimeE.fUT); float Total = UserTimeE.iUT.QuadPart - UserTimeS. iUT.QuadPart; Total /= 10 * 1000 * 1000; // Converts to seconds OutputDebugString(FloatToStr(Total).c_str());
TIP OutputDebugString() is a useful API function that sends a string to the Event Log debug window. Under normal circumstances, I have the tendency to use message boxes or to change the window caption, but in multithreaded applications these
06 9721 CH04
11/13/00
9:48 AM
Page 213
Advanced Programming with C++Builder CHAPTER 4
213
actions can sometimes be disastrous without considerable coding. OutputDebugString() is therefore a perfect alternative. The OutputDebugString() function is covered in more detail in the “Outputting Debug Information” section in Chapter 7, “Debugging Your Application.”
Synchronizing Threads Probably the greatest disadvantage of using threads is the difficulty in organizing them. Let’s say your application is simultaneously running two threads, which modify some global data. What will happen if they try to access the same data at the same time? Or what if the second thread has to wait for the first thread to process this data and then execute? To coordinate threads, Windows offers various methods of synchronization.
Critical Sections To illustrate two threads accessing the same global data, we’ll create a sample application using the TCriticalThread object (see Listing 4.28). LISTING 4.28
CriticalThreadUnit.cpp: TCriticalThread Thread Object
#include #pragma hdrstop #include “CriticalThreadUnit.h” #include “ThreadFormUnit.h” #pragma package(smart_init)
void __fastcall TCriticalThread::DisplayList() { ThreadForm->ListBox->Items->Add(Text); } void __fastcall TCriticalThread::Execute() { FreeOnTerminate = true;
ADVANCED PROGRAMMING WITH C++BUILDER
__fastcall TCriticalThread::TCriticalThread(bool CreateSuspended) : TThread(CreateSuspended) { }
4
06 9721 CH04
11/13/00
214
9:48 AM
Page 214
C++Builder 5 Essentials PART I
LISTING 4.28
Continued
for(int x = 0; x <= 50; x++) { if(Terminated) break; // EnterCriticalSection(&ThreadForm->CS); Sleep(50); ThreadForm->ListText.Insert(“=====”, 1); Text = ThreadForm->ListText; Synchronize(DisplayList); ThreadForm->ListText.SetLength(ThreadForm-> ListText.Length() - 5); // LeaveCriticalSection(&ThreadForm->CS); } }
And in our main unit, we’ll create two instances of this object (see Listing 4.29). LISTING 4.29
ThreadFormUnit.cpp
#include #pragma hdrstop #include “ThreadFormUnit.h” #include “CriticalThreadUnit.h” #pragma package(smart_init) #pragma resource “*.dfm” TThreadForm *ThreadForm; __fastcall TThreadForm::TThreadForm(TComponent* Owner) : TForm(Owner) { ListText = “=====”; // InitializeCriticalSection(&CS); } void __fastcall TThreadForm::StartClick(TObject *Sender) { TCriticalThread *FirstThread; FirstThread = new TCriticalThread(false); TCriticalThread *SecondThread; SecondThread = new TCriticalThread(false);
06 9721 CH04
11/13/00
9:48 AM
Page 215
Advanced Programming with C++Builder CHAPTER 4
LISTING 4.29
215
Continued
} void __fastcall TThreadForm::FormClose(TObject *, TCloseAction &Action) { // DeleteCriticalSection(&CS); }
Our code is both simple and useless, but it will demonstrate the importance of thread synchronization. First, the TCriticalThread object adds to the global ListText variable five equals (=) characters. It then adds the value of ListText to a ListBox. Finally, TCriticalThread() truncates five characters, thus setting ListText to the value it initially had. Logically, all ListBox items should display ==========, but as Figure 4.6 shows, that’s not always the case. Why? Because the second thread also accesses the same global variable.
FIGURE 4.6 The CriticalThread project without critical sections.
VOID InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); VOID EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection ); VOID LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
ADVANCED PROGRAMMING WITH C++BUILDER
Critical sections are an easy and efficient way to temporarily block other threads from accessing data (similar to the Lock() and Unlock() methods for graphic objects). To define a critical section, we’ll use four basic API functions:
4
06 9721 CH04
11/13/00
216
9:48 AM
Page 216
C++Builder 5 Essentials PART I VOID DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection );
It’s not so difficult to guess how to use these functions. First, we declare a variable of type CRITICAL_SECTION. We initialize this variable at program startup (InitializeCritical Section()) and delete it when the program closes (DeleteCriticalSection()). When our thread starts processing data, we block access to other threads with EnterCriticalSection() and, when it finishes, we exit the critical section (LeaveCriticalSection()). Go back to Listings 4.28 and 4.29, and comment out the four lines, which call the critical section functions I described. Then, open the header file of your main unit and add the following line: CRITICAL_SECTION CS;
As shown on Figure 4.7, all ListBox items now contain the same string.
FIGURE 4.7 The CriticalThread project with critical sections.
Mutexes Mutexes offer the functionality of critical sections, while adding other interesting features.
TIP Although featureless, critical sections are slightly faster then mutexes and semaphores. If time is an important factor in your application, consider using critical sections.
06 9721 CH04
11/13/00
9:48 AM
Page 217
Advanced Programming with C++Builder CHAPTER 4
217
Mutex objects are created using the CreateMutex() API function: HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName );
Once you have the handle of the newly created mutex object, you must use the WaitForSingleObject() function. This API call will request ownership for the mutex object, wait until this object becomes available, and use the mutex until ReleaseMutex() is called: HANDLE Mutex; Mutex = CreateMutex(NULL, false, NULL); if(Mutex == NULL) { ShowMessage(“Cannot create mutex!”); return; } // ... if(WaitForSingleObject(Mutex, INFINITE) == WAIT_OBJECT_0) { // ... } ReleaseMutex (Mutex);
NOTE
NOTE If a thread doesn’t release its ownership of a mutex object, this mutex is considered to be abandoned. Therefore, WaitForSingleObject() will return WAIT_ABANDONED. Although it’s not perfectly safe, you can always acquire ownership of an abandoned mutex.
ADVANCED PROGRAMMING WITH C++BUILDER
Unlike critical sections, the same mutex can be used by two or more processes.
4
06 9721 CH04
11/13/00
218
9:48 AM
Page 218
C++Builder 5 Essentials PART I
Others Other synchronization objects such as semaphores and timers are also available. By familiarizing yourself with critical sections and mutexes, you’ll already be one step ahead into mastering thread synchronization.
Introducing Design Patterns The use of design patterns for designing highly maintainable and well-understood software architectures has grown in popularity over the past few years. In this section we discuss the benefits of an important design philosophy.
Understanding the Recurring Nature of Patterns Everywhere we look and in everything we do, we encounter patterns. Patterns are those things that help us make sense of our activities and surroundings and those things that we identify with and use as a point of reference over and over again. They help us to communicate with others. When we need to convey an idea to a friend or colleague, we normally discuss only part of the story. The other person will fill in certain details that, due to concepts that we share with him, can be taken for granted. Patterns occur over and over again. Sometimes they are deliberately designed in, and often they just seem to happen of their own accord.
Recurring Patterns in Software Design By their very nature, computer programming languages don’t offer similar liberties. They require us to completely specify, in detail, all information about how a program is to operate. Such a program would be a very unpredictable tool if it were to attempt to make assumptions of its own. It is quite common for this necessary fussiness to cloud our own understanding of particular design problems or solutions. High-level object-oriented languages such as C++ offer much more expressive power than other languages, but still do not provide an effective means of communicating software design issues between people. In 1995, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides—a group of Computer Science researchers often known as “The Gang Of Four” —published an historic book entitled Design Patterns: Elements of Reusable Object-Oriented Software. This book suggested a completely new way for software developers to think through, talk about, and document designs. Its philosophy was that most complex software architectures can essentially be divided into several simpler design elements. These elements themselves are unlikely to be unique to a given architecture, and in fact they probably exist in some shape or form in countless other designs. Design patterns can be thought of as generic outlines that deal with a
06 9721 CH04
11/13/00
9:48 AM
Page 219
Advanced Programming with C++Builder CHAPTER 4
219
specific recurring programming problem. By tailoring and combining individual design patterns, complex software can be designed with past experience built-in, and in a way that makes it easier to understand. The authors explain: A design pattern systematically names, motivates, and explains a general design that addresses a recurring design problem in object-oriented systems. It describes the problem, the solution, when to apply the solution, and its consequences. It also gives implementation hints and examples. The solution is a general arrangement of objects and classes that solve the problem. The solution is customized and implemented to solve the problem in a particular context. The book includes a detailed introduction to design patterns, including a case study that shows how the techniques can be put into practice in a real-world project. It also contains a catalog of 23 design patterns that can be incorporated into your own designs.
Design Patterns as a Vocabulary If you are a reasonably experienced programmer, you will probably discover in the pattern catalog of Gamma et al. some patterns that you have previously used without actually thinking of them as being design patterns. For example, if your design includes what you have in the past described as “a class that has one globally accessible instance,” you could instead say “a Singleton.” Singleton is the term coined by Gamma et al. to describe a class with these characteristics. What you gain is a much more succinct vocabulary for describing your design. As with everyday vocabulary, things are much easier to understand when they are kept brief and to the point. Design patterns help us to be detailed yet concise.
Design Pattern Format
• Intent This section summarizes what problems the pattern addresses. For example (the Observer pattern): “Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.” • Also Known As
A list of any other names by which the pattern may be known.
• Motivation Normally a few paragraphs and illustrations describing situations in which the pattern might be employed. A case study is typically offered by way of example, sometimes with a class diagram that shows the results of applying and customizing the pattern to the specific case described. This section explains how the pattern might be put into practice in a real project.
4 ADVANCED PROGRAMMING WITH C++BUILDER
All design patterns follow a similar format so that they can be compared and contrasted easily. The format is documented in the book of Gamma et al. and designed to help us quickly match a particular design problem with one or more patterns that might be useful. The documentation of a design pattern is divided into a number of sections as described below.
06 9721 CH04
11/13/00
220
9:48 AM
Page 220
C++Builder 5 Essentials PART I
• Applicability A list of notes that helps assess whether or not the pattern is really appropriate for our specific circumstances. Typically begins: “Use pattern X when….” • Structure A class diagram that captures the pattern. While the Motivation section provides a real-world discussion of the pattern with tangible examples, the Structure diagram is completely generic. • Participants A brief description of the participating classes, as introduced in the Structure section. Cross-references class names against corresponding example terms in the Motivation section. • Collaborations A brief explanation of how the participants interact with each other (such as what functions each class calls on another, how, and why). • Consequences A list of potential benefits and pitfalls of using the pattern. This section can help identify the effort it could take to use the pattern in a specific project, and addresses how the pattern can impact on future development. There are always tradeoffs to keep in mind when actively using design patterns. • Implementation How to implement the pattern in software. This is normally the first place that specifics such as programming language features are mentioned. • Sample Code Mainly written in C++, though sometimes examples are given in other languages such as Smalltalk. • Known Uses projects.
A description of how the pattern has been used in existing commercial
• Related Patterns The pattern is usually best used in conjunction with other patterns listed in the “Related Patterns” section of the pattern documentation.
Design Pattern Classification The kinds of recurring problems faced in object-oriented software development can be classified further into a number of problem types. Naturally, these tend to follow the object-oriented paradigm itself, in which objects of various classes are created by the program, with each exhibiting certain structural properties and behavioral tendencies toward each other.
Creational Patterns These patterns focus on providing solutions to the question of how objects are to be created. For example, the Singleton pattern can be used to ensure that only a single instance of a class is available to the application. Also, the Factory Method is another creational pattern that is frequently used to allow the creation of an object whose class is not known until runtime, and the Prototype pattern can be used when new objects need to be created based upon already existing instances.
06 9721 CH04
11/13/00
9:48 AM
Page 221
Advanced Programming with C++Builder CHAPTER 4
221
Structural Patterns The structure of complex objects in an object-oriented schema can come about in two ways. First, this can happen as a result of inheritance—adding structural properties to a class by further subclassing it. Second, it can come about as a result of composition—defining an object’s structure in terms of other child objects. An example that should be familiar to users of C++Builder’s VCL is the Adapter pattern, also known as the Wrapper pattern. This is used to provide an alternative interface to an object when the existing interface is incompatible with other parts of the system. In the VCL, the TWinControl class is an adapter that replaces the Windows API’s handle-based and messagebased interface with one based on the property, event, and method model required for it to be used as a base class for reusable components that represent windowed controls. Another fundamental design goal is to uncouple an object’s interface from its implementation so that the two can vary independently. For example, this would allow the implementation to be altered without breaking existing code that works with the object. From a broader perspective, as a system evolves and grows, it is normally a good idea to subdivide it into independent subsystems to aid in its maintenance and to shorten the learning curve involved in understanding it. Patterns such as Bridge and Facade are useful in supporting these ideals.
Behavioral Patterns Behavioral patterns are concerned with the representation of algorithms in a program and the flow of control through a system. Fundamental behavioral concepts in object-oriented design, such as polymorphism, provide a level of control over the operations that are executed in an application and when they are executed. For example, polymorphism allows us to perform different operations on an object based on its type at runtime. Of course, we often need even more sophisticated behavior that is not directly supported by object-oriented principles.
Another vital behavioral pattern is called Observer, which is evident in many applications based around documents and the graphical views of them that exist. A prime example of this pattern can be seen in the C++Builder IDE. As you add or remove components from a form in
4 ADVANCED PROGRAMMING WITH C++BUILDER
One particular limitation of an object is that its class is fixed for its entire lifetime, which means that the implementation of the operations it supports is also fixed. This may pose problems such as when these operations need to vary according to the object’s state. The obvious solution would be to rewrite the operations so that they initially query the state and perform different steps accordingly. However, this will lead to unwieldy functions that are difficult to follow. A better solution would be to encapsulate families of operations into separate classes, and for the object to reconfigure itself with a new instance of one of these classes whenever its state changes. The object’s operations can then transparently forward calls to a corresponding operation of the current implementation object, to give the illusion of having changed its class. This is referred to as the State pattern.
06 9721 CH04
11/13/00
222
9:48 AM
Page 222
C++Builder 5 Essentials PART I
the Form Designer, declarations for these components are added to the form class declaration, as are the necessary header includes and package link pragmas. It appears as if the visual form and the C++ code are tightly linked and know each other intimately, enabling them to remain synchronized. It seems 99% certain that this happy situation arises from the use of the Observer pattern. This pattern allows two or more distinct objects that need to know nothing about each other to work independently with another shared object, which notifies all of its users (“observers”) when it undergoes change. In the case of the IDE, the shared object would be an internal representation of the form itself (the object that stores itself as a DFM file), and the observers would be objects that manage the Form Designer and Code Editor.
Parting Thoughts About Design Patterns This introduction to design patterns is intended to provide a reminder of the importance of a sound approach to software design. C++Builder professes to be a rapid development tool, and it is sometimes tempting to think of the planning stage as an unnecessary use of resources, because for many tasks a working prototype can be constructed easily, and the environment can be quite forgiving. However, for advanced development programs, it is important to avoid software composed of square pegs when the holes suddenly become round. A considered approach to design can be vital, and design patterns can be a source of inspiration for clever ideas and a way to keep one eye on the long-term implications of a particular design route without neglecting immediate needs. Design patterns do not constitute a development methodology at all, so to use one needn’t mean a complete overhaul of your programming practices. A design pattern is more like a toolkit of bright ideas to help you write better, more maintainable software.
Summary This chapter describes some of the most advanced techniques for C++Builder programming. Among other things, you learned how to use the Standard Template Library, smart pointers, and strong pointers; how to implement advanced exception handlers; how to create multithreaded applications; and how to use design patterns. The SCL provides container classes that make it easy to manipulate groups of elements. This is a task all projects have to address, so don’t reinvent the wheel. The strong memory management paradigm saves you time when writing exception-safe code, and it saves you time not looking for memory leaks. Unfortunately, you will never know how many days of frustration you saved. While the compiler’s default exception handler may be adequate for simple and small programs, it is much too crude for most projects. The capability to gather information about the
06 9721 CH04
11/13/00
9:48 AM
Page 223
Advanced Programming with C++Builder CHAPTER 4
223
system’s state at a given time and store it for later review is paramount in successfully supporting a product. Patterns are points of reference that we encounter over and over again. When applied to software development, design patterns allow us to understand and solve a particular problem, offering us hints, examples, and a source of inspiration. For developers, the terms multitasking and multithreading are often sources of confusion. Multitasking is the capability of the operating system to run multiple programs at the same time. Multithreading is the capability of a program to run multiple tasks (threads) at the same time. Most Windows applications use only one thread.
4 ADVANCED PROGRAMMING WITH C++BUILDER
06 9721 CH04
11/13/00
9:48 AM
Page 224
07 9721 ch5
11/13/00
9:50 AM
Page 225
User Interface Principles and Techniques Jamie Allsop Zexiang Wu
IN THIS CHAPTER • User Interface Guidelines • The Example Projects Used in This Chapter • Enhancing Usability by Providing Feedback to the User • Enhancing Usability Through Input Focus Control • Enhancing Usability Through Appearance • Enhancing Usability by Allowing Customization of the User Interface • Enhancing Usability by Remembering the User’s Preferences • Coping with Differing Screen Conditions • Coping with Complexity in the Implementation of the User Interface
CHAPTER
5
07 9721 ch5
11/13/00
226
9:50 AM
Page 226
C++Builder 5 Essentials PART I
One of C++Builder’s greatest strengths is its capability to allow visual development of an application’s user interface (UI). To make sure your interface meets the user’s expectations and requirements, you should adhere to some basic principles and guidelines. Coupled with the knowledge of how to implement the interface to meet these requirements, this should be enough to help you create intuitive, easy-to-use applications. The first section of this chapter looks at some principles of UI design and offers guidelines as to how to meet such principles. The remainder of the chapter builds on the first by offering implementation advice and suggestions in relation to each of the guidelines presented in the first section. A sample application is presented and used as a focus for the discussion of a variety of techniques related to UI creation and design. Not all of the topics covered are directly relevant to the sample application, and alternative examples are used in those cases.
User Interface Guidelines When designing a user interface, there are some basic principles and guidelines that you should adhere to. Most of these are common sense, but some may not be so obvious. The following list summarizes the main guidelines to be aware of. Note that these are not presented in any particular order. • Meet the user’s expectations—An application that enables a user to perform a specific task should function the way the user would expect. If you meet the user’s expectations, he will be comfortable using the interface. • Keep the interface clear and simple—An interface’s appearance should convey its function in an obvious manner and allow easy navigation from one part of the interface to another. Simple and clear interfaces don’t distract the user’s attention from key tasks. Group related controls together but avoid areas with a high density of controls. Also, ensure that controls that receive mouse input are of a reasonable size; this encourages error-free navigation. Similarly, don’t have very large distances between frequently used controls. Both situations will make the user tired, the first because excessive concentration is required and the second because excessive mouse movement is required. • Make the interface intuitive and familiar to use—Try to make it possible for users to figure out how to perform a task without having to be taught. Few users will be totally familiar with your interface, so you are guaranteed that at some time they will come across something unfamiliar. If this happens early in the learning process and the user cannot overcome his lack of knowledge, he may choose not to use your application. On the other hand, if you provide users with an intuitive interface, they will learn how to use it in less time and experience the more advanced capabilities of your application sooner. This is important when a user is trying to identify how powerful your application is and whether or not it is suitable for his needs. An additional consideration is to ensure that
07 9721 ch5
11/13/00
9:50 AM
Page 227
User Interface Principles and Techniques CHAPTER 5
227
tasks in your interface are logically sequenced; in other words, your interface should function in an order that makes sense to the user. This will help make the user feel comfortable with your application. • Keep the interface friendly—If you provide the user with a friendly interface, he won’t mind spending time learning and using it. If the user spends more time doing something, he will become more accomplished at it. A willingness to spend time using an interface that is familiar and intuitive will affect the learning curve of your application, allowing productive use in as short a time as possible. • Provide the user with feedback—Providing feedback to the user can help build his confidence and reassure him of what he is doing. It can also help keep the user’s interest when mundane or long tasks are being carried out. This is a very important facet of any interface, and you should endeavor to provide feedback that is as useful and accurate as possible. Feedback can take many forms, some of which are taken for granted, such as a button that looks pressed when it is clicked. Details such as these should not be considered optional. At a more fundamental level, your whole interface is feedback for the user. By adding extra feedback, you simply enhance what the user already experiences. In some cases, additional feedback may be essential, such as when the user has some form of disability (the subject of the next point). • Make your interface as accessible as possible—This means providing varied types of feedback and varied forms of input to meet the needs of users who may have difficulty with normal approaches. For example, you could provide additional audio feedback for users with impaired sight, or you could provide speech recognition–driven input for users who have difficulty manipulating the standard input devices. • Provide help—Sometimes a user will get stuck. To cope with this, you can do two things: Provide useful documentation and provide support. You should always provide some form of documentation, as either online help or printed documentation. Chapter 27, “Creating Help Files and Documentation,” is devoted to the creation of documentation support for your applications. If possible, you should also provide some form of support. The kind of support you provide can range from simple email-based support to 24-hour manned support, either telephone-based or on-site. Providing at least email-based support should be considered mandatory.
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
• Allow customization—Let the user customize the interface to his liking. Simple things such as choice of color can have a significant effect on whether a user likes an interface. The use of system colors, such as clMenu (for menus), can help your interface take on an appearance the user likes. But customization is not just letting the user move things around so the interface looks good. It is about allowing the user to access functionality that he uses frequently in a way that is most convenient for him. Not everyone will use your application in the same way. Allowing a user to modify the interface to reflect his normal usage patterns can help make your application become the user’s preferred tool.
07 9721 ch5
11/13/00
228
9:50 AM
Page 228
C++Builder 5 Essentials PART I
• Provide an escape route for the user—Even the most experienced user will take a misstep. Provide some form of escape to let him backtrack. In its most common form, this means providing an undo function, which is now almost a standard feature of all editingtype interfaces. • Inform the user of errors in an understandable way—No matter how foolproof you might think your interface is, some user somewhere will cause an error to occur. When that happens, the normal response is to display a message box to the user indicating what the error is and how to avoid it. Sometimes you may even give the user a second chance to complete the operation that caused the error. One of the single most annoying things to do in this situation is to display a message box that provides only a cryptic message that even the developer may not understand. From the user’s point of view, the interface will appear broken, and this is certainly not a comfortable feeling. You should therefore endeavor to provide constructive information regarding any errors that may arise. In fact, if possible, try to prevent the error from affecting the user at all by catching it when it occurs. A problem with all this is that it is very difficult to identify all possible sources of errors and either insulate the user from the error or provide constructive feedback about the error. Good error handling in an interface is often an area of continual improvement. • Use symbols, images, and color to make the interface more interesting and easier to navigate—Symbols make an interface quicker to navigate because it is quicker to recognize a symbol than it is to read text. A common approach to using symbols is to provide them alongside text. Users will initially rely on the text descriptions to aid navigation, but as familiarity with the related symbols grows, the user can use them instead. Symbols must be consistent throughout an interface to achieve the maximum benefit, and they should also be carefully designed so that their meaning is obvious. Images can also be used to enhance an interface, either as graphical elements that provide a function or simply to make the interface interesting. Careful thought is required when adding images. Color can be used to group related controls, provide visual separation, or carry additional information, such as syntax highlighting in an IDE. • Make use of all input devices—Different users like to do things in different ways. Some find different methods of input more convenient than others. Your interface should respond to both pointing input (such as the mouse) and keyboard input. You should also try to adhere to common conventions. For example, if your interface provides a copy function, a user will assume that he can use the keyboard to perform the copy and will expect that the keyboard shortcut is Ctrl+C. As was said at the beginning, most of these guidelines are common sense, but it doesn’t hurt to list them. When you’re under a pile of printed code or slaving over a hot debugger, it is sometimes easy to forget that someone somewhere who doesn’t even speak your language or know where your country is may end up using your program, and he will have very definite ideas about what it should do.
07 9721 ch5
11/13/00
9:50 AM
Page 229
User Interface Principles and Techniques CHAPTER 5
229
It is often worthwhile to review interfaces you create to see if they adhere to the guidelines presented or, better still, have them tested and reviewed by a third party. It can also be helpful to review other interfaces you are familiar with to help pinpoint interface designs that you like or don’t like. Sometimes studying what is bad about an interface can be just as instructive as considering what is good about an interface. You may want to identify aspects of interface design that are considered not good to help avoid repeating such mistakes in your own design.
The Example Projects Used in This Chapter The remainder of this chapter is broken into sections that deal with specific issues that arise when implementing the user interface for applications. Each section discusses a specific topic. Throughout most of these sections, reference will be made to the MiniCalculator application that has been developed to illustrate most of the topics discussed in this chapter. This application, along with full source code and all images, is available on the CD-ROM. There is also an executable version available (MiniCalulator.exe). It is useful to run this program and play with it. In those sections that are not directly relevant to the MiniCalculator project, other example projects are used. These are shown in Table 5.1, along with the titles of the sections in which they appear. For projects that appear in more than one section, the sections are ordered as they appear in this chapter. TABLE 5.1
Projects Other than MiniCalculator Used in This Chapter
Project
Section(s)
Focus.bpr
“Moving Input Focus” “Customizing the Client Area of an MDI Parent Form” “Using Action Lists” “Using Align” “Using Anchors” “Using Constraints”
MDIProject.bpr Panels.bpr
ProgressCursor.bpr ScreenInfo.bpr
“Using TProgressBar and TCGauge” “Using the Cursor” “Coping with Differing Screen Conditions”
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
Of the projects listed in Table 5.1, the MDIProject is notably different. It is a multipledocument interface (MDI) application. All the rest are single-document interface (SDI) applications. SDI applications typically have a single main form, and other forms are shown as required, either modally (such as dialog boxes) or non-modally, but they are not child windows of the main form. With an MDI application, some of the forms that are created are designated as child windows of the main (MDI parent) form. Such forms are confined to the client region of
07 9721 ch5
11/13/00
230
9:50 AM
Page 230
C++Builder 5 Essentials PART I
the MDI parent form. Essentially, the MDI parent form acts as a visual container of the child forms. Child forms are shown non-modally. This allows you to switch between child windows within the parent form. MDI applications generally offer certain common functionality, such as the capability to arrange child windows, merge menus, and open different kinds of child windows. To see how this done in practice, you can examine the MDIProject.bpr project.
Introducing the MiniCalculator Project The MiniCalculator program provides the basic functionality of a simple calculator. The interface has been designed to be similar to that of a real calculator, with additional features that a user would expect a Windows application to provide. This is an important point. Simply creating an interface that mimics a real calculator offers the user a familiar interface, but it also introduces all the restrictions that a real calculator has. What we want to achieve is an interface that, while familiar because of its similarity to a real calculator, does not have its usability and functionality constrained by that similarity. This is a topic we will visit again in the section, “Enhancing Usability Through Appearance.” Figure 5.1 shows a screenshot of MiniCalculator. As you can see, it has a highly graphical interface and looks like a calculator. Anyone with experience using a calculator should find it very simple to access the basic calculator functionality that MiniCalculator offers. In addition, anyone with experience with any basic Windows application will be able to access the additional functionality that we have been able to provide as a result of MiniCalculator being such an application.
FIGURE 5.1 The MiniCalculator.
07 9721 ch5
11/13/00
9:50 AM
Page 231
User Interface Principles and Techniques CHAPTER 5
231
It should be noted that only the components that come with C++Builder have been used to create the MiniCalculator application. No custom or third-party components have been employed. This should give you an idea of what is possible with C++Builder. The components that comprise the MiniCalculator program are listed in Table 5.2 under the tab on which they appear on the Component Palette. TABLE 5.2
Components Used in the MiniCalculator Project
Component Palette Tab
Component(s)
Standard
TActionList TMainMenu TPanel TPopupMenu
Additional
TApplicationEvents TBevel TBitBtn TControlBar TImage TSpeedButton
Win32
TImageList TStatusBar
Dialog
TColorDialog
Much of the time spent creating the MiniCalculator program was in designing the layout and creating the images (such as the buttons). All the images used in MiniCalculator were created using JASC’s Paint Shop Pro package. A trial version of Paint Shop Pro is available on the accompanying CD-ROM.
Enhancing Usability by Providing Feedback to the User
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
Giving the user of an application continual feedback is a good way to enhance the usability of the application’s interface. There are several standard approaches that are commonly employed. You can use a status bar to provide additional status information about the application, such as the cursor position. You can use hints to provide clues for the user. You can use progress indicators such as a progress bar to indicate the progress of long operations, and you can use the cursor shape to indicate the current mouse function, if any is possible. For example, an hourglass cursor is often used to indicate that no mouse action (other than moving) is possible, and a hand cursor is often used to indicate that a control is hyperlinked.
07 9721 ch5
11/13/00
232
9:50 AM
Page 232
C++Builder 5 Essentials PART I
This section looks at using each of these methods to provide feedback to the user and illustrates implementation techniques that are commonly required.
Using TProgressBar and TCGauge A progress bar is a good method of indicating the progress of a long operation. It reassures the user that the program is still functioning and carrying out the task requested of it. There are two progress components in C++Builder that can be used with a minimum of effort. These are TProgressBar and TCGauge. TProgressBar is found on the Win32 tab of the Component Palette, and TCGauge is found on the Samples tab of the Component Palette. The ProgressCursor.bpr project on the CD-ROM that accompanies this book shows the two controls in a variety of configurations. Of the two controls, TCGauge is in many ways superior. It is obvious what information TCGauge is providing, and the progress indicated is easily quantified.
In this way TProgressBar is not so good. The use of segmentation in TProgressBar is one of the reasons that it does not perform its task well. The segmentation bears no relation to the amount of progress being made. Therefore, the user can only guess at how much progress has been made. Still, it is one of the Win32 standard controls, and users are familiar with it. When an accurate estimate of progress is not required, it is adequate. TCGauge is also not perfect, but with the source in the cgauges.cpp file in the $(BCB)\Examples\Controls\Source directory, you can always change it to suit your needs. The operation of TProgressBar and TCGauge is similar. Given a TProgressBar called ProgressBar and a TCGauge called CGauge, we can set the minimum, maximum, and start position as follows: //Minimum ProgressBar->Min = 0; CGauge->MinValue = 0; //Maximum ProgressBar->Max = 100; CGauge->MaxValue = 100; //Current Position ProgressBar->Position = 0; CGauge->Progress = 0;
We can increment the current position for each control by 1 as follows: //Increment by one ProgressBar->Position = ProgressBar->Position + 1; CGauge->Progress = CGauge->Progress + 1;
07 9721 ch5
11/13/00
9:50 AM
Page 233
User Interface Principles and Techniques CHAPTER 5
233
For both controls, trying to increment past the maximum value for the control has no effect. However, TProgressBar also provides the StepIt() and StepBy() methods. StepIt() increments the Position of TProgressBar by the value of its Step property. StepBy() increments the Position of TProgressBar by the amount specified in the single int argument that you pass to the method. Incrementing Position using these two methods does not cause Position to stop at Max when it is reached. Instead, Position resets to 0 and continues to be incremented. You can see this in practice in the ProgressCursor.bpr project. The choice of progress indicator is up to you, but for long operations, you should always use an indicator of some kind, even if it’s only to let the user know that the program still functions.
Using the Cursor Changing the cursor image to provide feedback to the user or to provide additional information to the user is a common technique in interface programming. In C++Builder, handling the cursor is easy. You can access the cursor in one of two ways. You can set the TCursor properties of a control, such as the Cursor and DragCursor properties. These affect how the cursor appears when it is over the control under different circumstances. For example, the cursor specified in the DragCursor property is the one displayed when dragging the control. The cursor specified in the Cursor property is the one displayed when the cursor enters the control. Note that these are effective only when the Screen-> Cursor property is crDefault. You also can access the global cursor through the Cursor property of the global Screen variable. When you change the value of Screen’s Cursor property, you should always make a copy of the original cursor so that it can be restored when you have finished with the cursor. The structure you should use is TCursor OriginalCursor = Screen->Cursor; Screen->Cursor = crXXXX; // Assign the new cursor try { // Do what you want to do } __finally { // Restore the original cursor Screen->Cursor = OriginalCursor; } TECHNIQUES
USER INTERFACE PRINCIPLES AND
The ProgressCursor.bpr project on the CD-ROM that accompanies this book shows both methods of changing the cursor.
5
07 9721 ch5
11/13/00
234
9:50 AM
Page 234
C++Builder 5 Essentials PART I
Using Custom Cursors To use a custom cursor you must first assign your cursor to the global Screen variable’s Cursors array property. For custom cursors you can use any positive index. The stock cursors use -22 (crSizeAll) to 0 (crDefault). To obtain an HCURSOR to assign to the array use the WinAPI LoadCursor() or LoadCursorFromFile() function. To load animated cursors use the LoadCursorFromFile() function. For example in the ProgressCursor.bpr project we load the “Face” animated cursor as follows: Screen->Cursors[crFaceAnimatedCursor] = LoadCursorFromFile(“Face.ani”); Face.ani is the file containing the animated cursor. crFaceAnimatedCursor is a constant set equal to 1 in the initialization list of Form1’s constructor. This helps keep code more readable. We use this index when we assign the custom cursor to the global Screen variable’s Cursor property later in the program, as follows: Screen->Cursor = crFaceAnimatedCursor;
Incidentally, the looping motion of the Face animated cursor was created by moving the cursor hotspot for each frame of the animation. The LoadCursor() function loads a cursor from a resource. In ProgressCursor.bpr we use this to load the Eye cursor. First we create a resource file (.rc) with these contents: EyeCursor CURSOR Eye.cur
Then we use LoadCursor() to assign the HCURSOR to Screen->Cursors as follows: Screen->Cursors[crCustomEyeCursor] = LoadCursor(HInstance, “EyeCursor”);
Again, crCustomEyeCursor is a constant that keeps the code readable when we later assign the cursor to the Cursor property of CustomCursorCheckBox: CustomCursorCheckBox->Cursor = crCustomEyeCursor;
Using custom cursors is easy. The difficulty arises in creating the custom cursors. A good cursor editing tool is a must. For more information on using resources in your applications, refer to the “Using Predefined Images in Custom Property and Component Editors” section in Chapter 10, “Creating Property and Component Editors.” This section discusses resources in property and component editors, but the information is equally applicable here.
Using TStatusBar TStatusBar (found on the Win32 tab of the Component Palette) provides an excellent mechanism for providing feedback and additional information to the user of an application. It can even provide additional control to the user by responding to the mouse. In its simplest form, TStatusBar is a single panel (SimplePanel = true) displaying only text information, as determined by the value of the SimpleText property.
07 9721 ch5
11/13/00
9:50 AM
Page 235
User Interface Principles and Techniques CHAPTER 5
235
For many applications this is sufficient; however, TStatusBar can offer much more than this. TStatusBar can also have multiple panels, and each of these can be owner-drawn if required (set the Style property to psOwnerDraw). Each panel in a status bar is encapsulated by a TStatusPanel object. Table 5.3 shows the properties of TStatusPanel most often used. TABLE 5.3
TStatusPanel Properties Most Commonly Used
Property
Use
Alignment
Determines how the AnsiString text contained in the Text property should be aligned within the status panel. It can take the value taLeftJustify, taCenter, or taRightJustify. Determines the appearance of the bevel drawn around the edge of the status panel. It can take the value pbNone, pbLowered, or pbRaised. If Bevel is set to pbNone, then the panel has no bevel and it is indistinguishable from the rest of the status bar (unless it is owner drawn to appear differently).
Bevel
Index
Style
Text
Width
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
A read-only property inherited from TCollectionItem that indicates the index (0-based) of the panel within the status bar. It is used with the Items and Count property of TStatusBar’s Panels property. The Panels property is of type TStatusPanels. Items is an array of TStatusPanel objects. The Index property of a TStatusPanel indicates the index in the Items array. Use Panels->Count to iterate through the panels (Panels->Items) of a status bar. Determines how the panel is displayed. It can be either psText or psOwnerDraw. psText is the default and indicates that the string in the Text property is displayed using the status bar font with the alignment specified by the panel’s Alignment property. If Style is psOwnerDraw, then you must supply the necessary code to render the panel onto the status bar’s Canvas in the status panel’s OnDrawPanel event. Determines the string that will be displayed in the panel if the panel’s Style is psText. Even if the Style is psOwnerDraw, this value can still be used to store the string value to be rendered. Determines the width of the panel. The height of every panel is the same and can be determined by reading the ClientHeight property of the status bar that contains the panel. Note that TStatusPanel has no Visible property. If you want to hide a status panel from view, you can do so by setting its Width property to 0.
07 9721 ch5
11/13/00
236
9:50 AM
Page 236
C++Builder 5 Essentials PART I
To access the status panels of a status bar, you must use the Items property of TStatusBar’s Panels property. For example, to assign the string “This is Panel 0” to the Text property of the first panel (Index == 0) in a multipanel status bar (called StatusBar1), you would use the following code: StatusBar1->Panels->Items[0]->Text = “This is Panel 0”;
As mentioned in the Index entry of Table 5.3, TStatusBar’s Panels property is of type TStatusPanels, a TCollection descendant. As such, its Count property can be used to determine how many panels are present in the status bar. The MiniCalculator example program uses a TStatusBar with three panels. The first and third panels are owner drawn (Style = psOwnerDraw), and the second panel simply displays text assigned to its Text property (more about this in the next section, “Using Hints”). We will now look at the first owner-drawn panel. The MiniCalculator program can respond to input from the keyboard. This allows the user to use the application as if it were a real calculator, with the keyboard (mainly the numeric keyboard) acting as the calculator’s keys. By default this is enabled, but the user can disable it if he wants. In addition, there are times when the application cannot respond to keyboard input. This will occur when the main form of the application loses focus. In the MiniCalculator program, the first panel (0) is responsible for showing the user the status of the keyboard input and also for allowing the user to enable or disable keyboard input as he prefers. To indicate the status of keyboard input, three images are used: one to indicate that keyboard input is enabled and the main form has focus; one to indicate that the keyboard is enabled but that the main form does not have focus; and one to indicate that keyboard input is disabled. These are stored in three TImage components: HasKeyboardFocusImage, HasNoKeyboardFocusImage, and DisableKeyboardImage. To draw the image onto the status bar, we must implement TStatusBar’s OnDrawPanel event. The OnDrawPanel event has three parameters: TStatusBar* StatusBar, TStatusPanel* Panel, const TRect& Rect
These three parameters are a pointer to the status bar that triggered the event, a pointer to the panel the event is for, and a const TRect structure to indicate the region to be drawn. To draw on the panel, you use the Rect parameter to set the bounds of the image you are using and use the Canvas property of the StatusBar parameter to do the actual drawing. Listing 5.1 shows how this was done for the MiniCalculator program. Note that the code required to draw the other owner-drawn panel, with Panel->Index == 2, has been omitted for clarity. The code for this panel is shown in Listing 5.4.
07 9721 ch5
11/13/00
9:50 AM
Page 237
User Interface Principles and Techniques CHAPTER 5
LISTING 5.1
237
Implementing TStatusBar’s OnDrawPanel Event—Part 1
void __fastcall TMainForm::StatusBar1DrawPanel(TStatusBar* StatusBar, TStatusPanel* Panel, const TRect& Rect) { if(Panel->Index == 0) { if(EnableKeyboardInput) { if(CanUseKeyboard) { // Paint the Keyboard image onto the panel StatusBar->Canvas->Draw(Rect.Left, Rect.Top, HasKeyboardFocusImage->Picture->Bitmap); } else { StatusBar->Canvas->Draw(Rect.Left, Rect.Top, HasNoKeyboardFocusImage->Picture->Bitmap); } } else { StatusBar->Canvas->Draw(Rect.Left, Rect.Top, DisableKeyboardImage->Picture->Bitmap); } } // if(Panel->Index == 2) Omitted - see Listing 5.2 }
The code in Listing 5.1 is straightforward. First we make sure that Panel is the first panel. We do this by checking that its Index property is equal to 0. To decide which image to draw, we check the value of two variables: •
EnableKeyboardInput
•
CanUseKeyboard
If true, then keyboard input is enabled; false otherwise.
5 TECHNIQUES
These are bool properties, not variables, and help simplify the coding required for the status bar and the application as a whole. This is discussed again later in this section.
USER INTERFACE PRINCIPLES AND
If true, the main form has focus and keyboard input is possible (if enabled); false otherwise.
07 9721 ch5
11/13/00
238
9:50 AM
Page 238
C++Builder 5 Essentials PART I
Regardless of which image is used, the method of drawing is the same. The Draw() method of StatusBar’s Canvas property is used. This takes as its parameters the coordinates of the lefttop corner of the region on which you want to render your image and a pointer to a TGraphicderived object, such as a TBitmap object, as in this case. In the MiniCalculator program, the Height of the status bar is 30, and the Width of the first panel is 72. The panel width includes a one-pixel bevel, even if the Bevel property is set to pbNone; setting Bevel to pbNone simply means that the bevel will not be drawn. In addition to the one-pixel bevel, enclosing the panel there is a two-pixel strip across the top of the status bar. Therefore, to calculate the dimensions the Rect parameter will have, you can use the following: UseablePanelHeight = StatusBar->Height - 4 - 2*StatusBar->BorderWidth; UseablePanelWidth = Panel->Width - 2;
This indicates that the available space in the first panel is 70 pixels by 26 pixels. This is the size of the images that we want to display in the panel; the status bar’s height and the panel’s width were deliberately chosen so that this would be the case. Figure 5.2 illustrates this. Rect.Left = 1
Image is 70x26 pixels
2 pixels Rect.Top = 3 2 pixels
StatusBar–>Height = 30
Panel–>Width = 72
StatusBar–>BorderWidth = 0
Rect.Bottom = 29 Rect.Right = 71
FIGURE 5.2 An owner-drawn status bar.
The status bar in the MiniCalculator program also responds to user input from the mouse. When the left mouse button is pressed in the first panel of the status bar, the EnableKeyboardInput property is toggled, either enabling or disabling keyboard input. To do this we must implement TStatusBar’s OnMouseDown event. In the OnMouseDown event handler, we check to see if the mouse is over the first panel of the status bar when the event is fired. If it is, we toggle the EnableKeyboardInput property. Listing 5.2 shows the code.
07 9721 ch5
11/13/00
9:50 AM
Page 239
User Interface Principles and Techniques CHAPTER 5
LISTING 5.2
239
Implementing TStatusBar’s OnMouseDown Event
void __fastcall TMainForm::StatusBar1MouseDown(TObject* Sender, TMouseButton Button, TShiftState Shift, int X, int Y) { if(Button == mbLeft) { // Is the mouse inside the first panel ??? // and inside the panel’s bevel ??? if( X > 1 && X < (StatusBar1->Panels->Items[0]->Width - 1) && Y > 3 && Y < (StatusBar1->Height - 1) ) { // If yes then toggle the keyboard input if(EnableKeyboardInput) EnableKeyboardInput = false; else EnableKeyboardInput = true; } } }
Referring to Figure 5.2 should help make this code more clear. When the value of the EnableKeyboardInput property is changed, its set method is called. The code for this is shown in Listing 5.3. LISTING 5.3
The EnableKeyboardInput Set Method
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
void __fastcall TMainForm::SetEnableKeyboardInput(bool NewEnableKeyboardInput) { if(EnableKeyboardInput != NewEnableKeyboardInput) { FEnableKeyboardInput = NewEnableKeyboardInput; if(EnableKeyboardInput) { OnKeyDown = CalculatorKeyDown; OnKeyUp = CalculatorKeyUp; StatusBar1->Panels->Items[1]->Width = 60; } else { OnKeyDown = 0; OnKeyUp = 0;
07 9721 ch5
11/13/00
240
9:50 AM
Page 240
C++Builder 5 Essentials PART I
LISTING 5.3
Continued
StatusBar1->Panels->Items[1]->Width = 0; } StatusBar1->Invalidate(); } }
Of note in Listing 5.3 is that, when we change the value of FEnableKeyboardInput, the status bar’s Invalidate() method is called to force the control to repaint and therefore fire the OnDrawPanel event, which allows the image in panel 0 to be updated. Also, you can see that the main form’s OnKeyDown and OnKeyUp events are set. When they are set to 0, MiniCalculator will not respond to keyboard input. When they are set to the CalculatorKeyDown() and CalculatorKeyUp() methods, keyboard input is handled. It is possible to make the panel look and act just like an ordinary button. To do this, set the Bevel property to bpRaised. When the OnMouseDown event is fired, change the Bevel property to bpLowered; then when the OnMouseUp event occurs change the Bevel property back to bpRaised. In addition, you should check to see if OnMouseUp occurred inside the panel. If it did, then you can consider that the panel has been clicked and can act accordingly. We shall consider the third status bar panel in the MiniCalculator program. It is also owner drawn. This panel displays a description of a button’s function if appropriate. It is highly likely that this description will be larger than the space available to display it. If this is so, then it is preferable to truncate the description and add an ellipsis (...) to the end of the string. This makes it clear that the remainder of the string is hidden, and it also looks more tidy than having text simply chopped off at the end. To display a string in this way, we use the WinAPI DrawText()function. We will use this function throughout this chapter, so we will describe its capabilities briefly here. DrawText()
is declared as
int DrawText( HDC LPCTSTR int LPRECT UINT );
hDC, lpString, nCount, lpRect, uFormat
// // // // //
Pass Canvas->Handle Pass string.c_str(), where string is an AnsiString -1 for NULL terminated string, otherwise length Pointer to TRect structure with dimensions Text-drawing flags
returns the text height when successful. It is the formatting flags that you can use with this function that make it so versatile. These are listed in Table 5.4. Note that in Table 5.4 the entries are not arranged alphabetically. They are arranged in related groups.
DrawText()
07 9721 ch5
11/13/00
9:50 AM
Page 241
User Interface Principles and Techniques CHAPTER 5
TABLE 5.4
The WinAPI DrawText() Function Formatting Flags
Flag
Use
DT_CALCRECT
No text is drawn; the TRect pointed to by the lpRect parameter is modified to bound the formatted text. Justifies the text to the left of the TRect pointed to by the lpRect parameter. Justifies text to the center of the TRect pointed to by the lpRect parameter.
DT_LEFT DT_CENTER DT_RIGHT DT_BOTTOM DT_VCENTER DT_TOP DT_SINGLELINE DT_END_ELLIPSIS
DT_PATH_ELLIPSIS
DT_MODIFYSTRING
DT_EDITCONTROL
DT_EXPANDTABS DT_EXTERNALLEADING
5 TECHNIQUES
Justifies the text to the right of the TRect pointed to by the lpRect parameter. Aligns text to the bottom of the TRect pointed to by the lpRect parameter. Must be called with DT_SINGLELINE. Aligns text (vertically) to the center of the TRect pointed to by the lpRect parameter. Must be called with DT_SINGLELINE. Aligns text to the top of the TRect pointed to by the lpRect parameter. Must be called with DT_SINGLELINE. Displays text on a single line; line feeds and carriage returns do not break the line. If the text won’t fit in the TRect pointed to by the lpRect parameter then the text is truncated and an ellipsis (...) is appended. If DT_MODIFYSTRING is specified, the given string is also modified. Similar to DT_END_ELLIPSIS, but for paths with the backslash (\) character. Text before the backslash or between pairs is replaced by an ellipsis (...) if the text is too long. If DT_MODIFYSTRING is specified, the given string is also modified. If set and DT_END_ELLIPSIS or DT_PATH_ELLIPSIS is also specified, the text will be modified to reflect the text drawn. Has no effect otherwise. The text is displayed as if it were in a multiline edit control. Partially visible last lines are not displayed, and the average character width is calculated. Tabs are expanded. The default tab size is eight characters. The external leading height of the font (the space that should appear between lines) is displayed. Draws text without clipping. The operation is faster with this flag set.
USER INTERFACE PRINCIPLES AND
DT_NOCLIP
241
07 9721 ch5
11/13/00
242
9:50 AM
Page 242
C++Builder 5 Essentials PART I
TABLE 5.4
Continued
Flag
Use
DT_NOPREFIX
If this flag is included, prefix characters are not processed. For example, an ampersand (&) is drawn as an &. Normally this would be drawn as an accelerator key by drawing a line under the following character, as in menus. Draws the text in right-to-left order for bidirectional support. Sets the tab size. The default is eight characters. Places the new value in bits 8–15 of the uFormat parameter. If the text is too long to be displayed in the TRect pointed to by the lpRect parameter, the line is broken to the next line. Linefeed/carriage returns also break the line.
DT_RTLREADING DT_TABSTOP DT_WORDBREAK
Listing 5.4 shows the code required to render text onto the status bar’s Canvas. This is the same event handler as that shown in Listing 5.1. However, the owner-drawing code for Panel->Index == 0 has been omitted, and the code for Panel->Index == 2 is shown. LISTING 5.4
Implementing TStatusBar’s OnDrawPanel Event—Part 2
void __fastcall TMainForm::StatusBar1DrawPanel(TStatusBar* StatusBar, TStatusPanel* Panel, const TRect& Rect) { if(Panel->Index == 0) { // Omitted for clarity - see Listing 5.1 } else if(Panel->Index == 2) { TFontStyles FontStyle; TColor OldBrushColor = StatusBar->Canvas->Brush->Color; TFontStyles OldFontStyle = StatusBar->Canvas->Font->Style; StatusBar->Canvas->Font->Style = FontStyle; StatusBar->Canvas->Brush->Color = clSilver; StatusBar->Canvas->FillRect(Rect); TRect PanelRect = Rect; PanelRect.Left += 2; PanelRect.Right -= 2,
07 9721 ch5
11/13/00
9:50 AM
Page 243
User Interface Principles and Techniques CHAPTER 5
LISTING 5.4
243
Continued
DrawText( StatusBar->Canvas->Handle, Panel->Text.c_str(), -1, &PanelRect, DT_LEFT |DT_NOPREFIX |DT_END_ELLIPSIS |DT_SINGLELINE |DT_VCENTER |DrawTextBiDiModeFlagsReadingOnly() ); StatusBar->Canvas->Font->Style = OldFontStyle; StatusBar->Canvas->Brush->Color = OldBrushColor; } }
In Listing 5.4 we can see that the rendering of the button description into the third (Panel->Index == 2) status bar panel involves four steps: 1. First we save the current StatusBar->Canvas properties that we are going to change, and then we assign the values to the StatusBar->Canvas that we require. 2. We modify the Rect representing the panel’s region so that we have a 2-pixel border on both sides of the panel. 3. We draw the Panel->Text onto StatusBar->Canvas using the DrawRect() function. We align it to the left (DT_LEFT) and center it vertically (DT_VCENTER) within the PanelRect. In addition, we specify that Panel->Text should be drawn as a single line (DT_SINGLELINE) and that an ellipsis should be displayed if there is not enough room to draw the whole string (DT_END_ELLIPSIS). We also turn off the processing of prefix characters (DT_NOPREFIX) and use the TControl’s DrawTextBiDiModeFlagsReadingOnly() function to see if the DT_RTLREADING flag should be set. 4. Finally, we reset the StatusBar->Canvas properties that we modified to their original values. The DrawText() function is a powerful tool for rendering text onto a destination Canvas, and the formatting flags make it easy to customize how the text will appear. We use this function several times throughout the chapter, so it is worthwhile to familiarize yourself with it. TECHNIQUES
Hints are a good way to augment your interface and provide additional information or prompts to the user. The ShowHint property of a control determines whether or not the string in the control’s Hint property will appear when the mouse is paused over the control. Optionally, you
USER INTERFACE PRINCIPLES AND
Using Hints
5
07 9721 ch5
11/13/00
244
9:50 AM
Page 244
C++Builder 5 Essentials PART I
can pass this responsibility to the control’s parent by setting ParentShowHint to true. The control’s hint will be shown only if the parent allows hints to appear. This can sometimes make coding simpler and also makes it easy to turn hints on or off globally. Using a hint that pops up when the mouse is paused over a control is perhaps the most common form of displaying hints. A hint can consist of a short hint, a long hint, or both. To create a hint that contains both a short hint and a long hint, use the vertical bar character (|) in the hint text to separate the short hint from the long hint, with the short hint appearing first. For example AnsiString MyHint = “Short Hint|This is a Long Hint”;
Note that the terms Short Hint and Long Hint indicate the position of the hint within the whole hint string and do not refer to their lengths. The terms originate from the their use. The short hint is the first part of a hint and normally is used in a pop-up hint. The second part of the hint, the long hint, is normally more descriptive and displayed in a status bar. Because of these uses, the short hint will generally be short and the long hint will generally be longer. Only the short hint is shown when a pop-up hint is displayed. The long hint is passed to TApplication’s OnHint event. From there it is possible to assign the long hint to another control for display, for example to a status bar. To have only a short hint, include the | character at the end of the string; otherwise, the short hint will also be used as a long hint. To have only a long hint, start your string with the | character. To retrieve the short hint or long hint from a hint string, use the global GetShortHint() and GetLongHint() functions, respectively, passing your hint as the single argument. has several properties that allow you to customize how hints appear in your application, how long they take to appear, and how long they appear. The properties of interest are shown in Table 5.5.
TApplication
TABLE 5.5
TApplication Properties for Hints
Property
Use
Hint
Contains the long hint of a control when the mouse passes over it. Specifies the color of the hint box. By default this is the system color clInfoBk (typically a pale yellow color), but you may assign any color to it. Specifies how long a pop-up hint will stay visible, provided the mouse remains over the control. The default is 2500 milliseconds. You can assign a different value, also in milliseconds.
HintColor
HintHidePause
07 9721 ch5
11/13/00
9:50 AM
Page 245
User Interface Principles and Techniques CHAPTER 5
TABLE 5.5
245
Continued
Property
Use
HintPause
Specifies the time that must elapse before a control’s hint is displayed when the mouse is stationary above the control. The default is 500 milliseconds. You can assign a different value, also in milliseconds. Specifies whether or not shortcut information in a TCustomAction-derived object (an action object) is displayed in parentheses after the string specified in the action’s Hint property. For example, if an action called TCopyAction had a hint of “Copy” and a ShortCut of Ctrl+C, then the hint for the action would be displayed as “Copy (Ctrl+C)”. Specifies the time to wait before activating another hint if a hint has already been shown. The default is 500 milliseconds. You may assign a different value, also in milliseconds. Specifies whether or not hints are enabled for the entire application. If false, no hints will be shown. The default is true.
HintShortCuts
HintShortPause
ShowHint
In addition to the properties shown in Table 5.5, TApplication also provides the ActivateHint() method to force a hint to be displayed. It is declared in $(BCB)\Include\Vcl\Forms.hpp as void __fastcall ActivateHint(const Windows::TPoint& CursorPos);
To use the function, simply pass the current position of the mouse cursor using the global Mouse object’s CursorPos property. TApplication then shows the hint for the control under the cursor. For example, you would write Application->ActivateHint(Mouse->CursorPos);
provides the HintFont property that allows you to customize the font used to display pop-up hints. It should be apparent then that you can be reasonably flexible with hints in your application.
TScreen
5 TECHNIQUES
In the MiniCalculator program, a separate hint is shown for each panel in the status bar. This presents two problems. TStatusPanels do not have their own Hint properties because they represent only a region on the Canvas of TStatusBar. Only TStatusBar itself has a hint. To
USER INTERFACE PRINCIPLES AND
Using Hints Manually
07 9721 ch5
11/13/00
246
9:50 AM
Page 246
C++Builder 5 Essentials PART I
display a different hint for each panel, the position of the mouse cursor within TStatusBar must be determined and the Hint property of TStatusBar set appropriately. Additionally, when a hint is shown for a control, it is not shown again until the mouse leaves and then re-enters the control. This means that if the mouse enters the status bar on the first panel and a hint is displayed, moving the mouse so that it is over the second panel will not result in a hint being displayed. The mouse must be first moved out of the status bar and then back into the status bar at the required panel. If we want a hint to be shown for each panel without leaving the panel, we must force the hint of TStatusBar to be reshown. In the MiniCalculator program, we do this by implementing the TStatusBar’s OnMouseMove event. Listing 5.5 shows one possible implementation. LISTING 5.5
Implementing TStatusBar’s OnMouseMove Event to Allow Separate Hints for Each Status Bar Panel void __fastcall TMainForm::StatusBar1MouseMove(TObject* Sender, TShiftState Shift, int X, int Y) { int BorderWidth = StatusBar1->BorderWidth; TRect Panel0(StatusBar1->ClientOrigin.x + BorderWidth + 1, // Left StatusBar1->ClientOrigin.y + 3 + BorderWidth, // Top StatusBar1->ClientOrigin.x + BorderWidth + StatusBar1->Panels->Items[0]->Width - 1,
// Right
StatusBar1->ClientOrigin.y + 3 + StatusBar1->Height - 1);
// Bottom
TRect Panel1(Panel0.Right + 2 + 1, Panel0.Top, Panel0.Right + 2 + StatusBar1->Panels->Items[1]->Width - 1, Panel0.Bottom);
// Left // Top is same
TRect Panel2(Panel1.Right + 2 + 1, Panel0.Top, Panel1.Right + 2 + StatusBar1->Panels->Items[2]->Width - 1, Panel0.Bottom);
// Left // Top is same
// Right // Bottom is same
// Right // Bottom is same
07 9721 ch5
11/13/00
9:50 AM
Page 247
User Interface Principles and Techniques CHAPTER 5
LISTING 5.5
247
Continued
// See where the mouse is and then show the correct hint :-) // Use the WinAPI function to see if the cursor is in any of our Rects. // If it is, set the appropriate hint. Otherwise StatusBar1->Hint = “” //BOOL PtInRect( // CONST RECT *lprc, // POINT pt // );
// address of structure with rectangle // structure with point
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
if(PtInRect(&Panel0, Mouse->CursorPos)) { if(EnableKeyboardInput) { StatusBar1->Hint = “Click to Disable Keyboard Intput|”; } else { StatusBar1->Hint = “Click to Enable Keyboard Input|”; } if(StatusBar1->Tag != 0) { StatusBar1->Tag = 0; Application->ActivateHint(); } } else if(PtInRect(&Panel1, Mouse->CursorPos)) { StatusBar1->Hint = “Keyboard Short Cut|”; if(StatusBar1->Tag != 1) { StatusBar1->Tag = 1; Application->ActivateHint(); } } else if(PtInRect(&Panel2, Mouse->CursorPos)) { StatusBar1->Hint = “Button Function|”; if(StatusBar1->Tag != 2) { StatusBar1->Tag = 2; Application->ActivateHint(); }
07 9721 ch5
11/13/00
248
9:50 AM
Page 248
C++Builder 5 Essentials PART I
LISTING 5.5
Continued
} else { // No Hint StatusBar1->Tag = StatusBar1->Panels->Count; StatusBar1->Hint = “|”; } }
Of note in Listing 5.5 is the creation of a TRect structure to represent each region for the status bar panels. For each region, we include only the area within each panel’s bevel (we don’t include the bevel) and do not include the 2-pixel spacing between the panels. To refresh your memory regarding the regions of a status panel, it may be helpful to look again at Figure 5.2. We then use the WinAPI PtInRect() function to see if the mouse cursor is in any of the panels. PtInRect() is declared as BOOL PtInRect( CONST RECT *lprc, POINT pt );
// The address of a TRect structure // A TPoint structure
You simply pass the address of the TRect that you want to see if the mouse is in and pass the mouse position as a TPoint structure. If the mouse is in the TRect representing the panel, and this is the first time that the mouse has moved into the panel, StatusBar1’s Hint property is set to the correct string and TApplication’s ActivateHint() method is called to display the hint. Calling ActivateHint() forces an immediate display of the hint. This means there is no pause as there would be for normal hints. In some situations this is what you require, but in our case a pause before displaying the hint would be more appropriate. We need to implement some helper functions to allow us to do this. First it is important to point out the use of TStatusBar’s Tag property in determining whether a hint has already been displayed for a given panel. When an OnMouseMove event occurs inside a panel’s region, a hint is forced only if this is the first OnMouseMove event in that panel. This ensures correct behavior; a hint is displayed only once for a control when the mouse remains in that control. To show the hint again, the mouse must first leave the control. In this case, the mouse must only leave the panel. This will result in a different value being assigned to the StatusBar1’s Tag property. The index of each panel is assigned to StatusBar1’s Tag property on the first call to the OnMouseMove handler. When the mouse leaves the status bar control and then re-enters, the value of the Tag property is immaterial, because in this case we do not need to force a hint to be displayed. On entering the control and there being a hint to display, the
07 9721 ch5
11/13/00
9:50 AM
Page 249
User Interface Principles and Techniques CHAPTER 5
249
current hint will be shown. It is therefore important to ensure that the Hint property of TStatusBar is set to “” or “|”. Both symbolize empty hint strings. Note also that if an OnMouseMove event occurs on the status bar but not in any of the panels’ hint regions, the Tag property is set to the number of panels in the status bar. There definitely will not be a panel with this index. This allows the hint to be redisplayed if the mouse leaves the panel but not the status bar and does not enter any other panels. If the BorderWidth property of the status bar is large, leaving and re-entering a panel in this way will be noticeable. Displaying a hint after a pause, as is normally done, is more complex. To do this we use the WinAPI SetTimer() function to install a callback function that lets us know when the desired time period has elapsed. When it has, the hint is displayed. Several functions are required. The additional declarations needed in the form’s header file are shown in Listing 5.6. LISTING 5.6
Additional Declarations Required to Allow Delayed Manual Hint Display
UINT HintTimerHandle; static void CALLBACK HintTimerCallback(HWND Wnd, UINT Msg, UINT TimerID, DWORD Time); void __fastcall HintTimerExpired(); void __fastcall DisplayHint(int Pause); void __fastcall StopHintTimer();
The callback function, HintTimerCallback(), is made a static function in the form’s class declaration. This is because a pointer to this function will be passed to the WinAPI SetTimer() method, and it expects a normal function pointer. A variable HintTimerHandle is also declared to store the handle returned from the SetTimer() function so that the timer can be stopped when it is not needed. The implementation of these functions is shown in Listing 5.7. LISTING 5.7
Implementation of Manual Delayed Hint Helper Functions
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
//---------------------------------------------------------------------------// void CALLBACK TMainForm::HintTimerCallback(HWND Wnd, UINT Msg, UINT TimerID, DWORD Time) { // Add a bit of type safety TObject* VCLObject = reinterpret_cast(Wnd); TForm1* Form1Object = dynamic_cast(VCLObject); if(Form1Object) Form1Object->HintTimerExpired();
07 9721 ch5
11/13/00
250
9:50 AM
Page 250
C++Builder 5 Essentials PART I
LISTING 5.7
Continued
} //---------------------------------------------------------------------------// void __fastcall TMainForm::HintTimerExpired() { StopHintTimer(); Application->ActivateHint(Mouse->CursorPos); } //---------------------------------------------------------------------------// void __fastcall TMainForm::DisplayHint(int Pause) { StopHintTimer(); HintTimerHandle = SetTimer(this, 0, Pause, reinterpret_cast<TIMERPROC>(HintTimerCallback)); if(HintTimerHandle == 0) Application->CancelHint(); } //---------------------------------------------------------------------------// void __fastcall TMainForm::StopHintTimer() { if(HintTimerHandle != 0) { KillTimer(this, HintTimerHandle); HintTimerHandle = 0; } } //---------------------------------------------------------------------------//
The DisplayHint() function is where it all starts. This is called in the OnMouseDown event handler instead of Application->ActivateHint(), with Application->HintPause passed as the single argument. All the lines in Listing 5.5 that are as follows Application->ActivateHint();
should be replaced by DisplayHint(Application->HintPause);
We use Application->HintPause to retrieve the current delay used by the application. This ensures consistency with other hints. In some cases, though, you may want to pass a different value.
07 9721 ch5
11/13/00
9:50 AM
Page 251
User Interface Principles and Techniques CHAPTER 5
251
When DisplayHint() is called, it first stops any previous timer that is in use. It then sets a new timer, passing as arguments the this pointer, so that the HintTimerExpired()function can be called from the static callback function; the required delay in milliseconds (Pause); and a pointer to the callback function to receive the message that the timer has finished. Notice that the callback function pointer needs to be cast to TIMERPROC. We use reinterpret_cast for this because we are dealing with function pointers. If we are unsuccessful in setting the timer (that is, SetTimer() returns 0), then we cancel any current hint by calling Application-> CancelHint(). Without this, we cannot display our new hint. When the timer expires, our callback function is called. It has four parameters, but we are interested in only the first, the HWND Wnd parameter. This is where we stored our this pointer in the call to SetTimer(). With this we can call the HintTimerExpired() function to actually display the hint. Because the Wnd is not a pointer, we first cast it to a TObject* using reinterpret_cast. Then we try to dynamic_cast the TObject* to a TForm1*. This gives us a chance to check if the pointer is valid before we dereference it. Dereferencing an invalid pointer is an access violation and something we would like to avoid. Finally, the HintTimerExpired() function stops the current hint timer by calling StopHintTimer() and then displays the hint using Application->ActivateHint(). The StopHintTimer() function simply checks that there is a timer to stop (by checking the FHintTimerHandle variable). If so, it stops it with the WinAPI function KillTimer(). It then sets the FHintTimerHandle variable to 0. When the mouse passes over the panels of the status panel, the hints for each panel appear just as ordinary hints do.
Using Customized Hints Sometimes you will want to display custom hints to the user. In the MiniCalculator program, a pop-up hint is displayed when the mouse moves over the Memory Recall button. The hint displays the contents of the memory. We want this hint to look like the main display of the calculator (refer to Figure 5.1 to see the MiniCalculator interface). To create a custom hint, you must derive a new hint window class from THintWindow. You must then assign the TMetaClass* of the class type (using the __classid operator) to the global HintWindowClass TMetaClass* variable. First we will look at creating a THintWindow-derived class. Table 5.6 shows the virtual methods provided by THintWindow and gives the purpose of each. For convenience, the first line in each entry in the Purpose column shows the function’s return type and parameter list.
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
07 9721 ch5
11/13/00
252
9:50 AM
Page 252
C++Builder 5 Essentials PART I
TABLE 5.6
The virtual Methods of THintWindow
Method
Purpose
CreateParams()
void CreateParams(TCreateParams& Params)
Override this method to control the type of window created to represent the hint. Paint()
void Paint(void)
Override this method to control how the hint is rendered to the screen. Use the hint window’s ClientRect property to get the bounds of the area to be painted and the hint window’s Canvas property to perform the rendering. CalcHintRect()
Windows::TRect CalcHintRect(int MaxWidth, const AnsiString AHint, void* AData)
Override this method to set the desired size of the hint to be displayed. The three parameters can be used to make any required calculations. The TRect structure returned by this method indicates the size of the client area of the hint window. ActivateHint()
void ActivateHint(const Windows::TRect& Rect, const AnsiString AHint)
Override this method to control where on the screen the hint is displayed. As implemented in THintWindow, ActivateHint() displays the hint window at the coordinates provided by the Rect parameter if it appears on the screen. Otherwise, Rect is modified so that the nearest onscreen position is used. The ActivateHint() method also sets the Caption property of THintWindow. The Caption property is used in the CalcHintRect() method to determine how big the hint window needs to be and is used in the Paint() method to render the hint text (Caption) onto the hint window.
07 9721 ch5
11/13/00
9:50 AM
Page 253
User Interface Principles and Techniques CHAPTER 5
TABLE 5.6
253
Continued
Method
Purpose
ActivateHintData()
void ActivateHintData(const Windows::TRect& Rect, const AnsiString AHint, void* AData)
Override this method to use the additional void* parameter AData. Otherwise, this method is similar to the ActivateHint() method. As implemented in THintWindow, the AData parameter is ignored. IsHintMsg()
bool IsHintMsg(tagMSG& Msg)
Override this method to specify any messages that require the hint window to be hidden. As implemented in THintWindow, this occurs for all mouse, keyboard, command, and activation messages. The global Application object calls IsHintMsg() to check messages while the hint window is being displayed. If it returns true, the hint window is hidden. By overriding the virtual methods inherited from THintWindow, we can customize the appearance of our hint class. The definition for the THintWindow-derived class used in the MiniCalculator program is shown in Listing 5.8. The class is called TCalculatorHintWindow. LISTING 5.8
Class Definition for TCalculatorHintWindow
class TCalculatorHintWindow : public THintWindow { typedef THintWindow inherited; protected: virtual void __fastcall Paint(void); virtual void __fastcall CreateParams(TCreateParams &Params); public: __fastcall virtual TCalculatorHintWindow(Classes::TComponent* AOwner);
5
virtual void __fastcall ActivateHint(const Windows::TRect& Rect, const AnsiString AHint); TECHNIQUES
USER INTERFACE PRINCIPLES AND
virtual void __fastcall ActivateHintData(const Windows::TRect& Rect, const AnsiString AHint,
07 9721 ch5
11/13/00
254
9:50 AM
Page 254
C++Builder 5 Essentials PART I
LISTING 5.8
Continued void* AData);
virtual Windows::TRect __fastcall CalcHintRect(int MaxWidth, const AnsiString AHint, void* AData); virtual bool __fastcall IsHintMsg(tagMSG& Msg); __property BiDiMode ; __property Caption ; __property Color ; __property Canvas ; __property Font ; public: inline __fastcall virtual ~TCalculatorHintWindow(void) { } public: inline __fastcall TCalculatorHintWindow(HWND ParentWindow) : THintWindow(ParentWindow) { } };
Of the methods shown in the declaration, only CreateParams(), Paint(), and CalcHintRect() are modified from the inherited methods. The remainder simply call the inherited THintWindow methods in their implementations. Listing 5.9 shows the implementation of the TCalculatorHintWindow class. LISTING 5.9
Implementation of TCalculatorHintWindow
//---------------------------------------------------------------------------// // CONSTRUCTOR // //---------------------------------------------------------------------------// __fastcall TCalculatorHintWindow::TCalculatorHintWindow(Classes::TComponent* AOwner) : THintWindow(AOwner) { Canvas->Font->Name = “Arial”; Canvas->Font->Color = clBlack; }
07 9721 ch5
11/13/00
9:50 AM
Page 255
User Interface Principles and Techniques CHAPTER 5
LISTING 5.9
255
Continued
//---------------------------------------------------------------------------// // CreateParams // //---------------------------------------------------------------------------// void __fastcall TCalculatorHintWindow::CreateParams(TCreateParams &Params) { inherited::CreateParams(Params); Params.Style = WS_POPUP; Params.WindowClass.style = Params.WindowClass.style | CS_SAVEBITS; if(NewStyleControls) // Check the global NewStyleControls variable { Params.ExStyle = WS_EX_TOOLWINDOW; // Hint window is a Tool window AddBiDiModeExStyle(Params.ExStyle); } } //---------------------------------------------------------------------------// // Paint // //---------------------------------------------------------------------------// void __fastcall TCalculatorHintWindow::Paint(void) { TRect Rect = ClientRect; Canvas->Brush->Color = clBlack; Canvas->FillRect(Rect); Rect.Left += 4; Rect.Top += 4; Rect.Right -= 4; Rect.Bottom -= 4; Frame3D(Canvas, Rect, clBtnShadow, clBtnHighlight, 1); Canvas->Brush->Color = TColor(0xB4CDBB); Canvas->FillRect(Rect); Rect.Left += 1; Rect.Top += 5; Rect.Right -= 1; Rect.Bottom -= 1;
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
07 9721 ch5
11/13/00
256
9:50 AM
Page 256
C++Builder 5 Essentials PART I
LISTING 5.9
Continued
DrawText( Canvas->Handle, Caption.c_str(), -1, &Rect, DT_RIGHT |DT_NOPREFIX |DT_SINGLELINE |DrawTextBiDiModeFlagsReadingOnly() ); } //---------------------------------------------------------------------------// // CalcHintRect // //---------------------------------------------------------------------------// Windows::TRect __fastcall TCalculatorHintWindow::CalcHintRect(int MaxWidth, const AnsiString AHint, void* AData) { TRect Rect(0, 0, MaxWidth, 0); DrawText( Canvas->Handle, AHint.c_str(), -1, &Rect, DT_CALCRECT |DT_SINGLELINE |DT_NOPREFIX |DrawTextBiDiModeFlagsReadingOnly() ); // We want a minimum width for our hint: at least 3 times the height. if((Rect.Right - Rect.Left) < 3*(Rect.Bottom - Rect.Top)) { Rect.Right = Rect.Left + 3*(Rect.Bottom - Rect.Top); } Rect.Right += 20; Rect.Bottom += 12; return Rect; } //---------------------------------------------------------------------------// // ActivateHint //
07 9721 ch5
11/13/00
9:50 AM
Page 257
User Interface Principles and Techniques CHAPTER 5
LISTING 5.9
257
Continued
//---------------------------------------------------------------------------// void __fastcall TCalculatorHintWindow::ActivateHint(const Windows::TRect& Rect, const AnsiString AHint) { inherited::ActivateHint(Rect, AHint); } //---------------------------------------------------------------------------// // ActivateHintData // //---------------------------------------------------------------------------// void __fastcall TCalculatorHintWindow::ActivateHintData(const Windows::TRect& Rect, const AnsiString AHint, void* AData) { inherited::ActivateHintData(Rect, AHint, AData); } //---------------------------------------------------------------------------// // IsHintMsg // //---------------------------------------------------------------------------// bool __fastcall TCalculatorHintWindow::IsHintMsg(tagMSG& Msg) { return inherited::IsHintMsg(Msg); } //---------------------------------------------------------------------------//
We will now take a more detailed look at some of the implementation issues of the code presented in Listing 5.9. We begin with the constructor. The constructor is straightforward. We simply set the font style to Arial and the font color to clBlack. This makes the hint window font the same as that of MiniCalculator’s main display. The overridden CreateParams() method is almost the same as THintWindow’s method. The only difference is that we make the window style WS_POPUP only. THintWindow also adds WS_BORDER to the style. We do not want a border around our window, so we use only WS_POPUP. The remainder of the code is straightforward. The CS_SAVEBITS flag added to the WindowClass.style field of Params indicates that Windows should save the area of screen obscured by the hint as a bitmap and then draw it back when the hint disappears. This means that no WM_PAINT messages need to be sent to any windows CreateParams()
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
07 9721 ch5
11/13/00
258
9:50 AM
Page 258
C++Builder 5 Essentials PART I
covered by the hint. With small windows such as hints, this is practical. Finally, we check the value of the global NewStyleControls variable. If it is true (for Win9x and above it is), then we add the necessary extended styles, WS_EX_TOOLWINDOW to indicate that the hint window will be a tool window, and the necessary flags for bidirectional support, using the member function AddBiDiModeExStyle(). For more information about this function, refer to the C++Builder online help. The real work of the class is carried out in the CalcHintRect() and Paint() methods. In the CalcHintRect() method we first create a TRect variable, called Rect, setting its Right property to the value of the MaxWidth parameter. We then pass the address of Rect to the WinAPI DrawText() function, along with the AHint string and the Handle of our window’s Canvas. The function call is DrawText( Canvas->Handle, // Our Canvas Handle AHint.c_str(), // Returns a NULL terminated version of AHint -1, // Indicates the string is NULL terminated &Rect, // The TRect we want to determine the size of DT_CALCRECT // Function call calculates the TRect required |DT_SINGLELINE // We want a single line |DT_NOPREFIX // Don’t process prefix characters (such as &) |DrawTextBiDiModeFlagsReadingOnly() );
The DT_CALCRECT flag indicates that no text is actually drawn. Rather, the TRect required to accommodate the text is defined, based on the MaxWidth, the AHint string, and the Canvas->Handle. Once the TRect required to accommodate the hint has been calculated, we check to see if it has the correct aspect ratio. We want the hint to appear as a smaller version of the main display, so we want the width to be at least three times the height. In addition, we want extra room to the left side of and above the text. We therefore add a constant to the Bottom and Right of the TRect before returning the value. This will allow us to offset the text to the right and down within the hint window. The code is if((Rect.Right - Rect.Left) < 3*(Rect.Bottom - Rect.Top)) { Rect.Right = Rect.Left + 3*(Rect.Bottom - Rect.Top); } Rect.Right += 20; Rect.Bottom += 12; return Rect;
07 9721 ch5
11/13/00
9:50 AM
Page 259
User Interface Principles and Techniques CHAPTER 5
259
The ratio and constant values are arbitrary and are based on what looks right. Finally, we paint our hint window in the overridden Paint() method. The functionality of this method can be broken into two parts. The first part draws the background for the hint window, and the second renders the hint text. The code for the first part is TRect Rect = ClientRect; Canvas->Brush->Color = clBlack; Canvas->FillRect(Rect); Rect.Left += 4; Rect.Top += 4; Rect.Right -= 4; Rect.Bottom -= 4; Frame3D(Canvas, Rect, clBtnShadow, clBtnHighlight, 1); Canvas->Brush->Color = TColor(0xB4CDBB); Canvas->FillRect(Rect);
First we retrieve the TRect that bounds the area we can draw on. We paint it black, then 4 pixels in from the edge we use the VCL Frame3D() utility function to draw a lowered bevel. We then fill the area enclosed by the frame with a color to match MiniCalculator’s main display. Finally, we draw the text onto the hint window using this code: Rect.Left += 1; Rect.Top += 5; Rect.Right -= 1; Rect.Bottom -= 1; DrawText( Canvas->Handle, Caption.c_str(), -1, &Rect, DT_RIGHT // Right align the text |DT_NOPREFIX // No prefixes |DT_SINGLELINE // Single line |DrawTextBiDiModeFlagsReadingOnly() );
We specify the Rect variable that the text will drawn into. We leave a 1-pixel border on the left, right, and bottom and a 5-pixel border at the top. We then use the WinAPI DrawText() function to draw the text on the hint window’s Canvas. We use the same flags as we did for the
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
07 9721 ch5
11/13/00
260
9:50 AM
Page 260
C++Builder 5 Essentials PART I
method, except that we have removed the DT_CALCRECT flag (we are not calculating, we are drawing), and we have added the DT_RIGHT flag to align the text to the right of Rect. Figure 5.3 shows what the hint window looks like in use. Incidentally, the number displayed in the hint window in Figure 5.3 is not π but rather 22 divided by 7, often used as a rough approximation for π. CalcHintRect()
5 pixels 32 pixels 16 pixels 9 pixels 1 pixel 109 pixels
1 pixel
1 pixel 129 pixels
FIGURE 5.3 The TCalculatorHintWindow custom hint window.
To use the TCalculatorWindow hint window class we must assign its type to the global HintWindowClass variable as follows: HintWindowClass = __classid(TCalculatorHintWindow);
This is discussed in the next section.
Using TApplication’s OnHint Event The TApplicationEvents component (found on the Additional tab of the Component Palette) makes it easy to respond to TApplication events and notably to TApplication’s OnHint event. When TApplication’s OnHint event is fired, TApplication’s Hint property contains the long hint string of the Hint property of the control that caused the event to be fired. Remember that if no | character is used to separate the hint into a short hint and a long hint, then the string representing the control’s hint will be used for both. When implementing TApplication’s OnHint event handler, reading the value of this long hint is straightforward. You simply read the value of the global Application object’s Hint property, for example
07 9721 ch5
11/13/00
9:51 AM
Page 261
User Interface Principles and Techniques CHAPTER 5
261
AnsiString CurrentLongHint = Application->Hint;
In the MiniCalculator program, hint handling is centralized in the TApplication’s OnHint event. Of particular note in the MiniCalculator program is that we want to be able to display two pieces of information in our status bar, and we want to use a different hint window class for some of the pop-up hints. First we look at the need to display information in more than one panel of the status bar. To do this we need to separate the long hint we obtain from Application->Hint into two strings. The first string should contain the keyboard shortcut for each button in the calculator, and the second string should contain a description of the button. We have already seen how the | character can be used to separate short and long hints. We can also use this to separate the long hint into two halves. We simply add a | to the long hint at the position we want the string split. For example, the = key of MiniCalculator has no short hint, but its long hint is in two parts; “Enter” and “Equals”. “Enter” signifies the keyboard shortcut for the button, and “Equals” is the button’s description. The Hint property for this button is “|Enter|Equals”
This is of the format “ShortHint|LongHint1|LongHint2”. Only the string “Enter|Equals” will appear in the Application->Hint property. We can therefore use the GetShortHint() global function to retrieve the first long hint and the GetLongHint() global function to retrieve the second long hint. If you do not want either a first long hint or a second long hint, it is important to add a | character to the string to signify this. This is because GetShortHint() and GetLongHint() will return the whole string if the | character is not found. For example, the following string “||LastLongHint”
will result in Application->Hint containing “|LastLongHint”. In turn, when we call GetShortHint() the result is “”, and when we call GetLongHint() the result is “LastLongHint”. Similarly, if you want to have only a first long hint, you must write your hint string in the format “|FirstLongHint|”. To place the first long hint in the second status bar panel and place the last long hint in the third status bar panel requires the following code in the Application->OnHint handler: StatusBar1->Panels->Items[1]->Text = GetShortHint(Application->Hint); StatusBar1->Panels->Items[2]->Text = GetLongHint(Application->Hint);
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
We now turn our attention to the need to display different pop-up hint windows for different controls in the MiniCalculator program. In the previous section, “Using Customized Hints,” we saw how to develop our own hint windows. We also saw how to assign our hint window class to the global HintWindowClass variable using the __classid operator. To switch between hint
07 9721 ch5
11/13/00
262
9:51 AM
Page 262
C++Builder 5 Essentials PART I
window classes for different hints, we simply read the value of Application->Hint in the Application->OnHint handler and assign the HintWindowClass variable accordingly. Listing 5.10, the full implementation of the Application->OnHint handler as it appears in the MiniCalculator program, shows this. LISTING 5.10
Implementation of Application->OnHint Using TApplicationEvents->
OnHint void __fastcall TMainForm::ApplicationEvents1Hint(TObject *Sender) { if(Application->Hint==”Ctrl+V|Memory Recall” || Application->Hint==”LCD”) { Application->HintHidePause = 10000; // 10 seconds delay HintWindowClass = __classid(TCalculatorHintWindow); } else { Application->HintHidePause = HintDisplayTime; // Set in constructor HintWindowClass = __classid(THintWindow); } if(Application->Hint != “LCD”) { StatusBar1->Panels->Items[1]->Text = GetShortHint(Application->Hint); StatusBar1->Panels->Items[2]->Text = GetLongHint(Application->Hint); } }
We want to show our custom hint window when the user pauses over either the Memory Recall button (SpeedButtonMemoryRecall) or the LCD screen display (LCDScreen). We also want a longer delay to allow the reader to read the number that the hint will contain, because it may be rather complex. We use the long hint of the controls to see which control generated the hint. In the case of LCDScreen, we do not want this long hint displayed, so we do not assign the hint to the status panel if the hint is equal to “LCD”, the long hint of the LCDScreen TLabel control. The HintDisplayTime variable is set in the constructor to the Application’s HintHidePause value. This allows Application->HintHidePause to be restored to its original value when the standard hint window is displayed. Finally, we will look at how the short hints are assigned to the Hint property of these two controls. The method for both is similar, so we will look at only the Hint property of LCDScreen. We want to show a pop-up hint only if the width of the number being displayed is greater than the width of LCDScreen. We check every time LCDScreen is updated. This occurs in the UpdateLCDScreen() member function. Listing 5.11 shows the implementation.
07 9721 ch5
11/13/00
9:51 AM
Page 263
User Interface Principles and Techniques CHAPTER 5
LISTING 5.11
263
Implementation of UpdateLCDScreen()
void __fastcall TMainForm::UpdateLCDScreen(const AnsiString& NewNumber) { int NumberWidth = LCDScreen->Canvas->TextWidth(NewNumber); if(Operation == coComplete) { if( (NumberWidth >= LCDScreen->Width) && (LCDScreen->Alignment == taRightJustify) ) { LCDScreen->Alignment = taLeftJustify; } else if( (NumberWidth < LCDScreen->Width) && (LCDScreen->Alignment != taRightJustify) ) { LCDScreen->Alignment = taRightJustify; } } else if(LCDScreen->Alignment != taRightJustify) { LCDScreen->Alignment = taRightJustify; } LCDScreen->Caption = NewNumber; int pos = LCDScreen->Hint.Pos(“|”); int length = LCDScreen->Hint.Length(); AnsiString LCDScreenHint = LCDScreen->Hint.SubString(pos, length-pos+1); LCDScreen->Hint = NewNumber + LCDScreenHint; if(NumberWidth >= LCDScreen->Width) LCDScreen->ShowHint = true; else LCDScreen->ShowHint = false; }
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
UpdateLCDScreen() is called each time we want to change the value being displayed on the screen, with the new number required (as an AnsiString) being passed by const reference. We first determine the width in pixels that the new number will require when drawn onto the LCDScreen’s Canvas. Based on this information, the current operation being performed by the calculator, and the current LCDScreen->Width, we set the required justification. We then assign the NewNumber to the Caption property of LCDScreen. The remainder of the code does two things. First it prepends the new number to the Hint property of LCDScreen so that Hint becomes
07 9721 ch5
11/13/00
264
9:51 AM
Page 264
C++Builder 5 Essentials PART I “NewNumber|LCD”
Finally, depending on the width of the new number and the width of LCDScreen, we set LCDScreen’s ShowHint property. If the new number is wider than LCDScreen, we set LCDScreen->ShowHint to true; otherwise we suppress the display of a hint by setting LCDScreen->ShowHint to false.
Enhancing Usability Through Input Focus Control Sensible control of user input and input focus is one way to ensure that your program is as easy to use as possible. Input focus determines which user interface control receives attention from the standard input devices. Input received from serial and parallel devices that are not keyboards, mice, or other pointing devices is not included. While such devices can be associated with input focus, this is not the norm for user interface controls. A control is said to have input focus if it can respond to being manipulated by one of the standard input devices. You can do two things when a control has input focus: respond to the input or move the focus to another control that can respond to the input. We will briefly look at and give an example of each situation.
Responding to Input The MiniCalculator program responds to the keyboard and to the mouse. This functionality can be switched on or off by the user. The program also gives feedback to the user to indicate when the program can respond to keyboard input. To respond to input from the keyboard, the MainForm’s OnKeyDown and OnKeyUp events are handled. Two functions, CalculatorKeyDown() and CalculatorKeyUp(), are used for the implementation of the respective OnKeyDown and OnKeyUp events. These functions are assigned to MainForm’s events in the set method of the EnableKeyboardInput property. When MainForm is created, EnableKeyboardInput is set to true, which calls the SetEnableKeyboardInput() method. The implementation is shown in Listing 5.12, which is the same as that shown previously as Listing 5.3 and is repeated here for convenience. LISTING 5.12
Implementation of SetEnableKeyboardInput()
void __fastcall TMainForm::SetEnableKeyboardInput(bool NewEnableKeyboardInput) { if(EnableKeyboardInput != NewEnableKeyboardInput) { FEnableKeyboardInput = NewEnableKeyboardInput; if(EnableKeyboardInput) { OnKeyDown = CalculatorKeyDown;
07 9721 ch5
11/13/00
9:51 AM
Page 265
User Interface Principles and Techniques CHAPTER 5
LISTING 5.12
265
Continued
OnKeyUp = CalculatorKeyUp; StatusBar1->Panels->Items[1]->Width = 60; } else { OnKeyDown = 0; OnKeyUp = 0; StatusBar1->Panels->Items[1]->Width = 0; } StatusBar1->Invalidate(); } }
is initialized to false, ensuring that the body of the first if statement is executed. The lines of interest are FEnableKeyboardInput
OnKeyDown = CalculatorKeyDown; OnKeyUp = CalculatorKeyUp;
Here the events are assigned to the functions that will handle the events. The CalculatorKeyDown() method is shown in Listing 5.13. LISTING 5.13
Implementation of CalculatorKeyDown()
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
void __fastcall TMainForm::CalculatorKeyDown(TObject* Sender, WORD& Key, TShiftState Shift) { switch(Key) { case VK_NUMPAD1 : case ‘1’ : ButtonDown(cb1); ButtonPressNumber(cb1); break; case VK_NUMPAD2 : case ‘2’ : ButtonDown(cb2); ButtonPressNumber(cb2); break; case VK_NUMPAD3 : case ‘3’ : ButtonDown(cb3); ButtonPressNumber(cb3); break; case VK_NUMPAD4 :
07 9721 ch5
11/13/00
266
9:51 AM
Page 266
C++Builder 5 Essentials PART I
LISTING 5.13
Continued
case ‘4’
case VK_NUMPAD5 case ‘5’
case VK_NUMPAD6 case ‘6’
case VK_NUMPAD7 case ‘7’
case VK_NUMPAD8 case ‘8’
case VK_NUMPAD9 case ‘9’
case VK_NUMPAD0 case ‘0’
: ButtonDown(cb4); ButtonPressNumber(cb4); break; : : ButtonDown(cb5); ButtonPressNumber(cb5); break; : : ButtonDown(cb6); ButtonPressNumber(cb6); break; : : ButtonDown(cb7); ButtonPressNumber(cb7); break; : : ButtonDown(cb8); ButtonPressNumber(cb8); break; : : ButtonDown(cb9); ButtonPressNumber(cb9); break; : : ButtonDown(cb0); ButtonPress0(); break;
case VK_DECIMAL : ButtonDown(cbPoint); ButtonPressPoint(); break; case VK_PRIOR
case VK_ADD
: ButtonDown(cbExponent); ButtonPressExponent(); break;
: ButtonDown(cbAdd); ButtonPressOperation(coAdd); break; case VK_SUBTRACT : ButtonDown(cbSubtract); ButtonPressOperation(coSubtract); break; case VK_MULTIPLY : ButtonDown(cbMultiply); ButtonPressOperation(coMultiply);
07 9721 ch5
11/13/00
9:51 AM
Page 267
User Interface Principles and Techniques CHAPTER 5
LISTING 5.13
267
Continued
case VK_DIVIDE
case VK_RETURN
case VK_BACK
case VK_DELETE
break; : ButtonDown(cbDivide); ButtonPressOperation(coDivide); break; : ButtonDown(cbEquals); ButtonPressEquals(); break; : ButtonDown(cbBackspace); ButtonPressBackspace(); break; : if(Shift.Contains(ssCtrl)) { ButtonDown(cbAllClear); ButtonPressAllClear(); } else { ButtonDown(cbClear); ButtonPressClear(); } break;
case ‘C’ : if(Shift.Contains(ssCtrl)) { ButtonDown(cbMemoryAdd); ButtonPressMemoryAdd(); } break; case ‘V’ : if(Shift.Contains(ssCtrl)) { ButtonDown(cbMemoryRecall); ButtonPressMemoryRecall(); } break; } }
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
Like most key down handlers, CalculatorKeyDown() consists of a large switch statement that delegates each key occurrence to a function that deals with the actual processing. First the ButtonDown() method is called, which changes the appropriate button’s Down property to true. This provides the user with visual confirmation of the button he just pressed. Next a function that encapsulates the button’s actual function is called to perform the required processing. The
07 9721 ch5
11/13/00
268
9:51 AM
Page 268
C++Builder 5 Essentials PART I
method is nearly identical to the CalculatorKeyDown() method, with a couple of notable differences. Listing 5.14 shows the implementation of CalculatorKeyUp(). CalculatorKeyUp()
LISTING 5.14
Implementation of CalculatorKeyUp()
void __fastcall TMainForm::CalculatorKeyUp(TObject *Sender, WORD &Key, TShiftState Shift) { switch(Key) { case VK_NUMPAD1 : case ‘1’ : ButtonUp(cb1); break; case VK_NUMPAD2 : case ‘2’ : ButtonUp(cb2); break; case VK_NUMPAD3 : case ‘3’ : ButtonUp(cb3); break; case VK_NUMPAD4 : case ‘4’ : ButtonUp(cb4); break; case VK_NUMPAD5 : case ‘5’ : ButtonUp(cb5); break; case VK_NUMPAD6 : case ‘6’ : ButtonUp(cb6); break; case VK_NUMPAD7 : case ‘7’ : ButtonUp(cb7); break; case VK_NUMPAD8 : case ‘8’ : ButtonUp(cb8); break; case VK_NUMPAD9 : case ‘9’ : ButtonUp(cb9); break; case VK_NUMPAD0 : case ‘0’ : ButtonUp(cb0); break; case VK_DECIMAL : ButtonUp(cbPoint); break; case VK_PRIOR
: // PageUp for Exponent // Button toggles so KeyUp not required break;
07 9721 ch5
11/13/00
9:51 AM
Page 269
User Interface Principles and Techniques CHAPTER 5
LISTING 5.14
269
Continued
case VK_ADD
: ButtonUp(cbAdd); break; case VK_SUBTRACT : ButtonUp(cbSubtract); break; case VK_MULTIPLY : ButtonUp(cbMultiply); break; case VK_DIVIDE : ButtonUp(cbDivide); break; case VK_RETURN case VK_BACK case VK_DELETE
: ButtonUp(cbEquals); break; : ButtonUp(cbBackspace); break; : if(SpeedButtonAllClear->Down) { ButtonUp(cbAllClear); } else { ButtonUp(cbClear); } break;
case ‘C’ : ButtonUp(cbMemoryAdd); break; case ‘V’ : ButtonUp(cbMemoryRecall); break; } }
The first thing to notice is that there is no key up handler for VK_PRIOR (the Page Up key) for the Exponent button. This is because pressing the Exponent button toggles its up/down state. Therefore, we need only handle the key down event, which calls the ButtonPressExponent() method and sets the state of the button according to its current state.
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
Also worth noting is that the key down handler for Ctrl+C (see Listing 5.13) checks whether ssCtrl is contained in the Shift set parameter. However, the key up handler in Listing 5.14 does not. This is because we are interested only in whether or not Ctrl was pressed when C is pressed, not when C is released. Including the check for the key up occurrence can result in the Memory Add button remaining pressed if Ctrl is released before C is released. Obviously this is not desirable.
07 9721 ch5
11/13/00
270
9:51 AM
Page 270
C++Builder 5 Essentials PART I
Finally, we should note that the code used in the key down handler for VK_DELETE (the Delete key)—that is, for the SpeedButtonClear and SpeedButtonAllClear buttons—is distinguished by the presence of the ssCtrl in the Shift parameter (see Listing 5.13). However, in the key up handler (see Listing 5.14), no check is made for the Ctrl key. Instead, the Down property of the SpeedButtonAllClear button is checked to see if it is pressed. If so, it is released; otherwise, the SpeedButtonClear must be Down, in which case it is released.
Moving Input Focus In the MiniCalculator program, the main form (MainForm) must have input focus in order to respond to input from the keyboard. However, in certain situations, input focus can leave the main form. We detect these situations and then return input focus to the main form. Input focus can leave the main form in two circumstances. The first is when the application itself loses focus. We are not concerned about this situation because obviously the main form should lose input focus at this time. The second time the main form can lose focus is when an undocked control gains focus. This will occur when we click, drag, or resize an undocked control. The control, LCDPanel, can be undocked from the main form. Therefore, in the MiniCalculator program, we detect times when LCDPanel is undocked (LCDPanel->Floating == true) and receives input focus. We then call MainForm’s SetFocus() method to return focus to it. For example, if LCDPanel is resized when it is undocked, the main form loses focus. Therefore, in LCDPanel’s OnResize event we write if(LCDPanel->Floating) SetFocus();
to return focus to the main form. We have already seen how we monitor the state of the main form’s input focus in the section, “Using TStatusBar,” earlier in this chapter. A common technique often used in interfaces is to lead the user to complete edit boxes in order. When one is completed, focus is automatically moved to the next one. This makes completing such edit boxes easy. The FromToForm of the FromToUnit in the Focus.bpr project on the CD-ROM that accompanies this book illustrates how this is done. All that is required is that you implement each edit box’s OnKeyUp event. In this example we want the user to enter two 4digit numbers, one in each edit box. When the first edit box is complete, input focus is moved to the second box. When it is complete, input focus is moved to the OK button. If the second edit box is complete but the first is not, then input focus goes back to the first edit box. The implementation for edit box 1 (Edit1) and edit box 2 (Edit2) is shown in Listings 5.15 and 5.16, respectively. LISTING 5.15
Implementation of Edit1KeyUp()
void __fastcall TFromToForm::Edit1KeyUp(TObject* Sender, WORD& Key,
07 9721 ch5
11/13/00
9:51 AM
Page 271
User Interface Principles and Techniques CHAPTER 5
LISTING 5.15
271
Continued TShiftState Shift)
{ if(Edit1->Text.Length() == Edit1->MaxLength) { FinishNumberComplete = true; if(StartNumberComplete) { BitBtnOK->Enabled = true; BitBtnOK->SetFocus(); } else Edit2->SetFocus(); } else { FinishNumberComplete = false; BitBtnOK->Enabled = false; } }
LISTING 5.16
Implementation of Edit2KeyUp()
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
void __fastcall TFromToForm::Edit2KeyUp(TObject* Sender, WORD& Key, TShiftState Shift) { if(Edit2->Text.Length() == Edit2->MaxLength) { StartNumberComplete = true; if(FinishNumberComplete) { BitBtnOK->Enabled = true; BitBtnOK->SetFocus(); } else Edit1->SetFocus(); } else { StartNumberComplete = false; BitBtnOK->Enabled = false; } }
07 9721 ch5
11/13/00
272
9:51 AM
Page 272
C++Builder 5 Essentials PART I
To ensure that the first edit box (Edit1) has focus when FromToForm is first displayed, we set its TabOrder property to 0. We set Edit2’s TabOrder property to 1, BitBtnOK’s TabOrder to 2, and BitBtnCancel’s TabOrder to 3. The TabOrder property makes it easy to control the tab order in groups of controls, and it is worth thinking carefully about what order you use in your interface if tabbing through controls is appropriate. You should run the Focus project and see it working. One thing that should be noted about this simple example is that any character can be input to the edit boxes. We could write code that filters out all characters that are not numbers, but this is not the purpose of this example. An even better solution is to use an edit component that allows you to specify a string of characters that can be filtered in or filtered out of the edit box. Such a component is presented in the “Overriding DYNAMIC Functions” section of Chapter 11, “More Custom Component Techniques.”
Enhancing Usability Through Appearance One of the best ways to improve your user interface is to improve its appearance. This is more than just making your interface pleasant to look at; it is about conveying additional information through the use of symbols, color, layout, and shape. By optimizing the appearance of the user interface for your application, you help make the interface more intuitive and familiar. This has a twofold effect. First, users need less time to learn the interface, both because it is familiar and because unfamiliar elements can be guessed. Second, not only do users need less time to learn the interface, they also are prepared to spend more time using the interface. Coupling these benefits clearly improves the usefulness of your application dramatically. How do you decide on the best way to present the interface of your application to the user? That depends on the type of application you are developing. If you are developing an application that performs the task of a real-world equivalent that is widely used and understood, then your best bet is to use that equivalent as a guide for how your interface should look and act. If there is no obvious real-world equivalent, then you should still try to make your interface as familiar as possible. This usually means that you should make your interface appear as other well-known applications appear. Particular attention should be paid to those applications that perform a similar task or similar kind of task. Consider an example of creating a phone dialer application. How should the interface be designed? For maximum user familiarity, the best approach is to model the interface after a real phone. More users are more likely to be familiar with a phone interface than with an edit box that you simply type a number into. However, the choice is not so simple. You must remember the context in which the user meets the phone interface (that of being a computer application) and the task it is expected to carry out. Your application is a computer program, and in that sense the user will expect certain behavior. For example, when the user clicks on a button, he will expect it to look clicked. No matter how nice an interface is, if it doesn’t act
07 9721 ch5
11/13/00
9:51 AM
Page 273
User Interface Principles and Techniques CHAPTER 5
273
like an interface, it is a waste of time. Also, you must be careful when copying real-world equivalents so that you do not introduce their limitations to your application. For example, creating an interface that looks like a telephone handset is very nice, but if users must use the mouse to click each button to enter a number, they will soon become very frustrated. It is quicker and easier to use the keyboard to input numbers, so you must provide this facility. The task of the interface is equally important to deciding the appearance. In the case of a phone dialer application, we must consider that the capability to make phone calls may be secondary to the application’s true purpose. For example, it may be used primarily for sending and receiving faxes, with the capability to edit faxes a major element. In that case, an interface based on a real-world phone may be inappropriate. A traditional multiple document interface (MDI) file-based user interface may be more familiar and comfortable for users. If you opt for a file-based MDI interface, then other factors regarding appearance must be considered. For example, the first menu people will expect to see is the File menu; they will also expect a Window menu and so on. You can see then that how your interface appears to the user is a difficult decision with many factors that must be considered. Designing the appearance of an interface is linked to how the interface should function, and such a design can take a long time to develop. In the MiniCalculator program it was decided that the appearance of the interface should mirror that of a real-world calculator (though the functionality has not been restricted to that of a real-world calculator). This meant making the buttons symbolic rather than simple text buttons. As a result, the interface is highly graphical, consisting almost entirely of button images. Using symbols on buttons instead of text has the advantage that no separate resources for the buttons need be supplied for different locales. Of course this is not the case with text strings, such as the description strings that appear in the third panel of MiniCalculator’s status bar. In general it is preferable always to provide a symbol representing each button or menu item, even if text is used. The user will read the text and associate the symbols with the text. Later, when the user is familiar with the symbols, the text may no longer be required. In some situations it may be appropriate to allow the user to remove text descriptions from controls if they are no longer required. This can be of great benefit to the application, because it frees available screen space. This is particularly noticeable in the toolbars and coolbars of MDI applications, where it is desirable to have as large a space as possible within the parent window to allow child windows to be edited. Allowing the user to reposition text labels also can help.
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
This section looks at how symbols can be used on their own and with text to improve interface appearance.
07 9721 ch5
11/13/00
274
9:51 AM
Page 274
C++Builder 5 Essentials PART I
Using Symbols on Their Own with Buttons If you want to provide buttons that do not have text, or the text forms part of the button’s symbol, then TSpeedButton is a good choice. The advantage of TSpeedButton is that no focus rectangle is drawn on the button when it is clicked or receives focus, so your well-designed buttons won’t be disfigured. Additionally, TSpeedButton has a Down property, which gives you one extra state. The GroupIndex property is also helpful in controlling buttons as groups and saves you a lot of coding. A disadvantage of TSpeedButton is that it doesn’t have a window handle and some of the events that TBitBtn has. This is because it derives from TGraphicControl and not TWinControl. Apart from that, the choice is yours. The information presented here can be applied to either button. Normally TButton should not be used unless you definitely don’t want an image on your buttons. You can cover a button entirely with your own image. The buttons in MiniCalculator were created in this way. The main concern is that you must size both your bitmap and your button correctly. Figure 5.4 shows the bitmap for the plus (+) button. Up
Disabled
Clicked
Down
1
1
2
2 2
2
FIGURE 5.4 Glyph layout for TSpeedButton and TBitBtn.
Each image in the bitmap represents a state for the button. Normally, the Clicked and Down states will be the same, though not always. The method for sizing your button and bitmap is straightforward. Set the size of the button equal to the size of the image you are going to use to cover the button. Then add a 1-pixel border to the top and left side of your image and a 2-pixel border to the bottom and right side of your button. This is shown in Figure 5.4. Note that the Clicked and Down images will be automatically offset to the bottom and right. Therefore, the size of the image will be smaller. You should allow for this by placing a white or (preferably) transparent 2-pixel border to the bottom and right of each image. White has been used for clarity in Figure 5.4. To use the image for your button, save the button glyph as a bitmap and assign it to the Glyph property at designtime. Don’t forget to set the NumGlyphs property to the appropriate value. One thing to bear in mind when designing your buttons’ images is that when your button is depressed, it will have a black border on the left and top and a white border on the right and bottom. You should create your images with this in mind; otherwise your buttons might look strange.
07 9721 ch5
11/13/00
9:51 AM
Page 275
User Interface Principles and Techniques CHAPTER 5
275
You should note that the two memory buttons in MiniCalculator use M+ and MR as their symbols. This is something that you should avoid. These are not truly symbolic; they are the initials of each button’s function. A better choice would be something similar to a copy symbol for the memory add (M+) button and a paste symbol for the memory recall (MR) button. We leave the creation of such symbols as an exercise for you.
Using TSpeedButton’s GroupIndex Property The MiniCalculator makes extensive use of TSpeedButton’s GroupIndex property to help control the behavior of the buttons used in the interface. Examples include the three number-base buttons: octal, decimal, and hexadecimal all share the same GroupIndex of 2, and the AllowAllUp property is false. No other buttons in the interface have this GroupIndex. This means that only one of the number-base buttons may be depressed at any one time, and at least one of them must be depressed. We do not need to write any code to achieve this. Simply setting the GroupIndex property is enough. Similarly, the Exponent button (SpeedButtonExponent) has its own GroupIndex of 3, allowing it to be pressed or released regardless of the state of any other button in the interface. The remaining buttons in the interface all share the same GroupIndex of 1, with the AllowAllUp property set to true. This means all the buttons can be up at the same time, but only one may be depressed at any one time. Again, all this is achieved without having to write any code, making the GroupIndex property of TSpeedButton a useful tool when creating an interface.
A Note About Flicker To help reduce flickering when moving the control bar bands that contain the speed buttons in MiniCalculator, their ControlStyle properties have csOpaque added to them in the constructor of MainForm. csOpaque indicates that the control fills its client rectangle entirely, and therefore controls beneath the control do need repainting as they are obscured by the control. To see the difference this flag makes, comment out the code in MainForm’s constructor that adds the csOpaque flags and run the program.
Using Symbols in Addition to Text
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
In the MiniCalculator program, symbols were used in addition to text in the drop-down and pop-up menus. At its simplest level of use, TMenuItem allows you to add an image through its Bitmap property. If you are not going to disable the menu item, and you do not mind being limited to an image of 16×16 pixels, this provides a convenient and easy way to add an image to the menu item in question. However, if you are going to disable the menu item, using the Bitmap property is not so satisfactory. This is because the image used for the disabled menu item will be generated from the image you supply. In many cases the generated image will look fine, but if it doesn’t you will need to generate your own disabled image and owner-draw the menu item. Likewise, if you want an image of a size other than 16×16, then owner-drawing the menu item is the way to achieve that.
07 9721 ch5
11/13/00
276
9:51 AM
Page 276
C++Builder 5 Essentials PART I
In the MiniCalculator program, the ControlBarPopup menu uses the Bitmap property to add images to the menu items, but the View menu in MainMenu is owner drawn to provide images in the menu items. Owner-drawing a menu item is easier than you may think. To owner-draw a menu item, you must have either a non-NULL Images property in the parent menu or have its OwnerDraw property set to true. You must then write a handler for either the OnDrawItem event or the OnAdvancedDrawItem event. The OnAdvancedDrawItem event provides more information about the state of the menu item and is therefore preferable. A single event handler for all of the menu items in MainMenu’s View menu is shared between all the menu items. The Tag property of the menu item is used to determine which image should be displayed by the handler. The handler is called ViewMenuItemsAdvancedDrawItem() and is assigned to the OnAdvancedDrawItem events of the menu items in MainForm’s constructor. For example, for the menu item ViewDisplay, we write ViewDisplay->OnAdvancedDrawItem = ViewMenuItemsAdvancedDrawItem;
Listing 5.17 shows the implementation of ViewMenuItemsAdvancedDrawItem(). LISTING 5.17
Implementation of ViewMenuItemsAdvancedDrawItem()
void __fastcall TMainForm::ViewMenuItemsAdvancedDrawItem(TObject* Sender, TCanvas* ACanvas, const TRect& ARect, TOwnerDrawState State) { TMenuItem* MenuItem = dynamic_cast(Sender); if(MenuItem) { // Step 1 - Save ACanvas properties that we are going to change TColor OldFontColor = ACanvas->Font->Color; TColor OldBrushColor = ACanvas->Brush->Color; int TextOffset = ARect.Left+1; try { // Step 2 // //
Draw the check region and the image. The images used depends on whether the item is selected and whether it is checked...
std::auto_ptr CheckedImage(new Graphics::TBitmap()); std::auto_ptr ToolbarImage(new Graphics::TBitmap()); // Step 3
07 9721 ch5
11/13/00
9:51 AM
Page 277
User Interface Principles and Techniques CHAPTER 5
LISTING 5.17
277
Continued
ViewMenuImageList->GetBitmap(MenuItem->Tag, ToolbarImage.get()); ToolbarImage.get()->Transparent = true; // Step 4 if(State.Contains(odChecked)) { if(State.Contains(odSelected)) { MenuCheckImageList->GetBitmap(1, CheckedImage.get()); } else { MenuCheckImageList->GetBitmap(0, CheckedImage.get()); } ACanvas->Draw(ARect.Left+1, ARect.Top+2, CheckedImage.get()); } // Step 5 ACanvas->Draw(ARect.Left+21, ARect.Top+2, ToolbarImage.get()); TextOffset = ARect.Left + 60; } __finally { // Step 6 if(State.Contains(odSelected)) { ACanvas->Font->Color = clHighlightText; ACanvas->Brush->Color = clHighlight; } else { ACanvas->Font->Color = clWindowText; ACanvas->Brush->Color = clMenu; } TECHNIQUES
USER INTERFACE PRINCIPLES AND
ACanvas->FillRect( Rect( TextOffset, ARect.Top, ARect.Right,
5
07 9721 ch5
11/13/00
278
9:51 AM
Page 278
C++Builder 5 Essentials PART I
LISTING 5.17
Continued ARect.Bottom ) );
// Now draw the text :@) // Use the WinAPI function DrawText as it // draws the underscores correctly :-) DrawText( ACanvas->Handle, MenuItem->Caption.c_str(), MenuItem->Caption.Length(), &Rect(TextOffset+2, ARect.Top+2, ARect.Right, DT_EXPANDTABS|DT_SINGLELINE|DT_LEFT );
ARect.Bottom),
// Step 7 ACanvas->Font->Color = OldFontColor; ACanvas->Brush->Color = OldBrushColor; } } }
The code in ViewMenuItemsAdvancedDrawItem() draws three things. First it draws a check mark for the menu item, either up or down. Then it draws a 36-pixel wide and 18-pixel high image representing the panel whose visibility is to be changed. Finally, it renders the text description of the panel, with the correct accelerator key underlined. To do this we use the WinAPI DrawText() function that was described earlier, in the “Using TStatusBar” section. The images that are displayed are stored in the image list ViewMenuImageList, and the index for each image corresponds to the Tag property of the menu item that fired the event. We retrieve a pointer to the menu item that fired the event by dynamic_casting Sender to a TMenuItem*. The remainder of the function can be described as follows: 1. We save the ACanvas properties that we are going to change. 2. We create two bitmaps: one to hold the check mark image and one to hold the panel image. We use the C++ Standard Library’s auto_ptr<> template class to hold the pointer to the TBitmap image so that it will automatically be destroyed even if an exception is thrown. We perform this and all steps up to step 5 inside a try block. Step 6 is performed inside a __finally block. This is where the text is rendered. If an exception is thrown when drawing the images on the menu item, the try/__finally construct ensures that the code to render the text is always executed. 3. We retrieve the image for the panel based on the menu item’s Tag property from the image list ViewMenuImageList. We then set the bitmap’s Transparent property to true. Note how we use auto_ptr’s get() method to retrieve the TBitmap pointer.
07 9721 ch5
11/13/00
9:51 AM
Page 279
User Interface Principles and Techniques CHAPTER 5
279
4. If the State parameter contains the odChecked flag, then we draw a check mark image. Which one we draw depends on whether or not the State parameter also contains the odSelected flag. If we require a check mark image, we retrieve the appropriate one from the MenuCheckImageList. 5. We then draw the panel image and set the TextOffset variable to 60 to allow for the check mark space and panel image. We use this value to determine where we should begin drawing the menu item description text. 6. We now draw the menu item text description. We offset the text by the amount specified in the TextOffset variable. If an exception was thrown in the previous try block, this value will be 0; otherwise it will be 60 to allow for the images that have been drawn on the menu item. First we set text and background color based on whether the State parameter contains the odSelected flag. Next we fill the background using the current Brush->Color; finally, we draw the text using the WinAPI DrawText() function and the current Font->Color. 7. Now that we have finished drawing the menu item, we reset the old ACanvas properties that we changed. Having provided an implementation for the OnAdvancedDrawItem event, one thing remains. We must implement the OnMeasureItem event for each menu item to ensure there is enough space for our custom drawing. Again, as for the OnAdvancedDrawItem event, we write one implementation and share it with all menu items by setting each menu item’s OnMeasureItem event equal to the function in MainForm’s constructor. The function we use is called ViewMenuItemsMeasureItem(), and its implementation is shown in Listing 5.18. LISTING 5.18
Implementation of ViewMenuItemsMeasureItem()
void __fastcall TMainForm::ViewMenuItemsMeasureItem(TObject* Sender, TCanvas* ACanvas, int& Width, int& Height) { TMenuItem* MenuItem = dynamic_cast(Sender); if(MenuItem) { Height = 22; Width = ACanvas->TextWidth(MenuItem->Caption) + 62; } TECHNIQUES
USER INTERFACE PRINCIPLES AND
}
5
07 9721 ch5
11/13/00
280
9:51 AM
Page 280
C++Builder 5 Essentials PART I
To determine who fired the event, we dynamic_cast Sender to a TMenuItem pointer. If successful, we set the Height parameter to 22 pixels to allow for the height of the panel images plus a 2-pixel border above and below each image. The Width parameter is set to the width, in pixels, that will be occupied by the Caption property of the menu item using the current Canvas of the menu item, as specified by the ACanvas parameter. The TextWidth method is used to obtain this information. To this we add 62 to allow for the width of the check mark image (18 pixels) and the panel image (36 pixels) and borders between them, the edge of the menu item, and the text description (from the Caption property). As you can see, owner-drawing menu items is not too difficult, but it can make a big difference to your menus, improving both their appearance and flexibility. By sharing the event handlers for groups of menu items, you can minimize the amount of code, making the use of ownerdrawn menus even more attractive.
Using Color to Provide Visual Clues Using color is an easy and effective way to improve the clarity and intuitiveness of your interface. You can see how the color of the buttons in the MiniCalculator program has been used to group buttons according to their purpose. Teal buttons are for performing operations, black buttons are for inputting values, orange buttons are for editing or clearing values, blue buttons are memory functions, and green buttons are for changing the number base. Using color in this way helps make it easy to associate functionality with controls even if that functionality is not known. Much time can be spent on choosing appropriate colors or selecting colors that most people will find easy to differentiate, such as those who are color-blind. How you use color to improve the appearance and usefulness of your interfaces depends on many factors, but using color to create groups is a simple and effective method that can be easily employed in many situations. Other uses will also become apparent, depending on the application.
Using Shaped Controls A common desire in interface programming is to be released from the confines of the rectangular Windows world and embrace the artistic freedom that comes from using non-rectangular controls. There is more than one way to achieve this, but we will present one simple approach that can find a multitude of uses. To create a non-rectangular windowed control (a TWinControl descendant), use one of the WinAPI region functions to create a region and then assign it to the control. The region functions are used to describe a region of a device surface. They can be rectangular, elliptical, polygonal, or a combination of shapes. Regions can be filled, painted, inverted, and framed and can be used to test for the presence of the mouse cursor. Because regions can be of irregular shape, you can use them to create irregularly shaped buttons. A region is a graphics device
07 9721 ch5
11/13/00
9:51 AM
Page 281
User Interface Principles and Techniques CHAPTER 5
281
interface (GDI) object and must therefore be created. To create a region, use one of the many WinAPI functions designed for doing so. These are listed in Table 5.7. TABLE 5.7
Region-Creation Functions
CreateEllipticRgn()
Creates an elliptical region given the lefttop and right-bottom coordinates of the bounding rectangle. Creates an elliptical region given the bounding rectangle as a pointer to a TRect structure. Creates a polygonal region given an array of points that defines the region and a fill mode that determines which parts of the polygon are filled. Creates one or more polygonal regions given an array of arrays of points. A fill mode is specified to determine which parts of each polygon are filled. Creates a rectangular region given the lefttop and right-bottom coordinates of the bounding rectangle. Creates a rectangular region given the bounding rectangle as a pointer to a TRect structure. Creates a rounded rectangular region given the left-top and right-bottom coordinates of the bounding rectangle and the width and height of the ellipse that is used to form the rectangle’s corners. Creates a region based on the transformation of an existing region. Creates a region based on the combination of two other regions, given a mode that specifies how the two regions should be combined.
CreateEllipticRgnIndirect()
CreatePolygonRgn()
CreatePolyPolygonRgn()
CreateRectRgn()
CreateRectRgnIndirect()
CreateRoundRectRgn()
ExtCreateRegion() CombineRgn()
5 TECHNIQUES
Description
USER INTERFACE PRINCIPLES AND
Function
07 9721 ch5
11/13/00
282
9:51 AM
Page 282
C++Builder 5 Essentials PART I
To learn more about the region creation functions listed in Table 5.7, refer to the Win32 SDK online help. Another good source of information for this topic and all Win32 GUI issues is the excellent book, Win32 Programming, by Rector and Newcomer, published by Addison-Wesley (1997). Once you have created your region, you use the WinAPI SetWindowRgn() function to assign the region to a windowed control. The SetWindowRgn() function is declared as int SetWindowRgn( HWND hWnd, // handle to window whose window region is to be set HRGN hRgn, // handle to region BOOL bRedraw // window redraw flag - normally TRUE for visible controls );
The return value is non-zero to indicate success. As an example of how to use regions to create non-rectangular controls, the MiniCalculator program has a panel of buttons representing constant values, such as π. These are TBitBtn components set as rounded rectangular regions. To illustrate the code required, Listing 5.19 shows a snippet of the code that appears in MainForm’s constructor to create the rounded rectangular π button (called ConstantPieBitBtn). LISTING 5.19
Creating a Rounded Rectangular Button
// Step 1 - Create the region HRGN hRoundRectRegion1 = CreateRoundRectRgn( 0, 0, ConstantPieBitBtn->Width+1, ConstantPieBitBtn->Height+1, 14, 14 );
// // // // // //
// Step 2 - Assign the region to the button SetWindowRgn( ConstantPieBitBtn->Handle, hRoundRectRegion1,
Left Top Right Bottom Ellipse Width Ellipse Height
TRUE );
Normally when you are finished with a region, you delete it using the DeleteRgn() macro, passing the region handle as the single argument. However, when you assign a region handle using the SetWindowRgn() function, the operating system gains ownership of the region. You should not perform any further function calls using the region handle, and in particular you should not delete it. Creating the images for a non-rectangular button requires a bit of experimentation, but it is not too difficult. For rounded edges you will want to use some form of antialiasing to reduce the
07 9721 ch5
11/13/00
9:51 AM
Page 283
User Interface Principles and Techniques CHAPTER 5
283
pixilated look of the edges. The effectiveness of this depends on careful choice of color so that the button edges blend into the background. You can see how this was achieved by examining the images used for the MiniCalculator program. One thing to note is that, if you want to be successful with non-rectangular buttons, you will probably have to derive your own button component and override the OnPaint() method so that you have total control over how the button is rendered. Using TBitBtn as it stands is not satisfactory. The focus rectangle and default click behavior make it almost unusable for nonrectangular buttons. However, creating your own button component is not too difficult. Chapter 9, “Creating Custom Components,” should set you on the right track.
Enhancing Usability by Allowing Customization of the User Interface A good way to improve the usability of your interface is to allow the user to customize its appearance. This can be as simple as changing the color of different elements of the interface, or it can be as complex as allowing the user to undock parts of the interface or rearrange others. The capability to resize an interface is important, as is the capability to make only certain parts of the interface visible at any given time. Of all these, using color is probably the simplest. All you need to do is give the user access to the Color properties of the controls you use to create the interface. In some cases this may not be appropriate, such as when the interface is highly graphical, because there may only be small areas of the interface suitable for such customization. The MiniCalculator program is an example of this. A good way to meet the user’s expectations in terms of color is to use the system colors when possible. The system colors are shown in Table 5.8 along with a brief description of what they are for. TABLE 5.8
System Colors
clBackground
Current background color of the Windows desktop Current color of the title bar of the active window Current color of the title bar of inactive windows Current background color of menus Current background color of windows Current color of window frames Current color of text on menus Current color of text in windows Current color of the text on the title bar of the active window Current border color of the active window
clActiveCaption clInactiveCaption clMenu clWindow clWindowFrame clMenuText clWindowText clCaptionText clActiveBorder
5 TECHNIQUES
Description
USER INTERFACE PRINCIPLES AND
System Color
07 9721 ch5
11/13/00
284
9:51 AM
Page 284
C++Builder 5 Essentials PART I
TABLE 5.8
Continued
System Color
Description
clInactiveBorder
Current border color of inactive windows Current color of the application workspace Current background color of selected text Current color of selected text Current color of a button face Current color of a shadow cast by a button
clAppWorkSpace clHighlight clHighlightText clBtnFace clBtnShadow clGrayText clBtnText clInactiveCaptionText clBtnHighlight cl3DDkShadow cl3DLight clInfoText clInfoBk
Current color of text that is dimmed Current color of text on a button Current color of the text on the title bar of an inactive window Current color of the highlighting on a button Dark shadow for three-dimensional display elements Light color for three-dimensional display elements (for edges facing the light source) Text color for ToolTip controls Background color for ToolTip controls
For example, when displaying text in a window, use the clWindowText color. If the text is highlighted, use clHighlightText. These colors will already be specified to the user’s preference and should therefore be a good choice for the interface. This section concentrates on the resizing, aligning, visibility, and docking capabilities of a user interface. The MiniCalculator provides all these features, and so it is used as an example. The remainder of this section is broken into subsections, each giving an example of a particular technique.
Docking In the MiniCalculator program, the main display can be undocked from the rest of the interface and then positioned and resized independently. The main display is a TPanel called LCDPanel. To make it possible to undock this panel from the main form, we must do three things: 1. Set LCDPanel->DragKind to dkDock. 2. Set LCDPanel->DragMode to dmAutomatic. 3. Set MainForm->DockSite to true.
07 9721 ch5
11/13/00
9:51 AM
Page 285
User Interface Principles and Techniques CHAPTER 5
285
We can do all this at designtime using the Object Inspector. This is all that is required to make the LCDPanel dockable, but to make it work well, a little more work is required. We must consider what changes, if any, we need to make when LCDPanel is undocked from MainForm. This is not as simple as it first appears. Our first thought probably is to write a handler for the MainForm->OnUnDock event. However, this is not suitable because there is a bug in the VCL that results in OnUnDock not being fired the first time a control is undocked. If you require any resizing, then clearly it will not work as you expect. A better approach is to write a handler for LCDPanel’s OnDockEnd event and check the value of the Floating property of the panel. If Floating is true and this is the first call to OnDockEnd, then the control has been undocked. This event occurs at the same time as the OnUnDock event, so there is no perceptible difference to the user. The only additional requirement of using this method is that we must use a variable to indicate whether the call to OnEndDock is the first call in the docking action. This is because OnEndDock is called at the end of every move made by a docking control. We will use a bool variable, FirstLCDPanelEndDock, to indicate if the OnEndDock event is the first in the current docking sequence. We therefore add the line bool FirstLCDPanelEndDock;
to MainForm’s class definition and initialize it to true in MainForm’s constructor: FirstLCDPanelEndDock = true;
The code required in LCDPanel’s OnEndDock event is shown in Listing 5.20. LISTING 5.20
Implementation of LCDPanel->OnEndDock
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
void __fastcall TMainForm::LCDPanelEndDock(TObject *Sender, TObject *Target, int X, int Y) { if(LCDPanel->Floating) { SetFocus(); } if(FirstLCDPanelEndDock) { if(LCDPanel->Floating) FirstLCDPanelEndDock = false; Height = Height - LCDPanel->Height; } }
07 9721 ch5
11/13/00
286
9:51 AM
Page 286
C++Builder 5 Essentials PART I
If this is the first time that LCDPanel’s OnEndDock event is fired in the current docking sequence (that is, LCDPanel has just been undocked and FirstLCDPanelEndDock is true), then we resize MainForm by subtracting the Height of LCDPanel from MainForm’s current Height. We do this even if the control is not floating, because we add the Height of LCDPanel back to MainForm in MainForm’s OnDockDrop event, which will be fired if LCDPanel is not Floating. This can occur the very first time we try to undock LCDPanel where it is possible to undock and dock LCDPanel in the same docking action. We do not need to reposition the remaining two controls that are directly on MainForm— ButtonsControlBar and StatusBar1—because their Align properties are alClient and alBottom, respectively. We can now undock LCDPanel, and MainForm will be automatically resized appropriately. Notice that before we resize MainForm we first reset the FirstLCDPanelEndDock to false, but only if LCDPanel is Floating. Again, this is because the first time you undock the panel it is possible to undock and dock in the same action. LCDPanel may not be Floating, and setting FirstLCDPanelUnDock to false would mean that this code would not be executed the next time the panel is actually undocked. Note that every time LCDPanelEndDock() is called and LCDPanel->Floating is true, we call SetFocus() for the MainForm. This ensures that MainForm never loses input focus from the keyboard. This was discussed in more detail in the section “Enhancing Usability Through Input Focus Control.” Docking LCDPanel back onto MainForm is a bit more complicated than undocking it. First we must implement MainForm’s OnGetSiteInfo event handler. This event passes a TRect parameter, InfluenceRect, by reference. This TRect specifies where on the form docking will be activated if a dockable control is released over it. This allows you to specify docking regions on a control for specific controls. In the MiniCalculator program we specify a dockable region equal to the Height of LCDPanel and the ClientWidth of MainForm starting at the top of the main form. The event handler is shown in Listing 5.21. LISTING 5.21
Implementation of MainForm->OnGetSiteInfo
void __fastcall TMainForm::FormGetSiteInfo(TObject* Sender, TControl* DockClient, TRect& InfluenceRect, TPoint& MousePos, bool& CanDock) { if(DockClient->Name == “LCDPanel”) { InfluenceRect.Left = ClientOrigin.x; InfluenceRect.Top = ClientOrigin.y; InfluenceRect.Right = ClientOrigin.x + ClientWidth;
07 9721 ch5
11/13/00
9:51 AM
Page 287
User Interface Principles and Techniques CHAPTER 5
LISTING 5.21
287
Continued
InfluenceRect.Bottom = ClientOrigin.y + DockClient->Height; } }
The first thing we do inside FormGetSiteInfo() is check to see if the DockClient—the TControl pointer to the object that caused the event to be fired—is LCDPanel. If it is, then we define the docking site above which LCDPanel may be dropped by specifying suitable values for the InfluenceRect parameter. We do not use the remaining parameters: MousePos and CanDock. MousePos is a reference to the current cursor position, and CanDock is used to determine if the dock is allowed. With CanDock set to false, the DockClient cannot dock. We must now implement MainForm’s OnDockOver event. This event enables us to provide visual feedback to the user as to where the control will be docked if the control is currently over a dock site (the mouse is inside InfluenceRect) and the control is dockable (CanDock == true). We use the DockRect property of the Source parameter, a TDragDropObject pointer, to define the docking rectangle that appears to the user. The implementation of MainForm->OnDockOver is shown in Listing 5.22. LISTING 5.22
Implementation of MainForm->OnDockOver
void __fastcall TMainForm::FormDockOver(TObject* Sender, TDragDockObject* Source, int X, int Y, TDragState State, bool& Accept) { if(Source->Control->Name == “LCDPanel”) { TRect DockingRect( ClientOrigin.x, ClientOrigin.y, ClientOrigin.x + ClientWidth, ClientOrigin.y + Source->Control->Height ); Source->DockRect = DockingRect; }
5
When the docking control moves over its InfluenceRect (as defined in OnGetSiteInfo), the outline rectangle that signifies the control’s position is snapped to the Source->DockRect defined in OnDockOver. This gives the user visual confirmation of where the docking control will be docked if he releases the control. In this case, Source->DockRect is set equal to the
USER INTERFACE PRINCIPLES AND
TECHNIQUES
}
07 9721 ch5
11/13/00
288
9:51 AM
Page 288
C++Builder 5 Essentials PART I
of the control and the ClientWidth of the main form, with TRect starting at ClientOrigin. In fact, this is the same as the InfluenceRect specified in OnGetsiteInfo. Height
The remaining parameters are not used: X, the horizontal cursor position; Y the vertical cursor position; State, of type TDragState, the movement state of the mouse in relation to the control; and Accept. Setting Accept to false prevents the control from docking. Finally, we implement MainForm->OnDockDrop. This event allows us to resize the control to fit the DockRect specified in the OnDockOver handler. It also allows us to perform any other processing that is needed, such as resizing the form or resetting the Anchors or Align property. The implementation for MainForm->OnDockDrop is shown in Listing 5.23. LISTING 5.23
Implementation of MainForm->OnDockDrop
void __fastcall TMainForm::FormDockDrop(TObject* Sender, TDragDockObject* Source, int X, int Y) { if(Source->Control->Name == “LCDPanel”) { Source->Control->Top = 0; Source->Control->Left = 0; Source->Control->Width = ClientWidth; // Allow space... Height = Height + Source->Control->Height; // Must reset the Align of LCDPanel Source->Control->Align = alTop; // Reset the FirstLCDPanelEndDock flag FirstLCDPanelEndDock = true; } }
The implementation of FormDockDrop() as shown in Listing 5.23 is not as simple as it first appears. First we resize LCDPanel to fit the top of the form. We then allow space for the docked panel by increasing the Height of MainForm by the Height of LCDPanel. Next reset LCDPanel->Align to alTop. We must do this as the Align property is set to alNone when LCDPanel is undocked. Finally, we reset FirstLCDPanelEndDock to true in readiness for the next time LCDPanel is undocked.
07 9721 ch5
11/13/00
9:51 AM
Page 289
User Interface Principles and Techniques CHAPTER 5
289
There are two things of note. First, we must adjust the Height of MainForm before we reset the Align property of LCDPanel to alTop. If LCDPanel->Align is set to alTop before MainForm’s Height is adjusted, MainForm’s Height may be adjusted twice. This is because MainForm will be automatically resized to accommodate LCDPanel if LCDPanel->Align is alTop and there is not sufficient room. Subsequently changing MainForm’s Height manually results in twice as much extra height as was needed. Changing the Height of MainForm first circumvents this problem because there will always be enough room for LCDPanel. When its Align property is set to alTop, no automatic resizing is required. Second, we do not need to perform any repositioning of the other controls on MainForm. This is because there are only two other controls directly placed on MainForm, and both have their Align properties set to a value other than alNone. The two controls are ButtonsControlBar (a TControlBar) and StatusBar1 (a TStatusBar). StatusBar1->Align is alBottom, and ButtonsControlBar->Align is alClient. By providing the extra height on the form, these controls are automatically repositioned. In many ways, the docking capabilities of MiniCalculator are small, but they are sufficient. For a more involved example of docking in C++Builder, you should study the example project dockex.bpr in the $(BCB)\Examples\Docking folder of your C++Builder 5 installation.
Resizing To cope with resizing, you can handle both the OnResize and OnConstrainedResize events; which one depends on what you want to achieve. In the MiniCalculator program, there is an example of each: When LCDPanel is resized, we must update the justification of the labels within the panel in OnResize, and when ButtonsControlBar is resized, we must prevent the control from being made smaller than the control bands it contains in OnConstrainedResize. You can also control resizing more generally by setting the Align, Anchors, AutoSize, Constraints, Height, Left, Top, and Width properties of controls. There are examples of all but the AutoSize property in the MiniCalculator program. By default, AutoSize is false. Setting it to true causes the control to resize automatically to accommodate its contents.
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
Using Align The Align property can be used to create regions on a control, typically a form, that resize according to specific rules. To create areas that can be resized beside areas that cannot, use a combination of nested panels and set their Align properties to create the desired effect. There is an infinite number of possible layouts, and experimentation is useful. By combining the use of aligned panel components with Constraints carefully set, very clever resizing interfaces can be developed with a minimum of work.
07 9721 ch5
11/13/00
290
9:51 AM
Page 290
C++Builder 5 Essentials PART I
The sample project, Panels.bpr, contained on the CD-ROM that accompanies this book, illustrates simple use of panel components to create a resizing interface. The panels each have their Align properties variously set so that each panel exhibits different qualities when the main form, Form1, is resized. The Contraints of Panel1 and Panel2 are set so as to make the interface more robust by preventing the interface from being resized too small. In the MiniCalculator program, the interface is divided into three control regions. At the top is an alTop Aligned TPanel called LCDPanel. In the middle is an alClient Aligned TControlBar called ButtonsControlBar, and at the bottom is an alBottom Aligned TStatusBar called StatusBar1. The MinHeight and MaxHeight Contraints of LCDPanel and StatusBar1 are set to their actual Heights, which therefore cannot be modified. Figure 5.5 shows this layout. LCDPanel–>Contraints–>MinHeight == 77 LCDPanel–>Contraints–>MaxHeight == 77
alTop
alClient
alBottom
LCD Panel
ButtonsControlBar
StatusBar1
StatusBar1–>Contraints–>MinHeight == 30 StatusBar1–>Contraints–>MaxHeight == 30
FIGURE 5.5 The Align layout of MiniCalculator.
When MiniCalculator is resized, only the widths of LCDPanel and StatusBar1 can be changed, whereas both the height and width of ButtonsControlBar can be changed.
Using Anchors The Anchors is similar to the Align property but allows an extra degree of control. The use of anchors is essential to the correct layout of the LCD display at the top of MiniCalculator. The LCD display was created using two TPanel components, three TLabel components, and three TSpeedButton components. All the components are placed on a TPanel component called LCDPanel. LCDPanel has its Constraints->MinWidth property set to 227 pixels and its
07 9721 ch5
11/13/00
9:51 AM
Page 291
User Interface Principles and Techniques CHAPTER 5
291
and Constraints->MaxHeight properties both set to 77 pixels (as shown in Figure 5.5). Its height cannot be changed, and its width cannot be less than 227 pixels.
Constraints->MinHeight
Another TPanel component, BackgroundDisplay, is placed on top of LCDPanel and anchored to it by the anchors akLeft, akRight, akTop, and akBottom. This ensures that BackgroundPanel is resized when LCDPanel is resized, but with the border size maintained. Because the height of LCDPanel will not vary at runtime, the anchors akTop and akBottom are not required, but including them makes it easy to resize the height of LCDPanel at designtime. Figure 5.6 shows the anchors used by the three TLabel components that are placed on BackgroundPanel. This can be referred to when each of the labels is discussed. akLeft
akRight akTop
LCDPanel akBottom
akBottom BackgroundPanel
akRight akLeft
akRight
LCDScreen–>Constraints Key
ExponentLabel–>Constraints HistoryLabel–>Constraints
FIGURE 5.6 The anchors used by the three TLabel components in MiniCalculator’s display.
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
LCDScreen, a TLabel component, is anchored to the BackgroundDisplay TPanel component by the anchors akLeft, akRight, and akBottom. We could have included akTop, but we did not want the height of LCDScreen modified during designtime if we modified the height of either BackgroundPanel or LCDPanel. In a similar fashion to BackgroundDisplay, LCDScreen is anchored to akBottom unnecessarily at runtime but conveniently at designtime. If LCDPanel’s height is changed at designtime, BackgroundPanel’s height will also change. The height of LCDScreen will not be affected, but it will move to maintain its gap with the bottom of BackgroundPanel. The akLeft and akRight anchors ensure that the same left and right borders are maintained at both designtime and runtime.
07 9721 ch5
11/13/00
292
9:51 AM
Page 292
C++Builder 5 Essentials PART I
The HistoryLabel component is anchored to BackgroundPanel in almost the same way as LCDScreen; its Anchors property contains akLeft and akRight but, instead of akBottom, akTop is included. Again, as for LCDScreen, only akLeft and akRight are required, but including akBottom makes it easier to manipulate HistoryLabel at designtime. If the height of LCDPanel is changed at designtime, the distance of HistoryLabel from the edge of BackgroundPanel will remain constant. Therefore only the gap between HistoryPanel and LCDScreen will change. Finally, the ExponentLabel component is anchored to BackgroundPanel, using akRight and akBottom. The position of these anchors is the same as the akRight and akBottom anchors of LCDScreen. Also, as for LCDScreen, the akBottom anchor is not required at runtime, but including it makes it easier to manipulate the label at designtime. It is important to note that you can change the position of an anchor at runtime without having to re-anchor to that position. This is done with the LCDScreen component when an exponent is being edited. When SpeedButtonExponent is pressed, the LCDScreen label must be shrunk and moved away from above ExponentLabel. This is done so that when ExponentLabel is made visible, the user can see both it and LCDScreen. The code to reposition and resize LCDScreen to allow for the now visible ExponentLabel is as follows: int LCDScreenLeft = LCDScreen->Left; LCDScreen->Width = LCDScreen->Width - ExponentLabel->Width; LCDScreen->Left = LCDScreenLeft;
By changing the width of LCDScreen, either the left or right anchor must be moved. Which is moved depends on the value of LCDScreen’s Alignment property. If it is taRightJustify, then the left anchor will be moved. If it is taLeftJustify, then the right anchor will be moved, and if it is taCenter, both anchors will be moved. Referring to the previous code snippet, we can see that if we change the width of LCDScreen, whose Alignment will be taRightJustify, then the left anchor will be moved. Because the right anchor remains unchanged, the effect is to decrease the width of LCDScreen without moving it from behind ExponentLabel, which is now visible. We adjust the Left property of LCDScreen so that it has its old value, thereby shifting LCDScreen to the left as we desire. There is an alternative approach, and that is to change the alignment of LCDScreen to taLeftJustify before changing LCDScreen’s Width. After the width is changed, we simply change Alignment back to taRightJustify to restore the original setting. The following code is therefore equivalent to the previous code snippet: LCDScreen->Alignment = taLeftJustify; LCDScreen->Width = LCDScreen->Width - ExponentLabel->Width; LCDScreen->Alignment = taRightJustify;
The Anchors property is a very useful tool to help control the layout of an interface. A particularly common use is to anchor buttons on resizable dialog boxes. The sample project,
07 9721 ch5
11/13/00
9:51 AM
Page 293
User Interface Principles and Techniques CHAPTER 5
293
Panels.bpr,
on the accompanying CD-ROM, shows two TBitBtn components on Panel5 (the clRed panel). When the form is resized, the buttons maintain their distance from the right edge of the form. The same effect can be achieved by placing the buttons directly onto the form the required distance from the right edge and setting the right anchor for each button (akRight == true).
Using Constraints provide an excellent mechanism for constraining the size of a control. The main thing to remember about Constraints is that a control cannot be resized to a size outside the Constraints of itself and cannot be resized to violate the Constraints of any visible control that it contains. Constraints
The sample project, Panels.bpr, uses Constraints to prevent the main form from being made too small. Panel1’s Constraints->MinHeight property is set to 300, and Panel2’s Constraints->MinWidth property is also set to 300. This means that the client area of Form1 (the main form) cannot be less than 300×300. We could alternatively set the MinHeight and MinWidth Constraints of Form1, but to achieve the same value of 300×300 we need to take into account the difference between the form’s Height and its ClientHeight and the form’s Width and its ClientWidth. MiniCalculator also uses Constraints. The main form, MainForm, has Constraints-> MinHeight set to 52 pixels, and Constraints->MinWidth is set to 248 to ensure that the main menu is always visible. This also ensures that StatusBar1 remains visible because only its MinHeight and MaxHeight Constraints are set (both equal 30, the Height of StatusBar1). LCDPanel has its own MinWidth Constraint because it is dockable and can therefore be resized independently of MainForm. LCDPanel’s MinHeight and MaxHeight Constraints are both set to 77, the Height of LCDPanel. LCDPanel’s height therefore cannot be changed. ButtonsControlBar has its Constraints determined dynamically at runtime so that they always accommodate the controls it contains, regardless of their position within the control bar. To do this we must implement ButtonsControlBar’s OnConstrainedResize event.
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
Using OnConstrainedResize Use OnConstrainedResize to update acontrol’s Constraints when the control is resized. As was mentioned in the last section the MiniCalculator program dynamically updates the Constraints of the TControlBar component, ButtonsControlBar. The purpose of this is to constrain the minimum height and width of the control bar so that it will always accommodate the controls it contains, no matter where they are positioned within the control bar. The implementation of ButtonsControlBar->OnConstrainedResize is shown in Listing 5.24.
07 9721 ch5
11/13/00
294
9:51 AM
Page 294
C++Builder 5 Essentials PART I
LISTING 5.24
Implementation of ButtonsControlBar->OnConstrainedResize
void __fastcall TMainForm::ButtonsControlBarConstrainedResize(TObject* Sender, int& MinWidth, int& MinHeight, int& MaxWidth, int& MaxHeight) { GetControlBarMinWidthAndHeight(ButtonsControlBar, MinWidth, MinHeight); }
The OnConstrainedResize event has four parameters, each of which corresponds to one of the Constraints of the control for which the event is fired: MinWidth, MinHeight, MaxWidth, and MaxHeight. in itself doesn’t do very much. However, it does call the descriptively named GetControlBarMinWidthAndHeight(), passing as arguments a pointer to the control bar we are interested in and the MinWidth and MinHeight parameters so that their new values can be calculated and assigned. GetControlBarMinWidthAndHeight() is declared as follows: ButtonsControlBarConstrainedResize()
void __fastcall GetControlBarMinWidthAndHeight(TCustomControlBar* ControlBar, int& MinWidth, int& MinHeight);
Both the int parameters of GetControlBarMinWidthAndHeight() are passed by non-const reference, allowing us to modify the values passed. The implementation of GetControlBarMinWidthAndHeight() is shown in Listing 5.25. LISTING 5.25
Implementation of GetControlBarMinWidthAndHeight()
void __fastcall TMainForm::GetControlBarMinWidthAndHeight(TCustomControlBar* ControlBar, int& MinWidth, int& MinHeight) { int MinLeft = 0; int MinTop = 0; int MaxRight = 0; int MaxBottom = 0; bool FirstVisible = true; for(int i=0; iControlCount; ++i) {
07 9721 ch5
11/13/00
9:51 AM
Page 295
User Interface Principles and Techniques CHAPTER 5
LISTING 5.25
295
Continued
if(ControlBar->Controls[i]->Visible) { if(FirstVisible) { MinLeft = ControlBar->Controls[i]->Left-11; MinTop = ControlBar->Controls[i]->Top-2; MaxRight = ControlBar->Controls[i]->Left + ControlBar->Controls[i]->Width + 2; MaxBottom = ControlBar->Controls[i]->Top + ControlBar->Controls[i]->Height + 2; FirstVisible = false; } else { if((ControlBar->Controls[i]->Left-11) < MinLeft) { MinLeft = ControlBar->Controls[i]->Left-11; } if((ControlBar->Controls[i]->Top-2) < MinTop) { MinTop = ControlBar->Controls[i]->Top-2; } if( (ControlBar->Controls[i]->Left + ControlBar->Controls[i]->Width + 2) > MaxRight) { MaxRight = ControlBar->Controls[i]->Left + ControlBar->Controls[i]->Width + 2; } if( (ControlBar->Controls[i]->Top + ControlBar->Controls[i]->Height + 2) > MaxBottom) { MaxBottom = ControlBar->Controls[i]->Top + ControlBar->Controls[i]->Height + 2; } } } } MinWidth = (MaxRight - MinLeft); MinHeight = (MaxBottom - MinTop);
5
} TECHNIQUES
USER INTERFACE PRINCIPLES AND
The operation of GetControlBarMinWidthAndHeight() is quite simple. For each visible control in the control bar, we calculate a value to represent the left, right, top, and bottom of the
07 9721 ch5
11/13/00
296
9:51 AM
Page 296
C++Builder 5 Essentials PART I
control, including the control band border around the control. The control band border includes the handle of the band that is used for moving the control within the control bar. In this case there is an 11-pixel border on the left side and a 2-pixel border on the remaining sides. We then check each value against MinLeft, MaxRight, MinTop, and MaxBottom. If smaller or larger, as appropriate, we update the appropriate value. Finally, we calculate the required value for MinWidth and MinHeight as MinWidth = (MaxRight - MinLeft); MinHeight = (MaxBottom - MinTop);
These are now the new values of the MinWidth and MinHeight Constraints of ButtonsControlBar.
Using OnResize Use OnResize to update a control’s appearance or perform other necessary tasks when the control is resized. OnResize is called after the control has been resized. Do not use OnResize to update values in the Constraints property; use OnConstrainedResize instead. The reason is that the calling sequence for OnResize and OnConstrainedResize is OnResize→OnConstrainedResize[→OnResize]. If the values in the Constraints property are changed in OnConstrainedResize, OnResize is called a second time to allow any additional resizing to be handled. If you update Constraints in OnResize, your code will be executed twice. In MiniCalculator, the OnResize event for LCDPanel is implemented to ensure that the correct justification of the label components it contains is maintained. The implementation of the OnResize handler is shown in Listing 5.26. LISTING 5.26
Implementation of LCDPanel->OnResize
void __fastcall TMainForm::LCDPanelResize(TObject *Sender) { UpdateLCDScreen(LCDScreen->Caption); UpdateHistoryLabel(HistoryLabel->Caption); if(LCDPanel->Floating && MainForm->Visible) SetFocus(); }
The OnResize handler does three things. It updates the LCDScreen label, then it updates the HistoryLabel label, and finally it resets input focus to the main form if the panel is Floating (that is, it has been undocked from the main form). Note that it only calls SetFocus() if the main form is visible. This check is made because the OnResize event may occur before the main form is shown initially, such as when the panel is resized to match some previous setting. Refer to the section “Enhancing Usability by Remembering the User’s Preferences,” later in this chapter, for more information.
07 9721 ch5
11/13/00
9:51 AM
Page 297
User Interface Principles and Techniques CHAPTER 5
297
When we call UpdateLCDScreen(), we pass the current LCDScreen->Caption as the single argument. Essentially we then pass the current Caption and see if we need to adjust how the Caption is displayed based on the new size of LCDPanel. We saw the implementation of UpdateLCDScreen() in Listing 5.11, but we will repeat it in Listing 5.27 for convenience. LISTING 5.27
Implementation of UpdateLCDScreen()
void __fastcall TMainForm::UpdateLCDScreen(const AnsiString& NewNumber) { int NumberWidth = LCDScreen->Canvas->TextWidth(NewNumber); if(Operation == coComplete) { if( (NumberWidth >= LCDScreen->Width) && (LCDScreen->Alignment == taRightJustify) ) { LCDScreen->Alignment = taLeftJustify; } else if( (NumberWidth < LCDScreen->Width) && (LCDScreen->Alignment != taRightJustify) ) { LCDScreen->Alignment = taRightJustify; } } else if(LCDScreen->Alignment != taRightJustify) { LCDScreen->Alignment = taRightJustify; } LCDScreen->Caption = NewNumber; int pos = LCDScreen->Hint.Pos(“|”); int length = LCDScreen->Hint.Length(); AnsiString LCDScreenHint = LCDScreen->Hint.SubString(pos, length-pos+1); LCDScreen->Hint = NewNumber + LCDScreenHint; if(NumberWidth >= LCDScreen->Width) LCDScreen->ShowHint = true; else LCDScreen->ShowHint = false;
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
}
07 9721 ch5
11/13/00
298
9:51 AM
Page 298
C++Builder 5 Essentials PART I
The operation of UpdateLCDScreen() is broken into three stages. 1. We check the size, in pixels, required to display the NewNumber string. (In this case, NewNumber is actually the current Caption property of LCDScreen.) To do this we use the TextWidth() method of TCanvas. 2. We then set the correct justification (the Alignment property of LCDScreen), depending on whether an operation has just finished or there is one ongoing. If an operation has just finished, Operation is equal to coComplete, and LCDScreen is not wide enough to display the whole number, then we want to left-justify the label (LCDScreen->Alignment = taLeftJustify). Operation is coComplete when we are displaying the answer of an operation, and therefore it is more important to see the start of the number than the end of the number. If there is enough width to display the number, then we right-justify LCDScreen, because this is the preferred justification (LCDScreen->Alignment = taRightJustify). Otherwise, if Operation is not coComplete, then there is an operation ongoing; LCDScreen is displaying a value that is currently being edited or is capable of being edited. We want to right-justify (LCDScreen->Alignment = taRightJustify) the label Caption so that the part of the number that is being edited is always visible. 3. Finally, if NewNumber is too wide to be displayed on LCDScreen, we prepend NewNumber to LCDScreen’s Hint property. This is discussed at length in the previous section, “Using Customized Hints.” After we call UpdateLCDScreen(), we call UpdateHistoryLabel(), which performs a task similar to that performed by UpdateLCDScreen(). It adjusts the justification of HistoryLabel according to the width of the HistoryLabel->Caption text. However, unlike UpdateLCDScreen(), UpdateHistoryLabel() does not need to adjust the size of HistoryLabel. As a result, UpdateHistoryLabel() is much simpler. The implementation is shown in Listing 5.28. LISTING 5.28
Implementation of UpdateHistoryScreen()
void __fastcall TMainForm::UpdateHistoryLabel(const AnsiString& NewHistory) { int HistoryWidth = HistoryLabel->Canvas->TextWidth(NewHistory); if( (HistoryWidth >= HistoryLabel->Width) && (HistoryLabel->Alignment == taLeftJustify) ) { HistoryLabel->Alignment = taRightJustify; } else if( (HistoryWidth < HistoryLabel->Width) && (HistoryLabel->Alignment != taLeftJustify) ) {
07 9721 ch5
11/13/00
9:51 AM
Page 299
User Interface Principles and Techniques CHAPTER 5
LISTING 5.28
299
Continued
HistoryLabel->Alignment = taLeftJustify; } HistoryLabel->Caption = NewHistory; }
As for UpdateLCDScreen(), we use TextWidth() to determine the width in pixels of the NewHistory AnsiString. If NewHistory is too wide for the whole string to be displayed in HistoryLabel, then we right-justify HistoryLabel so that the most recent result is shown. Otherwise we left-justify HistoryLabel. This gives the appearance of text scrolling left when the right edge of HistoryLabel is reached. Finally, at the end of the OnResize handler we check to see if LCDPanel is Floating, indicating that it has been undocked from the main form (MainForm). If so, then MainForm will no longer have focus, because the action of resizing LCDPanel will have set the focus to the panel. We must therefore reset focus to the main form by calling SetFocus(). This ensures that MainForm can respond to keyboard input if it is enabled.
Using TControlBar is normally used as a visual container for TToolBars. In fact, it can be used as a visual container for other controls and is not restricted to toolbars; the controls contained by TControlBar do not even need to be of the same type. The MiniCalculator program uses TControlBar as a visual container for TPanel components. We call our TControlBar ButtonsControlBar; refer to Figure 5.5 for an image. To meet the requirements of our program, we must change some of the properties of ButtonsControlBar at designtime. We make the following changes: TControlBar
•
DockSite = false
We are not going to make the controls inside TControlBar dockable, and we do not want LCDPanel to be able to dock onto the control bar, so we set this property to false. •
RowSize = 31
The height of the bands within our control bar are exact multiples of 31. RowSnap is also true, ensuring the control bar aligns controls to 31 pixel rows. •
5
AutoDrag = false TECHNIQUES
USER INTERFACE PRINCIPLES AND
We do not want the bands in the control bar to be undocked when they are dragged off the control bar. Instead, we want them to remain inside the control bar. Therefore, we set AutoDrag to false.
07 9721 ch5
11/13/00
300
9:51 AM
Page 300
C++Builder 5 Essentials PART I
•
AutoDock = false
We do not allow docking on our control bar, and so we do not need the AutoDock facility. We set it to false. To use the control bar, we place the panels that we want to appear as control bar bands in the control bar at designtime. The control bar control does the rest. It adds the band grip to the left side of the panel (the two vertical lines that are used to drag the control within the control bar) and a frame around the panel. Some slight repositioning may be required.
TIP When MiniCalculator was being written, it was often necessary to move the buttons to different controls as the interface evolved. Doing this one control at a time is tedious and error prone. However, it is not possible to select the buttons by dragging a rectangle around them without also selecting the control on which they are placed. There is one easy way of selecting a large group of controls without selecting the control on which they are placed. Simply view the form as text (right-click the form and select View as Text from the context menu). Then you can cut and paste the text versions of the controls as you please.
Using a control bar to control how the panels are positioned within MiniCalculator greatly simplifies the code used to write the interface, and it allows the interface to be customized in a way that should be familiar to most users. In its current state, ButtonsControlBar allows reasonable flexibility, but more work is needed to make moving control bands more robust, particularly to prevent controls from overlapping. However, it is still possible to allow a certain degree of customization. Notably, the MiniCalculator provides the capability to left- or rightalign individual controls within ButtonsControlBar or all controls at once. It also allows the control bar to snap to the size of the controls that it contains. This functionality is accessed through a pop-up menu. Five choices are available: 1. Snap to Fit ButtonsControlBar
is resized to fit the controls it contains.
2. Align Left The control bar band under the mouse when the pop-up menu is activated is aligned to the left of ButtonsControlBar. 3. Align Right The control bar band under the mouse when the pop-up menu is activated is aligned to the right of ButtonsControlBar.
07 9721 ch5
11/13/00
9:51 AM
Page 301
User Interface Principles and Techniques CHAPTER 5
301
4. Align All Left All the control bar bands in ButtonsControlBar are aligned to the left. 5. Align All Right All the control bar bands in ButtonsControlBar are aligned to the right. Choices 1, 4, and 5 are always available, but choices 2 and 3 are only available if the pop-up menu, called ControlBarPopupMenu, is activated above one of the controls inside ButtonsControlBar.
Resizing TControlBar to Fit Its Contents First we will look at the code used to resize ButtonsControlBar to fit its contents. From the pop-up menu (ControlBarPopupMenu), clicking menu item SnapToFit1 executes its OnClick handler. This calls the helper function FitToControlBar(), which is declared as void __fastcall FitToControlBar(TCustomControlBar* ControlBar);
The single parameter is a pointer to the control bar that we want to fit to its contents. The implementation is shown in Listing 5.29. LISTING 5.29
Implementation of FitToControlBar()
void __fastcall TMainForm::FitToControlBar(TCustomControlBar* ControlBar) { int MinWidth = 0; int MinHeight = 0; GetControlBarMinWidthAndHeight(ControlBar, MinWidth, MinHeight); int WidthDifference = ButtonsControlBar->Width - MinWidth; int HeightDifference = ButtonsControlBar->Height - MinHeight; Width = Width - WidthDifference; Height = Height - HeightDifference; }
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
To resize the control bar, we first call GetControlBarMinWidthAndHeight(), whose implementation was shown in Listing 5.25. This gives us the minimum width and height of the control. We use these values to calculate the difference between the current width and height and the width and height to which we want to set the control bar. We then subtract these values from the main form’s Width and Height settings. We subtract from the main form’s width and height and not from ButtonsControlBar’s width and height because ButtonsControlBar’s Align property is set to alClient. It would be automatically resized to fit the form. Changing the form size works because ButtonsControlBar’s Align property is alClient; ButtonsControlBar will be automatically resized to fit the form.
07 9721 ch5
11/13/00
302
9:51 AM
Page 302
C++Builder 5 Essentials PART I
Aligning Controls Inside TControlBar MiniCalculator provides the capability of aligning the controls inside TControlBar either individually or all at once. The controls can be aligned either to the left or to the right. As we have already said, we use a pop-up menu called ControlBarPopupMenu to access this functionality. The functionality offered to the user depends on where in ButtonsControlBar the ControlBarPopupMenu is popped up. If it is activated over one of the controls inside ButtonsControlBar, then the user can align that control by itself or all controls. If ControlBarPopupMenu is activated and it is not over one of the controls inside ButtonsControlBar, then the user can choose only to align all the controls in ButtonsControlBar. To determine where on ButtonsControlBar the pop-up menu was activated, we implement ButtonsControlBar’s OnContextPopup event. This assumes that ButtonsControlBar’s PopupMenu property is set to ControlBarPopupMenu, the pop-up menu. ButtonsControlBar’s OnContextPopup event handler implementation is shown in Listing 5.30. LISTING 5.30
Implementation of ButtonsControlBar->OnContextPopup
void __fastcall TMainForm::ButtonsControlBarContextPopup(TObject *Sender, TPoint &MousePos, bool &Handled) { // Determine where on the Control Bar the mouse was clicked, // i.e. what control was it in ?? TRect* ControlRects = new TRect[ButtonsControlBar->ControlCount]; try { for(int i=0; i<ButtonsControlBar->ControlCount; ++i) { if(ButtonsControlBar->Controls[i]->Visible) { ControlRects[i] = ButtonsControlBar->Controls[i]->BoundsRect; ControlRects[i].Left -= 11; ControlRects[i].Top -= 2; ControlRects[i].Right += 2; ControlRects[i].Bottom += 2; } else { ControlRects[i] = TRect(0,0,0,0); // No Rect } }
07 9721 ch5
11/13/00
9:51 AM
Page 303
User Interface Principles and Techniques CHAPTER 5
LISTING 5.30
303
Continued
for(int i=0; i<ButtonsControlBar->ControlCount; ++i) { if(PtInRect(&ControlRects[i], MousePos)) { AlignLeft1->Visible = true; AlignRight1->Visible = true; ControlBarPopupMenu->Tag = ButtonsControlBar->Controls[i]->Tag; break; } else { AlignLeft1->Visible = false; AlignRight1->Visible = false; ControlBarPopupMenu->Tag = cbpNone; } } } __finally { delete [] ControlRects; } }
To determine whether the menu was activated over one of the visible controls in ButtonsControlBar, we use an array of TRects to store the boundaries of each control. If the control is not visible, we use a TRect with no size so that the mouse cannot be inside the control’s TRect when the menu pops up. To determine the TRect occupied by a control in ButtonsControlBar, we read the control’s BoundsRect property and then modify it to allow for the band grip (11 pixels on the left side) and the frame border (2 pixels on the remaining sides).
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
Once we have all the TRects for all the controls, we simply iterate through them and use the WinAPI PtInRect() function to see if the mouse lies inside any of the controls’ boundaries. If it does, we set the Tag property of ControlBarPopupMenu equal to that of the control the mouse is over. The Tag property is used to store which control the mouse was in when the popup menu was activated. We use this to determine which control to align in ControlBarPopupMenu’s OnClick handler if the user selects to align only a single control. In addition, if the mouse was in a control when the pop-up menu was activated, we make the menu items for aligning individual controls visible, namely AlignLeft1 and AlignRight1. Otherwise we keep them hidden.
07 9721 ch5
11/13/00
304
9:51 AM
Page 304
C++Builder 5 Essentials PART I
Notice the use of the resource protection try/__finally blocks to ensure that ControlRects is deleted even if an exception is thrown. Before we continue we should point out how we use the Tag property of the controls inside ButtonsControlBar to identify each control. Inside the constructor of MainForm we assign a value to the tag of each of the panel components used in the control bar. To aid readability we use an enum, TControlBarPanel, declared as enum TControlBarPanel { cbpFunctionButtons, cbpNumberButtons, cbpNone };
to enumerate the panels that are contained in ButtonsControlBar. In MainForm’s constructor we write FunctionButtonsPanel->Tag NumberButtonsPanel->Tag
= cbpFunctionButtons; = cbpNumberButtons;
If we want to add more panels to ButtonsControlBar, we simply add an entry to TControlBarPanel for the new panel and assign it to its Tag in MainForm’s constructor. For example, if we add a panel called ConstantsButtonsPanel, then we would add cbpConstantsButtons to TControlBarPanel and write ConstantsButtonsPanel->Tag = cbpConstantsButtons;
in MainForm’s constructor. The new panel is then ready for use. We can now look at how we align the controls within ButtonsControlBar. There are no properties of TControlBar that control the alignment of the bands it contains, so we must perform the alignment manually. The principles behind this are quite simple, though as we shall see the implementation is more involved than may be first thought. To illustrate the main principles, we will use a control called Panel as an example. To align controls to the left of the control bar, we set the Left property of the control we want to align to 11. This allows for the band grip at the left side of the control band that contains the control. Doing this sets the control band flush with the left side of the control bar. Hence, we write Panel->Left = 11;
When the control bar is resized, the control will remain flush against the left side of the control bar. In fact, this is the default behavior of TControlBar, which makes aligning controls to the left easy.
07 9721 ch5
11/13/00
9:51 AM
Page 305
User Interface Principles and Techniques CHAPTER 5
305
To align a control to the right is a little more complex. If we want to simply align the control to the current right side of the control bar, we set the Left property of the control so that the right side of the control is flush with the current right side of the control bar. We write the following: Panel->Left = ButtonsControlBar->ClientWidth - Panel->Width - 2;
When the control bar is made smaller than the current width, the control stays right-aligned, but when it is made larger than the current width, the control stays at the position indicated by its Left property. To force the control to stay right-aligned, we must set its Left property to a value greater than any possible width the control bar can take. We do this by setting the control’s Left property to the width of the screen: Panel->Left = Screen->Width;
Now when the control bar is resized, the control will remain aligned to the right side of the control bar. Listing 5.31 shows the implementation of the OnClick handler for the pop-up menu item that aligns a single control to the left. LISTING 5.31
Implementation of AlignLeft1Click()
void __fastcall TMainForm::AlignLeft1Click(TObject *Sender) { ArrangeControlBarBands(ButtonsControlBar, TControlBarPanel(ControlBarPopupMenu->Tag), cbaLeft); }
AlignLeft1Click() calls the helper function ArrangeControlBarBands() to perform the actual moving of the control. A pointer to the control bar is passed as the first argument, then the Tag of the control is passed to identify which control is being aligned. Finally, cbaLeft is passed to indicate that we want to align the control to the left. The declaration for ArrangeControlBarBands() is void __fastcall ArrangeControlBarBands(TCustomControlBar* ControlBar, TControlBarPanel CurrentBandTag, TControlBarAlignment Alignment); TControlBarAlignment
is an enum declared as
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
enum TControlBarAlignment { cbaLeft, cbaRight, cbaAllLeft, cbaAllRight };
07 9721 ch5
11/13/00
306
9:51 AM
Page 306
C++Builder 5 Essentials PART I
It is used by ArrangeControlBarBands() to determine what alignment is required. The implementation of ArrangeControlBarBands() is quite involved and is shown in Listing 5.32. LISTING 5.32
Implementation of ArrangeControlBarBands()
void __fastcall TMainForm::ArrangeControlBarBands(TControlBar* ControlBar, TControlBarPanel CurrentBandTag, TControlBarAlignment Alignment) { // Step 1 std::list BandList; TControlBandInfo ControlBarBand; // Step 2 for(int i=0; iControlCount; ++i) { if(ControlBar->Controls[i]->Tag == CurrentBandTag) { ControlBarBand = TControlBandInfo(ControlBar->Controls[i], ControlBar->Controls[i]->Left, ControlBar->Controls[i]->Top, ControlBar->Controls[i]->Height + 2, ControlBar->Controls[i]->Visible); } BandList.push_back(TControlBandInfo(ControlBar->Controls[i], ControlBar->Controls[i]->Left, ControlBar->Controls[i]->Top, ControlBar->Controls[i]->Height + 2, ControlBar->Controls[i]->Visible)); } // Step 3 if(Alignment == cbaLeft) { // This is the same as BandList.sort(); BandList.sort(std::less()); ControlBarBand.Left = 11; } else if(Alignment == cbaRight) { BandList.sort(std::greater()); ControlBarBand.Left = Screen->Width; }
07 9721 ch5
11/13/00
9:51 AM
Page 307
User Interface Principles and Techniques CHAPTER 5
LISTING 5.32
307
Continued
// Step 4 std::list::iterator pos; bool NoFreeColumn = false; for(pos = BandList.begin(); pos != BandList.end(); ++pos) { // Step 5 if( CurrentBandTag != pos->Control->Tag && pos->Visible ) { // Step 6 if( (Alignment == cbaLeft) && (pos->Left < (ControlBarBand.Control->Width + 2))) { NoFreeColumn = true; } if( (Alignment == cbaRight) && ( (ControlBar->ClientWidth-(pos->Left+pos->Control->Width+2)) < (ControlBarBand.Control->Width + 2) ) ) { NoFreeColumn = true; } // Step 7 if( ControlBarBand.Top >= pos->Top && ControlBarBand.Top < (pos->Top + pos->Height + 2) ) { // No free space move to the next free Row if(NoFreeColumn) ControlBarBand.Top = pos->Top + pos->Height + 2; else break; } // Step 8 else if( ControlBarBand.Top < pos->Top ) { // Free space std::list::iterator pos2; int Offset = 0; bool FirstVisibleControl = true;
TECHNIQUES
USER INTERFACE PRINCIPLES AND
// Step 9 for(pos2 = pos; pos2 != BandList.end(); ++pos2) { if( pos2->Visible
5
07 9721 ch5
11/13/00
308
9:51 AM
Page 308
C++Builder 5 Essentials PART I
LISTING 5.32
Continued && FirstVisibleControl && pos2->Control->Tag != CurrentBandTag) { // First control Offset = 2 + ControlBarBand.Top + ControlBarBand.Height - pos2->Top; FirstVisibleControl = false; } if(pos2->Visible) pos2->Top = pos2->Top + Offset; } break;
} NoFreeColumn = false; } } // Step 10 for(pos = BandList.begin(); pos != BandList.end(); ++pos) { pos->Control->Visible = false; if(pos->Control->Tag == CurrentBandTag) { pos->Left = ControlBarBand.Left; pos->Top = ControlBarBand.Top; } } // Step 11 if(Alignment == cbaLeft) { BandList.sort(std::less()); } else if(Alignment == cbaRight) { BandList.sort(std::greater()); } // Step 12 for(pos = BandList.begin(); pos != BandList.end(); ++pos) { pos->Control->Top = pos->Top; pos->Control->Left = pos->Left;
07 9721 ch5
11/13/00
9:51 AM
Page 309
User Interface Principles and Techniques CHAPTER 5
LISTING 5.32
309
Continued
pos->Control->Visible = pos->Visible; } }
The ArrangeControlBarBands() method makes use of the class TControlBandInfo. The definition of TControlBandInfo is shown in Listing 5.33. Of particular note in Listing 5.33 is the implementation of operator < and operator >. These are used to sort a list of TControlBandInfo objects. Both sort the control bands from top to bottom, but where the top value is the same, operator < sorts from left to right and operator > sorts from right to left. This is used to sort control bands when aligning left and when aligning right, respectively. The rest of the class is straightforward and allows the class to be used with the C++ Standard Library container classes. For more information, refer to the section “Introducing the Standard C++ Library and Templates” in Chapter 4, “Advanced Programming with C++Builder.” Referring back to Listing 5.32, we can summarize the operation of ArrangeControlBarBands(). The following steps refer to the steps shown in the listing as comments; for example, step 1 here refers to // Step 1: 1. We create a C++ Standard Library list<> of type TControlBandInfo called BandList and a single variable of type TControlBandInfo called ControlBarBand. This will be used to store information of the current band that we want to align. To use list<>, we must have the include statement #include <list> in our implementation file. 2. We then iterate through each of the controls in ButtonsControlBar and add a TControlBandInfo object to our BandList with each band’s relevant information. While iterating through the controls, we check to see if the control is the current control that we are aligning. If it is, we make an additional copy of its information in the ControlBarBand variable.
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
3. We then sort the list of TControlBandInfos, depending on the alignment we are performing. To align left we use operator < (std::less()), and to align right we use operator > (std::greater()). We use the Standard Library’s less<> and greater<> function objects to tell the sort() member function of list<> which we want to use. In fact, we do not need to specify if we are using operator < because this is the default operator used by sort(). At this stage we also set the Left member of ControlBarBand to the desired value based on the alignment. To use the less<> and greater<> function objects, we must have the include statement #include in our implementation file.
07 9721 ch5
11/13/00
310
9:51 AM
Page 310
C++Builder 5 Essentials PART I
4. We then create an iterator to iterate through our list of TControlBandInfos. For more information about iterators, refer to the section “Introducing the Standard C++ Library and Templates” in Chapter 4. 5. We check that the control we are comparing ControlBarBand with is not ControlBarBand and is Visible. 6. Then, based on the required alignment, we see if there is not enough space on the appropriate side of the current control (NoFreeColumn = true). This will be important if the control occupies the same row or rows as ControlBarBand. 7. If the control that we want to align is occupying the same row as another control, and there is no free space beside the control (NoFreeColumn == true), then we must move it to occupy the row just after that control. If there is free space, we are finished searching and exit the for loop. 8. Otherwise, if we find an unoccupied space, we set the Top member of ControlBarBand to the free top position. 9. We then iterate through the remaining controls that will appear below our control and offset their Tops to allow room for our control. 10. Now that we have calculated the new positions of the control bands, we iterate through them and set their Visible properties to false. When we reach the control that we are aligning, we copy the values from ControlBarBand to the equivalent list entry for the control. We set the visibility of the controls to false so that we can reposition them with ButtonsControlBar, making any changes to the values we assign. 11. Now that the new positions have been stored in our BandList, we sort the list once more to ensure the topmost control is the first. 12. Finally, we iterate through the controls one last time, assigning the new Top and Left values and setting the visibility of the controls back to their original values. Although less complex than it first appeared, ArrangeControlBarBands() is nevertheless quite involved. However, it is still not perfect and can be improved. The control bar is not broken into columns, so it is possible for a control to be moved unnecessarily. Regardless, it does show the main principles involved. LISTING 5.33
TControlBandInfo Class Definition
class TControlBandInfo { public: TControl* Control; int Left; int Top;
07 9721 ch5
11/13/00
9:51 AM
Page 311
User Interface Principles and Techniques CHAPTER 5
LISTING 5.33 int bool
311
Continued Height; Visible;
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
// Constructor inline __fastcall TControlBandInfo() : Control(0), Left(0), Top(0), Height(0), Visible(false) {} // Constructor inline __fastcall TControlBandInfo(TControl* control, int left, int top, int height, bool visible) : Control(control), Left(left), Top(top), Height(height), Visible(visible) {} // Copy Constructor inline __fastcall TControlBandInfo(const TControlBandInfo& ControlBandInfo) : Control(ControlBandInfo.Control), Left(ControlBandInfo.Left), Top(ControlBandInfo.Top), Height(ControlBandInfo.Height), Visible(ControlBandInfo.Visible) {} // OPERATOR = TControlBandInfo& operator=(const TControlBandInfo& ControlBandInfo) { Control = ControlBandInfo.Control; Left = ControlBandInfo.Left; Top = ControlBandInfo.Top; Height = ControlBandInfo.Height; Visible = ControlBandInfo.Visible; return *this; } // OPERATOR == bool operator==(const TControlBandInfo& ControlBandInfo) const { if( Control == ControlBandInfo.Control && Left == ControlBandInfo.Left
07 9721 ch5
11/13/00
312
9:51 AM
Page 312
C++Builder 5 Essentials PART I
LISTING 5.33
Continued
&& Top == ControlBandInfo.Top && Height == ControlBandInfo.Height && Visible == ControlBandInfo.Visible) { return true; } else return false; } // OPERATOR < bool operator<(const TControlBandInfo& ControlBandInfo) const { if(Top < ControlBandInfo.Top) return true; else if( Top == ControlBandInfo.Top && Left < ControlBandInfo.Left) return true; else return false; } // OPERATOR > bool operator>(const TControlBandInfo& ControlBandInfo) const { if(Top < ControlBandInfo.Top) return true; else if( Top == ControlBandInfo.Top && Left > ControlBandInfo.Left) return true; else return false; } };
The only difference between aligning a single control and all controls is that aligning all controls involves calling ArrangeControlBarBands() for all the controls in the control bar. For comparison, Listing 5.34 shows the OnClick event handler for aligning all controls to the right. Note that the controls are arranged in order; a list of TControlBandInfo objects is created and sorted. The TControl* from the sorted list is then used to obtain the Tag for use in the call to ArrangeControlBarBands(). Each control is then aligned to the right. Finally, after sorting, all controls are iterated through once more and their Left properties are set to Screen->Width to ensure right alignment when the control bar is resized. LISTING 5.34
Implementation of AlignAllRight1Click()
void __fastcall TMainForm::AlignAllRight1Click(TObject *Sender) { std::list BandList; for(int i=0; i<ButtonsControlBar->ControlCount; ++i)
07 9721 ch5
11/13/00
9:51 AM
Page 313
User Interface Principles and Techniques CHAPTER 5
LISTING 5.34
313
Continued
{ BandList.push_back(TControlBandInfo(ButtonsControlBar->Controls[i], ButtonsControlBar->Controls[i]->Left, ButtonsControlBar->Controls[i]->Top, ButtonsControlBar->Controls[i]->Height + 2, ButtonsControlBar->Controls[i]->Visible)); } BandList.sort(std::greater()); std::list::iterator pos; for(pos = BandList.begin(); pos != BandList.end(); ++pos) { ArrangeControlBarBands(ButtonsControlBar, TControlBarPanel(pos->Control->Tag), cbaRight); } BandList.sort(std::greater()); for(pos = BandList.begin(); pos != BandList.end(); ++pos) { pos->Control->Left = Screen->Width; } }
Using the methods previously described ensures reasonably predictable aligning behavior; a newly aligning control will not displace an existing aligned control if it shares the same row. Having said that, inconsistency still arises. Nevertheless, the current behavior gives adequate control over the interface.
Controlling Visibility
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
Offering users the ability to show or hide parts of the interface is a relatively easy way to allow user customization. By simply changing the Visible property of a control, you can control whether or not the control appears in the interface. This allows you to provide functionality that some users want but that others may find a nuisance. Those that need the functionality can make the required controls visible, and those that don’t want it can hide the controls. The main consideration with showing and hiding controls is that you must ensure that the appearance of the interface remains acceptable. In other words, hiding a control should not leave a large gap in the interface, and showing a control should not affect the current layout any more than necessary.
07 9721 ch5
11/13/00
314
9:51 AM
Page 314
C++Builder 5 Essentials PART I
The MiniCalculator program gives you the option of showing or hiding any of the controls in ButtonsControlBar as well as the ability to show or hide either the main display (LCDPanel) or the status bar (StatusBar1). This functionality is accessed from the View menu of the program’s main menu. We have already looked at this menu in the previous section, “Using Symbols in Addition to Text.” Here we look at what the menu actually does. When View is clicked and the menu is displayed, the controls whose visibility can be changed are examined one-by-one in the OnClick handler for the View menu item (called View1). This is done to see which are visible and which are not. The implementation is straightforward and is shown in Listing 5.35. LISTING 5.35
Implementation of View1Click()
void __fastcall TMainForm::View1Click(TObject *Sender) { if(LCDPanel->Visible) ViewDisplay->Checked = true; else ViewDisplay->Checked = false; if(NumberButtonsPanel->Visible) ViewNumberButtons->Checked = true; else ViewNumberButtons->Checked = false; if(FunctionButtonsPanel->Visible) ViewFunctionButtons->Checked = true; else ViewFunctionButtons->Checked = false; if(StatusBar1->Visible) ViewStatusBar->Checked = true; else ViewStatusBar->Checked = false; }
Hiding the main display (LCDPanel) is of dubious value, but we need the ability to reshow the panel because it may be hidden when the panel is undocked form the main form and is floating. Hence, we still need a menu entry for this panel. When the ViewDisplay menu item is clicked, LCDPanel is either shown or hidden, depending on its current visibility. The OnClick handler for ViewDisplay is shown in Listing 5. 36. LISTING 5.36
Implementation of ViewDisplayClick()
void __fastcall TMainForm::ViewDisplayClick(TObject *Sender) { if(ViewDisplay->Checked) { LCDPanel->Visible = false; ViewDisplay->Checked = false; if(!LCDPanel->Floating) {
07 9721 ch5
11/13/00
9:51 AM
Page 315
User Interface Principles and Techniques CHAPTER 5
LISTING 5.36
315
Continued
// Set the size of the form... Height = Height - LCDPanel->Height; } } else { if(!LCDPanel->Floating) { // Set the size of the form... Height = Height + LCDPanel->Height; } LCDPanel->Visible = true; ViewDisplay->Checked = true; if(LCDPanel->Floating) { SetFocus(); } //Reset the StatusBar to the bottom if(StatusBar1->Visible) StatusBar1->Align = alBottom; } }
Of all the controls, changing the visibility of LCDPanel is the most involved, because it may be undocked from the main form. However, it is still a reasonably simple operation. If LCDPanel is visible (ViewDisplay->Checked == true), then we hide it (LCDPanel-> Visible = false) and uncheck the menu item (ViewDisplay->Checked = false). Then, if LCDPanel is currently docked on the main form (!LCDPanel->Floating), we adjust the height of MainForm to account for the panel no longer being visible. If LCDPanel is not visible, we first check to see if it is currently docked to the main form (LCDPanel->Floating == false). If it is, we adjust the height of MainForm to accommodate the panel. We then make it visible and check the ViewDisplay menu item. If LCDPanel was undocked from MainForm, making it visible sets input focus to it. If this is the case, then we must reset input focus to the main form using SetFocus(). Finally, if StatusBar1 is visible, we realign it to the bottom of MainForm. TECHNIQUES
USER INTERFACE PRINCIPLES AND
Changing the visibility of StatusBar1 is much more straightforward. The only thing we must remember to do is to change the height of MainForm to account for the change. Listing 5.37 shows this.
5
07 9721 ch5
11/13/00
316
9:51 AM
Page 316
C++Builder 5 Essentials PART I
LISTING 5.37
Implementation of ViewStatusBarClick()
void __fastcall TMainForm::ViewStatusBarClick(TObject *Sender) { if(ViewStatusBar->Checked) { StatusBar1->Visible = false; ViewStatusBar->Checked = false; Height = Height - StatusBar1->Height; } else { Height = Height + StatusBar1->Height; StatusBar1->Visible = true; ViewStatusBar->Checked = true; } }
Finally, we change the visibility of the control bands inside ButtonsControlBar. The code for all bands is similar, so we shall only look at one, the one used to change the visibility of NumberButtonsPanel. Listing 5.38 shows the code. LISTING 5.38
Implementation of ViewNumberButtonsClick()
void __fastcall TMainForm::ViewNumberButtonsClick(TObject *Sender) { if(ViewNumberButtons->Checked) { NumberButtonsPanel->Visible = false; ViewNumberButtons->Checked = false; if(AutoFit->Checked) FitToControlBar(ButtonsControlBar); } else { NumberButtonsPanel->Visible = true; ViewNumberButtons->Checked = true; //Reset the StatusBar to the bottom if(StatusBar1->Visible) StatusBar1->Align = alBottom; } }
When hiding NumberButtonsPanel, we uncheck the related menu item and set NumberButtonsPanel->Visible to false. Then if the AutoFit menu item on the Tools, Options menu is checked, we call the FitToControlBar() helper function to resize ButtonsControlBar to fit the remaining visible controls. If there are no remaining controls, ButtonsControlBar will disappear as its Height becomes 0.
07 9721 ch5
11/13/00
9:51 AM
Page 317
User Interface Principles and Techniques CHAPTER 5
317
When reshowing NumberButtonsPanel, we check the related menu item and set NumberButtonsPanel->Visible to true. We do not need to resize ButtonsControlBar because that will be done automatically to accommodate the newly visible control. Finally, if StatusBar1 is visible, we realign StatusBar1 to the bottom of MainForm.
Customizing the Client Area of an MDI Parent Form Allowing the user to customize the background of an MDI parent form, typically by adding an image to it, is not as easy as it first appears and therefore deserves a special mention. To do this, you must subclass the window procedure of the client window of the parent form. (For more information about subclassing, refer to the “Using Non-Visual Components to Respond to Messages Sent to Other Components” section in Chapter 11, “More Custom Component Techniques.”) This is because the client window of the parent form is the background for the MDI child windows. You must draw on the client window, not the form itself. For more information about this, refer to the Win32 SDK online help under “Frame, Client, and Child Windows.” To access the client window, use the form’s ClientHandle property. To draw on the client window, you must respond to the WM_ERASEBKGND message. The MDIProject.bpr on the CD-ROM contains the code required to display an image on the background of an MDI parent form. The image may be centered, tiled, or stretched. When you are reading the code, you should note two things. First, we draw onto an offscreen bitmap, and then we use either the WinAPI BitBlt() or StretchBlt() function to draw the image onto the client window. This minimizes flicker. Second, we use the Draw() method to draw our image onto the Canvas of the offscreen bitmap. We do this rather than use BitBlt() because we want to support JPEG images. TJPEGImage derives from TGraphic and so implements the Draw() method, but TJPEGImage does not have a Canvas and so cannot be used with BitBlt().
Enhancing Usability by Remembering the User’s Preferences
TECHNIQUES
#include
5 USER INTERFACE PRINCIPLES AND
The easiest way to remember a user’s preferences is to store them in the Registry. C++Builder makes its easy to store and retrieve values to and from the Registry by making available two VCL classes designed for the purpose. These are TRegistry and TRegIniFile. Of these, TRegistry is given a cursory mention in the Developer’s Guide that ships with C++Builder, and we will not expand further on TRegistry here. We are more interested in the TRegIniFile class. The TRegIniFile class makes using the Registry very easy indeed, and you should use it in preference to TRegistry. TRegIniFile descends from TRegistry and offers greater functionality at a higher level of abstraction. To access either TRegistry or TRegIniFile you must add the following include statement to your unit.
07 9721 ch5
11/13/00
318
9:51 AM
Page 318
C++Builder 5 Essentials PART I
The properties and methods of TRegIniFile that you are most likely to need are shown in Table 5.9. TABLE 5.9
The Properties and Methods of TRegIniFile
Property or Method
Description
FileName
Read-only. Returns, as an AnsiString, the Registry path specified when the TRegIniFile object is created. Specifies the root key of the TRegIniFile object. By default this is HKEY_CURRENT_USER. You can change this if this is not the root key you want. Reads a bool value from a specified data value in a key. A default value can be supplied and is used if the entry does not exist. Reads an int value from a specified data value in a key. A default value can be supplied and is used if the entry does not exist. Reads an AnsiString value from a specified data value in a key. A default value can be supplied and is used if the entry does not exist. Writes a bool value to a specified data value in a key. If no such entry exists, it is created. Writes an int value to a specified data value in a key. If no such entry exists, it is created. Writes an AnsiString value to a specified data value in a key. If no such entry exists, it is created.
RootKey
ReadBool()
ReadInteger()
ReadString()
WriteBool() WriteInteger() WriteString()
An important feature of TRegIniFile is that reading from or writing to a Registry entry that does not exist does not result in an error. An alternative default value that you supply is used instead of the missing entry value. When writing, the missing Registry entry is created. This makes using TRegIniFile very simple. is used in the MiniCalculator program to store and read the user’s preferences. Registry entries are written on two occasions. If the user clicks the Save Current Configuration menu item on the Tools menu, then all the current settings for MiniCalculator are stored. Alternatively, if the user has checked the Auto Save Configuration option on the Option submenu of the Tools menu, then the configuration of MiniCalculator is written to the Registry when MiniCalculator is closed. We will look at the code for the first possibility, shown in Listing 5.39. TRegIniFile
07 9721 ch5
11/13/00
9:51 AM
Page 319
User Interface Principles and Techniques CHAPTER 5
LISTING 5.39
319
Implementation of SaveCurrentLayout1Click()
void __fastcall TMainForm::SaveCurrentLayout1Click(TObject *Sender) { std::auto_ptr Registry(new TRegIniFile(“Software\\MiniCalculator”)); // Save the option settings to the Registry Registry->WriteBool(“Options”,”AutoSaveLayout”,AutoSaveLayout->Checked); Registry->WriteBool(“Options”,”AutoFit”,AutoFit->Checked); // Now save all the settings WriteSettingsToRegistry (Registry); }
The first thing we do is create a new TRegIniFile object. We use the C++ Standard Library’s auto_ptr<> to ensure that it is destroyed when the auto_ptr<> goes out of scope, as would happen if an exception were thrown. In the constructor, we pass the sub-entry that we want to access in the Registry. The root key is automatically initialized to HKEY_CURRENT_USER, so the total path is HKEY_CURRENT_USER\Software\MiniCalculator
All our future read and write operations will take place inside this Registry entry. Next we use the WriteBool() method to write the options. The first parameter specifies the key and the second parameter the data name. The final parameter indicates the value of the data: Registry->WriteBool(“Options”,”AutoFit”,AutoFit->Checked);
Either parameter updates or creates a new entry of HKEY_CURRENT_USER\Software\MiniCalculator\Options: Name = AutoFit | Data = ?
If AutoFit->Checked is true, then the data value will be 1. Otherwise it will be 0. We then call WriteSettingsToRegistry() to write all the settings of MiniCalculator to the Registry. Listing 5.40 shows the code. LISTING 5.40
Implementation of WriteSettingsToRegistry()
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
void __fastcall TMainForm::WriteSettingsToRegistry(const std::auto_ptr& Registry) { // The LCDPanel
07 9721 ch5
11/13/00
320
9:51 AM
Page 320
C++Builder 5 Essentials PART I
LISTING 5.40
Continued
// - Color Registry->WriteInteger(“Settings\\Display\\Color”, “SurroundColor”, LCDPanel->Color); Registry->WriteInteger(“Settings\\Display\\Color”, “BackgroundColor”, BackgroundPanel->Color); Registry->WriteInteger(“Settings\\Display\\Color”, “ExponentColor”, ExponentEditColor); // - Docking Registry->WriteBool(“Settings\\Display”, “Floating”, LCDPanel->Floating); Registry->WriteInteger(“Settings\\Display”, “UndockWidth”, LCDPanel->UndockWidth); Registry->WriteInteger(“Settings\\Display”, “UndockHeight”, LCDPanel->UndockHeight); if(LCDPanel->Floating) { TRect UndockedRect; if(GetWindowRect(LCDPanel->HostDockSite->Handle, &UndockedRect)) { Registry->WriteInteger(“Settings\\Display”, “UndockLeft”, UndockedRect.Left); Registry->WriteInteger(“Settings\\Display”, “UndockTop”, UndockedRect.Top); Registry->WriteInteger(“Settings\\Display”, “UndockRight”, UndockedRect.Right); Registry->WriteInteger(“Settings\\Display”,
07 9721 ch5
11/13/00
9:51 AM
Page 321
User Interface Principles and Techniques CHAPTER 5
LISTING 5.40
321
Continued “UndockBottom”, UndockedRect.Bottom);
} } // The Main Form Registry->WriteInteger(“Settings\\Position”,”MainFormTop”,Top); Registry->WriteInteger(“Settings\\Position”,”MainFormLeft”,Left); Registry->WriteInteger(“Settings\\Size”,”MainFormHeight”,Height); Registry->WriteInteger(“Settings\\Size”,”MainFormWidth”,Width); // The Status Bar Registry->WriteBool(“Settings\\StatusBar”,”Visible”,StatusBar1->Visible); Registry->WriteBool(“Settings”,”EnableKeyboard”,EnableKeyboardInput); // The Control Bar settings for(int i=0; i<ButtonsControlBar->ControlCount; ++i) { AnsiString ControlPath = “Settings\\ControlBar\\”; ControlPath += ButtonsControlBar->Controls[i]->Name; Registry->WriteInteger(ControlPath, “Left”, ButtonsControlBar->Controls[i]->Left); Registry->WriteInteger(ControlPath, “Top”, ButtonsControlBar->Controls[i]->Top); Registry->WriteInteger(ControlPath, “Height”, ButtonsControlBar->Controls[i]->Height+2); Registry->WriteBool(ControlPath, “Visible”, ButtonsControlBar->Controls[i]->Visible); } }
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
The code to write values to the Registry is pretty straightforward. The hardest decision is what to write to the Registry. The only thing of particular note in this regard is the coordinates of the LCDPanel if it is undocked from the main form (Floating). If LCDPanel is undocked from the main form, it will be contained by an object of type FloatingDockSiteClass. The default type paints a border and title bar around the floating control. We need the bounding rectangle of the control’s docking host to accurately redisplay the panel in the same position. To access the
07 9721 ch5
11/13/00
322
9:51 AM
Page 322
C++Builder 5 Essentials PART I
docking host, we read the HostDockSite property. To get the bounding rectangle of the host dock site, we pass its window handle to the WinAPI function GetWindowRect() along with the address of a TRect to hold the boundary values, as follows: TRect UndockedRect; if(GetWindowRect(LCDPanel->HostDockSite->Handle, &UndockedRect)) { ... // code removed for clarity }
For ButtonsControlBar we iterate through all the controls and write the necessary values of each to a key equal to the control’s name. This will allow us to distinguish which entries are for which control when we read the Registry entries later. As you can see, writing to the Registry couldn’t really be any easier. The tricky part is knowing what to do with the information when you read it back in from the Registry. We will look at this now. Whenever MiniCalculator is executed and the constructor of MainForm is called, we call the function ReadAllValuesFromRegistry() to read in all the values we have stored there. The first time we do this there will be no Registry entries, but that doesn’t matter. The default values that we supply for each option or the settings that we want to read are used instead. Listing 5.41 shows the implementation of ReadAllValuesFromRegistry(). LISTING 5.41
Implementation of ReadAllValuesFromRegistry()
void __fastcall TMainForm::ReadAllValuesFromRegistry() { //Try to read the required setup values from the Registry //If this is the first time the program has been executed //then there will be no Registry keys std::auto_ptr Registry(new TRegIniFile(“SOFTWARE\\MiniCalculator”)); // Read the options AutoSaveLayout->Checked = Registry->ReadBool(“Options”,”AutoSaveLayout”,AutoSaveLayout->Checked); AutoFit->Checked = Registry->ReadBool(“Options”,”AutoFit”,AutoFit->Checked); // Now read the settings ReadSettingsFromRegistry (Registry); }
07 9721 ch5
11/13/00
9:51 AM
Page 323
User Interface Principles and Techniques CHAPTER 5
323
The code in ReadAllValuesFromRegistry() is pretty simple. We read the two options values and then read the rest of the settings by calling the function ReadSettingsFromRegistry(). Notice how we use the current value of each option as the default value (the third argument). This ensures that the absence of a Registry entry has no effect. The option is simply assigned its current value. You will notice this throughout Listing 5.42, which shows the implementation of the ReadSettingsFromRegistry() function. LISTING 5.42
Implementation of ReadSettingsFromRegistry()
void __fastcall TMainForm::ReadSettingsFromRegistry(const std::auto_ptr& Registry) { // The LCDPanel // - Color LCDPanel->Color = Registry->ReadInteger(“Settings\\Display\\Color”, “SurroundColor”, LCDPanel->Color); BackgroundPanel->Color = Registry->ReadInteger(“Settings\\Display\\Color”, “BackgroundColor”, BackgroundPanel->Color); ExponentViewColor = BackgroundPanel->Color; ExponentEditColor = Registry->ReadInteger(“Settings\\Display\\Color”, “ExponentColor”, ExponentEditColor);
// - Docking LCDPanel->UndockWidth = Registry->ReadInteger(“Settings\\Display”, “UndockWidth”, LCDPanel->UndockWidth); if(LCDPanel->UndockWidth > Screen->Width) { LCDPanel->UndockWidth = Screen->Width; } LCDPanel->UndockHeight = Registry->ReadInteger(“Settings\\Display”, “UndockHeight”, LCDPanel->UndockHeight); TECHNIQUES
USER INTERFACE PRINCIPLES AND
bool Floating = Registry->ReadBool(“Settings\\Display”, “Floating”, LCDPanel->Floating);
5
07 9721 ch5
11/13/00
324
9:51 AM
Page 324
C++Builder 5 Essentials PART I
LISTING 5.42
Continued
if(Floating) { int UndockLeft = Registry->ReadInteger(“Settings\\Display”, “UndockLeft”, LCDPanel->Left); int UndockTop = Registry->ReadInteger(“Settings\\Display”, “UndockTop”, LCDPanel->Top); TRect UndockedRect(UndockLeft, UndockTop, UndockLeft + LCDPanel->UndockWidth, UndockTop + LCDPanel->UndockHeight); int UndockRight = Registry->ReadInteger(“Settings\\Display”, “UndockRight”, UndockedRect.Right); int UndockBottom = Registry->ReadInteger(“Settings\\Display”, “UndockBottom”, UndockedRect.Bottom); if(UndockRight > Screen->Width) { int Offset = UndockRight - Screen->Width; UndockedRect.Right -= Offset; UndockedRect.Left -= Offset; if(UndockedRect.Left < 0) UndockedRect.Left = 0; } if(UndockBottom > Screen->Height) { int Offset = UndockBottom - Screen->Height; UndockedRect.Bottom -= Offset; UndockedRect.Top -= Offset; if(UndockedRect.Top < 0) { Offset = 0 - UndockedRect.Top; UndockedRect.Top = 0; UndockedRect.Bottom += Offset; } } LCDPanel->ManualFloat(UndockedRect); }
07 9721 ch5
11/13/00
9:51 AM
Page 325
User Interface Principles and Techniques CHAPTER 5
LISTING 5.42
325
Continued
// The Main Form int top, left, height, width; top left height width
= = = =
Registry->ReadInteger(“Settings\\Position”,”MainFormTop”,Top); Registry->ReadInteger(“Settings\\Position”,”MainFormLeft”,Left); Registry->ReadInteger(“Settings\\Size”,”MainFormHeight”,Height); Registry->ReadInteger(“Settings\\Size”,”MainFormWidth”,Width);
if(width > Screen->Width) width = Screen->Width; // disaster! if(left+width > Screen->Width) { left -= (left+width) - Screen->Width; } if(height > Screen->Height) height = Screen->Height; // disaster! if(top+height > Screen->Height) { top -= (top+height) - Screen->Height; } Top = top; Left = left; Height = height; Width = width;
// The Status Bar StatusBar1->Visible = Registry->ReadBool(“Settings\\StatusBar”, “Visible”, StatusBar1->Visible); //if(!StatusBar1->Visible) Height -= StatusBar1->Height; EnableKeyboardInput = Registry->ReadBool(“Settings”, “EnableKeyboard”, EnableKeyboardInput); // The Control Bar std::list BandList;
TECHNIQUES
int ControlLeft
5 USER INTERFACE PRINCIPLES AND
for(int i=0; i<ButtonsControlBar->ControlCount; ++i) { AnsiString ControlPath = “Settings\\ControlBar\\”; ControlPath += ButtonsControlBar->Controls[i]->Name;
07 9721 ch5
11/13/00
326
9:51 AM
Page 326
C++Builder 5 Essentials PART I
LISTING 5.42
Continued
= Registry->ReadInteger(ControlPath, “Left”, ButtonsControlBar->Controls[i]->Left); int ControlTop = Registry->ReadInteger(ControlPath, “Top”, ButtonsControlBar->Controls[i]->Top); int ControlHeight = Registry->ReadInteger(ControlPath, “Height”, ButtonsControlBar->Controls[i]->Height+2); bool ControlVisible = Registry->ReadBool(ControlPath, “Visible”, ButtonsControlBar->Controls[i]->Visible); BandList.push_back(TControlBandInfo(ButtonsControlBar->Controls[i], ControlLeft, ControlTop, ControlHeight, ControlVisible)); } BandList.sort(); std::list::iterator pos; for(pos = BandList.begin(); pos != BandList.end(); ++pos) { pos->Control->Visible = false; } for(pos = BandList.begin(); pos != BandList.end(); ++pos) { pos->Control->Top = pos->Top; pos->Control->Left = pos->Left; pos->Control->Visible = pos->Visible; } // Reset any size adjustments made by ButtonsControlBar Top = top; Left = left; Height = height; Width = width; }
07 9721 ch5
11/13/00
9:51 AM
Page 327
User Interface Principles and Techniques CHAPTER 5
327
As was stated earlier, writing to the Registry is easy. Reading from the Registry is equally easy. Deciding what to do with the data is slightly more complex, as you can see from Listing 5.42. Much of Listing 5.42 is self-explanatory but the handling of the LCDPanel data and the ButtonsControlBar data requires further explanation. If LCDPanel is floating, then we need to use the ManualFloat() method to undock the control. For this we need a TRect variable containing the correct region in which the panel should be displayed. We already stored this information in the UndockLeft, UndockTop, UndockRight, and UndockBottom Registry values. We also stored LCDPanel’s UndockWidth and UndockHeight. Your first thought will probably be to use the four UndockXXX coordinates to create a suitable TRect and then use this in the call to ManualFloat. This is not correct. A quirk of the implementation of ManualFloat is that it doesn’t use the TRect you pass in the manner you expect. The Left and Top properties represent the Left and Top of the floating docking host (not the LCDPanel), but the Right and Bottom properties do not represent the Right and Bottom of the floating docking host. Instead, the Right and Bottom properties are used to calculate the Width and Height of LCDPanel. Therefore, we must create our TRect as follows: TRect UndockedRect(UndockLeft, UndockTop, UndockLeft + LCDPanel->UndockWidth, UndockTop + LCDPanel->UndockHeight);
The remainder of the code for LCDPanel is concerned with repositioning the floating panel if it appears off the screen at lower resolutions. The interpretation of the Registry entries for ButtonsControlBar is also problematic. We must sort the controls into order of increasing Top properties, and we must then draw each control onto the control bar. We need to draw the topmost controls first because TControlBar automatically aligns controls either to the top or to just below an existing control. If we put the bottom-most controls on first, they would be moved to the top, and we do not want this to happen. The code used to sort controls is the same as that used in the ArrangeControlBarBands() method in Listing 5.32 and described in the section, “Aligning Controls Inside TControlBar,” earlier in this chapter. We create a list of TControlBandInfo objects, sort the list, make all the controls invisible (Visible = false), and then replace each visible control one by one, starting with the topmost. The TControlBandInfo class is shown in Listing 5.33 and also is described in the earlier section, “Aligning Controls Inside TControlBar.”
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
Notice that we reposition and resize the main form if the resolution causes the main form to be either too large or moved off the screen. This is an example of how to make your programs capable of handling varying screen conditions. This is the subject of the next section.
07 9721 ch5
11/13/00
328
9:51 AM
Page 328
C++Builder 5 Essentials PART I
Coping with Differing Screen Conditions This section looks at how to detect varying screen conditions so that you can display your interface consistently on different computers with different screen settings. There are three that should always be thought of: screen resolution, font sizes, and color depths. Of these three, resolution and font size are the most important. Limited color depth is not normally a major concern unless you design an interface that requires true color depth.
Coping with Different Screen Resolutions To determine the urrent resolution of the screen, read the global Screen variable’s Width and Height properties. You can then use each control’s ScaleBy property to scale each control so that the interface has the same apparent size at different screen resolutions. It is important to realize that there are limits to this. If you have designed an interface that is quite large, you may not be able to scale it down for lower resolutions; you may have to simply make it smaller. By noting the screen resolution at which the application is developed, you can calculate the size difference ratio by dividing the screen height at development time by the screen height at runtime. You can use this when setting the ScaleBy property. This technique is outlined in the Developer’s Guide that ships with C++Builder, and you should refer there for more information.
Coping with Different Font Sizes When you create an application, the PixelsPerInch property of the forms you use indicates the font size used during development. By default, TForm’s Scaled property is set to true. This means that your form and the controls on them will be automatically resized when the font size changes. In many cases, such as in the MiniCalculator program, you do not want this to happen. To prevent this automatic scaling, set Scaled to false. To determine the font size at runtime, simply read the PixelsPerInch property of the global Screen variable.
Coping with Different Color Depths The need to cope with different color depths arises when you develop an application on a machine at a higher color depth than that provided by the current machine running the application. If your application makes use of colors outside those available at the lower color depth, the operating system will display the application using the closest match for each color. This can sometimes turn a nice interface into an awful one. If this is a possibility with your application, then you should take steps to avoid it. There are two approaches. The first and simplest is to limit the colors used in the interface to those of the lowest color depth at which your application will be used. This will typically be
07 9721 ch5
11/13/00
9:51 AM
Page 329
User Interface Principles and Techniques CHAPTER 5
329
16-bit color and is more than adequate for most purposes. The second is to determine the color depth at runtime and supply different images for the interface according to the color depth available. To determine the current color depth of the screen at runtime, use the WinAPI GetDeviceCaps() function, passing the Canvas->Handle of one of your visual controls, such as the main form. The code required is int ColorDepth = 0; if(GetDeviceCaps(Canvas->Handle, RASTERCAPS) & RC_PALETTE) { ColorDepth = GetDeviceCaps(Canvas->Handle, COLORRES); } else { ColorDepth = GetDeviceCaps(Canvas->Handle, BITSPIXEL) * GetDeviceCaps(Canvas->Handle, PLANES); }
The sample project on the CD-ROM that accompanies this book, ScreenInfo.bpr, displays the color depth, font size, and screen resolution to illustrate determining these values at runtime.
Coping with Complexity in the Implementation of the User Interface Programming the interface of an application is a very difficult task. Taking the MiniCalculator program as an example, you will probably agree that it doesn’t do anything startling; in fact, the code required to make it add, subtract, multiply, and divide is very short, approximately 10 to 20 lines. A few hundred lines of code is used to allow conversions between number bases and so on, but the remaining 2,500 or so lines are devoted exclusively to the interface. Even with that, the interface is far from complete. Any techniques or tools that can help keep the complexity of the interface code to a minimum are therefore of great benefit. We look briefly at two that were used in the MiniCalculator program.
Using Action Lists
5 TECHNIQUES
USER INTERFACE PRINCIPLES AND
An action list’s sole purpose in life is to help make interface programming easier. It does this by centralizing code. Using action lists and the theory behind them is explained reasonably well in the Developer’s Guide (and online help) that ships with C++Builder 5, but it doesn’t hurt to show a practical example.
07 9721 ch5
11/13/00
330
9:51 AM
Page 330
C++Builder 5 Essentials PART I
In MiniCalculator, an action list called ActionList1 is used to centralize the copying action performed by the menu items in MainMenu’s Copy menu and also the copying action performed by the menu items in CopyPopupMenu. The MiniCalculator program allows a copy to be made of the current history, the current number being displayed, or the current number in the memory. An action is created for each: CopyHistoryAction, CopyNumberAction, and CopyMemoryAction. Each action has its own keyboard shortcut, which we specify by setting the action’s ShortCut property. CopyHistoryAction uses Ctrl+H, CopyNumberAction uses Ctrl+N, and CopyMemoryAction uses Ctrl+M. Finally, all that remains is to implement each action’s OnExecute event. Listing 5.43 shows the implementation of each. LISTING 5.43
Implementation of OnExecute for the actions CopyHistoryAction,
CopyNumberAction, and CopyMemoryAction //---------------------------------------------------------------------------// void __fastcall TMainForm::CopyHistoryActionExecute(TObject *Sender) { AnsiString HistoryString = HistoryLabel->Caption; Clipboard()->AsText = HistoryString; } //---------------------------------------------------------------------------// void __fastcall TMainForm::CopyNumberActionExecute(TObject *Sender) { AnsiString NumberString = LCDScreen->Caption; if(ExponentLabel->Caption != “E+0” && ExponentLabel->Caption != “E-0”) { NumberString += ExponentLabel->Caption; } Clipboard()->AsText = NumberString; } //---------------------------------------------------------------------------// void __fastcall TMainForm::CopyMemoryActionExecute(TObject *Sender) { Clipboard()->AsText = MemoryString; } //---------------------------------------------------------------------------//
Now that we have our actions defined, to use them with our menus we simply set the Action property of each menu item to the correct action. For example, in CopyPopupMenu, we set the Action property of the menu item called CopyHistory1 to CopyHistoryAction. We also set the Action property of the History1 menu item of the Copy menu in MainMenu to
07 9721 ch5
11/13/00
9:51 AM
Page 331
User Interface Principles and Techniques CHAPTER 5
331
CopyHistoryAction.
For both menu items, the ShortCut properties, ImageIndex properties, and Caption properties are all changed to reflect those in CopyHistoryAction. In addition, their OnClick events are set equal to CopyHistoryAction’s OnExecute event. Hence, the action is executed when the item is clicked. The example presented here, while simple, still benefits from the ease of coding that actions bring. When designing an interface with many menus and toolbars, actions can be indispensable. Another example of using actions can be found in the MDIProject.bpr project on the accompanying CD-ROM. As the name suggests, this is an MDI project and, like most MDI applications, it has a Windows menu that allows the user to arrange the child windows in various ways: tiling horizontally, tiling vertically, and cascading. To do this, we add one each of the standard actions TWindowTileHorizontal, TWindowTileVertical, and TWindowCascade to a TActionList. We do not need to write any code; all we do is assign the Action properties of the appropriate menu items to each standard action in the action list. If we provide a TImageList component and set the Images property of the action list to the image list, an image for each of the standard actions will be automatically added to the image list when the action is added to the action list. If we also assign the SubMenuImages property of the Windows menu item (called Window1) to the same image list, the images will automatically appear in the menu for each menu item as appropriate. In MDIProject.bpr, custom images are used.
Sharing Event Handlers We’ve already seen an example of sharing event handlers earlier in this chapter, in the section “Using Symbols in Addition to Text.” We will now examine the technique a little more closely and see how it can be used to reduce the complexity of user interface–related code.
TECHNIQUES
In MiniCalculator, this technique is employed several times. One such use is the implementation of OnClick events for the number buttons. Because each number button differs only in the
5 USER INTERFACE PRINCIPLES AND
When designing user interfaces, particularly those for data entry, it is common to create an interface consisting of several instances of one control to a form. Controls used in this way will generally perform very similar functions, or at least interpret the data entered to them in a similar way. For example, if you created a data entry form consisting of multiple TEdit components, you may want to use each TEdit component’s OnExit event to place the contents of each TEdit control into a structure or array for use elsewhere in the program. If such a form contained 10 TEdit controls, then we’d normally need 10 corresponding OnExit event handlers, one for each control. Because each event handler would perform an essentially similar task, we are duplicating code unnecessarily. For large interfaces this can quickly lead to long and complex code that becomes difficult to maintain. One way to prevent this is for all controls that perform similar tasks to share one event handler.
07 9721 ch5
11/13/00
332
9:51 AM
Page 332
C++Builder 5 Essentials PART I
number it represents, a single event handler can be shared among them. The OnClick event for each button is assigned to our single event handler, NumberSpeedButtonClick(), in MainForm’s constructor.
TIP It is possible (and desirable) to perform this assignment at designtime. Move the declaration for NumberSpeedButtonClick() to the IDE-managed part of MainForm’s header file. This will be the published section of the form’s class definition, with the comment // IDE-managed Components You can then select the OnClick handler by activating the drop-down list for each number button’s OnClick event in the Object Inspector. This method was used to assign the OnClick events of the color panel components used in the SettingsForm form.
Using this method means we must have some way of determining which button was clicked. To do this we store an enum value representing each button in the button’s Tag property. The enum we use is declared as enum TCalculatorButton { cb1=’1’, cb2=’2’, cb3=’3’, cb4=’4’, cb5=’5’, cb6=’6’, cb7=’7’, cb8=’8’, cb9=’9’, cbA=’A’, cbB=’B’, cbC=’C’, cbD=’D’, cbE=’E’, cbF=’F’, cb0, cbSign, cbPoint, cbExponent, cbAdd, cbSubtract, cbMultiply,
07 9721 ch5
11/13/00
9:51 AM
Page 333
User Interface Principles and Techniques CHAPTER 5
333
cbDivide, cbEquals, cbBackspace, cbClear, cbAllClear, cbMemoryAdd, cbMemoryRecall };
For convenience, we set each of the numbered buttons’ enum values equal to the character that represents it. This allows the Tag of each numbered button to be used directly to update the AnsiString that represents the calculator display. What matters here is that the enum value for each calculator button is unique. The appropriate enum for each button is assigned in MainForm’s constructor. For example, for the number 1 button we write SpeedButton1->Tag = cb1;
Listing 5.44 shows the implementation of NumberSpeedButtonClick(). LISTING 5.44
Implementation of NumberSpeedButtonClick()
void __fastcall TMainForm::NumberSpeedButtonClick(TObject *Sender) { TSpeedButton* SpeedButton = dynamic_cast(Sender); if(SpeedButton) { ButtonPressNumber(static_cast(SpeedButton->Tag)); ButtonUp(static_cast(SpeedButton->Tag)); } }
The first thing we do in NumberSpeedButtonClick() is dynamic_cast Sender to a TSpeedButton pointer. If successful, we know that a TSpeedButton fired the event, so we proceed. The next step simply calls the function ButtonPressNumber(), which updates the display with the number we pressed. ButtonPressNumber() expects a TCalculatorButton. Even though we assigned a TCalculatorButton to the Tag of each button, Tag is an int by definition. To remove the compiler warning telling us this, we must static_cast our Tag property to the TCalculatorButton enum type. Finally, we call ButtonUp() to make sure the button is released and cast the Tag property to the correct type. TECHNIQUES
USER INTERFACE PRINCIPLES AND
The technique described is simple and effective. There is a problem in assigning numbers only to the Tag property. Code can quickly become difficult to read because numbers on their own have little or no meaning. To maintain readability, MiniCalculator uses several enums to make it clear exactly what is being stored in the Tag property for each control.
5
07 9721 ch5
11/13/00
334
9:51 AM
Page 334
C++Builder 5 Essentials PART I
Another method of determining which control fired an event is to compare the Sender pointer with that of each control until a match is made. For example if(Sender == SpeedButton1) else if(Sender == SpeedButton2) else if(Sender == SpeedButton3) else if(Sender == SpeedButton4) // and so on ...
{ { { {
/* /* /* /*
code code code code
here here here here
*/ */ */ */
} } } }
This is a simple and effective approach. However, we used the Tag property approach because it offers greater versatility, such as allowing the Tag value to represent something other than simply an identifier. For example, it can represent the value of a button in addition to its identity.
Summary In this chapter we took an in-depth look at user interface design and implementation. We discussed user interface design philosophy and noted that above all a user interface should meet users’ expectations and be intuitive. Using the example MiniCalculator application, we saw how C++Builder can be employed to create effective, professional-quality user interfaces that give users ready access to all of an application’s functionality. We closely examined the use of some of C++Builder’s most oftenused standard controls, including TStatusBar, TSpeedButton, TLabel, TPanel, and TControlBar. We also saw simple and advanced techniques such as using the cursor, using hints, input focus control, customized buttons, shaped controls, menus, docking, resizing, anchors, and constraints; remembering users’ preferences by storing data in the Windows Registry; coping with different screen conditions; and managing user interface code complexity. In fact, we have just scratched the surface! It has been impossible to include or even mention many aspects of interface programming, notably localization and internationalization. This is a large topic that deserves a chapter of its own. For an introduction to the issues involved, refer to the “Language Internationalization and Localization” section of Chapter 28, “Software Distribution.” Creating an effective interface is a real challenge, as you will have gathered from reading this chapter. But it is a rewarding challenge with something concrete to see at the end. You may even want to experiment further with the MiniCalculator program to see if you can turn it into a really useful application; the foundation is there to build on. If you are interested in floatingpoint number handling, refer to the “Programming Using Floating-Point Numbers” section in Chapter 30, “Tips, Tricks, and How Tos.” This chapter should provide a good foundation. Using the techniques and advice presented here should help you design application user interfaces that not only have visual impact but are also a blessing to use.
08 9721 CH06
11/13/00
9:53 AM
Page 335
Compiling and Optimizing Your Application Jarrod Hollingworth
IN THIS CHAPTER • Understanding How the Compiler Works • Speeding Up Compile Times • Exploring the C++Builder 5 Compiler and Linker Enhancements • Optimizing: An Introduction • Optimizing for Execution Speed • Optimizing Other Aspects of Your Application
CHAPTER
6
08 9721 CH06
11/13/00
336
9:53 AM
Page 336
C++Builder 5 Essentials PART I
This chapter will explain how the compiler and linker work. You’ll see how to get the most speed out of your compiles and applications by using several speed-optimizing techniques. Other optimization aspects will also be discussed. Compiling and optimizing are interrelated techniques. Compiling is the process of taking highlevel code and data that programmers can work with and transforming it into low-level code and data that the CPU can use. Optimizing is the process of streamlining some aspect of an application, such as speed or size. As we’ll see, optimizing at a very low level can involve reordering machine code instructions and choosing the best set of instructions for the job. This is handled automatically to some extent by C++Builder’s optimizing compiler. Compiling and optimizing are processes that all programmers should understand. Compiler knowledge and an understanding of the machine code that the compiler generates will assist your high-level programming skills and help you with debugging. Optimizing should be a priority in your project goals. Throughout this chapter, the term compiler refers to C++Builder’s optimizing compiler as a whole. The term optimizer refers to the optimization process of the compiler in particular. The optimizer is not a separate utility but rather is integrated with the compiler. By the end of this chapter you will be more productive, have a better understanding of how applications work, and know how to streamline your applications. This chapter is also recommended as a prerequisite to Chapter 7, “Debugging Your Application.”
Understanding How the Compiler Works The compiler is one of the most commonly used features in C++Builder. It is also one of the most taken-for-granted and cursed features. In any case, the compiler is an integral part of C++Builder, without which most other features are virtually useless! What goes on when you select Make or Build? You get a running version of your application, bugs aside. Behind the scenes, it is quite a lot more involved than that. To build an application, the C++Builder compiler processes each unit in turn, performing several compilation phases on your code to produce an executable or DLL. These phases are outlined in the following list: • Phase 1: Preprocess Preprocessor directives are invoked, and macros are expanded throughout the source code. Header files are included. • Phase 2: Tokenizing and Parsing Output code from the preprocessor is analyzed, including the examination of tokens in the source and the creation of a syntax tree to allow code generation. • Phase 3: Code Generation Native machine instructions for each statement are generated. The code is optimized, using instruction pairing and other processor-dependent features. Machine code is written to disk in an .obj file. Debug information is written to the end of the file if necessary.
08 9721 CH06
11/13/00
9:53 AM
Page 337
Compiling and Optimizing Your Application CHAPTER 6
The C++Builder compiler uses what’s called the recursive descent model, with infinite lookahead. This basically means that preprocessing, tokenizing, parsing, and code generation are all performed at the same time during compilation in recursive steps, not with multiple passes like some other compilers. Various optimizations, such as subexpression folding (const math computations, for example), inlining, and integral constant replacements, are performed in the high-level (front-end) compilation phases. The expression tree is also highly compacted. Other optimizations such as scheduling and invariant code optimizations are performed in the low-level (backend) compilation phases. Optimizations are not performed across function boundaries. With its various optimizations, the compiler already produces fast code, compared to other mainstream compilers, but look for some tweaks to appear in the future, perhaps in C++Builder 6. For more information on compilers and how they work, see the following: • Introduction to Compiling Techniques, by J.P. Bennett, Second Edition, McGraw-Hill 1996, ISBN 0-07-709221-X. • Compilers and Compiler Generators. This is an excellent and comprehensive online book that can also be downloaded as PDF files. http://www.scifac.ru.ac.za/ compilers/
There are many books available on compilers, how they work, and how to make one. Check your local university library. Transcripts of several university courses covering compilers are also available on the Internet. Most compiler and linker options can be set from the Compiler, Advanced Compiler, Linker, and Advanced Linker tabs of the Project Options dialog. However, there are a few that can only be set in the project file or used on the command line. As discussed in Chapter 2, “C++Builder Projects and More on the IDE,” C++Builder 5 uses a new project file format based on Extensible Markup Language (XML). To use the remaining compiler and linker options, you need to modify the project file. You can open the project file in the IDE by selecting Project, Edit Option Source. Compiler options are set in the section of the project file in the entry, linker flags in , resource compiler options in , Pascal compiler options in , and assembler options in .
6 COMPILING AND OPTIMIZING YOUR APPLICATION
• Phase 4: Link The linker reads in all segment definitions from each object file and creates a global symbol table. A list of symbols necessary for the executable is generated. The linker then combines the compiled code from the required units, resources, and form files with statically linked libraries and a special startup object file to create the resulting executable or DLL file.
337
08 9721 CH06
11/13/00
338
9:53 AM
Page 338
C++Builder 5 Essentials PART I
TIP In the Examples folder where C++Builder was installed, you’ll find a fantastic program called WinTools. It lists the various command-line options for the compiler and other command-line tools provided with C++Builder, and it has some great built-in functions!
A complete listing of compiler, linker, and other command-line utility options are available in the C++Builder 5 help file.
Speeding Up Compile Times The C++Builder compiler is fast! It compiles C++ code almost twice as fast as the GNU C++ compiler and is comparable in speed to the Microsoft Visual C++ compiler. If you’ve used Delphi before, and you think that the C++Builder compiler takes much longer to compile a similar size application, you’re right. The relatively slow compilation speed of C++ when compared to Delphi’s Object Pascal is due to several reasons: • C++ allows for header (include) files. Object Pascal does not. Header files can be nested, and this can set up a lot of complex code to be processed. A simple 10-line program may be several hundred thousand lines long because of header file nesting, which takes up most of the compile time. • C++ has macros. Object Pascal does not. Macros require a preprocessor to parse and expand them. • C++ has templates. Object Pascal does not. Templates are very complex to analyze. • C++ semantics must conform to the ANSI standard. The “grammar” of C++ is somewhat more complex than that of Delphi, which is based on Pascal but developed to Borland’s standard. In general, C++ provides more flexibility in program design than Delphi’s Object Pascal. However, this comes at the expense of compile time and in some cases code readability. There are several simple methods you can employ to speed up your C++Builder compile times. The most dramatic improvement can be achieved by using precompiled headers. This and other methods are described in the following sections.
Precompiled Headers Precompiled headers are presented as a set of options on the Compiler tab of the Project Options dialog. When enabled by checking either Use Precompiled Headers or Cache Precompiled Headers, the compiler stores a compiled binary image of header files included in the various units in a disk-based file (vcl50.csm in the C++Builder lib directory by default).
08 9721 CH06
11/13/00
9:53 AM
Page 339
Compiling and Optimizing Your Application CHAPTER 6
The #pragma hdrstop directive in a unit causes the compiler to stop generating precompiled headers at that point. It is important to note that the order of the header files before the #pragma hdrstop directive in each unit is significant. Changing the order of the header files in two separate units can change the code resulting from those header files in each unit. Therefore, this requires both lists of header files to be compiled and stored separately as precompiled header groups. Header files after the #pragma hdrstop directive are processed each time the unit is compiled. Typically, you should include header files common to two or more units before this directive so that they are compiled once only. Include all header files specific to each unit after the directive. By doing this, we are trying to get the most common match between header file lists in each unit to obtain the most benefit from this option. The IDE automatically inserts the #pragma hdrstop directive in new units and places VCL header files included before the directive and unit specific header files after the directive. A good example of header file grouping and order is shown in the top section of the fictional units LoadPage.cpp and ViewOptions.cpp in Listings 6.1 and 6.2. LISTING 6.1
Precompiled Header File Group in LoadPage.cpp
LISTING 6.2
//------------------------------------// LoadPage.cpp
//------------------------------------// ViewOptions.cpp
#include #include <System.hpp> #include <Windows.hpp> #include “SearchMain.h” #pragma hdrstop
#include #include <System.hpp> #include <Windows.hpp> #include “SearchMain.h” #pragma hdrstop
#include “LoadPage.h” #include “CacheClass.h” //-------------------------------------
#include #include “ViewOptions.h” //-------------------------------------
// Code here...
// Code here...
Precompiled Header File Group in ViewOptions.cpp
6 COMPILING AND OPTIMIZING YOUR APPLICATION
Subsequent use of the same sequence of header files in another unit dramatically speeds up that unit’s compile time by using the header files previously compiled. Selecting Cache PreCompiled Headers causes the compiler to load the precompiled headers in memory to further speed up the compile process.
339
08 9721 CH06
11/13/00
340
9:53 AM
Page 340
C++Builder 5 Essentials PART I
By effectively grouping header files included in each unit and using precompiled headers, you can often see compile speeds increase up to 10 times!
NOTE For information on speeding up compile times even further using precompiled headers, there is an excellent article on the BCBDEV Web site at http://www.bcbdev.com/ under the Articles link.
Other Techniques for Speeding Up Compile Times There are other techniques that can be used to speed up compile times. They aren’t as effective as using correctly grouped precompiled headers, but they are worth considering if compile speed is very important, particularly on large projects. You should be careful about which header files are included in your units. Compiling unnecessary code is wasteful of precious compile time, so in general you should not include unused header files. However, if you have included an unused header file in a unit to preserve header grouping when using precompiled headers, leave it in. Also, avoid changing header files too often. Each time you change a header file, the precompiled header groups that use this header file must be regenerated. Use Make instead of Build. When Make is selected, the compiler attempts to detect which source files have been modified since they were last compiled and compiles only those. Build, on the other hand, will recompile every source file in the project. Obviously Build will take more time than Make, but there are times where Build is required. is recommended after changing project options and when files are checked out or updated from a version control system. You should also use Build when compiling a release version of your application. This could be a debug or beta build going to testers or the final version to ship. Build
You should uncheck the Don’t Generate State Files option on the Linker tab of Project Options. This will speed up subsequent compiles (particularly the first compile when re-opening the project and when working with multiple projects in the IDE) as the linker saves state information in a file. If you are not in a debugging phase for the project, disable all debugging options by selecting the Release button on the Compiler tab of Project Options and uncheck Use Debug Libraries on the Linker tab. If you do not yet need to compile a release version of the application, set Code Optimization on the Compiler tab of Project Options to None and uncheck Optimization in the Code Generation section on the Pascal tab.
08 9721 CH06
11/13/00
9:53 AM
Page 341
Compiling and Optimizing Your Application CHAPTER 6
If you are not using floating-point math in your applications, checking None in the Floating Point group of the Advanced Compiler tab will speed up the link time slightly, because the floating-point libraries will not be linked with your application. These are things you can do within C++Builder and your code to minimize compile times. However, an important consideration is the computer hardware you are using. A software development system such as C++Builder requires higher-than-average system specs for CPU speed, RAM, and disk speed. Increasing these will yield a faster compile. In general, you should place slower IDE peripherals (such as an older CD-ROM drive) on a separate IDE controller from the hard drive. Defragmenting your hard drive may also improve the compile time slightly. On multi-processor (SMP) machines you can take advantage of all processors by invoking compilation of several modules simultaneously. The Borland MAKE utility provided does not support this directly, but you can write a script to run individual MAKEs of separate modules simultaneously. Alternatively, you can use the free GNU Make with the -j [jobs] commandline switch for parallel execution. You can get GNU Make for Windows from http:// sourceware.cygnus.com/cygwin/. Download the full Cygwin distribution, or at least the cygwin1.dll and make.exe files. For documentation, see http://www.gnu.org/software/make. To use GNU Make in C++Builder 5, you’ll need to export a makefile, either from the Project menu in the IDE or using the BPR2MAK.EXE command-line utility, because the project file is now stored in XML format. See BPR2MAK.EXE in the online help index for more information. Finally, it probably doesn’t need to be said that you should close other applications when working with C++Builder, particularly those that are memory or CPU intensive. If you’re getting low on memory, things will certainly slow down considerably. I’ve also found that development on Windows NT and Windows 2000 is more responsive than Windows 95/98 (and provides a better debugging environment). One additional method of speeding up compile times is discussed in the section “Background Compilation,” later in this chapter. See that section for more information. Now let’s look at some of the new enhancements to the compiler and linker in C++Builder 5.
Exploring the C++Builder 5 Compiler and Linker Enhancements There are several new features and enhancements to the compiler and linker in C++Builder 5. Perhaps the most eagerly anticipated enhancement is background compilation. C++Builder 5
6 COMPILING AND OPTIMIZING YOUR APPLICATION
It is important to look at the application structure and consider using packages or DLLs for modular parts, particularly in large projects. Both Make and Build will be considerably faster.
341
08 9721 CH06
11/13/00
342
9:53 AM
Page 342
C++Builder 5 Essentials PART I
also includes Microsoft Visual C++ compatibility enhancements, new file support, enhanced linker warnings and switches, enhancements to extended error information, and more.
Background Compilation The new background compilation feature improves productivity by allowing you to keep working within the IDE when compiling your project. The larger and more complex the project, the more time the compilation process takes and the more benefit you will get out of this enhancement. During a background compile, you can continue editing files and forms in the IDE. As each file in the project is needed by the compiler, it is temporarily marked as read-only. As a result, for a short period of time the source file or form you are editing cannot be changed. During the background compile, you can also move between different files and forms in the IDE. You must be aware that some of the changes you are making at the time will be included in the compile and others won’t, depending on which files the compiler has already processed at the time of the change. Additionally, the compiler may catch incomplete changes and report compiler errors or warnings on these partial changes. Background compilation has some limitations. It cannot be used with packages, the Make All Projects option, or the Build All Projects option. Also, Code Insight features (code completion, code parameters, and ToolTip symbol insight) do not function during a background compile, and several menu items such as Project, Options are disabled.
TIP A background compile is up to 25% slower than a foreground compile. Background compilation is performed in a separate thread, and the thread switching, synchronized access to edit buffers, and synchronization with the IDE’s main thread are the cause of the speed reduction. When performing a Make or Build that you must wait for, such as when you need to do a test run, you should disable background compilation. Select Tools, Environment Options and go to the Preferences tab. Uncheck Background Compilation in the Compiling group.
The best way to use background compilation is to kick off a compile periodically (usually with Project, Make—Ctrl+F9) and continue working. If the project is being developed by a team, start a background compile after you incorporate their changes (usually after an update action if using a version control system) and continue your work on the project. When you get to the stage at which you need to do a test run, the compile will most likely happen very quickly. Try not to change header files in that last stage as all units that include the header will require a recompile. This is a compile that you need to wait for.
08 9721 CH06
11/13/00
9:53 AM
Page 343
Compiling and Optimizing Your Application CHAPTER 6
Miscellaneous Compiler Enhancements
There are seven new __declspec declarations that fill a variety of roles. The declaration causes the compiler to not generate prolog and epilog code (setup and restore code for a function call) for a particular function. The __declspec (noreturn) declaration informs the compiler that the function does not return (for example, an exit function that aborts the application). Previously, use of such a function might have resulted in a compiler warning message. __declspec(naked)
Extended Error Information is a new option on the Compiler tab of the Project Options dialog. When it is set, additional compiler information is available for compiler warnings and errors, such as the parser context (for example, the name of the function that was being parsed when the compiler found the error). Click the plus symbol (+) next to the warning or error to view the extended error information. A useful addition for debugging purposes is the new __FUNC__ preprocessor macro. It is expanded to a string literal of the name of the function in which it is contained. The following code would generate the messages Enter(TForm1::Button1Click) and Exit(TForm1::Button1Click) to the debug event log (View, Debug Windows, Event Log): #define DFUNC_ENTRY OutputDebugString(“Enter(“ __FUNC__ “)”) #define DFUNC_EXIT OutputDebugString(“Exit(“ __FUNC__ “)”) void __fastcall TForm1::Button1Click(TObject *Sender) { DFUNC_ENTRY; ShowMessage(“In the middle.”); // Some code. DFUNC_EXIT; }
The __FUNC__ macro also works in class methods declared in the class definition.
New Linker Enhancements A new Advanced Linker page has been added to the Project Options dialog. From this page, you can access several new and existing linker options, including delay loading selected DLLs. This option causes selected DLLs to be loaded only when an entry point is called, speeding up application startup and reducing memory requirements for DLLs that are infrequently used. This may also help in building modular applications by not requiring the DLLs to be shipped if they are not called when running in “reduced” mode.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
Microsoft Visual C++ compatibility enhancements include two new function modifiers, __msfastcall and __msreturn. These allow you to create functions in DLLs that can be used from Microsoft Visual C++ applications. Two new compiler switches, -VM and -pm, can be used to specify when the __msfastcall function modifier is used.
343
08 9721 CH06
11/13/00
344
9:53 AM
Page 344
C++Builder 5 Essentials PART I
Several new linker command-line switches have been added. The new Advanced Linker project options can be used to set some of the new command-line switches. Others include -GF to set advanced link image flags, -GD to generate a Delphi-compatible (.DRC) resource file, and -ad to link to a 32-bit Windows device driver. (You still need to modify the DDK headers and create the proper import libraries.) Additionally, you now have more control over linker warnings. See “Linker Enhancements” in the C++Builder 5 help file for more information on these and other linker enhancements.
Optimizing: An Introduction Optimizing is the process of streamlining some aspect of an application such as execution speed, program size, memory requirements, network bandwidth use, or disk access speed. Optimizing has been said to be both a science and an art; it involves both analytical analysis and creative design. Moore’s Law, which states that the number of transistors per square inch on an integrated circuit doubles about every 18 months, has been closely held for over three decades. The current Pentium III processor contains over 28 million transistors. Combined with increasing CPU clock speeds, this is bringing about a similar trend in CPU performance. The performance and specifications of other computer hardware constantly increase also. More RAM, bigger and faster hard drives, faster networks, faster CD-ROMs, faster video cards, and faster modems are appearing all the time. So why aren’t we satisfied with our current hardware? Because it’s not fast enough or big enough. As fast as hardware specifications increase, software requirements expand even more. Computer games are partially to blame for the hardware-software race. Modern games, especially those that use complex 3D graphics, push the hardware to the limit. Additionally, operating systems and user interfaces become more complex, and applications software uses more graphics, handles more data, and performs more functions. Non-linear desktop video editing, a relatively new hobby for non-professional enthusiasts, is a prime example. It requires lots of RAM, a fast CPU, and an extremely large, lightning-fast hard drive. When performance is a problem, you generally have two choices: a hardware solution or a software solution. Hardware solutions are often very limiting in the performance increase that can be achieved and are usually prohibitively expensive. However, they should not be ruled out as a viable solution, because in some cases a hardware solution can be the easiest to implement, particularly if the application is used in-house. A hardware solution isn’t generally optimizing, though. It is possible to configure or substitute hardware to optimize hardware performance, but that falls outside the scope of this book. The remainder of this chapter will look at software optimization.
08 9721 CH06
11/13/00
9:53 AM
Page 345
Compiling and Optimizing Your Application CHAPTER 6
You should only attempt to optimize a working application that has been tested and debugged. Set measurable performance goals for the application. If the application meets those goals, it doesn’t need optimizing. You should also avoid over-optimizing. Stop optimizing as soon as you get it to meet the goals you’ve set. Make performance goals realistic. It’s pointless to set a goal to develop an application to compress one hour of full-screen, full-rate video data to 20:1 in less than a minute (and it’s not possible in software on today’s computers). When it is difficult to know beforehand what is realistic, discuss it with several developers and make a reasonable guess. In most cases, performance goals should be at a level that is acceptable to the users and no more. It’s equally pointless to squeeze your program into 1MB file size when the user would be happy if it fits on a single 1.44MB floppy disk. When execution speed is the important factor, it may even be enough just to make the user think that the application runs fast enough. Optimization should be targeted only at the parts of the application that need optimizing. To optimize an application, you need to understand how it works and know exactly where it is underachieving. Concentrate your efforts and develop an optimization strategy that will achieve the best results first. It is also important to realize that optimization often adds complexity to your code. This can have a negative impact on maintainability, testability, and robustness. The more complex the code, the more likely that bugs will be introduced when changes are made to the application in the future. In addition, bugs will be harder to find and fix. Complex code is less comprehensible, and modifications made to the program in the future will require more development time. Every project should have a set of clearly defined goals. These will vary greatly from project to project. The following objectives should be prioritized and adhered to: • Speed • Size • Maintainability • Testability • Reusability • Robustness • Scalability • Portability • Usability • Safety
6 COMPILING AND OPTIMIZING YOUR APPLICATION
For many desktop applications, performance is not an issue. However, there are certain types of applications and functionality that do require a performance boost. In the majority of cases it’s execution speed, program size, or memory usage that needs tuning.
345
08 9721 CH06
11/13/00
346
9:53 AM
Page 346
C++Builder 5 Essentials PART I
The application design determines the level at which each of the objectives is met, so you need to keep all objectives in mind from the very beginning. Assuming that speed and size are fairly high in the list of priorities, you should make high-level design changes to optimize speed and size early on in the project. Low-level optimizations should be performed very late in the project. Often, a change in the design at any level can render some low-level optimizations worthless, wasting the time and effort invested. This is particularly true for time spent optimizing at the lowest level, hand-tuning assembly code (a black art today), which you will likely have to repeat if the code changes or higher-level optimizations such as using a different algorithm are performed. You should document any optimization changes that you make in the application and also if you suspect any areas performing under-par when developing it in the first place.
Optimizing for Execution Speed Optimizing for execution speed is the most common of all optimization tasks. It often drives the user’s overall impression of the application. In addition, with the increased complexity of calculations and branches in some of today’s applications and the increased volume of data to be processed, the CPU is still a limiting factor. C++Builder’s optimizing compiler produces fast executable code that is similar in performance to code compiled with Delphi, Visual C++, and GNU C++.
NOTE At the time this book is being written, John Jacobson is conducting Jake’s Code Efficiency Challenge to compare the speed of code using C++Builder, Delphi, and Visual C++. It looks at various solutions to specific problems and how fast the resulting code runs. You can find it at http://home.xnet.com/~johnjac/.
Each optimizing compiler has its own strengths and weaknesses, so when looking at a comparison of execution speed, a variety of tests is necessary. Tests claiming a difference in speed of five times or more between the code generated by different compilers are likely demonstrating a particular weakness of one compiler and should not be used for a general comparison. The C++Builder optimizing compiler will likely speed up your code by only 20% to 150% in most cases. There is still a need for much greater improvements in some applications. The most likely applications to require greater speed optimizing are those that • Perform a lot of complex mathematical calculations. Real-world scientific or engineering models, fractal generators, and applications that use 3D graphics are typical examples. • Process large amounts of data, such as data compression, sorting, searching, or encryption.
08 9721 CH06
11/13/00
9:53 AM
Page 347
Compiling and Optimizing Your Application CHAPTER 6
Sometimes the limiting factor is not the execution speed but something outside the control of the application, such as a slow hard disk, a congested WAN, or even a user who responds slowly to a blocking event. There are many techniques you can employ to optimize an application for speed. The techniques typically fall into the following categories: • Compiler optimization settings • Changes to the design and algorithms • Low-level code changes • Data changes • Hand-tuned assembly code • External optimization • Hiding execution speed The last option, while not exactly an optimization technique as such, can be particularly relevant in certain situations. If the task must be finished before the user can continue, then use a progress bar or an animation to show the user that the application is still functioning. If possible, perform the processing in a background thread while the user keeps working. Some optimization techniques produce better results than others, and some require much more effort to implement. Generally, the best performance gains can be achieved through design and algorithm changes, and the easiest to implement are compiler settings and low-level code changes. Not all optimization techniques may be appropriate for a particular application. It should be mentioned that several of the techniques for optimizing execution speed increase the size of the executable, going against any program size optimizations. In the following sections, most of the speed optimization techniques that will be discussed will be demonstrated using an example application.
Crozzle Solver Application Example The example application that we’ll use to demonstrate many of the optimization techniques is a “crozzle” solver. This example fits into the third category previously described; it involves complex branches and indexing statements. A description on what it does is necessary for some of the optimization techniques presented. A crozzle is like a crossword in that it involves interlocking words within a grid, except that you start with an empty grid and are given the list of words to place within the grid. The goal is to interlock the words in one contiguous block in such a way as to achieve the highest score. Each interlocked word within the crozzle solution is worth 10 points. Interlocking letters are also scored: A–F are 2 points, G–L are 4 points, M–R are 8 points, S–X are 16 points, Y is 32
6 COMPILING AND OPTIMIZING YOUR APPLICATION
• Solve a problem. These often iterate many times and contain complex branch or indexing statements. Event simulators, best-fit determination, and puzzle solvers are all examples.
347
08 9721 CH06
11/13/00
348
9:53 AM
Page 348
C++Builder 5 Essentials PART I
points, and Z is 64 points. Words can be used only once and are placed horizontally or vertically in a 15×10 grid. Only complete words are permitted to be formed within the grid, and you cannot score words within words. For example, if SCARE and CAR were in the word list, and the word SCARE is placed, you cannot claim an additional 10 points or interlocking C, A, or R for the inline word CAR. Traditionally, a crozzle uses a word list of about 110 to 120 words of between 3 and 15 letters. I chose this example because it is somewhat visual and a bit of fun, it takes an extraordinary amount of time to run, and it can be used to demonstrate many of the optimization techniques and difficulties that will be covered. About 12 years ago, I wrote a very crude version of the crozzle solver in BASIC and later in optimized assembly code in an attempt to win a $2000 prize in a magazine. Unfortunately, it wasn’t complete enough or optimized enough for the computers back then to produce even partial solutions within the available time. Through a much-improved algorithm, the version that we’ll use here is much more complete in terms of functionality and base speed than those early versions (it could still use a little polishing). It is about 2900 lines of source code contained in 4 units and 3 header files—not a large project but nice. Figure 6.1 shows the Crozzle Solver application that we’ll be working with. It is displaying a simple solved crozzle.
FIGURE 6.1 The Crozzle Solver application with a simple solved crozzle.
The initial version of the application is provided in the CrozzleInitial subdirectory on the CD-ROM that accompanies this book. The final version, after applying the optimization techniques discussed later, is provided in the CrozzleFinal subdirectory. Both C++Builder 4 and 5 are supplied for your convenience; they are identical except for the project file and to-do list.
08 9721 CH06
11/13/00
9:53 AM
Page 349
Compiling and Optimizing Your Application CHAPTER 6
The crozzle application can be used to solve words in the whole grid or in a specific portion of the grid. To solve the whole grid, use the Solve Whole button after the crozzle is loaded. To solve a specific area, select the area to solve with the mouse, set either Selection Bounds Words or Selection Bounds Interlocking Letters from the Options menu, and use the Solve Sel button. The crozzle solver automatically outputs the current highest-scoring crozzle solution to the file which can be loaded for viewing later. There are several interesting View options. Experiment with them to get an insight into how the Crozzle Solver application goes about its job. Presently, the Save and Save As options are not particularly useful, because it is difficult to save a crozzle during the solve process, and the crozzle ends with the same grid it started with. HighScore.crz,
Exponential Timings Through optimization, we’ll progressively speed up the crozzle solver in several stages. At each stage, relative speed improvements will be noted. These will be expressed as a percentage speed improvement over the previous timing, and the total number of times faster will be compared to the original timing. All timing and score tests are performed in the C++Builder IDE on a Pentium II 266MHz with 128MB of RAM, running Windows 2000 Professional. The timings listed are an average of three runs. Two example files, RunComplete.crz and RunPartial.crz, are provided on the CD-ROM in the same directory as the example applications. RunComplete.crz uses a list of 11 words to solve every solution possible in a relatively short period of time and will be used for all speed timings in this chapter. The RunPartial.crz example uses a full list of 115 words. Solving every possible solution in this example is simply not achievable with today’s hardware, irrespective of how well we optimize the program. Both examples solve from a blank crozzle grid, thus attempting all possible combinations. A typical user-generated high-scoring solution uses about 35 to 40 words from the complete word list, and the score varies from about 600 to 800 points. You might think that it would be fairly easy to write a program to try all combinations of words to get the best scoring solution. Think again! The number of combinations is phenomenal. Table 6.1 shows the run time of the initial Crozzle Solver application (compiled without optimization settings) when used to solve the first 5 to 11 words from the RunComplete.crz file.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
The Crozzle Solver application can be used to solve completely from a blank grid or to build on an existing, partially complete user- or computer-generated solution. The word list and crozzle grid are stored together in a CRZ text file. An example of each is provided in the files ExampleBlank.crz and ExampleToFinish.crz. Simply edit the file and type the words into the grid as required and type the full list of words below the grid, one per line. Leave the words initially placed in the grid in the word list.
349
08 9721 CH06
11/13/00
350
9:53 AM
Page 350
C++Builder 5 Essentials PART I
The table also shows the number of word combinations attempted, the number of complete valid solutions found, and the highest score achieved. TABLE 6.1
Example Run Data for a Word List of 5 to 11 Words
Words
Time (sec)
Word Combinations
Solutions
Highest Score
5 6 7 8 9 10 11
0.0318 0.288 2.96 47.1 458 6,294 79,560
3,577 31,257 317,477 4,765,661 44,057,533 554,577,981 >6,000,000,000
1,492 9,892 101,604 1,192,436 11,123,772 152,343,008 >1,900,000,000
90 102 120 146 164 190 208
The first thing to note from the table is that, even with a list of only 11 words (RunComplete.crz), the run time is over 22 hours! On average, the run time increases by almost 1100% for each extra word. Figure 6.2 shows a graph of the run time from Table 6.1. The table and graph are included in the CrozzleTimings.xls Microsoft Excel spreadsheet on the CD-ROM.
FIGURE 6.2 A graph of the run times for 5 to 11 words from Table 6.1.
From the table data and the run time graph we can conclude that the run time follows an exponential pattern. This is also apparent in a graph of the logarithm of the run time, which produces a linear result. This graph and a graph of the high score are also available in the spreadsheet on the CD-ROM.
08 9721 CH06
11/13/00
9:53 AM
Page 351
Compiling and Optimizing Your Application CHAPTER 6
We would expect both the run time and score to taper off as the number of words in the list exceeds about 40. Most words in a list of 40 could be placed in the grid simultaneously to form a solution. Until this point there has been ample space to place words in the grid, but now there is little space left. It is difficult to place additional words, and hence the number of combinations does not increase at the same rate for each additional word added to the list. In any case, we can see that there is no way to completely solve a list of 115 words, at least not in the life of this universe. The result for the example crozzle using the unoptimized (baseline) crozzle solver with all compiler optimizations disabled is Initial Timing: 79,560 seconds for 11 words Now let’s start speeding this thing up a bit!
Project Options for Execution Speed There are several project options that affect execution speed. They are spread across several tabs of the Project Options dialog. Set the following project options to maximize the speed: • Compiler tab: Click the Release button. It will set most optimizations for you. Set Code Optimization to Speed, disable all options in the Debugging group, and uncheck Stack Frames in the Compiling group. • Advanced Compiler tab: Set Instruction Set to Pentium Pro, set Data Alignment to Quad Word, set Calling Convention to Register, set Register Variables to Automatic, set Floating Point to Fast, and uncheck Correct Pentium FDIV flaw. • C++ tab: If possible with your application, disable exception handling by unchecking the Enable RTTI and Enable Exceptions options in the Exception Handling group. Otherwise, check Fast Exception Prologs. Set Virtual Tables to Smart. • Pascal tab: In the Code Generation group, check Optimization and Aligned Record Fields and uncheck Stack Frames and Pentium-Safe FDIV. Disable all options in the Runtime Errors and Debugging groups. • Linker tab: In the Linking group, uncheck Create Debug Information, Use Dynamic RTL, and Use Debug Libraries options. Set Map File to Off. If you are creating DLLs, the image base for all DLLs used by an application should be unique and offset sufficiently to prevent them from overlapping in memory. • Packages tab: Uncheck Build with Runtime Packages.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
By extrapolation we can estimate that, without further optimization, 15 words will take about 50 years to solve, 17 words will take more than 7,000 years, and 20 words more than 10 million! I can’t wait for the first 1EHz (Exahertz—one billion GHz) CPU, but even that would take over a hundred million years to completely solve a list of just 30 words.
351
08 9721 CH06
11/13/00
352
9:53 AM
Page 352
C++Builder 5 Essentials PART I
• Tasm tab: Set Debug Information to None. • CodeGuard tab: Uncheck CodeGuard Validation.
NOTE Enabling compiler and other optimization settings as described here can hinder debugging of your application. The optimizer may rearrange or even remove code and variables. This can stop breakpoints and single stepping in the debugger from functioning as expected. As previously mentioned, you should debug your application first.
For our crozzle application, several of these options will have no effect. However, just by enabling code optimization, we have a significant improvement in speed. With all options set as described, we now have the following: Current Timing: 51,240 seconds. Improvement: 55% (total speedup 1.55 times) You should be careful when setting the various options listed. The Instruction Set option on the Advanced Compiler tab should be set to the base target machine that you want the application to run on, because processors are not forward compatible. The difference between a 386 and a Pentium Pro for the Crozzle Solver application is negligible. With the compiler optimization set to Speed, simply setting Register Variables to Automatic rather than None results in a 40% speed increase for the crozzle application. Remember that C++Builder 5 allows different settings for each unit (node-level options). Use this to your advantage. If all speed-critical code is in one unit, consider aggressive settings for only that unit.
Detecting Bottlenecks Most of the remaining optimization techniques require you to have a good understanding of the application and specifically to know exactly where your application is spending most of its time. In most applications, between 80% and 90% of the time is spent executing only 10% to 20% of the code, typically in a few loops or a few commonly executed functions. Using the knowledge of where your program’s bottlenecks are, you can achieve the best results with the least effort. There are several techniques for finding bottlenecks: • Use a profiler • Manually time sections of code • Inspect the design and code
08 9721 CH06
11/13/00
9:53 AM
Page 353
Compiling and Optimizing Your Application CHAPTER 6
Profiling
Typically function profilers can monitor your application as they are, but some require changes in the source code. The choice of a function profiler or a line profiler depends on the structure of your application, but generally a function profiler is more than sufficient. Profilers work with multithreaded applications. There are several profilers available, and a few are specially designed to be used with C++Builder. One is Sleuth StopWatch by TurboPower Software Company. It is part of the Sleuth QA Suite. I’ll be using version 1.0, which allows function profiling, to profile the Crozzle Solver application. StopWatch also includes a code disassembly view that displays assembly code instruction pairing information that can assist you with low-level assembly code optimization. I should point out that at the time this book is being written, Sleuth QA Suite 2.0 is about to be released. It will be a great update and includes new features such as coverage analysis, line profiling, automation support, the capability to export views to HTML, user-defined views, and critical-path analysis. Sleuth QA Suite also includes Sleuth CodeWatch, a utility that is similar in function to CodeGuard. It detects memory and resource leaks and can filter out VCL leaks. It also catches memory overwrites, detects invalid parameter and return values for Win32 API calls, logs all Win32 API calls, and more. You can get more information and download a trial version of Sleuth QA Suite from http://www.turbopower.com/. Some other profilers of note are • QTime from Automated QA is available in both a lite and standard version. It works with C++Builder 3 and later. The standard version includes many useful features, including code coverage and code tracing. For more information or to download a trial version, go to http://www.totalqa.com/. • RQ’s Profiler is very inexpensive, but it requires you to add macro calls to your source code and link with the Profiler DLL. It has a code editor to assist with this. RQ’s Profiler works with C++Builder version 1 through 5. You can get more information and download a shareware trial version from http://ourworld.compuserve.com/homepages/rq/. • Intel’s VTune Analyzer includes a sampling profiler, a Code Coach to recommend C++ source level optimizations, call graph profiling, static assembly code analysis to indicate pairing and penalty issues on a variety of processors, and more. However, it does not integrate directly with C++Builder, and I found it difficult to use. It has both function
6 COMPILING AND OPTIMIZING YOUR APPLICATION
By far the easiest method of detecting bottlenecks is to use a profiler. A profiler is a tool that tracks the execution time of an application while it is running and provides detailed statistics on which portions of the application run most frequently or consume the most time. Most profilers track the execution time down to the function level. However, some profilers can track execution time down to the line level.
353
08 9721 CH06
11/13/00
354
9:53 AM
Page 354
C++Builder 5 Essentials PART I
and line profiling. For more information and to download a trial version, go to http://developer.intel.com/vtune/analyzer/.
A Profiling Example When you are profiling, you should stop all non-essential applications that may interfere with the timing. For an accurate view, you should repeat a profile test a few times and average the results. A problem with all profilers is that they don’t handle recursive functions very well. Timings are per function name rather than per invocation of the function, and they include time spent in child functions. This is difficult to interpret. Unfortunately, the Crozzle Solver application uses recursion to generate the crozzle solutions. The SolveWord() function is called recursively to build each word in the solution. We’re going to profile the Crozzle Solver application using Sleuth StopWatch from TurboPower. To solve the recursion problem, we’ll need to perform some code trickery. We’ll need to unroll the recursion to a particular depth. This means creating several copies of the functions in the recursion loop and chaining them together. The Crozzle Solver application has a recursion loop that is three functions wide and one function deep. This means that function A calls functions B, C, and D. Functions B, C, and D each call function A. In other words, function A fans out to three functions wide, and we can go down one level before returning to the first function. If you had a single function A that called itself, the recursion loop would be one wide and zero deep. In the Crozzle Solver application, function A is SolveWord(), functions B, C, and D are SolveFirstWord(), SolveAdjacent(), and SolveStandardWord(), respectively. SolveWord() builds on the current crozzle by placing one additional word using one of the other three specialized functions. SolveFirstWord() is used only to place all combinations of each initial word in the grid. SolveStandardWord() interlocks another word from the list with the current crozzle. If a word that is placed in the grid is adjacent to a word in the next row or column, then one or more invalid two-letter words are formed. Words must be placed to run through these squares to form a valid word. SolveAdjacent() attempts to do this. You can see this by enabling View, Thinking and View, Pause View to see how words are placed and how adjacent letters are solved. Either a new word is placed through the adjacent squares or the adjacent word is rejected. Fortunately, the runtime recursion depth of the Crozzle Solver application, which is the number of times that the function-call loop can be cycled, or the stack size if you like, is dependent on the number of words in the word list, which means that we can easily control it. The maximum depth is N+1, where N is the number of words in the list. We’ll unroll the recursion loop eight times to use a list of seven words.
08 9721 CH06
11/13/00
9:53 AM
Page 355
Compiling and Optimizing Your Application CHAPTER 6
Then we chain them together by changing the call to SolveWord() in the original SolveFirstWord(), SolveAdjacent(), and SolveStandardWord() functions to SolveWord2(). In SolveWord2() we call SolveFirstWord2(), SolveAdjacent2(), and SolveStandardWord2(). We change these functions to call SolveWord3() and so on until we finally get SolveWord8() to call SolveFirstWord8(), SolveAdjacent8(), and SolveStandardWord8(), at which time we leave these to call the original SolveWord(), thus completing the loop. It’s a bit tricky, but if you follow it through carefully you shouldn’t have much trouble. All we’re really trying to do is extend the loop depth to eight. We also need to add function prototypes for all of the newly created function copies. From within C++Builder we fire up Sleuth StopWatch from the Tools menu and set the analysis settings for this project by answering a few questions. We’ll set Trigger Mode profiling on the SolveCrozzle() routine and time all routines with source and time leaf nodes individually. Once the analysis settings are configured, we run the crozzle solver from within StopWatch, load the eight-word crozzle file, solve it completely, and exit the crozzle solver. StopWatch now displays the profiling information. Figure 6.3 shows the StopWatch profiling results for the Crozzle Solver application after completely solving a word list containing seven words.
FIGURE 6.3 The profiling results as displayed in Sleuth StopWatch.
To obtain the true stats for the recursive functions, we must total the Times Called, Net Time, and Gross Time values for each of the eight copies of the four functions. From that we recalculated the Net %, Net Average, and Gross Average. The final profiling results are shown in Table 6.2. Note that with non-recursive functions, the whole process of profiling is much simpler than this.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
First we copy the SolveWord(), SolveFirstWord(), SolveAdjacent(), and SolveStandardWord() functions and paste seven times. Next we modify the function names of the seven copies by adding a 2, 3, 4, and so on to the end of the name. Now we have eight unique copies of each function.
355
08 9721 CH06
11/13/00
356
9:53 AM
Page 356
C++Builder 5 Essentials PART I
TABLE 6.2
Final Profiling Results for the Crozzle Solver Using a List of Eight Words
Function
Calls
Net Time (ms)
Net %
Net Average (ms)
Gross Time (ms)
Gross Average (ms)
PlaceWord
317462 544926 317462 101604
1216.4 747.5 729.8 686.1
28.78 17.68 17.27 16.23
0.0038 0.0014 0.0023 0.0068
1216.4 747.5 729.8 686.1
0.0038 0.0014 0.0023 0.0068
167474 149988 317463 101604 1
356.6 253.95 116.7 114.0 0.00
8.43 6.01 2.76 2.70 0.00
0.002 0.0017 0.0004 0.0011 0.00
24029.0 283.4 28663.6 803.6 4224.6
0.1435 0.0019 0.0903 0.0079 4224.6
CanPlace UnPlaceWord CalcScore SolveStandardWord SolveAdjacent SolveWord CompleteSolution SolveFirstWord
From the profiling results we can see that we need to concentrate our efforts on the PlaceWord(), CanPlace(), UnPlaceWord(), and CalcScore() functions. The Crozzle Solver application has a rather broad bottleneck in terms of functions, but overall it is the entire solve algorithm that needs streamlining. Profiling should be performed at various stages of optimization so that you know what effect each change has on the execution structure and speed. You will probably find changes that actually slow down the application!
Manual Code Timing Manually timing sections of code involves inserting stopwatch-type timing code into your application at strategic places. With little knowledge of where the time-intensive sections are, you need to start at the top, with loops and individual function calls. You then drill down into the code until you identify the key areas that are consuming the most time. Listing 6.3 demonstrates how you can time a section of code. LISTING 6.3
Timing a Section of Code
#include void TMyClass::SomeFunction() { clock_t StartTime, StopTime; // Some code here. // Start the clock.
08 9721 CH06
11/13/00
9:53 AM
Page 357
Compiling and Optimizing Your Application CHAPTER 6
LISTING 6.3
Continued
// Code to time in here. // Stop the clock. StopTime = clock(); // Report the elapsed time. ShowMessage(“Elapsed time: “ + FloatToStrF((StopTime-StartTime)/CLK_TCK, ffFixed, 7, 2) + “ seconds”); // Rest of code here. }
The code that you want to time goes in the center section. If the code is rather fast (executes in under a second), you can add a simple loop around the section of code, unless of course the code has side effects that cause the behavior of the code to change after the first run. You then time the whole lot and divide the elapsed time by the number of loop iterations. Typically, if the timing code is in a function that is called many times, you wouldn’t use ShowMessage() to display the elapsed time. Instead, you could write it to the Debug Event log, using OutputDebugString() if you’re running in the IDE, or to your own file.
Design Inspection The design inspection method can be used where formalized design documentation exists. Look at the core of the application design and then look for critical points in the data flow or execution path. Analyze pseudo-code, flow charts, dataflow diagrams, and process sections of IPO and N-S diagrams and Warnier-Orr diagrams. Look for processes that are called from many places and loops, particularly nested loops. This will give you an idea of which areas are frequently called; in most cases, this is where bottlenecks occur.
Code Inspection The code inspection method involves looking at the source code level for loops, complex branches, indexing statements and pointer arithmetic, or mathematical calculations. Object-oriented code typically contains many small methods and often hides complex pointer arithmetic. With both the design inspection and code inspection methods, you will still need to time sections of code to verify that you are indeed looking at the troublesome areas. Use the manual timing methods previously described. With all methods, you need not only to find the bottlenecks but also to decide which bottlenecks are most relevant. A bottleneck in a background thread that has no effect on the user is not likely to be worth tuning.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
StartTime = clock();
357
08 9721 CH06
11/13/00
358
9:53 AM
Page 358
C++Builder 5 Essentials PART I
Optimizing the Design and Algorithms The biggest gains most often come from optimizing the design, choosing better algorithms, or optimizing the existing algorithms. To optimize the design you need a very good understanding of how the application works. On the other hand, algorithms are usually implemented at a fairly low level, often isolated from the bulk of the application. The code that implements the algorithm is often complex but, if designed and written well, it can still be easy to use.
Making Design Decisions Optimizing the design requires that you look at the bigger picture, at things such as application architecture and technologies used to implement fundamental features. It is mostly applicable to large applications that are modular or distributed, or that interface with other applications. There are many technical issues that will be faced when designing Windows applications. A method call to an interface in an in-process COM object will be faster than the same call to an out-of-process COM object, and faster again than the same call to a DCOM object on a remote machine. When scaling a modular application that uses COM as the framework to using DCOM, you need to take this into account. In many cases there are alternative technologies. What about CORBA or a custom TCP/IP socket–based interface? There are many more alternatives to consider for database applications. C++Builder 5 already includes three methods of database access: the BDE, ADO Express, and InterBase Express. It appears that there is another on the way: DB Express. There are more than 20 third-party BDE alternatives. There are always trade-offs between performance, usability, and features. In these and other technology decisions, a lot of homework is necessary to find and weigh the advantages and disadvantages of each alternative. Perhaps the best source for this information is the Borland newsgroups. Questions relating to performance and design decisions are often asked and, if you have a specific question in mind, there are experts who are glad to help. Keep your eye out for TeamB members. A complete listing of C++Builder-related Borland and other newsgroups is in Appendix A, “Information Sources.” The following is a list of several overall design optimization techniques: • Reduce complexity. Don’t modularize your application too much. Don’t “overnormalize” your database. • Streamline the execution path. Avoid chaining calls to distributed objects. • Avoid slow technologies. Choose the best one for the job. • Use efficient third-party tools for report generation, data compression and encryption, communication, and other needs. • Minimize graphics and visual controls, particularly if they are updated often. Consider disabling and hiding them during the update and enabling and displaying them again after the update is complete.
08 9721 CH06
11/13/00
9:53 AM
Page 359
Compiling and Optimizing Your Application CHAPTER 6
• Perform long tasks in a second thread where possible.
• Design effectively for multiprocessor machines. Spread the workload evenly between each processor and reduce blocking interprocess communication. • Reduce memory use. Multithreaded applications and those that run in an environment with several simultaneous applications running should be memory conscious to allow the operating system to schedule them efficiently. There’s no one set method for optimizing the design of your application. If in doubt, ask other experienced people in your field. User groups are a haven for serious and hobbyist programmers with varying backgrounds and experience. The Crozzle Solver application has a rather simple design. Most of the techniques listed don’t really apply to it. One design aspect of the crozzle solver that has been optimized in the initial design is TDrawGrid, used to display the crozzle. By default the current state of the crozzle is not updated in TDrawGrid throughout the solve process. However, the user can enable this from the View menu. One further design optimization that we can perform is to slightly streamline the execution path by restructuring the function hierarchy. Currently, when the Solve button is pressed, SolveCrozzle() is called. It in turn calls SolveWord() to start generating solutions. SolveWord() is a generic function that can be called to start solving a blank crozzle or to build on the initial or current crozzle. In the SolveWord() function, the following code appears: if (WordsPlaced.NumPlaced == 0) { SolveFirstWord(); return(true); }
The call to SolveFirstWord() is only required at the very highest level in recursion, as tested by the if statement (no words placed yet). The SolveWord() function is called recursively from all levels, which means that this if statement is executed every time. We can move the SolveFirstWord() function call outside the recursion loop and place it in the initial calling function, SolveCrozzle(). The simple call to SolveWord() from SolveCrozzle() now becomes this: if (SolveFromBlank) { SolveFirstWord(); } else { SolveWord(); }
6 COMPILING AND OPTIMIZING YOUR APPLICATION
• Minimize network traffic and disk access. Consider using client data sets, and read and write data in a few larger chunks instead of many smaller ones. On network or other communications links with a high latency, consider sending multiple requests and subsequent replies at once.
359
08 9721 CH06
11/13/00
360
9:53 AM
Page 360
C++Builder 5 Essentials PART I
We call SolveFirstWord() only if we have a blank crozzle; otherwise we still call SolveWord() to start building on the initial solution. This keeps the if statement from being evaluated in each call to SolveWord()—over 6,000,000,000 times in the 11-word example! Now we have the following results: Current Timing: 49,525 seconds. Improvement: 3.5% (total speedup 1.61 times) Not a huge result, but it was easy to implement.
Choosing Algorithms Algorithms usually provide the biggest scope for improving the speed of an application. For any given problem there are many ways to obtain a solution, so choosing an appropriate algorithm is important. For standard problems there are usually several reasonable algorithms publicly available. An algorithm is a method of solving a problem. It has five primary characteristics: • Boundedness. It stops at some point. • Correctness: It finds the correct answer to the problem. • Predictability: It will always do the same thing if given the same input data. • Finiteness: It can be described in a finite number of steps. • Definition: Each step in the algorithm has a well-defined meaning. The algorithms for a particular problem will often vary dramatically in speed. One example is in games development, where there is fierce competition between various 3D graphics engines. In some cases the speed or the algorithm varies according to the input data. Another example of speed differences is with various sorting algorithms. Three well-known ones are bubble sort, selection sort, and quick sort. If you compile and run the Threads application from the Examples folder installed with C++Builder, you’ll see that the three sorting algorithms vary in performance. The speed of these three sorting algorithms depends both on the number of elements to sort and on the initial order of the elements. Using the random set of 115 elements that the Threads application generates, quick sort is fastest and bubble sort slowest. With fewer than about 10 elements, however, bubble sort is fastest and quick sort slowest. With 115 elements already in sort order, bubble sort is fastest and quick sort slowest, and in reverse order selection sort is fastest and bubble sort slowest. If the speed of the algorithm in your application varies widely according to the input data, and if the input data varies, consider incorporating two or more algorithms into your application to perform the same task and call the most appropriate one. Algorithms are often described in big-O notation (asymptotic complexity). Big-O notation can be thought of as shorthand for describing an algorithm’s execution time relative to the size of
08 9721 CH06
11/13/00
9:53 AM
Page 361
Compiling and Optimizing Your Application CHAPTER 6
The bubble sort is O(N2) and therefore should be used only for small N. There are better sorting algorithms available for small N. Quicksort is an O(log N) algorithm and is one of the fastest sorting algorithms for large N. The crozzle solver algorithm is O(2N), not a good prospect. Sometimes it is possible to replace an O(N2) algorithm with an O(N) algorithm and an O(N) algorithm with an O(log N) algorithm. It is important to note that two different algorithms do not necessarily produce exactly the same result. You may choose a faster algorithm that has less accuracy or an increased error rate. Many years ago I wrote a circle algorithm that was much faster than the standard one available on the system for small to medium-size circles. It used a sqrt() function to draw points on the circumference instead of drawing an N-sided polygon. It produced a much betterlooking circle, but for large circles it became slower than the standard routine. Here are several good algorithm references: • “Numerical Recipes in C” is an online book available in PDF format at http://www.ulib.org/webRoot/Books/Numerical_Recipes/. • The Stony Brook Algorithm Repository has solutions for many different problems and includes source code: http://www.cs.sunysb.edu/~algorith/. • Object-Oriented Numerics (http://oonumerics.org/) has Blitz++, a C++ class library for scientific computing. It is a high-performance library using template techniques and provides dense arrays and vectors, random number generators, and small vectors and matrices. It is open source, and there are also many links to other available libraries. • Three avenues for game and graphics algorithms are http://www.gamedev.net/, http://www.magic-software.com/, and the Newsgroup comp.graphics.algorithms. • Dr. Dobb’s Journal (http://www.ddj.com/) contains articles on algorithms from time to time. In particular, see the April 2000 issue, a special on algorithms. You can purchase individual articles online or a CD-ROM of over 11 years of issues for around US$80. • Introduction to Algorithms by Cormen, Leiserson & Rivest. ISBN 0262031418. 1990. • Algorithms in C++ by Sedgewick. ISBN 0201510596. 1992. • The Art of Computer Programming: Sorting and Searching by Knuth. ISBN 0201896850. 1998. There are many algorithms in the areas of sorting, compression, graphics, and data storage that have publicly available source code to drop right into your application.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
the problem. O(N) describes a linear relationship between the execution time and the problem size. For one to three elements, the time required may be 1, 2, or 3 seconds. With an O(N2) algorithm, the time required to solve the problem increases with the square of the problem size. For one to three elements the time required may be 1, 4, or 9 seconds. With an O(2N) algorithm the growth rate is large, and such an algorithm is typically considered infeasible to complete.
361
08 9721 CH06
11/13/00
362
9:53 AM
Page 362
C++Builder 5 Essentials PART I
Improving Algorithms If an algorithm is non-standard, it’s likely that you developed it yourself, and you’re more likely to resort to seeking speed improvements in the existing algorithm than to seek a replacement algorithm. It is often possible to improve upon an existing algorithm, particularly if it is complex in nature. If you’re using the Standard Template Library (STL), you’ll find templates that provide arrays and sorting functions that have been optimized for specific data types. Containers vary in performance. In the STL documentation, the C++Builder help files contain several optimization hints with the STL.
TIP A great Web site that contains STL timings and optimizations can be found at http://www.tantalon.com/pete/cppopt/main.htm.
One generic approach to speeding up an algorithm is to use table look-ups or simple value look-ups instead of mathematical or logical calculations. The table or values can sometimes be generated prior to runtime and coded into a static array in source code. Other times they may need to be generated at runtime, before the algorithm is executed, or even within the algorithm itself.
NOTE Using precalculated tables or values requires memory to store them, and sometimes the size of this data can be very large. You need to weigh the speed against the size and find a happy medium.
When generating the table or values at runtime, you must weigh the time to create them and keep them current against the time saved in the algorithm. A prime example of the table approach is the Cyclic Redundancy Check (CRC) calculation algorithm. CRCs are often used in data transportation to validate that the data received is the same as the data sent. The sender calculates the CRC and appends it to the data. The receiver also calculates the CRC and compares it to the CRC sent with the data. If they match, the data was received correctly. In a slow CRC-calculation algorithm, the CRC is generated by iterating through each byte of the data and performing polynomial modulo two arithmetic. It sounds long and it takes long. With the faster table method, a table of 256 values, one for each ASCII character, is either pregenerated and initialized in the source code or generated at runtime before the CRC algorithm runs. Then in the algorithm a lookup into the table for each character of data avoids the need
08 9721 CH06
11/13/00
9:53 AM
Page 363
Compiling and Optimizing Your Application CHAPTER 6
Other candidates for a table of values are a large switch statement or numerous if else statements. If you are only performing static calculations or calling functions in the various case or if statements, consider storing the precalculated values or function pointers in a table. Replace a case statement with a simple table lookup using the switch value. Consider the following simple example: switch(NumSides) { case 1: Circle(x, y, r); break; case 2: PolyError(x, y, r); break; case 3: Triangle(x, y, r); break; case 4: Square(x, y, r); break; case 5: Pentagon(x, y, r); break; }
This can be replaced by the following: void (*Polyfunctions[5])(int, int, int) = { Circle, PolyError, Triangle, Square, Pentagon }; // Later in some function. PolyFunctions[NumSides](x, y, r);
Replace an if statement with a simple table lookup using a value to represent each possible combination of the Boolean expressions. In the expression if (A && B), A can be true or false and B can be true or false. There are four possible combinations. Using bitwise math, calculate a binary value of 00 when both A and B are false, 01 if B is true, 10 if A is true, or 11 if both A and B are true. The binary values are equivalent to decimal 0 to 3. Use this value to index the lookup table. The baseline crozzle solver algorithm includes several optimizations that store values that would normally require considerable effort to recalculate in tables for reuse. Some of these cases are presented in the following paragraphs. To place a word in the grid, the crozzle solver goes through each word in the word list. It goes through each letter in each word and determines where that letter appears in the grid (if at all). If it does appear, a series of tests are required to determine if the word can be interlocked at that place and whether the word can be placed horizontally or vertically. There are two optimizations here. First, an array is used to store the positions at which each letter of the alphabet is placed within the grid. As a word is placed, the position in the grid of each letter in the word is added to the appropriate array. This saves enormous time, because it isn’t necessary to scan 10 rows of 15 squares each time we want to search for a place to interlock the current word.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
for a lot of the calculations at each step. Calculations are still required, but they are greatly reduced. It is akin to looking up a large multiplication table rather than doing the math with pencil and paper.
363
08 9721 CH06
11/13/00
364
9:53 AM
Page 364
C++Builder 5 Essentials PART I
Secondly, as a word is placed in the grid, its orientation is stored in each square that the word occupies. Two simple Boolean values are used for each square: HorizWord and VertWord. In the case of an interlocking square, both can be set to true. This also saves time, because otherwise it is difficult to determine if the word can be placed horizontally or vertically at a particular square. This is easy to do visually, but it’s not so easy programmatically because of several square content tests that are involved and grid boundaries that must not be exceeded. Another stored value optimization is in the calculation of the crozzle score. As profiling showed us, the CalcScore() function is one of the bottlenecks. Rather than scanning the grid testing for interlocking squares and then adding the score of the letter at that square to the total score, the score for each letter square is set at the time the square is interlocked. This is stored with the other square information in the crozzle grid. All that is needed then is to total the score of each square in the grid (limited to the four boundaries of the words in the grid), saving on several value tests. This is as much as 5% faster. From the profiling that we performed earlier, we know that bottlenecks exist in a variety of functions, namely position searching, placement testing, word placement, and word removal. Later we’ll get into code optimizations, but if we could remove the overall workload in these functions, we’d see a great improvement. Let’s now apply some further algorithm optimizations to the crozzle solver. One of the biggest drawbacks with the current crozzle solver algorithm is that it generates duplicate combinations of interlocked words. If the words JAR and ROD are in the word list, then at one point the algorithm places the word JAR horizontally and later intersects the word ROD vertically through the R. Later the algorithm places the word ROD vertically and intersects the word JAR horizontally through the R, producing the same result! Let’s add some duplicate solution detection. This is rather complex to describe, but there are two basic rules, depending on whether we are solving from a partial solution or a blank crozzle: • Solve from a partial solution: We can only place a word if it intersects with the most recently placed word that is after it in the word list or if it intersects with any word placed after that word. If there is no word already placed that is after it in the word list, then it can intersect with all words. For example, given the word list CAB, BIT, CAT and solving a partial crozzle with the word CAB already placed, the word CAT can intersect the C of CAB. However, then BIT can only intersect with the T of CAT and not the B of BIT, because that combination was previously solved. • Solve from a blank crozzle: The previous rule applies, and we can further eliminate duplicate combinations. Once the initial word is placed, we cannot place a word that is earlier in the word list. There is no need to do so, because earlier we tried every combination using that word. For example, given the same word list (CAB, BIT, CAT) we can place the word CAB first and then intersect the words BIT and CAT. Later, when we
08 9721 CH06
11/13/00
9:53 AM
Page 365
Compiling and Optimizing Your Application CHAPTER 6
To implement this, we need to track the highest word in the word list that appears in the grid, and we need to set an index of which previously placed words we can intersect with. To solve from a blank grid, each time we try to place a word in the grid, we can skip all words in the word list before the first word placed. We can set the word list index of the first word that we can place outside the recursion loop in the SolveCrozzle() and SolveFirstWord() functions. The solution to the duplicate combination problem and the testing are both very laborious. Specific test cases were required to ensure that the algorithm still functions correctly by solving all possible combinations. This new duplicate combination elimination now has the following speed statistics: Current Timing: 24.33 seconds. Improvement: 203600% (total speedup 3270 times) Wow! Can that be right? You bet. Those duplicate combinations and attempts to place words that weren’t necessary were killing us. There is one other minor algorithm improvement that we’ll put in. When placing a standard word and determining whether a square in the grid that contains a letter is an interlocking square (that is, whether it has a horizontal word and a vertical word running through it), the algorithm currently tests the state of both the HorizWord and VertWord Boolean values for the square. This can be more easily determined simply by testing the score of the square. The results are now Current Timing: 23.68 seconds. Improvement: 2.7% (total speedup 3360 times) A final note on improving algorithms: Take a look at the various algorithms that are available (see references in the “Choosing Algorithms” section, earlier in this chapter). See how different algorithms that perform the same task vary in their approach to the problem. Sometimes you need to look outside the square.
Exploring Techniques for Streamlining Code There are many techniques for speeding up C++ code. Efficient-looking C++ code does not necessarily mean efficient machine code. It is also true that many of the C++ code improvements result in code that is less readable and less maintainable, something you should consider when sticking to project priorities.
The Modern Processor Modern processors are very complex, and so is the machine code they run. Other than using inline assembly, usually you can’t directly affect the machine code that is generated from your C++ code. However, there are a few things that you can do to improve the performance of your code. What those things are varies according to the features of a particular processor.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
remove the word CAB and place the word BIT first, there is no need to attempt to place the word CAB at any stage because we solved all combinations of this earlier.
365
08 9721 CH06
11/13/00
366
9:53 AM
Page 366
C++Builder 5 Essentials PART I
In the following sections we look at features of Intel’s Pentium processor, in particular the Pentium II processor. Most of the features covered apply equally to AMD’s K-series processors. Branch Prediction The most important optimization for the Pentium II processor is static and dynamic branch prediction. There are three types of branches: unconditional, forward conditional, and backward conditional. In C++, unconditional branches are generated directly from the use of continue, break, and goto statements and indirectly from if, ?: and switch statements. Forward conditional branches are generated directly from if, ?:, switch, and while statements. Backward conditional branches are generated directly from for, while, and do statements. Take for example the (badly designed) code in Listing 6.4, which can call a different algorithm to optimize the drawing of small circles. LISTING 6.4
A Bad Example of Branching
if (Radius > MaxRadius) { return(false); } if (Radius < 50 && Filled) { CircleSmallAlgorithmFilled(X, Y, Radius); } if (Radius < 50 && !Filled) { CircleSmallAlgorithmOpen(X, Y, Radius); } if (Radius >= 50 && Filled) { CircleLargeAlgorithmFilled(X, Y, Radius); } if (Radius >= 50 && !Filled) { CircleLargeAlgorithmOpen(X, Y, Radius); } return (true);
The machine code generated by the compiler for the first two if statements is actually similar to that in Listing 6.5. LISTING 6.5
Compiler-Generated Code for the Bad Branching Example
if (Radius <= MaxRadius) { goto ifb;
08 9721 CH06
11/13/00
9:53 AM
Page 367
Compiling and Optimizing Your Application CHAPTER 6
LISTING 6.5
Continued
ifb: if (Radius >= 50) { goto ifc; } if (!Filled) { goto ifc; } CircleSmallAlgorithmFilled(X, Y, Radius); ifc: // etc. The other three if statements contain similar code.
Notice in the compiler-generated code in Listing 6.5 that the if statements generate the opposite Boolean expression. Each condition in the if statement is a forward conditional branch. When else statements are used, unconditional branches are required at the end of each if or else block, except the last, to jump out of the block once all statements in it have been executed, skipping the code in all following else blocks. In dynamic branch prediction, the processor stores the last four jump or continue directions that each of the previous 512 branches took. From this branch history, the processor guesses which direction a branch will take the next time and pre-fetches the instructions at the target location. It can identify patterns up to four directions in length, such as a branch that jumps every second time or one that jumps twice, continues, jumps, with this pattern repeated. If the branch is not yet in the history, typically because the code has not been executed before, then static branch prediction comes into play. Unconditional and backward conditional branches are predicted to be taken (jump) and forward conditional branches are predicted not to be taken (continue). Incorrect static and dynamic branch predictions are expensive in time, particularly if they occur within a loop. An incorrect prediction means that the pre-fetched instructions must be flushed from the execution pipeline and the correct target instructions fetched. There are two primary changes you can make in C++ code to improve branching performance. First, reduce the number of branches. This reduces the possibility of incorrect predictions and also reduces the amount of branch history that must be kept. The second change is to use if else statements (and particularly nested if statements) instead of multiple if statements at the same block level.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
} return(false);
367
08 9721 CH06
11/13/00
368
9:53 AM
Page 368
C++Builder 5 Essentials PART I
In our previous circle-drawing code, if Radius exceeds MaxRadius, then only one forward conditional branch is executed. Otherwise, seven forward conditional branches must be executed to go through each possible combination. Listing 6.6 shows how to change the code to achieve a reduction in branching. LISTING 6.6
A Much-Improved Branching Design
if (Radius > MaxRadius) { return(false); } else if (Radius < 50) { if (Filled) { CircleSmallAlgorithmFilled(X, Y, Radius); } else { CircleSmallAlgorithmOpen(X, Y, Radius); } } else { if (Filled) { CircleLargeAlgorithmFilled(X, Y, Radius); } else { CircleLargeAlgorithmOpen(X, Y, Radius); } } return (true);
In Listing 6.6, the same single forward conditional branch is executed if Radius exceeds MaxRadius, but for three of the four combinations only three forward conditional and one unconditional branch are required. The last combination, Radius >= 50 and !Filled, requires just the three forward conditional branches. The unconditional branch is not needed because execution simply falls through to the code following the entire if else block. Unfortunately, we don’t have this control for the implied if else blocks present in switch statements. The other thing that we can do to streamline branches is to improve branch predictability. When executing this code for the first time, static branch prediction comes into play. The first part of the code generated by the compiler is a forward conditional branch. Remember that this is predicted to fall through, to continue. Suppose that 95% of the circles drawn do not exceed the maximum radius and also, this code is not held in the branch history, because a large number of branches have occurred since the last time the code was executed. Under these circumstances, the branch prediction is incorrect. We would also have bad branch prediction if most of the circles had a radius greater than or equal to 50. You should order your C++ code so that the most likely condition is executed in the first if block, the second most likely in the first else block, and so on. Listing 6.7 shows a more efficient piece of code, assuming most circles have a large but valid radius.
08 9721 CH06
11/13/00
9:53 AM
Page 369
Compiling and Optimizing Your Application CHAPTER 6
LISTING 6.7
Restructured Branching to Suit the Most Common Conditions
return (true);
A final possibility for Boolean expressions is to evaluate them using the bitwise operators | and & instead of || and &&. For example, the code if ((A && B) || C) can be replaced with if ((A & B) | C). When you use bitwise operators instead of Boolean operators, the expression turns into one large bitwise calculation, and only one forward conditional branch is required. In some cases, bitwise operator calculation is faster than multiple branches when using Boolean operators, but in other cases it is slower. You will need to perform some basic profiling or timing tests to determine this. Cache The processor has 16KB of level-1 cache for code and 16KB of level-1 cache for data. The level-1 cache is an area of processor memory that is used for all machine code instruction execution and memory access. If either the machine code that must be executed or the data that must be accessed is not in the cache, then time is required to load it. Loading the code into the cache often takes longer that it does to execute it. When programming loops, try to keep them as tight as possible to keep the entire loop in the code cache. The first time through the loop will be a little slower because of cache loading, but after that it will run at full speed if it’s still in the cache.
Code Tuning We’ll look at many code-tuning techniques using small code snippets along the way. At the end of this section we’ll apply several of these techniques to the Crozzle Solver application. Four general principles apply:
6 COMPILING AND OPTIMIZING YOUR APPLICATION
if (Radius <= MaxRadius) { if (Radius >= 50) { if (Filled) { CircleSmallAlgorithmFilled(X, Y, Radius); } else { CircleSmallAlgorithmOpen(X, Y, Radius); } } else { if (Filled) { CircleLargeAlgorithmFilled(X, Y, Radius); } else { CircleLargeAlgorithmOpen(X, Y, Radius); } } } else { return(false); }
369
08 9721 CH06
11/13/00
370
9:53 AM
Page 370
C++Builder 5 Essentials PART I
• Remove redundant code. In large applications, implementation of design and code changes can render some code obsolete. A newer, possibly more efficient function typically can replace an older and slower one. Sometimes the code can be eliminated altogether. • Don’t use the register keyword. With register variables set to automatic, the compiler makes very good decisions about which variables should be used with registers. • Use the const keyword for variables, function parameters, and member functions for which the value should not be modified. This allows the compiler to make specific optimizations accordingly. • Don’t defeat the type system. Unnecessarily mixing different data types, such as assigning a double to a long, causes data conversions that are slow and prevents the compiler from performing certain optimizations. Now let’s look at some specific tuning techniques. Use Inline Functions Standard function calls include overhead at the machine code level to pass and return parameters, either by pushing and popping values onto the stack or by priming specific processor registers, and to perform the actual function call and return. For very small functions such as class member access functions, this overhead is more than that of the actual function code.
NOTE For information on how function calls and stack pointers work at the machine-code level, see Chapter 4, “Procedure Calls, Interrupts, and Exceptions,” in Volume 1 of the Intel Architecture Software Developer’s Manual. You can download this manual in PDF format from Intel’s Web site at http://developer.intel.com/design/ processor/. Select the appropriate processor, for example the Pentium II, and then enter the Manuals section.
Inline functions can be used to eliminate or reduce function call overhead. An inline function operates programmatically in exactly the same manner as a normal function. However, when the code is compiled, a copy of the function is placed inline at the position of the function call. This also enables the optimizer to work more efficiently. There are two main drawbacks to inline functions. First, they increase the code size, particularly if the inline function is large and called from many places. Second, if the function call is within a loop, then the increased code size reduces the chance that the entire loop will fit into the code cache (as previously described).
08 9721 CH06
11/13/00
9:53 AM
Page 371
Compiling and Optimizing Your Application CHAPTER 6
LISTING 6.8
An Example of Inline and Normal Functions
class TMyClass { private: int FValue; public: void SetValue(int NewValue) { FValue = NewValue; } int GetValue() const { return(FValue); } void DoubleIt(); void HalveIt(); }; inline void TMyClass::DoubleIt() { FValue *= 2; } void TMyClass::HalveIt() { FValue /= 2; } inline int Negate(int InitValue) { return(-InitValue); } void SomeFunction() { TMyClass Abc; int NegNewVal; Abc.SetValue(10); Abc.DoubleIt(); NegNewVal = Negate(Abc.GetValue()); }
In Listing 6.8 TMyClass::SetValue(), TMyClass::GetValue(), TMyClass::DoubleIt(), and Negate() are all inline functions, but TMyClass::HalveIt() and SomeFunction() are not.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
Inline functions are defined by explicit use of the inline keyword for normal functions and class methods defined outside the class body, and automatically for class methods defined in the class body. Listing 6.8 demonstrates.
371
08 9721 CH06
11/13/00
372
9:53 AM
Page 372
C++Builder 5 Essentials PART I
TIP If you are experimenting with inline functions, you should note that they are disabled by default when compiling with full debugging options and are called as normal functions. You can override this behavior to see more easily how they work in the CPU view of the debugger by unchecking Disable Inline Expansions on the Compiler tab of Project Options.
To be expanded inline, inline functions must be defined prior to the functions that call them. In Listing 6.8, if Negate() were defined after SomeFunction(), then it would not be expanded inline. There are some restrictions to which functions can be expanded inline, particularly when exception handling is enabled. A function that has an exception specification, takes a class argument by value, or returns a class with a destructor by value will not be expanded inline. In the Crozzle Solver application, we must resort to trial and error to see which of the main functions produced a better result when inlined. Only the CanPlace() function produces a faster result. Current Timing: 23.46 seconds. Improvement: 0.9% (total speedup 3391 times) Despite the large number of function calls involved, other factors come into play, such as the code cache and the fact that we’re using the register calling convention by default (compiler options). Inline functions are best suited to small functions. Eliminate Temporary Objects and Variables The creation and destruction of temporary objects and variables take time, particularly for an object, which can contain complex constructor and destructor code. This should be reduced or eliminated. Declare objects, strings, and simple variable types such as int, long, and double in the closest local scope possible. Listing 6.9 creates three temporaries: Obj, Name, and Length. LISTING 6.9
Three Temporaries Are Created
void SomeFunc(bool State) { TComplexObj Obj; String Name; int Length; if (State) { Name = “This is a string.”; Length = Name.Length();
08 9721 CH06
11/13/00
9:53 AM
Page 373
Compiling and Optimizing Your Application CHAPTER 6
LISTING 6.9
Continued
}
We can reduce the time spent on temporary creation by localizing their scope or eliminating them altogether. Listing 6.10 demonstrates this. LISTING 6.10
Temporaries Moved to Minimal Scope
void SomeFunc(bool State) { if (State) { String Name; Name = “This is a string.”; DoSomething(Name, Name.Length()); } else { TComplexObj Obj; Obj.Weight = 5.2; Obj.Speed = 0; Obj.Acceleration = 1.5; Obj.DisplayForce(); } }
To speed up the creation of temporaries, initialize objects at declaration by using an initialization or copy constructor. Simply declaring them and then assigning an initial value uses the default constructor followed by the copy constructor. Listing 6.11 shows how Listing 6.10 can be improved with this principle. LISTING 6.11
Temporaries Initialized at Declaration
void SomeFunc(bool State) { if (State) { String Name(“This is a string.”); DoSomething(Name, Name.Length()); } else { TComplexObj Obj(5.2, 0, 1.5); Obj.DisplayForce(); } }
6 COMPILING AND OPTIMIZING YOUR APPLICATION
DoSomething(Name, Length); } else { Obj.Weight = 5.2; Obj.Speed = 0; Obj.Acceleration = 1.5; Obj.DisplayForce(); }
373
08 9721 CH06
11/13/00
374
9:53 AM
Page 374
C++Builder 5 Essentials PART I
When incrementing or decrementing, using the postfix ++ and -- operators, a temporary is created to store the return value, as in the ++ (postfix) operator of MyIntClass used in the expression A++ in Listing 6.12. LISTING 6.12
A Temporary Created for operator++ to Return Initial Value
class TMyIntClass { private: int FValue; public: TMyIntClass(int Init) { FValue = Init; } int GetValue() { return FValue; } // Postfix ++ operator. int operator++(int) { int Tmp = FValue; FValue = FValue + 1; return(Tmp); } // Prefix ++ operator. int operator++() { FValue = FValue + 1; return(FValue); } }; void MyFunc() { TMyIntClass A(1); // A temporary is used. A++; // The return value of A++ was not required. // It is better to use the following because no temporary is used. ++A; }
If the return value of the postfix increment or decrement operator is not required, use prefix increment or decrement instead. Then, no temporary is used. In Listing 6.12 we can eliminate the temporary by using ++A instead of A++. When an object is passed by value to a function, a temporary object is created with the copy constructor for the function to use. It is preferable to pass by const reference instead to eliminate the temporary. For example, a function defined as MyFunc(TMyClass A) should be defined as MyFunc(const TMyClass &A) to eliminate the temporary object. A temporary is also created if a const reference parameter is passed but the types do not match.
08 9721 CH06
11/13/00
9:53 AM
Page 375
Compiling and Optimizing Your Application CHAPTER 6
LISTING 6.13
Return Value Optimization
TMyClass TmpReturnFunc(bool Something) { TMyClass TmpObj(0, 0); if (Something) { TMyClass Obj1(1.5, 2.2), Obj2(1.8, 6.1); TmpObj = Obj1 + Obj2; } return(TmpObj); } TMyClass FastReturnFunc(bool Something) { if (Something) { TMyClass Obj1(1.5, 2.2), Obj2(1.8, 6.1); return(Obj1 + Obj2); } else { return(TMyClass(0, 0)); } } void MyFunc() { TMyClass A; A = TmpReturnFunc(AddThem); // Temp was created in TmpReturnFunc A = FastReturnFunc(AddThem); // Return object built directly in A. }
In the first function, TmpObj must be constructed and destructed. In the second function, the return value can be built in the location of assigned object A. Invariant Calculations Invariant calculations that appear inside a loop should be moved outside the loop. In Listing 6.14, the invariant if statement and x + 5 calculation in the first section of the code are moved outside the loop in the second section. Now they are evaluated once instead of each time through the loop.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
A final temporary object elimination technique is called return value optimization. Where an object or variable is to be returned by value and that value is created only for return, it is much better to build it in-place rather than to create a temporary to hold the value and return the temporary. This is demonstrated in Listing 6.13.
375
08 9721 CH06
11/13/00
376
9:53 AM
Page 376
C++Builder 5 Essentials PART I
LISTING 6.14
Moving Invariant Calculations Outside the Loop
// Slow loop. for (int i = 0 ; i < 10 ; i++) { if (InitializeType == Clear) { a[i] = 0; b[i] = 0; } else { a[i] = y[i]; b[i] = x + 5; } } // Fast loops. if (InitializeType == Clear) { for (int i = 0 ; i < 10 ; i++) { a[i] = 0; b[i] = 0; } } else { int Total = x + 5; for (int i = 0 ; i < 10 ; i++) { a[i] = y[i]; b[i] = Total; } }
Watch out for any invariant portions of expressions, such as parts of a calculation that are fixed, invariant array index values, and pointer math contained within loops. Move them out, also. Array Indexing and Pointer Math If you have an expression that uses complex array indexing or pointer math, and the expression is used several times, it might be better to calculate a pointer to the correct data element or object and reuse that pointer. Consider the sections of code in Listing 6.15 from the crozzle solver algorithm. Because the lines are so long, they have been split into multiple lines. LISTING 6.15
Complex Array Indexing (Pointer Math)
// Complex array index part one from SolveStandardWord(). k = CrozLetterPos[Words[CurrWordNum].Letters[CurrLetterIdx]].NumPositions-1; while (k >= 0 && CrozLetterPos[Words[CurrWordNum].Letters[ CurrLetterIdx]].WordPlacedIdx[k] >= PlacedIdxLimit) { CurrX = CrozLetterPos[Words[CurrWordNum].Letters[ CurrLetterIdx]].Position[k].x;
08 9721 CH06
11/13/00
9:53 AM
Page 377
Compiling and Optimizing Your Application CHAPTER 6
LISTING 6.15
Continued
} // Complex array index part two from PlaceWord(). CrozLetterPos[Words[WordNum].Letters[LetterIdx]].Position[ CrozLetterPos[Words[WordNum].Letters[ LetterIdx]].NumPositions].x = CurrX; CrozLetterPos[Words[WordNum].Letters[LetterIdx]].Position[ CrozLetterPos[Words[WordNum].Letters[ LetterIdx]].NumPositions].y = StartY; CrozLetterPos[Words[WordNum].Letters[LetterIdx]].WordPlacedIdx[ CrozLetterPos[Words[WordNum].Letters[ LetterIdx]].NumPositions] = CrozGrid[StartY][CurrX].HorizPlacedIdx; CrozLetterPos[Words[WordNum].Letters[ LetterIdx]].NumPositions++;
If you can follow that mess, you’re doing well! In both sections of code it is better to use a temporary pointer to the correct data item, a TCrozLetterPos structure. Listing 6.16 shows how to change the code to use temporary pointers. LISTING 6.16
Simplified Array Indexing (Pointer Math)
// Simple array index part one from SolveStandardWord(). TCrozLetterPos *TmpPos; TmpPos = &CrozLetterPos[Words[CurrWordNum].Letters[CurrLetterIdx]]; k = TmpPos->NumPositions-1; while (k >= 0 && TmpPos->WordPlacedIdx[k] >= PlacedIdxLimit) { CurrX = TmpPos->Position[k].x; CurrY = TmpPos->Position[k].y; // More code here. } // Simple array index part two from PlaceWord(). TCrozLetterPos *TmpPos; TmpPos = &CrozLetterPos[Words[WordNum].Letters[LetterIdx]]; TmpPos->Position[TmpPos->NumPositions].x = CurrX; TmpPos->Position[TmpPos->NumPositions].y = StartY; TmpPos->WordPlacedIdx[TmpPos->NumPositions] = CrozGrid[StartY][CurrX].HorizPlacedIdx; TmpPos->NumPositions++;
6 COMPILING AND OPTIMIZING YOUR APPLICATION
CurrY = CrozLetterPos[Words[CurrWordNum].Letters[ CurrLetterIdx]].Position[k].y; // More code here.
377
08 9721 CH06
11/13/00
378
9:53 AM
Page 378
C++Builder 5 Essentials PART I
The two sections of code are much more readable and perform much better. Current Timing: 22.28 seconds. Improvement: 5.3% (total speedup 3571 times) That’s an improvement of 5% just for these two small parts. Floating-Point Math With current processors, the floating-point instructions are very fast. The floating-point functions implemented in C++Builder, such as cos() and exp(), usually are fast enough. However, to achieve the ultimate floating-point math speed, consider using the new C++Builder 5 fastmath functions. To use them, add #include to the units that need them. Many standard math routines will be automatically remapped to fastmath routines, including cos(), exp(), log(), sin(), atan(), and sqrt(). To turn off the Auto-Remap feature and use the fastmath routines, explicitly add #define _FM_NO_REMAP before including the header and use _fm_ instead of where applicable. For example, replace cos() with _fm_cos(). See the online help for more information.
CAUTION The fastmath routines do not check most error conditions. Use them only where speed is a high priority and your application has been thoroughly tested.
One worthwhile floating-point math code tuning technique is the use of floating-point divides in a loop. If you are doing a floating point divide X = A/B and the divisor B is constant, calculate a temporary variable T = 1/B before the loop and change the calculation to X = A * T. This is beneficial because floating-point multiplication is slightly faster than a floating-point divide. Other Code-Tuning Techniques There are two other techniques that deserve mention but are more specialized for certain applications. They include • Disable Runtime Type Identification if possible. To do this, you must refrain from using dynamic_cast and typeid. • Avoid virtual functions where possible. Each call results in a function table lookup. • Where graphics are necessary, minimize repainting. Use InvalidateRect() to repaint only the portions of the control that are necessary. Use an off-screen surface to draw an object and then copy to the screen in one operation, or hide the control during the procedure using the Hide() and Show() methods.
08 9721 CH06
11/13/00
9:53 AM
Page 379
Compiling and Optimizing Your Application CHAPTER 6
That’s it for code tuning. We looked at several of the most useful techniques for improving the performance of your application. Now we need to look at a few other tuning aspects.
Techniques for Streamlining Data Just as there are techniques for streamlining code, there also are techniques for streamlining data. As we saw earlier, the processor has a cache for code and data. It is just as important to keep data in the cache as it is to keep code there. Sequential memory access is faster than random memory access because a block is transferred to the data cache as each portion of memory is read. The data to the end of that block is accessed within the cache, which is very fast. As a result, linked lists, hash tables, and trees are very inefficient, because the data is not stored sequentially.
TIP If you use linked lists, use a custom managed-memory pool. Allocate space for several nodes in the linked list and use the nodes for objects that are created and deleted in the list. When an object is deleted, do not free the memory; instead, reuse it for a subsequent object. This enhances location of data and avoids the overhead of allocating and freeing memory for each object.
The use of smaller data types increases the possibility that the required data is in the cache, because more variables and objects will fit in. This is particularly important with the use of structures and objects. When using multidimensional arrays, keep them as small as possible. It is important when designing data structures that items have a good locality of reference. If you have two arrays, X A[] and Y B[], you must decide the best way to structure the data, depending on how it is to be accessed. If the array A[] is accessed independently of B[], it is better to declare them separately. If both arrays are accessed at the same time, particularly with the same index, then it is better to group array items together. Listing 6.17 demonstrates both methods.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
In the SolveWord() function, we periodically call Application->ProcessMessages() to handle any user interaction and display updates that are pending. This is necessary because the program is executing code intensely and ignoring other events. If we remove this part of the code, we reduce the run time by 1.5%. We’ll deliberately leave it in so that the user can interrupt the solve process and change view options.
379
08 9721 CH06
11/13/00
380
9:53 AM
Page 380
C++Builder 5 Essentials PART I
LISTING 6.17
Separate and Simultaneous Access for Good Locality of Reference
// Good for separate sequential access. X A[100]; Y B[100]; void SepFunc() { int Sum = 0; for (int i = 0 ; i < 100 ; i++) { Sum += A[i]; } // later in code. B[Index] = Q * R; } // Good for access to both arrays simultaneously. struct { X A; Y B; } T[100]; void SimFunc() { int AveDiff = 0; for (int i = 0 ; i < 100 ; i++) { AveDiff += T.B[i] - T.A[i]; } AveDiff /= 100; }
By defining substructures to contain array items, we can modify the Crozzle Solver application to use better locality of reference in the TCrozLetterPos and TUnsolved structures. In doing so, the time actually increases to 22.65 seconds! In optimizing TCrozLetterPos, a 12-byte structure TPlacedLetter was created as the array item. This has stopped the processor from being able to use power of two pointer math in the machine code to calculate the position of the array items. Power of two math is supported within machine code instructions without a performance penalty. Padding TPlacedLetter with a 4-byte int to bring it to a total of 16 bytes (2 to the power of 4) yields a time of 22.04 seconds. We can do even better by changing the two int values in the TPos structure to 2-byte short values. This makes TPlacedLetter 8 bytes, a power of two, but we now have to pad the less-used TAdjLetter array item to 8 bytes by adding a dummy short value. Now the time is Current Timing: 21.79 seconds. Improvement: 2.2% (total speedup 3651 times)
08 9721 CH06
11/13/00
9:53 AM
Page 381
Compiling and Optimizing Your Application CHAPTER 6
LISTING 6.18
Accessing Data in a Multidimensional Array
// Slow way to clear the array. Indexing is not sequential. for (a = 0 ; a < 10 ; a++) { for (b = 0 ; b < 5 ; b++) { T[b][a] = 0; } } // Fast way to clear the array. Indexing is sequential. for (b = 0 ; b < 5 ; b++) { for (a = 0 ; a < 10 ; a++) { T[b][a] = 0; } }
The crozzle application already reads through the two-dimensional grid in the correct method. Another technique for streamlining data is to use reference counting to avoid constructing and destructing an object when it is used multiple times. If you dynamically create unchangeable (const) objects, consolidate the multiple instances. Create an instance of the object using new the first time it is required and set a reference count variable to 1. On subsequent instances, simply return a pointer to the same object and increase the reference count. When the object is no longer required and the reference count reaches zero, it can be deleted. Finally, redundant data is just as bad as redundant code. Find and remove it.
Hand-Tuning Assembly Code Writing assembly code these days is a black art. The instruction set for the Pentium II processor contains over 200 integer instructions, almost 100 floating-point instructions, and about 30 system instructions. In addition to those “regular” instructions, there’s about 60 streaming SIMD extension instructions and around 60 MMX instructions. Throw in the processor nuances such as level-1 and level-2 cache, multiple pipelines that support parallel execution, paging, branch prediction, specific register use, dynamic dataflow analysis, speculative execution, and other detailed features and it all looks a bit too hard. Despite all that you need to learn to be able to write efficient assembly code, it has one big advantage: ultimate control. You can design code that will outperform an equivalent set of code written in C++ because you can use specific features of the processor and machine code to
6 COMPILING AND OPTIMIZING YOUR APPLICATION
A multidimensional array such as T[5][10] is stored as 5 sets of 10 items of type T. What this means is that the items T[0][0] to T[0][9] are located adjacently in memory, followed by T[1][0] to T[1][9] and so on, right through to T[4][9]. Because of this ordering, there is a slow way and a fast way to access all items of the array in a nested loop. This is shown in Listing 6.18.
381
08 9721 CH06
11/13/00
382
9:53 AM
Page 382
C++Builder 5 Essentials PART I
optimize at the lowest level. The C++ compiler can generate and optimize only the code that you write in C++. One of the main advantages with using a high-level language, and a disadvantage in this case, is that it hides the underlying native machine code. Teaching and demonstrating x86 (the Pentium processor family) assembly code is beyond the scope of this book. There are many great books out there if you’re interested. Some good references available at your fingertips are • The Art of Assembly Language Programming (http://webster.cs.ucr.edu/) An online book with over 1500 pages, available for download in PDF format or viewable online in HTML. • Intel Architecture Software Developer’s Manual (http://developer.intel.com/design/processor/) This is a three-volume set with just about everything you want to know about the Pentium processors. It is available for download from the Manuals section. • Iczelion’s Win32 Assembly HomePage (http://win32asm.cjb.net) Win32 assembly site with great tutorials and links.
An excellent
• Optimizing for Pentium Microprocessors (http://www.agner.org/assem/) Agner Fog’s Web site, containing detailed information on optimizing assembly code for Pentium processors. • The x86 Programming Newsgroup (comp.lang.asm.x86). To get more familiar with assembly code, you should enable all debug options and disable optimizations. Step through your code at the machine-code level via the CPU view in the debugger. Simply add a breakpoint to your code, run your application and, when the breakpoint is hit, bring up the CPU view. Stepping over and into machine code works the same as stepping over and into your C++ code. Examine the C++ code and equivalent assembly code.
TIP You can generate assembly code listings of your C++ source files by adding the -B option to the CFLAG1 section in the project file and checking Generate Listing and Expanded Listing on the Tasm page of Project Options. Each CPP file will generate an equivalent ASM file when the project is compiled. A separate method is to run BCC32.EXE -I -S file.cpp on the command line. C++ code appears as comments; useful comments are also given on registers that contain C++ variables.
In several of the previous sections, I’ve described some of the processor-specific features such as the instruction and data cache and branch prediction. They are just as applicable to assembly code as they are to C++ code.
08 9721 CH06
11/13/00
9:53 AM
Page 383
Compiling and Optimizing Your Application CHAPTER 6
Assembly code can be added directly in your C++ code using the asm keyword for individual assembly statements or in a block enclosed in braces. A register can be used in C++ code by prefixing it with an underscore, such as _EDX, _ECX, and so on. Listing 6.19 shows C++ code that contains an inline assembly code block to compute the factorial of an integer. LISTING 6.19
An Example of Inline Assembly Code to Compute a Factorial
int Factorial(int Value) { _EDX = Value; asm { push ebp mov ebp,esp push ecx push edx mov [ebp-0x04],edx mov eax,0x00000001 cmp dword ptr [ebp-0x04],0x02 jl end top: imul dword ptr [ebp-0x04] dec dword ptr [ebp-0x04] cmp dword ptr [ebp-0x04],0x02 jnl top end: pop edx pop ecx pop ebp } return (_EAX); }
As you can see, inline assembly code is more complex than C++ code. Here are some tips when writing inline assembly code: • When writing assembly code, you’ll get the most benefit producing it from scratch, forgetting about any C++ deficiencies. It’s not a good idea to try to improve on the assembly code produced by the optimizing compiler. • Break code into small logical pieces, use well-defined interfaces, and document the code well.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
Writing assembly code is much more difficult than writing C++ code, and it is much easier to introduce bugs. Also, it is much harder to find bugs. Consider maintainability and the skills of other programmers working on the project when deciding whether or not to optimize using inline assembly code.
383
08 9721 CH06
11/13/00
384
9:53 AM
Page 384
C++Builder 5 Essentials PART I
• Use instruction pairing wherever possible. Often you will need to write assembly code that executes out of logical order to make best use of parallel execution. • Reduce dependency chains. Break up statements that rely on the value of the previous statement to make better use of instruction pairing. • When performing lengthy instructions such as floating-point instructions, make sure that the processor has something else to do during this time. Pair with other code. • Always match a return (RET) instruction with a call (CALL) instruction to make best use of the return stack buffer (RSB). • Align data to make best use of space and instruction speed. In particular, align data on 32-byte boundaries to avoid cache-line misses. • Using inline assembly code affects the compiler optimizations performed to the function as a whole. Unlike some other optimizers, the C++Builder optimizer still performs code optimizations on a function containing inline assembly. You should perform timings to determine whether the improvements in using inline assembly code outweigh the loss in compiler optimizations. You should note that there are particular register restrictions when using inline assembly code within C++. Register values are not guaranteed to be preserved across separate asm blocks. You must preserve the ECS register with functions using the __fastcall calling convention. You should always preserve the ESP and EBP registers. For other restrictions, see the references previously listed. Good luck with your hand-tuned assembly code!
Using External Optimization Apart from the internal application issues, sometimes optimization can be performed externally. We saw earlier that the speed of the various sort algorithms depends on the initial ordering of the data. This is true for other applications as well. In the Crozzle Solver application, the order of the words in the word list affects the run time. It is better to list the words in order from longest to shortest. Doing so produces the following result: Current Timing: 17.94 seconds. Improvement: 21.5% (total speedup 4435 times) Although not a speed optimization, with an open-ended solve such as the list of 115 words in the RunPartial.crz file, it would be better to set the word order such that the words most likely to produce the highest score are placed first in the word list. Some trade-off between the letter scores of each word and the length of the word would make an optimal order. Additionally, for the user to get an acceptable result for a long word list, a better approach would be to solve small areas of the grid using selection bounding and build on the solution a piece at a time.
08 9721 CH06
11/13/00
9:53 AM
Page 385
Compiling and Optimizing Your Application CHAPTER 6
Execution Speed Optimization Summary
• Using const function parameters where appropriate. • Merging the Boolean IsValid, IsLetter, and IsBlank values into a single enumeration to save on data space. • Searching for words based on non-interlocking squares in the grid rather than searching for squares based on the letters in each word. • Performing a round robin of words to place at each level in recursion rather than always starting at the beginning of the word list. This would not be faster but would more likely produce a higher score in a given amount of time. • Keeping a record of interlocking squares and using only those when computing the score of the current crozzle. I’ll leave these optimizations up to you. See what else you can come up with. The initial execution time of the crozzle solver was 79,560 seconds, or 22.1 hours. The final execution time was 17.94 seconds, an improvement of 4435 times. For O(2N) algorithms such as the crozzle solver, the level of speed increase shown here is sometimes achievable, though this is fairly rare in general algorithms. As a separate speed measure, solving the RunPartial.crz file for a period of five minutes with the initial crozzle solver produced a high score of 402. Using the final crozzle solver, the high score is 484.
Optimizing Other Aspects of Your Application Besides optimizing for execution speed, there are several other aspects that can be optimized in applications today, such as program size, runtime memory size, disk access speed, and network bandwidth. Each has several overall techniques, some of which are listed briefly in the next two sections.
Optimizing Program Size Optimizing for program size assumes that smaller is better. This is usually for reasons such as distribution, where floppy and CD-ROM capacities and Internet access speeds are a constraint, and for reduction in system memory used, which is particularly important if there are many applications running on the same machine. The C++Builder compiler options include a Size Optimization setting. This will produce a better result than disabling optimizations and a slightly better result than the Speed Optimization
6 COMPILING AND OPTIMIZING YOUR APPLICATION
I’ve shown you many techniques for optimizing the design, algorithm, code, and data and given an overview of using assembly code and external optimization influences. For the Crozzle Solver application, there are several other optimizations that can be implemented, such as the following:
385
08 9721 CH06
11/13/00
386
9:53 AM
Page 386
C++Builder 5 Essentials PART I
setting. The other main project settings that affect code size are several debugging options, which should be disabled and Data Alignment set to Byte. If the shipping size is important, you can build with runtime libraries and packages and ship these in the first release. This will control the size of subsequent updates. Use a modular design and send only the modules that have changed, or use a patch program to apply a small set of changes to a program. Going somewhat against speed optimizations, you can reduce the program size by using algorithms instead of static data tables. You can also reduce program size by creating the tables dynamically. If your program uses a lot of graphical or sound data, you can sometimes trade quality for size. Use JPEG for photo pictures and GIF or PNG for simple graphics. Use a higher compression ratio, fewer colors, or smaller images. Reduce the number of bits or the sample rate and consider mono over stereo for sound. Don’t forget the many file compression programs that are available, such as ZIP. Additionally, most installation programs will compress your files. One final consideration is the use of a runtime EXE and DLL decompressor. These have a slower startup time, but your program is always compact.
Final Optimization Aspects To find out more, make useof the Borland newsgroups and the Web sites devoted to programming. • Disk access Sequential read and write is fastest because it minimizes disk head movement. Read in large chunks, not a character at a time. • Startup time Specify a unique and non-overlapping load address for all of your DLLs. Minimize the use of ActiveX controls where they will be visible at startup. Display a splash screen, perhaps with a tip-of-the-day to distract the user. • Network latency Send multiple packets simultaneously and receive replies in a block. • Client/server database access Use cached updates. Perform a potentially long query in a thread (such as for an update). The thread can come back with a list of errors to allow the user to return and fix them one at a time. Use SQL Monitor to see what your program is up to. For incremental search fields, use a delay before retrieving the matching data set. For a grid or list box load no more than perhaps two screens of items. Set scrollbar size appropriately. Use ranges instead of filters to restrict the set of records to return. Remember that these rules apply generally. There will certainly be instances where you will need to go against some of them and cases where the opposite is true.
08 9721 CH06
11/13/00
9:53 AM
Page 387
Compiling and Optimizing Your Application CHAPTER 6
Summary
Compiling and optimizing are advanced topics that are important to understand. I have found that knowledge of both from an early stage has assisted me no end in programming and debugging, and I encourage you to examine these topics further through the references provided throughout the chapter and in your own research.
6 COMPILING AND OPTIMIZING YOUR APPLICATION
In this chapter we looked at how the compiler works, various methods to speed up compile times, and new features in C++Builder 5. We also took an in-depth look at optimization, particularly optimizing for execution speed.
387
08 9721 CH06
11/13/00
9:53 AM
Page 388
09 9721 CH07
11/13/00
9:41 AM
Page 389
Debugging Your Application Jarrod Hollingworth
IN THIS CHAPTER • Debugging Overview • Basic Debugging Techniques • Using the C++Builder Interactive Debugger • CodeGuard • Advanced Debugging • Testing
CHAPTER
7
09 9721 CH07
11/13/00
390
9:41 AM
Page 390
C++Builder 5 Essentials PART I
Throughout this chapter we will examine many aspects of debugging applications. The term “applications” is used in a fairly loose sense; many of the principles apply to all types of projects. Debugging is an advanced topic and an important aspect of programming that is often looked at by developers in only a cursory manner. This chapter is for all readers regardless of experience. We’ll not be covering debugging of specific areas of development such as ISAPI, DirectX, or MIDAS applications, though we will cover briefly debugging DLLs and using remote debugging, both of which can be extended to debugging COM, DCOM, CORBA, and ActiveX servers. We will also cover CodeGuard and several other new features in C++Builder 5, with some notes on testing.
Debugging Overview Debugging is the process of locating of and correcting software bugs. Admiral Grace Hopper coined the term “bug” on September 9, 1945, when a moth was found to be the cause of a glitch in the Harvard Mark II mainframe computer. This term was readily adopted in software development to mean a programming glitch or error. So why do we have bugs? Writing bug-free code is very difficult. You may remember the public announcement from Microsoft of the thousands of bugs present in the first consumer release of Windows 2000. In large and complex systems, the number of bugs can be huge. Bugs can be classified into several categories: • Syntactical The compiler catch this type of error. The presence of syntactical errors suggests that other less-noticeable bugs may also be present. • Build Under certain conditions, a compilation might not produce the correct executable. This may be caused by doing a Make instead of a Build after incorporating source code changes from a version control system, using an incorrect library path or include path, or continuing to make software changes while a background compilation is in progress. • Semantic These include using uninitialized variables, using & instead of &&, and using the wrong variable name. The compiler cannot detect these. • Algorithm Logic Bugs caused by logic errors are more difficult to find. A simple example would be sorting a list of items by a size property with the largest first instead of the smallest first. • Pairing Some programming tasks, usually resource-based, require that a second task be performed in response to an initial task. Examples of pairing bugs are memory that is
09 9721 CH07
11/13/00
9:41 AM
Page 391
Debugging Your Application CHAPTER 7
391
created but not freed, files that are opened but not closed, a stack onto which an item is pushed on but never popped off. A subtler example is a reference count that is incremented when an object is used and not decremented when it is released. • Interface This is a mismatch in data sent from one application to another, or in the interface definition. It is usually caused by an incorrect protocol or interface version. • Side Effects By executing some code, you inadvertently cause another part of the system to function incorrectly. Overwriting memory and initializing a variable with the wrong value are examples.
Most bugs fall under one of these categories. There are other, less specific categories, such as user interface bugs that result in an application that is difficult to use and performance bugs causing the application to be too slow. The first principle of writing bug-free software is that you can’t rely on the compiler to pick up your mistakes. By all means, enable all possible compiler warnings and take notice of them, but as a secondary function only. Of the bug categories listed previously, the compiler can help with only the first one. When you select to Make or Build the project, you should be confident that it will compile and run correctly. The Code Insight feature in C++Builder contains several features that both increase your productivity and reduce the incidence of bugs. These include code completion, code parameters, code templates, and ToolTip symbol insight. Each of these can help reduce bugs. See the C++Builder online help for further information on these great productivity features.
Project Guidelines Debugging should be seen as the last resort in the whole software error overview. You should endeavor to minimize bugs from the very beginning and build automatic defense and detection mechanisms into your software. In the “Optimizing: An Introduction” section of Chapter 6, “Compiling and Optimizing Your Application,” the following project objectives were presented: • Speed • Size • Maintainability • Testability • Reusability
7 DEBUGGING YOUR APPLICATION
• Basic Functionality Basic functionality bugs are those in which a feature works but not as intended, for example because some part of it was left out or because results are produced for data from an incorrect source.
09 9721 CH07
11/13/00
392
9:41 AM
Page 392
C++Builder 5 Essentials PART I
• Robustness • Scalability • Portability • Usability • Safety If maintainability, testability, reusability, robustness, usability, or safety is important to your project, then prevention, detection, and removal of bugs will be a very important process throughout the project. The saying “a rolling stone gathers no moss” is not true for software and bugs, if you consider the rolling stone to be your evolving application and the moss to be bugs! The more development you do, the more likely it is that bugs will be introduced. Don’t spend time developing features that are not likely to be used just because it’s fun to do. First, it is time that could be better spent making the product more robust. Secondly, these features can be the cause of unnecessary bugs. Spend time finding stable third-party components and libraries that you can use in your application. If you don’t have to develop it yourself, you’re less likely to introduce bugs yourself. When a new feature is implemented in the application, inform all other project members. This will alert them to the changes or potential conflicts in interfaces and allow them to reuse the new functionality that you have added. This reduces the likelihood of two separate implementations of the same feature and thus reduces development time and the incident of bugs.
Programming Guidelines Most good programming techniques reduce bugs and the time required locating and fixing them. Chapter 3, “Programming in C++Builder,” discusses several coding style issues and good programming practices. Use good object-oriented design and good information and functionality hiding. Each C++ function or class method should have one well-defined task. A very important factor in all software development is code readability and source-level documentation. Regardless of whether it is just you or a team of programmers developing the application, you should always write code as if other people need to understand it. Comment on particularly tricky sections of code or code that has been programmed a certain way for efficiency or validation purposes. During development, ask yourself what assumptions you are making. Assumptions can cause problems later, because quite often the normal boundaries in code and data are crossed. You should verify that the assumption is true before continuing, particularly if the assumption is based on user input or input from interfaces to external systems.
09 9721 CH07
11/13/00
9:41 AM
Page 393
Debugging Your Application CHAPTER 7
393
If specific functionality or output is mission critical or has an impact on safety, you should ensure that it works correctly. Consider using a second method, such as a different algorithm, to validate the results of the first. During development, you should step through each line of your source code mentally and then using the debugger as each small part is completed. Nothing will give you a better understanding of how your code works.
Preventative measures in C++ involve checking error return values of functions, throwing exceptions, using exception handlers to catch anticipated and unanticipated exceptions, and using good type checking. Some exception handling information is given in Chapters 3 and 4, “Advanced Programming with C++Builder.” Don’t forget to test your error handling code, too.
The Debugging Task When you finally come to the task of debugging, and you always will, there are a few key concepts that you should follow. When you find a bug, document and prioritize it immediately. You can use the new To-Do list feature of C++Builder 5, detailed in Chapter 2, “C++Builder Projects and More on the IDE,” or simply use an ordered text file if you don’t have formal bug-tracking software. Once the bug is documented, you should fix it as soon as possible. Don’t put it off until the end of the project. This is one of the major reasons for out-of-control bug lists and poor estimation of development time. If you keep to a near-zero bug list, you are doing yourself and the project manager a huge favor. The bug list is not always bad news, particularly when most of the bugs have been moved to the “fixed” list. Consider making the bug list and fixed list available to your customers (whether they are internal or external). Personally, I’m more confident in a product that is upfront and in which I can see that bugs are being resolved. Before implementing a bug fix, you should make a backup copy of the source code. Use a version control system or simply save the original source file under another name or in a separate folder. A fantastic reference for all programmers is the book Code Complete by Steve McConnell, published by Microsoft Press (ISBN 1-55615-484-4, 1993). It covers all aspects of software construction and is a great read. I refer to it as “the programmer’s bible.” It is still my favorite book on software development.
7 DEBUGGING YOUR APPLICATION
You should also be very careful when fixing existing bugs not to introduce others. Don’t assume because you’ve made a quick fix that it will work or that you haven’t broken something else. Stick to the rule “if it ain’t broke, don’t fix it!” Don’t pull one nut off each of three wheels to attach the fourth.
09 9721 CH07
11/13/00
394
9:41 AM
Page 394
C++Builder 5 Essentials PART I
Finally, never allow the same bug to bite you twice. Learn from your own and others’ mistakes and prevent them from happening in the future.
Basic Debugging Techniques In this section, we will examine some basic methods for locating bugs. In later sections, we will see how to locate bugs using the interactive debugger supplied with C++Builder and look at several advanced debugging topics. Before embarking on a possibly lengthy debugging mission, if you suspect that the problem lies in third-party components or libraries, you should check with the vendor to determine if such a problem is a known issue and if there is an update or patch that will correct it. The main techniques of locating bugs are • Manual or programmatic examination of application output as a result of testing. • Manual or programmatic examination of the execution path and internal data. • Programmatically trapping invalid conditions using assertions and other checks. • Programmatically catching exceptions through the implementation of exception handling. Manual examination relies on stepping through code or outputting information at appropriate places, often using the C++Builder interactive debugger. Using the knowledge of what the input data is and the correct course of action that the application should take, you can determine if it is functioning correctly. Programmatically trapping invalid conditions and exceptions can tell you exactly where the bug was discovered. The problem is that where the bug is discovered can often be very far from where the bug starts. Use these techniques as a starting point for locating bugs. Several of the techniques presented here are described in more detail later in this chapter. To track down the specific source of a bug, three main processes can be used, as described in the following list. • Mentally trace backward from where the bug is detected through the execution path that has occurred, using the knowledge of variable and object states. Examine the call stack to give you clues as to the execution path. Follow the data that could have caused the bug to occur. Use manual or programmatic examination or strategically placed assertions for subsequent runs to narrow the source of the bug. • From a point in the execution path that is known to be sound, walk through the code, mentally or using the interactive debugger, toward the point where the bug was detected. Use the same techniques as for the trace backward method.
09 9721 CH07
11/13/00
9:41 AM
Page 395
Debugging Your Application CHAPTER 7
395
• The third method is called “divide and conquer” and involves a kind of binary search or bracketing to find the bug. The region is refined—typically halved as in a binary search—repeatedly by changing the outer inspection points and narrowing the source of the bug to a small section of code. Again, use the techniques described for the trace backward method. However you decide to locate the bug, it should be a systematic approach based on correct and reproducible program output or debug information and not some random or sporadic approach. If you cannot reproduce the error reliably, then it is likely that the error is caused by uninitialized data or memory overwrites.
When you find a bug, spend the time to find the real cause of the problem and the right solution. Don’t use quick fixes, which are likely to miss other error causes or introduce other bugs. Once the fix is implemented, test it thoroughly.
Outputting Debug Information A basic debugging technique is to output information at one or more places in your application to see what code is being executed and the state of particular objects and variables. Only you can decide what output is appropriate, but you should display input data to verify that it is correct, display parameters passed to key functions, and display execution path information to show you what decisions the application makes at key stages. We’ll look at several techniques in this section that can be used to output this information. A useful new feature in C++Builder 5 is that _DEBUG is defined in the Conditional defines list on the Directories/Conditionals tab of the project options when the Full Debug button on the Compiler tab is pressed. It is defined by default. When the Release button is pressed, _DEBUG is removed from the Conditional defines list. You can change the name of this definition by adding a string value named DebugDefine in the HKEY_CURRENT_USERS\Software\Borland\ C++Builder\5.0\Debugging key in the Windows Registry. The _DEBUG definition allows you to specify certain sections of code that should be compiled into the application only when it is compiled with Full Debug options by using the #ifdef and #endif preprocessor directives. Users of C++Builder 4 and earlier must manually add and remove _DEBUG or a similar definition to the Conditional defines list or specify it in code with #define _DEBUG. We’ll use the _DEBUG definition throughout the remainder of this section.
7 DEBUGGING YOUR APPLICATION
If in a relatively short period of time you have not found the bug, take a short break and then try a different approach or consult another developer for his view. Often, describing the problem to someone else helps you realize where the bug is located. Be especially wary of recently modified code.
09 9721 CH07
11/13/00
396
9:41 AM
Page 396
C++Builder 5 Essentials PART I
There are several ways to output debugging information from within your code. The simplest method is to use printf() or cout << statements if you are debugging a console application. You must include the stdio.h header file when using printf() and iostream.h when using cout <<. Listing 7.1 shows how either of these methods can be used to write a generic debug message output function, MyDebugOutput(), and an example of how it might be used from within an application. The text Debug: will be displayed at the start of each debug message, to distinguish debug messages from regular application output. LISTING 7.1
Outputting Debug Messages in Console Applications
#include <stdio.h> #include void MyDebugOutput(AnsiString OutputMessage) { // Method 1: Display the debug message using printf(). printf(“Debug: %s\n”, OutputMessage.c_str()); // Method 2: Display the debug message using cout <<. cout << “Debug: “ << OutputMessage.c_str() << endl; } void NormalFunc(int MaxLines) { // Application code here. MyDebugOutput(“Before loop, MaxLines=” + IntToStr(MaxLines)); for (int i = 0 ; i < MaxLines ; i++) { MyDebugOutput(“In loop, i=” + IntToStr(i)); // More application code here. } }
For GUI applications, you can use a label, an edit box, or a memo on a form or use ShowMessage() or MessageDlg() pop-up dialog boxes. You can even use the Win32 API function MessageBeep(0xFFFFFFFF) to make a sound for an audible rather than visual cue. Listing 7.2 shows the MyDebugOutput() function from Listing 7.1, rewritten to use these GUI output methods. The MyDebugBeep() function in Listing 7.2 can be called anywhere that you need an audio cue in your application.
09 9721 CH07
11/13/00
9:41 AM
Page 397
Debugging Your Application CHAPTER 7
LISTING 7.2
397
Outputting Debug Messages in GUI Applications
void MyDebugOutput(AnsiString OutputMessage) { // Method 1: Display the debug message to a label, edit box and // memo on the main form. Any one of these is probably sufficient. MainForm->ErrorLabel->Caption = OutputMessage; MainForm->ErrorEdit->Text = OutputMessage; MainForm->ErrorMemo->Text = OutputMessage; // Method 2: Display the debug message using ShowMessage(). ShowMessage(OutputMessage);
} void MyDebugBeep() { // Make the default Windows beep. MessageBeep(0xFFFFFFFF); }
TIP In a multithreaded application, it is often helpful to also output the thread ID. The current thread ID can be retrieved with the GetCurrentThreadId() Win32 API function.
The previous debugging output methods are fine for basic purposes. One problem with the pop-up dialog boxes is that they are difficult to use when debugging a Paint() method of a graphical component. The display of the pop-up dialog box can obscure part of the control, causing a WM_PAINT message to call the Paint() method again. A better alternative if debugging within the IDE is to use the OutputDebugString() Win32 API function. It is a generic function that outputs a given string that can be picked up by a debugger. In our case, the message is output to the C++Builder integrated debugger and sent to the event log. No output is generated if the application is run outside the IDE (outside the debugger). You can see the event log from View, Debug Windows, Event Log or by pressing Ctrl+Alt+V. Dock the Event Log window below the Code Editor or Class Explorer. The OutputDebugString() function takes one parameter, the user-defined message to output of
DEBUGGING YOUR APPLICATION
// Method 3: Display the debug message using MessageDlg(). MessageDlg(OutputMessage, mtInformation, TMsgDlgButtons() << mbOK, 0);
7
09 9721 CH07
11/13/00
398
9:41 AM
Page 398
C++Builder 5 Essentials PART I
type const char *, giving you total control. All output to the event log is prefixed with ODS: and has a suffix of Process <project>.exe. Some great features are packed into two macros: TRACE and WARN. They use generate messages but automatically provide file and line number information and support the iostream operators for constructing the user-defined message. To enable these, you must use #define __TRACE and #define __WARN and after them add the statement #include . Then you can use them in your code. TRACE takes one argument, a string stream that is the user-defined message. WARN takes two arguments: a test expression and the user-defined message. It will only output the message if the expression evaluates to true. OutputDebugString()to
Listing 7.3 combines the _DEBUG define and the TRACE and WARN macros to conditionally generate the debug output. While it is okay to leave these compiled in when shipping your application, since the OutputDebugString() messages won’t appear anywhere, the application will suffer a very slight performance hit. The TRACEF macro in Listing 7.3 is similar to TRACE, but I’ve added the function name to the message. The OutputDebugString() function is also demonstrated. LISTING 7.3
Using TRACE, WARN, and OutputDebugString when Compiled in Debug Mode
#ifdef _DEBUG #define __TRACE #define __WARN #endif #include #pragma option -w-ccc #define TRACEF(s) TRACE(__FUNC__ “: “ << s) void MyFunc(AnsiString Title, int *MyArray, int Max) { int i, Sum = 0; TRACE(“Simple trace”); TRACEF(“Includes function name”); for (i = 0 ; i < 10 ; i++) { Sum += MyArray[i]; } TRACE(“The sum is “ << Sum); WARN(Sum > Max, “The Sum is too big! The maximum is “ << Max); OutputDebugString(Title.c_str()); }
09 9721 CH07
11/13/00
9:41 AM
Page 399
Debugging Your Application CHAPTER 7
399
You’ll notice in Listing 7.1 that the -w-ccc compiler option is set using the #pragma option preprocessor directive. TRACE and WARN are actually implemented as a do { } while (0) loop, which results in a Condition is always true compiler warning for each use. This directive disables that warning. Alternatively, you can set the compiler option directly in the project file or disable the warning through the project options by setting Warnings to Selected on the Compiler tab, pressing the Warnings button, and unchecking the Condition is always true... warning, prefixed with W8008 in C++Builder 5.
LISTING 7.4
A Simple Expression Log Macro
#ifdef _DEBUG #define __TRACE #define __WARN #endif #include #pragma option -w-ccc #define DEBUGEXP(x) TRACE(__FUNC__ “: “ #x “=” << (x)) void MyFunc() { int A = 6, B = 7; DEBUGEXP(A); DEBUGEXP(A+B); }
You must be aware of possible side effects that can occur from passing certain arguments to all of the macros covered so far, such as function calls, variable assignments, or ++ increment or -- decrement operators. For example, writing DEBUGEXP(++a) will actually increment a, but if _DEBUG is not defined, the statement is not executed, and a will not be incremented. Generally it is advisable to stay away from macros like DEBUGEXP and implement them as inline functions. However, a macro is required to use the __FUNC__ macro and # (convert to string) macro directive in the previous example.
7 DEBUGGING YOUR APPLICATION
Often, when outputting debug information, you’ll want to display the contents of some variable and include the variable name, such as SomeVar=Value, or display the result of some simple calculation along with the expression that produced it, like A+B=13. This can be difficult to put together in the formulation of the debug string and difficult to change. The # macro directive can alleviate a lot of the difficulties by converting a macro argument to a string literal. The DEBUGEXP macro in Listing 7.4 demonstrates this.
09 9721 CH07
11/13/00
400
9:41 AM
Page 400
C++Builder 5 Essentials PART I
One last pair of macros is presented in Listing 7.5 for tracing the execution of statements. LISTING 7.5
Tracing Code Execution
#ifdef _DEBUG #define __TRACE #define __WARN #endif #include #pragma option -w-ccc #define DEBUGTRCN(s) TRACE(“CMD: “ #s); s #define DEBUGTRCC(s) s; TRACE(“CMD: “ #s) void MyFunc() { int i, Sum; DEBUGTRCN(Sum = 0); DEBUGTRCN(for (int i = 0 ; i < 10 ; i++) {); DEBUGTRCN(Sum += i); DEBUGTRCC(}); DEBUGTRCN(if (Sum > 20) {); DEBUGTRCN(ShowMessage(“Sum is large”)); DEBUGTRCC(} else {); DEBUGTRCN(ShowMessage(“Sum is small”)); DEBUGTRCC(}); }
The DEBUGTRCN and DEBUGTRCC macros in Listing 7.5 expand to two statements each: One displays the statement that is being executed, and the other is the actual statement that executes. DEBUGTRCN is used for most statements, but for statements that include a closing brace (}), you must use DEBUGTRCC, as demonstrated in the example. Because the macros expand into two statements, they cannot be used for statements that must be singular, such as a single statement executed after an if or for statement, without using braces to encompass the statement. There are no side-effect issues with these macros, however.
Using Assertions An assertion is a means of validating assumptions made in code. It asserts that a particular expression is true. If the statement is not true, then the assertion can display an error message and abort or throw an exception. The WARN macro in the previous section is an example of a
09 9721 CH07
11/13/00
9:41 AM
Page 401
Debugging Your Application CHAPTER 7
401
basic assertion, except that it uses the reverse logic and only displays some debug output without aborting. Some programmers prefer to implement exception handling instead of assertions. We’ll look at that briefly in the next section, “Implementing a Global Exception Handler.” Adding the appropriate assertions to your code can be a lengthy process. It is best to add them as you code, so that the assumptions you’re making are fresh in your mind. Assertions are usually added to the start of a function call or class method but can be used anywhere in your code.
To use the built-in assertions in C++Builder, you must include the assert.h header file by adding #include to your code. Then, anywhere in your code that you want to make an assertion, simply enter assert(expression). Remember that, unlike the WARN macro, the expression should evaluate to true for normal operation and false if the assumption is incorrect. Listing 7.6 demonstrates the use of assertions for validating function parameters for range and validity and for general conditions. LISTING 7.6
Using Assertions
// Standard header files above this. #include // Functions start here. void MyFunc(int Width) { int Length; // Width can’t be zero. assert(Width != 0); Length = Area/Width; // Length should be less than MaxLength assert(Length < MaxLength); // Everything ok, continue.
7 DEBUGGING YOUR APPLICATION
Assertions are usually included only in a debug or test release and rarely in a final shipping release. However, the assertion statements should be left in the source code and compiled out with the use of #ifdef preprocessor directives. This allows you to use them to perform more testing in the future, after new features are added to the application, for example. Most assertions have the compile-out functionality built in.
09 9721 CH07
11/13/00
402
9:41 AM
Page 402
C++Builder 5 Essentials PART I
LISTING 7.6
Continued
} void DisplayNode(TNode *Node) { // Make sure that the node is valid. assert(Node != (TNode *)NULL); // Everything ok,
continue.
}
When an assertion fails in a console application, the message Assertion failed:, followed by the assertion expression, the unit filename, and the line number of the assertion statement, is output to stderr. In a GUI application, a dialog box is displayed that contains the assertion expression, the unit filename, and the line number of the assertion statement. Figure 7.1 shows the error message that would appear in a GUI application if zero were passed in the argument to MyFunc() in the previous example.
FIGURE 7.1 This assertion dialog box appears when an assertion is fired.
Assertions can be removed from your code at compile time by defining NDEBUG. This can be done in the Conditional defines list on the Directories/Conditionals tab of the project options or by adding #define NDEBUG before the #include if _DEBUG is not defined to tie all debug code to a single definition. As stated previously, assertion statements should be left in your code. Because assertions can be compiled out, the assertion statements should not contain any assignment statements, variable increments or decrements, or calls to functions that may create side effects. Going against this rule will create inconsistencies between debug and release versions of your application. Remember to document any assumptions that are unclear in the source code.
Implementing a Global Exception Handler An alternative or an addition to using assertions is to use good exception handling. In particular, implement a global exception handler to catch all unhandled exceptions.
09 9721 CH07
11/13/00
9:41 AM
Page 403
Debugging Your Application CHAPTER 7
403
A global exception handler can report what the exception is and where it occurred and possibly the current state of the application. An advanced global exception handler that logs the application state to a file is covered in detail in the “Implementing an Advanced Exception Handler” section in Chapter 4. A simple alternative to the method described in Chapter 4 is to compile with exception location information by checking Enable Exceptions and Location Information on the C++ tab of the project options or by setting the -xp compiler command-line option. By adding #include <except.h>, you have access to three important functions: •
__ThrowExceptionName():
Returns a (char
*)
pointer to the name of the thrown
•
__ThrowFileName():
Returns a (char
*)
pointer to the name of the file in which the
exception occurred. •
__ThrowLineNumber(): Returns an unsigned source file in which the exception occurred.
int
containing the line number of the
A simple global exception handler is demonstrated in the following code: #include <except.h> // In the main function for the application. try { // Do the main work here (call functions etc.) } catch ( Exception &e ) { ShowMessage(e.Message + “\nType: “ + __ThrowExceptionName() + “\nFile: “ + __ThrowFileName() + “\nLine: “ + AnsiString(__ThrowLineNumber())); }
Of course you can build a more elaborate version, for example one that could write the information to a file, so that you could ask the user to email the file to your help desk.
Other Basic Debugging Issues There are a few nuances of C++ that can make it hard to find a bug when eyeballing the code. Some of them are given in the following list: • Be aware that numbers starting with 0 (zero) are octal values. The number 010 is octal 10 (8 in decimal). • Be careful not to confuse the number 0 (zero) with the letter O (oh), when setting up input data and performing string or character comparisons.
7 DEBUGGING YOUR APPLICATION
exception.
09 9721 CH07
11/13/00
404
9:41 AM
Page 404
C++Builder 5 Essentials PART I
• Watch out for expressions that use the bitwise operators &, |, or ~ instead of the Boolean operators &&, ||, or ! unless it is intentional, such as for optimization reasons. • Enclose macro expressions in parentheses. A macro MAX(a,b) defined as a>=b?a:b will not work with Z = Y + MAX(A,B). Define it in parentheses, as (a>=b?a:b) instead. • Beware of side effects when using macros. If MAX(a,b) is defined as (a>=b?a:b), then the expression Z = MAX(A++,B) will cause A to be incremented twice, but Z will contain the value as if A were incremented once. • Warn other programmers that a macro is a macro and not a function by using uppercase names, and use inline functions instead of macros where possible. • Avoid tricky functions such as strncpy(s1, s2, length), which may or may not append a trailing NULL character to string s1 when copying string s2, depending on the value of the length argument and the length of s2. In the following sections, we move to more advanced debugging techniques.
Using the C++Builder Interactive Debugger C++Builder’s interactive debugger contains many advanced features, including expression evaluation, data setting, object inspection, complex breakpoints, a machine code disassembly view, an FPU and MMX register view, cross-process debugging, remote debugging, attaching to a running process, watching expression results, call stack viewing, the ability to single-step through code, and more. During development you will spend a lot of time using it, or at least you should! The debugger is not just for finding bugs; it is also a general development tool that can give you great insight into how your application works at a low level. To use the debugger effectively, you must first disable compiler optimizations. When compiler optimizations are enabled, the optimizer will do everything in its power to speed up or reduce the size of your code, including removing, rearranging, and grouping sections of code. This makes it very difficult to step through your code and to match up source code with machine code in the CPU view. If you set a breakpoint on a line and it is not hit when you are confident that the line was executed, it is probably because you have optimizations enabled. Screen real estate becomes a problem with the many debug views that you’re likely to need during a debugging session. In C++Builder 5, make use of the desktop settings to create a layout appropriate for programming and a separate layout for debugging. My typical desktop layout for debugging is shown in Figure 7.2. You can see: (A) The Call Stack, Watches, and Local Variables views docked together vertically and placed on the left of the screen. (B) The Code Editor placed center to upper-right and occupying most of the screen.
09 9721 CH07
11/13/00
9:41 AM
Page 405
Debugging Your Application CHAPTER 7
405
(C) The CPU view placed below the Code Editor window at the bottom right of the screen. From time to time during debugging, I display the Debug Inspector window and superimpose it over (A) and dock the Event Log below the Code Editor when the CPU view is not needed. For a layout such as this, a large monitor and screen resolution of 1280×1024 are highly recommended. B
7 DEBUGGING YOUR APPLICATION
A
C
FIGURE 7.2 An example debugging window layout.
For the remainder of this section, it is assumed that you understand the basics of the debugger, such as using source breakpoints with expression and pass-count conditions, stepping over and into code, and using ToolTip expression evaluation (holding the mouse pointer over an expression while the application is paused in the debugger). We’ll concentrate on several of the more advanced features.
Advanced Breakpoints Apart from the standard source breakpoints that simply halt execution when the selected source or assembly line is reached, there are more advanced breakpoints that can be used in particular
09 9721 CH07
11/13/00
406
9:41 AM
Page 406
C++Builder 5 Essentials PART I
debugging cases. In the following section, we’ll look at some of the new breakpoint features in C++Builder 5. Module load breakpoints are particularly useful when debugging DLLs and packages. You can pause execution when a specified module is loaded, providing a perfect entry point into the DLL or package for debugging. To set a module load breakpoint, you have two options. The first option applies when the application is already running within the IDE. First, display the Modules window by selecting View, Debug Windows, Modules. Next, in the modules list in the upper-left pane of the Modules window, locate the module for which you want to set the module load breakpoint. If the module is not in the modules list, then it has not yet been loaded by the application. In that case you will need to add the module to the modules list by selecting Add Module from the context menu, then name or browse to the module and select OK. Finally, select the module in the modules list and then select Break On Load from the context menu. This is shown in Figure 7.3. If the module has already been loaded by the application, then the breakpoint will work only when the module is loaded again, either after being dynamically unloaded or when the is restarted.
FIGURE 7.3 Setting a module load breakpoint from the Modules view.
The second option applies when the application is not yet running within the IDE. Select Run, Add Breakpoint, Module Load Breakpoint, then enter the module name or browse to it and select OK. Finally, run the application. Address breakpoints and Data breakpoints provide a way to pause the application when a particular code address is reached or data at a particular address is modified. They can only be added when the application is running or paused. Address breakpoints work in the same manner as source breakpoints, although instead of adding the breakpoint to a particular line of source code, you add the breakpoint to the memory address for a particular machine code instruction. When the machine code is executed, the breakpoint action is taken. If you set an address breakpoint for a machine code instruction that
09 9721 CH07
11/13/00
9:41 AM
Page 407
Debugging Your Application CHAPTER 7
407
is related to a line of source code, the breakpoint is set as a normal source breakpoint on that line of source code. Address breakpoints are typically used when debugging external modules on a low level using the CPU view. The CPU view is explained in the section “The CPU View,” later in this chapter.
Next, select View, Units, then select BreakpointForm from the units list and select OK. The unit will be displayed in the Code Editor. Scroll down to the AddressBreakpointButtonClick() function. Right-click on the Label2->Caption = “New Caption” statement in the function and select Debug, View CPU from the context menu. The CPU view is again displayed, this time at the memory address where the machine code for the C++ statement is located. In the upper-left pane in the CPU view, note the hexadecimal number on the far left of the line containing the machine code statement lea eax,[ebp-0x04]. On my system at present, this number is 004016ED, but it is likely to be different on yours. This is the address at which we will set the address breakpoint. To add an address breakpoint for this address, select Run, Add Breakpoint, Address Breakpoint, then, in the Address field, enter the address that you previously noted. Hexadecimal numbers, such as the address displayed in the CPU view that you noted, must be entered with a leading 0x; in my case I would specify the address as 0x004016ED. To test the address breakpoint, continue the program by selecting Run, Run. Select the application from the Windows taskbar and click the Address Breakpoint button. The address breakpoint previously set will cause the application to pause. The CPU view will be displayed with the current execution point, marked by a green arrow, set on the address breakpoint line of machine code. You can continue the application by selecting Run, Run. If, in the CPU view, you display the line of machine code at the address at which you wish to place an address breakpoint, you can set an address breakpoint simply by clicking in the gutter of the machine code line, as you would in source code for a source breakpoint. We could have done this in the previous example, but I want to demonstrate the overall principle of specifying the actual address. Data breakpoints can be invaluable in helping to track bugs, by locating where in the code a particular variable or memory location is being set. As an example, load the
7 DEBUGGING YOUR APPLICATION
We will now walk through a demonstration of how to set an address breakpoint. In the Breakpoints folder on the CD-ROM that accompanies this book, you will find the BreakpointProj.bpr project file. Load it with C++Builder, then compile and run the application by selecting Run, Run. When the form is displayed, pause the application by selecting Run, Program Pause from the C++Builder main menu. The CPU view, which we will use in a moment, will be displayed.
09 9721 CH07
11/13/00
408
9:41 AM
Page 408
C++Builder 5 Essentials PART I
project file from the previous demonstration. Run and then pause the application. Select Run, Add Breakpoint, Data Breakpoint. In the Address field, enter Form1>FClickCount and select OK. This private data member of the form counts the number of times that the form’s DataBreakpointButton button is clicked. Setting this data breakpoint will cause the application to break whenever the count is modified. BreakpointProj.bpr
As you can see, any valid data address expression can be entered, not just a memory address. Alternatively, to obtain the address, we can select Run, Inspect, enter Form1->FClickCount in the expression, and obtain the address from the top of the Debug Inspector window. This hexadecimal address could then be entered (with a leading 0x) in the data breakpoint Address field. To test the data breakpoint, continue the program by selecting Run, Run. Select the application from the Windows taskbar and click the Data Breakpoint button. The data breakpoint previously set will cause the application to pause at the location where the data was modified. If this is on a source line, the Code Editor will be displayed, otherwise the CPU view will be displayed. You can continue the application by selecting Run, Run.
TIP Adding a data breakpoint for a property such as the Caption of a label or the Text of an edit box is much trickier. These properties are not direct memory locations that are written to when the property is modified. Instead, the properties use Set functions to change their values. To break when the property is changed, it is easiest to add an address breakpoint for the Set function of the property, rather than finding the memory address where the data is actually stored and adding a data breakpoint. I’ll explain this method using the Caption property of ClickCountLabel on the form of the previous demonstration project. With the application paused, select Run, Inspect. In the Expression field enter Form1-> ClickCountLabel. Select the Properties tab in the Debug Inspector window and scroll down to the Caption property. The write method for this property is specified as SetText. Click on the Methods tab and scroll down to the SetText method. The address of this method will be displayed on the right. Select Run, Add Breakpoint, Address Breakpoint and enter the address of the SetText method, prefixing it with 0x and leaving out the colon, then click on OK. Continue the application by selecting Run, Run. Now, whenever the label caption is modified, the breakpoint will pause the application. For a standard AnsiString variable without a Set method to control its modification, such as the FSomeString private data member of the form in the demonstration project, you can set a data breakpoint on the .Data variable of the AnsiString class that contains the underlying string data. For the demonstration project, the data breakpoint would be set on Form1->FSomeString.Data.
09 9721 CH07
11/13/00
9:41 AM
Page 409
Debugging Your Application CHAPTER 7
409
When adding a data breakpoint, the Length field in the Add Data Breakpoint window should be specified for non-singular data types, such as structures or arrays. The breakpoint will pause the application when any memory location within this length from the data address is modified. Data breakpoints can also be added by selecting Break when Changed from the context menu in the View, Debug Windows, Watches view.
NOTE
New Breakpoint Features in C++Builder 5 Breakpoints can now be organized into groups and have actions. With breakpoint actions, you can enable and disable groups of actions, enable and disable exception handling, log a message to the event log, and log the evaluation of an expression to the event log. Using these features, you can set up complex breakpoint interaction to break only in specific program circumstances. For example, you can cause a set of breakpoints to be enabled only when a specific section of code is executed. By disabling and enabling exceptions, you can control error handling in known problem areas of code. Message logging helps automate variable inspection and execution tracing. The new breakpoint action and group information are available in the breakpoint ToolTip in the Code Editor and in the breakpoint list window.
C++Builder Debugging Views The debugger can be used to display many types of information that are helpful with debugging an application, such as local variables, a list of all breakpoints, the call stack, a list of the loaded modules, thread status, machine code, data and register status, an application event log, and more. New to C++Builder 5 is the Floating-Point Unit (FPU) view, which shows the current state of the floating point and MMX registers. All debugging views are accessible from the View, Debug Windows menu option or by pressing the appropriate shortcut key. In the following sections we’ll look at some of the advanced views and how you can use them in debugging your application.
7 DEBUGGING YOUR APPLICATION
Address and data breakpoints are valid only for the current application run. You must set them for each new run because the machine code instruction and data addresses can change each time.
09 9721 CH07
11/13/00
410
9:41 AM
Page 410
C++Builder 5 Essentials PART I
The CPU View The CPU view your application at the machine code level. The machine code and disassembled assembly code that make up your application are displayed along with the CPU registers and flags, the machine stack, and a memory dump. The CPU view has five panes, as depicted in Figure 7.4.
FIGURE 7.4 The CPU view.
The large pane on the left is the disassembly pane. It displays the disassembled machine code instructions, also known as assembly code, that make up your application. The instruction address is in the left column, followed by the machine code data and the equivalent assembly code. Above the disassembly pane, the effective address of the expression in the currently selected line of machine code, the value stored at that address, and the thread ID are displayed. In Figure 7.4 the effective address of [ebp+0x20] is 0x0012F4D4, the value stored at that location is 0x00000015, and the thread ID is 0x000002C0. If you enabled the Debug Information option on the Compiler tab of the project options before compiling your application, the disassembly pane shows your C++ source code lines above the corresponding assembly code instructions. Some C++ source code lines can be seen in Figure 7.4. In the disassembly pane, you can step through the machine code one instruction at a time, much like you step through one source code line at a time in the source code editor. The green arrow shows the current instruction that is about to be executed. You can set breakpoints and use other features similar to debugging in the source code editor. There are several options
09 9721 CH07
11/13/00
9:41 AM
Page 411
Debugging Your Application CHAPTER 7
411
available in the context menu, such as changing threads, searching through memory for data, and changing the current execution point. The CPU registers pane is to the right of the disassembly pane. It shows the current value of each of the CPU registers. When a register changes, it is shown in red. You can modify the value of the registers via the context menu. On the far right is the CPU flags pane. This is an expanded view of the EFL (32-bit flags) register in the CPU register pane. You can toggle the value of a flag through the context menu. Consult the online help for a description of each of the flags.
The final pane is the machine stack pane, located at the bottom right of the CPU window. It displays the content of the application’s current stack, pointed to by the ESP (stack pointer) CPU register. It is similar to the memory dump pane and offers similar context menu options. The CPU view is a good tool for getting to know how your application works at a very low level. If you come to understand your application at this level, then you will have a better understanding of pointers and arrays, you’ll know more about execution speed (helpful when optimizing your application), and you’ll find it easier to debug your application because you will know what’s going on in the background. The best reference for the x86 machine code instruction set and detailed information on the Pentium processor range is the Intel Architecture Software Developer’s Manual. This threevolume set tells you just about everything you want to know about the Pentium processors. It is available for download from the Manuals section for the appropriate processor on Intel’s Web site at http://developer.intel.com/design/processor/. More references and detail on assembly language programming can be found in Chapter 6. Assembly language programming is a black art these days. It is extremely complex and is usually reserved only for writing small sections of very efficient, speed-critical code.
The Call Stack View A call stack is the path of functions that lead directly to the current point of execution. Functions that have been previously called and returned are not in the call stack. The Call Stack view displays the call stack with the most recently entered function first, at the top of the list. Used in conjunction with conditional breakpoints, the Call Stack view provides useful information as to how the function containing the breakpoint was reached. This is particularly useful if the function is called from many places throughout the application.
7 DEBUGGING YOUR APPLICATION
Below the disassembly pane is the memory dump pane. It can be used to display the content of any memory location in your application’s address space. On the left is the memory address, followed by a hexadecimal dump of the memory at that address and an ASCII view of the memory. You can change how this data is displayed from the context menu and also go to a specified address or search for particular data.
09 9721 CH07
11/13/00
412
9:41 AM
Page 412
C++Builder 5 Essentials PART I
You can double-click a function listed in the Call Stack view to display it in the Code Editor. If there is no source code for the function—for example, if the function is located in an external module—then the disassembled machine code of the function is displayed in the CPU view. In either case, the next statement or instruction to be executed at that level in the call stack is selected. A new feature in C++Builder 5 is the capability to bring up the Local Variables view for a particular function on the call stack by selecting View Locals from the context menu.
The Threads View Debugging multiprocess and multithreaded applications can be very difficult. Threads in particular usually execute asynchronously. Often the threads in the application communicate with each other using the Win32 API PostThreadMessage() function or use a mutex object to gain access to a shared resource. When debugging a multithreaded application, you can pause an individual thread. One thread may hit a breakpoint and another may not. The problems start occurring when another thread is still running and is relying on inter-thread communication, or the stopped thread has an open mutex that another thread is waiting for. Even the fact that the application runs more slowly under the debugger can cause timing problems if the application is multithreaded. In general, it is bad programming practice not to allow for reasonable timing fluctuations, because you cannot control the environment in which the application is run. The Threads view helps to alleviate some of these difficulties by giving you a snapshot of the current status of all processes and threads in the application. Each process has a main thread and may have additional threads. The Threads view displays the threads in a hierarchical way, such that all threads of a process are grouped together. The first process and the main thread are listed first. The process name and process ID are shown for each process, and the thread ID, state, status, and location are shown for each thread. Figure 7.5 shows an example of the Threads view.
FIGURE 7.5 The Threads view, showing a single process running four threads.
09 9721 CH07
11/13/00
9:41 AM
Page 413
Debugging Your Application CHAPTER 7
413
For secondary processes, the process state is Spawned, Attached, or Cross-Process Attach. The process state is Runnable, Stopped, Blocked, or None. The thread location is the current source position for the thread. If the source is not available, the current execution address is shown.
The Modules View The Modules view lists all DLLs and packages that have been loaded with the currently running application or modules that have a module load breakpoint set when the application is not running. It is very useful when debugging DLLs and packages, as discussed in the “Advanced Breakpoints” section, earlier in this chapter. Figure 7.6 shows a typical Modules view.
FIGURE 7.6 The Modules view, listing modules, source files, and entry points.
The Modules view has three panes. The upper-left pane contains the list of modules, their base addresses, and the full paths to their locations. Note that the base address is the address at which the module was actually loaded, not necessarily the base address specified on the Linker tab of the project options when developing the module. By selecting a module, you can set a module load breakpoint from the context menu.
7 DEBUGGING YOUR APPLICATION
When debugging multiprocess or multithreaded applications, there is always a single current thread. The process that the thread belongs to is the current process. The current process and current thread are denoted in the Threads view by a green arrow, which can be seen in Figure 7.5. Most debugger views and actions relate to the current thread. The current process and current thread can be changed by selecting a process or thread in the Threads view and selecting Make Current from the context menu, from which you can also terminate a process. For information on additional settings and commands in the Threads view, see “Thread status box” in the Index of the C++Builder online help.
09 9721 CH07
11/13/00
414
9:41 AM
Page 414
C++Builder 5 Essentials PART I
The lower-left pane contains a tree view of the source files that were used to build the module. You can select a source file and view it in the Code Editor by selecting View Source from the context menu. The right pane lists the entry points for the module and the addresses of the entry points. From the context menu, you can go to the entry point. If there is source available for the entry point, it will be displayed in the Code Editor. If there is no source, the entry point will be displayed in the CPU view.
The New FPU View A new view in C++Builder 5 is the Floating-Point Unit (FPU) view. It enables you to view the state of the floating-point unit or MMX information when debugging your application. Figure 7.7 shows an example FPU view displaying the state of the floating-point unit.
FIGURE 7.7 The new FPU view in C++Builder 5, displaying the state of the floating-point unit.
The FPU view has three panes. The left pane displays the floating-point register stack (ST0 to ST7 registers), the control word, status word, and tag words of the FPU. For the floating-point register stack, the register status and value are shown. The status is either Empty, Zero, Valid, or Spec. (special), depending on the register stack contents. When a stack register’s status is not Empty, its value is also displayed. You can toggle the formatting of the value from long double to words by selecting the appropriate option under Display As in the context menu. You can also zero, empty, or set a value for a particular stack register and zero or set a value for the control word, status word and tag word from the context menu. The middle pane contains the FPU single and multi-bit control flags, which change as floatingpoint instructions are executed. Their values can be toggled or cycled via the context menu. The right pane contains the FPU status flags. It is an expanded view of the status word in the FPU registers pane, listing each status flag individually. Their values can be toggled or cycled via the context menu.
09 9721 CH07
11/13/00
9:41 AM
Page 415
Debugging Your Application CHAPTER 7
415
When a value changes, it is displayed in red in all panes. You can see this effect best by performing single steps through floating-point instructions in the CPU view.
Watches, Evaluating, and Modifying
You can add expressions to the watch list using one of three methods. The first method is from the Add Watch item of the context menu in the Watches view. The second method is by selecting Run, Add Watch, and the third method is by right-clicking on the appropriate expression in the Code Editor and selecting Debug, Add Watch at Cursor from the context menu. This last method automatically enters the expression for you. Watches can be edited, disabled, or enabled via the context menu. Watches can be deleted by selecting the appropriate expression and pressing the Delete key or by selecting Delete Watch from the context menu. If the expression cannot be evaluated because one or more of the parts of the expression is not in scope, then an undefined symbol message appears instead of the evaluated result. On the other hand, evaluating and modifying expressions allows you to more readily change the expression, view the subsequent result, and modify variables at runtime. With Evaluate/Modify, you can perform detailed live testing that is difficult to perform by other means. To use Evaluate/Modify, your application must be paused. There are two ways to use it. One is to simply select Run, Evaluate/Modify and enter the expression to evaluate. Perhaps the easiest method is to invoke Evaluate/Modify from the Code Editor. When the application is paused in the debugger, you can evaluate expressions in the source code simply by placing the mouse pointer over them. Evaluate/Modify allows you to change the expression at will. You can invoke it by right-clicking on the expression and selecting Debug, Evaluate/Modify. In the Evaluate/Modify window, you will see the expression and its result. The Modify field allows you to change the expression value if it is a simple data type. If you need to modify a structure or an array, you will have to modify each field or item individually. Function calls can be included in the expression. Be aware, though, that evaluating an expression produces the same result as if your application executed that expression. If the expression contains side effects, they will be reflected in the running state of your application when you continue to step through or run.
7 DEBUGGING YOUR APPLICATION
A watch is simply a means of viewing the content of an expression throughout the debugging process. An expression can be a simple variable name or a complex expression involving pointers, arrays, functions, values, and variables. The Watches view displays the expressions and their results in the watch list. You can display the Watches view by selecting View, Debug Windows, Watches. The Watches view is automatically displayed when a new watch expression is added.
09 9721 CH07
11/13/00
416
9:41 AM
Page 416
C++Builder 5 Essentials PART I
Unlike the Watches view, the Evaluate/Modify dialog box doesn’t update the result of the expression automatically when you step through your code. You must click the Evaluate button to see the current result. The expression result can also be formatted using a format modifier at the end of the expression. See the online help for more information. Typical uses for Evaluate/Modify include testing error conditions and tracking down bugs. To test an error condition, simply set a breakpoint at or just before the error check or step through the code to reach it and then force an error by setting the appropriate error value using Modify. Use Single Step or Run to verify that the error is handled correctly. If you suspect that a certain section of code contains a bug and sets incorrect data, set a breakpoint just after the suspected code, fix the data manually, and then continue execution to verify that the bad data is producing the bug’s symptoms. Trace backward through code until you locate the bug, or use a data breakpoint to find when the data is modified.
The Debug Inspector The Debug Inspector is like a runtime object inspector. It can be used to display the data, methods, and properties of classes, structures, arrays, functions, and simple data types at runtime, thus providing a convenient all-in-one watch/modifier. With the application paused in the debugger, you can start the Debug Inspector by selecting Run, Inspect and entering the element to inspect as an expression, or by right-clicking on an element expression in the Code Editor and selecting Debug, Inspect from the context menu. The element expression in the second method is automatically entered into the inspector. Figure 7.8 shows several data members of a form class in the Debug Inspector.
FIGURE 7.8 A form displayed in the Debug Inspector window.
09 9721 CH07
11/13/00
9:41 AM
Page 417
Debugging Your Application CHAPTER 7
417
The title of the Debug Inspector window contains the thread ID. In the top of the window are the name, type, and memory address of the element. There are up to three tabs, depending on the element type, that display the name and contents or address of each data member, method, or property. The Property tab is shown only for classes derived from the VCL. At the bottom of the window, the type of the currently selected item is shown. The values of simple types can be modified. If the item can be modified, an ellipsis will be shown in the value cell. Click on the ellipsis and enter the new value.
In C++Builder 5, there are three new Debug Inspector options that can be set from Tools, Debugger Options, in addition to the original Inspectors Stay On Top option. They are Show Inherited, Sort By Name, and Show Fully Qualified Names. Show Inherited switches the view in the Data, Methods, and Properties tabs between two modes, one that shows all intrinsic and inherited data members or properties of a class and one that shows only those declared in the class. Sort By Name switches between sorting the items listed by name or by declaration order. Show Fully Qualified Names shows inherited members using their fully qualified names and is displayed only if Show Inherited is also enabled. All three new options can be set via the context menu in the Debug Inspector. The Debug Inspector is a commonly used tool during a debugging session because it displays so many items at once. It also allows you to walk down and up the class and data hierarchy.
CodeGuard CodeGuard is new to C++Builder 5 Professional and Enterprise versions. It was previously seen in Borland C++ and has only now appeared in C++Builder. CodeGuard is a runtime checker for memory and resource use and function call validation. Specifically, CodeGuard can detect the following types of runtime errors: • Improper memory deallocation • Invalid file streams or handles • Invalid pointers • Use of memory that has been deallocated • Memory leaks • Allocated memory that is not deallocated • Incorrect arguments passed to Borland and Win32 API functions
7 DEBUGGING YOUR APPLICATION
The Debug Inspector can be used to walk down and back up the class and data hierarchy. To inspect one of the data members, methods, or properties in the current inspector window, simply select it and then choose Inspect from the context menu. You can also hide or show inherited items.
09 9721 CH07
11/13/00
418
9:41 AM
Page 418
C++Builder 5 Essentials PART I
• Borland and Win32 API functions that return an error value • Invalid resource handles passed in Borland and Win32 API functions An example of improper memory use is if your application attempts to free the same resource more than once or attempts to access memory that has already been freed. We’ll look at many of the bugs that cause these errors later in this section. CodeGuard outputs the errors that it finds in a log file that can be viewed within the C++Builder IDE. CodeGuard also can take you directly to the line of source code that caused the error.
Enabling and Configuring CodeGuard To enable CodeGuard, you must compile it into your application. To do this, enable the CodeGuard Validation option on the CodeGuard tab of the project options for your application. To allow source line number identification for errors, you should also enable Debug Information and Line Number Information on the Compiler tab. Recompile the application with Build or Build All Projects. Three other options are provided on the CodeGuard tab of the project options, as shown in Figure 7.9. The first allows CodeGuard to detect invalid pointers to local, global, and static variables and data overruns. The second allows CodeGuard to detect calls to methods of invalid or deleted objects. The third option allows CodeGuard to validate inline pointer accesses and will slow down program execution speed considerably. You will usually want to enable all three options. If you change any of these three options, you will need to recompile your application with Build or Build All Projects.
FIGURE 7.9 Enabling CodeGuard project options.
There are several CodeGuard options that can be configured via the CodeGuard configuration tool, accessible from Tools, CodeGuard Configuration, or the command-line tool CGCONFIG.EXE. The configuration options are grouped into four tabs. The Preferences tab, shown in Figure 7.10, is used to set general CodeGuard preferences. The Enable option allows you to enable and disable CodeGuard without recompiling your application. It should be checked to turn on runtime error checking with CodeGuard. To fully disable CodeGuard, you should uncheck the CodeGuard Validation option on the CodeGuard tab of the
09 9721 CH07
11/13/00
9:41 AM
Page 419
Debugging Your Application CHAPTER 7
419
project options and recompile your application with Build or Build All Projects. The stack fill frequency is a compromise between better error detection and execution speed for invalid access of the runtime stack. You should leave this at 2.
7 DEBUGGING YOUR APPLICATION
FIGURE 7.10 The Preferences page of the CodeGuard Configuration tool.
The CodeGuard Report and Error Message Box options define how CodeGuard reports errors. Under Report, the Statistics option will tell CodeGuard to output statistics on memory allocation and deallocation, selected Win32 API function calls, and resource usage, plus a module list at the end of the log file. The Resource Leaks option will report resource leaks detected, after the application terminates. Error Message Box will display a message box in applications run outside the IDE if CodeGuard detects an error. For information on other options on the Preferences tab, consult the C++Builder online help. The Resource Options and Function Options tabs of the CodeGuard configuration allow you to set various tracking options for resources, file handles, and function calls. You should use the default settings unless you have a specific need to change them. One useful option on the Function Options tab is to log each call to specific functions. The Ignored Modules tab allows you to tell CodeGuard to ignore specific modules (DLLs and packages) when detecting runtime errors. Use this option only if you have a specific need.
Using CodeGuard Using CodeGuard is as simple as enabling and configuring it as detailed in the previous section and then running your application. CodeGuard will monitor the enabled and configured aspects of your application as it runs within or external to the C++Builder IDE. It will also report any
09 9721 CH07
11/13/00
420
9:41 AM
Page 420
C++Builder 5 Essentials PART I
errors to the CodeGuard log file with the name .cgl. The CodeGuard log file is a text file that can be viewed and edited using any standard text editor; however, using the C++Builder IDE is the best way to access it. The CodeGuard log can be viewed within the IDE by selecting View, Debug Windows, CodeGuard Log or by pressing Ctrl+Alt+O. C++Builder interprets the information in the CodeGuard log file and displays it in a more user-friendly way, with each error listed in a tree view. Each error can be expanded to list information specific to the error type. This information includes details such as where a resource was used, allocated, and freed; the call stack at the time of the error; and pointers to the source line(s) in the code that caused the error. Figure 7.11 shows an example CodeGuard Log window and information for an Access In Freed Memory error.
FIGURE 7.11 A CodeGuard Log window and freed memory access information.
There are two buttons on the CodeGuard Log window: Stop and Clear. When the Stop button is active, CodeGuard will stop the application when an error is encountered. If it is inactive, execution will continue and further errors can be logged in the same run. When the Clear button is enabled, the CodeGuard log is cleared each time the application is run. Individual information nodes for the errors in the CodeGuard log can be double-clicked to jump to the source line in question if source is available, or the memory address in the CPU view if source is not available. You can also select View Source from the context menu. In Figure 7.11, double-clicking the selected Attempt to Access 4 Byte(s) line would jump to the source line where the freed memory was accessed. You can jump to the source line of error information that lists a call stack by double-clicking on the top function in the call stack.
09 9721 CH07
11/13/00
9:41 AM
Page 421
Debugging Your Application CHAPTER 7
421
Once CodeGuard has detected the error and you have jumped to the appropriate source line, it is only a matter of correcting the problem by using the correct memory/resource function or tracing the resource or function arguments forward or backward until the source of the problem is located. To simplify this process, you should use watches and data breakpoints.
Examining CodeGuard Errors and Their Causes CodeGuard can detect many runtime errors. Here we will briefly see what most of the CodeGuard error messages mean and see an example of how they are caused. Usually the CodeGuard error is self-explanatory, easy to find in the source code, and easy to correct.
For most errors, CodeGuard will report the call stack and list the applicable function at the top of the list, where the error occurred, and where the resource was allocated and deallocated if applicable. If a resource was involved, the number of bytes allocated and accessed is also reported where applicable. Access In Freed Memory An Access In Freed Memory error occurs when memory is accessed after it has been freed. Typically, the memory has been allocated with new or malloc and deallocated with delete or free. The following code shows an example of accessing freed memory: #include <stdio.h> #include class TSomeClass { int FNumber; public: int GetNumber() { return FNumber; } void SetNumber(int NewNumber) { FNumber = NewNumber; } int Double(int Val) { return Val*2; } int PubVal; }; void MyFunc() { TSomeClass *MyClass = new TSomeClass; delete MyClass; MyClass->PubVal = 10; // MyClass already freed. }
7 DEBUGGING YOUR APPLICATION
All of the following CodeGuard errors are demonstrated in an example application that is available in the CodeGuard folder on the CD-ROM that accompanies this book. The application is a simple form with buttons that cause the different runtime errors to occur. To use the application best, show the CodeGuard log before running the application and disable the Stop button.
09 9721 CH07
11/13/00
422
9:41 AM
Page 422
C++Builder 5 Essentials PART I
CodeGuard will report where the freed memory was accessed, where the memory was originally allocated, and where the memory was freed. All of the following CodeGuard errors replace the MyFunc() function in the previous example code to demonstrate other errors. Method Called On Freed Object The Method Called On Freed Object error is similar to the Access In Freed Memory error but is caused by a call to a method in a freed object rather than accessing memory as such. void MyFunc() { TSomeClass *MyClass = new TSomeClass; int Answer; delete MyClass; Answer = MyClass->Double(5); }
CodeGuard will report where the method of the freed object was called, where the object was created, and where the object was freed. Reference To Freed Resource You will see the Reference To Freed Resource error if you attempt to free a resource twice. There are several ways to create this error; the following code demonstrates one, and you can find another in the ReferenceButtonClick() function in the example application on the CD-ROM. void MyFunc() { TSomeClass *MyClass = new TSomeClass[10]; delete[] MyClass; delete[] MyClass; }
CodeGuard will report where the resource was freed the second time, causing the error. It will also report where the resource was allocated and where the resource was freed the first time. Method Called On Illegally Casted Object A call to a method outside the valid memory range will cause the Method Called On Illegally Casted Object error. The following example creates an array of two TSomeClass objects but attempts to call the method of the third object. Remember that arrays are zero based. void MyFunc() { TSomeClass *MyClass = new TSomeClass[2];
09 9721 CH07
11/13/00
9:41 AM
Page 423
Debugging Your Application CHAPTER 7
423
int Answer; Answer = MyClass[2].Double(5); // No such MyClass[2] delete[] MyClass; }
CodeGuard will report where the method of the bad object call is defined, where it was called, and where the object (memory) was allocated.
void MyFunc() { TSomeClass *MyClass2 = new TSomeClass; delete[] MyClass2; TSomeClass *MyClass3 = new TSomeClass[2]; delete MyClass3; }
CodeGuard will report where the resource was freed in an inconsistent manner and where the resource was originally allocated. Access Overrun An Access Overrun error occurs by accessing memory after the end of a region of memory, such as indexing the third array item in a list of two items or copying data past the end of a region of memory. Both of these are shown in the following example code. void MyFunc() { TSomeClass *MyClass = new TSomeClass[2]; MyClass[2].PubVal = 10; // No such MyClass[2] delete[] MyClass; char *CharList = new char[10]; strcpy(CharList, “1234567890”); // Trailing NULL overrun. delete[] CharList; }
CodeGuard will report where the overrun occurred and where the resource was originally allocated.
7 DEBUGGING YOUR APPLICATION
Resource Type Mismatch The Resource Type Mismatch error is caused by freeing a resource in a manner not consistent with how the resource was allocated, such as using free on memory allocated with new. The memory should be freed with delete instead. There are several ways in which this error can occur. Two are listed here, and there are more in the source code on the CD.
09 9721 CH07
11/13/00
424
9:41 AM
Page 424
C++Builder 5 Essentials PART I
Access Underrun An Access Underrun error is similar to an Access access is before the region of memory, not after it.
Overrun,
except that the invalid memory
void MyFunc() { int *IntList = new int[2]; IntList[-1] = 10; delete[] IntList; }
CodeGuard will report where the underrun occurred and where the resource was originally allocated. Access In Uninitialized Stack Accessing an uninitialized area of the stack will cause this error. In the following example code, a pointer to a local variable on the stack is returned from a function. When the function returns, that part of the stack is no longer valid, and an error occurs when it is accessed. void LocFunc(int **LocPtr) { int LocalVar; *LocPtr = &LocalVar; } void MyFunc() { int *LocPtr; LocFunc(&LocPtr); *LocPtr = 10; }
CodeGuard will report where the uninitialized stack access occurred. Access In Invalid Stack An Access In Invalid Stack error occurs when an attempt is made to access memory below the bottom of the stack. In the following example code, access is attempted below the Name array stored on the bottom of the stack. It is different from an Access Underrun error in that the access is in the stack and in an allocated memory area. void MyFunc() { char Name[20]; strcpy(&Name[-1], “Someone”); }
CodeGuard will report where the invalid stack access occurred.
09 9721 CH07
11/13/00
9:41 AM
Page 425
Debugging Your Application CHAPTER 7
425
Bad Parameter Bad Parameter errors usually occur when an invalid file or other resource handle is passed to one of the standard Borland or Win32 API functions. In the following example code, the file stream handle is invalid when fclose is called, because the file has not been opened. void MyFunc() { FILE *Stream; fclose(Stream); }
Function Failure CodeGuard monitors the return value of many of the standard Borland and Win32 API functions. A return value of -1 indicates that an error occurred when calling the function and will be logged by CodeGuard. The following example code calls the chdir() function with an invalid directory name, causing the -1 error value to be returned. void MyFunc() { chdir(“Z:\ZXCVBN”); }
CodeGuard reports where the function that returned the error value was called. Resource Leak A Resource Leak error occurs when a resource such as memory is allocated but not deallocated. For most of the objects in the VCL, the deallocation is handled automatically, so resource leak errors will most often occur when you allocate a custom area of memory or dynamically create a custom object. The second case is shown in the following example code: An object is created but not freed. void MyFunc() { TSomeClass *MyClass = new TSomeClass; }
CodeGuard will report where the resource was created. There are additional CodeGuard errors not covered here. Those described are the most common that you will encounter. For additional errors and examples, see CodeGuard in the C++Builder online help.
7 DEBUGGING YOUR APPLICATION
CodeGuard reports where the function was called with the bad parameter.
09 9721 CH07
11/13/00
426
9:41 AM
Page 426
C++Builder 5 Essentials PART I
Advanced Debugging As mentioned previously, debugging is an advanced topic in itself. However, there are several specific issues and cases that are beyond the basic debugging techniques presented in the first section of this chapter. For any serious application development and debugging, I thoroughly recommend that you use the Windows NT (WinNT) or Windows 2000 (Win2K, which is based on Windows NT) operating systems, and not the Windows 95 or Windows 98 (Win9x) operating systems. WinNT and Win2K provide a much more stable environment, particularly with buggy applications. WinNT-based operating systems handle application stopping and crashes much better than Win9x. On a Win9x system, it is much easier to crash C++Builder or even the whole system when debugging or stopping an application midstream. Use Run, Program Reset sparingly, and stop the application through normal user means if possible. When your application performs an illegal operation or access violation while running within the C++Builder IDE, an error occurs and you are presented with an error dialog box. On a Win9x system you should reset your application using Run, Program Reset before closing the dialog box. This usually recovers more reliably than when closing the dialog box first. For really serious debugging, particularly of Windows system applications, you can obtain a debug version of the Windows operating system, called a “debug binary” for Win9x and “checked/debug build” for WinNT/2K. The checked build provides error checking, argument verification, and system debugging code for the Windows operating system code and Win32 API functions, mostly in the form of assertions, that are not present in the retail version. This checking imposes a performance penalty. Check builds of Windows operating systems are provided with some Microsoft Developer Network (MSDN) subscriptions. Sometimes it is useful to know if your application is running in the context of the debugger. The Win32 API function IsDebuggerPresent() returns true if it is. You can use this fact to alter the behavior of the application at runtime, such as to output additional debug information to make the application easier to debug. Now let’s look at several advanced debugging issues.
Locating the Source of Access Violations Earlier in this chapter we examined some basic techniques for locating bugs. Access violations (AVs) are sometimes more difficult to locate than general program bugs. There are other application errors that are similar to AVs, and the techniques described here apply to those also. Access violations can be caused by access to memory that is not within the application’s memory space. If at all possible, you should use CodeGuard to check your application at runtime.
09 9721 CH07
11/13/00
9:41 AM
Page 427
Debugging Your Application CHAPTER 7
427
CodeGuard can detect many errors that would normally result in an AV and pinpoint the exact line of source code that produced the error. If you can’t use CodeGuard for your application or CodeGuard does not detect the error that caused the AV, there are other things you can do to track down the error. When an AV occurs, a dialog box is presented with the message Access violation at errors can present a different message, such as The instruction at 0xYYYYYYYY referenced memory at 0xZZZZZZZZ. In these cases, the YYYYYYYY address is the machine code that caused the error, and address ZZZZZZZZ is the invalid memory address that it attempted to access.
address YYYYYYYY. Read of address ZZZZZZZZ. Application
If you can’t reproduce the AV when running within C++Builder, simply pause your application using Run, Pause or by setting and hitting a breakpoint, then bring up the CPU view and select Goto Address from the context menu. This is not foolproof, but it often works. Enter the code address given in the AV dialog box in hexadecimal as 0xYYYYYYYY. The code around this address may give you some clue as to where in your source code the AV occurred, particularly if the application was compiled with debug information. When the memory address ZZZZZZZZ is close to zero, for instance 00000089, the cause is often an uninitialized pointer that has been accessed. The following code would produce an AV with this memory address because the MyButton object was never created with new. TButton *MyButton; MyButton->Height = 10;
What is actually happening is that when MyButton is declared it is initialized with a value of zero. The address 00000089 is actually the address of the Height property within the TButton object if it were located at address zero. As a general rule, you should explicitly initialize pointers to some recognizable value before the memory or object is allocated and back to that value once it has been freed. If you get an AV that lists this value, then you know an uninitialized pointer caused it. Sometimes an AV can occur in a multithreaded application in which concurrent access to objects and data is not controlled. These can be very difficult to find. Use data breakpoints and the outputting debug information techniques described earlier in this chapter if you suspect concurrency problems.
7 DEBUGGING YOUR APPLICATION
It is possible to locate where some access violations occurred by implementing a global exception handler. This was discussed earlier in this chapter. You can also use just-in-time (JIT) debugging, described later in this chapter, to bring the process into the debugger and go to the address YYYYYYYY. Alternatively, you can run your application within C++Builder and wait for the AV to occur.
09 9721 CH07
11/13/00
428
9:41 AM
Page 428
C++Builder 5 Essentials PART I
Attaching to a Running Process When a process is running outside the C++Builder IDE, you can still debug it using the integrated debugger by attaching to it while it is running. This feature can be handy during testing. When you detect the occurrence of a bug in the application, you can attach to the application process and track down the bug. The only drawback is that Windows does not provide a method for detaching from the process without terminating it. To attach to a running process, select Run, Attach to Process. The Attach To Process window is displayed with a list of running processes on the local machine. Select the appropriate process from the list and click the Attach button. The C++Builder debugger will then attach to the process. The process will be paused, and the CPU view will be displayed at the current execution point. You can step through the code, set breakpoints, load the source code if available using View Source from the context menu, inspect values, and so on. Attach To Process is even more handy with remote debugging. In the Attach To Process window, you can view and attach to processes on another machine that is running the remote debug server. This is covered in the “Using Remote Debugging” section, later in this chapter. In the window you can also view system processes by checking Show System Processes. You should be very careful about attaching to any old process; you can cause Windows to crash or hang by attaching to system processes. Stick to attaching to your own processes.
Using Just-In-Time Debugging Just-in-time (JIT) debugging is a feature of the Windows NT and Windows 2000 operating systems that allows you to debug a process at the time that it fails, such as when an access violation is caused. JIT debugging is not available on Windows 9x machines. If you’ve used Windows NT or Windows 2000 before, you’ve no doubt heard of Dr. Watson. This is a JIT debugging tool provided with Windows to help identify the cause of a program failure. The selected JIT debugging tool can be changed. The current JIT debugging tool is usually set via a Registry entry; however, new to C++Builder 5 is a feature that allows the Borland debugger launcher, BORDBG51.EXE, to be called instead of Dr. Watson. Then, with each JIT debugging instance, you can select which debugger to use from the debugger launcher, such as the C++Builder debugger, Delphi debugger, Dr. Watson, or even the Borland Turbo Debugger. Prior to C++Builder 5, the call to Dr. Watson could be replaced with a call directly to the C++Builder debugger; no debugger selection was available. If only one debugger is configured in the list, it is automatically launched. See “Just in time debuggers” in the C++Builder online help for instructions on how to configure the JIT debuggers to list in the debugger launcher.
09 9721 CH07
11/13/00
9:41 AM
Page 429
Debugging Your Application CHAPTER 7
429
Once configured, JIT debugging is easy to use. When the application crashes, Windows will run the debugger launcher. Select the appropriate debugger from the list, BCB (C++Builder) in this case, and click OK. At this point, C++Builder will start if it is not already running, and the application will be paused as if it were attached to while running. You can then use any of the techniques described earlier in this chapter to locate the source of the bug.
Remote Debugging
Remote debugging is very useful for debugging distributed applications, such as those that use DCOM or CORBA. Debugging should be performed locally whenever possible due to the reduced performance when debugging across a network. Remote debugging is supported for executables, DLLs, and packages. The application must have been compiled with debugging information, and the debugging symbol’s .tds file must be available with the application on the remote machine. The easiest way to achieve this is to load the application’s project into C++Builder on the local machine, specify the Final output path in the Directories/Conditionals tab of the project options to be the shared network folder on the remote machine where the application will run, and compile the application with debug information. Remotely debugging an application is virtually seamless. Once the remote debug session is connected, you work just as you would when debugging a local application.
Configuring Remote Debugging Remote debugging works by running the Borland debug server BORDBG51.EXE on the remote machine. You may notice that the Borland debug server is the same program as the Borland debug launcher, described previously in the “Using Remote Debugging” section. It can perform either of these functions depending on the command-line options used to start it. The debug server requires additional DLLs to be installed. The local C++Builder debugger communicates with the debug server. On remote Windows NT and Windows 2000 machines, the debug server is usually installed as a service. It will show as Borland Remote Debugging Service in the Services applet of the Control Panel. The debug server service can be started or stopped from the applet and can be set to start automatically when the system boots. Use the -install and -remove command-line options to install and remove the service.
7 DEBUGGING YOUR APPLICATION
Remote debugging is the capability to debug an application running on another machine using the C++Builder interactive debugger running on your local machine. It is beneficial for applications running on remote machines that would be inconvenient to access physically, and it does not require C++Builder to be installed on the remote machine.
09 9721 CH07
11/13/00
430
9:41 AM
Page 430
C++Builder 5 Essentials PART I
On remote Windows 9x machines, the debug server is a standalone process. This is also an option for WinNT/2K machines. In any case, the remote debug server must be running before remote debugging can commence. You can install the debug server with associated DLLs required from the C++Builder installation CD using the standard install dialog or by running SETUP.EXE in the RDEBUG folder of the CD. Remote debugging uses TCP/IP for communication between the local C++Builder IDE and the remote debug server. You must have TCP/IP networking configured correctly on both machines. To start the debug server manually, run BORDBG51.EXE or debugging rights to run the debug server.
-listen. You
will need administration
Using Remote Debugging When the debug server has been installed on the remote machine and it is already running, you can start debugging remotely. From the local C++Builder IDE, open the project for the remote application that you will be debugging. Select Run, Parameters, click the Remote tab, and set Remote Path to the remote application’s full path and application filename as you would use locally on that machine, such as C:\Temp\MyProj.exe. If you are debugging a DLL on the remote machine, enter the path and name of the remote application that will host the DLL. Enter any command-line parameters for the application in the Parameters field. Set Remote Host to the hostname or IP address of the remote machine. To start debugging immediately, or when you don’t have the application project loaded in C++Builder, just click the Load button. If you have the application project loaded, you can check Debug Project On Remote Machine and click OK. When you perform any debug command on the application within C++Builder, the debugging connection to the remote application will be established. You can then debug the application just as if it were running on the local machine. If you get the error Unable to connect to remote host, check that the debug server service or process is running, Remote Host is set correctly, and you have connectivity to the remote host using ping.exe or another network tool. If you get the error Could not find program ‘program’, then check that Remote Path is correct and that the application is actually located there. Another feature of remote debugging is an extension of Attach To Running Process. Select Run, Attach To Process, enter the name of the remote machine in the Remote Machine field, and press Enter. The processes on the remote machine are listed; select one and click Attach to debug it. To use remote process attachment, the remote machine must be running the debug server. Remember that when attaching to a running process, there is no way to detach without terminating it.
09 9721 CH07
11/13/00
9:41 AM
Page 431
Debugging Your Application CHAPTER 7
431
Debugging DLLs Debugging a DLL is very similar to debugging any normal executable application except that a host application is required to load it. You can create the host application that uses the DLL, but in most cases you will be using an existing host, such as an application written in another language that uses the DLL that you have developed.
When the host application is specified, either select Load to run the host application and begin debugging or simply press OK and run the host application at a later time with Run, Run. You might do this after setting additional breakpoints or setting up watches, for example. That’s all there is to it. When the breakpoint in the DLL code is hit, you can step through the source code and use the Debug Inspector, watches, or any other technique during the debug process. You can use this technique for debugging COM objects and ActiveX components, though for separate processes you can do this only on Windows NT and Windows 2000 systems, which allow cross-process debugging.
Looking at Other Debugging Tools A few other debugging tools are worth mentioning here to round out your toolset. As stated earlier in this chapter, bugs are often introduced when changes are made to the code. I recommend that you use a version control system or some type of basic versioning, even if it means copying the project folder periodically. Most version control systems have a built-in diff function to compare two versions of the source code, which can be a great help in tracking down new bugs. There are freeware diff utilities also available. At one stage, the littleknown windiff.exe shipped on some versions of the Windows installation CD. Visual Diff is a free tool from Starbase that can be downloaded from http://www.starbase.com. TeamSource is a concurrent version control system frontend that is provided with C++Builder 5 Enterprise and can be purchased as an add-on to the Professional edition. With it you can compare two revisions of the same source file. TeamSource is covered in detail in Chapter 29, “Software Installation and Updates.” CodeSite from Raize Software, Inc. (http://www.raize.com/), is a fairly inexpensive but advanced debug output tool. It allows you to send information from your application to a central viewer that provides tools to help you analyze it. All kinds of data can be sent, including complete objects. Each output message is time stamped; in itself this is useful, but it can also
7 DEBUGGING YOUR APPLICATION
Load the DLL project into C++Builder and set any breakpoints in the DLL source code as necessary. Specify the host application that will load the DLL by entering the full path and name of the host application in the Host Application field on the Local tab from the Run, Parameters dialog. Enter any command-line parameters for the application in the Parameters field if necessary.
09 9721 CH07
11/13/00
432
9:41 AM
Page 432
C++Builder 5 Essentials PART I
be used for basic code timing. Overseer Debugger is similar in function to CodeSite. It is free and can be obtained from http://delphree.clexpert.com/pages/projects/nexus/ overseer_debugger.htm. GExperts includes the dbugint debugging interface to log debug output in a central place for later review using the Gdebug.exe tool. Your application can log different message types and has some extra control over the debug information logged. GExperts is free and is provided on the Companion Tools CD-ROM with C++Builder Professional and Enterprise. It can be downloaded from http://www.gexperts.org/. ClassExplorer Pro from ToolsFactory (http://www.toolsfactory.com) is an integrated add-on for C++Builder and Delphi to assist with development and debugging. It greatly enhances class and file navigation and includes a context bar that displays the class and method of the current cursor position. It also provides a fully featured class designer including code generation templates and can create help file documentation of your classes and more. It is available free for non-commercial use. Sleuth QA Suite from TurboPower Software Company (http://www.turbopower.com) contains two products: a runtime resource and API checking program called Sleuth CodeWatch, which is similar to CodeGuard, and a profiler called Sleuth StopWatch. Both are designed specifically for use with C++Builder and Delphi. CodeWatch can detect and filter out known VCL resource leaks to allow you to concentrate on your own bugs. Real-time graphs can alert you to slow memory leaks that occur over time. Sleuth StopWatch was briefly demonstrated in Chapter 6. MemProof, provided on the Companion Tools CD-ROM with C++Builder Professional and Enterprise and also available at http://www.totalqa.com/, is another resource leak detection program. It is freeware and integrates with C++Builder. It can also track BDE and Interbase resource allocations, contains live counters, shows API function calls, and includes SQL tracing extension functions. There are many third-party bug-tracking tools available. Here are just a few. • PR-Tracker from Softwise Company. A relatively inexpensive solution to tracking problem reports, bugs, and defects (http://www.prtracker.com/). • SWBTracker from Software With Brains. Also relatively inexpensive, this defect management software for developers is packed with features (http://www.softwarewithbrains.com/). • TestTrack from Seapine Software Inc. (http://www.seapine.com/). Doc-o-Matic from ToolsFactory is a documentation system. It generates HTML and help files from the source code comments in your application (http://www.doc-o-matic.com).
09 9721 CH07
11/13/00
9:41 AM
Page 433
Debugging Your Application CHAPTER 7
433
WinSight is provided with C++Builder 5 Professional and Enterprise. It provides class, window, and message debugging information at runtime. You can use it to see how your application creates its classes and windows and to monitor a window’s messages. OLE/COM Object Viewer, OleView.exe, is provided free in the Microsoft Platform SDK, available for download from the Microsoft Web site. Many, many more tools exist for helping software developers in the area of debugging. If you find any gems, please let me know!
Testing is an extension to debugging. It is the detection and sometimes the location of errors. The two facets of software development sometimes overlap in the location of errors, when sufficient test output data can help pinpoint where the error is. Testing is an essential part of the software development process. Without it, you’re not very likely to produce a correctly working, let alone usable, application. The level of testing needed depends on the type of application you are developing, the importance of robustness and safety, and the resources that you have in the development time available (though if you’re short on time you should cut features and not quality). Testing is a massive subject in itself and many, many books have been devoted to it. Here we’ll outline some of the general testing principles.
Testing Stages and Techniques Many different stages of testing are carried out during the overall software development process. Some of the testing stages are described in the following list. Each stage is usually broken down into several smaller repetitive stages. The test stages are usually performed in the order listed: • Unit testing A unit is a small group of related functions or an object or objects. Unit testing and unit debugging often are performed together throughout the development process. • Code reviews This is a stage at which a programmer’s code is checked by a second programmer, usually by eyeballing the source code and discussing it with the original developer. • Component testing A component is a group of units or objects or even a whole program that performs a specific set of functions. It is tested as a whole. • Integration testing This typically involves combining one or more components into a functional system and testing the interaction between those components and the rest of the system.
7 DEBUGGING YOUR APPLICATION
Testing
09 9721 CH07
11/13/00
434
9:41 AM
Page 434
C++Builder 5 Essentials PART I
• System testing This is testing the system as a whole and usually concentrates on certain aspects such as performance or robustness. • Acceptance testing This is usually just a milestone indicating that the system is ready for use by the customer by verifying that certain component and integration tests were successful. • Field testing Typically, field testing is performed by shipping alpha and beta test releases of the system to customers, who provide feedback on errors that are discovered. • Regression testing This is simply rerunning previous tests after changes are integrated with a working system to verify that the changes work correctly. They are usually performed after implementing a bug fix or adding a new feature. In addition to these stages of testing, there are different testing techniques. The main two techniques are whitebox testing, also known as glassbox or structural testing, and blackbox testing, also known as functional testing. Whitebox testing fully exposes the internals of the part of the system being tested. A number of test cases are used to check the execution path through every line of code. For each branch in the code, an additional test case is required. In some cases, changes to the source code are necessary to support the full range of whitebox testing. For small applications or specific parts of larger ones, you can use the interactive debugger. Whitebox testing is usually performed at the unit and component testing stages.
TIP When performing whitebox testing, use debug output, breakpoints, or a profiler that can report code coverage to check which parts of your code are and are not being tested in your test procedures. Formulate specific test cases to ensure that each part of the code is tested.
Blackbox testing is where the part of the system being tested is treated as a black box. This means that for testing purposes the internals of the system (how it works) are hidden; the only visible portion of the system is the interfaces. Data is fed in, and the output data is reviewed for correctness; the detail of what went on inside the box is hidden. Blackbox testing is usually performed at the component, integration, and system testing stages. With blackbox testing, a wide range of inputs should be tested, including typical valid inputs, valid inputs that are not normally expected, and various invalid inputs. At the very minimum, all boundary conditions and a few middle points should be tested thoroughly.
09 9721 CH07
11/13/00
9:41 AM
Page 435
Debugging Your Application CHAPTER 7
435
The problem with blackbox testing is that you’re not sure which parts of the code have been tested and which haven’t, so by itself blackbox testing is often not enough. A combination of test methods generally produces the best results. Both whitebox and blackbox testing methods are covered to death in textbooks dedicated to software testing.
Testing Tips
You should attempt to build tests into your application during development, not on top of it later. The best time to develop tests is when the code is fresh in your mind. If possible, develop automated tests. When developing a replacement application, perform the data conversions early. This will not only ensure that you understand the existing system better, it will allow the development schedule to be estimated more accurately. Conversion tools, particularly if some detail of the existing data or system was missed, can take some time to develop. Don’t forget to test your tests! This particularly applies to built-in and automated testing. If the original tests don’t test your application correctly or test insufficiently, then they give false hope. The following are useful testing references: • A good book that covers all aspects of software testing, applicable to programmers, testers, and project managers alike is Testing Computer Software by Kaner, Nguyen, and Falk. Publisher: John Wiley & Sons. ISBN 0-471-35846-0, 1999. • Dr. Dobb’s Journal (http://www.ddj.com/) from time to time contains articles on testing. You can purchase individual articles online, or you can buy a CD with over 11 years of issues for around US$80. In particular, the March 2000 issue contains several articles on testing and debugging. Finally, tests are only as good as the application knowledge of the person analyzing the results. Know how the application should work and then verify that it does work.
7 DEBUGGING YOUR APPLICATION
Always test your code! I don’t know how many times in the past I’ve seen untested software, either mine or someone else’s, come back to haunt the programmer. It is always better to have a smaller set of robust software than a large set of buggy software. Test your code even if the schedule will be delayed because of it, but inform your team leader or the project manager if you think that this will be so.
09 9721 CH07
11/13/00
436
9:41 AM
Page 436
C++Builder 5 Essentials PART I
Summary Debugging is a very involved process of finding and fixing bugs. The breadth of issues could fill a book. I hope that this condensed version has shed some light on the subject in areas relevant to your development. There are several new debugger enhancements in C++Builder 5 that haven’t been covered here. Consult the online help for a complete list and their functions. An excellent resource for help on locating and fixing bugs in general is the Borland newsgroups. They are listed in the “Newsgroups” section in Appendix A, “Information Sources.” A good textbook on the subject is Writing Solid Code by Steve Maguire, published by Microsoft Press. ISBN 1-556-15551-4, 1993. As your programming experience grows, try to learn and remember new debugging techniques. To save considerable development, support, and maintenance time, do everything you can to prevent them in the first place.
10 9721 CH08
11/13/00
9:47 AM
Page 437
Using VCL Components Malcolm Smith Chris Winters Damon Chandler Jarrod Hollingworth Khalid Almannai
IN THIS CHAPTER • VCL Overview • The Streaming Mechanism • Common Control Updates • Miscellaneous VCL Enhancements • Extending the VCL—More than Just a TStringList
• Control Panel Applet Wizard Components • Making Use of Third-Party Components
CHAPTER
8
10 9721 CH08
11/13/00
438
9:47 AM
Page 438
C++Builder 5 Essentials PART I
C++Builder is a Rapid Application Development (RAD) system giving you a Visual Component Library (VCL, consisting of ready-made visual and non-visual objects and wrappers). It enables you to create applications with minimal effort while at the same time giving you the option to develop independently from the VCL as required. This chapter is the start of what C++Builder is all about—object-oriented development. C++Builder uses the concept of object reuse through components. Components are instances of classes accessible from what is known as the Component Palette. Building applications couldn’t get much easier—drop a component on a form, set some properties, write an event handler or two, and you’re done. We will look at how the VCL descends from TObject and how the Object Inspector is capable of using Runtime Type Information (RTTI) to interactively provide the developer with a means to inspect and edit common properties and events shared by multiple components. A comparison between regular C++ classes and the VCL and language extensions will be made in order to show the many advantages obtained by developers using C++Builder. We’ll also take a look at standard and common control support and how, as a developer, you need to be aware of the dependencies of the COMCTL32.DLL dynamic link library (DLL) provided by Microsoft Windows. The updates provided with C++Builder are then reviewed. It’s then time to look at some other VCL enhancements in C++Builder 5 worth mentioning. These include the new Help Hints, menu and added Registry access features, the TApplicationEvents component, and TIcon. From there we take a look at an example of extending the VCL, specifically, extending the class. This section demonstrates how easy it is to build upon the alreadypowerful string list container, including the storage of standard structures and classes. The discussion then takes a brief look at the new advanced Custom Draw events of the TTreeView, TListView, and TToolBar controls and how to create Control Panel applets using C++Builder. TStringList
Finally, we will look at how existing third-party components can make application development fast and provide the foundation for further enhancing already complex API wrappers. These components are built upon C++Builder’s Visual Component Library (VCL) and the PME model (properties, methods, and events).
VCL Overview Supplied with C++Builder is a chart that schematically represents the Visual Component Library (VCL) object hierarchy. Not surprisingly, this chart has expanded with each new version of C++Builder. What you can’t see, however, is the true complexity of the VCL. The explanation is simple: The VCL is much more than objects descending from other objects.
10 9721 CH08
11/13/00
9:47 AM
Page 439
Using VCL Components CHAPTER 8
439
The VCL is based upon what is known as the PME model, including properties, methods, and events. This architecture is joined with the Component Palette, Object Inspector, and IDE, giving developers a rapid approach to building applications, known as Rapid Application Development (RAD). A developer can drop components onto a form and have a working Windows application almost without writing a single line of code. Obviously, writing code is required to make the application fully functional, but the VCL handles most of the work for you, making application development very expedient and more enjoyable. You can spend more time building the working blocks of your application rather than having to spend repetitive time with the framework of each Windows application you develop. The remainder of this topic will take a very brief look at the hierarchy of VCL objects in general.
It All Starts at TObject The VCL is fundamentally a group of objects that descend from the abstract class TObject. This class provides the capability to respond to the creation and destruction of objects, supports message handling, and contains class type and Runtime Type Information (RTTI) of its published properties.
Descending from TObject are many simple non-persistent data classes, wrappers, and streams, such as TList, TStack, TPrinter, TRegistry, and TStream, to name a few. Persistent data, in terms of the VCL, refers to the mechanism of storing property values. The simplest example is the caption of a button or label. At designtime you enter the caption in the Object Inspector. This caption is maintained between programming sessions and is available at runtime. The data is persistent. Non-persistent classes, therefore, refers to simple classes designed to perform particular functions but unable to save their state between sessions. A wrapper can be described as a means of placing an envelope around the more complex Windows API. Wrappers allow for the creation of objects or components that are easier to use and can be used across multiple projects. Components and other objects to some extent shield you from the API and at the same time provide the convenience of using its powerful features in an easy-to-use fashion. Later chapters will provide additional information on these topics.
USING VCL COMPONENTS
RTTI enables you to determine the type of an object at runtime even when the code uses a pointer or reference to that object. As an example, C++Builder passes a TObject pointer to each of its events. This might be a mouse-click event or an object obtaining focus. Through the use of RTTI it is possible to perform a cast (dynamic_cast) to either use the object or determine the object type. The RTTI mechanism also allows testing an object type by using the typeid operator. The dynamic_cast operator is demonstrated later in this chapter. The C++Builder Language Guide online help provides additional information on this topic.
8
10 9721 CH08
11/13/00
440
9:47 AM
Page 440
C++Builder 5 Essentials PART I
Another commonly used descendant of TObject is the Exception class, which provides many built-in exception classes for handling conditions such as divide-by-zero and stream errors. This class can also be used for the creation of custom classes for use in your own applications with very minimal work. The other major branches of TObject include TPersistent, TComponent, TControl, and TWinControl.
TGraphicControl,
TPersistent adds methods to TObject that allow the object to save its state prior to destruction and reload that state when it is created again. This class is important in the creation of components that contain custom classes as properties. If the property needs to be streamed, it must descend from TPersistent rather than TObject. This branch includes many types of classes, with the most common including TCanvas, TClipboard, TGraphic, TGraphicsObject, and TStrings. TPersistent descends to provide TComponent, the common base for all VCL components.
Property streaming refers to the mechanism by which the object’s property values are written to the form’s file. When the project is reopened, the property values are streamed (or read) back, thereby restoring their previous values. objects provide the foundation for building C++Builder applications. Components have the capability to appear on the Component Palette, become parents for other components, control other components, and perform streaming operations.
TComponent
There are two types of components—visual and non-visual. Non-visual components require no visual interface and are therefore derived directly from TComponent. Visual components are required to have the capability to been seen and interact with the user at runtime. TControl adds the drawing routines and window events required for defining a visual component. These visual components are divided into two groups: windowed (TWinControl) and non-windowed (TGraphicControl). components are responsible for drawing themselves but never receive focus. Examples include TImage, TLabel, TBevel, and TShape. TGraphicControl
components are similar to TGraphicControl except that they can receive focus, allowing for user interaction. These components are known as windowed controls, have a window handle, and can contain (or be the parent of) other controls.
TWinControl
For more information on visual and non-visual components, see Chapter 9, “Creating Custom Components.”
Building on Existing Objects The C++Builder object-oriented architecture means faster application development through reuse of existing objects and supporting classes. Giving the component objects the capability to
10 9721 CH08
11/13/00
9:47 AM
Page 441
Using VCL Components CHAPTER 8
441
present their published properties to the developer via the Object Inspector provides the extra dimension that further improves the development cycle. But the Object Inspector does more than simply present the component’s published properties for review or editing. In Figure 8.1 you can see the Object Inspector showing the common properties of TLabel, TEdit, TButton, and TCheckBox controls. Looking at the hierarchy of these controls, we can see that TLabel descends from TGraphicControl, and the remainder descend from TWinControl. The common ancestor for all four controls is therefore TControl (because TGraphicControl and TWinControl descend from TControl).
8 USING VCL COMPONENTS
FIGURE 8.1 Common properties of multiple selected components.
Figure 8.1 is therefore displaying all of the common properties the components have inherited from TControl. If you change one of these properties while they are all selected, this change will be reflected in all of the controls simultaneously. Figure 8.2 shows the Object Inspector demonstrating the capability to locate components that are inherited from the property type being edited. In this example, the LinkedEdit property is of type TCustomMemo, and our form has a TRichEdit and a TMemo, both of which are descendants of TCustomMemo.
10 9721 CH08
11/13/00
442
9:47 AM
Page 442
C++Builder 5 Essentials PART I
FIGURE 8.2 Recognizing descendants of a property type.
Building objects from existing classes or objects allows you to develop additional functionality into the descendants while at the same time allowing the base classes to remain available for new descendants that require a common base with some additional unique features. Some developers might argue that this additional class and the associated RTTI contained within the hierarchy are only adding to the overhead of the application. This overhead is well worth the benefits this object model provides. Each of the objects and components provided with C++Builder can appear quite differently but share common features that are inherited from ancestor objects, making the development process more streamlined.
Using the VCL It is important to understand how the VCL differs from regular classes and objects. The VCL originates from Object Pascal. As a result, all VCL objects are created on the heap and referenced as pointers rather than static objects. Only VCL objects need to be created and used in this fashion. All standard C/C++ objects can be used in either fashion. An example will illustrate this. The following code first shows how a standard C++ class can be created on the stack and then on the heap. Then it shows how VCL objects must be created. The C++ class code is as follows:
10 9721 CH08
11/13/00
9:47 AM
Page 443
Using VCL Components CHAPTER 8
443
class MyClass { private: int MyVar; public: MyClass(void); };
Creating an instance of this class on the stack is shown next. When the class goes out of scope, the memory allocated for the object is released automatically. MyClass Tmp; Tmp.MyVar = 10; // do something
Next we look at creating an instance of this class on the heap. It is the responsibility of the creator to destroy the object before it goes out of scope. Otherwise the memory is not released, resulting in a memory leak.
VCL objects are like the second example, created on the heap. If you attempt to create a VCL object on the stack, the compiler will complain VCL style classes must be constructed using operator new. C++Builder has also made provision for automatic destruction of objects that have owners. Let’s assume you create a TLabel object dynamically and pass this as the owner: TLabel *MyLabel = new TLabel(this);
When MyLabel goes out of scope, you at first assume a memory leak has occurred. Objects of this nature don’t have to worry about freeing themselves explicitly because the VCL has a built-in mechanism to free all child objects before an owner object is destroyed. The term parent object was carefully avoided. This will be explained further. Non-visual components have owners, and visual components have owners andparents. The easiest way to distinguish the two terms is to think of the owner as the creator and the parent as the container allowing the component to exist. Suppose you have a TPanel component on a form, and on this component are three label components. There are two possible scenarios, depending on who created the labels. In the first scenario we have the labels being dropped onto the panel at designtime. In this case, the panel is the parent for each of the labels, but the application is the owner. When the
8 USING VCL COMPONENTS
MyClass *Tmp; Tmp = new MyClass; Tmp->MyVar = 10; // do something delete Tmp;
10 9721 CH08
11/13/00
444
9:47 AM
Page 444
C++Builder 5 Essentials PART I
application is closed, the VCL ensures that each of the child objects (the panel and the three labels) are all destroyed before the application itself is destroyed. In another case you might drop an aggregate component (a component made up of many components) onto a form. For this example we assume it is a panel with three labels on it. The component creates the panel and then creates three labels to sit on that panel. The panel is still the parent of the labels, but now the owner of the panel and the three labels is the component that was dropped onto the form. If the component is destroyed, then the panel and labels also will be automatically destroyed. This is a great feature for designtime objects, but if you create objects at runtime, you should delete them explicitly. You won’t have any memory leaks if you don’t, but it makes the code more readable and documents the intention of the code. The following code demonstrates creating these objects at runtime. TPanel *Panel1; TLabel *Label1, *Label2, *Label3; Panel1 = new TPanel(this); Panel1->Parent = Form1; Label1 = new TLabel(this); Label1->Parent = Panel1; Label2 = new TLabel(this); Label2->Parent = Panel1; Label3 = new TLabel(this); Label3->Parent = Panel1; // set other properties such as caption, position, etc // do something delete Label1; delete Label2; delete Label3; delete Panel1;
The panel is created with the application as the owner and the form as the parent. The labels are then created similarly, with the difference being that the panel is made the parent of the labels. You could move the labels to another panel just by changing the Parent property. It is possible to delete the panel so that the labels would be deleted automatically, but I prefer to delete everything explicitly. This becomes more important when dealing with real-world applications in which global pointers are shared. It is also good practice to set the pointers to NULL (or zero) after they have been deleted (unless they are about to go out of scope). If another part of your application tries to delete the object after it has been destroyed and you haven’t nulled the pointer, you will be certain to cause an access violation.
10 9721 CH08
11/13/00
9:47 AM
Page 445
Using VCL Components CHAPTER 8
445
The C++ Extensions C++Builder has added extensions to the C++ language to make it a powerful product capable of utilizing the VCL and fit seamlessly into the PME model. As a programmer, I’m not concerned with the controversial issues of C++ extensions, so long as they make the product powerful, maintain compatibility with ANSI standards, and provide for a shorter development cycle. In all cases, C++Builder manages this very well. The extensions are listed below with a brief description. Please note that each of the extensions is prefixed with two underscores.
The __automated Class Extension Object linking and embedding (OLE) does not provide type information in a type library. The automated section of a class that is derived from TAutoObject or a descendant of TAutoObject is used by applications supporting OLE automation. The required OLE automation information is generated for member functions and properties declared in this section. class TMyAutoClass : public TAutoObject { public: virtual __fastcall TMyAutoClass(void);
The last point to make is that the declared method is of type __fastcall (described a little later). This is required for automated functions and causes the compiler to attempt to pass the parameters in the registers rather than on the stack.
The __classid(class) Class Extension The __classid extension is used to bridge the gap between the VCL RTTI functions and the C++ language. It is used when an RTTI function requires class information as a parameter. As an example, the RegisterComponentEditor() method allows you to create and register a custom editor for your components. My business produces a security component package (MJFSecurity) specifically for C++Builder. This package includes a number of custom components designed for protecting applications from piracy. One of these components is called TAppLock, and it uses a custom editor called TAppLockEditor. As part of the Register() method of the package, the editor is registered using the following code: namespace Applock { void __fastcall PACKAGE Register()
USING VCL COMPONENTS
_ _automated: AnsiString __fastcall GetClassName(void) { return(“MyAutoClass”); } };
8
10 9721 CH08
11/13/00
446
9:47 AM
Page 446
C++Builder 5 Essentials PART I { // ... code omitted from here RegisterComponentEditor(__classid(TAppLock), __classid(TAppLockEditor)); } }
The RegisterComponentEditor() method takes two parameters as pointers to TMetaClass (C++Builder’s representation of the Object Pascal class-reference type). The TMetaClass for a given class can be acquired by using the __classid operator. The compiler uses the __classid operator to generate a pointer to the vtable (virtual table) for the given classname. For further information on MJFSecurity, go to http://www.mjfreelancing.com/. Additionally, Chapter 28, “Software Distribution,” discusses AppLock and covers other areas of security.
The __closure Class Extension Standard C++ allows you to assign a derived class instance to a base class pointer but does not allow you to assign a derived class’s member function to a base class member function pointer. Listing 8.1 demonstrates this problem. LISTING 8.1
Illegal Assignment of a Derived Class Member Function
enum HandTypes {htHour, htMinute, htSecond}; class TWatch { public: void MoveHand(HandTypes HandType); }; class TWatchBrand : public TWatch { public: void NewFeature(bool Start); }; void (TWatch::*Wptr)(bool); Wptr = &TWatchBrand::NewFeature;
// // // //
declare a base class member function pointer illegal assignment of derived class member function
C++Builder includes the __closure keyword to permit the previous situation. Listing 8.2 uses the classes from Listing 8.1 to demonstrate how this is done.
10 9721 CH08
11/13/00
9:47 AM
Page 447
Using VCL Components CHAPTER 8
447
LISTING 8.2
Assigning a Derived Class Member Function to a Base Member Function Pointer by Using __closure TWatchBrand *WObj = new TWatchBrand; // create the object void (__closure *Wptr)(bool); // define a closure pointer Wptr = WObj->NewFeature; // set it to the NewFeature member function Wptr(false); // call the function, passing false Wptr = 0; // set the pointer to NULL delete WObj; // delete the object
Note that we can also assign closure pointers to the base class member functions, as Listing 8.3 shows. LISTING 8.3
Assigning Closure Pointers to Base Class Member Functions
void (__closure *Wptr2)(HandTypes); Wptr2 = WObj->MoveHand; Wptr2(htSecond);
The __closure keyword is predominantly used for events in C++Builder.
8
The __declspec Class Extension
USING VCL COMPONENTS
VCL classes have restrictions imposed on them, such as the following: • No virtual base classes are allowed. • No multiple inheritance is allowed. • They must be dynamically allocated on the heap by using the global new operator. • They must have a destructor. • Copy constructors and assignment operators are not compiler generated for VCL-derived classes. The __declspec keyword is provided for language support with the VCL to overcome the previously mentioned items. The sysmac.h file provides macros that you should use if you need to use this keyword. The __declspec variations are discussed next. __declspec(delphiclass, package)
defines the macro DELPHICLASS as __declspec(delphiclass, package). The delphiclass argument is used for the declaration of classes derived from TObject. If a class is translated from Object Pascal, the compiler needs to know that the class is derived from TObject. Hence, this modifier is used. sysmac.h
Similarly, if you create a new class that is to be used as a property type in a component, and you need to forward declare the class, then you need to use the __declspec(delphiclass,
10 9721 CH08
11/13/00
448
9:47 AM
Page 448
C++Builder 5 Essentials PART I package) keyword for the compiler to understand. Listing 8.4 shows an example of forward declaring a class where a new component has a pointer to that class type.
LISTING 8.4
Forward Declaring a Class to Use as a Member of Another Class
class TMyObject; class TMyClass : public TComponent { private: TMyObject *FMyNewObject; public: __fastcall TMyClass(TComponent *Owner) : TComponent(Owner){} __fastcall ~TMyClass(void); }; class TMyObject : public TPersistent { public: __fastcall TMyObject(void){} __fastcall ~TMyObject(void); };
Listing 8.4 will compile, because the compiler only has a reference to the TMyObject class. If you need to include a property of type TMyObject, you need to tell the compiler the class is derived from a descendant of TObject. The modified code is shown in Listing 8.5. LISTING 8.5
Using DELPHICLASS to Forward Declare a Class Required as a Property of
Another Class class
DELPHICLASS TMyObject;
class TMyObject; class TMyClass : public TComponent { private: TMyObject *FMyNewObject; public: __fastcall TMyClass(TComponent *Owner) : TComponent(Owner){} __fastcall ~TMyClass(void);
10 9721 CH08
11/13/00
9:47 AM
Page 449
Using VCL Components CHAPTER 8
LISTING 8.5
449
Continued
__published: __property TMyObject *NewObject = {read = FMyNewObject}; }; class TMyObject : public TPersistent { public: __fastcall TMyObject(void){} __fastcall ~TMyObject(void); };
also defines a closely related macro RTL_DELPHICLASS as __declspec (delphiclass). This macro is used when non-packaged RTL (runtime library) functionality is required in a class. Since components are created in designtime packages or runtime/designtime packages, you will find the DELPHICLASS macro used. For more information on runtime and designtime packages, see “Understanding and Using Packages,” in Chapter 2, “C++Builder Projects and More on the IDE.” sysmac.h
8
__declspec(delphireturn, package)
defines the macro DELPHIRETURN as __declspec(delphireturn,
package).
The delphireturn parameter is used internally by C++Builder to allow classes created with C++Builder to support Object Pascal’s built-in data types and language constructs. Examples include Currency, AnsiString, Variant, TDateTime, and Set. Similar to __declspec(delphiclass), there is also a macro defined for __declspec (delphireturn). This macro, RTL_DELPHIRETURN, is used when Delphi’s semantics are required in non-packaged classes. __declspec(dynamic)
defines the macro DYNAMIC as __declspec(dynamic). The dynamic argument is used for dynamic functions and is valid only for classes derived from TObject. These are similar to virtual functions except that the vtable information is stored only in the object that created the functions. If you call a dynamic function that doesn’t exist, then the ancestor vtables are searched until the function is found. Dynamic functions effectively reduce the size of the vtables at the expense of a short delay to look up the ancestor tables. sysmac.h
Dynamic functions cannot be redeclared as virtual and vice versa. __declspec(hidesbase)
defines the macro HIDESBASE as __declspec(hidesbase). The hidesbase parameter is used to preserve Object Pascal semantics. Imagine a class called C1; derived from it is another class called C2. If both classes contain a function called foo, then C++ interprets sysmac.h
USING VCL COMPONENTS
sysmac.h
10 9721 CH08
11/13/00
450
9:47 AM
Page 450
C++Builder 5 Essentials PART I
as a replacement for C1::foo(). In Object Pascal, C2::foo() is a different function to C1::foo(). To preserve this in C++ classes, you use the HIDESBASE macro. Listing 8.6 demonstrates its use.
C2::foo()
LISTING 8.6
Using HIDESBASE to Override Class Methods in Descendant Classes
class TMyObject : public TPersistent { public: __fastcall TMyObject(void){} __fastcall ~TMyObject(void); void __fastcall Func(void){} }; class TMyObject2 : public TMyObject { public: __fastcall TMyObject2(void){} __fastcall ~TMyObject2(void); HIDESBASE void __fastcall Func(void){} };
__declspec(hidesbase, dynamic)
defines the macro HIDESBASEDYNAMIC as __declspec(hidesbase, dynamic). This is used when Object Pascal’s semantics need to be preserved for dynamic functions. The HIDESBASE macro is used when you need to preserve the way Pascal overrides methods in descendant classes. The HIDESBASEDYNAMIC macro does the same for dynamic methods. sysmac.h
__declspec(package)
defines the macro PACKAGE as __declspec(package). The package argument in indicates that the code defining the class can be compiled in a package. This allows classes to be imported and exported from the resulting BPL file. sysmac.h
__declspec(package)
__declspec(pascalimplementation) sysmac.h defines the macro PASCALIMPLEMENTATION as __declspec(pascalimplementation). The pascalimplementation argument indicates that the code defining the class was implemented in Object Pascal. This modifier appears in an Object Pascal portability header file with an .hpp extension.
The __fastcall Keyword The __fastcall keyword is used to declare functions that expect parameters to be passed in registers. If the parameter is a floating-point or struct type, the registers are not used.
10 9721 CH08
11/13/00
9:47 AM
Page 451
Using VCL Components CHAPTER 8
451
In general, use the __fastcall keyword only when declaring VCL class member functions. Using this modifier in all other cases will more often than not result in reduced performance. Typical examples of its use is found in all form class and component member functions. You should also note that the __fastcall modifier is subject to name mangling.
The __property Keyword The __property keyword is used to declare properties in classes, even non-VCL classes. Properties are data members, with the following additional features: • They have associated read and write methods. • They can indicate default values. • They can be streamed to and from a form file. • They can extend a property defined in a base class. • They can make the data member read-only or write-only. Refer to Chapter 9, “Creating Custom Components,” for a more detailed look at properties.
The __published Keyword
This RTTI is useful in event handlers. All standard events have a parameter passed of type TObject*. This parameter can be queried when the type of sender is unknown. The following example shows how this is done when the OnButtonClick event is called from either a button click or a manual call such as Button1Click(NULL). Note that you can also do a manual call, passing the pointer of another button such as Button1Click(Button2). Listing 8.7 shows how this can be implemented. LISTING 8.7
Using RTTI to Query an Object Type
void __fastcall TForm1::Button1Click(TObect *Sender) { TButton *Button = dynamic_cast(Sender); if (Button) { // do something if a button was pressed. } else {
8 USING VCL COMPONENTS
The __published keyword is permitted only in classes descendant from TObject. Visibility rules for the __published section of a VCL class are the same as the public section, with the addition of RTTI being generated for data members and properties declared. RTTI enables you to query the data members, properties, and member functions of a class.
10 9721 CH08
11/13/00
452
9:47 AM
Page 452
C++Builder 5 Essentials PART I
LISTING 8.7
Continued
// do something when a call was made // passing a NULL value for *Sender) } }
The Streaming Mechanism C++Builder is referred to as a Rapid Application Development (RAD) environment. This is partly because of the GUI interface to the developer and the object-orientated nature of the language itself. In addition, C++Builder uses a streaming (read and write) mechanism to maintain property settings. During designtime, you set various properties of components. The IDE stores these settings (described in more detail later) as part of the form on which these components belong. The forms are saved as part of the project in a file with a .dfm extension. What is stored in the form’s file is loaded again at runtime. In other words, the properties are persistent. C++Builder 5 has the option to save the form in text or binary format (as was the case with previous versions of C++Builder). Under the Tools menu, choose Environment, Options, followed by the Preferences tab. On this tab is an option to create new forms as text. With this option selected, you can open the .dfm files in any text editor. A standard C/C++ class contains three main areas: public, private, and protected. C++Builder adds another section called __published. The __published extension is available only for classes descending from TObject. class TSample : public TObject { public: private: protected: __published: }
The __published area is used to define properties that will be storable. Storable properties are those that are written to the form file at designtime. To understand this better, open any project in C++Builder and right-click one of the forms. Choose the menu item View as Text to see how the form information is stored. Right-click the code and select View as Form to return to the graphical view again. The storing of properties is achieved using the Store-and-Load Mechanism, which will satisfy the component writer’s requirements under most circumstances. We’ll discuss when this procedure is insufficient in the next section, “Advanced Streaming Requirements.”
10 9721 CH08
11/13/00
9:47 AM
Page 453
Using VCL Components CHAPTER 8
453
When a component is created, the developer gives it a set of default values for its published properties. These defaults are assigned in the constructor of the component. At runtime, the user modifies these properties via the Object Inspector. In fact, every property shown in the Object Inspector is a published property. Properties can be declared in the public section of a component’s definition (the class), but these will be available only at runtime. Chapter 9 covers properties in more detail, but a brief example of default and stored properties is required at this point. Imagine two properties, one with a default value of 10 and the other defined not to be stored. __property int SomeProperty1 = {read=FProp1, write=FProp1, default=10}; __property AnsiString SomeProperty2 = {read=FProp2, stored=false};
The declaration of SomeProperty1 does not set its value to 10. This is always done in the constructor. The default keyword is used to tell the IDE to store the value of this property in the form file only if it has a value other than 10. The second property, SomeProperty2, is declared to not store its value in the form file. An example of this might be a property to indicate the current version for the component. Since the version number will not change, it does not need to be stored in the form.
Component construction and the associated property streaming processes are something that the programmer often doesn’t have to worry about.
Advanced Streaming Requirements Component properties can be numerical, character, strings, enumerated types (including Boolean), sets, or more complex objects such as custom-defined structs and classes. C++Builder has built-in handling for the streaming of simple data types. Support for streaming custom objects is provided if that class is derived from TPersistent. TPersistent provides the capability to assign objects to other objects and enable the reading and writing of their properties to and from a stream. Additional information can be found in the index of the online help under the topic “TPersistent.”
Some property types, such as arrays, require their own property editor. Without an editor, the Object Inspector is unable to provide the programmer an interface to edit the contents of the property. Refer to Chapter 10, “Creating Property and Component Editors,” for more information on property editors. For more information on properties and how they are used, refer to Chapter 9.
8 USING VCL COMPONENTS
When the component is saved, the property information that differs from the default is written to the form file. When a project is opened again, an instance of each component is created, the component properties are set to their defined defaults, and the stored, non-default values are read and assigned.
10 9721 CH08
11/13/00
454
9:47 AM
Page 454
C++Builder 5 Essentials PART I
Streaming Unpublished Properties So far we have learned that the Object Inspector provides the programmer a designtime interface to the published properties of a component. This is the default behavior of components, but we are not limited to this. We have also discovered that properties derived from TPersistent have the capability to stream to and from a form file. This means we have the capability to create persistent properties that do not appear in the Object Inspector. Additionally, we can create streaming methods for properties that C++Builder does not know how to read or write. Saving an unpublished property is achieved by adding code to tell C++Builder how to read and write the property’s value. This is accomplished in a two-step process. • Override the DefineProperties() method. The previously defined methods are passed to what is known as a filer object. • Create methods to read and write the property value. We have already ascertained that published properties are automatically streamed to the form file. This is handled using read and write methods defined for the various property types. The property names and the methods used to perform the streaming are defined by the DefineProperties() method. When you want to stream a non-published property, you need to tell C++Builder. This is done by overriding the DefineProperties() method. Listing 8.8 provides an example component, TSampleComp, that has three unpublished properties. The component is capable of streaming the properties by the methods provided. It creates an instance of a second component called TComp at runtime and is referenced via the property Comp3. Since this component is not dropped onto a form by the developer, the properties for it are not automatically streamed to the form file. We will provide our component with the code it requires to stream this information as well. Read through Listing 8.8, and then we will work through it. LISTING 8.8
A Component to Stream Unpublished Properties
// A minimum class declaration to get the example to compile class TComp : public TComponent { public: __fastcall TComp::TComp(TComponent *Owner) : TComponent(OWner) {} }; class TSampleComp : public TComponent { private: int FProp1;
10 9721 CH08
11/13/00
9:47 AM
Page 455
Using VCL Components CHAPTER 8
LISTING 8.8
455
Continued
AnsiString FProp2; TComp *FComp3; void void void void void void
__fastcall __fastcall __fastcall __fastcall __fastcall __fastcall
ReadProp1(TReader *Reader); WriteProp1(TWriter *Writer); ReadProp2(TReader *Reader); WriteProp2(TWriter *Writer); ReadComp3(TReader *Reader); WriteComp3(TWriter *Writer);
protected: void __fastcall DefineProperties(TFiler *Filer); public: __fastcall TSampleComp(TComponent* Owner); __fastcall ~TSampleComp(void);
}; void __fastcall TSampleComp::TSampleComp(TComponent* Owner) : TComponent(Owner) { FProp1 = 10; // The default FComp3 = new TComp(NULL); // we will need to stream this } void __fastcall TSampleComp::~TSampleComp (void) { if(FComp3) delete FComp3; } void __fastcall TSampleComp::DefineProperties(TFiler *Filer) { // Call the base method first TComponent::DefineProperties(Filer); Filer->DefineProperty(“Prop1”, ReadProp1, WriteProp1, (FProp1 != 10)); Filer->DefineProperty(“Prop2”, ReadProp2, WriteProp2, (FProp2 != “”)); // need to determine if the properties for Comp3 need to be written
8 USING VCL COMPONENTS
__property int Prop1 = {read = FProp1, write = FProp1, default = 10}; __property AnsiString Prop2 = {read = FProp2, write = FProp2, nodefault}; __property TComp *Comp3 = {read = FComp3, write = FComp3};
10 9721 CH08
11/13/00
456
9:47 AM
Page 456
C++Builder 5 Essentials PART I
LISTING 8.8
Continued
bool WriteValue; if(Filer->Ancestor) // check for inherited value { TSampleComp *FilerComp = dynamic_cast(Filer->Ancestor); if(FilerComp->Comp3 == NULL) WriteValue = (Comp3 != NULL); else { if((Comp3 == NULL) || (FilerComp->Comp3->Name != Comp3->Name)) WriteValue = true; else WriteValue = false; } } else // no inherited value, write property if not null WriteValue = (Comp3 != NULL); Filer->DefineProperty(“Comp3”, ReadComp3, WriteComp3, WriteValue); } void __fastcall TSampleComp::ReadProp1(TReader *Reader) { Prop1 = Reader->ReadInteger(); } void __fastcall TSampleComp::WriteProp1(TWriter *Writer) { Writer->WriteInteger(FProp1); } void __fastcall TSampleComp::ReadProp2(TReader *Reader) { FProp2 = Reader->ReadString(); } void __fastcall TSampleComp::WriteProp2(TWriter *Writer) { Writer->WriteString(FProp2); } void __fastcall TSampleComp::ReadComp3(TReader *Reader) { if(Reader->ReadBoolean())
10 9721 CH08
11/13/00
9:47 AM
Page 457
Using VCL Components CHAPTER 8
LISTING 8.8
457
Continued
FComp3 = (TComp *)Reader->ReadComponent(NULL); } void __fastcall TSampleComp::WriteComp3(TWriter *Writer) { if(FComp3) { Writer->WriteBoolean(true); Writer->WriteComponent(Comp3); } else Writer->WriteBoolean (false); }
Let’s get the easy properties out of the way first. The DefineProperties() method contains these two lines of code to register the first two properties: Filer->DefineProperty(“Prop1”, ReadProp1, WriteProp1, (FProp1 != 10)); Filer->DefineProperty(“Prop2”, ReadProp2, WriteProp2, (FProp2 != “”));
The Comp3 property is different and will require some additional explanation. This property is different to other properties because it is a component (instantiated by the class at runtime) rather than a data type. Listing 8.9 presents the section of code responsible for determining if this property requires streaming. LISTING 8.9
Determining if a Property That Is a Component Requires Streaming
bool WriteValue; if(Filer->Ancestor) // check for inherited value { TSampleComp *FilerComp = dynamic_cast(Filer->Ancestor); if(FilerComp->Comp3 == NULL) WriteValue = (Comp3 != NULL); else { if((Comp3 == NULL) || (FilerComp->Comp3->Name != Comp3->Name)) WriteValue = true;
USING VCL COMPONENTS
This tells C++Builder to use the read and write methods provided when streaming these properties. The last parameter is a flag to indicate if we have data to store. Prop1 and Prop2 need to be stored only if their values differ from the default value.
8
10 9721 CH08
11/13/00
458
9:47 AM
Page 458
C++Builder 5 Essentials PART I
LISTING 8.9
Continued
else WriteValue = false; } } else // no inherited value, write property if not null WriteValue = (Comp3 != NULL); Filer->DefineProperty(“Comp3”, ReadComp3, WriteComp3, WriteValue);
This property represents a component instantiated at runtime. Since the component is not dropped onto a form, the default mechanism of streaming the properties is not performed. This DefineProperties() method will take care of this for us. First we need to determine if the filer’s Ancestor property is true to avoid saving a property value in inherited forms. If there is no inherited value, then we will stream the property (which is a component) if Comp3 is not NULL. If the filer’s Ancestor property is true, then we need to next look at the Comp3 property of the ancestor. If this property is NULL, then we stream our property (TSampleComp->Comp3) if it is not NULL. If the filer’s Ancestor Comp3 property is not NULL, then we perform two final checks. If our property (TsampleComp->Comp3) is NULL or the name of our Comp3 property is different from the ancestor’s, then we will stream the property (a component). Finally, we define our property, using DefineProperty() as previously explained. This sounds confusing at first because it is hard to imagine a property being a component. Read through the code a few times and try to grasp what it is we are working with. It’s quite easy from then on. A final note for you to explore. We have looked at the DefineProperty() method, which deals with data types such as integers, strings, chars, Booleans, and enumerated types. There is another method, DefineBinaryProperty(), that is designed to be used for the streaming of binary information such as graphics and sound files. Refer to the “DefineBinaryProperty” section of “TWriter” in the online help index foradditional information on this.
Common Control Updates Unlike other windowing environments, Microsoft Windows provides several predefined control classes that can be used with little effort in all Windows applications. The control classes that fall under the category of standard controls include buttons, list boxes, combo boxes, static controls, edit controls, and scrollbars. These types of controls are preregistered and can be used without an explicit call to the RegisterClass() API function. Other types of control classes,
10 9721 CH08
11/13/00
9:47 AM
Page 459
Using VCL Components CHAPTER 8
459
termed common controls, include list views, tree views, toolbars, trackbars, header controls, and many more. Unlike the standard controls, the common controls require the use of the InitCommonControlEx() API function for class registration. This function also serves to ensure that the common control library is loaded.
Common Control Dynamic Link Library Most common control classes reside in a special DLL file, COMCTL32.DLL. Unlike the standard controls, the common controls are frequently updated, at which time a new version of the COMCTL32.DLL file is supplied. Typically, this file is updated when a newer version of Microsoft Internet Explorer is installed. Care must be taken when using a common control class that is only present in newer versions of the common control DLL. Moreover, several classes that are present in all versions exhibit features present only in newer versions. As such, use of the DllGetVersion() API function (4.71+) and the _WIN32_IE macro is recommended to ensure compatibility with the version installed on the target platform. Table 8.1 lists the various versions of the common control DLL, including its corresponding distribution platform and _WIN32_IE macro result. TABLE 8.1
8 Common Control Library Versions
Distribution Platform
Version 4.00 4.70 4.71 4.72 5.80 5.81
_WIN32_IE
Result Windows 95/NT 4.0 Internet Explorer 3.X Internet Explorer 4.0 Windows 98/IE 4.01 Internet Explorer 5.0 Windows 2000
0x0200 0x0300 0x0400 0x0500 0x0500 0x0501
The code in Listing 8.10 demonstrates how to use the DllGetVersion() function to determine the version of the common control library. LISTING 8.10
Using the DllGetVersion() Function
#include <shlwapi.h> unsigned int __fastcall GetComctl32Version() { // // == GetComctl32Version() ==
USING VCL COMPONENTS
COMCTL32.DLL
10 9721 CH08
11/13/00
460
9:47 AM
Page 460
C++Builder 5 Essentials PART I
LISTING 8.10 // // // // // // // // // //
Continued
Determines if the common control DLL implements the DllGetVersion() function and if so, uses it to extract the version information. The return value is 100x the version number, if the DllGetVersion() function is present, otherwise, the function returns zero. == See also == http://msdn.microsoft.com/library/psdk/shellcc/shell/Versions.htm http://support.microsoft.com/support/kb/articles/Q186/1/76.ASP
unsigned int result = 0; HINSTANCE hLib = LoadLibrary(“COMCTL32.DLL”); if (hLib) { DLLGETVERSIONPROC DllGetVersion = reinterpret_cast( GetProcAddress(hLib, ”DllGetVersion”) ); if (DllGetVersion) { DLLVERSIONINFO version_info; memset(&version_info, 0, sizeof(DLLVERSIONINFO)); version_info.cbSize = sizeof(DLLVERSIONINFO); if (SUCCEEDED(DllGetVersion(&version_info))) { result = 100 * version_info.dwMajorVersion + version_info.dwMinorVersion; } } FreeLibrary(hLib); } return result; } __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { // // test to see if the common control DLL // is prior to version 4.71... // if (GetComctl32Version() < 471) {
10 9721 CH08
11/13/00
9:47 AM
Page 461
Using VCL Components CHAPTER 8
LISTING 8.10
461
Continued
throw EWin32Error( “This program requires that you have at least version 4.71\n” “of the common control library installed. Please refer to\n” “the following Knowledge Base article for information on\n” “upgrading your common control DLL:\n\n” “http:// support.microsoft.com/support/kb/articles/Q186/1/76.ASP” ); } }
In fact, several VCL components that encapsulate certain common control classes provide features that are available only with newer versions of the common control library. When working with a component that utilizes newer features, you must consider the DLL version installed on the platform on which the application is to run. Oftentimes this differs from that of the development platform. The VCL provides no runtime check for compatibility between the common control library and specific components or component features. If your project relies on a common control–based component, be sure to use the GetComctl32Version() function appropriately.
C++Builder Common Control Updates
With each update of the common control library, new structures, messages, and functions are introduced. As such, developers of the VCL must continuously strive to keep the corresponding components current by incorporating these new aspects. This perpetual process makes each new version of C++Builder that much more valuable than its predecessors. Indeed, C++Builder 5 exhibits many of the latest additions to the common control DLL. In this section, we’ll examine those updates that are unique to C++Builder 5. These include, but are not limited to, the following: • Updates to the TListView class • Updates to the THeaderControl class •
TToolBar
Custom Draw support
TListView Common Control Updates The TListView class encapsulates the Windows list view common control. Undoubtedly the most versatile of the VCL components, the TListView class exhibits nearly every aspect of the corresponding list view control. Let’s now explore several updates to the TListView class that are introduced in C++Builder 5. We will restrict our discussion here to those aspects that correspond to the common controls library.
USING VCL COMPONENTS
The VCL includes several components that encapsulate many of the Windows common control classes. Specifically, these include those components located on the Win32 page of the Component Palette.
8
10 9721 CH08
11/13/00
462
9:47 AM
Page 462
C++Builder 5 Essentials PART I
Sub-Item Images First introduced in COMCTL32.DLL version 4.70, the LVS_EX_SUBITEMIMAGES extended style allows report mode list view controls to display an image next to each sub-item. This style is especially useful when each column is used to represent information that is best conveyed with pictorial support. The TListView class adds this extended style by default. The TListItem class introduces the SubItemImages property that can be used to associate an image (from the corresponding TListView::SmallImages property) with each sub-item. Hovering Time The LVM_SETHOVERTIME message was first introduced in version 4.71 of COMCTL32.DLL. When used in conjunction with the LVS_EX_TRACKSELECT extended style, this message allows an application to adjust the amount of time that the mouse cursor must hover over an item before that item is selected. The TListView class supports this message through the TListView::HoverTime property. By setting the TListView::HotTrack property to true and assigning a millisecond value to the HoverTime property, we can easily manipulate the required hovering time. Working Areas A unique feature of the list view control is its capability to contain multiple working areas. That is, the client area of the list view can be partitioned into several virtual rectangular regions. This feature was first introduced with COMCTL32.DLL version 4.71, where the LVS_EX_MULTIWORKAREAS extended style and LVM_GETWORKAREAS, LVM_SETWORKAREAS, and LVM_GETNUMBEROFWORKAREAS messages were added. The TListView class supports this feature through the TListView::WorkAreas property. This property, of type TWorkAreas*, maintains a collection of TWorkArea objects. The TWorkArea class presents the TWorkArea::Rect property that can be used to define the bounding rectangle of a particular working area. It is from within the TWorkAreas class, and specifically the private TWorkArea::Update member function, that the LVM_SETWORKAREAS message is used. The TListView class also introduces the ShowWorkAreas property, used in conjunction with the extended style. The TListView class extends the underlying list view’s support of working areas by allowing each working area to exhibit a colored outline rectangle and caption. These features, maintained by the TWorkArea class through the TWorkArea::Color and TWorkArea::Caption properties, respectively, are supported in the TListView class via direct handling of the WM_PAINT message and the internal TCustomListView::DrawWorkAreas member function. LVS_EX_MULTIWORKAREAS
Information ToolTips The LVS_EX_INFOTIP extended style provides the capability for a list view control to display a ToolTip on a per-item basis. First introduced in COMCTL32.DLL version 4.71, this extended style incites the corresponding LVN_GETINFOTIP notification message to be sent. It is in response to
10 9721 CH08
11/13/00
9:47 AM
Page 463
Using VCL Components CHAPTER 8
463
this notification that the TListView class fires the OnInfoTip event. By providing a handler for this event, we can easily display a hint window for each item. THeaderControl Common Control Updates The THeaderControl class encapsulates the Windows header common control. This control is most universally known through its association with the list view control. Version 4.70 of the common control DLL introduced several styles and messages that provide extended functionality for header controls. Accordingly, the THeaderControl class has been updated in C++Builder 5 to reflect these changes.
Resizing Display Specification of the HDS_FULLDRAG style allows the sections of the header control to reflect their new sizes during a resize operation. The result is similar to the effect seen during a window resizing operation when the Show Window Contents While Dragging option is specified as part of the desktop display effects. In fact, the HDS_FULLDRAG style has no effect unless this option is selected. The THeaderControl class provides support for this style via the FullDrag property. TToolBar Custom Draw Support The TToolBar class encapsulates the Windows toolbar common control. The common control DLL was updated in version 4.70 to provide Custom Draw support for many common controls, including the toolbar. Version 4.71 introduced several enhancements to the Custom Draw service, specifically for the toolbar control. Consequently, the TToolBar class is extended in C++Builder 5 to support the Custom Draw service and its new features. As mentioned, Custom Draw support was first introduced for the toolbar control in version 4.70 of the common control DLL. Specifically, the NM_CUSTOMDRAW notification message, NMCUSTOMDRAW structure, and TBSTYLE_CUSTOMERASE style were introduced. Version 4.71 added the NMTBCUSTOMDRAW structure, specifically for use with toolbars. In addition, the TBCDRF_NOEDGES, TBCDRF_HILITEHOTTRACK, TBCDRF_NOOFFSET, TBCDRF_NOMARK, TBSTATE_MARKED, and TBCDRF_NOETCHEDEFFECT specialized response flags were added.
8 USING VCL COMPONENTS
Drag-and-Drop Reordering The THeaderControl class introduces support for the HDS_DRAGDROP style through the DragReorder property. This property allows specification of whether the sections of the header control can be reordered via drag-and-drop. That is, when the property is true, the user can simply use the mouse to drag a header section to a new location within the control. In conjunction with the DragReorder property, the THeaderControl class provides the OnSectionDrag and OnSectionEndDrag events. The former is fired in response to the HDN_ENDDRAG notification message that is sent when the user completes a drag operation. The latter event is fired in response to the NM_RELEASECAPTURE notification message, sent when the header control loses mouse capture, implicitly indicating the end of a drop operation.
10 9721 CH08
11/13/00
464
9:47 AM
Page 464
C++Builder 5 Essentials PART I
The TToolBar class supports the Custom Draw service through the OnCustomDraw, OnCustomDrawButton, OnAdvancedCustomDraw, and OnAdvancedCustomDrawButton events. Further, the specialized response flags are supported via the TTBCustomDrawFlags type. Unfortunately, the TBSTYLE_CUSTOMERASE style is not supported in C++Builder 5. See the “Advanced Custom Draw Events” section later in this chapter for more information on the Custom Draw events of TToolBar.
Summary of C++Builder 5 Common Control Updates The common control library continues to change at an astounding rate. Indeed, this places a great demand on Borland developers who must continually update the VCL to reflect these changes. Inevitably, those classes that do encapsulate a common control will always be one step behind their API counterparts. We have seen firsthand evidence of this, when a new feature has been added to the common control library but not to the VCL or has been delayed until a subsequent release. For example, the Custom Draw service has been available for the trackbar control since version 4.70, yet it still has no VCL support. Still, the VCL does provide support for those features that are most commonly used and will never restrict us from using the Windows API directly. Moreover, C++Builder 5 shows evidence that the VCL is adapting and even extending some aspects of the common control library. We saw a notable example of this in the TListView class, where support for multiple working areas exceeds that of the underlying list view control. Undoubtedly, we will continue to see further enhancements in subsequent versions.
Miscellaneous VCL Enhancements C++Builder 5 contains several other enhancements to the VCL, general improvements that make life easier for C++Builder developers. A few of the best improvements are listed in the following sections.
New Help Hint and Menu Features You can now set the Help Hint and menu fonts in your applications by using the new HintFont and MenuFont properties of TScreen. You can now also render owner-drawn menu items based on additional states that the OnDrawItem event of TMenuItem does not cover, such as disabled, grayed, default, and inactive, by using the State parameter of the new OnAdvancedDrawItem event. The new AutoHotKeys property of TMainMenu and TMenuItem can be used to automatically set hotkeys for menu items by setting it to maAutomatic. This setting will also resolve duplicate hotkeys by assigning a different key to each if possible. Context menus can now be easily controlled using the new OnContextPopup event of TControl. Now, for example, you can easily display a custom pop-up menu, dialog, picture, or just about anything in response to the right-click of a control.
10 9721 CH08
11/13/00
9:47 AM
Page 465
Using VCL Components CHAPTER 8
465
The animation of pop-up menus can be controlled with the MenuAnimation property of TPopupMenu. It can be set to simply appear or to slide in from the left, right, top, or bottom. This feature will work only on Windows 98, Windows NT, or later systems.
Registry Access A feature that has been in high demand is the new Access property of the TRegistry, TRegistryIniFile, and TRegIniFile objects. It allows you to specify the level of access to use when opening Registry keys. Some standard values for the Access property include KEY_ALL_ACCESS, KEY_READ, and KEY_CREATE_SUB_KEY. Previously, a custom third-party component was required to provide this functionality to read keys under HKEY_LOCAL_MACHINE on Windows NT systems without Administrator privileges. Often a developer would find that his application worked well on Windows 98 systems but did not work as expected on Windows NT systems.
VCL Documentation Enhancements
FIGURE 8.3 The routines listing for the stdctrls unit defining TButton.
8 USING VCL COMPONENTS
The VCL documentation now includes a list of routines for each VCL unit. You can find it in the “Routines Listing, by Unit/Header” section of “Visual Component Library Reference” in the C++Builder help contents. You can also click on the unit name in the help topic for an object to display a list of all classes in that unit. Figure 8.3 shows the routines listing for stdctrls, the unit in which TButton is defined.
10 9721 CH08
11/13/00
466
9:47 AM
Page 466
C++Builder 5 Essentials PART I
New TApplicationEvents Component The new TApplicationEvents component (on the Additional component tab) makes it easy to set event handlers for application-level events from the Object Inspector. By dropping a TApplicationEvents component on a form, you can easily set event handlers for events such as OnActive, OnDeactivate, OnIdle, or OnMessage, to name a few. This can be achieved simply by double-clicking the desired event property of the TApplicationEvents component in the Object Inspector as you would for any other component.
TIcon Enhancements The TIcon class now has support for multi-resolution icons and icons with more than 16 colors. The icon image with the closest match to the Width and Height properties specified before loading the icon image will be used. The Width and Height values are only suggestions; the actual icon image will not be stretched or shrunken to fit if an icon image does not match the size exactly.
Other Miscellaneous VCL Enhancements Other VCL enhancements include the new AutoSnap property of the TSplitter component to control the action to take when a user drags the splitter to resize an object smaller than its minimum size. The new BiDiKeyboard and NonBiDiKeyboard properties of TApplication specify the keyboard layout, and the new Contnrs unit introduces a few utility classes for managing stacks and queues. There are several other VCL enhancements. Consult the C++Builder online help for more information.
Extending the VCL—More than Just a TStringList is one of the most utilized classes in applications where development time is short and memory and speed are not going to be a major issue. The TStringList class is a very useful container capable of storing strings and associated objects. The class has readily available properties and methods that allow us to manipulate strings and stored objects. TStringList
This section will show a typical example of using a TStringList descendant to hold text information in an AnsiString and relational information that further describes that text. We are going to capture all available database sessions, aliases, tables, and related fields on the current system. We are going to make the class versatile enough that it will store related database information (sessions, aliases, tables, and fields) without the need for four different object types.
10 9721 CH08
11/13/00
9:47 AM
Page 467
Using VCL Components CHAPTER 8
467
Using TStringList as a Container The help files provided with C++Builder are quite adequate for most cases, so this section will concentrate on the major area of storing objects against individual strings. It is highly recommended that you read the Visual Component Library Reference help file for a more detailed explanation of the properties and methods available for the TStringList class. Initially concentrate on the Strings, Objects, and CommaText properties, followed by the Add(), Delete(), and IndexOf() methods. Be sure to read all of the available properties and methods to fully appreciate what is available. The TStringList object is more than a class that holds strings with provisions for sorting, exchanging, inserting, adding, and deleting items. It also has provision to hold a reference to another VCL object. Why a VCL object? If you look at the declaration of the Objects property, you will see it returns a pointer of type TObject: __property System::TObject* Objects[int Index] = {read=GetObject, ➥write=PutObject};
StringList1->Objects[index] = dynamic_cast(Memo1);
When you want to retrieve that information, you cast back to what you require: TObject *MyObject = StringList1->Objects[i]; TMemo *MyMemo = dynamic_cast(MyObject);
This code assumes you are expecting the object always to refer to a TMemo object. In your own application you should first check that MyMemo is not NULL before using it.
Storing Non-VCL Objects The Objects property of TStringList is of type TObject and must therefore reference an object of this type or one of its descendants. To store structures or classes in the string list is to create a class that descends from TObject. Listing 8.11 shows an example. LISTING 8.11
Deriving a Class from TObject for Future Storage Requirements
class MyStruct : public TObject { public:
8 USING VCL COMPONENTS
The question then becomes one of how to store information relevant to our application. If you want to store a pointer to an existing VCL object or component, then the assignment is performed by casting the object to a TObject*:
10 9721 CH08
11/13/00
468
9:47 AM
Page 468
C++Builder 5 Essentials PART I
LISTING 8.11
Continued
int Age char Sex; bool Married; };
By descending the class from TObject (or an object that descends from TObject), we can now link this structure to our string list. For this section we are going to create a class descending from TStringList that will capture all available database sessions. The class will hold all the names of these sessions in the Strings property. It will then determine all available aliases for each of the sessions. This information will be stored in another instance of the same class type and will be linked to the parent class via the Objects property. This process will continue for each of the alias’s tables, and for each table it will locate all available field names. How are we going to achieve all this with one class? • The session class must hold the session names and contain a link to all the related aliases. • The alias class must hold the alias names and contain a link to all the related tables. • The table class must hold the table names and contain a link to all the related fields. • The field class must contain the field names. In all four cases, we need to store a list of names and store an object against each name. Because the object stored against each name needs to hold related information, it is possible to store an object of the same type; just fill it with different data. In the TDBList-Lite and TDBList-Full folders on the CD-ROM that accompanies this book, you will find two versions of the example project, TDBListDemo.bpr. The Lite version demonstrates the techniques discussed in the following sections and can be used as a reference as you read. The Full version includes some enhancements to the Lite version, outlined in the “Making Improvements” section, later in this chapter.
NOTE Depending on system configuration, users may find that a variety of login screens are presented. This is due to the variety of BDE drivers set up when C++Builder is installed (look in the BDE Administrator applet found in the Control Panel). Press the Escape key if you don’t know the login information. If you run the program within the IDE, you might also experience some exceptions being thrown due to the login failures. The code presented has not been designed for full exception handling and has been left to the user for further enhancement.
10 9721 CH08
11/13/00
9:47 AM
Page 469
Using VCL Components CHAPTER 8
469
Linking Strings to Objects of the Same Type The TStringList is a very convenient storage container of string lists. It has the additional capability of maintaining a pointer to another object of type TObject or one of its descendants. The instantiation and deletion of these objects is up to you, but once you assign the pointer to the Objects property of TStringList, the management of that pointer is maintained as additional strings are added, removed, or inserted. Because this class adds objects to each of the members, the class will also be responsible for freeing the memory associated with each of these objects when an item is removed. Failure to do so will cause a memory leak. The class presented for this discussion is called TDBList and is a descendant of TStringList. We will start by looking at the relevant section of the header file for the TDBList class, shown in Listing 8.12. LISTING 8.12
Initial TDBList Declaration in dblist.h
#include enum DBListType {ltSession, ltAlias, ltTable, ltField};
8
class TDBList;
void void void void
__fastcall __fastcall __fastcall __fastcall
GetSessions(void); GetAliases(void); GetTables(void); GetFields(void);
public: void __fastcall DeleteObjects(void); void __fastcall EnumDBList(void); virtual void
__fastcall Clear(void);
__fastcall TDBList(TDBList *pListParent, DBListType pListType, ➥ AnsiString pParentItemName); __fastcall ~TDBList(void);
USING VCL COMPONENTS
class TDBList : public TStringList { private: TTable *TempTable; TDBList *FListParent; DBListType FListType; AnsiString FParentItemName;
10 9721 CH08
11/13/00
470
9:47 AM
Page 470
C++Builder 5 Essentials PART I
LISTING 8.12
Continued
__property TDBList *ListParent = {read = FListParent}; __property DBListType ListType = {read = FListType}; __property AnsiString ParentItemName = {read = FParentItemName}; };
The class TDBList is designed with the intention of being able to use an instance of the same class type for the Objects property. Using this approach eliminates the need to create custom classes for each of the session, alias, table, and field data objects. We could have created a base class and its descendants to represent the previously mentioned object types, but I chose to take a different approach. As each object is created, it needs a pointer to the owner that created it (so that we can retrieve information). This is achieved by creating a property of type TDBList*. This read-only property is called ListParent. The object also requires a flag to indicate what type of object it is representing. This is done via the enumerated type DBListType. The flag property is called ListType. This property will direct the class of the tasks it is required to perform, including the retrieval of related information and storing it in the Objects property. Finally, the class will need to know the item name of the parent class. Item name refers to the name of the session, alias, or table currently being enumerated. By the time we get down to the field level, we can retrieve the item names of all parent classes via the ListParent property. This will become clearer as we work through the source code. In order for this class to contain a property of the same type, we need to forward declare the class. You will find the forward declaration immediately before the declaration of the TDBlist class. The GetSessions(), GetAliases(), GetTables(), and GetFields() methods are self explanatory but will be touched upon as we look at the source code. The EnumDBList method is where all the work begins, and the DeleteObjects method is used to free up previously allocated memory for the Objects property. These are the two most important areas of this class and will be covered in greater detail shortly. That’s it for the header file. Quite impressive for something that has the potential to return quite a huge amount of information. Now it’s over to the source code. We will start with the constructor and progressively work our way through the tasks performed by the class. At this point you might be wondering what all this has to do with the TStringList class. So far we have created a class, descendant from TStringList, and built the framework for the self-populating object. As the object populates itself with data, it will retrieve a list of names (the strings) and then create another TDBList object (which also populates itself). This object is linked to the Objects property.
10 9721 CH08
11/13/00
9:47 AM
Page 471
Using VCL Components CHAPTER 8
471
We are now going to look at how we build this specialized class. The discussion will finish up by showing the versatility of the class by overriding the sort method of the TStringList class. The modification we introduce will sort all the strings in the list and then continue to sort all the strings in all linked Objects. When the class is first instantiated, the constructor sets all private variables and then performs the enumeration after performing some simple checks based on the list type just created. This is shown in Listing 8.13. LISTING 8.13
TDBList Constructor
__fastcall TDBList::TDBList(TDBList *pListParent, DBListType pListType, AnsiString pParentItemName) : FListParent(pListParent), FListType(pListType), FParentItemName(pParentItemName), TStringList() { TempTable = 0;
8 // not applicable to ltSession // sessions don’t have a parent (creator) // setting to NULL just to make sure !
case ltAlias: if (FParentItemName == “”) FParentItemName = “Default”; break; case ltTable: break;
// don’t need to do anything for tables
case ltField: TempTable = new TTable(NULL); break; } EnumDBList(); if (TempTable) delete TempTable; }
USING VCL COMPONENTS
switch(FListType) { case ltSession: FParentItemName = “”; FListParent = 0; break;
10 9721 CH08
11/13/00
472
9:47 AM
Page 472
C++Builder 5 Essentials PART I
The checks are to safeguard against the user passing parameters to the class that are invalid or not required. If the user is creating a TDBList that will hold session information, we are setting ParentItemName to an empty string and ListParent to NULL. If the user passes values for these parameters, there would be no side effects, but it is always better to code for safety. You never know how you’re going to expand on this class in the future. For example, you might create a method that traverses back through a list until it gets to the topmost level. The easiest way to determine you’re at the top is by checking if ListParent is NULL. Adding this code ensures we don’t cause any exceptions to be thrown due to user error. If the user is creating a list of aliases and the ParentItemName property (which is the session name) is an empty string, we are changing it to “Default”. This allows the GetAliases() method to function correctly when no ParentItemName is provided by using the default session. At this stage of the class development there are no checks to perform if the user is creating a list of tables. This will be explained when we look at the GetTables() method. If the user is creating a list of fields, the only thing we need to do is instantiate a new TTable object. This will also be explained when we look at the GetFields() method. Finally, the TDBList object enumerates the list by calling the EnumDBList() method. If there was a TTable object created upon return from EnumDBList, we delete it because we no longer require it.
NOTE The TTable object is required to enumerate the field names. There are two approaches that can be used for the creation of this TTable object. The method used here simply creates the TTable object when required and then destroys it when finished. This has the drawback of slowing the TDBList object when there are large numbers of tables to be enumerated. An alternative approach would be to create a TTable object when the main TDBList object is created. All successive TDBList objects can then check for the existence of a ParentList and if true could search back through the list to find the master TDBList object that created the TTable. This alternative is mentioned again at the end of this section, with source code available on the accompanying CD-ROM.
Now we will look at the EnumDBList() method, shown in Listing 8.14.
10 9721 CH08
11/13/00
9:47 AM
Page 473
Using VCL Components CHAPTER 8
LISTING 8.14
473
EnumDBList() Method of TDBList
void __fastcall TDBList::EnumDBList(void) { // This method collects all names of the object type // (session, alias, table, field) and then proceeds to // link the next object type. // delete the current list and associated items if (Count) DeleteObjects(); // now get the list of appropriate objects and their names switch(ListType) { case ltSession: GetSessions(); break; GetAliases(); break;
case ltTable:
GetTables(); break;
case ltField:
GetFields(); break;
} }
Listing 8.14 is pretty self explanatory. First we check if the list already has data. If it does, we delete the list by calling the DeleteObjects() method, shown in Listing 8.15. LISTING 8.15
The DeleteObjects() Method of TDBList
void __fastcall TDBList::DeleteObjects(void) { for(int i = 0; i < Count; i++) { if (Objects[i]) { delete Objects[i]; Objects[i] = 0; } } Clear (); }
8 USING VCL COMPONENTS
case ltAlias:
10 9721 CH08
11/13/00
474
9:47 AM
Page 474
C++Builder 5 Essentials PART I
The DeleteObjects() method iterates through all the items in the list. If the current string has an associated object, we delete the memory at that location and set the pointer to NULL. The destructor for TDBList calls DeleteObjects() (refer to Listing 8.15). This ensures that all objects linked to other objects automatically have their memory freed. At the end of this loop we clear the string list and return. We have now cleared all linked objects (by deleting them), and the current TDBList object is empty. Back to the EnumDBList() method (refer to Listing 8.14). This method looks at the type of items it is intended to enumerate via ListType and calls the appropriate method. The GetSessions(), GetAliases(), GetTables(), and GetFields() methods are shown in Listing 8.16. Also included is a brief outline of their functions. The database-related issues will be discussed only as far as their importance to this topic is concerned. LISTING 8.16
Retrieving Session, Alias, Table, and Field Information
void __fastcall TDBList::GetSessions(void) { try { Sessions->GetSessionNames(this); // now for each session name we need to collect all related aliases for (int i = 0; i < Count; i++) { TDBList *AliasList = new TDBList(this, ltAlias, Strings[i]); Objects[i] = dynamic_cast(AliasList); } } catch(...) { DeleteObjects(); } } void __fastcall TDBList::GetAliases(void) { AnsiString SessionName = ParentItemName; try { Sessions->List[SessionName]->GetAliasNames(this); for(int i = 0; i < Count; i++)
10 9721 CH08
11/13/00
9:47 AM
Page 475
Using VCL Components CHAPTER 8
LISTING 8.16
475
Continued
{ TDBList *TableList = new TDBList(this, ltTable, Strings[i]); Objects[i] = dynamic_cast(TableList); } } catch(Exception &e) { DeleteObjects(); } } void __fastcall TDBList::GetTables(void) { AnsiString AliasName = ParentItemName; AnsiString SessionName(“Default”);
try { Sessions->List[SessionName]->GetTableNames(AliasName, “”, true, false, this); for(int i = 0; i < Count; i++) { try { TDBList *FieldList = new TDBList(this, ltField, Strings[i]); Objects[i] = dynamic_cast(FieldList); } catch(Exception &e) { DeleteObjects(); } } } catch(...) { DeleteObjects(); }
8 USING VCL COMPONENTS
if(ListParent) SessionName = ListParent->ParentItemName; else SessionName = “Default”;
10 9721 CH08
11/13/00
476
9:47 AM
Page 476
C++Builder 5 Essentials PART I
LISTING 8.16
Continued
} void __fastcall TDBList::GetFields(void) { AnsiString SessionName(“Default”); AnsiString AliasName; AnsiString TableName = ParentItemName;; TDBList *pList = ListParent; if(pList) { AliasName = pList->ParentItemName; pList = pList->ListParent; if(pList) SessionName = pList->ParentItemName;; } try { TempTable->Active = false; TempTable->SessionName = SessionName; TempTable->DatabaseName = AliasName; TempTable->TableType = ttDefault; TempTable->TableName = TableName; TempTable->Active = true; try { TempTable->GetFieldNames(this); for(int i = 0; i < Count; i++) Objects[i] = 0; } catch(Exception &e) { DeleteObjects(); } } catch(Exception &e) { DeleteObjects (); } }
10 9721 CH08
11/13/00
9:47 AM
Page 477
Using VCL Components CHAPTER 8
477
GetSessions() The GetSessions() method retrieves a list of all available session names via the Sessions-> GetSessionNames(this) command. Passing this into GetSessionNames() causes the list of session names to be returned into our list, the TDBList class. This is possible because TDBList is derived from TStringList and therefore has all the same properties and methods as TStringList. The next (and most important) part of this method is the iteration through each item in our list, shown in the following code: for(int i = 0; i < Count; i++) { TDBList *AliasList = new TDBList(this, ltAlias, Strings[i]); Objects[i] = dynamic_cast(AliasList); }
This code creates a new TDBList object, asking it to enumerate all the aliases available for the session name referred to by Strings[i]. If you refer back to the constructor in Listing 8.13, you will see that this causes the EnumDBList() method to be called for each new object. In turn, this will cause the GetAliases() method to be called.
NOTE Code readability is important in simple but powerful objects such as TDBList. The code presented could have been written as shown below (because the pointer AliasList is only a temporary pointer to a newly created TDBList object), but it is not as easy to read. I sometimes write all my code as presented previously and then come back later when all the bugs are ironed out and make changes such as these. It is important to document the code, showing the previous code compared to the new, just in case you need to come back later to make modifications. Original version: for(int i = 0; i < Count; i++) { TDBList *AliasList = new TDBList(this, ltAlias, Strings[i]); Objects[i] = dynamic_cast(AliasList); }
Alternative: for(int i = 0; i < Count; i++) Objects[i] = dynamic_cast(new TDBList(this, ltAlias, ➥Strings[i]));
8 USING VCL COMPONENTS
Each of the new objects is informed that this is the ParentList, therefore ensuring we have a continuous link between all TDBList objects created.
10 9721 CH08
11/13/00
478
9:47 AM
Page 478
C++Builder 5 Essentials PART I
If an exception is raised at any time during the enumeration process, we capture that exception and delete all objects created for the current item. This would occur, for example, when a connection is attempted on an alias and the user does not provide correct login details. In theory no objects would have been created, but it is better to code for other situations in which a network link may go down halfway through the enumeration process. GetAliases() The GetAliases() method first determines the session name using the ParentItemName property. If there is no parent, the constructor ensures that ParentItemName was set to “Default”. Using SessionName, the method finds a list of all aliases using the Sessions->List[SessionName]->GetAliasNames(this) command. The VCL has a global variable called Sessions that maintains a list of available sessions. We use SessionName to get a pointer to the required session and then call the GetAliasNames() method, passing this as the string list that is to be populated. The method continues on to create more TDBList objects, this time a list of table names for each of the aliases found. Again the constructor for each new object causes EnumDBList() to be called, causing GetTables to be called. Assuming there was a ParentList, we now have a list of sessions, and for each of the items a linked list of aliases. We have now created a list of tables for each alias. GetTables() The GetTables() method starts by determining the session and alias details, setting defaults as appropriate. Note how the session name is achieved by taking the ParentItemName of the ListParent property. ListParent, if there is one, is the owner of this current object. This has to be a list of aliases. The ParentItemName of the aliases then has to be the name of the session for this alias. Using the given session and alias names, we retrieve a list of available tables. The parameters of GetTableNames() are well documented in the online help. It is recommended that you look it up for additional reference, because you might want to change them based on your requirements. For each string in the list, we then create an additional TDBList object to hold a list of field names. Each of these objects is assigned to the Objects property, thereby continuing the list of linked objects. GetFields() The GetFields() method is the end of the chain. We first establish the name of the table, alias, and session (using the ParentItemName and ListParent properties). The constructor for each
10 9721 CH08
11/13/00
9:47 AM
Page 479
Using VCL Components CHAPTER 8
479
field list object creates a temporary TTable object; this is used to enumerate the field names for the given table. (At the end of this section we will discuss optimizing this part of the code.) Once we create the TTable object, we retrieve the list of field names and then ensure that the Object property for each item is set to NULL. Although this is the default, it is best to do this to make the code more readable (in terms of intended functionality).
Creating a Chain of Events At the start of the discussion of this object, we looked at how EnumDBList() calls the GetSessions() method. This method, however, then calls GetAliases(), which in turn calls GetTables(), for which each item calls GetFields(). At the end of the process we have a full list of sessions and all related aliases, tables, and fields—not a bad effort from the construction of a single object. The TDBListDemo.bpr project on the CD-ROM that accompanies this book shows how this object can be interrogated for the information it contains. The information is presented in a TMemo control. Because the design of the TDBList class uses linked objects of the same class type, the demo application uses a recursive process to retrieve the information.
LISTING 8.17
Enumerate All Information and Display It
void __fastcall TMainForm::StartButtonClick(TObject *Sender) { TDBList *TheDBList = new TDBList(0, ltSession, “”); if(TheDBList) { TheDBList->Sort(); // sorts all levels PopulateMemo(TheDBList); delete TheDBList; } }
The PopulateMemo() method, shown in Listing 8.18, formats the string to print according to its type and then determines if there is a linked object. If there is an object assigned, it is cast back to a TDBList* and passed onto the PopulateMemo() method again. This process will stop when it reaches the list of field names, because the Objects property is NULL.
8 USING VCL COMPONENTS
We create a single TDBList object, sort it using an overridden sort method (discussed shortly), and then call the PopulateMemo() method of our form. Briefly, the method looks at the type of items the passed list contains. This is shown in Listing 8.17.
10 9721 CH08
11/13/00
480
9:47 AM
Page 480
C++Builder 5 Essentials PART I
LISTING 8.18
Format Output Recursively
void __fastcall TMainForm::PopulateMemo(TDBList *TheList) { TDBList *NextObject; // handles when recursive calls pass a NULL pointer for(int i = 0; TheList && i < TheList->Count; i++) { switch(TheList->ListType) { case ltSession: Tabs = “”; break; case ltAlias: Tabs = “\t”; break; case ltTable: Tabs = “\t\t”; break; case ltField: Tabs = “\t\t\t”; break; } Memo1->Lines->Add(Tabs + TheList->Strings[i]); if(TheList->Objects[i]) { NextObject = dynamic_cast(TheList->Objects[i]); PopulateMemo (NextObject); } } }
This is another example of how good object design can result in simple but powerful code.
Sorting the Lists The TStringList object has a Sort() method that is inherited by the TDBList class. The only problem we have is that if we sort the list of strings we create in our object, it will sort the lists only for that object. What if we want to sort the strings in all the linked lists as well? The approach we took in the design of our TDBList object makes this a very simple task. The Sort() method is declared in our object the same as for TStringList. The overridden method is then implemented as shown in Listing 8.19.
10 9721 CH08
11/13/00
9:47 AM
Page 481
Using VCL Components CHAPTER 8
LISTING 8.19
481
Overriding the TStringList Sort Method
void __fastcall TDBList::Sort(void) { TStringList::Sort(); TDBList *NextList; for(int i = 0; i < Count; i++) { if(Objects[i]) { NextList = dynamic_cast(Objects[i]); NextList->Sort(); // this will continue sorting all levels } } }
Making Improvements If this code was to be used in a real-world application, there would be some minor enhancements required. • Add a parameter to the constructor to indicate if it should auto-populate. If no parameter is given, then auto-populate by default. • Modify the object to create a single TTable for the entire process. Searching back to the master object (via pointers) is a lot faster than creating TTables each time a list of field names is to be enumerated. • The Add(), Clear(), Delete(), and Insert() methods should all be overridden to automatically populate (or delete) a complete linked list of items. Though not discussed here in detail, the enhancements are included in the Full version of the TDBListDemo.bpr project on the CD-ROM. I recommend you refer to the supplied source code to see how simple it is to implement each of the enhancements. What follows is a few short notes about the changes made and why. The overridden Clear() method now ensures that any linked objects are deleted before the list is cleared. This prevents memory leaks.
8 USING VCL COMPONENTS
We start by calling the inherited Sort() method. This causes all the strings in the current object to be sorted. All pointers maintained by the Objects property are moved automatically. We then check each of the Objects properties for a valid pointer. We typecast the Objects property back to a TDBList* and then call the Sort() method for that object. This will continue all the way down to the field level, where it will stop because all the Objects properties are NULL.
10 9721 CH08
11/13/00
482
9:47 AM
Page 482
C++Builder 5 Essentials PART I
The EnumDBList() method now calls Clear() rather than DeleteObjects() because it now also performs the task of DeleteObjects(). DeleteObjects has been modified only to delete any linked objects. It no longer clears the list. This would cause it to remove the list of strings, which we don’t want it to do any more. More importantly, if the call to Clear() was performed, we would end up in a loop between Clear() and DeleteObjects() that would eventually result in a stack overflow. If you want to remove the objects, call DeleteObjects(). If you want it to clear the list of strings and associated objects, call Clear().
The biggest change is to the overridden Add() method. If the user wants to add another string to the list, the list should treat it like all the other ListType strings and find any relational information. In other words, if the user adds another string to the list and the list holds alias names, then the list should find all related tables and field names. This is performed by the Add() method. What is interesting here is that the GetSessionNames(), GetAliasNames(), GetTableNames(), and GetFieldNames() methods all use Add() to build the string list. This is to our advantage, because each time a new entry is made to the list, our class will automatically find all related information. We can now remove all the loops in our GetSessions(), GetAliases(), and GetTables() methods. If we left the loops in place, we would be duplicating the work already performed by the Add() method. After the string is added, we call a new method called CreateObject(). This method is used to assign a new object to the newly added string. The Insert() method is also overridden, the only change being the additional call to CreateObject(). Like the Add() method, this links a new TDBList object to the new string. The Delete() method is overridden to delete any linked objects before deleting the current item. The class has been modified to create a single instance of a TTable object. To find this table, we need to add a property (called MasterTable). This property is responsible for working back through the linked objects until it finds the TTable object that was first instantiated.
Advanced Custom Draw Events Custom draw events are used in the TTreeView, TListView, and TToolBar controls. They allow you to control the painting of the control canvas or its items. By setting the DefaultDraw parameter to false, you can paint the canvas yourself. C++Builder 5 introduced advanced custom draw events that give the programmer more control over the painting process of these components. These new events contain the word Advanced. Let’s look at these components and see what custom draw events each has.
10 9721 CH08
11/13/00
9:47 AM
Page 483
Using VCL Components CHAPTER 8
483
The TTreeView Component This component has four custom draw events, as shown in Table 8.2. TABLE 8.2
Tree View Custom Draw Events
Events
Description
OnCustomDraw
Occurs prior to the control painting. It is used to paint an image on the control canvas. Occurs prior to the control painting. It is used to paint individual items. Similar to the OnCustomDraw event, with the exception that it occurs on all of the painting process. Similar to the OnAdvanced-CustomDraw event but used to paint individual items.
OnCustomDrawItem OnAdvancedCustomDraw OnAdvancedCustomDrawItem
The events listed in Table 8.2 are specific for the TTreeView component, but you will see later that the TListView and TToolButton components have similar events.
1.
cdPrePaint
(pre-painting)
2.
cdPostPaint
3.
cdPreErase
4.
cdPostErase
(post-painting)
(pre-erasing) (post-erasing)
Note that OnCustomDraw and OnCustomDrawItem events occur only before the painting process, while the advanced events occur at all of these stages. The TTreeView OnAdvancedCustomDrawItem event has another advantage over It allows you to enable or disable the painting of the image associated with the current node. This feature is available only for the TTreeView component.
OnCustomDrawItem.
The TListView Component The custom draw events of the TListView component are similar to those of the TTreeView component. TListView also has two more custom draw events: OnCustomDrawSubItem and OnAdvancedCustomDrawSubItem. These two events are used to paint the TListView sub-items, which are displayed when the ViewStyle property of TListView is set to vsReport.
8 USING VCL COMPONENTS
There are four stages for each painting process:
10 9721 CH08
11/13/00
484
9:47 AM
Page 484
C++Builder 5 Essentials PART I
The TToolBar Component Again, this component’s events are similar to those of TTreeView, except that it has OnCustomDrawButton and OnAdvancedCustomDrawButton instead of OnCustomDrawItem and OnAdvancedCustomDrawItem, respectively. Also, the TTreeNode parameter is replaced by the TToolBar parameter.
Advanced Custom Draw Events Example In the CustomDraw folder on the CD-ROM that accompanies this book, you will find a sample project, CustomDraw.bpr, that demonstrates how to use some of the events presented in the previous sections. It demonstrates use of the OnCustomDraw event of TTreeView, drawing a custom bitmap on the tree view canvas, and also controls the drawing of each item in the tree using the OnAdvancedCustomDrawItem event. Examine the code in the Unit1.cpp file to see how it works.
Control Panel Applet Wizard Components In the days of Windows 3.x, Control Panel applets were used mostly to configure hardware devices. Under Windows 95/98/2000, Control Panel applets can be used to configure not only hardware, but software too, or any other application. These applets can configure an application externally, instead of requiring the application to be loaded to change the configuration options from a menu item, for example. You can control the environment or behavior with a Control Panel applet. Then your program runs normally. This enables you to remove the configuration code from the main application and allows the configuration options of multiple applications to be set from one place. C++Builder 5 has a wizard to make Control Panel applet writing easier. You can easily create icons for applets, help files, and forms and parameters for each applet.
The Basics of an Applet An applet is a small utility to control the behavior of a hardware device or an application. Without loading the whole application, an applet can load just the environment settings for you to configure. Then the applet can control the behavior. Applets are generally small and are used only for these purposes. You have seen applets under Control Panel (see Figure 8.4).
10 9721 CH08
11/13/00
9:47 AM
Page 485
Using VCL Components CHAPTER 8
FIGURE 8.4 The Control Panel displays applets under Windows 2000.
Applets can contain other applets. For example, you can create an applet that has two forms configuring two different applications, with two different icons. The System and Add/Remove Hardware applets provided with Windows 95/98 both run from the SYSDM.CPL applet file in the Windows System folder. They perform different functions and are represented by different icons. Although they have different forms and appear to be separate, they are contained in the same applet application. Applets are called by the controlling application. The controlling application sends messages to the applet to inquire about it, and the applet returns values to the controlling application. These messages are then used to control the applet’s behavior. The return values depend on the parameters passed to the applet from the controlling application. Writing an applet must adhere to a certain order because the messages sent to the applet are in a certain order. Table 8.3 shows the order of messages and values.
8 USING VCL COMPONENTS
An applet is a specialized DLL that is renamed with a .cpl extension. The controlling application reads the .cpl file and polls it for information. The DLL has a special entry-point function, CPLApplet(), that is called to run the applet. The controlling application is generally the Control Panel (usually CONTROL.EXE), but any application can handle Control Panel applets. To do this, the application must handle the messages and operation of the applet.
485
10 9721 CH08
11/13/00
486
9:47 AM
Page 486
C++Builder 5 Essentials PART I
TABLE 8.3
The Order and Operation of Applet Messages
Order Message: CPL_INIT (#define constant = 1) Called first. Immediate callout after the CPL file controlling the applet is loaded.
Operation This message is sent to indicate CPlApplet() was found. lParam1 and lParam2 are not used. Returns true or false, indicating whether the Control Panel
should proceed. If it fails, it releases control of the applet and ceases all communication with the CPL file. Message: CPL_GETCOUNT (#define constant = 2) Called after CPL_INIT. It returns This message is sent to determine the a non-zero value. number of applets to be displayed. It returns a non-zero value. Return the number of applets you want to display in the Control Panel window. lParam1 and lParam2 are not used yet. Message: CPL_INQUIRE (#define constant = 3) Called after CPL_GETCOUNT. Return This message is sent for information about value is greater than or equal to 1. The each applet. CPLApplet() provides inforCPLApplet() function will be called mation on a dialog box. The lParam1 paraonce for each dialog box, specifying meter points to the loading dialog. The which dialog box with a zero-based lParam2 parameter points to the structure indexed value. The value is placed CPLINFO. From this structure, CPLApplet() in lParam1. gets information such as name and icon. Message: CPL_NEWINQUIRE (#define constant value = 8) If CPL_INQUIRE is not used, you can use Same operation as CPL_INQUIRE except his. This has the same order and effect lParam2 points to a structure named as CPL_INQUIRE. NEWCPLINFO, signifying new data for the CPL itself. For performance, Win 95/NT uses CPL_INQUIRE. Message: CPL_DBLCLK (#define constant value = 5) Called after the user has double-clicked This message is sent when the applet’s icon on the applet’s icon. has been double-clicked. lParam1 is the applet number that was selected. lParam2 is the applet’s lData value. This message should initiate the applet’s dialog box and will show the form you designed for the applet.
10 9721 CH08
11/13/00
9:47 AM
Page 487
Using VCL Components CHAPTER 8
TABLE 8.3
487
Continued
Order Message: CPL_STOP (#define constant = 6) Called once for each dialog box right before the application closes. It is specified by the lParam1 parameter on its zero-based index value.
Operation CPLApplet() should attempt to free any resources allocated for the dialog box. This message is sent for each applet when the Control Panel is exiting. lParam1 is the applet number. lParam2 is the applet’s lData value. Do all applet-specific cleaning up here.
Message: CPL_EXIT (#define constant = 7) This message is sent once after the last CPL_STOP message is sent from the indexed dialogs. It is called immediately before the controlling application uses the FreeLibrary() to free the CPL containing the applet.
This message is sent just before the Control Panel calls FreeLibrary(). lParam1 and lParam2 are not used. Do non-applet– specific cleaning up here: freeing resources, memory deallocation, and so on.
8
Now we’ll walk through writing a simple applet using C++Builder’s Control Panel Applet Wizard.
NOTE We did not include how to create an icon for this applet in this chapter. We assume you already know how to use the Image Editor to create ICO-type icon files. Because this example requires an ICO-type file, create an icon for it or retrieve one. If you want to do so, you can create an icon now before you proceed.
Let’s begin by starting up C++Builder. When C++Builder is fully loaded, choose File, Close All to close everything in the IDE. Choose File, New and the New Items dialog box will appear. From there, select the Control Panel Application icon, shown in Figure 8.5. Notice that there is also a Control Panel Module icon beside it. This is used to add other modules to a Control Panel applet.
USING VCL COMPONENTS
Don’t let this scare you away. C++Builder handles all the messages within the applet’s events. With C++Builder’s new CPL Wizard, it’s quite easy to write an applet. In Chapter 24, “Using the Win32 API,” we show how Control Panel applets can be written without the CPL Wizard, the Win32 way.
10 9721 CH08
11/13/00
488
9:47 AM
Page 488
C++Builder 5 Essentials PART I
FIGURE 8.5 Starting the Control Panel Applet Wizard in C++Builder.
C++Builder starts the Control Panel Applet Wizard. It begins to create the important information and generate code to create the Control Panel applet. A window will appear that resembles the Data Module Designer, except that this window has two tabs, Components and Data Diagram, which are seen in Figure 8.6. When the code is finally generated, C++Builder generates the TAppletModule class for the Control Panel applet. By pressing F12, you can see that C++Builder has generated this code for you.
FIGURE 8.6 The Module Explorer. This looks like the Data Module Designer but shows modules and components.
This application originates from the CtlPanel unit within C++Builder. It hides most of the hard work of designing a Control Panel applet. The CtlPanel unit contains three important Control Panel routines named EAppletException, TAppletApplication, and TAppletModule. TAppletApplication is the singleton class container to hold the TAppletModule, such as the
10 9721 CH08
11/13/00
9:47 AM
Page 489
Using VCL Components CHAPTER 8
489
one we just generated. You can include more TAppletModules in a project if you want. For example, if you want to add more Control Panel modules to our applet, choose File, New and select Control Panel Module. This generates another TAppletModule and inserts it in the TAppletApplication Control Panel applet project. A global application variable of TAppletApplication is declared in the CtlPanel unit. It represents the application for a Control Panel applet project. When a new Control Panel project is created, C++Builder constructs an application object and assigns it to Application. If the project represents a Control Panel applet, Application is initialized to a TAppletApplication object. As a result, a single TAppletModule contains all the functionality and is the encapsulation of a Control Panel applet.
Properties of TAppletModule There are properties to set within C++Builder to manipulate the applet. The properties can control the help for the applet, the icon of the applet, and the display name of the applet. The properties of the TAppletModule in the Object Inspector are shown in Figure 8.7.
8 USING VCL COMPONENTS
FIGURE 8.7 The Object Inspector, showing the properties of AppletModule1.
The following list describes each property in detail: •
is the property that points to the icon for this Control Panel applet module. If you include another TAppletModule, then you include an icon for that module or applet within the application. The icons that are valid are ICO-type files. When you double-click on the property, C++Builder will invoke the Icon File Finder, with which you must find a file with an ICO extension. When this property is set with a proper icon, the Object Inspector will associate it with the applet under Control Panel. AppletIcon
10 9721 CH08
11/13/00
490
9:47 AM
Page 490
C++Builder 5 Essentials PART I
•
Caption
is the string that will be displayed under the Control Panel applet icon. It usually represents the title of the applet itself.
•
is the string that is displayed in the status bar under Control Panel. It is usually the description of the applet itself.
•
ResidIcon
•
ResidInfo
•
ResidName
Help
is the resource ID of the icon associated with the applet module. This property is mutually exclusive with the AppletIcon property. is the resource ID of the help string associated with the applet module. This property is mutually exclusive with the Help property. is the resource ID of the caption string associated with the applet module. This property is mutually exclusive with the Caption property.
Normally, if you want to create just one Control Panel applet with one module, you can simply edit the AppletIcon, Caption, and Help properties and not bother with the ResidIcon, ResidInfo, and ResidName properties.
Events of TAppletModule The events in TAppletModule are shown in Figure 8.8. However, there is no online help for writing Control Panel applets and very little on the subjects of TAppletApplication and CtlPanel. Fortunately, there is some degree of help within the events of TAppletModule. You can access the online help as you would with any other component, by pressing Ctrl+F1 within the Events property field.
FIGURE 8.8 The Object Inspector, showing the events of AppletModule1.
Most of the events will not be needed for our example. We know that OnCreate and OnDestroy are triggered when the Control Panel applet is created and terminated. These events also are triggered when the Control Panel opens and closes. Remember that the Control Panel still
10 9721 CH08
11/13/00
9:47 AM
Page 491
Using VCL Components CHAPTER 8
491
communicates with the applet event if it isn’t double-clicked. It obtains information such as titles, help, icon association, and the like. OnInquire, OnNewInquire, OnStop,
and OnStartWParams will not be used, but they are for in case you need to obtain information to send our application any messages.
For this example we will be using the OnActivate event, which is triggered when the Application applet is double-clicked under Control Panel. OnActivate has the Sender object argument, which points to the corresponding module that was activated. It also contains a Data argument for Inquire or NewInquire events to handle. Refer to the “Writing Control Panel Applets the Old-Fashioned Way” section in Chapter 24 for more information on the Inquire and NewInquire messages.
GUI Design of the Control Panel Applet So let’s resume our Control Panel applet design. You should still be in C++Builder and ready to go. This will be a simple applet with three buttons. With AppletModule1 selected, double-click the AppletIcon property in the Object Inspector to invoke the Picture Editor dialog. Choose a suitable icon, if you have not created one. You can create one by using the Image Editor and saving it as an .ICO file. Control Panel Applet,
then set the Help property to This
is
Choose File, New Form. An empty form will appear. Set its width and height to a moderate size. You also can resize the form with your mouse, or you can set the form’s Width and Height properties in the Object Inspector. Select a TButton component from the Standard tab and, with your left mouse button, drop it onto the form. Set all the button’s properties as indicated in Table 8.4. TABLE 8.4
Property Settings for the Applet
Component
Property
Value
Button1
Caption Width
Hide Taskbar 100
Button2
Caption Width
Show Taskbar 100
Button3
Caption Width
Close 100
Set Button1’s OnClick event handler by double-clicking the button with your left mouse button and entering the following code:
USING VCL COMPONENTS
Set the Caption property to My an example of my applet.
8
10 9721 CH08
11/13/00
492
9:47 AM
Page 492
C++Builder 5 Essentials PART I void __fastcall TForm2::Button1Click(TObject *Sender) { HANDLE Hide_TaskBar; Hide_TaskBar = FindWindow(“Shell_traywnd”, “”); SetWindowPos(Hide_TaskBar, 0, 0, 0, 0, 0, SWP_HIDEWINDOW); }
Now, set Button2’s OnClick event handler by double-clicking on it with your left mouse button and entering the following code: void __fastcall TForm2::Button2Click(TObject *Sender) { HANDLE Hide_TaskBar; Hide_TaskBar = FindWindow(“Shell_traywnd”, “”); SetWindowPos(Hide_TaskBar, 0, 0, 0, 0, 0, SWP_SHOWWINDOW); }
Finally set Button3’s OnClick event handler by double-clicking it with your mouse button and entering the following code: void __fastcall TForm2::Button3Click(TObject *Sender) { Close(); }
We must now change the way in which the form is created. When we added the form to the project C++Builder added code to automatically create the form. We must remove this code and create the form dynamically when the applet is activated. Firstly, open the Project1.cpp source file in the Code Editor. Remove the following line from the DllEntryPoint() function. Application->CreateForm(__classid(TForm2), &Form2);
Next, display the Module Explorer for AppletModule1 by selecting View, Forms, or by pressing Shift+F12, then select AppletModule1 and click Ok. In the Object Inspector, double-click to the right of the OnActivate event on the Events tab to create an OnActivate event handler. Add the following code to the event handler. void __fastcall TAppletModule1::AppletModuleActivate(TObject *Sender, int Data) { // Create the form dynamically. if (!Form2) { Form2 = new TForm2(this); } // Show the form and wait for the user to finish. Form2->ShowModal(); // Delete the dynamically created form (free the object created). delete Form2; }
10 9721 CH08
11/13/00
9:47 AM
Page 493
Using VCL Components CHAPTER 8
493
The final project is shown in Figure 8.9. Since we are almost done, let’s save our project. From the C++Builder main menu select File, Save All. Save the files in the project to a directory of your choice. To test out Control Panel applets, C++Builder can automatically rename your file and send it to the appropriate Windows system directory. We can test this inside the applet module. Display the Module Explorer and right-click in the applet module to display the context menu. It will contain three options: Install Control Panel Applet, Uninstall Control Panel Applet, and Launch Control Panel (see Figure 8.10). Select Install Control Panel Applet. C++Builder will then compile the applet application and report that the applet has been installed successfully. If so, you can right-click again and select Launch Control Panel. This will bring up the Control Panel. The icon and name of the applet should be there. Double-click on it to test it out. When you test your applet, the dialog box that we designed earlier will appear. Click Close to terminate the applet (see Figure 8.11).
8 USING VCL COMPONENTS
FIGURE 8.9 Our finished project, fully expanded and ready to test.
10 9721 CH08
11/13/00
494
9:47 AM
Page 494
C++Builder 5 Essentials PART I
FIGURE 8.10 Right-clicking within the Module Explorer will display a context menu with options to install or uninstall your applet or launch Control Panel.
FIGURE 8.11 Our applet in action.
Another way to run our Control Panel applet without Control Panel interfering is to issue the following command at the Start, Run menu: rundll32 shell32.dll,Control_RunDLL project1.cpl @0
Where @ indicates which applet dialog you would like to trigger, at a zero-based value. For example, @0 above will trigger the first applet dialog. This is the first form in the applet.
10 9721 CH08
11/13/00
9:47 AM
Page 495
Using VCL Components CHAPTER 8
495
Because our example has only one dialog, you do not have to use the @ symbol. If we have two forms, the following statement will fire the second dialog: rundll32 shell32.dll,Control_RunDLL mycpl.cpl @1
How does this happen? Rundll32.exe calls upon Shell32.dll. Inside the Shell32.dll is a special function to call different applets and parameters. You can try this with other applets in Windows; some will work and some will not. That’s because they do not have multiple modules or applications within the applet. However, this is just another way to operate a Control Panel applet without using Control Panel at all.
Control Panel Applet Summary Writing Control Panel applets can be tedious, because you have to compile the applet, rename the file, send it to the \System folder under the main Windows folder, and test it out. Sometimes Control Panel will not release your applet. C++Builder takes most of the hard work out for you.
Making Use of Third-Party Components
Third-Party Component Advantages and Disadvantages Obtaining components from a third party (another software vendor) has a number of advantages and disadvantages.
Advantages of Third-Party Components • There is no need to re-invent the wheel. If someone else has gone to the trouble of writing a component that does what you need, then you don’t have to worry about the development and debugging of that part of your project. • If you purchase the source code, you can develop the component further and fix any bugs that you discover during testing. • If the components are purchased from a developer, then additional support is sometimes available, usually by email.
8 USING VCL COMPONENTS
C++Builder comes with a large number of prebuilt components. The exact number available depends on the version of C++Builder. The components supplied will be adequate for most developers, but eventually a time will come when you need a component specialized for a given task. When this day comes you will have two choices. The first is to purchase a thirdparty component package (or download a freeware version from the Internet). The second choice is to create your own. This section will look at the first option. Chapters 9, 10, and 11, “More Custom Component Techniques,” will explain the latter.
10 9721 CH08
11/13/00
496
9:47 AM
Page 496
C++Builder 5 Essentials PART I
• If your project does not require that you hand over the source code, you can use the components in future projects, thereby reducing the development time of those projects. • If the component is a wrapper for a complex task that you are unfamiliar with, you can focus on the main functions of the application being developed.
Disadvantages of Third-Party Components • You need to spend time testing the components to be confident they perform exactly as expected and required. • If you don’t have the source code, you have no control over future enhancement of the component. • You have no guarantee the component is not producing memory leaks and other nasty side effects. This is less of a concern if the source code is available, but it still requires time to decipher the methodology of the component and check. • Most developers currently are producing components in Delphi, which means the code is Pascal rather than C++. With time, hopefully this will change. • If the component does not behave correctly or you are having difficulty in its implementation, you are dependent on the original author for future support. If the component was a freeware download from the Internet, this type of support is not always guaranteed. • Components that must be purchased are an additional expense for the project. If this cost is not accounted for at the time of consultation, you have to decide if the purchase can be offset against the time you will save. This won’t be as much of a concern if the components can be used in future projects as well, in which case they will eventually pay for themselves. • If your customer is paying for the project source code as part of an agreement, he will also require the component packages. Ownership (or additional purchase) of the components must therefore be made available at the completion of the project.
Where to Look for More C++Builder Resources The Internet is the largest pool available for locating third-party components. But the Internet is so vast, where do you start? There are a number of major sites. Before you know it you will have more components than you know what to do with. You will also stumble across a plethora of documentation, tutorials, and other pertinent material to help you along the path of C++Builder project development. You can find a comprehensive list of Web sites in Appendix A, “Information Sources.”
10 9721 CH08
11/13/00
9:47 AM
Page 497
Using VCL Components CHAPTER 8
497
Summary C++Builder is a RAD tool based on class objects and reuse. This chapter looked at the use of existing components, with comments on their advantages and disadvantages. Next we looked at an overview of the VCL and how it starts at TObject and branches off to five major branches. These include TPersistent, TComponent, TControl, TGraphicControl, and TWinControl. We then went on to see how C++Builder incorporates RTTI to give the IDE the capability to display the properties and events of components via the Object Inspector. We also acknowledged the Object Inspector’s capability to determine common properties and events for objects sharing the same base objects. We examined the nature of VCL objects in terms of their creation on the heap rather than the stack. We looked at the extensions added to the C++ language to allow the Object Pascal background of the VCL to integrate into the C++ environment of C++Builder.
Each version of C++Builder continuously updates the VCL to take advantage of Microsoft’s COMCTL32.DLL updates. At the same time, Inprise continues to enhance VCL features. In this chapter we looked at the enhancements in the custom draw events of the TTreeView, TListView, and TToolBar controls. Also covered were the new Help Hint, the TApplicationEvents component, and added menu, TIcon, and Registry-access features. Control panel applets provide a standardized way to configure hardware and software. We presented how you can create your own Control Panel applets using C++Builder. To demonstrate the available power of the VCL, we looked at how the TStringList class can be used as a base class for a more powerful object capable of linking additional objects (of the same type) to each of the string items. We also looked at how good object design results in simple code that is powerful to use and simple to maintain. TStringList provides virtual methods that perform tasks such as adding, sorting, and removing strings. We took the Sort method as an example to demonstrate how it can be overridden to perform the sort on all linked objects for each of the strings in the list.
8 USING VCL COMPONENTS
Then we discussed the standard and common controls supported in C++Builder. These controls take advantage of COMCTL32.DLL as provided by Microsoft Windows. The main hurdle for component and class developers is the constant updating required to keep in line with Microsoft’s development work. As a developer, it is imperative to be aware that users can differ widely in their chosen operating systems. Not everyone is going to have the latest version of this DLL, and hence you need to provide the capability to update it if necessary.
10 9721 CH08
11/13/00
498
9:47 AM
Page 498
C++Builder 5 Essentials PART I
C++Builder is a powerful object-based RAD tool that is simple to use once you understand the background of the VCL and how it is related to Object Pascal. With your current knowledge of C++ in combination with the extensions provided in C++Builder, you have the foundation for powerful application development with reduced cost of ownership.
11 9721 CH09
11/13/00
9:55 AM
Page 499
Creating Custom Components Malcolm Smith Sean Rock Jamie Allsop
IN THIS CHAPTER • Why Create Custom Components • Understanding Component Writing • Writing Non-Visual Components • Writing Visual Components • Creating Custom Data-Aware Components • Registering Components
CHAPTER
9
11 9721 CH09
11/13/00
500
9:55 AM
Page 500
C++Builder 5 Essentials PART I
The Visual Component Library (VCL) is an extremely powerful tool, and putting together an application is very easy, using the many stock components, classes, and methods that C++Builder provides. However, in some situations you may find that a component doesn’t quite provide the capabilities you need. The capability to write and modify components gives you a distinct advantage over other programming languages and is one reason why C++Builder is the tool of choice for many programmers around the world. Creating custom components will give you an insight into how the VCL works and increase your productivity with C++Builder. Judging by the number of commercial component sites on the Internet, it can be a profitable exercise as well. In this chapter, you will learn about creating properties, methods, and events; creating dataaware components; linking components together; component exceptions; modifying existing components; responding to messages sent to your component; and drawing your own components on the screen. You will also deal with some aspects of the Windows API, so you should review Chapter 24, “Using the Win32 API,” if you are unfamiliar with API calls. You can also refer to the Win32 help files that ship with C++Builder for more information. Packages are mentioned in this chapter, so if this is a new concept for you, you should review “Understanding and Using Packages” in Chapter 2, “C++Builder Projects and More on the IDE,” and “Using Packages Versus DLLs” in Chapter 15, “DLLs and Plug-Ins.” You can also find more information under “PACKAGE macro” and “Creating packages and DLLs” in the C++Builder online help.
Why Create Custom Components The task of creating components can be quite daunting at first. After reading several articles and tutorials on this topic, it is quite easy to find yourself wondering where to start. The easiest approach is to start with a component that already exists and build upon its features and capabilities. As trivial as this may seem, you might just find yourself customizing or extending a number of the standard VCL components to suit the design and style of your real-world applications. While building a database application, you might drop a TDBGrid onto your form and change several properties to the same value. Similarly, while developing some in-house utilities, you always drop a TStatusBar onto your form, add some panels, and remove the size grip. Instead of doing this for every project, it would make sense to create your own custom component and have these properties set for you automatically. Not only does this make each new application faster to create, but I also have confidence that they are all bug free. Additionally, should I discover a new bug, all I have to do is correct the code in the component and recompile my projects. They will all inherit the changes without any additional reprogramming.
11 9721 CH09
11/13/00
9:55 AM
Page 501
Creating Custom Components CHAPTER 9
501
Understanding Component Writing There are different types of components. Therefore, the ancestor of your own components will be determined by the very nature of that component. Non-visual components are derived from TComponent. TComponent is the minimal descendant that can be used for the creation of a component, because it has the functionality for integration into the IDE and streaming of properties. For a more detailed explanation of streaming, refer to the section “The Streaming Mechanism” in Chapter 8, “Using VCL Components.” A non-visual component is one that is simply a wrapper for other complex code in which there is no visual representation provided to the user. An example is a component that receives error log information and automatically sends it to a linked edit control such as a TMemo or TRichEdit and appends it to a file on disk. The component itself is invisible to the user of the application, but it continues to function in the background, providing the functionality required by the application. Windowed components are derived from TWinControl. These objects appear to the user at runtime and can be interacted with (such as selecting a file from a list). Although it is possible to create your own components from TWinControl, C++Builder provides the TCustomControl component to make this task easier. Graphic components are similar to windowed components, with the main difference being that they don’t have a window handle and therefore do not interact with the user. The absence of a handle also means fewer resources are being consumed. Although these components do not interact with the user, it is possible to have these components react to window messages such as those from mouse events. These components are derived from TGraphicControl.
Why Build Upon an Existing Component?
Take TLabel as an example, of which every project has more than one. If every project you created needed to maintain a particular design style, you could find yourself adding multiple label components and changing their properties to the same values for each new application. By creating a custom component descending from TLabel, you can add several of these new labels to a form and be left with only the task of setting their captions and positions. To demonstrate how easy this can be, we can create a component in about a minute and having to type only three lines of code. From C++Builder’s menu, choose Component, New Component. After the New Component dialog opens, select TLabel for the new component’s
CREATING CUSTOM COMPONENTS
The biggest advantage you will find from building upon existing components is the reduced development time of projects. It is also worthwhile to know that all of your components used in your projects are bug free.
9
11 9721 CH09
11/13/00
502
9:55 AM
Page 502
C++Builder 5 Essentials PART I
Ancestor Type, and for the Class Name, type in TStyleLabel. For a component that you will be installing into C++Builder’s Component Palette and using in applications, you will probably want to choose a more descriptive class name. For this example, you could leave the other options with their default values and simply click the OK button. C++Builder will create the unit files for you; all that is needed is to add the lines of code that will set the label’s properties. Once you’ve made the changes needed, save the file and from C++Builder’s menu choose Component, Install Component. If you have the file open in C++Builder’s IDE, the Unit File Name edit box will reflect the component’s file. Click the OK button to install the component in the Component Palette. Listings 9.1 and 9.2 show the complete code. LISTING 9.1
The TStyleLabel Header File, StyleLabel.h
//--------------------------------------------------------------------------#include <SysUtils.hpp> #include #include #include #include <StdCtrls.hpp> //--------------------------------------------------------------------------class PACKAGE TStyleLabel : public TLabel { private: protected: public: __fastcall TStyleLabel(TComponent* Owner); __published: }; //--------------------------------------------------------------------------#endif
LISTING 9.2
The TStyleLabel Code File, StyleLabel.cpp
//--------------------------------------------------------------------------#include #pragma hdrstop #include “StyleLabel.h” #pragma package(smart_init) //--------------------------------------------------------------------------// ValidCtrCheck is used to assure that the components created do not have // any pure virtual functions. // static inline void ValidCtrCheck(TStyleLabel *)
11 9721 CH09
11/13/00
9:55 AM
Page 503
Creating Custom Components CHAPTER 9
LISTING 9.2
503
Continued
{ new TStyleLabel(NULL); } //--------------------------------------------------------------------------__fastcall TStyleLabel::TStyleLabel(TComponent* Owner) : TLabel(Owner) { Font->Name = “Verdana”; Font->Size = 12; Font->Style = Font->Style << fsBold; } //--------------------------------------------------------------------------namespace Stylelabel { void __fastcall PACKAGE Register() { TComponentClass classes[1] = {__classid(TStyleLabel)}; RegisterComponents(“TestPack”, classes, 0); } } //---------------------------------------------------------------------------
Another advantage to building upon existing components is the capability to create a base class with all the functionality it requires while leaving the properties unpublished. An example of this would be a specific TListBox type of component that doesn’t have the Items property published to the user. By descending this component from TcustomListBox, it is possible to publish the properties you want the user to have access to (at designtime) while making the others available (such as the Items property) only at runtime.
Designing Custom Components Although it might seem trivial, the same rules apply to component design as per-application development when creating a custom component from an existing one. It is important to think about the possible future direction your components might take. The previously mentioned components that provide a list of database information don’t just descend from TListBox. Instead we decided to create a custom version of TCustomListBox that would contain the additional properties common to each descendant we wanted to create. Each new component was then built upon this custom version, eliminating the need for three different versions of the same code. The final version of each component contained nothing more than the code (properties, methods, and events) that made it unique compared to its relatives.
CREATING CUSTOM COMPONENTS
Finally, the properties and events you add to an existing component means writing far less code than if you create the component from scratch.
9
11 9721 CH09
11/13/00
504
9:55 AM
Page 504
C++Builder 5 Essentials PART I
Using the VCL Chart To gain an appreciation for C++Builder’s VCL architecture, take some time to review the VCL chart that ships with the product. This resource gives you a quick visual overview of not only what components are available but also what they are derived from. During your learning phase of component design and creation, you should endeavor to model your own components in this same object-oriented fashion, by creating strong, versatile base classes from which to create custom components. Although the source code for the C++Builder components are written in Pascal, it is a worthwhile exercise to look at each of the base classes for a particular component and see for yourself how they all come together. You will soon observe how components sharing the same properties are all derived from the same base class or a descendant of one. Finally, the chart shows at a glance what base classes are available for your own custom component requirements. In combination with the VCL help files, you can quickly determine the most suitable class from which to derive your components. As mentioned previously, the minimum base class will be TComponent, TWinControl, or TGraphicControl, depending on the type of component you will be creating.
Writing Non-Visual Components The world of components is built upon three main entities: properties, events, and methods. This section looks at each of these, with the aim of giving you a greater understanding of what makes up a component and how components work together to provide the building blocks of your C++Builder applications.
Properties Properties come in two flavors: published and non-published. Published properties are available in the C++Builder Integrated Development Environment (IDE) at designtime (they are also available at runtime). Non-published properties are used at runtime by your application. We will look at non-published properties first.
Non-Published Properties A component is a packaged class with some additional functionality. Take a look at the sample class in Listing 9.3. LISTING 9.3
Getting and Setting Private Variables
class LengthClass { private:
11 9721 CH09
11/13/00
9:55 AM
Page 505
Creating Custom Components CHAPTER 9
LISTING 9.3
505
Continued
int FLength; public: LengthClass(void){} ~LengthClass(void){} int GetLength(void); void SetLength(int pLength); void LengthFunction (void); }
Listing 9.3 shows a private variable used internally by the class and the methods used by the application to read and write its value. This can easily lead to messy code. Take a look at Listing 9.4 for another example. LISTING 9.4
Using Set and Get Methods
LengthClass Rope; Rope.SetLength(15); // do something int NewLength = Rope.GetLength();
The code in Listing 9.4 isn’t complex by any means, but it can quickly become difficult to read in a complex application. Wouldn’t it be better if we could refer to Length as a property of the class? This is what C++Builder allows us to do. In C++Builder, the class could be written as shown in Listing 9.5. LISTING 9.5
Using a Property to Get and Set Private Variables
public: LengthClass2(void){} ~LengthClass2(void){} void LengthFunction(void); __property int Length = {read = FLength, write = FLength}; }
The sample code in Listing 9.4 would be as shown in Listing 9.6.
CREATING CUSTOM COMPONENTS
class LengthClass2 { private: int FLength;
9
11 9721 CH09
11/13/00
506
9:55 AM
Page 506
C++Builder 5 Essentials PART I
LISTING 9.6
Setting and Getting with a Property
LengthClass Rope; Rope.Length = 15; // do something int NewLength = Rope.Length;
The class declaration has now been altered to use a __property (an extension to the C++ language in C++Builder). This property has read and write keywords defined. In Listing 9.6, when you read the Length property, you are returned the value of FLength; when you set the Length property, you are setting the value of FLength. Why go to all this trouble when you could just make the FLength variable public? Properties allow you to do the following: • You can make the Length property read-only by not using the write keyword. • You can provide an application public access to private information of the class without affecting the implementation of the class. This is more relevant when the property value is derived or some action needs to be taken when the value of the property changes. Listing 9.7 shows a slight variation. LISTING 9.7
Combining Set and Get Methods with Properties
class LengthClass3 { private: int FLength; int GetLength(void); void SetLength(int pLength); public: LengthClass3(void){} ~LengthClass3(void){} void LengthFunction(void); __property int Length = {read = GetLength, write = SetLength}; }
The example in Listing 9.7 is starting to show how properties can become quite powerful. The property declaration shows that the value is returned by the GetLength() method when Length is read. When Length needs to be set, the SetLength() method is called. The GetLength() method might perform some calculations based on other private members of the class. The SetLength() method might perform some validation and then continue to perform some additional tasks before finally setting the value of FLength.
11 9721 CH09
11/13/00
9:55 AM
Page 507
Creating Custom Components CHAPTER 9
507
In C++Builder, an example of this is the connection to a database source when the name of an alias is changed. As a developer, you change the name of the alias. In the background, the component is disconnecting from the current database (if there is one) before attempting to connect to the new source. The implementation is hidden from the user, but it is made available by the use of properties.
Types of Properties Properties can be of any type, whether it is a simple data type such as int, bool, short, and so on or a custom class. When using custom classes as property types, there are two considerations. The first is that the class must be derived from TPersistent (at a minimum) if it is to be streamed to the form. The second is that, if you need to forward declare the class, you need to use the __declspec(delphiclass) keyword. The code in Listing 9.8 will compile using typical forward declaration. Note that we haven’t yet defined a property. LISTING 9.8
Forward Declaration
class MyClass; class PACKAGE MyComponent : public TComponent { private: MyClass *FMyClass; // ... };
The PACKAGE keyword between the class name and class keyword is a macro that expands to code that allows the component to be exported from a package library (.BPL—Borland Package Library). A package library is a special kind of DLL that allows code to be shared between applications. For more information about package libraries and the PACKAGE macro, see “PACKAGE macro” and “Creating packages and DLLs” in the C++Builder online help. But if we want to add a property of type MyClass, we need to modify the forward declaration as shown in Listing 9.9.
9 CREATING CUSTOM COMPONENTS
class MyClass : public TPeristent { public: __fastcall MyClass (void){} };
11 9721 CH09
11/13/00
508
9:55 AM
Page 508
C++Builder 5 Essentials PART I
LISTING 9.9
Custom Class Property
class __declspec(delphiclass) MyClass; class PACKAGE MyComponent : public TComponent { private: MyClass *FMyClass; // ... __published: __property MyClass *Class1 = {read = FMyClass, write = FMyClass}; }; class MyClass : public TPeristent { public: __fastcall MyClass (void){} };
Published Properties Publishing properties provides users with access to the properties of the component within the C++Builder IDE at designtime. The properties are displayed in the Object Inspector, allowing the user to see or change the current value of those properties. The properties are also available at runtime, but their main purpose is to provide the user a quick method of setting up the component settings without the need to write a single line of code. Additionally, published properties are streamed to the form, so their values become persistent. This means the values are restored each time the project is opened and when the executable is launched. Published properties are defined the same as all other properties, but they are defined in the __published area of the class declaration. Listing 9.10 shows an example. LISTING 9.10
Publishing a Property
class PACKAGE LengthClass : public TComponent { private: int FLength; int GetLength(void); void SetLength(int pLength); public: __fastcall LengthClass(TObject *Owner) : TComponent(Owner) {} __fastcall ~LengthClass(void){}
11 9721 CH09
11/13/00
9:55 AM
Page 509
Creating Custom Components CHAPTER 9
LISTING 9.10
509
Continued
void LengthFunction(void); __published: __property int Length = {read = Getlength, write = Setlength}; }
The previous class is the same as in Listing 9.9 except that the Length property has been moved to the __published section. Published properties shown in the Object Inspector are readable and writeable, but it is possible to make a property read-only and still visible in the IDE by creating a dummy write method. If we were to add a published property in the above component that shows the current version of the component, it would be done as in Listing 9.11. LISTING 9.11
A Version Property
const int MajorVersion = 1; const int MinorVersion = 0; class PACKAGE LengthClass : public TComponent { private: AnsiString FVersion; int FLength; int GetLength(void); void SetLength(int pLength); void SetVersion(AnsiString /* pVersion */ ) {FVersion = AnsiString(MajorVersion) + “.” + AnsiString(MinorVersion);}
__published: __property int Length = {read = Getlength, write = Setlength}; __property AnsiString Version = {read = FVersion, write = SetVersion}; }
We have defined a private variable FVersion, which has its value set in the class constructor. We have then added the Version property to the __published section and assigned the read and write keywords. The read keyword returns the value of Fversion, and the write method sets the value back to the original value. The variable name in the parameter list of
9 CREATING CUSTOM COMPONENTS
public: __fastcall LengthClass(TObject *Owner) : TComponent(Owner) {SetVersion(“”);} __fastcall ~LengthClass(void){} void LengthFunction(void);
11 9721 CH09
11/13/00
510
9:55 AM
Page 510
C++Builder 5 Essentials PART I
has been commented out to prevent compiler warnings that the variable is declared but not used. Because the property is of type AnsiString, the SetVersion() method by design must have an AnsiString parameter in the declaration.
SetVersion()
Array Properties Some properties are arrays, rather than simple data types such as bool, int, and AnsiString. This is not greatly documented for the user. An example of an array property is the Lines property of the TMemo component. This property allows the user to access the individual lines of the Memo component. Array properties are declared the same as other properties, with two main differences: The declaration includes the appropriate indexes with required types, and these indexes are not limited to being integers. Listings 9.12 through 9.15 illustrate the use of two properties. One takes a string as an index, and the other takes an integer value as an index. LISTING 9.12
Using a String as an Index
class PACKAGE TStringAliasComponent : public TComponent { private: TStringList RealList; TStringList AliasList; __AnsiString __fastcall GetStringAlias(AnsiString RawString); AnsiString __fastcall GetRealString(int Index); void __fastcall SetRealString(int Index, AnsiString Value); public: __property AnsiString AliasString[AnsiString RawString] = {read = GetStringAlias}; __property AnsiString RealString[int Index] = {read=GetRealString, write=SetRealString}; }
The previous example could be part of a component that internally stores a list of strings and another list of alias strings. The AliasString property takes the RawString value and returns the alias via the GetStringAlias() method. The one thing many component writers are confused about when they first start using array properties is that the declaration uses index notation (that is, []), yet in code you use the same notation as calling another method. Look at the RealString property, and notice that not only does it have an AnsiString return type but it also takes an integer as an index. The GetRealString() method would be called when retrieving a particular string from the list based on the index, as in Listing 9.13.
11 9721 CH09
11/13/00
9:55 AM
Page 511
Creating Custom Components CHAPTER 9
LISTING 9.13
511
Array Property Read Method
AnsiString __fastcall TStringAliasComponent::GetRealString(int Index) { if(Index > (RealList->Count –1)) return “”; return RealList->Strings[Index]; }
In code, the property would look like this: AnsiString str = StringAlias1->RealString[0];
Now take a look at the SetRealString() method. If you are unfamiliar with using arrays as properties, this method declaration may look a bit odd. It takes as its first parameter an integer value as its index and an AnsiString value. The RealList TStringList variable will insert the AnsiString in the list at the position specified by the index parameter. Listing 9.14 shows the definition of the SetRealString() method. LISTING 9.14
Array Property Write Method
void __fastcall TStringAliasComponent::SetRealString(int Index, AnsiString Value) { if((RealList->Count – 1) < Index) RealList->Add(Value); else RealList->Insert(Index, Value); }
StringAlias1->RealString[1] = “Some String”;
Now here is the fun part. The GetStringAlias() method is the read method for the AliasString property, which takes a string as an index. You know that the string lists are arrays of strings, so every string has an index, or a position within the list. You can use the IndexOf() method of TStringList to compare the string passed as the index against the strings contained in the list. This method returns an integer value that is the index of the string within the list, or it returns -1 if the string is not present. Now all you have to do is return the string with the index returned from the call to IndexOf() from the list of aliases. This is demonstrated in Listing 9.15.
9 CREATING CUSTOM COMPONENTS
In Listing 9.14, the value of the Index parameter is checked against the number of strings already in the list. If Index is greater, then the string specified by Value is simply added to the end of the list. Otherwise, the Insert() method of TStringList is called to insert the string at the position specified by Index. Now you can assign a string to the list like this:
11 9721 CH09
11/13/00
512
9:55 AM
Page 512
C++Builder 5 Essentials PART I
LISTING 9.15
The GetStringAlias() Method
AnsiString __fastcall TStringAliasComponent::GetStringAlias( AnsiString RawString) { int Index; Index = RealList->IndexOf(RawString); if((Index == -1) || (Index > (AliasList->Count-1))) return RawString; return AliasList->Strings [Index]; }
To use the property, you would do something like this: AnsiString MyAliasString = StringAlias1->AliasString(“The Raw String”);
Beyond Read and Write The code examples in Listing 9.5 thru 9.15 have shown properties using read and write keywords as part of the declaration. C++Builder also provides three more options: default, nodefault, and stored. The default keyword does not set the default value for the property. Instead, it tells C++Builder what default value will be assigned to this property (by the developer) in the component constructor. The IDE then uses this information to determine whether the value of the property needs to be streamed to the form. If the property is assigned a value equivalent to the default, then the value of this property will not be saved as part of the form. For example __property int IntegerProperty = {read = Finteger, write = Finteger, default = 10};
The nodefault keyword tells the IDE that this property has no default value associated with it. When a property is declared for the first time, there is no need to include the nodefault keyword, because the absence of a default means there is no default. The nodefault keyword is mainly used when you need to change the definition of the inherited property. For example __property int DescendantInteger = {read = Finteger, write = Finteger, nodefault};
Be aware that the value of a property with the nodefault keyword in its declaration will be streamed only if a value is assigned to the property or underlying member variable, either in one of its methods or via the Object Inspector. The stored keyword is used to control the storing of properties. All published properties are stored by default. You can change this behavior by setting the stored keyword to true or
11 9721 CH09
11/13/00
9:55 AM
Page 513
Creating Custom Components CHAPTER 9
513
or by giving the name of a function that returns a Boolean result. The code in Listing 9.16 shows an example of the stored keyword in use.
false
LISTING 9.16
Using the stored Keyword
class PACKAGE LengthClass : public TComponent { protected: int FProp; bool StoreProperty(void); __published: __property __property __property stored = }
int AlwaysStore = {read = FProp, write = FProp, stored = true}; int NeverStore = {read = FProp, write = FProp, stored = false}; int SimetimesStore = {read = FProp, write = FProp, StoreProperty};
Order of Creation If your component has properties that depend on the values of other properties during the streaming phase, you can control the order in which they load (and hence initialize) by declaring them in the required order in the class header. For example, the code in Listing 9.17 loads the properties in the order PropA, PropB, PropC. LISTING 9.17
Property Dependencies
public: __property int PropA = {read = FPropA, write = FPropA}; __property bool PropB = {read = FPropB, write = SetPropB}; __property String PropC = {read = FPropC, write = SetPropC}; }
If you have properties with dependencies and are having trouble getting them to initialize correctly, ensure that the order of the property declarations in the class is correct.
9 CREATING CUSTOM COMPONENTS
class PACKAGE SampleComponent : public TComponent { private: int FPropA; bool FPropB; String FProC; void __fastcall SetPropB(bool pPropB); void __fastcall SetPropC(String pPropC);
11 9721 CH09
11/13/00
514
9:55 AM
Page 514
C++Builder 5 Essentials PART I
Events An event in a component is the call of an optional method in response to another incident. The incident could be a hook for the user to perform a task before the component continues, the catching of an exception, or the trapping of a Windows message. As a simple example, let’s assume we have a component that traverses directories from a given root location. If this component were designed to notify the user when the current directory has changed, this would be referred to as an event. When the event occurs, the component determines if the user has provided an event handler (a method attached to the event) and calls the respective method. If this all sounds confusing, take a look at Listing 9.18. LISTING 9.18
Declaring an Event Property
class PACKAGE TTraverseDir : public TComponent { private: AnsiString FCurrentDir; TNotifyEvent *FOnDirChanged; public: __fastcall TTraverseDir(TObject *Owner) : TComponent(Owner){ FOnDirChanged = 0;} __fastcall ~TTraverseDir(void){} __fastcall Execute(); __published: __property AnsiString CurrentDir = {read = FCurrentDir}; __property TNotifyEvent OnDirChanged = {read = FOnDirChanged, write = FOnDirChanged}; }
Listing 9.18 shows the relevant sections of code to describe the declaration of a read-only property and a standard event. When this component is executed, there will be instances when the current directory is changed. Let’s have a look at some example code: void __fastcall TTraverseDir::Execute(void) { // perform the traversing of a directory // This is where the directory has changed, // call the DirChanged event if there is one. if(FOnDirChanged)
11 9721 CH09
11/13/00
9:55 AM
Page 515
Creating Custom Components CHAPTER 9
515
FOnDirChanged(this); // remainder of component code here }
The variable FOnDirChanged in the previous example is a pointer to a TNotifyEvent, which is declared as typedef void __fastcall (__closure *TNotifyEvent)(System::TObject* Sender)
As you can see, the declaration indicates that a single parameter of type TObject* is expected. When the event is created (by double-clicking the event in the Object Inspector), the IDE creates the following code: void __fastcall TTraverseDir::Traverse1DirChanged(TObject *Sender) { }
Within this code, the user can now add his own code to be performed when this event is called. In this case, the event is a standard event that simply passes a pointer of the object that generated the event. This pointer allows you to distinguish between multiple components of the same type within the project. void __fastcall TTraverseDir::Traverse1DirChanged(TObject *Sender) { if(Sender == Traverse1) // perform this code for the component called Traverse1 else // handle the alternative here }
How Do We Create an Event That Contains Additional Parameters?
9
You will recall that the standard event is defined as shown in the following code:
The following code shows how to define a custom event: typedef void __fastcall (__closure *TDirChangedEvent)(System::TObject* Sender, bool &Abort)
In the previous code we have done two things: • Created a unique typedef. TNotifyEvent is now TDirChangedEvent. • Added the required parameters to the parameter list. We can now modify our class declaration. The changes are shown in Listing 9.19.
CREATING CUSTOM COMPONENTS
typedef void __fastcall (__closure *TNotifyEvent)(System::TObject* Sender)
11 9721 CH09
11/13/00
516
9:55 AM
Page 516
C++Builder 5 Essentials PART I
LISTING 9.19
Custom Event Properties
typedef void __fastcall (__closure *TDirChangedEvent)( System::TObject* Sender, bool &Abort) class PACKAGE TTraverseDir : public TComponent { private: TDirChangedEvent *FOnDirChanged; __published: __property TDirChangedEvent OnDirChanged = {read = FOnDirChanged, write = FOnDirChanged}; }
Now when the user creates the event, the IDE will add the following code: void __fastcall TTraverseDir::Traverse1DirChanged(TObject *Sender, bool &Abort) { }
There is only one more change to make, as shown in Listing 9.20: the source code that calls the event. LISTING 9.20
Calling the Event
void __fastcall TTraverseDir::Execute(void) { // perform the traversing of a directory bool &Abort = false; // This is where the directory has changed, // call the DirChanged event if there is one. if(FOnDirChanged) FOnDirChanged(this, Abort); if(Abort) // handle the abort process // remainder of component code here }
The component has been sufficiently modified to allow the user to abort the process if required.
11 9721 CH09
11/13/00
9:55 AM
Page 517
Creating Custom Components CHAPTER 9
517
Methods Methods of a component are supporting routines developed to carry out the various tasks required. They are no different from the methods defined for a typical class. In writing components, the goal is to minimize the number of methods the application needs to call. There are some simple rules to follow when designing your components: • The user must not be required to call any methods to make the component behave the way he expects. For example, the component must take care of all initializations. • There must be no dependencies on the order in which given methods must be called. You must design your component to allow for any combination of events to take place. For example, if a user calls a routine that is state dependent (such as trying to query a database when there is no active connection), then the component must handle the situation. Whether the component should attempt to connect or should throw an exception is up to the developer, based on the component’s function. • The user must not be able to call a method that would change the state of a component while it is performing another task. The best way to handle these situations is to write your methods so that they check the current component state. If all of the requirements are not valid, the component should attempt to correct the problem. Design your components to throw an exception if the component state cannot be corrected. If possible, create custom exceptions so that the user can check for these specific exception types. This is explained further in the next section. Try to create properties rather than methods. Properties enable you to hide the implementation from the user and hence make the component more intuitive to use. As an example, a database component has a property called Active and equivalent Open() and Close() methods. The following two lines are equivalent:
Likewise, so are these: Database1->Active = false; Database1->Close();
In my opinion, the Active property is what should be available to the user. It could be argued that Open() and Close() methods better describe what the component is doing, but I don’t think there is a need for them when they can be represented by a single property, Active. Methods you write for components will typically be public or protected. Private methods should only be written if they are very specific for that component, to the point that even derived components should not call them.
CREATING CUSTOM COMPONENTS
Database1->Active = true Database1->Open();
9
11 9721 CH09
11/13/00
518
9:55 AM
Page 518
C++Builder 5 Essentials PART I
Public Methods Public methods are those that the user needs to make the component perform as required. It is important to make sure that the methods are efficient, so that they don’t tie up the operating system for too long. If this is unavoidable, consider creating an event (or a callback function) that can be used by the developer to inform the user of any processing activity taking place. Providing for the user to abort the processing (by either passing a reference to the event or using another property) is another possibility. Imagine a component that searches a tree of directories for a given file. Depending on the system being searched, this could take a great deal of processing time. Rather than leaving the user wondering if the application has ceased functioning, it is better to create an event that is called within the method. This event can then provide feedback, such as the current directory being traversed.
Protected Methods If your components have methods that must not be called by the application developer but need to be called from derived components, these methods are declared as protected. This ensures that the method is not called at the wrong time. It is safer to create public methods for the user that call protected methods when all requirements are established first. When a method is created for the implementation of properties, it should be declared as a virtual protected method. This allows descendant components to enhance or replace the implementation used. An example of a virtual protected method is the Loaded() method of components. When a component is completely loaded (streamed from the form), the Loaded() method is called. In some cases, a descendant component needs to know when the component is loaded after all properties have been read, so that it can perform some additional tasks. An example is a component that performs validation in a property setter but cannot perform the validation until all properties have been read. In such a case, create a private variable called IsLoaded and set this to false in the constructor. (Although this would be done by default, doing it this way makes the code more readable.) Then overload the Loaded() method and set IsLoaded to true. This variable can then be used in the property-implementation methods to perform validation as required. Listings 9.21 and 9.22 are from the custom TAliasComboBox component. TAliasComboBox is part of the free MJFPack package, which can be downloaded from http://www.mjfreelancing.com. The package contains other components that can be linked together in this fashion.
11 9721 CH09
11/13/00
9:55 AM
Page 519
Creating Custom Components CHAPTER 9
LISTING 9.21
519
The TAliasComboBox Header File
class PACKAGE TAliasComboBox : public TSmartComboBox { private: bool IsLoaded; protected: virtual void __fastcall Loaded(void); }
LISTING 9.22
The TAliasComboBox Source File
void __fastcall TAliasComboBox: :Loaded(void) { TComponent::Loaded(); if(!ComponentState.Contains(csDesigning)) { IsLoaded = true; GetAliases(); } }
Creating Component Exceptions Sometimes it is possible to rethrow an exception that you have caught in your component, which allows the user to deal with the situation. You have more than likely performed a number of steps in your component that need to be cleaned up when an exception occurs. After you have performed the cleanup process, you need to do one of two things. First, you can rethrow the exception. This would be the standard approach for an error such as Divide By Zero. However, there are situations in which it would be better to convert the exception into an event. This provides very clean handling methods for your users. Don’t make
9 CREATING CUSTOM COMPONENTS
In this code, you can see that the Loaded() method has been overloaded in the class declaration. In the .CPP file, start by calling the ancestor Loaded() method and then your additional code. Listing 9.22 shows the component verifying that it is not in design mode before it retrieves available alias information. Because the state of certain properties may depend on other properties, additional methods for this component check the IsLoaded variable before performing any processing that may require the value of those properties to be set. Essentially, most of the processing by this component is performed only at runtime.
11 9721 CH09
11/13/00
520
9:55 AM
Page 520
C++Builder 5 Essentials PART I
the mistake of converting all exceptions to events, because this can sometimes make it harder for your users to develop their applications. An example might help to make this clearer. Imagine a component performing a number of sequential database queries. This component would be made up of a TStrings property that contains all the queries and an Execute() method that performs them. How does the user want to use this component? Something such as the following would be the most desirable. MultiQuery->Queries->Assign(Memo1->Lines); MultiQuery1->Execute();
This is very simple code for the user to implement, but what about a possible exception? Should the user be required to handle any exceptions himself? This might not be the best approach during one of the queries. A better approach would be to build an event that is called when an exception occurs. Within the event, the user should have the opportunity to abort the process. Let’s create a custom exception that will be called if the user attempts to execute an individual query when it is outside the available index range. For the moment, assume there is another method called ExecuteItem() that takes an index to the list of available queries. First, we need to create the exception in the header file. This is as simple as creating a new exception class derived from the Exception class, as shown in Listing 9.23. LISTING 9.23
A Custom Exception Class
class EMultiQueryIndexOutOfBounds : public Exception { public: __fastcall EMultiQueryIndexOutOfBounds(const AnsiString Msg) : Exception(Msg){} };
That’s it. Now if the user tries to execute a query (by index) and the index provided is outside the available range, we can throw our unique exception. The code for throwing this exception would be that in Listing 9.24. LISTING 9.24
Throwing the Custom Exception
void __fastcall TMultiQuery::ExecuteItem(int Index) { if(Index < 0 || Index > Queries->Count) throw EmultiQueryIndexOutOfBounds;
11 9721 CH09
11/13/00
9:55 AM
Page 521
Creating Custom Components CHAPTER 9
LISTING 9.24
521
Continued
// ... perform the query here }
As you can see from Listings 9.23 and 9.24, a custom exception is very easy to create and implement. If this component is to perform the query at designtime, you need to provide the user with a message (rather than have an exception thrown within the IDE). You should modify the code as shown in Listing 9.25. LISTING 9.25
Throwing an Exception at Designtime
void __fastcall TMultiQuery::ExecuteItem(int Index) { if(Index < 0 || Index > Queries->Count) { if(ComponentState.Contains(csDesigning)) throw EmultiQueryIndexOutOfBounds(“The Query index is out of range”); else throw EmultiQueryIndexOutOfBounds; } // ... perform the query here }
The namespace As you develop your components and name them, there might be other developers who by coincidence use the same names. This will cause conflicts when using both components in the same project. This is overcome with the namespace keyword.
LISTING 9.26
namespace Code
namespace Aliascombobox { void __fastcall PACKAGE Register() { TComponentClass classes[1] = {__classid(TAliasComboBox)}; RegisterComponents(“MJF Pack”, classes, 0); } }
CREATING CUSTOM COMPONENTS
When a component is created using the New Component Wizard, the IDE creates code similar to that shown in Listing 9.26.
9
11 9721 CH09
11/13/00
522
9:55 AM
Page 522
C++Builder 5 Essentials PART I
The namespace keyword ensures that the component is created in its own subsystem. Let’s look at a case where namespace needs to be used even further within a package. Suppose that two developers build a clock component and they both happen to create a const variable to indicate the default time mode. If both clocks are used in an application, the compiler will complain because of the duplication. // From the first developer const bool Mode12; // 12 hour mode by default class PACKAGE TClock1 : public TComponent { } // From the second developer const bool Mode12; // 12 hour mode by default class PACKAGE TClock2 : public TComponent { }
As you can see, it is important to develop your component packages with this possibility in mind. To get around this issue, use the namespace keyword. After all the #include statements in your header file, surround the code as shown in Listing 9.27. LISTING 9.27
Surrounding Your Code
namespace NClock1 { class PACKAGE TClock1 : public TComponent { } }
Develop a convention for all your components. For example, you could start your namespace identifiers with a capital N followed by the component name. If it is possible that the same name already has been used, come up with something unique, such as prefixing with your company’s initials. Using namespaces in this fashion ensures that your packages will integrate smoothly with others.
Responding to Messages The VCL does a fantastic job of handling almost all of the window messages you will ever require. There are times, however, when there will be a need to respond to an additional message to further enhance your project. An example of such a requirement is to support filename drag-and-drop from Windows Explorer onto a string Grid component. We can create such a component, called
11 9721 CH09
11/13/00
9:55 AM
Page 523
Creating Custom Components CHAPTER 9 TsuperStringGrid,
523
that is nothing more than a descendant of TStringGrid with some addi-
tional functionality. The drag-and-drop operation is handled by the API message WM_DROPFILES. The information needed to carry out the operation is stored in the TWMDropFiles structure. The interception of window messages in components is the same as for other areas of your projects. The only difference is that we are working with a component and not with the form of a project. Hence, we set up a message map as shown in Listing 9.28. LISTING 9.28
Trapping Messages
BEGIN_MESSAGE_MAP MESSAGE_HANDLER(WM_DROPFILES, TWMDropFiles, WmDropFiles) END_MESSAGE_MAP(TStringGrid)
NOTE There are no trailing semicolons used in declaring the message map. This is because BEGIN_MESSAGE_MAP, MESSAGE_HANDLER, and END_MESSAGE_MAP are macros that expand to code during compilation. The macros contain the necessary semicolons.
The code in Listing 9.28 creates a message map for the component (note TStringGrid in the END_MESSAGE_MAP macro). The message handler will pass all intercepts of the WM_DROPFILES messages to the WmDropFiles() method (which will be created shortly). The information is passed to this method in the TWMDropFiles structure as defined by Windows.
protected: void __fastcall WmDropFiles(TWMDropFiles &Message);
You’ll notice we have provided a reference to the required structure as a parameter of the method. Before this component will work, we need to register the component with Windows, telling it the string grid is allowed to accept the dropped filenames. This is performed when the component is loaded via the DragAcceptFiles() command. DragAcceptFiles(Handle, FCanDropFiles);
In the previous code, the FCanDropFiles variable is used by the component to indicate whether it is allowed to accept the filenames as part of a drag-and-drop operation.
9 CREATING CUSTOM COMPONENTS
Now we need to create the method that will handle the message. In the protected section of the component we define the method as shown in the following code:
11 9721 CH09
11/13/00
524
9:55 AM
Page 524
C++Builder 5 Essentials PART I
Now the method accepts the filenames when the component intercepts the Windows message. The code in Listing 9.29 is stripped slightly from the full version. LISTING 9.29
Accepting Dropped Files
void __fastcall TSuperStringGrid::WmDropFiles(TWMDropFiles &Message) { char buff[MAX_PATH]; HDROP hDrop = (HDROP)Message.Drop; POINT Point; int NumFiles = DragQueryFile(hDrop, -1, NULL, NULL); TStringList *DFiles = new TStringList; DFiles->Clear(); DragQueryPoint(hDrop, &Point); for(int i = 0; i < NumFiles; i++) { DragQueryFile(hDrop, i, buff, sizeof(buff)); DFiles->Add(buff); } DragFinish(hDrop); // do what you want with the list of files now stored in DFiles delete DFiles; }
An explanation of this code is beyond the scope of this chapter. The help files supplied with C++Builder provide a good overview of what each function performs. As you can see, intercepting messages is not hard once you understand how to set them up, although some understanding of the Windows API is required. Refer to the messages.hpp file for a list of the message structures available.
Designtime Versus Runtime We’ve already made some references to the operation of a component at designtime compared to runtime. Designtime operation refers to how the component behaves while the user is creating the project in the IDE. Runtime operation refers to what the component does when the application is executed. The TComponent object has a property (a Set) called ComponentState that is made up of the following constants: csAncestor, csDesigning, csDesignInstance, csDestroying, csFixups, csFreeNotification, csInline, csLoading, csReading, csWriting, and csUpdating. Table 9.1 lists these ComponentState flags and gives the purpose of each.
11 9721 CH09
11/13/00
9:55 AM
Page 525
Creating Custom Components CHAPTER 9
TABLE 9.1
525
The ComponentState Flags
Purpose
csAncestor
Indicates that the component was introduced in an ancestor form. Set only if csDesigning is also set. Set or cleared in the TComponent::SetAncestor() method. Indicates that the component is being manipulated at designtime. Used to distinguish designtime and runtime manipulation. Set or cleared in the TComponent::SetDesigning() method. Indicates that the component is the root object in a designer. For example, it is set for a frame when you are designing it, but not on a frame that acts like a component. This flag always appears with csDesigning. Set or cleared in the TComponent::SetDesignInstance() method. New in C++Builder 5. Indicates that the component is being destroyed. Set in the TComponent::Destroying() method. Indicates that the component is linked to a component in another form that has not yet been loaded. This flag is cleared when all pending fixups are resolved. Cleared in the GlobalFixupReferences() global function. Indicates that the component has sent a notification to other forms that it is being destroyed but has not yet been destroyed. Set in the TComponent::FreeNotification() method. New in C++Builder 5. Indicates that the component is a top-level component that can be modified at designtime and also embedded in a form. This flag is used to identify nested frames while loading and saving. Set or cleared in the component’s SetInline() method. Also set in the TReader::ReadComponent() method. New in C++Builder 5. Indicates that a filer object is currently loading the component. This flag is set when the component is first created and not cleared until the component and all its children are fully loaded (when the Loaded() method is called). Set in the TReader::ReadComponent() and TReader::ReadRootComponent() methods. Cleared in the TComponent::Loaded() method. (For more information on filer objects, see “TFiler” in the C++Builder online help index.)
csDesigning
csDesignInstance
csDestroying csFixups
csFreeNotification
csInline
csLoading
9 CREATING CUSTOM COMPONENTS
Flag
11 9721 CH09
11/13/00
526
9:55 AM
Page 526
C++Builder 5 Essentials PART I
TABLE 9.1
Continued
Flag
Purpose
csReading
Indicates that the component is reading its property values from a stream. Note that the csLoading flag is always set when csReading is set. That is, csReading is set for the period of time that a component is reading in property values when the component is loading. Set and cleared in the TReader::ReadComponent() and TReader::ReadRootComponent() methods. Indicates that the component is writing its property values to a stream. Set and cleared in the TWriter::WriteComponent() method. Indicates that the component is being updated to reflect changes in an ancestor form. Set only if csAncestor is also set. Set in the TComponent::Updating() method and cleared in the TComponent::Updated() method.
csWriting
csUpdating
The Set member we are most interested in is csDesigning. As long as the component exists in the IDE (as part of a developing project), the component will contain this constant as part of the Set to indicate that it is being used at designtime. To determine if a component is being used at designtime, use the following code: if(ComponentState.Contains(csDesigning)) // carry out the designtime code here else // carry out the runtime code here
Why would you need to run certain code at runtime only? There are many instances when this is required, such as the following: • To specifically validate a property that has dependencies available only at runtime • To display a warning message to the user if he sets an inappropriate property value • To display a selection dialog or a property editor if an invalid property value is given Many component writers don’t go to the trouble of providing the user with these types of warnings and dialogs. However, it is these extra features that make a component more intuitive and user friendly.
Linking Components Linking components refers to giving a component the capability to reference or alter another component in the same project. An example in C++Builder is the TDriveComboBox component. This component has a property called DirList that allows the developer to select a
11 9721 CH09
11/13/00
9:55 AM
Page 527
Creating Custom Components CHAPTER 9
527
TDirectoryListBox component available on the same form. This type of link gives the developer a quick and easy method to update the directory listing automatically every time the selected drive is changed. Creating a project to display a list of directories and filenames doesn’t get any easier than dropping three components (TDriveComboBox, TdirectoryListBox, and TFileListBox) onto a form and setting two properties. Of course, you still need to assign code to the event handlers to actually make the project perform something useful, but up to that point there isn’t a single line of code to be written.
Providing a link to other components starts by creating a property of the required type. If you create a property of type TLabel, the Object Inspector will show all available components on the form that are of type TLabel. To show how this works for descendant components, we are going to create a simple component that can link to a TMemo or a TRichEdit component. To do this, you need to realize that both components descend from TCustomMemo. Let’s start by creating a component descending from TComponent that has a property called LinkedEdit (see Listing 9.30). LISTING 9.30
Linked Components
class PACKAGE TMsgLog : public TComponent { private: TCustomMemo *FLinkedEdit; ➥// can be TMemo or TRichEdit or any other derived component public: __fastcall TMsgLog(TComponent* Owner); __fastcall ~TMsgLog(void); void __fastcall OutputMsg(const AnsiString Message);
__published: __property TCustomMemo *LinkedEdit = {read = FLinkedEdit, write = FLinkedEdit}; };
The code in Listing 9.30 creates the component with a single property, called LinkedEdit. There are two more things to take care of. First, we need to output the messages to the linked Memo or RichEdit component (if there is one). We also need to take care of the possibility that the user might delete the linked edit control. The OutputMsg() method is used to pass the text
CREATING CUSTOM COMPONENTS
protected: virtual void __fastcall Notification(TComponent *AComponent, TOperation Operation);
9
11 9721 CH09
11/13/00
528
9:55 AM
Page 528
C++Builder 5 Essentials PART I
message to the linked edit control, and the Notification() method is used to detect if it has been deleted. The following provides the output: void __fastcall TMsgLog::OutputMsg(const AnsiString Message) { if(FLinkedEdit) FLinkedEdit->Lines->Add(Message); }
Because both TMemo and TRichEdit components have a Lines property, there is no need to perform any casting. If you need to perform a task that is component specific (or handled differently), use the code shown in Listing 9.31. LISTING 9.31
The OutputMsg() Method
void __fastcall TMsgLog::OutputMsg(const AnsiString Message) { TMemo *LinkedMemo = 0; TRichEdit *LinkedRichEdit = 0; LinkedMemo = dynamic_cast(FLinkedEdit); LinkedRichEdit = dynamic_cast(FLinkedEdit); if(FLinkedMemo) FLinkedMemo->Lines->Add(Message); else { FLinkedRichEdit->Font->Color = clRed; FLinkedRichEdit->Lines->Add(Message); } }
The final check is to detect the linked edit control being deleted. This is done by overloading the Notification() method of TComponent as shown in Listing 9.32. LISTING 9.32
The Notification() Method
void __fastcall TMsgLog::Notification(TComponent *AComponent, TOperation Operation) { // We don’t care about controls being added. if(Operation != opRemove) return ; // We have to check each one in case the user did something
11 9721 CH09
11/13/00
9:55 AM
Page 529
Creating Custom Components CHAPTER 9
LISTING 9.32
529
Continued
// like have the same label attached to multiple properties. if(AComponent == FLinkedEdit) FLinkedEdit = 0; }
The code in Listing 9.32 shows how to handle code resulting from another component being deleted. The first two lines are to show the purpose of the Operation parameter. The most important code is the last two lines, which compare the pointer AComponent to the LinkedEdit property (a pointer to a component descending from TCustomMemo). If the pointers match, we NULL the LinkedEdit pointer. This removes the reference from the Object Inspector and ensures that our code is no longer pointing to a memory address that is about to be lost (when the edit component is actually deleted). Note that LinkedEdit = 0 is the same as LinkedEdit = NULL. One final point is that if you link your component to another that has dependencies (such as TDBDataSet descendants that require a database connection), it is up to you to ensure that these dependencies are checked and handled appropriately. Good component design is recognized when the user has the least amount of work to do in order to get the component to behave as expected.
Linking Events Between Components We’ve looked at how components can be linked together via properties. Our discussion so far has been of how a property of TMsgLog can be linked to another component so that messaging can be provided automatically without the user having to write the associated code.
Component events can be implemented differently according to the nature of the event itself. If the component is looping through a process, then the code might simply have a call to execute the event handler if one exists. Take a look at the following example: // start of loop if(FOnExit) FOnExit(this); endif; // ... // end of loop
9 CREATING CUSTOM COMPONENTS
What we are going to look at now is how to link events between components. Continuing with the previous examples, we’re going to show how we intercept the OnExit event for the linked edit control (note that TMemo and TRichEdit both have an OnExit event and are of type TNotifyEvent) so that we can perform some additional processing after the user’s code has executed. Let’s assume the linked edit control is not read-only. This means the user could enter something into the log; this change needs to be recorded as a user-edited entry. We will demonstrate how to perform the intercept and leave the functionality up to you.
11 9721 CH09
11/13/00
530
9:55 AM
Page 530
C++Builder 5 Essentials PART I
Other events could result from a message. Listing 9.26 showed the message map macro for accepting files dropped onto a control from Windows Explorer as follows: BEGIN_MESSAGE_MAP MESSAGE_HANDLER(WM_DROPFILES, TWMDropFiles, WmDropFiles) END_MESSAGE_MAP(TStringGrid)
If our component has an OnDrop event, we can write our implementation as shown in the following code: void __fastcall TSuperStringGrid::WmDropFiles(TWMDropFiles &Message) { if(FOnDrop) FOnDrop(this); endif; // ... remainder of code here }
What you should have noticed by now is that the components maintain a pointer to the event handler, such as FOnExit and FOnDrop in the previous example. This makes it very easy to create our own pointer to note where the user’s handler resides and then redirect the user’s event so that it calls an internal method instead. This internal method will execute the user’s original code, followed by the component’s code (or vice versa). The only other consideration to make is when you redirect the pointers. The logical place to do this is in the component’s Loaded() method. This is called when the entire component is streamed from the form, and hence all of the user’s event handlers have been assigned. Define the Loaded() method and a pointer to a standard event in your class. (The event is the same type as the one we are going to intercept; in our case it is the OnExit event, which is of type TNotifyEvent.) We also need an internal method with the same declaration as the event we are intercepting. In our class we create a method called MsgLogOnExit. This is the method that will be called before the OnExit event of the linked edit control. In Listing 9.33, we include a typedef of type TComponent called Inherited. The reason will become obvious when we get to the source code. LISTING 9.33
The TMsgLog Class Header File
class PACKAGE TMsgLog : public TComponent { typedef TComponent Inherited; private: TNotifyEvent *FonUsersExit; void __fastcall MsgLogOnExit(TObject *Sender);
11 9721 CH09
11/13/00
9:55 AM
Page 531
Creating Custom Components CHAPTER 9
LISTING 9.33
531
Continued
protected: virtual void __fastcall Loaded(void); // ... remainder of code not shown }
In the source code, you might have something such as Listing 9.34. LISTING 9.34
The TMsgLog Class Source File
void __fastcall TMsgLog::TMsgLog(TComponent *Owner) { FOnUsersExit = 0; } void __fastcall TMsgLog::Loaded(void) { Inherited::Loaded(); if(!ComponentState.Contains(csDesigning)) { if(FlinkedEdit) { if(FlinkedEdit->OnExit) FOnUsersExit = FlinkedEdit->OnExit; FlinkedEdit->OnExit = MsgLogOnExit; } }
9
}
// ... and now perform the additional code we want to do }
When the component is first created, the constructor initializes FOnUsersExit to NULL. When the form is completely streamed, the component’s OnLoaded event is called. This starts by calling the inherited method first (the typedef simply helps to make the code easy to read). Next we make sure the component is not in design mode. If the application is in runtime mode, we see if the component has a linked edit control. If so, we find out if the user has assigned a
CREATING CUSTOM COMPONENTS
void __fastcall TMsgLog::MsgLogOnExit(TObject *Sender) { if(FOnUsersExit) FOnUsersExit(this);
11 9721 CH09
11/13/00
532
9:55 AM
Page 532
C++Builder 5 Essentials PART I
method to the OnExit event of that control. If these tests are true, we set our internal pointer FOnUsersExit to the address of the user’s event handler. Finally, we reassign the edit control’s event handler to our internal method MsgLogOnExit(). This results in the MsgLogOnExit() method being called every time the cursor exits the edit control, even if the user did not assign an event handler. The MsgLogOnExit() method starts by determining if the user assigned an event handler; if so, it is executed. We then continue to perform the additional processing tasks we want to implement. The decision to call the user’s event before or after our own code is executed depends on the nature of the event, such as data encryption or validation.
Writing Visual Components As you’ve seen, components can be any part of a program that the developer can interact with. Components can be non-visual (TOpenDialog or TTable) or visual (TListBox or TButton). The most obvious difference between them is that visual components have the same visual characteristics during designtime as they do during runtime. As the properties of the component that determine its visual appearance are changed in the Object Inspector, the component must be redrawn or repainted to reflect those changes. Windowed controls are wrappers for Windows Common Controls, and Windows will take care of redrawing the control more often than not. In some situations, such as with a component that is not related to any existing control, redrawing the component is up to you. In either case, it is helpful to know some of the useful classes that C++Builder provides for drawing onscreen.
Where to Begin One of the most important considerations when writing components is the parent class from which to inherit. You should review the help files and the VCL source code if you have it. This is time well spent; there is nothing more frustrating than having worked on a component for hours or days just to discover that it doesn’t have the capabilities you need. If you are writing a windowed component (one that can receive input focus and has a window handle), derive it from TCustomControl or TWinControl. If your component is purely graphical, such as a TSpeedButton, then derive from TGraphicControl. There are very few if any limitations when it comes to writing visual components, and there is a wealth of freeware and shareware components and source code on the Internet from which to get ideas.
TCanvas The TCanvas object is C++Builder’s wrapper for the Device Context. It encapsulates various tools for drawing complex shapes and graphics onscreen. TCanvas can be accessed through the Canvas property of most components, although some windowed controls are drawn by
11 9721 CH09
11/13/00
9:55 AM
Page 533
Creating Custom Components CHAPTER 9
533
Windows and so do not provide a Canvas property. There are ways around this, and we’ll discuss them shortly. TCanvas also provides several methods to draw lines, shapes, and complex graphics onscreen. Listing 9.35 is an example of how to draw a line diagonally from the upper-left corner to the bottom-right corner of the canvas. The LineTo() method draws a line from the current pen position to the coordinates specified in the X and Y variables. First set the start position of the line by calling the MoveTo() method. LISTING 9.35
Drawing a Line Using MoveTo()
Canvas->MoveTo(0, 0); int X = ClientRect.Right; int Y = ClientRect.Bottom; Canvas->LineTo (X, Y);
Listing 9.36 uses the Frame3D() method to draw a frame around a canvas, giving the control a button appearance. LISTING 9.36
Creating a Button Appearance
int PenWidth = 2; TColor Top = clBtnHighlight; TColor Bottom = clBtnShadow; Frame3D(Canvas, ClientRect, Top, Bottom, PenWidth);
NOTE HDC is the data type returned by the call to GetDC(). It is simply the handle of the DeviceContext and is synonymous with the Handle property of TCanvas.
HDC dc = GetDC(SomeComponent->Handle);
Listing 9.37 uses a form with TPaintBox (we’ll use TPaintBox because its Canvas property is published) and calls the RoundRect() API to draw an ellipse within the TPaintBox. The TPaintBox can be placed anywhere on the form. The code would be placed in the OnPaint
9 CREATING CUSTOM COMPONENTS
It is also very common to use API drawing routines with the TCanvas object to accomplish certain effects. Some API drawing methods use the DeviceContext of the control, although it isn’t always necessary to get the HDC of the control to call an API that requires it. To get the HDC of a control, use the GetDC() API.
11 9721 CH09
11/13/00
534
9:55 AM
Page 534
C++Builder 5 Essentials PART I
event handler for the TPaintBox. The full project can be found in the PaintBox1 folder on the CD-ROM that accompanies this book. The project filename is Project1.bpr. LISTING 9.37
Using API Drawing Methods
void __fastcall TForm1::PaintBox1Paint(TObject *Sender) { // We’ll use a TRect structure to save on typing TRect Rect; int nLeftRect, nTopRect, nRightRect, nBottomRect, nWidth, nHeight; Rect = PaintBox1->ClientRect; nLeftRect = Rect.Left; nTopRect = Rect.Top; nRightRect = Rect.Right; nBottomRect = Rect.Bottom; nWidth = Rect.Right - Rect.Left; nHeight = Rect.Bottom - Rect.Top; if(RoundRect( PaintBox1->Canvas->Handle, // handle of device context nLeftRect, // x-coord. of bounding rect’s upper-left corner nTopRect, // y-coord. of bounding rect’s upper-left corner nRightRect, // x-coord. of bounding rect’s lower-right corner nBottomRect, // y-coord. of bounding rect’s lower-right corner nWidth, // width of ellipse used to draw rounded corners nHeight // height of ellipse used to draw rounded corners ) == 0) ShowMessage(“RoundRect failed...”); }
Try changing the values of the nWidth and nHeight variables. Start with zero; the rectangle will have sharp corners. As you increase the value of these two variables, the corners of the rectangle will become more rounded. This method and other similar drawing routines can be used to create buttons or other controls that are rounded or elliptical. Some examples will be shown later. See “Painting and Drawing Functions” in the Win32 help files that ship with C++Builder for more information.
Using Graphics in Components Graphics are becoming more commonplace in components. Some familiar examples are TSpeedButton and TBitButton, and there are several freeware, shareware, and commercial components available that use graphics of some sort. Graphics add more visual appeal to components and, fortunately, C++Builder provides several classes to handle bitmaps, icons, JPEGs,
11 9721 CH09
11/13/00
9:55 AM
Page 535
Creating Custom Components CHAPTER 9
535
and GIFs. The norm for drawing components is to use an off-screen bitmap to do the drawing and then copy the bitmap to the onscreen canvas. This reduces screen flicker because the canvas is painted only once. This is very useful if the image you are working with contains complex shapes or images. The TBitmap class has a Canvas property, which is a TCanvas object and thus allows you to draw shapes and graphics off the screen. The following example uses a form with a TPaintBox component. A TBitmap object is created and used to draw an image similar to a TSpeedButton with its Flat property set to true. The TBitmap is then copied to the screen in one action. In this example we add a TButton, which will change the appearance of the image from raised to lowered. The full project can be found in the PaintBox2 folder on the CD-ROM that accompanies this book. The project filename is Project1.bpr. First, take a look at the header file in Listing 9.38. LISTING 9.38
Creating a Raised or Lowered Appearance
class TForm1 : public TForm { __published: // IDE-managed Components TPaintBox *PaintBox1; TButton *Button1; private: // User declarations bool IsUp; public: // User declarations __fastcall TForm1(TComponent* Owner); };
The OnClick event of the button is quite simple. It changes the value of the IsUp variable, changes the Caption property of the button based on the new value, and calls the TPaintBox’s Repaint() method to redraw the image. This is shown Listing 9.39. LISTING 9.39
The Button1Click() Method
void __fastcall TForm1::Button1Click(TObject *Sender) { IsUp = !IsUp; Button1->Caption = (IsUp) ? “Down” : “Up”; PaintBox1->Repaint (); }
9 CREATING CUSTOM COMPONENTS
We declare a Boolean variable IsUp, which we’ll use to swap the highlight and shadow colors and to change the caption of the button. If IsUp is true, the image is in its up state; if the value of IsUp is false, the image is in its down state. Because IsUp is a member variable, it will be initialized to false when the form is created. The Caption property of Button1 can be set to Up via the Object Inspector.
11 9721 CH09
11/13/00
536
9:55 AM
Page 536
C++Builder 5 Essentials PART I
A private method, SwapColors(), is declared and will change the highlight and shadow colors based on the value of the IsUp variable. This is shown in Listing 9.40. LISTING 9.40
The SwapColors() Method
void __fastcall TForm1::SwapColors(TColor &Top, TColor &Bottom) { Top = (IsUp) ? clBtnHighlight : clBtnShadow; Bottom = (IsUp) ? clBtnShadow : clBtnHighlight; }
The final step is to create an event handler for the OnPaint event of the TPaintBox. This is shown in Listing 9.41. LISTING 9.41
Painting the Button
void __fastcall TForm1::PaintBox1Paint(TObject *Sender) { TColor TopColor, BottomColor; TRect Rect; Rect = PaintBox1->ClientRect; Graphics::TBitmap *bit = new Graphics::TBitmap; bit->Width = PaintBox1->Width; bit->Height = PaintBox1->Height; bit->Canvas->Brush->Color = clBtnFace; bit->Canvas->FillRect(Rect); SwapColors(TopColor, BottomColor); Frame3D(bit->Canvas, Rect, TopColor, BottomColor, 2); PaintBox1->Canvas->Draw(0, 0, bit); delete bit; }
Listing 9.42 will go one step further and demonstrate how to use bitmap files as well as drawing lines on a canvas. Most button components, for example, contain not only lines and borders that give it shape, but also icons, bitmaps, and text. This can become a bit more complicated because it requires a second TBitmap to load the graphics file, the position of the graphic must be calculated and copied to the first bitmap, and the final result must be copied to the onscreen canvas. The full project can be found in the PaintBox3 folder on the CD-ROM that accompanies this book. The project filename is Project1.bpr.
11 9721 CH09
11/13/00
9:55 AM
Page 537
Creating Custom Components CHAPTER 9
LISTING 9.42
537
Using Bitmaps and Lines
void __fastcall TForm1::PaintBox1Paint(TObject *Sender) { TColor TopColor, BottomColor; TRect Rect, gRect; Rect = PaintBox1->ClientRect; Graphics::TBitmap *bit = new Graphics::TBitmap; Graphics::TBitmap *bitFile = new Graphics::TBitmap; bitFile->LoadFromFile(“geom1b.bmp”); // size the off-screen bitmap to size of on-screen canvas bit->Width = PaintBox1->Width; bit->Height = PaintBox1->Height; // fill the canvas with the brush’s color bit->Canvas->Brush->Color = clBtnFace; bit->Canvas->FillRect(Rect); // position the second TRect structure centered h/v within Rect gRect.Left = ((Rect.Right - Rect.Left) / 2) - (bitFile->Width / 2); gRect.Top = ((Rect.Bottom - Rect.Top) / 2) - (bitFile->Height / 2); // move the inner rect up and over by 1 pixel to give the appearance of // the panel moving up and down gRect.Top += (IsUp) ? 0 : 1; gRect.Left += (IsUp) ? 0 : 1; gRect.Right = bitFile->Width + gRect.Left;; gRect.Bottom = bitFile->Height + gRect.Top;
// draw the borders SwapColors(TopColor, BottomColor); Frame3D(bit->Canvas, Rect, TopColor, BottomColor, 2); // copy the off-screen bitmap to the on-screen canvas BitBlt(PaintBox1->Canvas->Handle, 0, 0, PaintBox1->ClientWidth, PaintBox1->ClientHeight, bit->Canvas->Handle, 0, 0, SRCCOPY); delete bitFile; delete bit; }
CREATING CUSTOM COMPONENTS
// copy the bitmap to the off-screen bitmap object using transparency bit->Canvas->BrushCopy(gRect, bitFile, TRect(0,0,bitFile->Width, bitFile->Height), bitFile->TransparentColor);
9
11 9721 CH09
11/13/00
538
9:55 AM
Page 538
C++Builder 5 Essentials PART I
Responding to Mouse Messages Graphical components are normally derived from TGraphicControl, which provides a canvas to draw on and handles WM_PAINT messages. Remember that non-windowed components do not need to receive input focus and do not have or need a window handle. Although these types of components cannot receive input focus, the VCL provides custom messages for mouse events that can be trapped. For example, when the Flat property of a TSpeedButton is set to true, the button pops up to show its borders when the user moves the mouse cursor over it, and it returns to a flat appearance when the mouse is moved away from the button. This effect is accomplished by responding to two messages—CM_MOUSEENTER and CM_MOUSELEAVE, respectively. These messages are shown in Listing 9.43. LISTING 9.43
The CM_MOUSEENTER and CM_MOUSELEAVE Messages
void __fastcall CMMouseEnter(TMessage &Msg); void __fastcall CMMouseLeave(TMessage &Msg);
// CM_MOUSEENTER // CM_MOUSELEAVE
BEGIN_MESSAGE_MAP MESSAGE_HANDLER(CM_MOUSEENTER, TMessage, CMMouseEnter) MESSAGE_HANDLER(CM_MOUSELEAVE, TMessage, CMMouseLeave) END_MESSAGE_MAP(TBaseComponentName)
Another important message to consider is the CM_ENABLEDCHANGED message. The Enabled property of TGraphicControl is declared as public, and the setter method simply sends the control the CM_ENABLECHANGED message so that the necessary action can be taken, such as showing text or graphics as grayed or not firing an event. If you want to give your component the capability to be enabled or disabled, you would redeclare this property as published in your component’s header file and declare the method and message handler. Without it, users will still be able to assign a true or false value to the Enabled property at runtime, but it will have no effect. The CM_ENABLECHANGED message is shown in Listing 9.44. LISTING 9.44
The CM_ENABLEDCHANGED Message
void __fastcall CMEnabledChanged(TMessage &Msg); __published: __property Enabled ; BEGIN_MESSAGE_MAP MESSAGE_HANDLER(CM_ENABLEDCHANGED, TMessage, CMEnabledChanged) END_MESSAGE_MAP(TYourComponentName)
11 9721 CH09
11/13/00
9:55 AM
Page 539
Creating Custom Components CHAPTER 9
539
Other mouse events such as OnMouseUp, OnMouseDown, and OnMouseOver are conveniently declared in the protected section of Tcontrol, so all that is necessary is to override the methods to which you want to respond. If you want derivatives of your component to have the capability to override these events, remember to declare them in the protected section of the component’s header file. This is shown in Listing 9.45. LISTING 9.45
Overriding TControl’s Mouse Events
private: TMmouseEvent FOnMouseUp; TMouseEvent FOnMouseDown; TMouseMoveEvent FOnMouseMove; protected: void __fastcall MouseDown(TMouseButton Button, TShiftState Shift, int X, int Y); void __fastcall MouseMove(TshiftState Shift, int X, int Y); void __fastcall MouseUp(TMouseButton Button, TShiftState Shift, int X, int Y); __published: __property TMouseEvent OnMouseUp = {read=FOnMouseUp, write=FOnMouseUp}; __property TMouseEvent OnMouseDown = {read=FOnMouseDown, write=FOnMouseDown}; __property TMouseMoveEvent OnMouseMove = {read=FOnMouseMove, write=FOnMouseMove};
These messages and others are defined in messages.hpp. If you have the VCL source code, take time to find out which messages or events are available and which methods can be overridden.
Putting It All Together This section will cover putting all the above techniques into a basic component that you can expand and enhance. This component is not complete, although it could be installed onto C++Builder’s Component Palette and used in an application. As a component writer, you should never leave things to chance; the easier your component is to use, the more likely it will
9 CREATING CUSTOM COMPONENTS
In the example projects shown previously, an event handler was created for the OnPaint() event of TPaintBox. This event is fired when the control receives the WM_PAINT message. TGraphicControl traps this message and provides a virtual Paint() method that can be overridden in descendant components to draw the control onscreen or, as TPaintBox does, provide an OnPaint() event.
11 9721 CH09
11/13/00
540
9:55 AM
Page 540
C++Builder 5 Essentials PART I
be used. The example shown in Listings 9.46 and 9.47 will be a type of Button component that responds like a TSpeedButton and has a bitmap and text. The source code is shown in Listings 9.46 and 9.47, and then we’ll look at some of the obvious enhancements that could be made. The source code is also provided in the ExampleButton folder on the CD-ROM that accompanies this book. LISTING 9.46
The TExampleButton Header File, ExampleButton.h
//--------------------------------------------------------------------------#ifndef ExampleButtonH #define ExampleButtonH //--------------------------------------------------------------------------#include <SysUtils.hpp> #include #include #include //--------------------------------------------------------------------------enum TExButtonState {esUp, esDown, esFlat, esDisabled}; class PACKAGE TExampleButton : public TGraphicControl { private: Graphics::TBitmap *FGlyph; AnsiString FCaption; TImageList *FImage; TExButtonState FState; bool FMouseInControl; TNotifyEvent FOnClick; void __fastcall SetGlyph(Graphics::TBitmap *Value); void __fastcall SetCaption(AnsiString Value); void __fastcall BeforeDestruction(void); void __fastcall SwapColors(TColor &Top, TColor &Bottom); void __fastcall CalcGlyphLayout(TRect &r); void __fastcall CalcTextLayout(TRect &r); MESSAGE void __fastcall CMMouseEnter(TMessage &Msg); MESSAGE void __fastcall CMMouseLeave(TMessage &Msg); MESSAGE void __fastcall CMEnabledChanged(TMessage &Msg); protected: void __fastcall Paint(void); void __fastcall MouseDown(TMouseButton Button, TShiftState Shift, int X, int Y); void __fastcall MouseUp(TMouseButton Button, TShiftState Shift, int X, int Y);
11 9721 CH09
11/13/00
9:55 AM
Page 541
Creating Custom Components CHAPTER 9
LISTING 9.46
541
Continued
public: __fastcall TExampleButton(TComponent* Owner); __published: __property AnsiString Caption = {read=FCaption, write=SetCaption}; __property Graphics::TBitmap * Glyph = {read=FGlyph, write=SetGlyph}; __property TNotifyEvent OnClick = {read=FOnClick, write=FOnClick}; BEGIN_MESSAGE_MAP MESSAGE_HANDLER(CM_MOUSEENTER, TMessage, CMMouseEnter) MESSAGE_HANDLER(CM_MOUSELEAVE, TMessage, CMMouseLeave) MESSAGE_HANDLER(CM_ENABLEDCHANGED, TMessage, CMEnabledChanged) END_MESSAGE_MAP(TGraphicControl) }; //--------------------------------------------------------------------------#endif
LISTING 9.47
The TExampleButton Source File, ExampleButton.cpp
//--------------------------------------------------------------------------#include #pragma hdrstop
static inline void ValidCtrCheck(TExampleButton *) { new TExampleButton(NULL); } //--------------------------------------------------------------------------__fastcall TExampleButton::TExampleButton(TComponent* Owner) : TGraphicControl(Owner) { SetBounds(0,0,50,50); ControlStyle = ControlStyle << csReplicatable; FState = esFlat; }
9 CREATING CUSTOM COMPONENTS
#include “ExampleButton.h” #pragma package(smart_init) //--------------------------------------------------------------------------// ValidCtrCheck is used to assure that the components created do not have // any pure virtual functions. //
11 9721 CH09
11/13/00
542
9:55 AM
Page 542
C++Builder 5 Essentials PART I
LISTING 9.47
Continued
//--------------------------------------------------------------------------namespace Examplebutton { void __fastcall PACKAGE Register() { TComponentClass classes[1] = {__classid(TExampleButton)}; RegisterComponents(“Samples”, classes, 0); } } // --------------------------------------------------------------------------void __fastcall TExampleButton::CMMouseEnter(TMessage &Msg) { if(Enabled) { FState = esUp; FMouseInControl = true; Invalidate(); } } // --------------------------------------------------------------------------void __fastcall TExampleButton::CMMouseLeave(TMessage &Msg) { if(Enabled) { FState = esFlat; FMouseInControl = false; Invalidate(); } } // --------------------------------------------------------------------------void __fastcall TExampleButton::CMEnabledChanged(TMessage &Msg) { FState = (Enabled) ? esFlat : esDisabled; Invalidate(); } // --------------------------------------------------------------------------void __fastcall TExampleButton::MouseDown(TMouseButton Button, TShiftState Shift, int X, int Y) { if(Button == mbLeft) { if(Enabled && FMouseInControl) {
11 9721 CH09
11/13/00
9:55 AM
Page 543
Creating Custom Components CHAPTER 9
LISTING 9.47
543
Continued
FState = esDown; Invalidate(); } } } // --------------------------------------------------------------------------void __fastcall TExampleButton::MouseUp(TMouseButton Button, TShiftState Shift, int X, int Y) { if(Button == mbLeft) { if(Enabled && FMouseInControl) { FState = esUp; Invalidate(); if(FOnClick) FOnClick(this); } } } // --------------------------------------------------------------------------void __fastcall TExampleButton::SetGlyph(Graphics::TBitmap * Value) { if(Value == NULL) return; if(!FGlyph) FGlyph = new Graphics::TBitmap; FGlyph->Assign(Value); Invalidate();
// --------------------------------------------------------------------------void __fastcall TExampleButton::SetCaption(AnsiString Value) { FCaption = Value; Invalidate(); } // --------------------------------------------------------------------------void __fastcall TExampleButton::SwapColors(TColor &Top, TColor &Bottom) { if(ComponentState.Contains(csDesigning)) { FState = esUp; }
CREATING CUSTOM COMPONENTS
}
9
11 9721 CH09
11/13/00
544
9:55 AM
Page 544
C++Builder 5 Essentials PART I
LISTING 9.47
Continued
Top = (FState == esUp) ? clBtnHighlight : clBtnShadow; Bottom = (FState == esDown) ? clBtnHighlight : clBtnShadow; } // --------------------------------------------------------------------------void __fastcall TExampleButton::BeforeDestruction(void) { if(FImage) delete FImage; if(FGlyph) delete FGlyph; } // --------------------------------------------------------------------------void __fastcall TExampleButton::Paint(void) { TRect cRect, tRect, gRect; TColor TopColor, BottomColor; Canvas->Brush->Color = clBtnFace; Canvas->FillRect(ClientRect); cRect = ClientRect; Graphics::TBitmap *bit = new Graphics::TBitmap; bit->Width = ClientWidth; bit->Height = ClientHeight; bit->Canvas->Brush->Color = clBtnFace; bit->Canvas->FillRect(cRect); if(FGlyph) if(!FGlyph->Empty) { CalcGlyphLayout(gRect); bit->Canvas->BrushCopy(gRect, FGlyph, Rect(0,0,FGlyph->Width,FGlyph->Height), FGlyph->TransparentColor); } if(!FCaption.IsEmpty()) { CalcTextLayout(tRect); bit->Canvas->TextRect(tRect, tRect.Left,tRect.Top, FCaption); } if(FState == esUp || FState == esDown) { SwapColors(TopColor, BottomColor); Frame3D(bit->Canvas, cRect, TopColor, BottomColor, 1); }
11 9721 CH09
11/13/00
9:55 AM
Page 545
Creating Custom Components CHAPTER 9
LISTING 9.47
545
Continued
BitBlt(Canvas->Handle, 0, 0, ClientWidth, ClientHeight, bit->Canvas->Handle, 0, 0, SRCCOPY); delete bit; } // --------------------------------------------------------------------------void __fastcall TExampleButton::CalcGlyphLayout(TRect &r) { int TotalHeight=0; int TextHeight=0; if(!FCaption.IsEmpty()) TextHeight = Canvas->TextHeight(FCaption); // the added 5 could be a ‘Spacing’ property but for simplicity we just // added the 5. TotalHeight = FGlyph->Height + TextHeight + 5; r = Rect((ClientWidth/2)-(FGlyph->Width/2), ((ClientHeight/2)-(TotalHeight/2)), FGlyph->Width + (ClientWidth/2)-(FGlyph->Width/2), FGlyph->Height + ((ClientHeight/2)-(TotalHeight/2))); } // --------------------------------------------------------------------------void __fastcall TExampleButton::CalcTextLayout(TRect &r) { int TotalHeight=0; int TextHeight=0; int TextWidth=0; TRect temp;
TextHeight = Canvas->TextHeight(FCaption); TextWidth = Canvas->TextWidth(FCaption); TotalHeight += TextHeight + 5; temp.Left = 0; temp.Top = (ClientHeight/2)-(TotalHeight/2); temp.Bottom = temp.Top + TotalHeight; temp.Right = ClientWidth; r = Rect(((ClientWidth/2) - (TextWidth/2)), temp.Bottom-TextHeight, ((ClientWidth/2)-(TextWidth/2))+TextWidth, temp.Bottom); }
CREATING CUSTOM COMPONENTS
if(FGlyph) TotalHeight = FGlyph->Height;
9
11 9721 CH09
11/13/00
546
9:55 AM
Page 546
C++Builder 5 Essentials PART I
Here only the OnClick event is published. In a real component you would more than likely publish the OnMouseUp, OnMouseDown, and OnMouseMove events as well. The two properties Caption and Glyph are published, but you should have a Font property to allow users to change the font of the caption. It would probably be a good idea to catch the CM_FONTCHANGED message so that the positions of the button glyph and caption can be redrawn accordingly. In calculating the position of the image and the text, we use a value of 5 pixels as the spacing between the two. It would also be a good idea to create a property that allows the user to specify this value. In Listing 9.47, take a look at the write method for the Glyph property, SetGlyph(). If a NULL pointer is assigned to the Glyph property, the method simply returns without doing anything. This might seem like typical behavior for this type of property, but once you have assigned an image there is no way to get rid of it. In other words, you cannot show only the caption without deleting the component and creating a new one. The last thing we will look at is the Boolean FMouseInControl variable. Because the control is responding to mouse events, it is wise to keep track of it. This variable is used to track whether the mouse cursor is over the control. Without this variable in place, certain member functions would be called inappropriately, because the component will still receive mouse events even though the action did not begin over the control. For example, if a user clicked and held the mouse button and then moved the mouse over the component and released the button, the CMMouseUp() method would be called without the component knowing that the mouse is actually over the control. This in effect would cause the component to redraw itself in its Up state and would not redraw unless you moved the mouse away and then back again or clicked the button. The FMouseInControl variable prevents this. In Listing 9.47, the shape of the button is drawn using the Frame3D() method. By including the header file in your source file, you can gain access to another method for drawing button shapes, DrawButtonFace(), shown in Listing 9.48. Buttons.hpp
LISTING 9.48
The DrawButtonFace() Method
TRect DrawButtonFace(TCanvas *Canvas, const TRect Client, int BevelWidth, TButtonStyle Style, bool IsRounded, bool IsDown, bool IsFocused);
The DrawButtonFace() method will draw a button shape the size of Client on the Canvas specified. Some of the parameters are effective according to the value of the others. For example, the BevelWidth and IsRounded parameters seem to have an effect only when Style is bsWin31. IsFocused has no apparent effect.
11 9721 CH09
11/13/00
9:55 AM
Page 547
Creating Custom Components CHAPTER 9
547
The DrawButtonFace() method uses the DrawEdge() API (see the Win32 online help included with C++Builder). This can also be used in your own drawing routines.
Modifying Windowed Components As stated previously, windowed components are wrappers for the standard Windows controls. These components already know how to draw themselves, so you don’t have to worry about that. In modifying windowed controls, you most likely will want to change what the component actually does rather than its look. Fortunately, the VCL provides protected methods in these components that can be overridden to do just that. In this last example we’ll use some of the techniques shown in this chapter to create a more familiar and robust replacement for the TFileListBox component that comes standard with C++Builder. Before we write any code, it’s a good idea to get an overview of what we want to accomplish. Remember, we want to make this component as easy to use as possible and relieve the user from the task of writing code that is common when using a component that lists filenames. The following lists the changes we’ll make in our component: • Display the correct icon for each file. • Give the user the capability to launch an application or open a document when the item is double-clicked. • Allow the user the option to add a particular item to the list box. • Allow an item to be selected when right-clicked. • Show a horizontal scrollbar when an item is longer than the width of the list box. • Maintain compatibility with TDirectoryListBox.
Next we’ll consider the changes we want to make to this component and declare some new properties, methods, and events. First we want to allow the user to launch an application or open a document by double-clicking an item. We can declare a Boolean property that will allow the user to turn this option on or off as shown in the following code. __property bool CanLaunch = {read=FCanLaunch, write=FCanLaunch, default=true};
9 CREATING CUSTOM COMPONENTS
Now that we know what our component should do, we must decide from which base class to derive it. As stated previously, C++Builder provides custom classes from which to derive new components. This is not the case as is in our component. TDirectoryListBox and TFileListBox are linked together through the FileList property of TDirectoryListBox. This property is declared as a pointer to a TFileListBox, so a component derived from TCustomListBox or TListBox will not be visible to the property. To maintain compatibility with TDirectoryListBox, we will have to derive our component from TFileListBox. Fortunately, the methods it uses to read the filenames are protected, so all we have to do is override them in our new component.
11 9721 CH09
11/13/00
548
9:55 AM
Page 548
C++Builder 5 Essentials PART I
When a user double-clicks on the list box, it is sent a WM_LBUTTONDBLCLK message. TCustomListBox conveniently provides a protected method that is called in response to this message. Listing 9.49 shows how we can override the DblClick() method to launch an application or open a document. LISTING 9.49
The DblClick() Method
void __fastcall TSHFileListBox::DblClick(void) { if(FCanLaunch) { int ii=0; // go through the list and find which item is selected for(ii=0; ii < Items->Count; ii++) { if(Selected[ii]) { AnsiString str = Items->Strings[ii]; ShellExecute(Handle, “open”, str.c_str(), 0, 0, SW_SHOWDEFAULT); } } } // fire the OnDblClick event if(FOnDblClick) FOnDblClick(this); }
It the FCanLaunch variable is true, we must first find which item is selected and then use the ShellExecute() API to launch the application. This method also fires an OnDblClick event, which is declared as shown in the following code. private: TNotifyEvent FOnDblClick; __published: __property TNotifyEvent OnDblClick = {read=FOnDblClick, write=FOnDblClick};
The OnDblClick event does not really need to provide any information, so we can declare it as a TNotifyEvent variable. This behavior can certainly be changed if the need arises, but for now this will suffice. Now let’s tackle the problem of allowing an item to be selected when right-clicked. First we need to declare a new property as shown in the following code. __property bool RightBtnClick = {read=FRightBtnSelect, write=FRightBtnSelect, default=true};
11 9721 CH09
11/13/00
9:55 AM
Page 549
Creating Custom Components CHAPTER 9
549
Notice that the property is referencing a member variable, and there is no read or write method. This is because we’ll use the member variable in the MouseUp() event to determine whether or not to select the item. Listing 9.50 shows the code for the MouseUp() method. LISTING 9.50
The MouseUp() Method
//---------------------------------------------------------------------------void __fastcall TSHFileListBox::MouseUp(TMouseButton Button, TShiftState Shift, int X, int Y) { if(!FRightBtnSel) return; TPoint ItemPos = Point(X,Y); // is there an item under the mouse ? int Index = ItemAtPos(ItemPos, true); // if not just return if(Index == -1) return; // else select the item Perform(LB_SETCURSEL, (WPARAM)Index, 0); }
Next we want to allow the user the option to add a particular item. TFileListBox has a Mask property that allows you to specify file extensions that can be added to the list box. It is possible to redeclare the Mask property and provide a read and write method that filters the filenames according to the value of the Mask property. I took the easy way out and chose to provide an event that allows the user to apply his own algorithm for filtering the filenames. You could keep this event in place and still cater to the Mask property to provide even more functionality.
9 CREATING CUSTOM COMPONENTS
The code in Listing 9.50 is fairly simple. First we check the FRightBtnSel variable to see if we can select the item. Next we need to convert the mouse coordinates to a TPoint structure. To find which item is under the mouse, we use the ItemAtPos() method of TcustomListBox, which takes the TPoint structure we created and a Boolean value that determines if the return value should be -1 or one more than the last item in the list box if the coordinate contained in the TPoint structure is beyond the last item. Here the parameter is true and, if the return value is -1, the method simply returns. You could change this value to false and remove the if() statement that checks the return value. Finally, we use the Perform() method to force the control to act as if it has received a window message. The first parameter to the Perform() method is the actual message we want to simulate. LB_SETCURSEL tells the list box that the selection has been changed by the mouse. The second parameter is the index of the item we want to select; the third parameter is not used, so it is zero.
11 9721 CH09
11/13/00
550
9:55 AM
Page 550
C++Builder 5 Essentials PART I
First let’s declare our new event. typedef void __fastcall (__closure *TAddItemEvent)(TObject *Sender, AnsiString Item, bool &CanAdd);
As you can see, the event provides three parameters. Sender is the list box, Item is an AnsiString value that contains the filename, and CanAdd is a Boolean value that determines if the item can be added. Notice that the CanAdd parameter is passed by reference so that a user can change this value to false in the event handler to prevent Item from being added to the items in the list box. Before we look at how to get the filenames and add them to the list box, let’s look at Listing 9.51 and see how we can use the same icons as Windows Explorer. LISTING 9.51
Getting the System Image List
SHFILEINFO shfi; DWORD iHnd; TImageList *Images; Images = new TImageList(this); Images->ShareImages = true; iHnd = SHGetFileInfo(“”, 0, &shfi, sizeof(shfi), SHGFI_SYSICONINDEX | SHGFI_SHELLICONSIZE | SHGFI_SMALLICON); if(iHnd != 0) Images->Handle = iHnd;
Notice in Listing 9.51 that the ShareImages property of FImages is set to true. This is very important. It lets the image list know that it should not destroy its handle when the component is destroyed. The handle of the system image list belongs to the system, and if your component destroys it, Windows will not be able to display any of its icons for menus and shortcuts. This isn’t permanent; you would just have to reboot your system so that Windows could get the handle of the system images again. At this point we can override the ReadFileNames() method of TFileListBox to retrieve the filenames in a slightly different manner. Our version will use the shell to get the filenames using COM interfaces. Because walking an itemid list can look a bit messy and is beyond the scope of this chapter, I will not go into detail. We will create a new method, AddItem(), shown in Listing 9.52. It will retrieve the display name of the file and its icon index in the system image list and fire the OnAddItem event we created previously.
11 9721 CH09
11/13/00
9:55 AM
Page 551
Creating Custom Components CHAPTER 9
551
NOTE An itemid is another name for an item identifier or identifier list. You can find more information about “Item Identifiers and Identifier Lists” in the Win32 help files that ship with C++Builder.
LISTING 9.52
The AddItem() Method
int __fastcall TSHFileListBox::AddItem(LPITEMIDLIST pidl) { SHFILEINFO shfi; int Index; SHGetFileInfo((char*)pidl, 0, &shfi, sizeof(shfi), SHGFI_PIDL | ➥SHGFI_SYSICONINDEX | SHGFI_SMALLICON | SHGFI_DISPLAYNAME | SHGFI_USEFILEATTRIBUTES); // fire the OnAddItem event to allow the user the choice to add the // file name or not bool FCanAdd = true; if(FOnAddItem) FOnAddItem(this, AnsiString(shfi.szDisplayName), FCanAdd);
The AddItem() method takes an itemid as its only parameter and returns an integer value. In Listing 9.52 we use the SHGetFileInfo() API to retrieve the display name of the file and its icon index. Once we have the file’s display name, we create a Boolean variable named CanAdd to determine if the item can be added and then fire the OnAddItem event. We can then check the value of CanAdd and, if it is true, we go ahead and add the new item to the list box. After the item is added, we use the TextWidth() method of TCanvas to get its width in pixels. This value
9 CREATING CUSTOM COMPONENTS
if(FCanAdd) { TShellFileListItem *ShellInfo = new TShellFileListItem(pidl, shfi.iIcon); Index = Items->AddObject(AnsiString(shfi.szDisplayName), ➥(TObject*)ShellInfo); // return the length of the file name return Canvas->TextWidth(Items->Strings[Index]); } // return zero as the length as the file has not been added return 0; }
11 9721 CH09
11/13/00
552
9:55 AM
Page 552
C++Builder 5 Essentials PART I
is the return value of the method if the item was added or zero if not. You will see the reason for this shortly. One thing that we haven’t discussed yet is the TShellFileListItem class. Because we need to do the actual drawing of the icons and text in the list box, we need some way of keeping track of each item’s icon index. For each item that is added to the list box, we create an instance of TShellFileListItem and assign it to the Object property of the list box’s Items property. This way we can retrieve it later when we need to draw the item’s icon. TShellFileListItem also holds a copy of the item’s itemid. This is for possible future enhancements; for example, you could create a descendant of TSHFileListBox and override the MouseUp() method to display the context menu for the file. One thing to remember about using the Object property in this way is that the memory being used to hold the TShellFileListItem instance must be freed when the item is deleted from the list box. We do this by overriding the DeleteString() method, as shown in Listing 9.55. As stated previously, the return value from the AddItem() method is the length in pixels of the item that has just been added to the list box. This value is used to determine the longest item and to display a horizontal scrollbar if the longest item is longer than the width of the list box. Take a look at the following code: while(Fetched > 0) { // add the item to the listbox int l = AddItem(rgelt); if(l > hExtent) hExtent = l; ppenumIDList->Next(celt, &rgelt, &Fetched); }
This is a snippet from the ReadFileNames() method. It loops through a folder’s itemid list and retrieves an itemid for each file. The AddItem() method returns the item’s length and compares it to the previous item. If it is longer, the variable l is assigned the new length, and the process is repeated until no more files remain. At the end of this loop, l holds the length of the longest item. Then the DoHorizontalScrollBar() method can be called to determine if the horizontal scrollbar needs to be displayed. The DoHorizontalScrollBars() method, shown in Listing 9.53, takes an integer value as its parameter. This is the length in pixels of an item just added to the list. The value is increased by 2 pixels for the left margin and, if the ShowGlyphs property is true, 18 pixels more is added to compensate for the width of the image and spacing between the image and text. Finally, the Perform() method is called to set the horizontal extent of the items in the list box, which in effect will show the scrollbar if the value of WPARAM is greater than the width of the control.
11 9721 CH09
11/13/00
9:55 AM
Page 553
Creating Custom Components CHAPTER 9
LISTING 9.53
553
Adding a Horizontal Scrollbar
void __fastcall TSHFileListBox::DoHorizontalScrollBar(int he) { he += 2; if(ShowGlyphs) he += 18; Perform(LB_SETHORIZONTALEXTENT,
he, 0);
}
Listings 9.54 and 9.55 are the full source code for the TSHFileListBox component, which can be found in the SHFileListBox folder on the CD-ROM that accompanies this book. LISTING 9.54
The TSHFileListBox Header File, SHFileListBox.h
//--------------------------------------------------------------------------#ifndef SHFileListBoxH #define SHFileListBoxH //--------------------------------------------------------------------------#include <SysUtils.hpp> #include #include #include #include #include <StdCtrls.hpp> #include “ShlObj.h” //---------------------------------------------------------------------------
typedef void __fastcall (__closure *TAddItemEvent)(TObject *Sender, AnsiString Item, bool &CanAdd);
9 CREATING CUSTOM COMPONENTS
class TShellFileListItem : public TObject { private: LPITEMIDLIST Fpidl; int FImageIndex; public: __fastcall TShellFileListItem(LPITEMIDLIST lpidl, int Index); __fastcall ~TShellFileListItem(void); __property LPITEMIDLIST pidl = {read=Fpidl}; __property int ImageIndex = {read=FImageIndex}; };
11 9721 CH09
11/13/00
554
9:55 AM
Page 554
C++Builder 5 Essentials PART I
LISTING 9.54
Continued
class PACKAGE TSHFileListBox : public TFileListBox { private: TImageList *FImages; TNotifyEvent FOnDblClick; bool FCanLaunch; bool FRightBtnSel; TAddItemEvent FOnAddItem; int __fastcall AddItem(LPITEMIDLIST pidl); void __fastcall GetSysImages(void); protected: void __fastcall DblClick(void); void __fastcall ReadFileNames(void); void __fastcall MouseUp(TMouseButton Button, TShiftState Shift, int X, int Y); void __fastcall DrawItem(int Index, const TRect &Rect, TOwnerDrawState State); void __fastcall DoHorizontalScrollBar(int he); void __fastcall DeleteString(int Index); public: __fastcall TSHFileListBox(TComponent* Owner); __fastcall ~TSHFileListBox(void); __published: __property bool CanLaunch = {read=FCanLaunch, write=FCanLaunch, default=true}; __property bool RightBtnSel = {read=FRightBtnSel, write=FRightBtnSel, default=true}; __property TNotifyEvent OnDblClick = {read=FOnDblClick, write=FOnDblClick}; __property TAddItemEvent OnAddItem = {read=FOnAddItem, write=FOnAddItem}; }; #endif
LISTING 9.55
The TSHFileListBox Source File, SHFileListBox.cpp
//--------------------------------------------------------------------------#include #pragma hdrstop #include “SHFileListBox.h” #pragma package(smart_init) //--------------------------------------------------------------------------__fastcall TShellFileListItem::TShellFileListItem(LPITEMIDLIST lpidl, int Index)
11 9721 CH09
11/13/00
9:55 AM
Page 555
Creating Custom Components CHAPTER 9
LISTING 9.55
555
Continued
: TObject() { // store a copy of the file’s pidl Fpidl = CopyPIDL(lpidl); // and save its icon index FImageIndex = Index; } //---------------------------------------------------------------------------__fastcall TShellFileListItem::~TShellFileListItem(void) { LPMALLOC lpMalloc=NULL; if(SUCCEEDED(SHGetMalloc(&lpMalloc))) { // free the memory associated with the pidl lpMalloc->Free(Fpidl); lpMalloc->Release(); } }
9 CREATING CUSTOM COMPONENTS
//--------------------------------------------------------------------------__fastcall TSHFileListBox::TSHFileListBox(TComponent* Owner) : TFileListBox(Owner) { ItemHeight = 18; ShowGlyphs = true; FCanLaunch = true; FRightBtnSel = true; } //--------------------------------------------------------------------------__fastcall TSHFileListBox::~TSHFileListBox(void) { // free the images if(FImages) delete FImages; FImages = NULL; } //---------------------------------------------------------------------------void __fastcall TSHFileListBox::DeleteString(int Index) { // This method is called in response to the LB_DELETESTRING messeage // First delete the TShellFileListItem pointed to by the string’s // Object property TShellFileListItem *ShellItem = reinterpret_cast (Items->Objects[Index]); delete ShellItem;
11 9721 CH09
11/13/00
556
9:55 AM
Page 556
C++Builder 5 Essentials PART I
LISTING 9.55
Continued
ShellItem = NULL; // now delete the item Items->Delete(Index); } //---------------------------------------------------------------------------namespace Shfilelistbox { void __fastcall PACKAGE Register() { TComponentClass classes[1] = {__classid(TSHFileListBox)}; RegisterComponents(“Samples”, classes, 0); } } //--------------------------------------------------------------------------void __fastcall TSHFileListBox::ReadFileNames(void) { LPMALLOC g_pMalloc; LPSHELLFOLDER pisf; LPSHELLFOLDER sfChild; LPITEMIDLIST pidlDirectory; LPITEMIDLIST rgelt; LPENUMIDLIST ppenumIDList; int hExtent; try { try { if(HandleAllocated()) { GetSysImages(); // prohibit screen updates Items->BeginUpdate(); // delete the items already in the list Items->Clear(); // get the shell’s global allocator if(SHGetMalloc(&g_pMalloc) != NOERROR) { return; } // get the desktop’s IShellFolder interface if(SHGetDesktopFolder(&pisf) != NOERROR) { return; }
11 9721 CH09
11/13/00
9:55 AM
Page 557
Creating Custom Components CHAPTER 9
LISTING 9.55
557
Continued
// convert folder string to WideChar WideChar oleStr[MAX_PATH]; FDirectory.WideChar(oleStr, MAX_PATH); unsigned long pchEaten; unsigned long pdwAttributes; // get pidl of current folder pisf->ParseDisplayName(Handle, 0, oleStr, &pchEaten, &pidlDirectory, &pdwAttributes); // get an IShellFolder interface for the current folder if(pisf->BindToObject(pidlDirectory,NULL, IID_IShellFolder, (void**)&sfChild) != NOERROR) { return; } // enumerate the objects within the folder sfChild->EnumObjects(Handle, SHCONTF_NONFOLDERS | SHCONTF_INCLUDEHIDDEN, &ppenumIDList);
} } catch(Exception &E) { throw(E); // re-throw any exceptions } } __finally { // make sure we do this regardless g_pMalloc->Free(rgelt); g_pMalloc->Free(ppenumIDList); g_pMalloc->Free(pidlDirectory);
9 CREATING CUSTOM COMPONENTS
// walk through the enumlist ULONG celt = 1; ULONG Fetched = 0; ppenumIDList->Next(celt, &rgelt, &Fetched); hExtent = 0; while(Fetched > 0) { // add the item to the listbox int l = AddItem(rgelt); if(l > hExtent) hExtent = l; ppenumIDList->Next(celt, &rgelt, &Fetched); }
11 9721 CH09
11/13/00
558
9:55 AM
Page 558
C++Builder 5 Essentials PART I
LISTING 9.55
Continued
pisf->Release(); sfChild->Release(); g_pMalloc->Release(); Items->EndUpdate(); } // Show the horizontal scrollbar if necessary DoHorizontalScrollBar(hExtent); } // --------------------------------------------------------------------------void __fastcall TSHFileListBox::DoHorizontalScrollBar(int he) { // add a little space for the margins he += 2; // if we’re showing the images make room for it plus a bit more // for the space between the image and the text if(ShowGlyphs) he += 18; Perform(LB_SETHORIZONTALEXTENT, he, 0); } // --------------------------------------------------------------------------void __fastcall TSHFileListBox::GetSysImages(void) { SHFILEINFO shfi; DWORD iHnd; if(!FImages) { FImages = new TImageList(this); FImages->ShareImages = true; FImages->Height = 16; FImages->Width = 16; iHnd = SHGetFileInfo(“”, 0, &shfi, sizeof(shfi), SHGFI_SYSICONINDEX | SHGFI_SHELLICONSIZE | SHGFI_SMALLICON); if(iHnd != 0) FImages->Handle = iHnd; } } // --------------------------------------------------------------------------int __fastcall TSHFileListBox::AddItem(LPITEMIDLIST pidl) { SHFILEINFO shfi; int Index; SHGetFileInfo((char*)pidl, 0, &shfi, sizeof(shfi), SHGFI_PIDL |
11 9721 CH09
11/13/00
9:55 AM
Page 559
Creating Custom Components CHAPTER 9
LISTING 9.55
559
Continued
SHGFI_SYSICONINDEX | SHGFI_SMALLICON | SHGFI_DISPLAYNAME | SHGFI_USEFILEATTRIBUTES); // fire the OnAddItem event to allow the user the choice to add the // file name or not bool FCanAdd = true; if(FOnAddItem) FOnAddItem(this, AnsiString(shfi.szDisplayName), FCanAdd); if(FCanAdd) { TShellFileListItem *ShellInfo = new TShellFileListItem(pidl, shfi.iIcon); Index = Items->AddObject(AnsiString(shfi.szDisplayName), (TObject*)ShellInfo); // return the length of the file name return Canvas->TextWidth(Items->Strings[Index]); } // return zero as the length as the file has not been added return 0; } // --------------------------------------------------------------------------void __fastcall TSHFileListBox::DrawItem(int Index, const TRect &Rect, TOwnerDrawState State) { int Offset;
9 CREATING CUSTOM COMPONENTS
Canvas->FillRect(Rect); Offset = 2; if(ShowGlyphs) { TShellFileListItem *ShellItem = reinterpret_cast (Items->Objects[Index]); // draw the file’s icon in the listbox FImages->Draw(Canvas, Rect.Left+2, Rect.Top+2, ShellItem->ImageIndex, true); Offset += 18; } int Texty = Canvas->TextHeight(Items->Strings[Index]); Texty = ((ItemHeight - Texty) / 2) + 1; // now draw the text Canvas->TextOut(Rect.Left + Offset, Rect.Top + Texty, Items>Strings[Index]); } //---------------------------------------------------------------------------void __fastcall TSHFileListBox::DblClick(void)
11 9721 CH09
11/13/00
560
9:55 AM
Page 560
C++Builder 5 Essentials PART I
LISTING 9.55
Continued
{ if(FCanLaunch) { int ii=0; // go through the list and find which item is selected for(ii=0; ii < Items->Count; ii++) { if(Selected[ii]) { AnsiString str = Items->Strings[ii]; ShellExecute(Handle, “open”, str.c_str(), 0, 0, SW_SHOWDEFAULT); } } } // fire the OnDblClick event if(FOnDblClick) FOnDblClick(this); } //---------------------------------------------------------------------------void __fastcall TSHFileListBox::MouseUp(TMouseButton Button, TShiftState Shift, int X, int Y) { if(!FRightBtnSel) return; TPoint ItemPos = Point(X,Y); // is there an item under the mouse ? int Index = ItemAtPos(ItemPos, true); // if not just return if(Index == -1) return; // else select the item Perform(LB_SETCURSEL, (WPARAM)Index, 0); } //---------------------------------------------------------------------------// ValidCtrCheck is used to assure that the components created do not have // any pure virtual functions. // static inline void ValidCtrCheck(TSHFileListBox *) { new TSHFileListBox (NULL); }
11 9721 CH09
11/13/00
9:55 AM
Page 561
Creating Custom Components CHAPTER 9
561
Creating Custom Data-Aware Components Just as with any other custom component, it is important to decide from the start which ancestor will be used for the creation of a data-aware component. In this section we are going to look at extending the TMaskEdit edit component so that it will read data from a datasource and display it in the masked format provided. This type of control is known as a data-browsing control. We will then extend this control further to make it a data-aware control, meaning that changes to the field or database will be reflected in both directions.
Making the Control Read-Only The control we are going to create already has ReadOnly, a read-only property, so we don’t have to create it. If your component doesn’t, create the property as you would for any other component. If our component did not already have the ReadOnly property, we would create it as shown in Listing 9.56 (note that this code is not required for this component). LISTING 9.56
Creating a Read-Only Property
class PACKAGE TDBMaskEdit : public TMaskEdit { private: bool FReadOnly; protected: public: __fastcall TDBMaskEdit(TComponent* Owner); __published: __property ReadOnly = {read = FReadOnly, write = FReadOnly, default = true}; };
__fastcall TDBMaskEdit::TDBMaskEdit(TComponent* Owner) : TMaskEdit(Owner) { FReadOnly = true; }
Finally, we need to ensure that the component acts as a read-only control. You need to override the method normally associated with the user accessing the control. If we were creating a dataaware grid, it would be the SelectCell() method, in which you would check the value of the ReadOnly property and act accordingly. If the value of ReadOnly is false, you call the inherited method; otherwise, just return.
CREATING CUSTOM COMPONENTS
In the constructor we would set the default value of the property.
9
11 9721 CH09
11/13/00
562
9:55 AM
Page 562
C++Builder 5 Essentials PART I
If the TMaskEdit control had a SelectEdit() method, the code would look like this: bool __fastcall TDBMaskEdit::SelectEdit(void) { if(FReadOnly) return(false); else return(TMaskEdit::SelectEdit()); }
In this case, we don’t have to worry about the ReadOnly property. TMaskEdit already has one.
Establishing the Link For our control to become data aware, we need to provide it the data link required to communicate with a data member of a database. This data link class is called TFieldDataLink. A data-aware control owns its data link class. It is the control’s responsibility to create, initialize, and destroy the data link. Establishing the link requires three steps: 1. Declare the data link class as a member of the control 2. Declare the read and write access properties as appropriate 3. Initialize the data link
Declare the Data Link The data link is a class of type TFieldDataLink and requires DBCTRLS.HPP to be included in the header file. #include class PACKAGE TDBMaskEdit : public TMaskEdit { private: TFieldDataLink *FDataLink; ... };
Our data-aware component now requires DataSource and DataField properties (just like all other data-aware controls). These properties use “pass-through” methods to access properties of the data link class. This enables the control and its data link to share the same datasource and field.
11 9721 CH09
11/13/00
9:55 AM
Page 563
Creating Custom Components CHAPTER 9
563
Declare read and write Access The access you allow your control is governed by the declaration of the properties themselves. We are going to give our component full access. It has a ReadOnly property that will automatically take care of the read-only option, because the user will be unable to edit the control. Note that this will not stop the developer from writing code to write directly to the linked field of the database via this control. If you require read-only access, simply leave out the write option. The code in Listings 9.57 and 9.58 shows the declaration of the properties and their corresponding read and write implementation methods. LISTING 9.57
The TDBMaskEdit Class Declaration from the Header File
class PACKAGE TDBMaskEdit : public TMaskEdit { private: ... AnsiString __fastcall GetDataField(void); TDataSource* __fastcall GetDataSource(void); void __fastcall SetDataField(AnsiString pDataField); void __fastcall SetDataSource(TDataSource *pDataSource); ... __published: __property AnsiString DataField = {read = GetDataField, write = SetDataField, nodefault}; __property TDataSource *DataSource = {read = GetDataSource, write = SetDataSource, nodefault}; };
LISTING 9.58
The TDBMaskEdit Methods from the Source File
TDataSource * __fastcall TDBMaskEdit::GetDataSource(void) { return(FDataLink->DataSource); } void __fastcall TDBMaskEdit::SetDataField(AnsiString pDataField) { FDataLink->FieldName = pDataField;
CREATING CUSTOM COMPONENTS
AnsiString __fastcall TDBMaskEdit::GetDataField(void) { return(FDataLink->FieldName); }
9
11 9721 CH09
11/13/00
564
9:55 AM
Page 564
C++Builder 5 Essentials PART I
LISTING 9.58
Continued
} void __fastcall TDBMaskEdit::SetDataSource(TDataSource *pDataSource) { if(pDataSource != NULL) pDataSource->FreeNotification(this); FDataLink->DataSource = pDataSource; }
The only code here that requires additional explanation is the FreeNotification() method of pDataSource. C++Builder maintains an internal list of objects so that all other objects can be notified when the object is about to be destroyed. The FreeNotification() method is called automatically for components on the same form, but in this case there is a chance that a component on another form (such as a data module) has references to it. As a result, we need to call FreeNotification() so that the object can be added to the internal list for all other forms.
Initialize the Data Link You might think that everything that needs to be done has been done. If you attempt to compile this component and add it to a form, you will find access violations reported in the Object Inspector for the DataField and DataSource properties. The reason is that the internal FieldDataLink object has not been instantiated. Add the following declaration to the public section of the class’s header file: __fastcall ~TDBMaskEdit(void);
Add the following code to the component’s constructor and destructor: __fastcall TDBMaskEdit::TDBMaskEdit(TComponent* Owner) : TMaskEdit(Owner) { FDataLink = new TFieldDataLink(); FDataLink->Control = this; } __fastcall TDBMaskEdit::~TDBMaskEdit(void) { if(FDataLink) { FDataLink->Control = 0; FDataLink->OnUpdateData = 0; delete FDataLink; } }
11 9721 CH09
11/13/00
9:55 AM
Page 565
Creating Custom Components CHAPTER 9
565
The Control property of FDataLink is of type TComponent. This property must be set to the component that uses the TFieldDataLink object to manage its link to a TField object. We need to set the Control property to this to indicate that this component is responsible for the link. Accessing the TObject is achieved by adding a read-only property. Add the property to the public section of the class definition. __property TField *Field = {read = GetField};
Add the GetField declaration to the private section: TField * __fastcall GetField(void);
Add the following code to the source file: TField * __fastcall TDBMaskEdit::GetField(void) { return(FDataLink->Field); }
Using the OnDataChange Event So far we have created a component that can link to a datasource but doesn’t yet respond to data changes. We are now going to add code that enables the control to respond to changes in the field, such as moving to a new record. Data link classes have an OnDataChange event that is called when the datasource indicates a change to the data. To give our component the capability to respond to these changes, we add a method and assign it to the OnDataChange event.
NOTE
The OnDataChange event is of type TNotifyEvent, so we need to add our method with the same prototype. Add the following line of code to the private section of the component header. class PACKAGE TDBMaskEdit : public TMaskEdit { private: // ... void __fastcall DataChange(TObject *Sender); }
CREATING CUSTOM COMPONENTS
TDataLink is a helper class used by data-aware objects. Look in the online help files that ship with C++Builder for a listing of its properties, methods, and events.
9
11 9721 CH09
11/13/00
566
9:55 AM
Page 566
C++Builder 5 Essentials PART I
We need to assign the DataChange() method to the OnDataChange event in the constructor. We also remove this assignment in the component destructor. __fastcall TDBMaskEdit::TDBMaskEdit(TComponent* Owner) : TMaskEdit(Owner) { FDataLink = new TFieldDataLink(); FDataLink->Control = this; FDataLink->OnDataChange = DataChange; } __fastcall TDBMaskEdit::~TDBMaskEdit(void) { if(FDataLink) { FDataLink->Control = 0; FDataLink->OnUpdateData = 0; FDataLink->OnDataChange = 0; delete FDataLink; } }
Finally, define the DataChange() method as shown in the following code: void __fastcall TDBMaskEdit::DataChange(TObject *Sender) { if(!FDataLink->Field) { if(ComponentState.Contains(csDesigning)) Text = Name; else Text = “”; } else Text = FDataLink->Field->AsString; }
The DataChange() method first checks to see if the data link is pointing to a datasource (and field). If there is no valid pointer, the Text property (a member of the inherited component) is set to an empty string (at runtime) or the control name (at designtime). If a valid field is set, the Text property is set to the value of the field’s content via the AsString property of the TField object. You now have a data-browsing control, so-called because it is capable only of displaying data changes in a datasource. It’s now time to turn this component into a data-editing control.
11 9721 CH09
11/13/00
9:55 AM
Page 567
Creating Custom Components CHAPTER 9
567
Changing to a Data-Editing Control Turning a data-browsing control into a data-editing control requires additional code to respond to key and mouse events. This enables any changes made to the control to be reflected in the underlying field of the linked database.
The ReadOnly Property When a user places a data-editing control into his project, he expects the control NOT to be read-only. The default value for the ReadOnly property of TMaskEdit (the inherited class) is false, so we have nothing further to do. If you create a component that has a custom ReadOnly property added, be sure to set the default value to false.
Keyboard and Mouse Events If you refer to the controls.hpp file, you will find protected methods of TMaskEdit called KeyDown() and MouseDown(). These methods respond to the corresponding window messages (WM_KEYDOWN, WM_LBUTTONDOWN, WM_MBUTTONDOWN, and WM_RBUTTONDOWN) and call the appropriate event if one is defined by the user. To override these methods, add the KeyDown() and MouseDown() methods to the TDBMaskEdit class. Take the declarations from the controls.hpp file. virtual void __fastcall MouseDown(TMouseButton, TShiftState Shift, int X, int Y); virtual void __fastcall KeyDown(unsigned short &Key, TShiftState Shift);
Refer to the controls.hpp file (or the help file) to determine the original declaration. Next we add the source code, shown in Listing 9.59. LISTING 9.59
The MouseDown() and KeyDown() Methods
void __fastcall TDBMaskEdit::KeyDown(unsigned short &Key, TShiftState Shift)
CREATING CUSTOM COMPONENTS
void __fastcall TDBMaskEdit::MouseDown(TMouseButton Button, TShiftState Shift, int X, int Y) { if(!ReadOnly && FDataLink->Edit()) TMaskEdit::MouseDown(Button, Shift, X, Y); else { if(OnMouseDown) OnMouseDown(this, Button, Shift, X , Y); } }
9
11 9721 CH09
11/13/00
568
9:55 AM
Page 568
C++Builder 5 Essentials PART I
LISTING 9.59
Continued
{ Set Keys; Keys = Keys << VK_PRIOR << VK_NEXT << VK_END << VK_HOME << VK_LEFT << VK_UP << VK_RIGHT << VK_DOWN; if(!ReadOnly && (Keys.Contains(Key)) && FDataLink->Edit()) TMaskEdit::KeyDown(Key, Shift); else { if(OnKeyDown) OnKeyDown(this, Key, Shift); } }
In both cases, we check to make sure the component is not read-only and the FieldDataLink is in edit mode. The KeyDown() method also checks for any cursor control keys (defined in winuser.h). If all checks pass, then the field can be edited, so the inherited method is called. This method will automatically call the associated user event if one is defined. If the field cannot be edited, the user event is executed (if one exists).
Working Toward a Dataset Update If the user modifies the contents of the data-aware control, the change must be reflected in the field. Similarly, if the field value is altered, the data-aware control will require a corresponding update. The TDBMaskEdit control already has a DataChange() method that is called by the TFieldDataLink OnDataChange event. This method reflects the change of the field value in the TDBMaskEdit control. This takes care of the first scenario. Now we need to update the field value when the user modifies the contents of the control. The TFieldDataLink class has an OnUpdateData event where the data-aware control can write any pending edits to the record in the dataset. We can now create an UpdateData() method for TDBMaskEdit and assign this method to the OnUpdateData event of the TFieldDataLink class. Add the declaration for our UpdateData() method to the TDBMaskEdit control as shown by the following code: void __fastcall UpdateData(TObject *Sender);
Assign this method to the TFieldDataLink OnUpdateData event in the constructor: __fastcall TDBMaskEdit::TDBMaskEdit(TComponent* Owner) : TMaskEdit(Owner)
11 9721 CH09
11/13/00
9:55 AM
Page 569
Creating Custom Components CHAPTER 9
569
{ FDataLink = new TFieldDataLink(); FDataLink->Control = this; FDataLink->OnUpdateData = UpdateData; FDataLink->OnDataChange = DataChange; }
Set the field value to the current contents of the TDBMaskEdit control: void __fastcall TDBMaskEdit::UpdateData(TObject *Sender) { if(FDataLink->CanModify) FDataLink->Field->AsString = Text; }
The TDBMaskEdit control is a descendant of TMaskEdit, which happens to be a descendant of the TCustomEdit class. This class has a protected Change() method that is triggered by Windows events. This method then triggers the OnChange event. We are going to override the Change() method so that it updates the dataset before calling the inherited method. In the protected section of the TDBMaskEdit class, add the following method: DYNAMIC void __fastcall Change(void);
Add the Change() method in Listing 9.60 to the source code. LISTING 9.60
The Change() Method
AnsiString ChangedValue = Text; // get cursor position too int CursorPosition = SelStart; // need to be in edit mode if(FDataLink->CanModify && FDataLink->Edit()) { Text = ChangedValue; // just in case we were not in edit mode
9 CREATING CUSTOM COMPONENTS
void __fastcall TDBMaskEdit::Change(void) { if(FDataLink) { // we need to see if the datasource is in edit mode // if not then we need to save the current value because // placing the datasource into edit mode will change the // current value to that already present in the table
11 9721 CH09
11/13/00
570
9:55 AM
Page 570
C++Builder 5 Essentials PART I
LISTING 9.60
Continued SelStart = CursorPosition; FDataLink->Modified(); // posting a change (the datasource // is not put back into edit mode) }
} TMaskEdit::Change (); }
This change notifies the TFieldDataLink class that modifications have been made and finishes up by calling the inherited Change() method. The final step is to provide for when focus is moved away from the control. The TWinControl class responds to the CM_EXIT message by generating an OnExit event. We can also respond to this message as a method of updating the record of the linked dataset. This is done by creating a message map in the TDBMaskEdit class. Add the following code to the private section: void __fastcall CMExit(TWMNoParams Message); BEGIN_MESSAGE_MAP MESSAGE_HANDLER(CM_EXIT, TWMNoParams, CMExit) END_MESSAGE_MAP(TMaskEdit)
This message map indicates that the CMExit() method will be called in response to a CM_EXIT message with the relevant information passed in the TWMNoParams structure. The CMExit() method is added to the source file. void __fastcall TDBMaskEdit::CMExit(void) { try { ValidateEdit(); if(FDataLink && FDataLink->CanModify) FDataLink->UpdateRecord(); } catch(...) { SetFocus(); throw; } }
11 9721 CH09
11/13/00
9:55 AM
Page 571
Creating Custom Components CHAPTER 9
571
This attempts to validate the contents of the field against the defined mask. If the datasource can be modified, the record is updated in the dataset. If an exception is raised, the cursor is positioned back in the control that caused the problem, and the exception is raised again to be handled by the application.
Adding a Final Message C++Builder has a component called TDBCtrlGrid. This control displays records from a datasource in a free-form layout. When this component updates its datasource, it sends out the message CM_GETDATALINK. If you perform a search for this in the C++Builder header files, you’ll find a message map defined in all of the database controls. Following this with the corresponding .pas file, you will find message handlers such as the following: procedure TDBEdit.CMGetDataLink(var Message: TMessage); begin Message.Result := Integer(FDataLink); end;
We can add this support to our component by adding the message map, declaring the method, and implementing the message handler. In the private section void __fastcall CMGetDataLink(TMessage Message);
In the public section, modify the message map to look like the following: BEGIN_MESSAGE_MAP MESSAGE_HANDLER(CM_EXIT, TWMNoParams, CMExit) MESSAGE_HANDLER(CM_GETDATALINK, TMessage, CMGetDataLink) END_MESSAGE_MAP(TMaskEdit)
9
Finally, implement the method in the source file:
And that’s it. We now have a complete data-aware control that behaves just like any other data control.
Registering Components Registering components is a straightforward, multistage procedure. The first stage is simple. You must ensure that any component you want to install onto the Component Palette does not contain any pure virtual (or pure DYNAMIC) functions—in other words, functions of the following form:
CREATING CUSTOM COMPONENTS
void __fastcall TDBMaskEdit::CMGetDataLink(TMessage Message) { Message.Result = (int)FDataLink; }
11 9721 CH09
11/13/00
572
9:55 AM
Page 572
C++Builder 5 Essentials PART I virtual ReturnType __fastcall FunctionName(ParameterList) = 0;
Note that the __fastcall keyword is not a requirement of pure virtual functions, but it will be present in component member functions. This is why it is shown. You can check for pure virtual functions manually by examining the class definition for the component, or you can call the function ValidCtrCheck(), passing a pointer to your component as an argument. The ValidCtrCheck() function is placed anywhere in the implementation file. For a component called TcustomComponent, it is of the form static inline void ValidCtrCheck(TCustomComponent *) { new TCustomComponent(NULL); }
All this function does is try to create an instance of TCustomComponent. Because you cannot create an instance of a class with a pure virtual function, the compiler will give the following compilation errors: E2352 Cannot create instance of abstract class ‘TCustomComponent’ E2353 Class ‘TCustomComponent’ is abstract because of ‘function’
The second error will identify the pure virtual function. Both errors refer to this line: new TCustomComponent(NULL);
Using this function is often not necessary, because it is not likely you will create a pure virtual function by accident. However, when you use the IDE to create a new component, this function is automatically added to the implementation file. Then, you may as well leave it there just in case. Once you have determined that your component is not an abstract base class and can be instantiated from, you can now write the code to perform the actual registration. To do this, you must write a Register() function. The Register() function must be enclosed in a namespace that is the same as the name of the file in which it is contained. There is one proviso that must be met. The first letter of the namespace must be in uppercase, and the remaining letters must be in lowercase. Hence, the Register() function must appear in your code in the following format: namespace Thenameofthefilethisisin { void __fastcall PACKAGE Register() { // Registration code goes here } }
11 9721 CH09
11/13/00
9:55 AM
Page 573
Creating Custom Components CHAPTER 9
573
You must not forget the PACKAGE macro in front of the Register() function. Now that the Register() function is in place, it requires only that the component (or components) that we want to register is registered. To do this, use the RegisterComponents() function. This is declared in $(BCB)\Include\Vcl\Classes.hpp as extern PACKAGE void __fastcall RegisterComponents(const AnsiString Page, TMetaClass* const * ComponentClasses, const int ComponentClasses_Size);
expects two things to be passed to it: an AnsiString representing the name of the palette page onto which the component is to be installed, and an open array of TMetaClass pointers to the components to be installed. If the AnsiString value for Page does not match one of the palette pages already present in the Component Palette, a new page is created with the name of the AnsiString passed. The value of this argument can be obtained from a string resource if required, allowing different strings to be used for different locales. RegisterComponents()
The TMetaClass* open array requires more thought. There are essentially two ways of doing this: Use the OPENARRAY macro or create the array by hand. Let’s look at an example that illustrates both approaches. Consider that we want to register three components: TCustomComponent1, TCustomComponent2, and TCustomComponent3. We want to register these onto a new palette page, MyCustomComponents. First we must obtain the TMetaClass* for each of the three components. We do this by using the __classid operator, for example: __classid(TCustomComponent1)
Using the OPENARRAY macro, we can write the RegisterComponents() function as follows:
We could use TComponentClass instead of TMetaClass*, because it is a typedef for TMetaClass*, declared in $(BCB)\Include\Vcl\Classes.hpp as: typedef TMetaClass* TComponentClass;
The OPENARRAY macro is discussed in more detail in Chapter 10, “Creating Property and Component Editors,” in the section “Registering a Property or Properties in a Category.” Note that you are restricted to registering a maximum of 19 arguments (components) in any single RegisterComponents call due to limitations of the OPENARRAY macro. Normally this is not a problem.
9 CREATING CUSTOM COMPONENTS
RegisterComponents(“MyCustomComponents”, OPENARRAY( TMetaClass*, ( __classid(TCustomComponent1), __classid(TCustomComponent2), __classid(TCustomComponent3) ) ) );
11 9721 CH09
11/13/00
574
9:55 AM
Page 574
C++Builder 5 Essentials PART I
The other approach is to declare and initialize an array of TMetaClass* (or TComponentClass) by hand: TMetaClass Components[3] = { __classid(TCustomComponent1), __classid(TCustomComponent2), __classid(TCustomComponent3) };
We then pass this to the RegisterComponents() function as before, but this time we must also pass the value of the last valid index for the array, in this case 2: RegisterComponents(“MyCustomComponents”, Components, 2);
The final function call is simpler, but there is a greater chance of error in passing an incorrect value for the last parameter. We can now see what a complete Register() function looks like: namespace Thenameofthefilethisisin { void __fastcall PACKAGE Register() { RegisterComponents(“MyCustomComponents”, OPENARRAY( TMetaClass*, ( __classid(TCustomComponent1), __classid(TCustomComponent2), __classid(TCustomComponent3) ) ) ); } }
Remember that you may have as many RegisterComponents() functions in the Register() function as required. You may also include other registrations such as those for property and component editors. This is the subject of the next chapter. You can place the component registration in the implementation file of the component, but typically the registration code should be isolated from the component implementation. For more details on this, refer to the “Component Distribution and Related Issues” section in Chapter 11, “More Custom Component Techniques.”
Summary This chapter covers a lot of ground. When creating custom components, review the VCL Chart and the VCL source code when deciding from which base class to derive your new component. You saw a simple example of how to enhance existing components with the TStyleLabel component and how to create non-visual components. You also saw examples of creating dataaware components and how they can be linked together, just like some of C++Builder’s stock data-aware controls.
11 9721 CH09
11/13/00
9:55 AM
Page 575
Creating Custom Components CHAPTER 9
575
You will most likely find that creating your own custom visual components involves much more work than modifying an existing control. Start simple, and try to build classes to handle some of the more complicated stuff for you. You saw how to trap messages sent to your component by Windows, modify the base class’s default handler, create custom events, and call those events from within the component. Remember to make as many properties, methods, and events as is necessary so as not restrict the component user. Whether you concentrate on writing applications, components, or both, creating custom components is almost another area of expertise with C++Builder. The more complicated your components become, the more your programming expertise will be tried. By creating custom components, you will not only increase your knowledge of the VCL architecture, you also will enhance your skills with C++Builder, as well as your C++ programming skills in general. This will give you complete control over the user interface of your applications and decreasing development time.
9 CREATING CUSTOM COMPONENTS
11 9721 CH09
11/13/00
9:55 AM
Page 576
12 9721 CH10
11/13/00
9:44 AM
Page 577
Creating Property and Component Editors Jamie Allsop
IN THIS CHAPTER • Creating Custom Property Editors • Properties and Exceptions • Registering Custom Property Editors • Using Images in Property Editors • Installing Editor-Only Packages • Using Linked Image Lists in Property Editors • Creating Custom Component Editors • Registering Component Editors • Using Predefined Images in Custom Property and Component Editors • Registering Property Categories in Custom Components
CHAPTER
10
12 9721 CH10
11/13/00
578
9:44 AM
Page 578
C++Builder 5 Essentials PART I
As stated in Chapter 9, “Creating Custom Components,” components are the building blocks of C++Builder. They are the essential elements of every C++Builder program. Developers spend much of their time working with components, learning about their features, and trying to make best use of the facilities that they offer. To that end, improving the designtime interface of a component is one of the single most powerful ways to improve a component’s usefulness. Great effort is often spent by developers to improve the user interface for their customers. Component writers should also consider the interface that they present to their customers. This chapter covers the techniques required to successfully implement property editors and component editors. Some of the biggest changes to C++Builder’s IDE have been to allow improved property and component interfaces at designtime to help improve the productivity of developers. All of the new features added in C++Builder 5 are covered in depth, and definitive guidelines are presented as to their proper use. This chapter aims to be a complete coverage of this often neglected area of component creation, and component developers should find it a useful reference. The chapter is divided into four main sections. The first section covers all aspects of property editors, including the new image capabilities. Component editors are then given a similar treatment. An effort has been made to present a logical approach to the development process for each. The third section covers the use of resources in editors. This is often mentioned, but actual guidelines are rarely given. Finally, the property categories are discussed, and details are given of how to register properties for specific categories, along with details on creating custom categories. Component writers should find this chapter informative. All the code of the property editors and component editors discussed in this chapter is contained on the accompanying CD-ROM. By examining the source to these editors, it should be possible to develop a good understanding of the issues involved in creating custom editors for components. The code shown in the listings throughout the chapter is contained in two packages: the EnhancedEditors package (EnhancedEditors.bpk, a designtime-only package), and the NewAdditionalComponents designtime and runtime packages. The designtime package, called NewAdditionalComponentsDTP.bpk, contains property and component editor code as well as registration code. The runtime package, called NewAdditionalComponentsRTP.bpk, contains code for components. It would probably be helpful to install these packages into your installation of C++Builder 5 before reading this chapter. That way you can see the effect that the property and component editors have while you are reading the chapter. Before you install either package, first copy the folder called Chapter10Packages to your hard drive. It contains the files you require. Feel free to give the folder a more imaginative name. Then copy both files in the System folder to a folder on the system path, for example Windows\System on Windows 9x machines or WINNT\System32 on Windows NT and Windows 2000 machines. These files are runtime files
12 9721 CH10
11/13/00
9:44 AM
Page 579
Creating Property and Component Editors CHAPTER 10
579
required by the two designtime packages. Both of the designtime packages require the TNonVCLTypeInfoPackage.bpl runtime-only file (this file is discussed again shortly), and the NewAdditionalComponentsDTP.bpl designtime-only package also requires the NewAdditionalComponentsRTP.bpl runtime-only package. To install the EnhancedEditors package, run C++Builder 5 and click on Install Packages on the Component menu. Click the Add button in the Design packages group and browse for the EnhancedEditors.bpl file. When you click Open, the Add Design Package dialog will close and the package will appear in the Design packages list as Enhanced Property and Component Editors. Click OK to finish. Table 10.1 lists the property and component editors contained in the EnhancedEditors package and indicates whether or not they are registered (in other words, installed) with the IDE when the package is installed. TABLE 10.1
Property and Component Editors Registered by the EnhancedEditors
Package
Registered
TShapeTypePropertyEditor
Yes Yes No—An Abstract Base Class Yes Yes Yes Not required Yes Not required Yes Not required Not required Yes Yes Yes Yes Yes Yes Yes
TImageListPropertyEditor TImageIndexProperty TPersistentDerivedImageIndexProperty TComponentDerivedImageIndexProperty TMenuItemImageIndexProperty TTabSheetImageIndexProperty TToolButtonImageIndexProperty TCoolBandImageIndexProperty TListColumnImageIndexProperty TCustomActionImageIndexProperty THeaderSectionImageIndexProperty TDisplayCursorProperty TDisplayFontNameProperty TUnsignedProperty TCharPropertyEditor TSignedCharProperty TUnsignedCharProperty TImageComponentEditor
10 PROPERTY AND COMPONENT EDITORS
Editors
12 9721 CH10
11/13/00
580
9:44 AM
Page 580
C++Builder 5 Essentials PART I
The method for installing the NewAdditionalComponents package is the same as for the EnhancedEditors package. Click on Install Packages on the Component menu. Click the Add button in the Design packages group and browse for the NewAdditionalComponentsDTP.bpl file. When you click Open, the Add Design Package dialog will close and the package will appear in the Design packages list as New Components for the Additional Palette Page. Click OK to finish. The following components are registered with the IDE by this package: •
TEnhancedImage
•
TFilterEdit
The TImageIndexPropertyEditor property editor is also registered by this package. Additionally, the TNonVCLTypeInfoPackage runtime-only package (TNonVCLTypeInfoPackage.bpk) is included. This contains code referred to in Listings 10.7 and 10.8 in the section “Obtaining a TTypeInfo* (PTypeInfo) from an Existing Property and Class for a Non-VCL Type,” later in this chapter. Both the EnhancedEditors.bpk and the NewAdditionalComponentsDTP.bpk package require this package for their registration code. Therefore, if you want to recompile either package, the header files (*.h) and import file (.bpi) of this package must be found by the IDE when it is compiling and linking the packages.
Creating Custom Property Editors One of the best ways to improve a component’s designtime interface is to ensure that property editors are easy to use and intuitive. This section looks at the main principles involved in creating your own property editors. All custom property editors descend ultimately from TPropertyEditor, which provides the basic functionality required for the editor to function within the IDE. Listing 10.1 shows the the class definition for TPropertyEditor (from $(BCB)\Include\Vcl\DsgnIntf.hpp, where $(BCB) is the C++Builder 5 installation directory). LISTING 10.1
TPropertyEditor Class Definition
class DELPHICLASS TPropertyEditor; typedef void __fastcall (__closure *TGetPropEditProc)(TPropertyEditor* Prop); class PASCALIMPLEMENTATION TPropertyEditor : public System::TObject { typedef System::TObject inherited; private: _di_IFormDesigner FDesigner; TInstProp *FPropList; int FPropCount; AnsiString __fastcall GetPrivateDirectory();
12 9721 CH10
11/13/00
9:44 AM
Page 581
Creating Property and Component Editors CHAPTER 10
LISTING 10.1
581
Continued
void __fastcall SetPropEntry(int Index, Classes::TPersistent* AInstance, Typinfo::PPropInfo APropInfo); protected: __fastcall virtual TPropertyEditor(const _di_IFormDesigner ADesigner, int APropCount); Typinfo::PPropInfo __fastcall GetPropInfo(void); Extended __fastcall GetFloatValue(void); Extended __fastcall GetFloatValueAt(int Index); __int64 __fastcall GetInt64Value(void); __int64 __fastcall GetInt64ValueAt(int Index); Sysutils::TMethod __fastcall GetMethodValue(); Sysutils::TMethod __fastcall GetMethodValueAt(int Index); int __fastcall GetOrdValue(void); int __fastcall GetOrdValueAt(int Index); AnsiString __fastcall GetStrValue(); AnsiString __fastcall GetStrValueAt(int Index); Variant __fastcall GetVarValue(); Variant __fastcall GetVarValueAt(int Index); void __fastcall Modified(void); void __fastcall SetFloatValue(Extended Value); void __fastcall SetMethodValue(const Sysutils::TMethod &Value); void __fastcall SetInt64Value(__int64 Value); void __fastcall SetOrdValue(int Value); void __fastcall SetStrValue(const AnsiString Value); void __fastcall SetVarValue(const Variant &Value); public: __fastcall virtual ~TPropertyEditor(void);
10 PROPERTY AND COMPONENT EDITORS
virtual void __fastcall Activate(void); virtual bool __fastcall AllEqual(void); virtual bool __fastcall AutoFill(void); virtual void __fastcall Edit(void); virtual TPropertyAttributes __fastcall GetAttributes(void); Classes::TPersistent* __fastcall GetComponent(int Index); virtual int __fastcall GetEditLimit(void); virtual AnsiString __fastcall GetName(); virtual void __fastcall GetProperties(TGetPropEditProc Proc); Typinfo::PTypeInfo __fastcall GetPropType(void); virtual AnsiString __fastcall GetValue(); AnsiString __fastcall GetVisualValue(); virtual void __fastcall GetValues(Classes::TGetStrProc Proc); virtual void __fastcall Initialize(void);
12 9721 CH10
11/13/00
582
9:44 AM
Page 582
C++Builder 5 Essentials PART I
LISTING 10.1
Continued
void __fastcall Revert(void); virtual void __fastcall SetValue(const AnsiString Value); bool __fastcall ValueAvailable(void); DYNAMIC void __fastcall ListMeasureWidth(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AWidth); DYNAMIC void __fastcall ListMeasureHeight(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AHeight); DYNAMIC void __fastcall ListDrawValue(const AnsiString Value, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected); DYNAMIC void __fastcall PropDrawName(Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected); DYNAMIC void __fastcall PropDrawValue(Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected); __property __property __property __property
_di_IFormDesigner Designer = {read=FDesigner}; AnsiString PrivateDirectory = {read=GetPrivateDirectory}; int PropCount = {read=FPropCount, nodefault}; AnsiString Value = {read=GetValue, write=SetValue};
};
To customize the editor’s behavior, one or more TPropertyEditor virtual (or DYNAMIC) functions must be overridden. You can save a lot of coding by deriving your custom property editor from the most appropriate property editor class. The hierarchy of TPropertyEditor descendants is shown in Figure 10.1. Descendants in shaded boxes are those that override the custom rendering functionality of TPropertyEditor. See the section “Using Images in Property Editors,” later in this chapter, for more information. The hierarchy shown in Figure 10.1 is useful when deciding which property editor to inherit from. The purpose of each property editor is fairly self-explanatory, with the exception of one or two of the more specialized. For your convenience, brief descriptions of the more commonly encountered property editors are given in Table 10.2.
12 9721 CH10
11/13/00
9:44 AM
Page 583
Creating Property and Component Editors CHAPTER 10
583
TObject TPropertyEditor TOrdinalProperty TIntegerProperty TFontCharsetProperty TColorProperty TCursorProperty TModalResultProperty TTabOrderProperty TCharProperty TEnumProperty TBoolProperty TBrushProperty TPenProperty TSetProperty TShortCutProperty TInt64Property TFloatProperty TStringProperty TComponentNameProperty TFontNameProperty TImeNameProperty TMPFileNameProperty TCaptionProperty TNestedProperty TSetElementProperty TClassProperty TFontProperty TMethodProperty
10
TComponentProperty
TTimeProperty TDateTimeProperty
FIGURE 10.1 The TPropertyEditor inheritance hierarchy.
PROPERTY AND COMPONENT EDITORS
TDateProperty
12 9721 CH10
11/13/00
584
9:44 AM
Page 584
C++Builder 5 Essentials PART I
TABLE 10.2
Common Property Editor Classes and Their Use
Property Editor Class
Use
TCaptionProperty
The editor for all Caption and Text named AnsiString properties. The Caption property of TForm and the Text property of TEdit are examples. The difference between this property editor and the TStringProperty from which it derives is that the component being edited is continually updated as the property is edited. With TStringProperty the updating occurs after the edit has finished. The default editor for all char properties and sub-types of char. Displays either the character of the property’s value or the value itself preceded by the # character. The PasswordChar (char) property of TMaskEdit is an example. The default editor for TPersistent-derived class properties. Published properties of the class are displayed as sub-properties when the + image before the property name is clicked. The Constraints (TSizeConstraints*) property of TForm is an example. The default editor for TColor type properties. Displays the color as a clXXX value if one exists, otherwise displays the value as hexadecimal (in BGR format; 0x00BBGGRR). The value can be entered as either a clXXX value or as a number. Also allows the clXXX value to be picked from a list. When the property is double-clicked, the Color dialog is displayed. The Color (TColor) property of TForm is an example. The default editor for pointers to TComponent-derived objects. The editor displays a drop-down list of type-compatible objects that appear in the same form as the component being edited. The Images (TCustomImageList*) property of TToolBar is an example. For TCursor properties. Allows a cursor to be selected from a list that gives each cursor’s name and its corresponding image. The Cursor (TCursor) property of TForm is an example. The default editor for all enum-based properties. A drop-down list displays the possible values the property can take. The Align (TAlign) and BorderStyle (TFormBorderStyle) properties of TForm are examples. The default editor for all floating-point–based properties, namely double, long double, and float. The PrintLeftMargin (double) and PrintRightMargin (double) properties of TF1Book are examples.
TCharProperty
TClassProperty
TColorProperty
TComponentProperty
TCursorProperty
TEnumProperty
TFloatProperty
12 9721 CH10
11/13/00
9:44 AM
Page 585
Creating Property and Component Editors CHAPTER 10
TABLE 10.2
585
Continued
Property Editor Class
Use
TFontProperty
For TFont properties. The editor allows the font settings to be edited either through the Font dialog (by clicking the ellipses button) or by editing an expandable list of sub-properties. The Font (TFont) property of TForm is an example. The default editor for all int properties. The Height (int) and Width (int) properties of TForm are examples. The default editor for pointer-to-method (member function) properties; that is, events. The editor displays a drop-down list of event handlers for the event type matching that of the property. The OnClick and OnClose events of TForm are examples. All ordinal- (that is integral) based property editors ultimately descend from this class, such as TIntegerProperty, TCharProperty, TenumProperty, and TSetProperty. The class from which all property editors are descended. This editor is used to edit the individual elements of a Set. The property can be set to true to indicate that the element is contained in the Set and false to indicate that it is not. The default editor for all Set properties. Each element of the Set is displayed as a sub-property of the Set property allowing each element to be removed from or added to the Set as desired. The Anchors (TAnchors) and BorderIcons (TBorderIcons) properties of TForm are examples. The default editor for AnsiString properties. The Hint and Name properties of TForm are examples.
TIntegerProperty TMethodProperty
TOrdinalProperty
TPropertyEditor TSetElementProperty
TSetProperty
TStringProperty
Choosing the right property editor to inherit from is linked inextricably with the requirements specification of the property editor. In fact, the hardest part of creating a custom property editor is deciding exactly what behavior is required. This is an issue that will come up later in this section. The stages of developing a new property editor are summarized in the following list:
2. Decide whether a custom property editor is even required. By slightly changing how a property is used, it may be that no custom property editor is necessary. To this end, it is
10 PROPERTY AND COMPONENT EDITORS
1. Decide exactly how you want the editor to behave. Property editors often are developed to offer a bounded choice that ensures proper component operation and an intuitive interface. The nature of bounding, such as to restrict the user to a choice of some discrete predefined values, must be decided.
12 9721 CH10
11/13/00
586
9:44 AM
Page 586
C++Builder 5 Essentials PART I
important to know which property editors are registered for which property types; Table 10.2 can be used as a guide. Because this section is about creating custom property editors, this thread will not be explored further. Needless to say, you cannot know too much about the existing property editors and how they work. A good source of information is the $(BCB)\Source\ToolsApi\DsgnIntf.pas file. 3. Choose carefully the property editor from which your custom property editor descends. A careful choice can save a lot of unnecessary coding. 4. Decide which property attributes are applicable to your property editor. 5. Determine which functions of the parent property editor need to be overridden and which do not. 6. Finally, write the necessary code and try it out. Once it has been decided that a custom property editor is required and the parent property editor class has been chosen, the next step is to decide which property attributes are suitable. Every property editor has a method called GetAttributes() that returns a TPropertyAttributes Set. This tells the Object Inspector how the property is to be used. For example, if the property will display a drop-down list of values, you must ensure that paValueList is contained by the TPropertyAttributes Set returned by the property editor’s GetAttributes() method. Unless the property attributes of the parent property editor class exactly match those required in the custom property editor, the GetAttributes() method must be overridden. Table 10.3 shows the different values that can be contained by the TPropertyAttributes Set. Methods that may require overridding as a result of the property editor having a particular attribute are also shown. TABLE 10.3
TPropertyAttributes Set Values
Value
Purpose
paAutoUpdate
Properties whose editors have this attribute are updated automatically as they are changed in the Object Inspector, such as the Caption property of TLabel. Normally a property will not be updated until the Return key is pressed or focus leaves the property. SetValue() is called to convert the AnsiString representation to the proper format and ensure the value is valid. Overriding SetValue() is probably necessary. Override: SetValue(const AnsiString Value) Properties with this attribute display an ellipsis button (...) on the right side of the property value region. When clicked, this displays a form to allow the property to be edited. When the ellipses button is pressed, the Edit() method of the property editor is invoked. This must therefore be overridden for properties with this attribute. Override: Edit()
paDialog
12 9721 CH10
11/13/00
9:44 AM
Page 587
Creating Property and Component Editors CHAPTER 10
TABLE 10.3
587
Continued
Value
Purpose
paFullWidthName
paMultiSelect
paReadOnly paRevertable
paSortList paSubProperties
paValueList
Properties with this attribute do not display a value region in the Object Inspector. Rather, the property name extends to the full width of the Object Inspector. Properties whose editors have this attribute may be edited when more than one component is selected on a form. For example, the property editor for the Caption property of TLabel and TButton has this attribute. When several TLabel and TButton components are placed on a form and selected, the Caption properties can be edited simultaneously. The Object Inspector displays all properties whose editors have the paMultiSelect attribute and whose property names and types are exactly the same. Properties whose editors have this attribute cannot be edited in the Object Inspector. Properties whose editors have this attribute enable the Revert to Inherited menu item in the Object Inspector’s context menu, allowing the property editor to revert the current property value to some default value. Properties with this attribute have their value lists sorted by the Object Inspector. Properties with this attribute tell the Object Inspector that the property editor has sub-properties that can be edited. A + symbol is placed in front of the property name. The TFont property editor is an example of this. In order to tell the Object Inspector which subproperties to display, GetProperties() must be overridden. Override: GetProperties(TGetPropEditProc Proc) Properties whose editors have this attribute display a drop-down list of possible values that the property can take. A value may still be entered manually in the editable property value region. For example, TColor properties behave in this way. To provide a list of values for the Object Inspector to display, you must override the GetValues() method. Override: GetValues(Classes::TGetStrProc Proc)
PROPERTY AND COMPONENT EDITORS
Once the attributes of the property editor have been decided, it is easy to see which methods of the parent property editor must be overridden. Other methods may also require overriding; this will depend on the specifications of the property editor. Table 10.4 lists the virtual and DYNAMIC methods of TPropertyEditor. The methods are grouped and ordered according to their use and are not listed alphabetically.
10
12 9721 CH10
11/13/00
588
9:44 AM
Page 588
C++Builder 5 Essentials PART I
TABLE 10.4
The virtual and DYNAMIC Methods of TPropertyEditor
Method
Declaration and Purpose
GetAttributes()
virtual TPropertyAttributes __fastcall GetAttributes(void);
Returns a TPropertyAttributes Set. Invoked to set the property editor attributes. GetValue()
virtual AnsiString __fastcall GetValue();
Returns an AnsiString that represents the property’s value. By default (that is, in TPropertyEditor) it returns (unknown). Therefore, if you derive directly from TPropertyEditor, you must override this method to return the correct value. SetValue()
virtual void __fastcall SetValue(const AnsiString Value);
Called to set the value of a property. SetValue() must convert the AnsiString representation of the property’s value to a suitable format. If an invalid value is entered, SetValue() should throw an exception that describes the error. Note that SetValue() takes a const AnsiString as its parameter and returns void. An exception therefore is the only appropriate method of dealing with invalid values. Edit()
virtual void __fastcall Edit(void);
Invoked when the ellipses button is pressed or the property is double-clicked (GetAttributes() should return paDialog). Normally used to display a form to allow more intuitive editing of the property value. Edit() can call GetValue() and SetValue(), or it can read and write the property value directly. If this is the case, then input validation should be carried out. If an invalid value is entered, an exception describing the error should be thrown. GetValues()
virtual void __fastcall GetValues (Classes::TGetStrProcProc);
Only called when paValueList is returned by GetAttributes(). The single parameter Proc is of type TGetStrProc, a __closure (pointer to an instance member function), declared in $(BCB)\Include\Vcl\Classes.hpp as: typedef void __fastcall (__closure *TGetStrProc) (const AnsiString S);
12 9721 CH10
11/13/00
9:44 AM
Page 589
Creating Property and Component Editors CHAPTER 10
TABLE 10.4
589
Continued
Method
Declaration and Purpose The Proc parameter is in fact the address of a method with a const AnsiString called S as its single parameter, which adds the AnsiString passed to the property editor’s drop-down list. Call Proc(const AnsiString S) once for every value that should be displayed in the property value’s drop-down list, for example: Proc(value1); //value1 is an AnsiString Proc(value2); //value2 is an AnsiString
and so on. Activate()
virtual void __fastcall Activate(void);
Invoked when the property is selected in the Object Inspector. Allows the property editor attributes to be determined only when the property becomes selected (with the exception of paSubProperties and paMultiSelect). AllEqual()
virtual bool __fastcall AllEqual(void);
Returns a bool value. Called only when paMultiSelect is one of the property editor’s attributes (when it is returned by GetAttributes()). It determines if all properties of the same name and type for which that editor is registered are equal when more than one is selected at once (it returns true). If this is the case (they are equal), then GetValue() is called to display the value; otherwise the value region is blanked. AutoFill()
virtual bool __fastcall AutoFill(void);
Returns a bool value. Called only when paValueList is returned by GetAttributes(), it determines whether or not (returns true or false) the values returned by GetValues() can be selected incrementally in the Object Inspector. By default it returns true. GetEditLimit()
virtual int __fastcall GetEditLimit(void);
Returns an int representing the maximum number of input characters allowed in the Object Inspector for this property. Overriding this method allows this number to be changed. The default value for the Object Inspector is 255. GetName()
virtual AnsiString __fastcall GetName();
PROPERTY AND COMPONENT EDITORS
Returns an AnsiString that is used by the Object Inspector to display the property name. This should be overridden only when the name determined from the property’s type information is not the name that you want to appear in the Object Inspector.
10
12 9721 CH10
11/13/00
590
9:44 AM
Page 590
C++Builder 5 Essentials PART I
TABLE 10.4
Continued
Method
Declaration and Purpose
GetProperties()
virtual void __fastcall GetProperties(TGetPropEditProc Proc);
If it is required that subproperties be displayed, then you must override this method. The single parameter Proc is of type TGetPropEditProc, a __closure declared in $(BCB)\Include\Vcl\DsgnIntf.hpp as typedef void __fastcall (__closure *TGetPropEditProc)(TPropertyEditor* Prop); Proc is therefore the address of a method with a pointer to a TPropertyEditor-derived editor called Prop as its single parameter. Call Proc(TPropertyEditor* Prop) once for each subproperty, passing a pointer to a TPropertyEditor-derived editor as an argument. For example, TSetProperty overrides this method and passes a TSetElementProperty pointer for each element in its Set. TClassProperty also overrides the GetProperties()
method, displaying a subproperty for each of the class’s published properties. Initialize()
virtual void __fastcall Initialize(void);
This is invoked when the Object Inspector is going to use the property editor. Initialize() is called after the property editor has been constructed but before it is used. When several components are selected at once, property editors are constructed but are often then discarded because they will not be used. This method allows the possibility of postponing certain operations until it is certain that they will be required. ListMeasureWidth()
DYNAMIC void __fastcall ListMeasureWidth(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AWidth);
This is called during the width calculation phase of the property’s drop-down list. If images are to be placed alongside text in the drop-down list, this method should be overridden to ensure the list is wide enough. ListMeasureHeight()
DYNAMIC void __fastcall ListMeasureHeight(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AHeight);
This is called during the height calculation phase of the property’s drop-down list. If an image’s height is greater than that of the property value text, this must be overridden to prevent clipping the image.
12 9721 CH10
11/13/00
9:44 AM
Page 591
Creating Property and Component Editors CHAPTER 10
TABLE 10.4
591
Continued
Method
Declaration and Purpose
ListDrawValue()
DYNAMIC void __fastcall ListDrawValue(const AnsiString Value, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected);
This is called to render the current list item in the property’s drop-down list. If an image is to be rendered, this method must be overridden. The default behavior of this method is to render the text representing the current list value. PropDrawValue()
DYNAMIC void __fastcall PropDrawValue(Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected);
This is called when the property value itself is to be rendered in the Object Inspector. If an image is to be rendered with the property value, this method must be overridden. PropDrawName()
DYNAMIC void __fastcall PropDrawName(Graphics::TCanvas* ACanvas, const Windows::TRect &ARect, bool ASelected);
This is called when the property name is to be rendered in the Object Inspector. If an image is to be rendered with the property name, this method must be overridden. However, this is rarely needed. You now should have a reasonable idea of the capabilities that can be implemented for a custom property editor. The next few sections look at some of the most important methods and present basic coding guidelines for their proper use. The last five methods (ListMeasureWidth(), ListMeasureHeight(), ListDrawValue(), PropDrawValue(), and PropDrawName()) are concerned with rendering images in the Object Inspector and are looked at in the section “Using Images in Property Editors,” later in this chapter. The methods that are most often overridden by custom property editors are GetAttributes(), and GetValues(), the first five methods in Table 10.4. Listing 10.2 shows a class definition for a custom property editor derived from TPropertyEditor. GetValue(), SetValue(), Edit(),
A key point to note in Listing 10.2 is the use of the typedef: typedef TPropertyEditor inherited;
PROPERTY AND COMPONENT EDITORS
TIP
10
12 9721 CH10
11/13/00
592
9:44 AM
Page 592
C++Builder 5 Essentials PART I
This allows inherited to be used as a namespace modifier in place of TPropertyEditor. This is commonly encountered in the VCL and makes it easy to call parent (in this case TPropertyEditor) methods explicitly while retaining code maintainability. If the name of the parent class changes, only this one occurrence needs to be updated. For example, you can write code such as this in the property editor’s GetAttributes() method: return inherited::GetAttributes() << paValueList >> paMultiSelect
This calls the property editor’s base class GetAttributes() method, returning a TPropertyAttributes Set. paValueList is added to this Set, and paMultiSelect is removed from the Set. The final Set is returned.
LISTING 10.2
Definition Code for a Custom Property Editor
class TCustomPropertyEditor : public TPropertyEditor { typedef TPropertyEditor inherited; public: virtual virtual virtual virtual virtual
TPropertyAttributes __fastcall GetAttributes(void); AnsiString __fastcall GetValue(); void __fastcall SetValue(const AnsiString Value); void __fastcall Edit(void); void __fastcall GetValues(Classes::TGetStrProc Proc);
protected: #pragma option push -w-inl inline __fastcall virtual TCustomPropertyEditor(const _di_IFormDesigner ADesigner, int APropCount) : TPropertyEditor(ADesigner, APropCount) { } #pragma option pop public: #pragma option push -w-inl inline __fastcall virtual ~TCustomProperty(void) { } #pragma option pop };
12 9721 CH10
11/13/00
9:44 AM
Page 593
Creating Property and Component Editors CHAPTER 10
593
The GetAttributes() Method The GetAttributes() method is very simple to implement. The only consideration is that you should change only the attributes that the parent class returns that have a direct effect on your code. Remaining attributes should be unchanged, so that you add only attributes that you definitely need and remove only attributes that you definitely don’t want. Be sure to check the attributes of the parent class. You may not need to change them at all. For example, a property editor that derives directly from TPropertyEditor is required to display a drop-down list of values and should not be used when multiple components are selected. Suitable code for the GetAttributes() method is TPropertyAttributes __fastcall TCustomPropertyEditor::GetAttributes(void) { return inherited::GetAttributes() << paValueList >> paMultiSelect; }
Since TPropertyEditor::GetAttributes() returns paRevertable, the following is the same: TPropertyAttributes __fastcall TCustomPropertyEditor::GetAttributes(void) { return TPropertyAttributes() << paValueList << paRevertable >> paMultiSelect; }
The GetValue() Method Use the GetValue() method to return an AnsiString representation of the value of the property being edited. To do this, use one of the GetXxxValue() methods from the TPropertyEditor class, where Xxx will be one of Float, Int64, Method, Ord, Str, or Var. These are listed in Table 10.5. TABLE 10.5
TPropertyEditor GetXxxValue() Methods
Method
Description
GetFloatValue()
Returns an Extended value, in other words a long double. Used to retrieve floating-point property values, such as float, double, and long double. Returns an __int64 value. Used to retrieve Int64 (__int64) property values. Returns a TMethod structure:
GetInt64Value() GetMethodValue()
property values, in other words, events.
10 PROPERTY AND COMPONENT EDITORS
struct TMethod { void *Code; void *Data; }; Used to retrieve Closure
12 9721 CH10
11/13/00
594
9:44 AM
Page 594
C++Builder 5 Essentials PART I
TABLE 10.5
Continued
Method
Description
GetOrdValue()
Returns an int value. Used to retrieve Ordinal property values such as char, signed char, unsigned char, int, unsigned, short and long. Can also be used to retrieve a pointer value; the int must be cast to the appropriate pointer using reinterpret_cast. Returns an AnsiString value. Used to retrieve string (AnsiString) property values.
GetStrValue() GetVarValue()
Returns a Variant by value. Used to retrieve Variant property values. The Variant class models Object Pascal’s intrinsic variant type. Refer to the online help for a description of Variants.
The following code shows an implementation of the GetValue() method to retrieve the value of a char-based property by calling the GetOrdValue() method. AnsiString __fastcall TCustomPropertyEditor::GetValue() { char ch = static_cast(GetOrdValue()); if(ch > 32 && ch < 128) return ch; else return AnsiString().sprintf(“#%d”, ch); // // // //
Note the ‘#’ character is pre-pended to characters that cannot be displayed directly. This is how the VCL displays non-printable character values, for example #8 is the backspace character (\b).
}
Notice the use of static_cast to cast the returned int value as a char. The casting operators are often used when overriding the GetValue() and SetValue() methods of TPropertyEditor. It is essential that their proper use be understood.
The SetValue() Method Use the SetValue() method to set a property’s actual value by converting the AnsiString representation to a suitable format. To do this, use one of the SetXxxValue() methods from TPropertyEditor, where Xxx will be one of Float, Int64, Method, Ord, Str, or Var. These are listed in Table 10.6.
12 9721 CH10
11/13/00
9:44 AM
Page 595
Creating Property and Component Editors CHAPTER 10
TABLE 10.6
595
TPropertyEditor SetXxxValue() Methods
Method
Sets
SetFloatValue()
Pass an Extended (long double) value as an argument. Used to set floating-point property values, namely float, double, long double. Pass an __int64 value as an argument. Used to set Int64 (__int64) property values. Pass a TMethod structure as an argument. Used to set Closure (event) property values. Pass an int value as an argument. Used to set Ordinal property values, namely char, signed char, unsigned char, int, unsigned, short, long. It can also be used to set pointer property values, though the pointer value must first be cast to an int using reinterpret_cast. Pass an AnsiString as an argument. Used to set string (AnsiString) property values. Pass a Variant as an argument. Used for variant (Variant) property values.
SetInt64Value() SetMethodValue() SetOrdValue()
SetStrValue() SetVarValue()
should ensure that values passed to it are valid before calling one of the SetXxxValue() methods, and it should raise an exception if this is not the case. The EPropertyError exception is sensible to use or serve as a base class from which to derive your own exception class. Sample code for an int property is shown in the following, where a value of less than zero is not allowed: SetValue()
void __fastcall TCustomPropertyEditor::SetValue(const AnsiString Value) { if(Value.ToInt() < 0) { throw EPropertyError(“The value must be greater than 0”); } else SetOrdValue(Value.ToInt()); }
The Edit() Method
__property AnsiString Value = {read=GetValue, write=SetValue};
10 PROPERTY AND COMPONENT EDITORS
The Edit() method is generally used to offer a better interface to the user. Often this is a form behaving as a dialog. The Edit() method can also call GetValue() and SetValue() or even call the GetXxxValue() and SetXxxValue() methods. It should be noted at this point that TPropertyEditor (and derived classes) has a property called Value whose read and write methods are GetValue() and SetValue(), respectively. Its declaration is
12 9721 CH10
11/13/00
596
9:44 AM
Page 596
C++Builder 5 Essentials PART I
This can be used instead of calling GetValue() and SetValue() directly. Regardless of how GetValue() and SetValue() are called, the Edit() method should be able to display a suitable form to allow intuitive editing of the property’s value. There are two basic approaches that can be taken. The first is to allow the form to update the property’s value while it is displayed. The second is to use the form as a dialog to retrieve the desired value or values from the user and then set the property’s value when the form returns a modal result of mrOK upon closure. Which of the two approaches is taken affects the code that appears in the Edit() method. Now we’ll consider the first instance, in which the form will continually update the value of the property. There are two basic types of property value: one that represents a single entity, such as an int, and one that represents a collection of values, such as the class TFont (though the property editor for TFont behaves according to the second approach). The difference between the two is in how Value is used to update the property. In a class property, Value is a pointer. For the form to be able to update the property, it must have the address of Value or whatever Value points to. For a class property this is simple; the pointer to the class is read from Value, and the class’s values are edited through that pointer. A convenient way to do this is to declare a property of the same type as the property to be edited. This can then be equated to Value before the form is shown, allowing initial values to be displayed and stored. In a single entity, a reference to Value should be passed in the form’s constructor. Using a reference to Value ensures that each time it is modified the GetValue() and SetValue() methods are called. The only other consideration for this approach is that it is probably a good idea to store the value or values that the property had when the form was originally shown. This allows the edit operation to be cancelled and any previous value or values restored. Suitable code for these situations is shown in Listing 10.3 and Listing 10.4, for a class property and a single entity property, respectively. LISTING 10.3
Code for a Custom Form to Be Called from the Edit() Method for a Class
Property // First show important code for TMyPropertyForm // IN THE HEADER FILE //---------------------------------------------------------------------------// #ifndef MyPropertyFormH #define MyPropertyFormH //---------------------------------------------------------------------------// #include #include #include <StdCtrls.hpp>
12 9721 CH10
11/13/00
9:44 AM
Page 597
Creating Property and Component Editors CHAPTER 10
LISTING 10.3
597
Continued
#include #include “HeaderDeclaringTPropertyClass” //---------------------------------------------------------------------------// class TMyPropertyForm : public TForm { __published: // IDE-managed Components private: TPropertyClass* FPropertyClass; // Other declarations here for example restore values if ‘Cancel’ // is pressed protected: void __fastcall SetPropertyClass(TPropertyClass* Pointer); public: __fastcall TMyPropertyForm(TComponent* Owner); __property TPropertyClass* PropertyClass = {read=FPropertyClass, write=SetPropertyClass}; // Other declarations here }; //---------------------------------------------------------------------------// #endif // THE IMPLEMENTATION FILE //---------------------------------------------------------------------------// #include #pragma hdrstop
10 PROPERTY AND COMPONENT EDITORS
#include “MyPropertyForm.h” //---------------------------------------------------------------------------// #pragma package(smart_init) #pragma resource “*.dfm” //---------------------------------------------------------------------------// __fastcall TMyPropertyForm::TMyPropertyForm(TComponent* Owner) : TForm(Owner) { } //---------------------------------------------------------------------------// void __fastcall TMyPropertyForm::SetPropertyClass(TPropertyClass* Pointer) { FPropertyClass = Pointer; if(FPropertyClass != 0) { // Store current property values }
12 9721 CH10
11/13/00
598
9:44 AM
Page 598
C++Builder 5 Essentials PART I
LISTING 10.3
Continued
} //---------------------------------------------------------------------------// // NOW SHOW THE Edit() METHOD #include “MyPropertyForm.h” // Remember this void __fastcall TCustomPropertyEditor::Edit(void) { // Create the form std::auto_ptr MyPropertyForm(new TMyPropertyForm(0)); // Link the property MyPropertyForm->PropertyClass = reinterpret_cast(GetOrdValue()); // Show the form. The form does all the work. MyPropertyForm->ShowModal(); } //---------------------------------------------------------------------------//
Notice the use of reinterpret_cast to convert the ordinal (int) representation of the pointer to the class to an actual pointer to the class. Listing 10.4 is shorter than Listing 10.3 because only the different code is shown. LISTING 10.4
Code for a Custom Form to Be Called from the Edit() Method for an int
Property // First show important code for TMyPropertyForm //---------------------------------------------------------------------------// // IN THE HEADER FILE CHANGE THE DEFINITION TO: class TMyPropertyForm : public TForm { __published: // IDE-managed Components private: AnsiString& Value; int OldValue; // Other decalrations here public: __fastcall TMyPropertyForm(TComponent* Owner, AnsiString& PropertyValue); // Other declarations here }; //---------------------------------------------------------------------------//
12 9721 CH10
11/13/00
9:44 AM
Page 599
Creating Property and Component Editors CHAPTER 10
LISTING 10.4
599
Continued
#endif //---------------------------------------------------------------------------// // IN THE IMPLEMENTATION FILE MODIFY THE CONSTRUCTOR TO: __fastcall TMyPropertyForm::TMyPropertyForm(TComponent* Owner, AnsiString& PropertyValue) : TForm(Owner),Value(PropertyValue) { // Store the current property value. In this case it is an int // so code such as this is required OldValue = Value.ToInt(); } //---------------------------------------------------------------------------// // NOW SHOW THE Edit() METHOD, almost the same... #include “MyPropertyForm.h” // Remember this void __fastcall TCustomPropertyEditor::Edit(void) { // Create the form as before, but pass the extra parameter! std::auto_ptr MyPropertyForm(new TMyPropertyForm(0, Value)); // Show the form. The form does all the work. MyPropertyForm->ShowModal(); } //---------------------------------------------------------------------------//
The difference between the second approach and the previous approach is that the value is modified after the modal form returns rather than continually modifying it while the form is displayed. This is the more common way to use a form to edit a property’s value. Listing 10.5 shows the basic code required in the Edit() method. LISTING 10.5
Code for a Custom Form to Be Called from the Edit() Method with No Updating Until Closing
void __fastcall TCustomPropertyEditor::Edit(void) { // Create the form
10 PROPERTY AND COMPONENT EDITORS
#include “MyPropertyDialog.h” // Include the header for the Dialog! // Dialog is TMyPropertyDialog
12 9721 CH10
11/13/00
600
9:44 AM
Page 600
C++Builder 5 Essentials PART I
LISTING 10.5
Continued
std::auto_ptr MyPropertyDialog(new TMyPropertyDialog(0)); // // // //
Set the current property values in the dialog MyPropertyDialog->value1 = GetValue(); MyPropertyDialog->value2 = GetXxxValue(); and so on...
// Show the form and see the result. if(MyPropertyDialog->ShowModal() == IDOK) { // Then set the new property value(s) } }
Note that TMyPropertyDialog might not be a dialog itself, but a wrapper for a dialog, similar to the standard dialog components. If this is the case, then the dialog would be shown by calling the wrapper’s Execute() method. For more information on this method of displaying a dialog, refer to the C++Builder online help under “Making a Dialog Box a Component.” In this case, such a dialog wrapper need only descend from TObject, not TComponent.
The GetValues() Method The GetValues() method is used to populate the drop-down list of a property. This is done by successively calling Proc() and passing an AnsiString representation of the value. For example, if a series of values is desired that represents the transmission rate between a computer’s communication port and an external modem, then assuming the property editor had paValueList as an attribute, the GetValues() method could be written as follows: void __fastcall GetValues(Classes::TGetStrProc Proc) { Proc(“300”); Proc(“9600”); Proc(“57600”); // and so on... }
Using the TPropertyEditor Properties TPropertyEditor has four properties that can be used when writing custom property editors. One of these, Value, we have already met in the previous two sections. The remaining three properties are not used very often. They are described in the following list:
12 9721 CH10
11/13/00
9:44 AM
Page 601
Creating Property and Component Editors CHAPTER 10
•
This property is read-only and returns a pointer to the IDE’s IFormDesigner interface. This is used to inform the IDE when certain events occur or to request the IDE to perform certain actions. For example, if you write your own implementation for one of the SetXxxValue() methods, you must tell the IDE that you have modified the property. You do this by calling Designer->Modifed();. In fact, you would call TPropertyEditor’s Modified() method, which calls the same code. TPropertyEditor’s Revert() method also uses this property. You probably will not need to use this property. It is shown for completeness.
•
PrivateDirectory
•
PropCount
601
Designer
This property is a directory, represented as an AnsiString, as returned by GetPrivateDirectory(), which itself obtains the directory from Designer->GetPrivateDirectory(). Hence we can see that this directory is specified by the IDE. If your property editor requires a directory to store additional files, then it should be the directory specified by this property. This property is read-only. This property is read-only and returns the number of properties being edited when more than one component is selected. It is only used when GetAttributes() returns paMultiSelect.
Considerations when Choosing a Suitable Property Editor Consider a property in a component that wraps the Windows communication API and allows different baud rates to be set. The values that may be chosen are predetermined, though a userdefined baud rate may be specified. What is the best way to enter such values? It would be nice to have a drop-down list of choices. It also would be nice if the values in the drop-down list were numbers, not enumerations. The first thought that springs to mind is a custom property editor that descends from TIntegerProperty but displays a drop-down list of the values that may be set. A user-defined value could be entered in the editing region of the property value in the Object Inspector. This is trivial to implement and will work fine.
We could restrict the values allowable to only those in the drop-down list by overriding the method and then creating two separate properties: one to enter a user-defined baud rate and a Boolean property to indicate which we want to use.
SetValue()
10 PROPERTY AND COMPONENT EDITORS
Have we really thought about whether this is the best approach? Let’s think again. All is well when a value from the drop-down list is chosen, but we must detect when a user-defined value is entered. This is relatively simple but requires that all values in the list be compared with the value returned by the property. If it is different, it is a user-defined baud rate. The component must then request a user-defined baud rate from the communication API equal to the value entered. Some values may be too big or too small. We must therefore perform bounds checking each time a value is entered. Our property editor is simple, but we have to write an increasing amount of maintenance code to support it. Not only that, but all these problems will be revisited by the runtime code.
12 9721 CH10
11/13/00
602
9:44 AM
Page 602
C++Builder 5 Essentials PART I
It seems that we are doing an awful lot of code writing just to enter a simple integer. Let’s go back to the start and look at our original requirements. We want to be able to enter a value from a given list of possible values, and we want to be able to specify a user-defined value, which may not be acceptable. Our initial thought was probably to use an enumeration for the values, but the convenience of using actual integer values made that option seem more attractive. Let’s look at the enumeration route. A set of values is easily generated; we can even ensure that they appear in numerical order in the drop-down list by using underscores between the enumeration initials and the value. For example, given an enum called TBaudRate with the initials br, the baud rates 9600 and 115200 could be represented as br___9600 and br_115200, respectively. We can even add a brUserDefined value to the enum. When brUserDefined is selected, an int property can be read and the value tried. We therefore need this property as well. To do all this, we don’t need to create a custom property editor at all since TEnumProperty is already defined as an editor for enum based properties. We have a problem though: Any time we want to set or get a value at runtime, we must use the enumeration, and this is often not convenient. We must make this enumeration available to the component user. In the interest of keeping the global namespace clean, we could wrap the enum in a namespace, but this will make the enum even more of a hassle to use, so we won’t do that. In fact, most components don’t do this either. That is why initials are used in the enum’s values. For more information on naming enums, refer to the “Choosing Type Names” section in Chapter 3, “Programming in C++Builder.” UserDefined
So which is best? It all depends on exactly what is required of the property and the component as a whole. Since this is a hypothetical discussion, it is hard to choose which method is better. The one thing to remember is that you must make your components robust and easy to use. Overly complex code should be avoided especially, because it may hide some of the more subtle features of how your component works. The enumeration approach may be a bit of a hassle as you convert to and from int values, but everyone knows what you can and cannot do with them. The time you save on not having to write a custom property editor could be used elsewhere. Remember also that if you need to read a value, often you can simply create a read-only property so that, for example, the int value of the baud rate could be stored when it is successfully set by the enum property. This then could be read from an int-based read-only property. Always think carefully when you are writing property editors and components in general. Consider the big picture and think ahead.
Properties and Exceptions When a property value is to be changed and there is a possibility that the new value may not be valid, the function obtaining the value should detect the invalid value and throw an exception so that the user can enter a valid value. Where can the property value be changed? It can be requested to change from one of three places: from a property editor dialog, from a property
12 9721 CH10
11/13/00
9:44 AM
Page 603
Creating Property and Component Editors CHAPTER 10
603
editor, and from the property itself at runtime. The relationship between the three is shown in Figure 10.2. Note that the parameter to the SetValue() method is a const AnsiString even though it is pass-by-value (see Chapter 3 for a discussion of the const keyword). This only restricts Value from being modified within SetValue(). This is contrary to the normal use of const, where the main purpose of the keyword is to indicate that the argument passed to the function will not be modified. With pass-by-value, the argument is copied so it will not be modified in any way. If an error occurs, then throwing an exception is the only appropriate way of informing the user. The other set methods may also be written using this approach, that is, pass-by-value variables declared as const.
Edit property value from here
Property Editor Dialog private: TColor DialogColor; Public: void SetDialogColor(TColor NewColor);
void SetDialogColor(TColor NewColor) { // Implementation }
NewColor not valid: Exception thrown
NewColor valid: New Color assigned to Value
Edit property value from here
Property Editor _property AnsiString Value = {read=GetValue, write=SetValue};
void SetValue(const AnsiString Value) { // Implementation }
Value not valid: Exception thrown
Value valid: Value assigned to Color
Edit property value from here
Property _property TColor Color = {read=FColor, write=SetColor};
void SetColor (TColor Newcolor) { // Implementation } NewColor valid: New Color assigned to FColor
FIGURE 10.2 Exceptions thrown when editing a property.
10 PROPERTY AND COMPONENT EDITORS
FColor
NewColor not valid: Exception thrown
12 9721 CH10
11/13/00
604
9:44 AM
Page 604
C++Builder 5 Essentials PART I
From Figure 10.2 it can be seen that the set method for the property, in this case SetColor(), is ultimately called every time (unless an exception is thrown). It may then be tempting to detect only the validity of the property value at this stage and throw an exception from here. Remember that the purpose of throwing the exception is to detect the error and allow the user to enter a new value. By the time an exception is thrown from the property’s set method, the user’s edit operation is most likely finished. This may mean redisplaying a dialog or other inconveniences. You should throw an exception at the source of the error. Throwing an exception only from the property editor (or property editor dialog) is also no good, because the property editor will not be used at runtime, letting invalid values silently cause havoc. The solution is to throw an exception from the point of error. It may not be the easiest solution to implement, but it is the most robust.
Registering Custom Property Editors Registering property editors is almost straightforward. I say almost because even though RegisterPropertyEditor() is all that is required, the parameters that need to be passed are not always so trivial. As with other registration functions, the RegisterPropertyEditor() function must be placed inside the package’s Register() function. The declaration for the RegisterPropertyEditor() function is extern PACKAGE void __fastcall RegisterPropertyEditor(Typinfo::PTypeInfo PropertyType, TMetaClass* ComponentClass, const AnsiString PropertyName, TMetaClass* EditorClass);
Each parameter’s purpose and an example of its use are shown in Table 10.7. The PropertyType and PropertyName parameters are used to specify criteria that must be matched by a property for it to be considered for use with the property editor. TABLE 10.7
RegisterPropertyEditor() Parameters
Parameter Name
Purpose
PropertyType
This parameter expects a pointer to a TTypeInfo structure that contains type information for the property for which the editor is to be used. This parameter must be specified. If the property type is a VCL-derived class, the pointer can be obtained using the __typeinfo macro: __typeinfo(TVCLClass)
Otherwise, it must be obtained either by examining the typeinfo of a similar existing property or by manually creating it. Both techniques are discussed in this section.
12 9721 CH10
11/13/00
9:44 AM
Page 605
Creating Property and Component Editors CHAPTER 10
TABLE 10.7
605
Continued
Parameter Name
Purpose
ComponentClass
This parameter is used to specify whether or not the editor is to be used for all matching properties in all components or only matching properties in components of the type specified. To specify a particular component type, use the __classid operator (which returns TMetaClass* as required) with the component class name: __classid(TComponentClassName)
PropertyName
EditorClass
Otherwise, specify all components by passing 0 as the parameter. This parameter is used to specify a property name, in the form of an AnsiString, that a property must have (in addition to having the same type information). It is used to restrict the property specification. If all properties of matching type information are required, an empty AnsiString is passed (“”). If ComponentClass is 0, this parameter is ignored. This parameter must be specified. It tells the IDE which property editor you want to register. As in the ComponentClass parameter, a TMetaClass pointer is expected. The property editor class name is therefore passed wrapped in the __classid operator, such as __classid(TPropertyEditorClassName)
In Table 10.7 you can see that ComponentClass and PropertyName can both be given a value so that they do not restrict the property editor to a specific component class or property name, respectively. This is contrary to their normal use. The only parameter that requires any further comment is PropertyType. As was stated before, the __typeinfo macro can be used to retrieve this information if the property type is a VCL-based class (ultimately derived from TObject). The __typeinfo macro is defined in $(BCB)\Include\Vcl\Sysmac.h as #define __typeinfo(type)
(PTypeInfo)TObject::ClassInfo(__classid(type))
If the property is not a VCL class, then information must be obtained through other means. There are two approaches to this: Either the appropriate PTypeInfo can be obtained from the property’s name and the PTypeInfo of the class it belongs to or the PTypeInfo can be manually generated. PTypeInfo
is a pointer to a TTypeInfo structure:
TTypeInfo
is declared in $(BCB)\Include\Vcl\Typinfo.hpp as
struct TTypeInfo {
PROPERTY AND COMPONENT EDITORS
typedef TTypeInfo* PTypeInfo;
10
12 9721 CH10
11/13/00
606
9:44 AM
Page 606
C++Builder 5 Essentials PART I TTypeKind Kind; System::ShortString Name; }; TTypeKind,
declared in the same file, is an enumeration of type kinds. It is declared as
enum TTypeKind { tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat, tkString, tkSet, tkClass, tkMethod, tkWChar, tkLString, tkWString, tkVariant, tkArray, tkRecord, tkInterface, tkInt64, tkDynArray };
The Name variable is a string version of the actual type. For example, int is “int”, and AnsiString is “AnsiString”. The following two sections discuss how a TTypeInfo* pointer can be obtained for non-VCL property types.
Obtaining a TTypeInfo* (PTypeInfo) from an Existing Property and Class for a Non-VCL Type This approach requires that a VCL class containing the property already be defined and accessible. Then a PTypeInfo for that property type can be obtained using the GetPropInfo() function declared in $(BCB)\Include\Vcl\Typinfo.hpp. PPropInfo is a typedef for a TPropInfo pointer, as in the following: typedef TPropInfo* PPropInfo;
The GetPropInfo() function returns a pointer to a TPropInfo structure (PPropInfo) for a property within a particular class with a given property name, and optionally of a specific TTypeKind. It is available in one of four overloaded versions: extern PACKAGE PPropInfo __fastcall GetPropInfo(PTypeInfo TypeInfo, const AnsiString PropName); extern PACKAGE PPropInfo __fastcall GetPropInfo(PTypeInfo TypeInfo, const AnsiString PropName, TTypeKinds AKinds); extern PACKAGE PPropInfo __fastcall GetPropInfo(TMetaClass* AClass, const AnsiString PropName, TTypeKinds AKinds); extern PACKAGE PPropInfo __fastcall GetPropInfo(System::TObject* Instance, const AnsiString PropName, TTypeKinds AKinds);
12 9721 CH10
11/13/00
9:44 AM
Page 607
Creating Property and Component Editors CHAPTER 10
607
These overloaded versions all ultimately call the first overloaded version of the method listed, namely extern PACKAGE PPropInfo __fastcall GetPropInfo(PTypeInfo TypeInfo, const AnsiString PropName);
This is the version we are most interested in. The other versions also allow a Set of type TTypeKinds to be specified . This is a Set of the TTypeKind enumeration and is used to specify a TypeKind or TypeKinds that the property must also match. From the PPropInfo returned, we can obtain a pointer to an appropriate PTypeInfo for the property, which is the PropType field of the TPropInfo structure. TPropInfo is declared in $(BCB)\Include\Vcl\Typinfo.hpp as struct TPropInfo { PTypeInfo* PropType; void* GetProc; void* SetProc; void* StoredProc; int Index; int Default; short NameIndex; System::ShortString Name; };
For example, the PTypeInfo for the Name property of TFont can be obtained by first obtaining a PPropInfo: PPropInfo FontNamePropInfo = Typinfo::GetPropInfo(__typeinfo(TFont), “Name”);
Then obtain the PTypeInfo for the required property: PTypeInfo FontNameTypeInfo = *FontNamePropInfo->PropType;
This PTypeInfo value can now be passed to the RegisterPropertyEditor() function. What we have actually obtained from this is a pointer to the TTypeInfo for an AnsiString property. This PTypeInfo could therefore be obtained and used as the PTypeInfo parameter anytime the PTypeInfo for an AnsiString is required. Additionally, the PTypeInfo for a custom property for a custom component can be similarly obtained: PPropInfo CustomPropInfo = Typinfo::GetPropInfo(__typeinfo(TCustomComponent), “CustomPropertyName”);
Note that it is possibly more clear if TTypeInfo* and TPropInfo* are used instead of their respective typedefs (PTypeInfo and PPropInfo). The typedefs have been used here for easy comparison with the GetPropInfo() function declarations.
10 PROPERTY AND COMPONENT EDITORS
PTypeInfo CustomTypeInfo = *CustomPropInfo->PropType;
12 9721 CH10
11/13/00
608
9:44 AM
Page 608
C++Builder 5 Essentials PART I
The intermediate steps shown to obtain the PTypeInfo can be ignored. For example, the following can be used as an argument to RegisterPropertyEditor() for the custom property of a custom component: *(Typinfo::GetPropInfo(__typeinfo(TCustomComponent), “CustomPropertyName”))->PropType
This method of obtaining a TTypeInfo* relies on there being a published property of the desired type already in use by the VCL. This may not always be the case. Also, sometimes it may appear that a type already in use matches a type you want to use, but in fact it does not. An example of this is the Interval property of the TTimer component. The type of the Interval property is Cardinal, which is typedefed to unsigned int in the file $(BCB)\Include\Vcl\Sysmac.h. It is reasonable then to believe that retrieving the TypeInfo* for this property would allow you to register property editors for unsigned int properties. This is not so. You must have a property whose type is unsigned int, and it must appear in a C++-implemented class. There is an important lesson here. The TTypeInfo* for a non-VCL class type is not necessarily the same if the property belongs to an Object Pascal–implemented class and not a C++-implemented class. There is a very simple and effective way around this problem, and that is to create a class containing published properties of the types we desire. We then use the techniques previously discussed to retrieve a suitable TTypeInfo*, which we then use to register our property editor. Listing 10.6 shows such a class. LISTING 10.6
Non-VCL Property Types in a Single Class
class PACKAGE TNonVCLTypesClass : public TObject { public: __published: // Fundamental Integer Types __property int IntProperty = {}; __property unsigned int UnsignedIntProperty = {}; __property short int ShortIntProperty = {}; __property unsigned short int UnsignedShortIntProperty = {}; __property long int LongIntProperty = {}; __property unsigned long int UnsignedLongIntProperty = {}; __property char CharProperty = {}; __property unsigned char UnsignedCharProperty = {}; __property signed char SignedCharProperty = {}; // Fundamental Floating Point Types
12 9721 CH10
11/13/00
9:44 AM
Page 609
Creating Property and Component Editors CHAPTER 10
LISTING 10.6
609
Continued
__property double DoubleProperty = {}; __property long double LongDoubleProperty = {}; __property float FloatProperty = {}; // Fundamental Boolean type __property bool BoolProperty = {}; // The AnsiString class __property AnsiString AnsiStringProperty = {}; private: // Private Constructor, class cannot be instantiated from inline __fastcall TNonVCLTypesClass() : TObject() { } };
If you created a component called TTestComponent with an unsigned int property called Size, the following code would allow you to register a custom property editor: RegisterPropertyEditor( *(Typinfo::GetPropInfo (__typeinfo(TNonVCLTypesClass), “UnsignedIntProperty”) )->PropType, __classid(TTestComponent), “Size”, __classid(TUnsignedProperty) );
The first parameter is a bit confusing. It is shown again for clarification: *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “UnsignedIntProperty”))->PropType
This is the same as the code we saw earlier in this section. It’s not very attractive to look at or easy to write. To help make it easier to use, you can create a class that contains static member functions that return the correct TTypeInfo* for each type. The definition for such a class is shown in Listing 10.7. LISTING 10.7
NonVCLTypeInfo.h
10 PROPERTY AND COMPONENT EDITORS
//---------------------------------------------------------------------------// #ifndef NonVCLTypeInfoH #define NonVCLTypeInfoH //---------------------------------------------------------------------------// #ifndef TypInfoHPP
12 9721 CH10
11/13/00
610
9:44 AM
Page 610
C++Builder 5 Essentials PART I
LISTING 10.7
Continued
#include #endif //---------------------------------------------------------------------------// class PACKAGE TNonVCLTypeInfo : public TObject { public: // Fundamental Integer Types static PTypeInfo __fastcall Int(); static PTypeInfo __fastcall UnsignedInt(); static PTypeInfo __fastcall ShortInt(); static PTypeInfo __fastcall UnsignedShortInt(); static PTypeInfo __fastcall LongInt(); static PTypeInfo __fastcall UnsignedLongInt(); static PTypeInfo __fastcall Char(); static PTypeInfo __fastcall UnsignedChar(); static PTypeInfo __fastcall SignedChar(); // Fundamental Floating Point Types static PTypeInfo __fastcall Double(); static PTypeInfo __fastcall LongDouble(); static PTypeInfo __fastcall Float(); // Fundamental Boolean type static PTypeInfo __fastcall Bool(); // The AnsiString class static PTypeInfo __fastcall AnsiString(); private: // Private Constructor, class cannot be instantiated from inline __fastcall TNonVCLTypeInfo() : TObject() { } }; // The definition for TNonVCLTypesClass goes here (Listing 10.6) //---------------------------------------------------------------------------// #endif
12 9721 CH10
11/13/00
9:44 AM
Page 611
Creating Property and Component Editors CHAPTER 10
611
The implementation is shown in Listing 10.8. LISTING 10.8
NonVCLTypeInfo.cpp
#include #pragma hdrstop #include “NonVCLTypeInfo.h” //---------------------------------------------------------------------------// #pragma package(smart_init) //---------------------------------------------------------------------------// PTypeInfo __fastcall TNonVCLTypeInfo::Int() { return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “IntProperty”))->PropType; } PTypeInfo __fastcall TNonVCLTypeInfo::UnsignedInt() { return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “UnsignedIntProperty”))->PropType; } //---------------------------------------------------------------------------// PTypeInfo __fastcall TNonVCLTypeInfo::ShortInt() { return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “ShortIntProperty”))->PropType; } PTypeInfo __fastcall TNonVCLTypeInfo::UnsignedShortInt() { return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “UnsignedShortIntProperty”))->PropType; } //---------------------------------------------------------------------------//
PTypeInfo __fastcall TNonVCLTypeInfo::UnsignedLongInt()
10 PROPERTY AND COMPONENT EDITORS
PTypeInfo __fastcall TNonVCLTypeInfo::LongInt() { return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “LongIntProperty”))->PropType; }
12 9721 CH10
11/13/00
612
9:44 AM
Page 612
C++Builder 5 Essentials PART I
LISTING 10.8
Continued
{ return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “UnsignedLongIntProperty”))->PropType; } //---------------------------------------------------------------------------// PTypeInfo __fastcall TNonVCLTypeInfo::Char() { return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “CharProperty”))->PropType; } PTypeInfo __fastcall TNonVCLTypeInfo::UnsignedChar() { return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “UnsignedCharProperty”))->PropType; } PTypeInfo __fastcall TNonVCLTypeInfo::SignedChar() { return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “SignedCharProperty”))->PropType; } //---------------------------------------------------------------------------// PTypeInfo __fastcall TNonVCLTypeInfo::Double() { return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “DoubleProperty”))->PropType; } PTypeInfo __fastcall TNonVCLTypeInfo::LongDouble() { return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), ”LongDoubleProperty”))->PropType; } PTypeInfo __fastcall TNonVCLTypeInfo::Float() { return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “FloatProperty”))->PropType; } //---------------------------------------------------------------------------// PTypeInfo __fastcall TNonVCLTypeInfo::Bool()
12 9721 CH10
11/13/00
9:44 AM
Page 613
Creating Property and Component Editors CHAPTER 10
LISTING 10.8
613
Continued
{ return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “BoolProperty”))->PropType; } //---------------------------------------------------------------------------// PTypeInfo __fastcall TNonVCLTypeInfo::AnsiString() { return *(Typinfo::GetPropInfo(__typeinfo(TNonVCLTypesClass), “AnsiStringProperty”))->PropType; } //---------------------------------------------------------------------------//
Using our previous example of registering a property editor for an unsigned int property called Size in a component called TTestComponent, the registration function is RegisterPropertyEditor(TNonVCLTypeInfo::UnsignedInt(), __classid(TTestComponent), “Size”, __classid(TUnsignedProperty));
The previous code is simple, easy to understand, and easy to write. This should be your preferred method of registering property editors for non-VCL based properties. It was mentioned earlier that determining a TTypeInfo* for a non-VCL property implemented in Object Pascal is not the same as one implemented in C++. An example of this is the PasswordChar property of TMaskEdit. To register a new property editor for all char types requires two registrations: one for Object Pascal–implemented properties and one for C++ implementations. The previous approach (a special class containing the appropriate non-VCL type properties) works fine for C++ implementations, but in order to get the correct TTypeInfo* for the Object Pascal implementations, the TTypeInfo* pointer must be determined directly from the VCL class, in this case from the PasswordChar property of TMaskEdit. This was the very first way we used to obtain a TTypeInfo*. If we want to register a new char property editor called TCharPropertyEditor for all components and all properties of type char, the registrations required are TPropInfo* VCLCharPropInfo = Typinfo::GetPropInfo(__typeinfo(TMaskEdit), “PasswordChar”); // Register the property editor for native VCL (Object Pascal) components
PROPERTY AND COMPONENT EDITORS
RegisterPropertyEditor(*VCLCharPropInfo->PropType, 0, “”, __classid(TCharPropertyEditor));
10
12 9721 CH10
11/13/00
614
9:44 AM
Page 614
C++Builder 5 Essentials PART I // Register the property editor for C++ implemented components RegisterPropertyEditor(TNonVCLTypeInfo::Char(), 0, “”, __classid(TCharPropertyEditor));
Obtaining a TTypeInfo* (PTypeInfo) for a Non-VCL Type by Manual Creation Creating a TTypeInfo* manually is an alternative approach to obtaining a TTypeInfo* from a VCL class for a non-VCL type. It is shown largely for comparison purposes and also because it is a commonly used technique. However, it should generally be avoided in preference to the first method. Manually creating the required PTypeInfo pointer can be done in place before the call to RegisterPropertyEditor(), or the code can be placed in a function that will return the pointer. There are two ways to write the code to do this. One is to declare a static TTypeInfo structure locally, assign the appropriate values to it, and use a reference to it as the PTypeInfo argument. The other is to allocate a TTypeInfo structure dynamically, assign the appropriate values, and then use the pointer as the PTypeInfo argument. Both methods for generating a suitable PTypeInfo for an AnsiString property are shown in Listing 10.9. Note that this code and other similar functions are found in the GetTypeInfo unit on the CD-ROM. LISTING 10.9
Manually Creating a TTypeInfo*
//---------------------------------------------------------------------------// // As Functions // //---------------------------------------------------------------------------// TTypeInfo* AnsiStringTypeInfo(void) { static TTypeInfo TypeInfo; TypeInfo.Name = “AnsiString”; TypeInfo.Kind = tkLString; return &TypeInfo; } // OR TTypeInfo* AnsiStringTypeInfo(void) { TTypeInfo* TypeInfo = new TTypeInfo; TypeInfo->Name = “AnsiString”; TypeInfo->Kind = tkLString;
12 9721 CH10
11/13/00
9:44 AM
Page 615
Creating Property and Component Editors CHAPTER 10
LISTING 10.9
615
Continued
return TypeInfo; } //---------------- In the Registration code simply write:--------------------// RegisterPropertyEditor(AnsiStringTypeInfo(), 0 , “”, __classid(TAnsiStringPropertyEditor));
//---------------------------------------------------------------------------// // In Place Before RegisterPropertyEditor() // //---------------------------------------------------------------------------// static TTypeInfo AnsiStringTypeInfo; TypeInfo.Name = “AnsiString”; TypeInfo.Kind = tkLString; RegisterPropertyEditor(&AnsiStringTypeInfo, 0 , “”, __classid(TAnsiStringPropertyEditor)); // OR TTypeInfo* AnsiStringTypeInfo = new TTypeInfo; TypeInfo->Name = “AnsiString”; TypeInfo->Kind = tkLString; RegisterPropertyEditor(AnsiStringTypeInfo, 0 , “”, __classid(TAnsiStringPropertyEditor));
Notice that when the TTypeInfo structure is dynamically allocated (with new), it is not deleted after the call to RegisterPropertyEditor(). If this is done, the registration will fail. The reason for this is explained in the following section.
Which of the two approaches you use to obtain a TTypeInfo* for a non-VCL type—determine it from a VCL class or manually create it—is straightforward. Always use the first method when you can. In particular, you must use the first method if you are writing a property editor
PROPERTY AND COMPONENT EDITORS
How to Obtain a TTypeInfo* for a Non-VCL Type
10
12 9721 CH10
11/13/00
616
9:44 AM
Page 616
C++Builder 5 Essentials PART I
to override an existing property editor for which an editor has been specifically registered by the VCL (as opposed to being determined dynamically) or one that has been previously registered using the first approach. In general, the first approach is more robust, because you are using the VCL’s representation of the TTypeInfo* for the given property. The need to use the first method to override a property editor registered using the first method should be noted. Creating a class with static member functions to return a suitable TTypeInfo* makes the first method just as easy as the manual creation method and should be considered the superior technique. An important point about using the two approaches is that writing a function to a specific PTypeInfo (the second method) is not the same as obtaining the PTypeInfo from the VCL (the first method). The reason for this is that the implementation of TPropertyClassRec, used internally by the RegisterPropertyEditor() function, maintains only a PTypeInfo variable, not the actual values that it points to, namely the Name and Kind of the TTypeInfo. This is why a reference to a locally declared non-static TTypeInfo structure cannot be used and a dynamically allocated TTypeInfo structure must not be deleted (it is simply abandoned on free store). Registering property editors is then relatively easy. However, care must be taken to ensure that the parameters passed are exact. Often it is possible to compile and install property editors that do not appear to function, only to find later that the registration code is not quite right (such as when the PropertyName parameter has been spelled incorrectly) and that the property editor worked all along.
Rules for Overriding Property Editors With the knowledge of how to register custom property editors and the realization that it is possible to override any previously installed property editor, the question is this: What are the rules for overriding property editors? The following highlights the two main considerations: • In general, property editors are used from newest to oldest. In other words, the most recently installed property editor for a given property will be used. The exception to this is noted in the next point. • A newly registered property editor will override an existing property editor only if the specification used to register it is at least as specific as that used to register the existing editor. For example, if a property editor is registered specifically for the Shape property (of type TShapeType) in the TShape component, then installing a new editor for properties of type TShapeType for all components (ComponentClass == 0) will not override the property editor for the Shape property of TShape. The only other consideration when overriding property editors is the method used to obtain the appropriate PTypeInfo, as previously discussed. Such property overriding can be seen in practice by examining the EnhancedEditors package on the accompanying CD-ROM.
12 9721 CH10
11/13/00
9:44 AM
Page 617
Creating Property and Component Editors CHAPTER 10
617
Using Images in Property Editors This section introduces the techniques required to render images in the Object Inspector for custom property editors. Some property editors already render images in the Object Inspector, and those were listed previously in Chapter 2, “C++Builder Projects and More on the IDE,” in Table 2.4. Inheriting a property editor from one of the property editors listed in Table 2.4 or using a type that is registered for one of those property editors enables the use of the image rendering already coded for each. For example, a property of type TColor will appear automatically in the Object Inspector as other TColor properties do. However, there are many more types of properties that could benefit from the use of images when editing the property. To facilitate this, TPropertyEditor (the base class for all property editors) has six new methods, five of which can be overridden. The declarations for the five overridable functions are as follows: DYNAMIC void __fastcall ListMeasureWidth(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AWidth); DYNAMIC void __fastcall ListMeasureHeight(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AHeight); DYNAMIC void __fastcall ListDrawValue(const AnsiString Value, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected); DYNAMIC void __fastcall PropDrawValue(Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected); DYNAMIC void __fastcall PropDrawName(Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected);
The remaining method, to be used in conjunction with the XxxxDrawValue methods, is declared as AnsiString __fastcall GetVisualValue();
10 PROPERTY AND COMPONENT EDITORS
12 9721 CH10
11/13/00
618
9:44 AM
Page 618
C++Builder 5 Essentials PART I
These are listed in Table 10.8, along with a description of the purpose of each. TABLE 10.8
New Methods for TPropertyEditor to Allow Custom Images
Method
Purpose
ListMeasureWidth()
This is used to allow the default width of an entry in the dropdown list to be modified. As the width of the overall drop-down list is set to that of the widest entry or greater, this is effectively the minimum width of the drop-down list.
ListMeasureHeight()
This is used to allow the default height of each list entry to be modified. Unless a large image is displayed (as is the case with TCursor properties), this method does not generally need to be overridden. This is called to render each property value in the drop-down list. This is called to render the selected property value for the property when it does not have focus. When the property has focus, the current property value is shown as an editable AnsiString. This is called to render the property name in the Object Inspector. It is not required often. This is used to return the displayable value of the property. This method is used in conjunction with the ListDrawValue() and PropDrawValue() methods to render the AnsiString representation of the property value.
ListDrawValue() PropDrawValue()
PropDrawName() GetVisualValue()
Where in the Object Inspector these methods are used is illustrated in Figure 10.3. You can see that the three most important methods to override are ListMeasureWidth(), ListDrawValue(), and PropDrawValue(). PropDrawValue PropDrawName
Object Inspector
ListMeasureHeight ListDrawValue
ListMeasureWidth
FIGURE 10.3 Areas in the Object Inspector that are affected by the new overridable TPropertyEditor methods.
12 9721 CH10
11/13/00
9:44 AM
Page 619
Creating Property and Component Editors CHAPTER 10
619
To create your own custom images in the Object Inspector, you must derive a new property editor class from TPropertyEditor or from a class derived from TPropertyEditor. Which you do depends on the type of the property that the editor is for. For example, a property of type int would descend from TIntegerProperty. Refer to the section “Creating Custom Property Editors,” earlier in this chapter, for more information. A new property editor class can then be defined according to the format in Listing 10.10. As an example, the editor is derived from TEnumProperty. LISTING 10.10
Definition Code for a Property Editor That Renders Custom Images
#include “DsgnIntf.hpp” class TCustomImagePropertyEditor : public TEnumProperty { typedef TEnumProperty inherited; public: DYNAMIC void __fastcall ListMeasureWidth(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AWidth); DYNAMIC void __fastcall ListMeasureHeight(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AHeight); DYNAMIC void __fastcall ListDrawValue(const AnsiString Value, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected); DYNAMIC void __fastcall PropDrawValue(Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected); DYNAMIC void __fastcall PropDrawName(Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected);
10 PROPERTY AND COMPONENT EDITORS
protected: #pragma option push -w-inl inline __fastcall virtual TCustomImagePropertyEditor(const _di_IFormDesigner ADesigner, int APropCount)
12 9721 CH10
11/13/00
620
9:44 AM
Page 620
C++Builder 5 Essentials PART I
LISTING 10.10
Continued : TEnumProperty(ADesigner, APropCount)
{ } #pragma option pop public: #pragma option push -w-inl inline __fastcall virtual ~TCustomImagePropertyEditor(void) { } #pragma option pop };
It is assumed that only the drawing behavior of the property editor is to be modified. The remainder of the class is not altered. The implementation of each of the five DYNAMIC functions is discussed in the sections that follow. For each of the methods, comments will indicate the code that should be present in each method. This will be followed by the actual code used to produce the images shown in Figure 10.4, which shows a finished property editor in use.
FIGURE 10.4 The TShapeTypePropertyEditor in use.
12 9721 CH10
11/13/00
9:44 AM
Page 621
Creating Property and Component Editors CHAPTER 10
621
As an example, a property editor for the TShapeType enumeration from the TShape component will be developed. The class definition for such a property editor is exactly the same as that shown in Listing 10.10. However, the class is called TShapeTypePropertyEditor. The parameters used in the five image-rendering methods are detailed in Table 10.9, so that an overall picture of how they are used can be developed. TABLE 10.9
Parameters for Custom Image-Rendering Methods
Method
Purpose
AWidth
This is the current width in pixels of the AnsiString representation of the value as it will be displayed in the Object Inspector, including leading and trailing space. This is the default height of the display area for the current item. Typically this is 2 pixels greater than the height of ACanvas->TextHeight(“Ag”), where Ag is chosen simply to remind the reader that the actual font height of the current font is returned, that is the ascender height (from A) plus the descender height (from g). Adding 2 pixels allows a 1-pixel border. Remember that the ascender height also includes the internal leading height (used for accents, umlauts, and tildes in non-English character sets), typically 2 to 3 pixels. Refer to Figure 10.5 for clarification. This encapsulates the device context for the current item in the Object Inspector. This represents the client area of the region to be painted. This parameter is true when the list item is currently selected in the Object Inspector.
AHeight
ACanvas ARect ASelected
Figure 10.5 shows a diagram illustrating how the height of text is calculated.
Internal Leading Ascender Text Height
Descender
Calculating text height.
PROPERTY AND COMPONENT EDITORS
FIGURE 10.5
10
12 9721 CH10
11/13/00
622
9:44 AM
Page 622
C++Builder 5 Essentials PART I
Figure 10.6 shows the relationship between the parameters in Table 10.9 and the actual rendering of an image and text in the Object Inspector. This figure will be referred to throughout the discussion, and additional information is therefore shown. ACanvas Rect (ARect.Left, ARect.Top, vRight, ARect.Bottom) Rect (vRight, ARect.Top, ARect.Right, ARect.Bottom) ARect.Left
TextRect (Rect, vRight+1, ARect.Top+1, “stRoundSquare”)
ARect.Top TextHeight (“Ag”) = 16
AHeight
ARect.Bottom TextWidth (“stRoundSquare”)
vRight = ARect.Bottom - ARect.Top + ARect.Left
ARect.Right
Rect (ARect.Left+1, ARect.Top+1, vRight–1, ARect.Bottom–1) AWidth
FIGURE 10.6 The relationship between image-rendering parameters and actual display.
The ListMeasureWidth() Method Initially, AWidth is equal to the return value of ACanvas->TextWidth(Value). However, if an image is added to the display, the width of the image must be added to AWidth to update it. This method, called during the width calculation phase of the drop-down list, allows you to do this. If a square image region is required, AWidth can simply be adjusted by adding ACanvas->TextHeight(“Ag”)+2 to its current value. This is because this value will equal the default AHeight value, as previously mentioned in Table 10.9. (Also, see Figure 10.6, in which ACanvas->TextHeight(“Ag”)+2 is 18 (16+2) pixels.) Remember that Ag could be replaced by any characters. If a larger image is required, a multiple of this value can be used or a constant can be added to the width. If the image width is known, then this can simply be added to the current AWidth value. The code is shown in Listing 10.11.
12 9721 CH10
11/13/00
9:44 AM
Page 623
Creating Property and Component Editors CHAPTER 10
LISTING 10.11
623
Overriding the ListMeasureWidth() Method
void __fastcall TShapeTypePropertyEditor::ListMeasureWidth(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AWidth) { AWidth += (ACanvas->TextHeight(“Ag”)+2) + 0; // 0 can be replaced // by a constant }
The ListMeasureHeight() Method Unless an image is required that is larger then the default height, this method does not require overriding. However, if it does need modified, AHeight must not be given a value smaller than ACanvas->TextHeight(“Ag”)+2, because this would clip the text displayed. Therefore, two choices are available. A constant value can be added to the current AHeight, normally to maintain a constant ratio with the image width, or AHeight can be changed directly. If it is changed directly, the new value must be greater than ACanvas->TextHeight(“Ag”)+2; otherwise this value should be used. The code is shown in Listing 10.12. LISTING 10.12
Overriding the ListMeasureHeight() Method
void __fastcall TShapeTypePropertyEditor::ListMeasureHeight(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AHeight) { AHeight += 0; // 0 could be replaced by a constant value } // OR :
10 PROPERTY AND COMPONENT EDITORS
void __fastcall TShapeTypePropertyEditor::ListMeasureHeight(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AHeight) { if( (ACanvas->TextHeight(“Ag”)+2) < ImageHeight ) { AHeight = ImageHeight; } }
12 9721 CH10
11/13/00
624
9:44 AM
Page 624
C++Builder 5 Essentials PART I
The ListDrawValue() Method This method does most of the hard work. It is this method that renders each item in the dropdown list by drawing directly onto the list item’s canvas. To write well-behaved code, this method should have the layout in Listing 10.13. To get an appreciation of what the actual rendering code is doing, refer to Figure 10.6. For the big picture, refer to Figure 10.3. LISTING 10.13
A Template for Overriding the ListDrawValue() Method
void __fastcall TCustomImagePropertyEditor::ListDrawValue(const AnsiString Value, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected) { // Declare an int vRight to indicate the right most edge of the image. // The v prefix is used to indicate that it is a variable. This used // to follow the convention used in DsgnIntf.pas. try { // Step 1 - Save ACanvas properties that we are going to change. // Step 2 - Frame the area to be modified. This is required so that any // previous rendering on the canvas is overwritten. For example // when the IDE selection rendering is applied, i.e. the // property value is surrounded by a dashed yellow and black // line and the AnsiString representation is highlighted in // clNavy, and focus then moves to another list value the // modified parts of ACanvas are cleared, ready for the custom // rendering. If the entire ACanvas is going to be changed then // this operation is not required. // Step 3 - Perform any preparation required. For example paint a // background color and place a highlight box around the image // of the list value if ASelected is true. // // To choose a color to match the current text used by windows // select clWindowText, this is useful as an image border, hence // this is often selected as a suitable ACanvas->Pen color. // // To give the appearance of a clear background, clear border or // both set the ACanvas->Brush and/or ACanvas->Pen color to // clWindow. // // To use a color the same as the Object Inspector choose // clBtnFace.
12 9721 CH10
11/13/00
9:44 AM
Page 625
Creating Property and Component Editors CHAPTER 10
LISTING 10.13
625
Continued
// Step 4 - Determine the value of the current list item. // Step 5 - Draw the required image onto ACanvas. // Step 6 - Restore modified ACanvas properties to their original values. } __finally { // Perform the following operation to render the AnsiString // representation of the current item, i.e. Value, onto ACanvas. // 1. Either call the parents ListDrawValue method passing vRight as the // l (left) parameter of the Rect variable, i.e. // // TEnumProperty::ListDrawValue(Value, // ACanvas, // Rect(vRight, // ARect.Top, // ARect.Right, // ARect.Bottom), // ASelected); // which becomes: // // inherited::ListDrawValue(Value, // ACanvas, // Rect(vRight, // ARect.Top, // ARect.Right, // ARect.Bottom), // ASelected); // // using our typedef which is more maintainable.
} }
10 PROPERTY AND COMPONENT EDITORS
// 2. Or perform this operation directly by calling the TextRect() member // function directly removing the need to call the parent version of // this (ListDrawValue()) virtual function // i.e. // ACanvas->TextRect( Rect(vRight, // ARect.Top, // ARect.Right, // ARect.Bottom), // vRight+1, // ARect.Top+1, // Value );
12 9721 CH10
11/13/00
626
9:44 AM
Page 626
C++Builder 5 Essentials PART I
Actual code based on the template in Listing 10.13 is shown in Listing 10.14. The code renders each item in the drop-down list. Each item in the list consists of an image followed by text representing the enum value to which the item refers. Figure 10.4 shows an image of the rendered drop-down list. LISTING 10.14
An Implementation of the ListDrawValue() Method
void __fastcall TShapeTypePropertyEditor::ListDrawValue(const AnsiString Value, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected) { // Declare vRight (‘v’ stands for variable) int vRight = ARect.Bottom - ARect.Top + ARect.Left; try { // Step 1 - Save ACanvas properties that we are going to change TColor vOldPenColor = ACanvas->Pen->Color; TColor vOldBrushColor = ACanvas->Brush->Color;
// Step 2 - Frame the area to be modified. ACanvas->Pen->Color = ACanvas->Brush->Color; ACanvas->Rectangle(ARect.Left, ARect.Top, vRight, ARect.Bottom);
// Step 3 - Perform any preparation required. if(ASelected) { ACanvas->Pen->Color = clYellow; } else { ACanvas->Pen->Color = clBtnFace; }
// // // //
Choose a Pen color depending on whether the list value is selected or not
ACanvas->Brush->Color = clBtnFace;
// Choose a background color to // match the Object Inspector
12 9721 CH10
11/13/00
9:44 AM
Page 627
Creating Property and Component Editors CHAPTER 10
LISTING 10.14
627
Continued
ACanvas->Rectangle( ARect.Left + 1, ARect.Top + 1, vRight - 1, ARect.Bottom - 1 );
// // // //
Draw the background onto the Canvas using the current Pen and the current Brush :-)
// Step 4 - Determine the value of the current list item TShapeType ShapeType = TShapeType(GetEnumValue(GetPropType(), Value)); // Step 5 - Draw the required image onto ACanvas ACanvas->Pen->Color = clBlack; ACanvas->Brush->Color = clWhite; switch(ShapeType) { case stRectangle
10 PROPERTY AND COMPONENT EDITORS
: ACanvas->Rectangle(ARect.Left+2, ARect.Top+4, vRight-2, ARect.Bottom-4); break; case stSquare : ACanvas->Rectangle(ARect.Left+2, ARect.Top+2, vRight-2, ARect.Bottom-2); break; case stRoundRect : ACanvas->RoundRect(ARect.Left+2, ARect.Top+4, vRight-2, ARect.Bottom-4, (ARect.Bottom-ARect.Top-6)/2, (ARect.Bottom-ARect.Top-6)/2); break; case stRoundSquare : ACanvas->RoundRect(ARect.Left+2, ARect.Top+2, vRight-2, ARect.Bottom-2, (ARect.Bottom-ARect.Top)/3, (ARect.Bottom-ARect.Top)/3); break; case stEllipse : ACanvas->Ellipse(ARect.Left+1, ARect.Top+2, vRight-1, ARect.Bottom-2); break;
12 9721 CH10
11/13/00
628
9:44 AM
Page 628
C++Builder 5 Essentials PART I
LISTING 10.14
Continued
case stCircle
: ACanvas->Ellipse(ARect.Left+1, ARect.Top+1, vRight-1, ARect.Bottom-1); break;
default : break; } // Step 6 - Restore modified ACanvas properties to their original values ACanvas->Pen->Color = vOldPenColor; ACanvas->Brush->Color = vOldBrushColor; } __finally { // Render the AnsiString representation onto ACanvas // Use method 1, call the parent method inherited::ListDrawValue(Value, ACanvas, Rect(vRight, ARect.Top, ARect.Right, ARect.Bottom), ASelected); } }
Step 4 in Listing 10.14 is of crucial importance to the operation of ListDrawValue(). The value of the drop-down list item is determined here. This allows a decision to be made in Step 5 as to what should be rendered. For enumerations such as TShapeType, the AnsiString representation of the value must be converted to an actual value. The code that performs this is TShapeType ShapeType = TShapeType(GetEnumValue(GetPropType(), Value));
is declared in $(BCB)\Include\Vcl\TypInfo.hpp and returns an int value. This int value is used to construct a new TShapeType variable called ShapeType. The function GetPropType() returns a pointer to a TTypeInfo structure containing the TypeInfo for the property type (in this case TShapeType). This could alternatively have been obtained using GetEnumValue()
*GetPropInfo()->PropType
12 9721 CH10
11/13/00
9:44 AM
Page 629
Creating Property and Component Editors CHAPTER 10
629
This is similar to the approach used to obtain type information when registering property editors (see the section “Obtaining a TTypeInfo* (PTypeInfo) from an Existing Property and Class for a Non-VCL Type,” earlier in this chapter, for more details) and can be used more generally. Value is the AnsiString representation of the current enumeration value. GetPropType() and GetPropInfo() are both member functions of TPropertyEditor and as such are declared in $(BCB)\Include\Vcl\DsgnIntf.hpp. Techniques such as these are indispensable to writing property editors, so it is important to be aware of them. Each of the images is rendered according to the bounding ARect parameter. This means that the code does not need to be modified to enlarge or reduce the rendered images. To do this, simply change the values of AWidth and AHeight. Changing the constant 0 in the ListMeasureWidth() and ListMeasureHeight() methods to 10, for example, will increase the rendered image size in the drop-down list by 10 pixels in each direction. Note that the image in the property value region will not be affected.
The PropDrawValue() Method This method is responsible for rendering the current property value in the Object Inspector. The height of the area to be rendered is fixed (ARect.Bottom - ARect.Top) so there is less flexibility over the images that can be rendered compared with images rendered in the dropdown list. The code required for this operation is the same as that required to render the same value in the drop-down list. The only difference is the value of the ARect parameter. The rendering can therefore be carried out by the ListDrawValue() method, passing the PropDrawValue() parameters as arguments. The code for this member function is shown in Listing 10.15. LISTING 10.15
An Implementation of the PropDrawValue() Method
10 PROPERTY AND COMPONENT EDITORS
void __fastcall TShapeTypePropertyEditor::PropDrawValue(Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected) { if( GetVisualValue() != “” ) { ListDrawValue(GetVisualValue(), ACanvas, ARect, ASelected); } else { // As in the ListDrawValue method either the parent method can be called // or the code required to render the text called directly, i.e. //
12 9721 CH10
11/13/00
630
9:44 AM
Page 630
C++Builder 5 Essentials PART I
LISTING 10.15 // // // // // // // // // //
Continued
inherited::PropDrawValue(ACanvas, ARect, ASelected); or: ACanvas->TextRect( ARect, ARect.Left+1, ARect.Top+1, GetVisualValue() ); For comparison the text is rendered directly, i.e.
ACanvas->TextRect( ARect, ARect.Left+1, ARect.Top+1, GetVisualValue() ); } }
The PropDrawName() Method This is the last of the overridable methods for custom rendering and the one least often required. It controls the rendering of the property Name (see Figure 10.3). As with the PropDrawValue() method, the height of the drawing region is fixed. This method has limited use, though it can be used to add symbols to properties that exhibit certain behavior, read-only properties for instance (such as About properties). Overuse should be avoided because it may confuse rather than help users. Another possible use is to add an image to TComponent-derived properties to indicate the component required. This method is not used in the TShapeTypePropertyEditor example, but the required code, should it be needed, is shown in Listing 10.16. LISTING 10.16
An Implementation of the PropDrawName() Method
void __fastcall TCustomImagePropertyEditor::PropDrawValue(Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected) { if( GetName() != “” ) { // Write a function to render the desired image, similar to // the ListDrawValue() method, i.e. //
12 9721 CH10
11/13/00
9:44 AM
Page 631
Creating Property and Component Editors CHAPTER 10
LISTING 10.16
631
Continued
// PropDrawNameValue(GetName(), ACanvas, ARect, ASelected); // Must be // defined } else { // // // // // // // // // // // // //
As in the PropDrawValue method either the parent method can be called or the code required to render the text called directly, i.e. inherited::PropDrawName(ACanvas, ARect, ASelected); or: ACanvas->TextRect( ARect, vRect.Left+1, ARect.Top+1, GetName() ); For comparison the text is rendered directly, i.e.
ACanvas->TextRect( ARect, ARect.Left+1, ARect.Top+1, GetName() ); } }
The TImageListPropertyEditor from the EnhancedEditors package (see Table 10.1) does implement this method to display an icon representing a TImageList component for TCustomImageList* properties. Listing 10.17 shows its implementation of this method for comparison. Note that ImageListPropertyImage is a resource loaded in the property editor’s constructor. LISTING 10.17
An Alternative Implementation of the PropDrawName() Method
try { // Clear the canvas using the current pen and brush
10 PROPERTY AND COMPONENT EDITORS
void __fastcall TImageListPropertyEditor::PropDrawName (Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected) { TRect ValueRect = ARect;
12 9721 CH10
11/13/00
632
9:44 AM
Page 632
C++Builder 5 Essentials PART I
LISTING 10.17
Continued
ACanvas->FillRect(ARect); if(GetName() != “”) { if(Screen->PixelsPerInch > 96) // If Large fonts { ACanvas->Draw( ARect.Left + 1, ARect.Top + 2, ImageListPropertyImage ); } else // Otherwise small fonts { ACanvas->Draw( ARect.Left + 1, ARect.Top, ImageListPropertyImage ); } ValueRect = Rect( ARect.Left + 16 + 2, ARect.Top, ARect.Right, ARect.Bottom ); } } __finally { // Whether or not we successfully draw the image we must draw the text inherited::PropDrawName(ACanvas, ValueRect, ASelected); } }
The code in Listing 10.17 is reasonably straightforward. Of note is the try/__finally block to ensure that the text is always rendered. The code inside the try block is similar to that in Listing 10.16; the only difference is that the ImageListPropertyImage resource is positioned differently, depending on whether the screen is using large or small fonts. Once the ImageListPropertyImage resource is rendered, the Rect for rendering the text is offset to allow for the width of the resource, in this case 16 pixels.
Installing Editor-Only Packages In the previous section, an editor for properties of type TShapeType was developed, but there was no mention of its registration. This will now be discussed. In the earlier section “Registering Custom Property Editors,” we saw that we must use the RegisterPropertyEditor() function
12 9721 CH10
11/13/00
9:44 AM
Page 633
Creating Property and Component Editors CHAPTER 10
633
to register the editor with the IDE. This will be discussed again shortly. The only other considerations are that this function must be called inside the Register() function and that this Register() function must be wrapped inside a namespace that is the same as the name of the file in which it is contained, with the restriction that the first letter of the namespace must be in uppercase and the remaining letters lowercase. You are not registering a component and will not have used the IDE Component Wizard to set up your basic code structure, so you must write this registration code yourself. Remembering this could save time, because not all coding mistakes give errors at compile time. The basic code structure required is shown in Listing 10.18. LISTING 10.18
Registering Editor-Only Packages
#include #pragma hdrstop #include “NameOfThisFile” // Header empty except for include guards #include “PropertyEditors.h” // Include the file that contains the Property // Editor code #include // Include for TPropInfo* and GetPropInfo() //---------------------------------------------------------------------------// #pragma package(smart_init) //---------------------------------------------------------------------------// namespace Nameofthisfile // First letter UPPERCASE, all other letters lowercase { void __fastcall PACKAGE Register() { // Using the following Registration registers the ‘TShapePropertyEditor’ // for ALL properties of type ‘TShapeType’ for ALL components. TPropInfo* TShapeTypePropInfo = Typinfo::GetPropInfo(__typeinfo(TShape), “Shape”); RegisterPropertyEditor(*TShapeTypePropInfo->PropType, 0, “”, __classid(TShapeTypePropertyEditor)); } }
PROPERTY AND COMPONENT EDITORS
Note that in Listing 10.18 the header file (NameOfThisFile.h) included for the registration code is empty and does not need to be included or present. In the package source file, the implementation file (.cpp) is included through the USEUNIT() macro, so the header is not needed.
10
12 9721 CH10
11/13/00
634
9:44 AM
Page 634
C++Builder 5 Essentials PART I
Because the purpose of the package that contains this code is to register only editors (property or component), then the Designtime Only option in the Usage Options section on the Description page of the package’s Options dialog should be selected. Property and component editors are used from newest to oldest, provided their registration is at least as specific as that for the old editor. This allows editors currently registered to be overridden. This is the effect of the EnhancedEditors package (EnhancedEditors.bpk) accompanying this chapter on the CD-ROM. As can be seen from Listing 10.18, TShapeTypePropertyEditor is registered for ALL properties of type TShapeType for ALL components. Hence, the Shape property of TShape will now use this property editor. This opens up the possibility of separately customizing and updating the property and component editors of existing components. As was mentioned in the section “Images in Drop-Down Lists in the Object Inspector” in Chapter 2, “C++Builder Projects and More on the IDE,” the TFontNameProperty editor is effectively disabled by default. To enable it requires the creation of an Expert using the Open Tools API. This is not easy. An alternative approach is to override the TFontNameProperty property editor to achieve the desired result. Such a property editor, called TDisplayFontNameProperty, has also been included in the EnhancedEditors package to enable the reader to examine how this is done.
Using Linked Image Lists in Property Editors A common use of images in drop-down lists for property editors is to display images in an image index property that is linked to a component’s TCustomImageList* property, or to display images contained in a component’s parent’s TCustomImageList* property. Linking an image index property to a TCustomImageList* in the same component will be shown as a general example. However, the only difference between linking to a TCustomImageList* on the same component and linking to a TCustomImageList* on a parent component is the method used to retrieve a copy of the pointer to the TCustomImageList, as we shall see. This will be highlighted in the “Linking to a Parent’s TCustomImageList” and “A Generalized Solution for ImageIndex Properties” sections later in this chapter. To illustrate how this is done, a TEnhancedImage component will be developed that has both an image index property and a TCustomImageList* property. Note that a TCustomImageList pointer is used to allow for other classes derived from this class; otherwise, TImageList* would be used. The purpose of this component is simple: to allow the TEnhancedImage component to be linked to a TImageList component if there is one placed on the same form. By changing the image index property of the TEnhancedImage component, the image displayed will match the image contained in the TImageList component at that index. It would also be preferable that this behavior be enabled only when required; otherwise, the component should behave as a normal TImage component. The definition for this component is shown in Listing 10.19.
12 9721 CH10
11/13/00
9:44 AM
Page 635
Creating Property and Component Editors CHAPTER 10
LISTING 10.19
635
Definition of the TEnhancedImage Component
//---------------------------------------------------------------------------// #ifndef EnhancedImageH #define EnhancedImageH //---------------------------------------------------------------------------// #include <SysUtils.hpp> #include #include #include //---------------------------------------------------------------------------// class PACKAGE TEnhancedImage : public TImage { typedef TImage inherited; private: Imglist::TCustomImageList* FImageList; int FImageIndex; bool FUseImageList; protected: virtual void __fastcall SetUseImageList(bool NewUseImageList); virtual void __fastcall SetImageIndex(int NewImageIndex); virtual void __fastcall SetImageList (Imglist::TCustomImageList* NewImageList); virtual void __fastcall UpdatePicture(void); // Override Notification from TComponent virtual void __fastcall Notification(TComponent* AComponent, TOperation Operation); public: __fastcall TEnhancedImage(TComponent* Owner); __published: __property bool UseImageList = {read=FUseImageList, write=SetUseImageList, default=false}; __property Imglist::TCustomImageList* ImageList = {read=FImageList, write=SetImageList, default=0};
}; //---------------------------------------------------------------------------// #endif
10 PROPERTY AND COMPONENT EDITORS
__property int ImageIndex = {read=FImageIndex, write=SetImageIndex, default=-1};
12 9721 CH10
11/13/00
636
9:44 AM
Page 636
C++Builder 5 Essentials PART I
Because we want only to add extra functionality to the TImage component, we simply need to add our required additional properties to a class derived from TImage and then write any get and set functions necessary. Three properties are added: one for the TImageList, called ImageList, one for the image index, called ImageIndex, and one to decide whether to use the TImageList, called UseImageList. For each of these, we need a set method and an initialization in the class’s constructor. We also override TComponent’s Notification() method and have a function to update the picture if necessary each time one of these properties is changed. The implementation code is shown in Listing 10.20. LISTING 10.20
Implementation for the TEnhancedImage Component Functions
//---------------------------------------------------------------------------// __fastcall TEnhancedImage::TEnhancedImage(TComponent* Owner) : TImage(Owner), FUseImageList(false), FImageIndex(-1), FImageList(0) { } //---------------------------------------------------------------------------// void __fastcall TEnhancedImage::SetUseImageList(bool NewUseImageList) { if(NewUseImageList != UseImageList) { FUseImageList = NewUseImageList; UpdatePicture(); } } //---------------------------------------------------------------------------// void __fastcall TEnhancedImage::SetImageIndex (int NewImageIndex) { if(NewImageIndex != FImageIndex) { FImageIndex = NewImageIndex; UpdatePicture(); } } //---------------------------------------------------------------------------// void __fastcall TEnhancedImage::SetImageList(Imglist::TCustomImageList* NewImageList) { if(NewImageList != FImageList) { FImageList = NewImageList;
12 9721 CH10
11/13/00
9:44 AM
Page 637
Creating Property and Component Editors CHAPTER 10
LISTING 10.20
637
Continued
if(ImageList == 0) ImageIndex =-1; } } //---------------------------------------------------------------------------// void __fastcall TEnhancedImage::UpdatePicture(void) { if( UseImageList && ImageList != 0 && ImageIndex >= 0 && ImageIndex < ImageList->Count ) { std::auto_ptr Image(new Graphics::TBitmap()); ImageList->GetBitmap(ImageIndex, Image.get()); Picture->Assign(Image.get()); } } //---------------------------------------------------------------------------// void __fastcall TEnhancedImage::Notification(TComponent* AComponent, TOperation Operation) { inherited::Notification(AComponent, Operation); if(Operation == opRemove) { if(AComponent == ImageList) ImageList = 0; // Can make more checks for other properties if needed } } //---------------------------------------------------------------------------//
10 PROPERTY AND COMPONENT EDITORS
The constructor holds no surprises; the data members for the class’s properties are initialized in an initializer list. Looking now at SetUseImageList() and SetImageIndex(), we see that they both operate in the same way. If the new value requested does not equal the current value, then the current value is set to the new value. The UpdatePicture() method is then called. This looks at all the current property settings and adjusts the Picture property (inherited from TImage) as required. SetImageList() is similar; if the new value requested does not equal the current value, the current value is set to the new value. However, if the new value is 0 (that is, NULL: there is no TImageList), then ImageIndex is set to -1, the property’s default value. Notice that after the initial assignment to FImageList, FImageList is not used in the following if statement; instead, the ImageList property is. This returns FImageList, so in this case it makes no difference which is used. However, if the property’s internal representation may be different from that of the property itself (that is, a get function is required for the property), then it is better to read the property, not the data member. This also makes code more clear.
12 9721 CH10
11/13/00
638
9:44 AM
Page 638
C++Builder 5 Essentials PART I
Similarly, ImageIndex, not FimageIndex, is updated to ensure that ImageIndex’s set function is called. Attention should be paid to issues such as these when writing custom components. The function that does most of the work is the UpdatePicture() function. It checks that there is a TImageList linked to the component, that we want to use the TImageList (UseImageList = true), and that the ImageIndex value is a valid index for the TImageList; in other words, it is zero or greater and less than the number of images in the list. If this is all true, the image is retrieved from the image list into the Graphics::TBitmap object (the Image variable) from which it is assigned to the Picture property. Notice that the Graphics::TBitmap object is declared using std::auto_ptr; this ensures that the object is deleted even if an exception is thrown. A common alternative is to use a try/__finally block. If this were the case, the code inside the if statement in UpdatePicture() would become Graphics::TBitmap* Image = new Graphics::TBitmap(); try { ImageList->GetBitmap(ImageIndex, Image); Picture->Assign(Image); } __finally { delete Image; }
Finally, the Notification() function derived from TComponent is overridden so that if a linked TImageList is deleted the component can reset the ImageList pointer to zero. Note that the inherited (that is, TImage) implementation is called first to ensure all notifications are handled. Again, ImageList is used instead of FImageList so that the set function for ImageList is automatically called. This is essential to the correct operation of the component; if it is not done, access violations are guaranteed. We now have a component for which to write our image index property editor. The editor is for the ImageIndex property, an int property. Note that TImageIndex could be used instead, because this is simply a typedef for int (declared in $(BCB)\Include\Vcl\Imglist.hpp). The property editor is therefore derived from TIntegerProperty. Listing 10.21 shows the definition for our ImageIndex property editor. We call it TImageIndexPropertyEditor. LISTING 10.21
TImageIndexPropertyEditor Definition
#include “DsgnIntf.hpp” class TImageIndexPropertyEditor : public TIntegerProperty { typedef TIntegerProperty inherited;
12 9721 CH10
11/13/00
9:44 AM
Page 639
Creating Property and Component Editors CHAPTER 10
LISTING 10.21
639
Continued
private: static const int Border = 2; // Border around image in Pixels static const int MaxImageWidth = 64; // Max Width of image in Pixels static const int MaxImageHeight = 64; // Max Height of image in Pixels protected:protected: virtual Imglist::TCustomImageList* __fastcall GetComponentImageList(void); public: virtual TPropertyAttributes __fastcall GetAttributes(void); virtual void __fastcall GetValues(Classes::TGetStrProc Proc); DYNAMIC void __fastcall ListMeasureWidth(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AWidth); DYNAMIC void __fastcall ListMeasureHeight(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AHeight); DYNAMIC void __fastcall ListDrawValue(const AnsiString Value, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected); // This is a read-only property to be used as the // pointer to the component’s image list __property Imglist::TCustomImageList* ComponentImageList = {read=GetImageList}; protected: #pragma option push -w-inl inline __fastcall virtual TImageIndexPropertyEditor(const _di_IFormDesigner ADesigner, int APropCount) : TIntegerProperty(ADesigner, APropCount) { } #pragma option pop
};
10 PROPERTY AND COMPONENT EDITORS
public: #pragma option push -w-inl inline __fastcall virtual ~TImageIndexPropertyEditor(void) { } #pragma option pop
12 9721 CH10
11/13/00
640
9:44 AM
Page 640
C++Builder 5 Essentials PART I
The code in Listing 10.21 is similar to that previously shown for other property editors. However, a read-only Imglist::TCustomImageList* property has been added along with an appropriate get function: virtual Imglist::TCustomImageList* __fastcall GetComponentImageList(void);
It is this get function that retrieves the pointer value of the component’s TCustomImageList* property. This allows the images of the image list to be accessed. As in the previous section, we shall look at each of the methods of the TImageIndexPropertyEditor in turn to discuss any implementation issues.
The GetAttributes() Method This is the method used to determine the atributes that the property editor should exhibit in the Object Inspector. We want to have a drop-down list of values (paValueList), and we do not want the property to be available for editing when more than one component is selected at once (paMutliSelect). The implementation required for this is TPropertyAttributes __fastcall TImageIndexPropertyEditor::GetAttributes(void) { return (inherited::GetAttributes()<< paValueList >> paMultiSelect); }
The GetComponentImageList() Method This is the key to the successful operation of this property editor. It is this method that makes the link between the component’s TCustomImageList* and the image index property. It does this by retrieving a pointer to the component to which the property being edited belongs. It then uses this pointer to return the component’s TCustomImageList*. The code required if the TCustomImageList* and image index property are in the same component is as follows: Imglist::TCustomImageList* __fastcall TImageIndexPropertyEditor::GetComponentImageList(void) { TEnhancedImage* Component = dynamic_cast(GetComponent(0)); if(Component) { return Component->ImageList; } else return 0; }
Here we use TPropertyEditor’s GetComponent() method, declared as Classes::TPersistent* __fastcall GetComponent(int Index);
12 9721 CH10
11/13/00
9:44 AM
Page 641
Creating Property and Component Editors CHAPTER 10
641
This returns TPersistent* to the Index component being edited by this property editor. Because this editor does not have the paMultiSelect attribute, it can edit only one component at a time. As such, only a pointer to that single component (Index == 0) can be returned. This is then dynamic_casted to the component type that this editor is for. Once we have a correct pointer to our component, it is simple to return the pointer to the TCustomImageList property. If the dynamic_cast fails, 0 is returned. The code required for an image index property whose component’s parent contains the is more complex. This is discussed in the section “Linking to a Parent’s TCustomImageList,” later in this chapter. TCustomImageList
The GetValues() Method This method is used to populate the property’s drop-down list with appropriate values. In this case, the index of each image in the image list is appropriate. The code required is as follows: void __fastcall TImageIndexPropertyEditor::GetValues(Classes::TGetStrProc Proc) { TCustomImageList* ImageList = ComponentImageList; if(ImageList != 0) { for(int i = 0; iCount; ++i) Proc(IntToStr(i)); } }
The function simply returns the current image index as an AnsiString. Notice that the ComponentImageList property is not used directly. Its value is assigned to the TCustomImageList* ImageList local variable. This is because GetComponentImageList() is called every time ImageList is read. If ComponentImageList was used directly, there would be many unnecessary calls to GetComponentImageList(). It is better to read ComponentImageList once and copy it. This is done with all the methods that require the ComponentImageList pointer.
The ListMeasureWidth() and ListMeasureHeight() Methods 10 PROPERTY AND COMPONENT EDITORS
Of major concern to both these methods is the width and height of the images contained in the TImageList component. Because the size could be very large, upper limits on the displayable width and height should be specified. A sensible figure (and the one used by the VCL) is 64 pixels square. If the image width or height is greater than 64, then we will draw an image of only that width or height. The StretchDraw() method of TCanvas can be used to render a reasonable representation of a large image.
12 9721 CH10
11/13/00
642
9:44 AM
Page 642
C++Builder 5 Essentials PART I
The choice of 64 as the maximum height and width of each image in the drop-down list is based on several criteria. First we must remember that we have a finite size with which to work, namely the resolution of the screen that displays the images. Also, we will most likely have multiple images. If we have five images, then we require 5×64 = 320 pixels in height to display the list. Even at higher resolutions, this will make the list quite large. A resolution of 64 is large enough to see detail on larger images but not too large as to make it impractical. It is also a power of 2, which most people who work with computers like. In the following code, the maximum allowed width and height have been replaced by the static const data members MaxImageWidth and MaxImageHeight, respectively. Also, a static const data member Border has been used to represent the desired border width around the image and is currently set to 2 pixels. The code for ListMeasureWidth() is void __fastcall TImageIndexPropertyEditor::ListMeasureWidth(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AWidth) { TCustomImageList* ImageList = ComponentImageList; if(ImageList != 0) { if(ImageList->Width < MaxImageWidth) { AWidth += ImageList->Width + Border*2; } else AWidth += MaxImageWidth + Border*2; } }
Note that an offset of 4 pixels (Border*2) is used to allow for some space between the images rendered and the text numbers representing the image index value. The code for ListMeasureHeight() is similar: void __fastcall TImageIndexPropertyEditor::ListMeasureHeight(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AHeight) { TCustomImageList* ImageList = ComponentImageList; if(ImageList != 0) { if( ImageList->Height < MaxImageHeight && ImageList->Height > AHeight) { AHeight = ImageList->Height + Border*2;
12 9721 CH10
11/13/00
9:44 AM
Page 643
Creating Property and Component Editors CHAPTER 10
643
} else if(ImageList->Height > AHeight) AHeight = MaxImageHeight + Border*2; }}
The code for the height appears slightly more complex because we need to consider the possibility that the image is less than the height required to render the text.
The ListDrawValue() Method This is the most complex of the methods to implement. This is the method used to render the images from the TImageList component that the TCustomImageList* property points to. The code is shown in Listing 10.22. LISTING 10.22
Implementing the ListDrawValue() Method
void __fastcall TImageIndexPropertyEditor::ListDrawValue(const AnsiString Value, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected) { TRect ValueRect = ARect; try { TCustomImageList* ImageList = ComponentImageList; // Clear the canvas using the current pen and brush ACanvas->FillRect(ARect); if(ImageList != 0 && Value != “”) { int ImageWidth = ImageList->Width; int ImageHeight = ImageList->Height; if(ImageWidth > MaxImageWidth || ImageHeight > MaxImageHeight) { std::auto_ptr Image(new Graphics::TBitmap());
// Draw the image from the ImageList onto the Bitmap’s Canvas ImageList->Draw(Image->Canvas, 0, 0, StrToInt(Value), true);
10 PROPERTY AND COMPONENT EDITORS
// Set the Bitmap’s width and height to that of the image Image->Width = ImageWidth; Image->Height = ImageHeight;
12 9721 CH10
11/13/00
644
9:44 AM
Page 644
C++Builder 5 Essentials PART I
LISTING 10.22
Continued
if(ImageWidth > MaxImageWidth) ImageWidth = MaxImageWidth; if(ImageHeight > MaxImageHeight) ImageHeight = MaxImageHeight; // Define the area to draw the image into TRect ImageRect = Rect( ARect.Left + Border, ARect.Top + Border, ARect.Left + Border + ImageWidth, ARect.Top + Border + ImageHeight ); // Draw the image onto the canvas using StretchDraw ACanvas->StretchDraw(ImageRect, Image.get()); } else { // Draw the image directly onto the canvas from the ImageList // leaving a 2 pixel border ImageList->Draw( ACanvas, ARect.Left + Border, ARect.Top + Border, StrToInt(Value), true ); } ValueRect = Rect( ARect.Left + ImageWidth + Border*2, ARect.Top, ARect.Right, ARect.Bottom ); } } __finally { // Whether or not we successfully draw the image we must draw the text inherited::ListDrawValue(Value, ACanvas, ValueRect, ASelected); } }
The essence of the code in Listing 10.22 is the determination of whether the size of the image means that it should be drawn using ACanvas->StretchDraw() or simply drawn as it stands. If the width or height of the image is larger than either the MaxImageWidth or MaxImageHeight static const data member, respectively, the image is first copied to a Graphics::TBitmap object before being Stretchdrawn onto the list’s canvas. Otherwise, the image is drawn directly onto the list’s canvas.
12 9721 CH10
11/13/00
9:44 AM
Page 645
Creating Property and Component Editors CHAPTER 10
645
Notice the use of the try/__finally block to ensure that the inherited ListDrawValue() method is called to render the Value text, regardless of whether or not an exception occurs in the rest of the function, unless of course one is thrown on the first line outside the try block. This is intentional, because an exception thrown here would mean that ValueRect would not be valid, and the code in the __finally block could not be executed. Aside from the elements of code just mentioned, the function is similar to that shown previously for the ListDrawValue() method, hence the canvas must be cleared so that previous rendering is removed. Be aware that this implementation of ListDrawValue() method fixes some dimension errors found in similar ListDrawValue() methods used in the VCL and also stretches the image when required. The VCL implementation fails to do this (at the time of writing of this book).
The PropDrawValue() Method This method is overridden to display a small iconic version of the image. Its usefulness for this particular class is dubious, but the technique is shown for completeness. It is straightforward to remove the method if it is not required. The code is almost identical to that for the ListDrawValue() method, except that the MaxImageWidth and MaxImageHeight values are replaced by a CurrentMaxSide variable that is calculated from the height of the ARect parameter, allowing for a 1-pixel border around the image. Also, the inherited PropDrawValue() method is called instead of ListDrawValue(). The code is shown in Listing 10.23. LISTING 10.23
Implementing the PropDrawValue() Method
void __fastcall TImageIndexPropertyEditor::PropDrawValue(Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected) { TRect ValueRect = ARect; try { TCustomImageList* ImageList = ComponentImageList; // Clear the canvas using the current pen and brush ACanvas->FillRect(ARect);
10 PROPERTY AND COMPONENT EDITORS
if(ImageList != 0 && GetVisualValue() != “”) { int ImageWidth = ImageList->Width;
12 9721 CH10
11/13/00
646
9:44 AM
Page 646
C++Builder 5 Essentials PART I
LISTING 10.23
Continued
int ImageHeight = ImageList->Height; // Calculate the MaxSide as we want a square to display our image int CurrentMaxSide = ARect.Bottom - ARect.Top - 2; if(ImageWidth > CurrentMaxSide || ImageHeight > CurrentMaxSide) { std::auto_ptr Image(new Graphics::TBitmap()); // Set the Bitmap’s width and height to that of the image Image->Width = ImageWidth; Image->Height = ImageHeight; // Draw the image from the ImageList onto the Bitmap’s Canvas ImageList->Draw(Image->Canvas, 0, 0, StrToInt(Value), true); if(ImageWidth > CurrentMaxSide) ImageWidth = CurrentMaxSide; if(ImageHeight > CurrentMaxSide) ImageHeight = CurrentMaxSide; // Define the area to draw the image into TRect ImageRect = Rect( ARect.Left + 2, ARect.Top + 1, ARect.Left + 2 + ImageWidth, ARect.Top + 1 + ImageHeight ); // Draw the image onto the canvas using StretchDraw ACanvas->StretchDraw(ImageRect, Image); } else { // Draw the image directly onto the canvas from the ImageList // leaving a 1 pixel border ImageList->Draw( ACanvas, ARect.Left + 2, ARect.Top + 1, StrToInt(Value), true ); } ValueRect = Rect( ARect.Left + ImageWidth + 2, ARect.Top, ARect.Right, ARect.Bottom ); } }
12 9721 CH10
11/13/00
9:44 AM
Page 647
Creating Property and Component Editors CHAPTER 10
LISTING 10.23
647
Continued
__finally { // Whether or not we successfully draw the image we must draw the text inherited::PropDrawValue(ACanvas, ValueRect, ASelected); } }
Other Considerations when Rendering Images The TImageIndexPropertyEditor property editor as it stands does everything that we want it to do except for one thing: When an image is larger than the allowable size and it is Stretchdrawn() onto the list’s canvas, the aspect ratio of the image is not maintained. The code for this has not been included for this operation because it significantly complicates the code required for all four of the rendering methods. This would hide the main principles that should be understood when explaining the necessary steps to implement this property editor. However, this operation is probably the more correct way of implementing this property editor.
Linking to a Parent’s TCustomImageList An implementation of TMenuItem’s ImageIndex property will be presented to illustrate the code required to display an image in an image index property from a TCustomImageList descendant that is present in the component’s parent class. Only the class definition and the GetParentImageList() method (the replacement for the GetComponentImageList() method) are shown, because the implementation for the other methods is essentially the same as that shown earlier for TEnhancedImage’s ImageIndex property. We will call our property editor TmenuItemImageIndexProperty. Its class definition is shown in Listing 10.24. LISTING 10.24
Definition Code for the TMenuItemImageIndexProperty Property Editor
#include “Dsgnintf.hpp” class PACKAGE TMenuItemImageIndexProperty : public TIntegerProperty { typedef TIntegerProperty inherited;
10 PROPERTY AND COMPONENT EDITORS
private: static const int Border = 2; static const int MaxImageWidth = 64; static const int MaxImageHeight = 64;
12 9721 CH10
11/13/00
648
9:44 AM
Page 648
C++Builder 5 Essentials PART I
LISTING 10.24
Continued
protected: virtual Imglist::TCustomImageList* __fastcall GetParentImageList(void); public: virtual TPropertyAttributes __fastcall GetAttributes(void); virtual void __fastcall GetValues(Classes::TGetStrProc Proc); DYNAMIC void __fastcall ListMeasureWidth(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AWidth); DYNAMIC void __fastcall ListMeasureHeight(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AHeight); DYNAMIC void __fastcall ListDrawValue(const AnsiString Value, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected); DYNAMIC void __fastcall PropDrawValue(Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected); // This is a read-only property to be used as the pointer to the component’s // image list __property Imglist::TCustomImageList* ParentImageList = {read=GetParentImageList}; protected: #pragma option push -w-inl inline __fastcall virtual TMenuItemImageIndexProperty(const _di_IFormDesigner ADesigner, int APropCount) : TIntegerProperty(ADesigner, APropCount) { } #pragma option pop public: #pragma option push -w-inl inline __fastcall virtual ~TMenuItemImageIndexProperty(void) { } #pragma option pop };
12 9721 CH10
11/13/00
9:44 AM
Page 649
Creating Property and Component Editors CHAPTER 10
649
The GetParentImageList() function can be implemented as shown in Listing 10.25. LISTING 10.25
Implementation Code for the GetParentImageList()Method
Imglist::TCustomImageList* __fastcall TMenuItemImageIndexProperty::GetParentImageList(void) { TMenuItem* Component = dynamic_cast(GetComponent(0)); if(Component) { TMenuItem* ParentMenuItem = Component->Parent; while(ParentMenuItem != 0 && ParentMenuItem->SubMenuImages == 0) { ParentMenuItem = ParentMenuItem->Parent; } if(ParentMenuItem != 0) return ParentMenuItem->SubMenuImages; else { TMenu* ParentMenu = Component->GetParentMenu(); if(ParentMenu != 0) return ParentMenu->Images; } } return 0; }
10 PROPERTY AND COMPONENT EDITORS
As always, the first and most important stage is to obtain a pointer to the component of the property being edited, in this case a pointer to a TMenuItem component. Once this is done, the parent of the TMenuItem can be obtained. In the case of a TMenuItem, the parent could either be another TMenuItem (in other words, the current TMenuItem is a sub-menu item) or a TMenu (or descendant such as TPopupMenu). If the TMenuItem is a sub-menu, then we must check to see if a SubMenuImages TCustomImageList* is available from the parent TMenuItem. The parent TMenuItem (if there is one) could also have a parent TMenuItem with available SubMenuImages, and so on. All possible parents are iterated through until none remain. If a parent TMenuItem is found with a non-zero SubMenuImages property, then the iteration stops and a pointer to the TCustomImageList is returned; otherwise, the Parent property will eventually point to zero. If this happens, the parent TMenu is obtained through the GetParentMenu() function. The TCustomImageList* of the TMenu’s Images property is then returned. Most other ImageIndex property editors in components that refer to a parent’s TCustomImageList are simpler to implement than this because normally there will be only one possible parent.
12 9721 CH10
11/13/00
650
9:44 AM
Page 650
C++Builder 5 Essentials PART I
There is an easier way to implement this function (which the astute among you will have already thought of), and that is to use TMenuItem’s GetImageList() function to return a suitable TCustomImageList pointer. The implementation of the GetParentImageList() function then becomes Imglist::TCustomImageList* __fastcall TMenuItemImageIndexProperty::GetParentImageList(void) { TMenuItem* Component = dynamic_cast(GetComponent(0)); if(Component) { return Component->GetImageList(); } return 0; }
The more complex method was shown to highlight the principles of obtaining a pointer to the component’s parent. We shall now advance our discussion further and consider an ImageIndex property editor for the THeaderSection class derived from TCollectionItem (and ultimately TPersistent) and used in the THeaderControl component. Our property editor will be called THeaderSectionIndexProperty. This will illustrate another useful technique when writing property editors and lead us to a more general solution for implementing and registering ImageIndex property editors for classes whose parent contains the TCustomImageList pointer. Only the GetParentImageList() function will be shown (see Listing 10.26). As with the TMenuItemImageIndexProperty property editor, the primary concern is to obtain a pointer to the parent of the component that contains the ImageIndex property. If we examine the THeaderSection class, we see that it is a TCollectionItem-derived class and therefore has a corresponding TCollection-derived container class, in this case a THeaderSections class. A pointer to THeaderSection’s container (a THeaderSections object) can be obtained from its Collection property. This returns a TCollection*, which must then be dynamic_casted to a THeaderSections pointer. Once we have a pointer to a collection items container, we need to determine the parent of the collection, which will contain the image list we require. The only method available for us to do this is the protected GetOwner() function. This returns a TPersistent*. Unfortunately, the function is protected so we have no way of getting at it directly. However, there is a method of calling this function. To call a member of a class that is protected or private, you must do two things. First you must create a descendant of the class and promote the visibility of the required members (for example, functions or properties). In other words, you must redeclare as public those class
12 9721 CH10
11/13/00
9:44 AM
Page 651
Creating Property and Component Editors CHAPTER 10
651
members that you require access to. Such a class is called an access class. Once this is done, it is possible to access these class members by static_casting the class pointer to that of the access class. This technique is used in Listing 10.26. LISTING 10.26
Code Required for the
THeaderSectionIndexProperty::GetParentImageList() Function //---------------------------------------------------------------------------// // // // THeaderSectionsAccessImageIndexProperty // // // //---------------------------------------------------------------------------// class THeaderSectionsAccess : public THeaderSections { public: DYNAMIC Classes::TPersistent* __fastcall GetOwner(void); }; //---------------------------------------------------------------------------// // // // THeaderSectionImageIndexProperty // // // //---------------------------------------------------------------------------// //---------------------------------------------------------------------------// Imglist::TCustomImageList* __fastcall THeaderSectionImageIndexProperty::GetParentImageList(void) { THeaderSection* Component = dynamic_cast(GetComponent(0)); if(Component) { THeaderSections* HeaderSections = dynamic_cast(Component->Collection); if(HeaderSections) { TPersistent* Owner = static_cast(HeaderSections)->GetOwner();
PROPERTY AND COMPONENT EDITORS
THeaderControl* HeaderControl = dynamic_cast(Owner);
10
12 9721 CH10
11/13/00
652
9:44 AM
Page 652
C++Builder 5 Essentials PART I
LISTING 10.26
Continued
if(HeaderControl) { return HeaderControl->Images; } } } return 0; } //---------------------------------------------------------------------------//
Once a pointer to the owner of the collection has been obtained, it can be dynamic_casted to the required type, in this case a pointer to a THeaderControl. We then return the pointer to the THeaderControl’s TCustomImageList.
A Generalized Solution for ImageIndex Properties The VCL contains several TPersistent- and TComponent-derived classes with ImageIndex properties. The previous section covered just two. Others are shown in Table 10.10. TABLE 10.10
VCL Classes with ImageIndex Properties
Class
Derivation Hierarchy
TCoolBand
→ TCollectionItem → TPersistent → TComponent → TPersistent → TCollectionItem → TPersistent → TCollectionItem → TPersistent → TComponent → TPersistent → TWinControl → TControl → TComponent → TPersistent → TGraphicControl → TControl → TComponent → TPersistent
TCustomAction THeaderSection TListColumn TMenuItem TTabSheet TToolButton
Note that Table 10.10 draws a distinction between those classes that descend from TComponent and those that descend only from TPersistent. Also, three of the classes, TListColumn, TMenuItem and TtoolButton, have special considerations when determining the parent TCustomImageList*. This will become clear later, when the implementations for generalized ImageIndex property editors are presented. It would be possible and indeed perfectly proper to declare a property editor for the property of each of these classes. The code required for this is present on the accompanying CD-ROM in the PropertyEditors unit in the EnhancedEditors package.
ImageIndex
12 9721 CH10
11/13/00
9:44 AM
Page 653
Creating Property and Component Editors CHAPTER 10
653
However, it is also possible to write two more general property editors, one for TPersistentderived classes and one for TComponent-derived classes. Regardless of what is done, it is appropriate to define a base class for any such property editors, allowing new property editors to be derived from the base class. Only the GetImageList() method will need to be overridden by derived classes. A suitable class definition is shown in Listing 10.27. LISTING 10.27
Definition of the TImageIndexProperty Base Class
class PACKAGE TImageIndexProperty : public TIntegerProperty { typedef TIntegerProperty inherited; private: static const int Border = 2; static const int MaxImageWidth = 64; static const int MaxImageHeight = 64; protected: virtual Imglist::TCustomImageList* __fastcall GetImageList(void) = 0; public: virtual TPropertyAttributes __fastcall GetAttributes(void); virtual void __fastcall GetValues(Classes::TGetStrProc Proc); DYNAMIC void __fastcall ListMeasureWidth(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AWidth); DYNAMIC void __fastcall ListMeasureHeight(const AnsiString Value, Graphics::TCanvas* ACanvas, int& AHeight); DYNAMIC void __fastcall ListDrawValue(const AnsiString Value, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected); DYNAMIC void __fastcall PropDrawValue(Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, bool ASelected);
PROPERTY AND COMPONENT EDITORS
// This is a read-only property to be used as the pointer to the component’s // image list __property Imglist::TCustomImageList* RemoteImageList = {read=GetImageList};
10
12 9721 CH10
11/13/00
654
9:44 AM
Page 654
C++Builder 5 Essentials PART I
LISTING 10.27
Continued
protected: #pragma option push -w-inl inline __fastcall virtual TImageIndexProperty(const _di_IFormDesigner ADesigner, int APropCount) : TIntegerProperty(ADesigner, APropCount) { } #pragma option pop public: #pragma option push -w-inl inline __fastcall virtual ~TImageIndexProperty(void) { } #pragma option pop };
Note that TImageIndexProperty is an abstract base class because it contains a pure virtual function. This is the GetImageList() function that derived classes must implement. The implementation of TImageIndexProperty is similar to that shown previously. By deriving our ImageIndex property editors from this class, we only need to write the Such a method for a TPersistentDerivedImageIndexProperty property editor is shown in Listing 10.28. Note that the variable FParentImageListName is a private AnsiString member initialized in the constructor as “Images”. GetImageList()method.
LISTING 10.28
Implementation of TPersistentDerivedImageIndexProperty
//---------------------------------------------------------------------------// // // // TComponentAccess // // // //---------------------------------------------------------------------------// class TComponentAccess : public TComponent { public: DYNAMIC Classes::TPersistent* __fastcall GetOwner(void); }; //---------------------------------------------------------------------------// // // // TPersistentDerivedImageIndexProperty // // // //---------------------------------------------------------------------------//
12 9721 CH10
11/13/00
9:44 AM
Page 655
Creating Property and Component Editors CHAPTER 10
LISTING 10.28
655
Continued
__fastcall TPersistentDerivedImageIndexProperty::TPersistentDerivedImageIndexProperty (const _di_IFormDesigner ADesigner, int APropCount) : TImageIndexProperty(ADesigner, APropCount) { FParentImageListName = “Images”; } //---------------------------------------------------------------------------// Imglist::TCustomImageList* __fastcall TPersistentDerivedImageIndexProperty::GetImageList(void) { TPersistent* Parent = static_cast(GetComponent(0))->GetOwner(); while( Parent != 0 && !dynamic_cast(Parent) ) { Parent = static_cast(Parent)->GetOwner(); } if(Parent == 0) return 0; PPropInfo PropInfo = Typinfo::GetPropInfo(static_cast(Parent->ClassInfo()), FParentImageListName); if(PropInfo == 0) return 0; return ( reinterpret_cast(Typinfo::GetOrdProp(Parent, PropInfo)) ); } //---------------------------------------------------------------------------//
10 PROPERTY AND COMPONENT EDITORS
The code in Listing 10.28 is more straightforward than it first appears. As before, we use an access class to promote the visibility of, in this case, TComponent’s protected GetOwner() method. Initially, the immediate parent of the component whose property is being edited is obtained by treating the component as a TComponent and calling the GetOwner() method. If there is a parent class and it is not a TComponent, then we check to see if the parent has a TComponent-derived parent. If it does have a TComponent-derived parent, we check to see if that parent has a TComponent-derived parent, and so on. When the while loop finishes, either
12 9721 CH10
11/13/00
656
9:44 AM
Page 656
C++Builder 5 Essentials PART I
we have found a TComponent-derived parent or Parent is NULL. Providing there is a TComponent-derived parent, we can try to obtain a PPropInfo for a property that has the same name as that contained in the FParentImageListName AnsiString data member and belongs to Parent. This line is used to determine a PTypeInfo for Parent: static_cast(Parent->ClassInfo())
If a matching property exists, the GetOrdProp() function is used to obtain the TCustomImageList pointer. Because GetOrdProp() returns an int value, this must be cast to the correct pointer using reinterpret_cast. This GetImageList()implementation can therefore be used for any TPersistent-derived classes with an ImageIndex property, so long as the parent’s image list property is called “Images” (for example, TCoolBand and THeaderSection). If it is not, then a class must be derived from this one, and the AnsiString FParentImageListName must be changed. This is done with the TListColumn class, whose parent image list property is called “SmallImages”. Similarly, a property editor class for TComponent-derived classes, TComponentDerivedImageIndexProperty, can be written. The implementation of the GetImageList() method is shown in Listing 10.29. LISTING 10.29
Implementation of TComponentDerivedImageIndexProperty::
GetImageList() Imglist::TCustomImageList* __fastcall TComponentDerivedImageIndexProperty::GetImageList(void) { TComponent* Parent = static_cast(GetComponent(0))->GetParentComponent(); if(Parent == 0) return 0; PPropInfo PropInfo = Typinfo::GetPropInfo(static_cast(Parent->ClassInfo()), FParentImageListName); if(PropInfo == 0) return 0; return ( reinterpret_cast(Typinfo::GetOrdProp(Parent, PropInfo)) ); }
12 9721 CH10
11/13/00
9:44 AM
Page 657
Creating Property and Component Editors CHAPTER 10
657
The code for the GetImageList() method of the TComponentDerivedImageIndexProperty property editor is almost identical to that of TPersistentDerivedImageIndexProperty. The only difference is that Parent is initially obtained as a TComponent-derived class, simplifying the code. This implementation can be used for TComponent-derived classes with an ImageIndex property, provided the parent’s TCustomImageList property is called “Images” (for example, TTabSheet). This could also be used for TMenuItem and TToolButton, but in the case of TMenuItem we want to consider the SubMenuImages property also. The previous implementation for TMenuItem is better. For TToolButton, a better implementation is to use the ImageIndex property for the parent’s Images property only when the button is enabled; otherwise, use the parent’s DisabledImages property. The implementation for the GetImageList() method of a TToolButtonImageIndexProperty property editor is shown in Listing 10.30. LISTING 10.30
Implementation of TToolButtonImageIndexProperty::GetImageList()
Imglist::TCustomImageList* __fastcall TToolButtonImageIndexProperty::GetImageList(void) { TToolButton* Component = dynamic_cast(GetComponent(0)); if(Component) { TToolBar* ParentToolBar = dynamic_cast(Component->Parent); if(ParentToolBar) { if(Component->Enabled) return ParentToolBar->Images; else return ParentToolBar->DisabledImages; } } return 0; }
The code shown in Listing 10.29 is self-explanatory; if the TToolButton being edited is disabled (Enabled = false) then the DisabledImages image list in the parent TToolBar is used; otherwise, the normal Images image list is used. The class hierarchy for this general solution to ImageIndex properties is shown in Figure 10.7.
10 PROPERTY AND COMPONENT EDITORS
From Table 10.10 we know that the TPersistentDerivedImageIndexProperty property editor will be used for the TCoolBand and THeaderSection components. The TComponentDerivedImageIndexProperty property editor will be used for the TCustomAction and the TTabSheet components.
12 9721 CH10
11/13/00
658
9:44 AM
Page 658
C++Builder 5 Essentials PART I
TObject TPropertyEditor TOrdinalProperty TIntegerProperty
TImageIndexProperty TPersistentDerivedImageIndexProperty TComponentDerivedImageIndexProperty TListColumnImageIndexProperty TToolButtonImageIndexProperty TMenuItemImageIndexProperty
FIGURE 10.7 The TImageIndexProperty inheritance hierarchy.
Creating Custom Component Editors The previous section discussed property editors for components as a method of allowing a more intuitive and robust interface at designtime. Component Editors take this further by allowing custom editors for the whole component to be created. Custom component editors also allow the context menu for each component (shown when the component is right-clicked) to be customized, along with specifying the behavior when a component is double-clicked on a form. This section, like the previous one, presents the background and principles required to create custom component editors. Component editors add the possibility of customizing the default behavior associated with editing a component and also allow additional behavior to be specified. There are two classes available for creating component editors: TComponentEditor and TDefaultEditor. The relationship between the two is shown in Figure 10.8. Figure 10.8 shows additional information, namely the virtual functions that should be overridden in order to customize the component editor’s behavior. This can be referred to when the virtual functions themselves are discussed later in this section, in Table 10.12 and in subsequent sections with the methods’ names. As was stated initially, creating a custom component editor allows the default behavior that occurs in response to the component being right-clicked or double-clicked in the IDE to be specified. Table 10.11 lists both mouse events and indicates which of the virtual functions are invoked. The default behavior of each of the classes is also stated.
12 9721 CH10
11/13/00
9:44 AM
Page 659
Creating Property and Component Editors CHAPTER 10
659
TObject TInterfaceObject TComponentEditor public virtual methods Edit() GetVerbCount() GetVerb() PrepareItem() ExecuteVerb() Copy()
Legend Method
Implemented virtual method
Method
Unimplemented virtual method
TDefaultEditor public virtual methods Edit() GetVerbCount() GetVerb() PrepareItem() ExecuteVerb() Copy() protected virtual method EditProperty()
FIGURE 10.8 The TComponentEditor inheritance hierarchy.
TABLE 10.11
TComponentEditor and TDefaultEditor Mouse Responses
Default Action
virtual
Functions Invoked
Right-Clicked
The component’s context menu is displayed.
GetVerbCount() is invoked first. This is used to return the number of items to be added to the top of the default context menu. GetVerb()is called next. This allows an AnsiString representing each of the menu items to be returned.
10 PROPERTY AND COMPONENT EDITORS
When the component is…
12 9721 CH10
11/13/00
660
9:44 AM
Page 660
C++Builder 5 Essentials PART I
TABLE 10.11
Continued
When the component is…
Default Action
virtual
Functions Invoked
PrepareItem()is called before the menu item is shown, allowing it to be customized. ExecuteVerb()is called only if one of the newly added menu items is clicked. Code to execute the desired behavior goes here.
Double-Clicked
The default action depends on the class from which the editor is derived.
Edit()is invoked. Code to perform the desired action is placed here.
TComponentEditor:
If items have been added to the context menu, the first item is executed. TDefaultEditor:
An empty event handler is created for OnChange, OnCreate, or OnClick, whichever appears first in the component’s list of event properties. If none of the above events exist for the component, a handler is created for the first event that appears. If the component has no events, then nothing happens. In Figure 10.8 we can see that TComponentEditor and TDefaultEditor are essentially the same in that they offer similar functionality. Where they differ (as seen in Table 10.11) is in the implementation of the Edit() method. Choosing which of the two classes to derive your custom component editor from should be based on the following criteria. If you want the component editor to generate an empty event handler for one of three default events or for a particular event, when the component is double-clicked, then you should derive it from TDefaultEditor; otherwise, derive it from TComponentEditor. If you do not create a custom component editor for a component, C++Builder uses TDefaultEditor.
12 9721 CH10
11/13/00
9:44 AM
Page 661
Creating Property and Component Editors CHAPTER 10
661
Once the decision has been made as to which component editor class to derive from, the appropriate methods should be overridden. Table 10.12 lists the methods from both classes and details the purpose of each. TABLE 10.12 virtual
TComponentEditor and TDefaultEditor virtual Functions
Function
Purpose
int GetVerbCount(void)
Returns an int representing the number of menu items (verbs, as in doing words) that are going to be added.
AnsiString GetVerb(int Index)
Returns an AnsiString representing the menu item’s name as it will appear in the context menu. The following conventions should be remembered: Use & to designate a hotkey. Append ... to an item that executes a dialog. Use a - to make the menu item a separator bar. PrepareItem()is called for each verb in the context menu, passing the TMenuItem that will be used to represent the verb in the context menu. This allows the menu item to be customized. It can also be used to hide an item by setting its Visible property to false. ExecuteVerb()is invoked when one of the custom menu items is selected. Index indicates which one. Edit() is invoked when the component is double-clicked. What happens is user defined. The default behavior is listed in Table 10.11. Used to determine which event an empty handler is generated for when the component is double-clicked.
void PrepareItem(int Index, const Menus::TMenuItem* AItem)
void ExecuteVerb(int Index)
void Edit(void)
void EditProperty(TPropertyEditor* PropertyEditor, bool& Continue, bool& FreeEditor) (TDefaultEditor only) void Copy(void)
10 PROPERTY AND COMPONENT EDITORS
Copy()should be invoked when the component is copied to the Clipboard. This needs to be overridden only if a special format is needed to be copied to the Clipboard, such as an image from a graphical component.
12 9721 CH10
11/13/00
662
9:44 AM
Page 662
C++Builder 5 Essentials PART I
Suitable class definitions for TComponentEditor- and TDefaultComponent-derived component editors are shown in Listing 10.31 and Listing 10.32, respectively. LISTING 10.31
Definition Code for a Custom TComponentEditor-Derived Component
Editor #include “dsgnintf.hpp” class TCustomComponentEditor : public TComponentEditor { typedef TComponentEditor inherited; public: // Double-Click virtual void __fastcall Edit(void); // Right-Click // CONTEXT MENU - Step 1 virtual int __fastcall GetVerbCount(void); // - Step 2 virtual AnsiString __fastcall GetVerb(int Index); // - Step 3 (OPTIONAL) virtual void __fastcall PrepareItem(int Index, const Menus::TMenuItem* AItem); // - Step 4 virtual void __fastcall ExecuteVerb(int Index); // Copy to Clipboard virtual void __fastcall Copy(void); public: #pragma option push -w-inl inline __fastcall virtual TCustomComponentEditor(Classes::TComponent* AComponent, _di_IFormDesigner ADesigner) : TComponentEditor(AComponent, ADesigner) { } #pragma option pop public: #pragma option push -w-inl inline __fastcall virtual ~TCustomComponentEditor(void) { } #pragma option pop };
12 9721 CH10
11/13/00
9:44 AM
Page 663
Creating Property and Component Editors CHAPTER 10
LISTING 10.32
663
Definition Code for a Custom TDefaultEditor-Derived Component Editor
#include “dsgnintf.hpp” class TCustomDefaultEditor : public TDefaultEditor { typedef TDefaultEditor inherited; protected: // Double-Click // CHOOSE EVENT virtual void __fastcall EditProperty(TPropertyEditor* PropertyEditor, bool& Continue, bool& FreeEditor); public: // Right-Click // CONTEXT MENU - Step 1 virtual int __fastcall GetVerbCount(void); // - Step 2 virtual AnsiString __fastcall GetVerb(int Index); // - Step 3 (OPTIONAL) virtual void __fastcall PrepareItem(int Index, const Menus::TMenuItem* AItem); // - Step 4 virtual void __fastcall ExecuteVerb(int Index); // Copy to Clipboard virtual void __fastcall Copy(void); public: #pragma option push -w-inl inline __fastcall virtual TCustomDefaultEditor(Classes::TComponent* AComponent, _di_IFormDesigner ADesigner) : TDefaultEditor(AComponent, ADesigner) { } #pragma option pop public: #pragma option push -w-inl inline __fastcall virtual ~TCustomDefaultEditor(void) { } #pragma option pop };
10 PROPERTY AND COMPONENT EDITORS
12 9721 CH10
11/13/00
664
9:44 AM
Page 664
C++Builder 5 Essentials PART I
In Listing 10.31 and Listing 10.32, it can be seen that there is little difference between the definitions of the two kinds of component editor. In fact, the techniques for implementing context menu items are identical. The difference between the classes is that you override the Edit() method for a TComponentEditor-derived class’s double-click behavior, whereas you override the EditProperty() method for a TDefaultEditor class’s double-click behavior. The following sections take each of the virtual methods in turn and discuss implementation issues. Information presented for the Edit() method is applicable only to TComponentEditor-derived classes, and information presented for the EditProperty() method is applicable only to TDefaultEditor-derived classes. Note that the example namespace modifiers used in the function implementation headers reflect this. TCustomComponentEditor is a hypothetical TComponentEditor-derived class, TCustomDefaultEditor is a hypothetical TDefaultEditor-derived class, and TMyCustomEditor is a class that could be derived from either.
The Edit() Method The main purpose of overriding the Edit() method is to display a form to the user to allow easier editing of the component’s values. A good example of this is the component editor for the TChart component on the Additional page of the Component Palette. To this end, the code required is similar to that presented for TPropertyEditor’s Edit() method in the “Creating Custom Property Editors” section, earlier in this chapter. As before, there are two approaches to implementing such a form. Either the form can update the component as the form itself is modified, or the component can be updated after the form is closed. There is one extra and very important consideration that must be remembered: Each time the component is updated, the Modified() method of TComponentEditor’s Designer property must be called. This is so that the IDE knows that the component has been modified. Hence, the following is required after code that modifies the component: if(Designer) Designer->Modified();
An if statement is used in the previous code to ensure that a non-zero value is returned from Designer before we try to call Modified(). If zero is returned, there is little we can do, because it means the IDE is not accessible. We know that, for the form to be able to change the component’s properties, we must somehow link the form to the component, in a similar fashion as for property editors previously. This is reasonably straightforward and requires two things. The first is that a public property should be declared in the form’s definition that is a pointer to the type of component the component editor is for. Secondly, this must be pointed to the actual instance of the component that is to be edited. The pointer to the current instance of the component is obtained by using TComponentEditor’s Component property, as follows:
12 9721 CH10
11/13/00
9:44 AM
Page 665
Creating Property and Component Editors CHAPTER 10
665
TMyComponent* MyComponent = dynamic_cast(Component);
The pointer obtained can be equated to the form’s component pointer property. However, we must also make a reference to Designer available from within the form so that the IDE can be notified of changes that are made to the component. This can be passed as a parameter in the form’s constructor. The component can then be modified directly through the property in the form. Suitable code for this approach is shown in Listing 10.33. Don’t forget to call Designer->Modified() after the component is modified by the form. LISTING 10.33 Code for a Custom Component Editor Form to Be Called from Edit() That Allows Continual Updating // First show important code for TComponentEditorForm // IN THE HEADER FILE //---------------------------------------------------------------------------// #ifndef MyComponentEditorFormH #define MyComponentEditorFormH //---------------------------------------------------------------------------// #include #include #include <StdCtrls.hpp> #include #include “HeaderDeclaringTComponentClass” //---------------------------------------------------------------------------// class TMyComponentEditorForm : public TForm { __published: // IDE-managed Components private: TComponentClass* FComponentClass; _di_IformDesigner& Designer; // Other declarations here for example restore values if ‘Cancel’ // is pressed protected: void __fastcall SetComponentClass(TComponentClass* Pointer); public: __fastcall TMyComponentEditorForm(TComponent* Owner, _di_IformDesigner& EditorDesigner);
}; //---------------------------------------------------------------------------// #endif
10 PROPERTY AND COMPONENT EDITORS
__property TComponentClass* ComponentClass = {read=FComponentClass, write=SetComponentClass}; // Other declarations here
12 9721 CH10
11/13/00
666
9:44 AM
Page 666
C++Builder 5 Essentials PART I
LISTING 10.33
Continued
// THE IMPLEMENTATION FILE //---------------------------------------------------------------------------// #include #pragma hdrstop #include ”MyComponentEditorForm.h” //---------------------------------------------------------------------------// #pragma package(smart_init) #pragma resource “*.dfm” //---------------------------------------------------------------------------// __fastcall TMyComponentEditorForm:: TMyComponentEditorForm(TComponent* Owner, _di_IformDesigner& EditorDesigner) : TForm(Owner), Designer(EditorDesigner) { } //---------------------------------------------------------------------------// void __fastcall TMyPropertyForm::SetComponentClass(TComponentClass* Pointer) { FComponentClass = Pointer; if(FComponentClass != 0) { // Store current component values and display them } } //---------------------------------------------------------------------------// // NOW SHOW THE Edit() METHOD #include “MyComponentEditorForm.h” // Remember this void __fastcall TCustomComponentEditor::Edit(void) { // Create the form std::auto_ptr MyComponentEditorForm(new TMyComponentEditorForm(0)); // Link the component property MyComponentEditorForm->ComponentClass = dynamic_cast(Component); // Show the form. The form does all the work. MyPropertyForm->ShowModal(); }
12 9721 CH10
11/13/00
9:44 AM
Page 667
Creating Property and Component Editors CHAPTER 10
667
As in the case of custom property editor forms, the component’s current property values can be stored when the form’s Component property is linked to the component. This allows the operation to be cancelled and the previous values restored. One thing to pay attention to is the possibility of a NULL pointer being returned from dynamic_cast; this should not occur, but if it does the form will not be able to modify any of the component’s properties. An exception could be thrown to indicate this to the user. The second approach to implementing the Edit() method is equally simple. A form is displayed as a dialog and, when it returns, the values entered are assigned to the component. A pointer to the current instance of the component being edited is obtained from TComponentEditor’s Component property: TMyComponent* MyComponent = dynamic_cast(Component);
The code required in the Edit() method in this approach to its implementation is greater because the component property values must be assigned to the form after it is created but before it is shown. On closing, the form’s values must be assigned to the requisite component properties. The code required for the Edit() method is shown in Listing 10.34. LISTING 10.34 Code for a Custom Form to Be Called from the Edit() Method with No Updating Until Closing #include “MyComponentEditorDialog.h” // Include the header for the Dialog! // Dialog is TMyComponentDialog void __fastcall TCustomComponentEditor::Edit(void) { TMyComponent* MyComponent = dynamic_cast(Component); if(MyComponent != 0) { // Create the form std::auto_ptr MyComponentDialog(new TMyComponentDialog(0)); // // // //
Set the current property values in the dialog MyComponentDialog->value1 = MyComponent->value1; MyComponentDialog->value2 = MyComponent->value2; and so on...
10 PROPERTY AND COMPONENT EDITORS
// Show the form and see the result. if(MyPropertyDialog->ShowModal() == IDOK) { // Then set the new property value(s) // MyComponent->value1 = MyComponentDialog->value1;
12 9721 CH10
11/13/00
668
9:44 AM
Page 668
C++Builder 5 Essentials PART I
LISTING 10.34
Continued
// MyComponent->value2 = MyComponentDialog->value2; // and so on... if(Designer) Designer->Modified(); // DON’T FORGET! } } else { throw EInvalidPointer (“Cannot Edit: A component pointer is not available!”); } }
In the second approach to implementing the Edit() method shown in Listing 10.34, implementation code for the dialog has not been shown. This is because there are no special considerations specific to this approach that need to be highlighted. Also be aware that a dialog wrapper class could be used instead of calling the dialog directly, in which case the dialog’s Execute() method would be called to display the dialog.
The EditProperty() Method The purpose of overriding the EditProperty() method is to specify a particular event or one of a number of possible events that should have an empty event handler generated for it by the IDE when the component is double-clicked. For example, consider a component for serial communications. Typically, the most commonly used event would be one that signals when data has been received and is available, perhaps named OnDataReceived. For this to be the event for which a handler is generated, EditProperty() needs to be overridden as follows: void __fastcall TCustomDefaultEditor::EditProperty(TPropertyEditor* PropertyEditor, bool& Continue, bool& FreeEditor) { if( PropertyEditor->ClassNameIs(“TMethodProperty”) && (CompareText(PropertyEditor->GetName(), “OnDataReceived”) == 0) ) { inherited::EditProperty(PropertyEditor, Continue, FreeEditor); } }
The if statement checks two things. First it checks that the property editor is a TMethodProperty class; in other words, it checks that the property editor is for an event. It then checks to see if the property editor is called OnDataReceived. The CompareText() function is used for this. CompareText() returns 0 when the two AnsiStrings passed to it are
12 9721 CH10
11/13/00
9:44 AM
Page 669
Creating Property and Component Editors CHAPTER 10
669
equal. Note that CompareText() is not case sensitive. If the property editor matches these criteria, then the parent EditProperty() method is called, in this case TDefaultEditor’s EditProperty(), which generates the empty event handler for this event. This is called by using the inherited typedef as a namespace modifier, so the previous code could be written as follows: TDefaultEditor::EditProperty(PropertyEditor, Continue, FreeEditor);
The reason for using the typedef is that if the name of TDefaultEditor ever changed, the implementation code would not be affected. Only the class definition in the header file would need to be changed. If a choice of events was to be specified, perhaps because the same component editor was to be registered for a variety of components, then the if statement would be replaced by if-else-if statements. For example: if( PropertyEditor->ClassNameIs(“TMethodProperty”) && (CompareText(PropertyEditor->GetName(), “OnEvent1”) == 0) ) { inherited::EditProperty(PropertyEditor, Continue, FreeEditor); } else if( PropertyEditor->ClassNameIs(“TMethodProperty”) && (CompareText(PropertyEditor->GetName(), “OnEvent2”) == 0) ) { inherited::EditProperty(PropertyEditor, Continue, FreeEditor); } else if( PropertyEditor->ClassNameIs(“TMethodProperty”) && (CompareText(PropertyEditor->GetName(), “OnEvent3”) == 0) ) { inherited::EditProperty(PropertyEditor, Continue, FreeEditor); }
It also could be replaced by a single if that ORs the possible event occurrences:
In either case, the first matching occurrence will be used.
10 PROPERTY AND COMPONENT EDITORS
if( (PropertyEditor->ClassNameIs(“TMethodProperty”) && (CompareText(PropertyEditor->GetName(), “OnEvent1”) == 0) || (PropertyEditor->ClassNameIs(“TMethodProperty”) && (CompareText(PropertyEditor->GetName(), “OnEvent1”) == 0) || (PropertyEditor->ClassNameIs(“TMethodProperty”) && (CompareText(PropertyEditor->GetName(), “OnEvent1”) == 0) ) { inherited::EditProperty(PropertyEditor, Continue, FreeEditor); }
12 9721 CH10
11/13/00
670
9:44 AM
Page 670
C++Builder 5 Essentials PART I
The GetVerbCount() Method Few methods are as easy to override as this. Simply return an integer that represents the number of additional menu items that you want to appear in the component’s context menu. Don’t forget that a separator bar constitutes a menu item. Sample code for three custom menu items would be as follows: int __fastcall TMyCustomEditor::GetVerbCount(void) { return 4; }
The GetVerb() Method Almost as straightforward as the GetVerbCount() method, this method requires that the AnsiString text for each menu item be returned. Remember that returning a - makes the menu item a separator bar. Sample code is AnsiString __fastcall TMyCustomEditor::GetVerb(int Index) { switch(Index) { case 0 : return “&Edit Component...”; case 1 : return “© 2000 Me”; case 2 : return “-”; case 3 : return “Do Something Else”; default : return “”; } }
If you do not specify an accelerator key (using the & symbol), then one is determined automatically by the IDE. In fact, all predefined context menu items’ accelerator keys are determined by the IDE at runtime. This allows clashes with user-defined accelerator keys to be avoided. Accelerator key definitions for user-defined menu items take precedence over a predefined context menu item’s accelerator key definitions. If a clash occurs, the predefined menu item’s accelerator key is reassigned to a different letter. Finally, remember that a separator bar is automatically placed between the custom menu items and the predefined menu items, so it is not necessary to add one as the last item. However, doing so will not make any difference, because the context menu’s AutoLineReduction property is set to maAutomatic (refer to the C++Builder online help for further details).
The PrepareItem() Method This method, new to C++Builder 5, need not be implemented, and in fact it generally isn’t. What it offers is the option to customize each menu item further. Most notably, it allows custom rendering of a menu item, the ability to disable the menu item (Enable = false), the
12 9721 CH10
11/13/00
9:44 AM
Page 671
Creating Property and Component Editors CHAPTER 10
671
ability to hide the menu item (Visible = false), and the ability to add sub-menu items. This is possible because PrepareItem() has two parameters. The first, Index, serves the same purpose as it does in the preceding context menu functions, namely to indicate which menu item the function call refers to. However, the second parameter is a pointer to the menu item (TMenuItem) that will be used to represent the menu item in the context menu. This gives you access to all the facilities that TMenuItem offers. There is a catch, however: The pointer is to const TMenuItem, so it is not possible to modify the menu item through the pointer passed. Instead, a non-const pointer of type TMenuItem should be pointed to the same menu item and the menu item modified through that pointer. For example, maintaining continuity with our previous examples, to custom render the second menu item (the copyright item), we would write the code in Listing 10.35. LISTING 10.35
Basic Code for the PrepareItem() Method
void __fastcall TMyCustomEditor::PrepareItem(int Index, const Menus::TMenuItem* AItem) { switch(Index) { case 0 : break; case 1 : { TMenuItem* MenuItem = const_cast(AItem); // // // // // // // // // // // //
Now that we have a pointer we can do what we like For example: 1. To Disable the menu item write MenuItem->Enabled = false; 2. To Hide the menu item write – MenuItem->Visible = false; 3. To add a bitmap to the menu item write – MenuItem->Bitmap->LoadFromResourceName (reinterpret_cast(HInstance), “BITMAPNAME”); or any other stuff, for example assign an event handler or even add menu sub-items...
} break;
} }
PROPERTY AND COMPONENT EDITORS
case 2 : break; case 3 : break; default : break;
10
12 9721 CH10
11/13/00
672
9:44 AM
Page 672
C++Builder 5 Essentials PART I
Pay particular attention to this line: TMenuItem* MenuItem = const_cast(AItem);
This is where we obtain the pointer with which we can edit the TMenuItem. Also note the third example of adding a bitmap to the menu item: MenuItem->Bitmap->LoadFromResourceName(reinterpret_cast(HInstance), “BITMAPNAME”);
This assumes that a resource file has been imported into the package that contains an image called BITMAPNAME. Otherwise, MenuItem will be unable to load the image. Not being able to load the image is quite disastrous: The IDE will crash, so make sure your names are right. For more information on this, refer to the section “Using Predefined Images in Custom Property and Component Editors,” later in this chapter, where this topic is covered in more detail. Note also that reinterpret_cast is used to cast HInstance of type void* to type int, as expected by the LoadFromResourceName member function.
Adding Custom Event Handlers to Context Menu Items Adding custom event handlers to custom menu items involves a two-step process. First, the required event handler must be written as a member function of the component editor class. Its signature must match exactly that of the event it is to handle. Second, when the PrepareItem() function is called and the non-const MenuItem pointer is obtained, the handler member function can be equated to the appropriate MenuItem event. For example, to create a custom event handler for the menu item’s OnAdvancedDrawItem event, declare a function with a name such as AdvancedDrawMenuItem1() (because it refers to MenuItem 1) with the same parameters as OnAdvancedDrawItem in the component editor’s class definition. You will probably want to make it protected and virtual, just in case you want to derive another class from this one. The code appearing in the class definition is as follows: protected: virtual void __fastcall AdvancedDrawMenuItem1(System::TObject* Sender, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, TOwnerDrawState State);
The empty implementation for this would be virtual void __fastcall TMyCustomEditor::AdvancedDrawMenuItem1(System::TObject* Sender, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, TOwnerDrawState State) { // Custom rendering code here }
12 9721 CH10
11/13/00
9:44 AM
Page 673
Creating Property and Component Editors CHAPTER 10
673
The second stage, to ensure that our event handler is called for this menu item, is to set MenuItem’s OnAdvancedDrawItem event to this one. Add the following line of code to that shown previously in Listing 10.35 (after MenuItem is obtained): MenuItem->OnAdvancedDrawItem = AdvancedDrawMenuItem1;
Now, each time OnAdvancedDrawItem() is called, our custom rendering code will be executed. The remaining TMenuItem events can also be overridden: OnMeasureItem, OnDrawItem, and OnClick; removing the need for code in the ExecuteVerb() method for this item. However, this is not advised, because ExecuteVerb() conveniently centralizes the code associated with clicking on the context menu. That leaves only the OnMeasureItem and OnDrawItem events. Essentially, OnDrawItem is a simpler (and older) version of OnAdvancedDrawItem. It is called less often and contains less information. Use the OnAdvancedDrawItem instead. However, OnMeasureItem is a useful event that allows the size of the menu item as it appears in the context menu to be modified. The code required in the class definition for this event is as follows: protected: virtual void __fastcall MeasureMenuItem1(System::TObject* Sender, Graphics::TCanvas* ACanvas, int& Width, int& Height);
A typical implementation for this would be virtual void __fastcall TMyCustomEditor::MeasureMenuItem1(System::TObject* Sender, Graphics::TCanvas* ACanvas, int& Width, int& Height) { Width = x - Height; // Where x is the required width subtracting Height // allows for Flip Children’s sub-menu arrow Height = y; // Where y is the required height }
Adding the line of code that follows to the PrepareItem() method in Listing 10.35 in the correct section for this item ensures the event will be called: MenuItem->OnMeasureItem = MeasureMenuItem1;
10 PROPERTY AND COMPONENT EDITORS
One thing to remember is that modifying Width will have an effect on the size of the menu item only if it is bigger than the current context menu width, which is most likely controlled by other context menu items. In other words, the current context menu width will be equal to the width of the widest item. Notice that if a value is assigned to Width, perhaps because an image is going to be drawn inside the menu item, the value that should be assigned will be the desired width minus the default height. The reason for this is to allow for the sub-menu arrow symbol for the IDE’s menu item Flip Children. See Figure 10.9.
12 9721 CH10
11/13/00
674
9:44 AM
Page 674
C++Builder 5 Essentials PART I Actual Width Specified Width 1 1 2 3
Tab Order Creation Order… Flip Children
Height
Add to Repository… View as Text Context Menu Height
FIGURE 10.9 Cropped view of the TImageComponentEditor context menu, showing height and width.
The width required for the sub-menu arrow symbol is equal to the default Height of the Flip Children menu item. This value is added to any Width value that you specify, so in order to prevent having an unpainted strip down the right side of your context menu item, you must account for it by subtracting it from the width that you specify. Modifying the Height parameter will always have an effect on the height of the menu item, and setting it to 0 will make the item disappear. The motivation behind defining your own custom event handlers for any of the menu items is so that the rendering of the item can be customized. There is increased scope for this with the new OnAdvancedDrawItem event. The TOwnerDrawState Set variable gives a lot of information about the current state of the item. For example, if the menu item is selected, State will contain odSelected, allowing code such as this to be placed in the event handler: if(State.Contains(odSelected)) { // Draw the item with a clRed background } else { // Draw the item with a clBtnFace background }
Remember when you assign a handler to either OnAdvancedDrawItem or OnDrawItem that you are responsible for the entire rendering process, including displaying the text on the item’s canvas. You will need to use the TextRect method of TCanvas to do so. For more information on this, refer to the “Using Images in Property Editors” section earlier in this chapter or to the C++Builder online help. An example custom component editor (TImageComponentEditor) that handles the OnAdvancedDrawItem and OnMeasureItem events for editing the TImage class is shown in Figure 10.10. The component editor also implements both Copy to Clipboard and Paste from Clipboard methods.
12 9721 CH10
11/13/00
9:44 AM
Page 675
Creating Property and Component Editors CHAPTER 10
675
FIGURE 10.10 The context menu for TImageComponentEditor.
The possibilities offered by customizing the menu items using these events are endless. For example, it is possible to place your company logo as an image on one of the menu items or make all your custom menu items appear larger with a nicer background, making them stand out from the IDE-defined items. Incidentally, if you place menu items that perform no function, consider placing them after those that do. It can be very irritating after right-clicking on a component to have to study the menu for the item needed, especially if it is a common operation. Normally, items used most often should be placed first in the menu.
Adding Sub-Menu Items to Context Menu Items
First, the sub-menu items must be declared. If more than one sub-menu is required (as is the requirement here), it is simplest to declare an array of pointers to TMenuItems. We must be able
10 PROPERTY AND COMPONENT EDITORS
Adding sub-menu items (which are TMenuItems themselves) to a custom context menu item requires that you create the sub-menu items that you want to add at runtime. The sub-menu items are then added to the appropriate menu item using the Add() method. Typically, more than one sub-menu will be added, and the Add() method is overloaded to accept an array of TMenuItems as well as single TMenuItems. Because the added sub-menu items are also of type TMenuItem, they have all the functionality of MenuItem and can be similarly customized. As an example, code to add sub-menu items to the second menu item will be shown (remember that the index is zero based). The number added is arbitrary; this could be made a static const value in the component editor class, for example. A symbolic name, NoOfSubMenusForItem1, is used in the code snippets for greater clarity.
12 9721 CH10
11/13/00
676
9:44 AM
Page 676
C++Builder 5 Essentials PART I
to access the sub-menu items throughout our component editor class, so we shall declare the pointer array as a private variable in the class definition: TMenuItem* SubMenuItemsFor1[NoOfSubMenusForItem1];
The sub-menu items must be constructed. A good place to do this is in the component editor’s constructor. Currently, the constructor is empty and inline. It needs to be changed in both the class definition and the class implementation. The code required is // In “MyCustomEditor.h” change the constructor declaration to // the following and remove the surrounding #pragma option push // and pop directives __fastcall virtual TCustomComponentEditor(Classes::TComponent* AComponent, _di_IFormDesigner ADesigner); // The implementation for the constructor becomes: __fastcall TCustomComponentEditor:: TCustomComponentEditor(Classes::TComponent* AComponent, _di_IFormDesigner ADesigner) : TComponentEditor(AComponent, ADesigner) { for(int i=0; iCaption.sprintf(“Sub-Menu %d”, i); // Other Sub-Menu initialization } // Other Sub-Menu initialization }
If the sub-menus are created in the component editor’s constructor, then they should be deleted in the component editor’s destructor. It is also currently empty and inline, so it must be changed as the constructor was. The code required is // In “MyCustomEditor.h” change the destructor declaration to // the following and remove the surrounding #pragma option push // and pop directives __fastcall virtual ~TCustomComponentEditor(void); // The implementation for the destructor becomes: __fastcall TCustomComponentEditor::~TCustomComponentEditor(void) { for(int i=0; i
12 9721 CH10
11/13/00
9:44 AM
Page 677
Creating Property and Component Editors CHAPTER 10
677
{ delete SubMenuItemsFor1[i]; } }
With the code in place, it is trivial to add the sub-menus to menu item 1. Looking back to Listing 10.35, an implementation of the PrepareItem() method, we simply add the following line of code after the non-const pointer MenuItem is obtained: MenuItem->Add(SubMenuItemsFor1, NoOfSubMenuItemsFor1-1);
From here the sub-menus can be used as any other menu items on the context menu.
CAUTION Be careful not to assign code to a menu item with sub-menus in the ExecuteVerb() method. This can have unpredictable results.
The ExecuteVerb() Method The ExecuteVerb() method is used to place the code that should be executed when one of the custom context menu items is clicked. The basic structure is the same as that for the GetVerb() method; that is, the code is wrapped inside a switch statement. Sample code is as follows: void __fastcall TMyCustomEditor::ExecuteVerb(int Index) { switch(Index) { case 0 : EditComponet(); break; case 1 : break; // Do nothing - copyright info case 2 : break; // Do nothing - Separator line case 3 : // Do something else ... break; default : break; } }
10 PROPERTY AND COMPONENT EDITORS
This shows the basic structure required to implement the ExecuteVerb() method. Typically, a menu item will show a dialog when it is clicked, unless the item is there as a line separator or to present textual or graphical information. To that end, the code that should be placed here depends very much on the features of the component being edited. In our example, clicking on the first menu item should invoke a form through which to edit the component. This is typical and the most useful for users. The code needed is identical to that shown previously for the
12 9721 CH10
11/13/00
678
9:44 AM
Page 678
C++Builder 5 Essentials PART I
method. If the component editor is derived from TComponentEditor, and the Edit() method already contains the code required to show the component editor form, then it makes sense not to repeat that code. The best approach is to place the necessary code in a separate function, in this case EditComponent(), and call that function in both the ExecuteVerb() and Edit() methods. In fact, if the first menu item is used for this function, you need only ensure that the code is called from the ExecuteVerb() method. This is because TComponentEditor already implements the Edit() method to execute the code associated with the first menu item. Consequently, the Edit() method need not be overridden. Regardless of whether code is duplicated, if the code required to invoke a dialog is complex, it is better placed in a separate function. Edit()
All the necessary information regarding displaying forms has already been presented, and you are referred there for further information. The fourth method has been left undefined. Depending on the component, it could be anything. However, in all probability it will display a form to the user. The code presented previously for the Edit() method will also be applicable in this situation.
The Copy() Method The Copy() method is used to copy additional Clipboard formats to the Clipboard, to allow additional functionality that users may expect or find especially useful. This might be something such as the capability to copy an image in a TImage component to the Clipboard so that it can be pasted into a graphics package. The code required to implement this method depends entirely on what data is to be copied, making the implementation of this method highly variable. Therefore, it will not be dwelled on. The principles are shown in the following sample code, which allows an image from a TImage component to be copied to the Clipboard. #include “Clipbrd.hpp” void __fastcall TImageComponentEditor::Copy(void) { // Step 1 : Obtain a suitable pointer to the component TImage* Image = dynamic_cast<TImage*>(Component); // Step 2 : If successful then proceed if(Image) { // Step 3 : Obtain the required data in a format the // clipboard will recognize WORD AFormat; unsigned AData; HPALETTE APalette;
12 9721 CH10
11/13/00
9:44 AM
Page 679
Creating Property and Component Editors CHAPTER 10
679
Image->Picture->SaveToClipboardFormat(AFormat, AData, APalette); // Step 4 : Obtain a pointer to the global instance // of the clipboard TClipboard* TheClipboard = Clipboard(); // Step 5 : Copy the data to the clipboard TheClipboard->SetAsHandle(AFormat, AData); } }
The first stage is straightforward. A suitable pointer is obtained by dynamic_casting the TComponent pointer returned by TComponentEditor’s Component property. If this doesn’t work, then something is wrong. The second stage involves presenting the data in a way that the Clipboard will recognize. The data formats that the Clipboard supports are listed in the online help (it is also possible to register custom Clipboard formats; however, this is beyond the scope of this discussion). Once this is done, a pointer to the global instance of the Clipboard is obtained. Calling the global Clipboard() function returns this pointer. A new instance of TClipboard should not be created. Finally, the data can be copied to the Clipboard. A simpler implementation of the function is as follows: void __fastcall TImageComponentEditor::Copy(void) { TImage* Image = dynamic_cast<TImage*>(Component); if(Image) { Clipboard()->Assign(Image->Picture); } }
The more complex approach was shown because it is more general, and the techniques are transferable to other copy operations. It is important to note that this Copy() function does not interfere with the IDE’s copying and pasting of components on forms using the normal menu and key shortcut methods. This function offers additional copying capabilities and must be invoked manually. It could therefore be placed as a menu item on the component’s context menu. It is also perfectly conceivable that a Paste() method be defined and implemented. The definition for such a method would be virtual void __fastcall Paste(void);
void __fastcall TImageComponentEditor::Paste(void) { TImage* Image = dynamic_cast<TImage*>(Component);
PROPERTY AND COMPONENT EDITORS
The corresponding implementation is
10
12 9721 CH10
11/13/00
680
9:44 AM
Page 680
C++Builder 5 Essentials PART I if(Image) { Image->Picture->Assign(Clipboard()); } }
Registering Component Editors Registering component editors uses RegisterComponentEditor() and is straightforward. Its declaration is extern PACKAGE void __fastcall RegisterComponentEditor(TMetaClass* ComponentClass, TMetaClass* ComponentEditor);
This must be called inside the package’s Register() function. Only two parameters are required. Both parameters will be TObject descendants, so the __classid operator can be used to obtain a TMetaClass pointer for each. The first parameter is the component class for which the component editor is to be registered. The second parameter is the component editor class itself. For example, to register a custom TImage component editor called TImageComponentEditor, you would write the following: RegisterComponentEditor(__classid(TImage), __classid(TImageComponentEditor));
Component editors are like property editors in that they are used from newest to oldest. As a result, it is possible to override existing component editors in preference to custom component editors offering greater capabilities. Also as with property editors, it is possible to register component editor packages without components. This has been done with the TImageComponentEditor component editor discussed previously. It is included in the package containing the enhanced property editors developed in the “Using Images in Property Editors” section, earlier in this chapter.
Using Predefined Images in Custom Property and Component Editors Much of the previous discussion centered around the graphical customization that could be performed. It was indicated that there was good scope for using predefined images to enhance the look and feel of the editors. However, little specific advice on this was given. The techniques presented in this section are guidelines that can be applied not just to custom editors (property or component) but to projects in general.
12 9721 CH10
11/13/00
9:44 AM
Page 681
Creating Property and Component Editors CHAPTER 10
681
The first stage in using predefined images is to create them. This can be done using any graphics package, even C++Builder’s Image Editor. The main concern is to ensure that the files for the images are in bitmap format (.bmp). You may also want to consider the color depth of the target platform. It may be that you can only use 8-bit color. You might create two sets of images at different color depths. With current systems, this is not as great a concern as it once was. Once the images are available, all that needs to be done is to incorporate them into your package. You have a couple of choices on how to do this. One way is to use C++Builder’s Image Editor tool to create a compiled resource file (.res file extension). You can then paste your images into bitmaps in the compiled resource file. Almost immediately you will probably notice that Image Editor is very limited and does not seem to understand the idea of palettes. However, you can easily create a compiled resource file using this method. Make a note of the resource names that you give to the bitmaps; these are the names that you will use when you want to load the resource in your code. It should be noted that Image Editor also allows you to create icons and cursors and add them to thecompiled resource file, which can be quite useful. Another approach is to manually create a resource script file (.rc). To do this, create a new text file by selecting File, New and choosing Text from the New (default) page. Add a line in the following format: ResourceID BITMAP Filename.bmp
Do this for each bitmap you want to add to the resource. ResourceID is the identifier for the resource and is how the code you write refers to the resource. ResourceID can be either an integer value or a unique identifier string. The identifier string is simply the string typed in place of ResourceID. If an integer value is required, a header file is normally created for the resource file and #define statements used to equate the string identifiers to numbers. This is resolved when the resource file is compiled. In the examples presented here, ResourceID will be an identifier string. BITMAP is a keyword telling the compiler that the resource is a bitmap, and Filename is the file containing the bitmap. This may or may not be enclosed in quotation marks—”Filename.bmp” is also valid. For example, the resource file for the EnhancedEditors package on the CD-ROM is BITMAP BITMAP BITMAP BITMAP BITMAP BITMAP
“CopyImage.bmp” “PasteImage.bmp” “GrayedPasteImage.bmp” “GrayedCopyImage.bmp” “ActiveWritersGuildLogo.bmp” “InActiveWritersGuildLogo.bmp”
is used as a tag to help occurrences of the resources stand out. Often all uppercase letters are used for resource names to make them stand out. However, all-uppercase words are
RESOURCE_
10 PROPERTY AND COMPONENT EDITORS
RESOURCE_CopyImage RESOURCE_PasteImage RESOURCE_GreyedPasteImage RESOURCE_GreyedCopyImage RESOURCE_ActiveWritersGuildLogo RESOURCE_InActiveWritersGuildLogo
12 9721 CH10
11/13/00
682
9:44 AM
Page 682
C++Builder 5 Essentials PART I
difficult to read, so we will adopt the format shown in the previous code as an improvement to this practice. You may want to use RES_ for resources in compiled resource files (for example those created from the Image Editor) and RC_ for resources referenced in a resource script file. This allows a distinction to be made between the two in your code. To finish the resource script, simply save the file with the .rc extension. This is our resource file and all we need to add the specified images to our package (or project). The manual method of creating a resource script file offers some advantages over using C++Builder’s Image Editor. First, it allows individual images to be easily updated and edited using any graphics software tool. Also, it allows more than just bitmaps, icons, and cursors to be included. This allows a more centralized approach to be taken to resources based on their use, not their type. Image Editor is useful but lacks real power. Its inability to deal adequately with bitmap palettes makes it very frustrating. It’s a shame that such a great visual RAD tool should come with such a woeful image resource editor and manager. Good editing is not so important, but the integrity of pasted images should be maintained, and the resource managing aspect should be improved. Perhaps a new version will appear in the next release.
Adding Resource Files to Packages Once you have a saved .res containing the images that you want to use, or a .rc file referencing them, including it in your package is straightforward. From the Package Editor, click the Add to Package button. From the Add Unit page, browse to the .res or .rc file and click OK to include it. When this is done, a line similar to one of the following will be added between the #pragma hrdstop and #pragma package(smart_init) directives in the package’s source file: USERES(“PathnameOfCompiledResource.res”); // for .res files USERC(“PathnameOfResource.rc”); // for .rc files
Normally you place the resource file in the same directory as the package, in which case only the filename of the resource is required. The difference between the use of a compiled resource and a resource script is that the compiled resource requires only linking to the package’s .bpl file (or the project’s .exe file); the resource script file is compiled when the package is compiled. Alternatively, compiled resource files can also be added to the package using the #pragma resource directive: #pragma resource “PathnameOfCompiledResource.res”
Once the resources are added to the package, they can be accessed at will.
Using Resources in Property and Component Editors In the previous sections on property editors and component editors, it was suggested that predefined images could be used, for example, in the drop-down list of a property value, as menu
12 9721 CH10
11/13/00
9:44 AM
Page 683
Creating Property and Component Editors CHAPTER 10
683
item images, or even as static logos in component editors. In fact, the possibilities are endless. In this section we will look at the key issues involved in using such images to accomplish this. We have seen how to add resources to a package. With this done, it is reasonably simple to make use of those resources. When we want access to the resources, we typically load them into memory in a format that we can use. For single images, we should use TBitmap. (We also could use TPicture, which has a TBitmap property, or TImage, which has a TPicture property.) For multiple images of the same size, we should use TImageList. The methods offered by TBitmap for loading image resources are as follows: void __fastcall LoadFromResourceName(int Instance, const AnsiString ResName); void __fastcall LoadFromResourceID(int Instance, int ResID);
The first function, LoadFromResourceName(), is for use with resources with string name identifiers. The second is for use with resources with integer identifiers. In the examples that follow, LoadFromResourceName() will be used. The first parameter, Instance, expects the instance handle for the package that contains the resource. This is contained in the global HInstance variable, defined in $(BCB)\Include\Vcl\Sysinit.hpp as extern PACKAGE HINSTANCE HInstance; HINSTANCE is defined in $(BCB)\Include\wtypes.h as a void*. Hence HInstance must be cast to an int before use. Casting from a pointer to an int requires that we use reinterpret_cast: reinterpret_cast(HInstance) TImageList
offers the following methods for loading image resources (derived from
TCustomImageList): bool __fastcall ResourceLoad(TResType ResType, AnsiString Name, Graphics::TColor MaskColor); bool __fastcall ResInstLoad(int Instance, TResType ResType, System::AnsiString Name, Graphics::TColor MaskColor);
10 PROPERTY AND COMPONENT EDITORS
Only ResInstLoad() is usable inside a package, so it is the only one we will discuss, though the functions are the same, apart from the Instance variable. As before, the first variable required is HInstance. The second variable tells the image list what type the resource is: rtBitmap for a bitmap, rtIcon for an icon, and rtCursor for a cursor. A mask color can be specified in the last parameter, which is used only if TImageList’s Mask property is true.
12 9721 CH10
11/13/00
684
9:44 AM
Page 684
C++Builder 5 Essentials PART I
Loading resources is reasonably trouble free. The main question is when to load the resources. Listing 10.36 shows a possible implementation of the OnAdvancedDrawItem event handler for the third custom context menu item in TImageComponentEditor. LISTING 10.36
Loading and Displaying Image Resources
void __fastcall TImageComponentEditor::AdvancedDrawMenuItem3(System::TObject* Sender, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, TOwnerDrawState State) { std::auto_ptr Logo(new Graphics::TBitmap()); if(State.Contains(odSelected)) { Logo->LoadFromResourceName(reinterpret_cast(HInstance), “RESOURCE_ActiveWritersGuildLogo”); } else { Logo->LoadFromResourceName(reinterpret_cast(HInstance), “RESOURCE_InActiveWritersGuildLogo”); } // Draw Logo onto the Canvas ACanvas->Draw(ARect.Left, ARect.Top, Logo); }
Listing 10.36 requires little explanation. It simply checks the state of the menu item. It displays one image if it is selected and another if it is not. This works fine and is perfectly reasonable. However, it is inefficient. Logo is constructed every time the mouse moves over MenuItem3, the RESOURCE_ActiveWritersGuildLogo resource is loaded into it, and then Logo is destructed after the resource image is rendered. When the mouse moves off MenuItem3, this is repeated, except that the RESOURCE_InActiveWritersGuildLogo is loaded. There is an alternative approach that is more efficient. It requires that the resources used by the component editor be loaded into TBitmap objects or TImageList objects when the component editor is constructed. In the present example function, this would require two TBitmap objects to be constructed in the TImageComponentEditor’s constructor (and deleted in its destructor) and the necessary resources loaded into them. Pointers to the bitmaps would be private variables. Suitable code is shown in Listing 10.37.
12 9721 CH10
11/13/00
9:44 AM
Page 685
Creating Property and Component Editors CHAPTER 10
LISTING 10.37
685
Improved Code-Handling Image Resources
// FIRST THE CLASS DEFINITION #include “dsgnintf.hpp” class TImageComponentEditor : public TDefaultEditor { typedef TComponentEditor inherited; private: virtual void __fastcall AdvancedDrawMenuItem3(System::TObject* Sender, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, TOwnerDrawState State);
virtual void __fastcall MeasureMenuItem3(System::TObject* Sender, Graphics::TCanvas* ACanvas, int& Width, int& Height); // Private Data Graphics::TBitmap* ActiveWritersGuildLogo; Graphics::TBitmap* InActiveWritersGuildLogo; public: // Right-Click // CONTEXT MENU - Step 1 virtual int __fastcall GetVerbCount(void); // - Step 2 virtual AnsiString __fastcall GetVerb(int Index); // - Step 3 virtual void __fastcall PrepareItem(int Index, const Menus::TMenuItem* AItem); // - Step 4 virtual void __fastcall ExecuteVerb(int Index); // Copy image to Clipboard virtual void __fastcall Copy(void);
public: __fastcall virtual TImageComponentEditor(Classes::TComponent* AComponent,
10 PROPERTY AND COMPONENT EDITORS
// Paste image from Clipboard virtual void __fastcall Paste(void);
12 9721 CH10
11/13/00
686
9:44 AM
Page 686
C++Builder 5 Essentials PART I
LISTING 10.37
Continued _di_IFormDesigner ADesigner);
public: __fastcall virtual ~TImageComponentEditor(void); }; // NOW THE IMPLEMENTATION CODE //---------------------------------------------------------------------------// // CONSTRUCTOR // //---------------------------------------------------------------------------// __fastcall TImageComponentEditor::TImageComponentEditor (Classes::TComponent* AComponent, _di_IFormDesigner ADesigner) : TDefaultEditor(AComponent, ADesigner) { ActiveWritersGuildLogo = new Graphics::TBitmap(); InActiveWritersGuildLogo = new Graphics::TBitmap(); ActiveWritersGuildLogo->LoadFromResourceName (reinterpret_cast(HInstance), “RESOURCE_ActiveWritersGuildLogo”); InActiveWritersGuildLogo->LoadFromResourceName (reinterpret_cast(HInstance), “RESOURCE_InActiveWritersGuildLogo”); } //---------------------------------------------------------------------------// // DESTRUCTOR // //---------------------------------------------------------------------------// __fastcall TImageComponentEditor::~TImageComponentEditor(void) { delete ActiveWritersGuildLogo; delete InActiveWritersGuildLogo; } //---------------------------------------------------------------------------// void __fastcall TImageComponentEditor::AdvancedDrawMenuItem3(System::TObject* Sender, Graphics::TCanvas* ACanvas, const Windows::TRect& ARect, TOwnerDrawState State) { if(State.Contains(odSelected)) { ACanvas->Draw(ARect.Left, ARect.Top, ActiveWritersGuildLogo);
12 9721 CH10
11/13/00
9:44 AM
Page 687
Creating Property and Component Editors CHAPTER 10
LISTING 10.37
687
Continued
} else { ACanvas->Draw(ARect.Left, ARect.Top, InActiveWritersGuildLogo); } } //---------------------------------------------------------------------------//
In the new implementation of the OnAdvancedDrawItem event handler, you can see that it is much simpler and contains only code required to render the image, as it should be. One final note: It is possible to load resources into a TImageList object using a loop. Simply name the set of related images to be loaded with a number as the last character or characters. For example, suppose we have 17 images that are suitable for placement in an image list. They are named RESOURCE_ImageXX, where XX is a number ranging from 01 to 17. We would write // In the class definition --------------------------------------------------// TImageList* ImageList; //---------------------------------------------------------------------------// // In the Constructor -------------------------------------------------------// ImageList = new TImageList(this); ImageList->Masked = false; for(int i=0; i<17; ++i) { ImageList->ResInstLoad(reinterpret_cast(HInstance), rtBitmap, AnsiString(“RESOURCE_Image”).cat_sprintf(“%.2d”,i+1), clWhite); } //---------------------------------------------------------------------------// // In the Destructor --------------------------------------------------------// delete ImageList; //---------------------------------------------------------------------------//
As you can see, working with resources is not difficult and can enable your editors and (perhaps even more) your projects to have a pleasing and professional interface.
A category is a class that inherits ultimately from TPropertyCategory. This section looks at how you can use categories in custom components. There are essentially two issues that must
PROPERTY AND COMPONENT EDITORS
Registering Property Categories in Custom Components
10
12 9721 CH10
11/13/00
688
9:44 AM
Page 688
C++Builder 5 Essentials PART I
be addressed. The first is to determine which categories are to be used by a component; if categories other than the 13 predefined categories are required (see Table 10.13), then such custom category classes must be created. The second is the registration of the appropriate properties (and events) in the appropriate categories. Each of these will be discussed in turn.
Understanding Categories and Category Creation There are 13 predefined property categories in C++Builder. Their names and corresponding classes are shown in Table 10.13. TABLE 10.13
Category Classes in C++Builder
Category Name
Category Class
Action Data Database Drag, Drop and Docking Help and Hints Input Layout Legacy Linkage Locale Localizable Miscellaneous Visual
TActionCategory TDataCategory TDatabaseCategory TDragNDropCategory THelpCategory TInputCategory TLayoutCategory TLegacyCategory TLinkageCategory TLocaleCategory TLocalizableCategory TMiscellaneousCategory TVisualCategory
For a brief description of each category, refer to the “Property Categories in the Object Inspector” section in Chapter 2. Declarations for the property categories listed in Table 10.13 can be found in the $(BCB)\Include\Vcl\DsgnIntf.hpp file. Creating a new category requires that a new class be defined that inherits from TPropertyCategory or one of its descendants (for example, one of the 13 predefined categories). There is an additional stipulation that the Name and Description methods be overridden to suit the new category. Strictly speaking, overriding Description is not required, but it is trivial to accomplish and so should be done. Defining a new category requires the code in Listings 10.38 and 10.39 to be written with the appropriate customizations. NameOfCategory should be replaced with the actual name of the category, and the string values returned by Name and Description need to be suitable.
12 9721 CH10
11/13/00
9:44 AM
Page 689
Creating Property and Component Editors CHAPTER 10
LISTING 10.38
689
Definition Code for a New Category
#include class PACKAGE TNameOfCategory : public TPropertyCategory { typedef TPropertyCategory inherited; public: #pragma option push -w-inl virtual AnsiString __fastcall Name() { return Name(__classid(TNameOfCategory)); } #pragma option pop static AnsiString __fastcall Name(System::TMetaClass* vmt); #pragma option push -w-inl virtual AnsiString __fastcall Description() { return Description(__classid(TNameOfCategory)); } #pragma option pop static AnsiString __fastcall Description(System::TMetaClass* vmt); #pragma option push -w-inl // Constructor inline __fastcall TNameOfCategory (void) : TPropertyCategory() { } #pragma option pop #pragma option push -w-inl // Destructor inline __fastcall virtual ~TNameOfCategory #pragma option pop
(void) { }
};
10 PROPERTY AND COMPONENT EDITORS
The two static member functions Name and Description must be overridden. Suitable implementation code for each is shown in Listing 10.39. An appropriate string is simply returned in each case. The string returned from Name is the string that is used in the Object Inspector. An alternative approach to implementing the Name and Description methods is to load a string from a resource so that different strings could be used for different locales.
12 9721 CH10
11/13/00
690
9:44 AM
Page 690
C++Builder 5 Essentials PART I
LISTING 10.39
Implementation Code for a New Category
AnsiString __fastcall TNameOfCategory::Name(System::TMetaClass* vmt) { return “Category Name”; } AnsiString __fastcall TNameOfCategory::Description(System::TMetaClass* vmt) { return “Category Name properties and/or events”; }
The definition code should be placed in the header file that contains your other registrationrelated definitions and the implementation code in the respective .cpp file. Once this is done, the new category is available for registering properties (and events). If your category does not derive directly from TPropertyCategory (for example you inherit from TInputCategory), then the typedef for inherited must be changed to reflect that, though there is no real benefit from such a derivation. It is best to inherit directly from TPropertyCategory.
Registering a Property or Properties in a Category C++Builder provides two overloaded functions for registering a property or properties in a given category: RegisterPropertyInCategory() or RegisterPropertiesInCategory() (declared in $(BCB)\Include\Vcl\DsgnIntf.hpp). These registration functions are called in the package’s Register() function. In effect, you are not registering a single property for a given category but rather a single property filter that may be applicable to more than one property. In this respect, the names chosen for the registration functions are misleading. For registering a single property filter in a category, you can use one of four overloaded functions: extern PACKAGE TPropertyFilter* __fastcall RegisterPropertyInCategory(TMetaClass* ACategoryClass, const AnsiString APropertyName); extern PACKAGE TPropertyFilter* __fastcall RegisterPropertyInCategory(TMetaClass* ACategoryClass, TMetaClass* AComponentClass, const AnsiString APropertyName); extern PACKAGE TPropertyFilter* __fastcall RegisterPropertyInCategory(TMetaClass* ACategoryClass, Typinfo::PTypeInfo APropertyType, const AnsiString APropertyName); extern PACKAGE TPropertyFilter* __fastcall RegisterPropertyInCategory(TMetaClass* ACategoryClass, Typinfo::PTypeInfo APropertyType);
12 9721 CH10
11/13/00
9:44 AM
Page 691
Creating Property and Component Editors CHAPTER 10
691
Each of the previous functions uses a different method to specify which property filter you want to register in a given category. To register several property filters into a category at once, you can use one of the three overloaded versions of RegisterPropertiesInCategory: extern PACKAGE TPropertyCategory* __fastcall RegisterPropertiesInCategory(TMetaClass* ACategoryClass, const System::TVarRec* AFilters, const int AFilters_Size); extern PACKAGE TPropertyCategory* __fastcall RegisterPropertiesInCategory(TMetaClass* ACategoryClass, TMetaClass* AComponentClass, const AnsiString* AFilters, const int AFilters_Size); extern PACKAGE TPropertyCategory* __fastcall RegisterPropertiesInCategory(TMetaClass* ACategoryClass, Typinfo::PTypeInfo APropertyType, const AnsiString* AFilters, const int AFilters_Size);
We see that there are seven functions from which you can register property filters for a specific category: four to register a single property filter and three to register multiple property filters. Based on the input parameters, the RegisterPropertyInCategory() function generates a single TPropertyFilter, and the RegisterPropertiesInCategory() function generates multiple TPropertyFilters. The IDE then uses a list of TPropertyFilters to determine which properties belong in which categories. Remember that a property can belong to more than one category. The definition for TPropertyFilter, found in the $(BCB)\Include\Vcl\DsgnIntf.hpp file, is shown in Listing 10.40. LISTING 10.40
Definition of TPropertyFilter
class DELPHICLASS TPropertyFilter; class PASCALIMPLEMENTATION TPropertyFilter : public System::TObject { typedef System::TObject inherited; private: Masks::TMask* FMask; TMetaClass* FComponentClass; Typinfo::TTypeInfo* FPropertyType;
PROPERTY AND COMPONENT EDITORS
int FGroup;
10
12 9721 CH10
11/13/00
692
9:44 AM
Page 692
C++Builder 5 Essentials PART I
LISTING 10.40
Continued
public: __fastcall TPropertyFilter(const AnsiString APropertyName, TMetaClass* AComponentClass, Typinfo::PTypeInfo APropertyType); __fastcall virtual ~TPropertyFilter(void); bool __fastcall Match(const AnsiString APropertyName, TMetaClass* AComponentClass, Typinfo::PTypeInfo APropertyType); __property TMetaClass* ComponentClass = {read=FComponentClass}; __property Typinfo::PTypeInfo PropertyType = {read=FPropertyType}; };
Essentially, TPropertyFilter contains three important data members. These are shown in the following list: • Property Name Mask (FMask) If this is not an empty AnsiString, a TMask object (FMask) is created based on the AnsiString. This stores the property name mask that any property must match in order to satisfy this field of the filter. See Table 10.14 for a description of the special mask characters that can be used. Otherwise, any property name will do. • Component Class (FComponentClass) This is a TMetaClass* that indicates which component class a property must be part of to satisfy this field of the filter. If this is set to 0, any component class can be used. • Property Type (FPropertyType) This is a PTypeInfo (TTypeInfo*) that indicates what type the property must be to satisfy this field of the filter. If this is set to 0, the property type is irrelevant. Table 10.14 shows the special characters that the property name argument can contain to generate a suitable property name mask. TABLE 10.14
Special Mask Characters for the Property Name in a TPropertyFilter
Character
Purpose
*
Use as a wildcard character. Use the * character to signify any number of any character. Use as a wildcard character. Use the ? character to signify a single wildcard character.
?
12 9721 CH10
11/13/00
9:44 AM
Page 693
Creating Property and Component Editors CHAPTER 10
TABLE 10.14
693
Continued
Character
Purpose
[SetorRangeorBoth]
Use to specify a set or range (or both) of characters that a single character must match. For example, [AbcDE0-9] includes the characters AbcDE0123456789. Use to specify a set or range (or both) of characters that a single character must not match. For example, [!AbcDEf-j] excludes the characters AbcDEfghij.
[!SetorRangeorBoth]
A character is any alphanumeric character. The mask is case-sensitive. A set is a group of characters enclosed in brackets ([]). The characters are not separated by either commas or spaces—for example, [AbcDE]. A range is a range of characters; the first and last characters in a range are separated by a character. Ranges are enclosed in brackets ([]). For example, [0-9] includes all the characters 0123456789. Examples of using these registration functions are shown in Listing 10.40, earlier in this chapter. Table 10.15 lists the parameters to these functions and discusses the purpose of each. With the exception of ACategoryClass, all the parameters are used to specify the TPropertyFilter or TPropertyFilters relevant to a given property category, as defined by ACategoryClass. TABLE 10.15 Parameters to the RegisterPropertyInCategory()and RegisterPropertiesInCategory() Functions Parameter
Purpose
TMetaClass* ACategoryClass
Used to specify the category for which you want the property to be registered. Use the __classid operator to obtain the TMetaClass pointer for the given category class. Example: __classid(TMyCategory)
TMetaClass* AComponentClass
__classid(TMyComponent)
10 PROPERTY AND COMPONENT EDITORS
Used to generate the property filter. Use this parameter to specify a component to which the property must belong in order for it to be registered for the given category. Use the __classid operator to obtain the TMetaClass* for the required component class. Example:
12 9721 CH10
11/13/00
694
9:44 AM
Page 694
C++Builder 5 Essentials PART I
TABLE 10.15
Continued
Parameter
Purpose
const AnsiString PropertyName
Used to generate the property filter. Use this parameter to specify a property name mask that the property must match to be registered for the given category. For example, specify “Shape” to restrict the registration to those properties called Shape, or “OnMouse*” to restrict the registration to those properties (probably events) that begin OnMouse. Used to generate the property filter. Use this parameter to specify a PTypeInfo that the property must match in order to be registered for the given category. Effectively, this parameter allows you to restrict the properties to be registered to only those of a particular type. For example, you could restrict the registration to only those properties of type int by using the argument &IntTypeInfo, where IntTypeInfo is defined as
Typinfo::PTypeInfo APropertyType
static TTypeInfo IntTypeInfo; IntTypeInfo.Name = “int”; IntTypeInfo.Kind = tkInteger;
const AnsiString* AFilters, const int AFilters_Size
const System::TVarRec* AFilters, const int AFilters_Size
Refer to the “Registering Custom Property Editors” section for more details. Used to generate the property filters and to specify an array of property name masks (by value) that must be matched by a property to be registered for the given category. Use the OPENARRAY macro to achieve this. Used to generate the property filters and to specify an array of TVarRec values (by value) that must be matched by a property in order to be registered for the given category. Use the ARRAYOFCONST macro to achieve this. Values should be an AnsiString (to specify a property name mask), TMetaClass* (to specify a component class the property should belong to), or PTypeInfo (to specify a property type that the property must be).
12 9721 CH10
11/13/00
9:44 AM
Page 695
Creating Property and Component Editors CHAPTER 10
695
From Table 10.15 we can see that the RegisterPropertiesInCategory() function requires either a const array of TVarRec ($(BCB)\Include\Vcl\Systvar.h) by value or a const array of AnsiStrings by value. For a const array of TVarRec by value, the ARRAYOFCONST(values) macro is used ($(BCB)\Include\Vcl\Sysopen.h). The ARRAYOFCONST(values) macro equates to OpenArrayvalues, OpenArrayCountvalues.GetHigh()
This allows you to pass an array of TVarRecs by value to the registration functions and is why the array pointer parameter and the array size parameter are considered together. There is one problem with this, and that is that both OpenArray and OpenArrayCount are limited to 19 arguments. Therefore, if you need to register 20 or more properties or events to the same category, you must call the RegisterPropertiesInCategory() function more than once. Similarly, for a const array of AnsiStrings by value, the OPENARRAY(type,values) macro can be used, which equates to OpenArrayvalues, OpenArrayCountvalues.GetHigh()
For an OPENARRAY of AnsiStrings we write the following: OPENARRAY(AnsiString, “Value1”, “Value2”, “Value3”);
Three example AnsiStrings are shown. As with the ARRAYOFCONST macro, you are limited to 19 arguments or fewer. Table 10.16 shows the filters produced by each of the four overloaded versions of the RegisterPropertyInCategory() function. TABLE 10.16
TPropertyFilters Generated by the RegisterPropertyInCategory()
Function
Overloaded Function Parameter List
Property Filter(s) Generated: (Mask, Component, Type)
(TMetaClass* ACategoryClass, const AnsiString APropertyName)
(APropertyName, 0, 0)
(TMetaClass* ACategoryClass, TMetaClass* AComponentClass, const AnsiString APropertyName)
(APropertyName,
(TMetaClass* ACategoryClass, Typinfo::PTypeInfo APropertyType, const AnsiString APropertyName)
(APropertyName, 0, APropertyType)
(TMetaClass* ACategoryClass,
(“”, 0,
APropertyType)
10 PROPERTY AND COMPONENT EDITORS
Typinfo::PTypeInfo APropertyType)
AComponentClass, 0)
12 9721 CH10
11/13/00
696
9:44 AM
Page 696
C++Builder 5 Essentials PART I
Table 10.17 shows the filters produced by each of the three overloaded versions of the RegisterPropertiesInCategory() function. TABLE 10.17
TPropertyFilters Generated by the RegisterPropertiesInCategory()
Function
Overloaded Function Parameter List
Property Filter(s) Generated: (Mask, Component, Type)
(TMetaClass* ACategoryClass, const System::TVarRec* AFilters,
If AFilters[i] is an AnsiString: (AFilters[i], 0, 0) If AFilters[i] is a TMetaClass*: (“”, AFilters[i], 0) If AFilters[i] is a PTypeInfo: (“”, 0, AFilters[i]) (AFilters[i], AComponentClass, 0)
const int AFilters_Size)
(TMetaClass* ACategoryClass, TMetaClass* AComponentClass, const AnsiString* AFilters, const int AFilters_Size) (TMetaClass* ACategoryClass, Typinfo::PTypeInfo APropertyType, const AnsiString* AFilters, const int AFilters_Size)
(AFilters[i],
0, APropertyType)
Tables 10.16 and 10.17 should be used as a guide when choosing which of the seven overloaded versions of the two registration functions to use when registering property filters. The property filters are described in terms of a property name mask (Mask), a component class that the property must belong to (Component), and a type the property must be (Type). Of special note in Table 10.17 is the first RegisterPropertiesInCategory() overloaded function. The property filters produced from this depend on the argument types used in the TVarRec array. Note also that different types can be used in the same array, making this function very flexible. Listing 10.41 shows sample code using these registration functions. The code in Listing 10.41 assumes that the categories used have been previously defined and are available. LISTING 10.41
Registering Properties in Categories in C++Builder
namespace Nameoffilecontainingthisregistration { void __fastcall PACKAGE Register() { // 1 - Register a single property filter for TMouseCategory. // The filter is (“OnMouse*”, 0, 0), i.e. all properties
12 9721 CH10
11/13/00
9:44 AM
Page 697
Creating Property and Component Editors CHAPTER 10
LISTING 10.41
697
Continued
// (probably events) whose names begin “OnMouse” // Use: // RegisterPropertyInCategory(TMetaClass* ACategoryClass, // const AnsiString APropertyName); RegisterPropertyInCategory(__classid(TMouseCategory), “OnMouse*”); // 2 - Register two property filters for TMouseCategory. // The first filter is (“”, 0, CursoTypeInfo), i.e. for properties // of type TCursor. // The second filter is (“OnMouse*”, 0, 0) // Use: // RegisterPropertiesInCategory(TMetaClass* ACategoryClass, // const System::TVarRec* AFilters, // const int AFilters_Size); PTypeInfo CursorTypeInfo = *Typinfo::GetPropInfo(__typeinfo(TForm),”Cursor”)->PropType; RegisterPropertiesInCategory(__classid(TMouseCategory), ARRAYOFCONST( ( CursorTypeInfo, AnsiString(“OnMouse*”), AnsiString(“EventName2”) )) ); // 3 // // // // // Use // // // //
Register two property filters for TMouseCategory. The first filter is (“OnClick”, 0, 0), i.e. for any property (probably event) whose name is “OnClick”. The second filter is (“OnDblClick”, 0, 0), i.e. for any property (probably event) whose name is “OnDblClick”. : RegisterPropertiesInCategory(TMetaClass* ACategoryClass, TMetaClass* AComponentClass, const AnsiString* AFilters, const int AFilters_Size)
TMetaClass* AnyComponent = 0;
} }
10 PROPERTY AND COMPONENT EDITORS
RegisterPropertiesInCategory( __classid(TMouseCategory), AnyComponent, OPENARRAY( AnsiString, (“OnClick”, ”OnDblClick”) ) );
12 9721 CH10
11/13/00
698
9:44 AM
Page 698
C++Builder 5 Essentials PART I
In Listing 10.41 not all possible uses and overloaded versions of the two category registration functions are shown. However, each of the possible parameters’ uses is illustrated, so the correct method of calling each registration function should be easy to determine. Of all the parameters, AComponentClass is the least useful on its own, because the sub-properties of a class are unlikely to all belong in the same category unless the class is itself a property, in which case the sub-properties will fall under the same category as the class that contains them, by default. Note that a NULL TMetaClass* is passed as an argument to the AComponentClass parameter in the third registration shown. This indicates that only the property name masks are part of the property filter. You cannot simply pass 0 because there will be an ambiguity over which version of RegisterPropertiesInCategory() you are calling. For example, a literal 0 could equally be a value for a PTypeInfo parameter. A similar technique can be used to pass a NULL PTypeInfo: PTypeInfo AnyPropertyType = 0;
Summary This chapter’s aim was to cover the main concerns and techniques associated with the development of a component’s designtime interface. To do this, a lot has been covered. The following highlights the main topics. • The creation of custom property editors was discussed throughout the chapter. We saw that there are many different property editor classes already defined by the VCL and that correctly choosing the base class for our custom property editors can save a lot of unnecessary coding. Much of the information presented dealt with how to correctly override TPropertyEditor’s virtual (and DYNAMIC) methods. We paid particular attention to the new methods introduced in C++Builder 5, such as the five DYNAMIC methods used for rendering images in the Object Inspector. We also touched on the issue of throwing exceptions to indicate to the user when an invalid property value has been entered. • A large portion of the chapter was devoted to the creation of custom component editors. We saw that, in contrast to creating custom property editors, there are only two classes from which we can derive our custom component editors: TComponentEditor and TDefaultEditor. Which one we choose depends on which double-click behavior we want: If we want our custom component editor to generate an event handler, we derive from TDefaultEditor; otherwise we use TComponentEditor. We also discussed how to correctly override TComponentEditor’s virtual methods. We paid particular attention to the PrepareItem() method, new to C++Builder 5, which allows users to fully customize the context menu that our custom component editor displays.
12 9721 CH10
11/13/00
9:44 AM
Page 699
Creating Property and Component Editors CHAPTER 10
699
• For both property and component editors we saw that the Edit() virtual method can be used to display a form for editing a property or component. We saw that there were two approaches to this: Allow the form to modify the property or component while the form is being displayed or modify the property or component only after the form has returned. • The use of image resources to improve the interface of an editor was discussed. Definite guidelines were given for this often-neglected topic. The material for this topic should prove useful for C++Builder projects in general. • The correct techniques for registering property and component editors were detailed. In particular, the problem of determining type information for non-VCL types was examined and a robust solution presented. • Property categories, new to C++Builder 5, were examined. We saw how to create our own custom proerty categories and also how to register property filters, which allow the IDE to determine which categories a property should appear in. After reading this chapter, you should be able to tackle most of these issues. In particular, if time has been spent in some of the darker corners of the code, you should have developed a reasonable appreciation of how type information is dealt with by the VCL. Creating property and component editors can be tricky, and many of the methods required can easily be misunderstood. Hopefully, much of the mystery that sometimes surrounds these topics has been removed.
10 PROPERTY AND COMPONENT EDITORS
12 9721 CH10
11/13/00
9:44 AM
Page 700
13 9721 CH11
11/13/00
9:52 AM
Page 701
More Custom Component Techniques Jamie Allsop Damon Chandler Malcolm Smith
IN THIS CHAPTER • Miscellaneous Considerations for Custom Components • Frames • Component Distribution and Related Issues
CHAPTER
11
13 9721 CH11
11/13/00
702
9:52 AM
Page 702
C++Builder 5 Essentials PART I
This is the last of the chapters discussing component writing, and it is not as general as Chapter 9, “Creating Custom Components,” or as focused as Chapter 10, “Creating Property and Component Editors.” This chapter brings together a variety of topics related to component creation. It begins by drawing attention to a few of the more commonly encountered problems component writers face. Topics such as using callback functions in components to using namespaces in event parameter lists are covered. Frames are also introduced. Frames are an important addition to C++Builder, and it is likely that they will prove to be a very useful tool. The information in this chapter should help you come to grips with the different facets of Frames. The last section of this chapter concerns itself with the distribution of components. This sometimes tricky task is discussed at length and several of the main issues associated with component distribution highlighted. The subject is broken down into several sections, each focusing on a particular topic. Those who are involved in distributing their components will find this section a good starting point for information and guidelines.
Miscellaneous Considerations for Custom Components This section deals with a variety of common and not-so-common problems encountered when developing custom components. Some of the topics covered are advanced and some are basic; however, it is likely that most current component writers have encountered at least one of the situations described, and future writers are likely to encounter them as well. By bringing these considerations together in one place, it is hoped that time will not be wasted on them in the future.
Displaying a Class Property’s Published Properties in the Object Inspector If a component’s property is a class and we want to be able to expand a list of its published properties, we should derive the class from TPersistent. If this is done, the TClassProperty property editor class will be used to edit the class property, allowing a list of the class’s published properties to be expanded and edited as required. For such a TPersistent derived property to be used in a component definition, the compiler must know that the class is a VCL style class. This means that if a forward declaration is used for the class, the DELPHICLASS macro (which expands to __declspec(delphiclass,package)) must be used to tell the compiler the class is a VCL class; otherwise, an error will result. For example class DELPHICLASS TPersitentDerivedClass; // Forward declaration
If the compiler finds the class definition before it is used as a property, this is not required. See the “VCL Overview” section in Chapter 8, “Using VCL Components,” for more details and an
13 9721 CH11
11/13/00
9:52 AM
Page 703
More Custom Component Techniques CHAPTER 11
Using Namespaces in Event Parameter Lists It is important to note that it is not possible to directly use namespace modifiers in event parameter lists. This can result in events that are effectively useless. For example, consider the following event declaration: typedef void __fastcall (__closure *TNotifyFrameAvailable)(System::TObject* Sender, Graphics::TBitmap* Frame);
The event declared above is to be fired when a new image frame is available for a particular component, for example an image capture component. In order for the event to be used, we require a variable to hold the event’s value, a property of the event type (normally __published), and a function to fire the event (normally protected). // Variable to hold the event value (in the private section) private: TNotifyFrameAvailable FOnFrameAvailable; // Property of the event type (in the __published section) public: __published: __property TNotifyFrameAvailable OnFrameAvailable = {read=FOnFrameAvailable, write=FOnFrameAvailable}; // Function to fire the event in the implementation file void __fastcall ComponentName::FrameAvailable(Graphics::TBitmap* Frame) { if(FOnFrameAvailable != 0) FOnFrameAvaliable(this, Frame); }
Everything looks fine—it may not be the best approach, but from a code point of view it is reasonable. However, the code cannot be used as it stands. If a component using this code is installed in the IDE and the OnFrameAvailable event double-clicked, the IDE will generate the following event handler (for a form called Form1 and a component called Component1): void __fastcall TForm1::Component1OnFrameAvailable(TObject* Sender, TBitmap* Frame) { // Event Handler code here }
11 MORE CUSTOM COMPONENT TECHNIQUES
example. In general, any forward declaration of a VCL style class should have the DELPHICLASS macro as part of its declaration. TFont is an example of a TPersistent derived class property.
703
13 9721 CH11
11/13/00
704
9:52 AM
Page 704
C++Builder 5 Essentials PART I
The problem should now be obvious. The namespace modifiers are now missing from the event’s parameter list. In the case of the TObject* Sender parameter, this is not a problem. TObject is unambiguous and this code will compile. However, when we come to TBitmap, we have a big problem. TBitmap is ambiguous, and this code will produce a compiler error. TBitmap could be either Graphics::TBitmap, as we want, or Windows::TBitmap, which we don’t want. Modifying the parameter list is not possible because this will result in an incompatible parameter list. The solution is to avoid the use of namespaces in event parameter lists. However, a workaround to this problem exists. Instead of using Graphics::TBitmap as the type for the pointer, we use a typedef of the type, as in the following: typedef Graphics::TBitmap GraphicsTBitmap;
The event declaration then becomes typedef void __fastcall (__closure *TNotifyFrameAvailable)(System::TObject* Sender, GraphicsTBitmap* Frame);
If we assume that the function to fire the event is similarly modified, the handler now generated by the IDE is void __fastcall TForm1::Component1OnFrameAvailable(TObject* Sender, GraphicsTBitmap* Frame) { // Event Handler code here } TObject is still stripped of its System:: modifier, which does not matter. However, TBitmap’s namespace modifier is maintained by the typedef. This code will successfully compile. A consideration of this approach is that the typedef must be made available to the component user, so it must be global. Therefore, it is important to choose the typedef name carefully. Simply removing the :: gives an unambiguous name whose meaning is clear and is unlikely to clash with other names.
Considerations when Determining an Event’s Parameter List Events are effectively pointers to member functions. As such, a suitable parameter list for the event must be specified. Normally, this is done using a typedef, for example typedef void __fastcall (__closure *TNotifyIsDataTransmitted)(System::TObject* Sender, bool DataTransmitted);
The parameter list for this declared event is (System::TObject* Sender, bool DataTransmitted)
13 9721 CH11
11/13/00
9:52 AM
Page 705
More Custom Component Techniques CHAPTER 11
Remember that the actual parameter list, as far as the compiler is concerned, is
The parameter names Sender and DataTransmitted are ignored. In fact, it is not necessary to have names here at all. However, using names makes the parameter list more descriptive, which is why they are always used in event and function declarations. As a result, the following parameter list (System::TObject* Sender, bool DataReceived)
is considered by the compiler to be the same as the previous one. Therefore, from the compiler’s point of view, the following event declaration has the same parameter list as the previous one: typedef void __fastcall (__closure *TNotifyIsDataReceived)(System::TObject* Sender, bool DataReceived);
A problem arises when the IDE is used to generate an event handler for a component that contains such events. It uses the parameter names for the first public event property that occurs in the component definition. If, for example, two events are declared, as in the following code, the parameter names used by the IDE will be Sender and DataReceived, respectively. // Declared Globally typedef void __fastcall (__closure *TNotifyIsDataTransmitted)(System::TObject* Sender, bool DataTransmitted); typedef void __fastcall (__closure *TNotifyIsDataReceived)(System::TObject* Sender, bool DataReceived); // In the class definition private: TNotifyIsDataTransmitted FOnIsDataTransmitted; TNotifyIsDataReceived FOnIsDataReceived; public: __published: __property TNotifyIsDataReceived OnIsDataReceived = {read=FOnIsDataReceived, write=FOnIsDataReceived}; __property TNotifyIsDataTransmitted OnIsDataTransmitted = {read=FOnIsDataTransmitted, write=FOnIsDataTransmitted};
11 MORE CUSTOM COMPONENT TECHNIQUES
(System::TObject* , bool )
705
13 9721 CH11
11/13/00
706
9:52 AM
Page 706
C++Builder 5 Essentials PART I
Hence, for a component Component1 on a form Form1, the event handler generated for the OnIsDataTransmitted event is void __fastcall TForm1::Component1OnIsDataTransmittedEvent(TObject* Sender, bool DataReceived) { // Event Handler code here }
Swapping the order of the two property declarations will result in the IDE using Sender and DataTransmitted as parameter names. This is a problem to which there is more than one solution, each with its own advantages and disadvantages. One solution is to introduce dummy parameters, normally done using an empty class definition: class NotUsed1{}; class NotUsed2{};
Using these empty classes in the event declarations results in the following: typedef void __fastcall (__closure *TNotifyIsDataTransmitted)(System::TObject* Sender, bool DataTransmitted, NotUsed1 Dummy); typedef void __fastcall (__closure *TNotifyIsDataReceived)(System::TObject* Sender, bool DataReceived, NotUsed2 Dummy);
When firing the event, the following is required: if(FOnIsDataTransmitted != NULL) FOnIsDataTransmitted (this, true, NotUsed1()); if(FOnIsDataReceived != NULL) FOnIsDataReceived (this, true, NotUsed2());
Simply construct the required empty class in place of the call to the event handler. Of course, only one of the preceding event declarations requires an empty class parameter for the two to be distinguished. The additional parameter has been used for both for consistency. The empty class definitions can be reused in other event declarations whose parameter list types match, so it is unlikely that you will need to define too many of them. You can even make them globally available by placing them in a separate file for inclusion when required. An advantage of this technique is that it solves the problem at the source, leaving nothing for the component user to do. Just be sure it is clear to the user that the parameters serve no purpose other than to allow the IDE to display the proper parameter names. A second solution is to use a single event declaration for both events, but add an enum type to the parameter list. This parameter can then be used to indicate whether the event is a receiving
13 9721 CH11
11/13/00
9:52 AM
Page 707
More Custom Component Techniques CHAPTER 11
event or a transmitting event. For example, we can declare the following enumeration to indicate whether we are receiving data or we are transmitting data:
Only a single event declaration is required: typedef void __fastcall (__closure *TDataCommEvent)(System::TObject* Sender, TDataCommMode CommMode);
An event would then appear in the class definition as private: TDataCommEvent FOnDataCommEvent; public: __published: __property TDataCommEvent OnDataCommEvent = {read=FOnDataCommEvent, write=FOnDataCommEvent};
Two methods are used to fire the event: one when we are receiving data and one when we are transmitting data. Suitable code for the two methods is as follows: void __fastcall ComponentName::DataReceivied() { if(FOnDataCommEvent != 0) FOnDataCommEvent (this, dcmDataReceived); } void __fastcall ComponentName::DataTransmitted() { if(FOnDataCommEvent != 0) FOnDataCommEvent (this, dcmDataTransmitted); }
The user can then use the CommMode parameter in the event handler to determine which process has occurred. Another solution is to create a more generic event that would have proper meaning for either event. A possible alternative to the previous two declarations would be the following single declaration: typedef void __fastcall (__closure *TNotifySendReceiveData)(System::TObject* Sender, bool Succeeded);
The events would then appear in the class definition as private: TNotifySendReceiveData FOnIsDataTransmitted; TNotifySendReceiveData FOnIsDataReceived; public: __published:
11 MORE CUSTOM COMPONENT TECHNIQUES
enum TDataCommMode { dcmDataReceived, dcmDataTransmitted };
707
13 9721 CH11
11/13/00
708
9:52 AM
Page 708
C++Builder 5 Essentials PART I __property TNotifySendReceiveData OnIsDataReceived = {read=FOnIsDataReceived, write=FOnIsDataReceived}; __property TNotifySendReceiveData OnIsDataTransmitted = {read=FOnIsDataTransmitted, write=FOnIsDataTransmitted};
An advantage of either of the these two methods is that the number of event declarations required is minimized. However, it may not always be possible to use either of the previous two methods, so it is important to be aware of the first approach of using dummy class parameters. If you are using a third-party component that exhibits this problem of different events with the same parameter list and you do not have access to the source of the component, it is possible to simply change the parameter names manually. Sometimes you may find that this improves the readability of the code. Changing the name manually is neither very maintainable nor very practical, but it does work. As long as some name is present, the code will compile unaffected by the change. For example, consider adding the following three components to a form: TEdit, TUpDown, and TLabel (default names Edit1, UpDown1, and Label1, respectively). Associate UpDown1 with Edit1. Double-click the OnChangeEx event of UpDown1. The IDE generates the following event declaration in the form’s class definition and the empty event handler in the form’s implementation file: // In the class definition void __fastcall UpDown1ChangingEx(TObject *Sender, bool &AllowChange, short NewValue, TUpDownDirection Direction); // In the implementation file void __fastcall TForm1::UpDown1ChangingEx(TObject *Sender, bool &AllowChange, short NewValue, TUpDownDirection Direction) { }
Place the following code in the event handler: void __fastcall TForm1::UpDown1ChangingEx(TObject *Sender, bool &AllowChange, short NewValue, TUpDownDirection Direction) { Label1->Caption = NewValue; // Automatically converted to an AnsiString }
Compile and run the code. When UpDown1 is clicked, the value in Label1 changes to reflect the new value. Now change the parameter name of NewValue in the declaration to Pandas and to Elephants in the implementation as follows:
13 9721 CH11
11/13/00
9:52 AM
Page 709
More Custom Component Techniques CHAPTER 11
// In the implementation file void __fastcall TForm1::UpDown1ChangingEx(TObject *Sender, bool &AllowChange, short Elephants, TUpDownDirection Direction) { Label1->Caption = Elephants; // Automatically converted to an AnsiString }
Make sure that the code inside the event handler is also changed (NewValue to Elephants). Compile and run. Everything works as before. The compiler does not care what names you use for your parameters, but you must have some name for the parameters in the event handler declaration; you cannot simply delete the names. If you do, your code will get an Incomplete method declaration in class class error. You can, of course, comment out or delete those names that are not required in the implementation if you want.
Overriding DYNAMIC Functions The DYNAMIC macro expands to __declspec(dynamic) and is used to indicate functions that are effectively the same as virtual functions. For information on the exact differences, please see the “VCL Overview” section in Chapter 8. Basically, the main point to remember is that a DYNAMIC function cannot be overridden by a virtual function and vice versa. Also, each time a DYNAMIC function is re-declared in a child class, the re-declaration must include the DYNAMIC keyword. Those who have used older versions of C++Builder may be caught out by this, because several functions are now declared as DYNAMIC. If you are overriding an existing VCL component, it is prudent to check the header file for the component to see which of the methods you are overriding are DYNAMIC methods. For example, the KeyPress event function of TEdit is DYNAMIC, not virtual (as is incorrectly stated in the Developer’s Guide manual that ships with C++Builder). To illustrate this, the source for a simple TFilterEdit component will be shown. The component definition is shown in Listing 11.1 and the implementation in Listing 11.2. LISTING 11.1
Definition for the TFilterEdit Component
class PACKAGE TFilterEdit : public TEdit { private: AnsiString FFilter; bool FExcludeFilter;
11 MORE CUSTOM COMPONENT TECHNIQUES
// In the class definition void __fastcall UpDown1ChangingEx(TObject *Sender, bool &AllowChange, short Pandas, TUpDownDirection Direction);
709
13 9721 CH11
11/13/00
710
9:52 AM
Page 710
C++Builder 5 Essentials PART I
LISTING 11.1
Continued
protected: DYNAMIC void __fastcall KeyPress(char &Key); public: __fastcall TFilterEdit(TComponent* Owner); __published: __property AnsiString Filter = {read=FFilterString, write=FFilterString}; __property bool ExcludeFilter = {read=FExcludeFilter, write=FExcludeFilter, default=false}; };
LISTING 11.2
Implementation for the TFilterEdit Component
//-----------------------------------------------------------// __fastcall TFilterEdit::TFilterEdit (TComponent* Owner) : TEdit(Owner), FExcludeFilter(false) { } //-----------------------------------------------------------// void __fastcall TFilterEdit::KeyPress(char &Key) { // Check that Key is not Backspace if(Key != ‘\b’) { // See if Key is in the Filter for(int i=1; i<=FFilter.Length(); ++i) { if(Key == FFilter[i]) { //Key is in the Filter if(ExcludeFilter) { Key = 0; return; } else return; // Do nothing } }//end for // Key is not in the Filter
13 9721 CH11
11/13/00
9:52 AM
Page 711
More Custom Component Techniques CHAPTER 11
LISTING 11.2
Continued
} } //-----------------------------------------------------------//
Notice the declaration of the KeyPress method with the DYNAMIC macro. The TFilterEdit component shown basically allows an AnsiString of characters to be input as a filter. This filter is used to either exclude the characters present in the filter (ExcludeFilter = true) or allow only those characters present in the filter (ExcludeFilter = false), the default. This component is contained on the CD-ROM that accompanies this book, as part of the NewAdditionalComponents package that accompanies Chapter 10. The introduction to Chapter 10 details how to install this package.
Handling Messages in Custom Components It is highly probable that at some stage you will write a custom component that requires you to handle messages. We have already seen several examples of this in Chapter 9. In many cases the code is simple and easy to implement. This section presents an overview of the situations you might encounter and suitable techniques to deal with them. It can be used as a reference when dealing with message handling in custom components. The simplest case to consider is the handling of messages in visual components, those that are derived ultimately from TControl, though more probably from TGraphicControl or TWinControl. Handling messages in non-visual components, those that derive directly from TComponent, is a little more complex, and this is also covered.
NOTE You can create your own user-defined messages that can be sent to your components. Simply use a #define statement for the message, giving it a value of WM_USER and some constant value. WM_USER ensures that your message is in the correct range for user-defined messages and does not conflict with any system messages. The value of WM_USER + Constant should not be greater that 0x7FFF. It is also sensible to precede your user-defined message names with some obvious prefix, such as UM for User Message. For example, you could define a user message as #define UM_MYMESSAGE (WM_USER + 1);
The number you add is arbitrary. Just be sure you haven’t defined another message with the same number.
11 MORE CUSTOM COMPONENT TECHNIQUES
if(!ExcludeFilter) Key = 0;
711
13 9721 CH11
11/13/00
712
9:52 AM
Page 712
C++Builder 5 Essentials PART I
Message handling in visual components can be carried out either by using a message map or by overriding the component’s WndProc() method. Using a message map will be considered first.
Using a Message Map to Handle Messages in Visual Components To use a message map to handle messages you must do two things: 1. Add a message map to your component’s class definition, normally in the protected area, with a VCL_MESSAGE_HANDLER entry for each message you want to handle. 2. Write a message handler method for each message. The message handler method should return void and take a reference to the message structure used for the message as its single parameter. Writing a message map is simple. Start your message map with the macro BEGIN_MESSAGE_MAP
Follow this macro with one or more VCL_MESSAGE_MAP macros, one for each message you wish to handle. The macro is of the form VCL_MESSAGE_MAP(msg,type,meth)
You must pass appropriate values for msg, type, and meth. msg expects the actual message you want to handle, type expects the structure used by the VCL to represent this message, and meth expects the name of the message handler method that will be used to handle this message.
NOTE VCL_MESSAGE_HANDLER is used instead of MESSAGE_HANDLER. This is because the Microsoft Active Template Library (ATL) has defined a MESSAGE_HANDLER macro. If you are not using ATL, MESSAGE_HANDLER is defined as VCL_MESSAGE_HANDLER, so you can use the slightly shorter macro if you want.
For example, if you want to handle the WM_CHAR message in a method called WMChar(), the VCL_MESSAGE_MAP entry in the message map would be VCL_MESSAGE_MAP(WM_CHAR, TWMChar, WMChar)
is created by adding a T to the message name, removing the underscore, and leaving only the start of each word in uppercase. To verify that this is the correct structure to represent this message, check the $(BCB)\Include\Vcl\Messages.hpp file, where message structures used by the VCL are declared. If there is not a message structure declared specifically for the message you want to handle, then use the TMessage structure for this parameter.
TWMChar
13 9721 CH11
11/13/00
9:52 AM
Page 713
More Custom Component Techniques CHAPTER 11
TIP struct TMessage { unsigned Msg; union { struct { Word WParamLo; Word WParamHi; Word LParamLo; Word LParamHi; Word ResultLo; Word ResultHi; }; struct { int WParam; int LParam; int Result; }; }; };
Compare this with the more specific TWMKey structure for key related messages: struct TWMKey { unsigned Msg; Word CharCode; Word Unused; int KeyData; int Result; };
Obviously this is much clearer and easier to use than the TMessage structure. If you need to handle a message for which there is not a specific message structure already defined, you can define your own. The structure should be 16 bytes long and should have a format similar to one of the following: struct TMyMessageStructure { unsigned Msg; // Msg parameter Word MessageData1; // WParam parameter
11 MORE CUSTOM COMPONENT TECHNIQUES
The TMessage structure is defined in $(BCB)\Include\Vcl\Messages.hpp as
713
13 9721 CH11
11/13/00
714
9:52 AM
Page 714
C++Builder 5 Essentials PART I
Word Unused; int MessageData2; int Result;
// WParam parameter // LParam parameter // Result parameter
}; // or struct TMyMessageStructure { unsigned Msg; // Msg int MessageData1; // WParam int MessageData2; // LParam int Result; // Result };
parameter parameter parameter parameter
The first 4 bytes should be the Msg field and the last 4 bytes the Result field. How the 8 bytes for the WParam and LParam data is arranged depends on what is most suitable for the message. For examples, refer to $(BCB)\Include\Vcl\Messages.hpp.
Finally, add the END_MESSAGE_MAP(base) macro as the last line of the message map, where base is the VCL class from which your component inherits. For example, if your component inherits from TwinControl, this line would be END_MESSAGE_MAP(TWinControl)
How do the macros work? They expand to override the component’s Dispatch() method, as shown in Listing 11.3. The actual macro definitions can be found in $(BCB)\Include\Vcl\Sysmac.h. LISTING 11.3
The Dispatch() Method from Expanded Message Map Handlers
//BEGIN_MESSAGE_MAP expands to: virtual void __fastcall Dispatch(void* Message) { switch(((TMessage*)Message)->Msg) { //VCL_MESSAGE_HANDLER(msg,type,meth) expands to: case msg : meth(*((type*)Message)); break; //END_MESSAGE_MAP(base) expands to: default: base::Dispatch(Message); break; } }
13 9721 CH11
11/13/00
9:52 AM
Page 715
More Custom Component Techniques CHAPTER 11
Using our example of a message map to respond to the WM_CHAR message in a component derived from TWinControl with a message handling method called WMChar(), the message map and resultant macro expansion are shown in Listings 11.4 and 11.5, respectively. LISTING 11.4
Message Map for WM_CHAR
BEGIN_MESSAGE_MAP VCL_MESSAGE_HANDLER(WM_CHAR,TWMChar,WMChar) END_MESSAGE_MAP(TWinControl)
LISTING 11.5
Expanded Dispatch() Method for WM_CHAR
virtual void __fastcall Dispatch(void* Message) { switch(((TMessage*)Message)->Msg) { case msg : WMChar(*((TWMChar*)Message)); break; default : TWinControl::Dispatch(Message); break; } }
Using the message map macros greatly simplifies the process of overriding the Dispatch() method, at the cost of hiding how message handling actually works. Once you are aware of what is going on, the macros save time and minimize errors. Now that the Dispatch() method has been overridden, we turn our attention to the implementation of the message-handling method for the message we want to handle. As was stated at the beginning of this section, this method should return void and have as its single parameter a reference to the message structure used for the message we want to handle. In the most general case, where the message structure used is TMessage, the declaration for the handler method would be of the form void __fastcall MessageHandlerName(TMessage& Message);
In our example of using TWMChar, the declaration for a suitable function is void __fastcall WMChar(TWMChar& Message);
11 MORE CUSTOM COMPONENT TECHNIQUES
We override the Dispatch()method because it is called by default at the end of the component’s WndProc() method. It is the WndProc() method that actually receives the messages. The WndProc() method is looked at in more detail in the next section, “Overriding WndProc() to Handle Messages in Visual Components.”
715
13 9721 CH11
11/13/00
716
9:52 AM
Page 716
C++Builder 5 Essentials PART I
NOTE You may want to precede the declaration of your message handler function with the MESSAGE macro. Although this expands to nothing when the code is compiled, it is helpful to make your message handlers more visible in your class definition. For example, the following is obviously a message handler: MESSAGE void __fastcall MessageHandlerName(TMessage& Message);.
How the message handler is implemented depends on how your component should respond to the message and what kind of message you are trying to handle. General considerations are • If the message being handled serves as a notification for more than one condition (the condition being determined by examining one of the parameters of the message), the Dispatch() method of the component’s base class should be called if the message does not contain the required condition. • If the handler for the message performs only extra processing associated with the message, then the Dispatch() method of the component’s base class should be called. • If the handler for the message performs all the necessary processing associated with the message, including any processing carried out by base classes, then the Dispatch() method of the component’s base class does not need to be called. • If you specifically do not want any processing to be carried out in response to the message apart from that contained in the handler, then do not call the Dispatch() method of the base class. Calling the Dispatch() method of the component’s base class ensures that any default message handling is carried out. In most cases you will call this method after you have carried out the processing required to handle a given message. If you don’t call the Dispatch() method of the base class, you will probably have to set the Result field of the message structure passed to the handler method (often a TMessage structure). What value you should use depends on the message you handle and how you handle it. Refer to the Win32 SDK help files for details on the messages you are using. A typical implementation for a message handler method would be of this form: void __fastcall TCustomComponent::MessageHandler(TMessage& Message) { if(Message.SomeField == SomeValue) { // Process Message // Message.Result = ? }
13 9721 CH11
11/13/00
9:52 AM
Page 717
More Custom Component Techniques CHAPTER 11
}
Using message maps is a straightforward way of handling messages in visual components. For several examples of using messages in components, refer to the “Writing Visual Components” section of Chapter 9.
Overriding WndProc() to Handle Messages in Visual Components The WndProc() method receives the messages sent to a component. The Dispatch() method is called at the end of the WndProc() method to dispatch any message received to the appropriate handler. To handle messages in a component, you can simply override this method and then detect the messages you are interested in. Be sure to call the WndProc() method of the base class if a message received is not one you want to handle, or if there is some default handling that should occur for a message. This ensures that Dispatch() will be called. The general form of the overridden WndProc() method would be as follows: void __fastcall TCustomComponent::WndProc(TMessage& Message) { if(Message.Msg == SomeMessage) { // Process Message // Message.Result = ? } else { BaseClass::WndProc(Message); } }
Remember to check the details of the message you handle to see if you need to set the Result field of Message to a certain value. It is important to call the base class’s WndProc() method because it ultimately calls Dispatch(), ensuring that other messages are handled correctly. It also ensures that messages are handled properly when the component is being manipulated at designtime. When you override WndProc(), you get a chance to respond to messages before any other message handling is carried out. This means it is possible to ignore messages as well.
Using AllocateHWnd() and DeallocateHWnd() to Handle Messages in Non-Visual Components Non-visual components (those that derive from TComponent) do not have a window and do not receive messages. However, it is common to require non-visual components to respond to messages. This is often the case when creating components that wrap parts of the Windows API. If
11 MORE CUSTOM COMPONENT TECHNIQUES
else { BaseClass::Dispatch(Message); }
717
13 9721 CH11
11/13/00
718
9:52 AM
Page 718
C++Builder 5 Essentials PART I
the component itself needs to receive messages, you can use the AllocateHWnd() and DeallocateHWnd() functions to create and destroy a hidden window for use by the component. AllocateHWnd() and DeallocateHWnd() are declared in $(BCB)\Include\Vcl\Forms.hpp as HWND __fastcall AllocateHWnd(Controls::TWndMethod Method); void __fastcall DeallocateHWnd(HWND Wnd);
When you call AllocateHWnd(), you pass the name of the window procedure (WndProc() method) that you want to receive the messages. This will be of the form void __fastcall WindowProcedureName(TMessage& Message);
This is because of the declaration for TWndMethod, which is a closure. The declaration can be found in $(BCB)\Include\Vcl\Controls.hpp and is repeated at the end of the next section, “Using Non-Visual Components to Respond to Messages Sent to Other Components.” This has the same form as the WndProc() method in TControl descendants. AllocateHWnd() then returns a handle to the window created. To use AllocateHWnd() to receive messages in a nonvisual component, you must do four things: 1. Add a window procedure function to your component to receive messages to the window created by AllocateHWnd(). The function should be declared as void __fastcall WndProc(TMessage& Message);. 2. Add a private HWND data member called FWindowHandle to store the window handle returned by ALlocateHWnd(). 3. Call AllocateHWnd() in the component’s constructor to allocate a window, passing WndProc as the single argument. 4. Call DeallocateHWnd() in the component’s destructor, passing FWindowHandle as the single argument. Perform your message handling in the implementation of your WndProc() method. In summary, you must add the following lines to your component’s class definition: // In the private section HWND FWindowHandle; // In the protected section void __fastcall WndProc(TMessage& Message);
In the implementation, you must add the following line to the component’s constructor: // In the CONSTRUCTOR FWindowHandle = AllocateHWnd(WndProc);
Next, add the following line to the component’s destructor: // In the DESTRUCTOR DeallocateHWnd(FWindowHandle);
13 9721 CH11
11/13/00
9:52 AM
Page 719
More Custom Component Techniques CHAPTER 11
And finally, implement the WndProc() method:
Dispatch(&Message); // Dispatch others }
For messages that you don’t want to handle, you can call Dispatch().
Using Non-Visual Components to Respond to Messages Sent to Other Components Sometimes non-visual components are needed to monitor messages sent to other windowed controls (TWinControl descendants) such as a button or a form. When this is the case, it is necessary to replace the control’s window procedure with one supplied by your component. This means that messages destined for the control will arrive instead at your component. The catch is that you normally need to check for only one or two messages and require that the control handle all other messages. You must make sure unhandled messages are sent to the original control for handling. Subclassing is a technique in which you call your own window procedure to perform custom message handling in place of that of another window. Then you call the window’s original window procedure to perform default message handling. Two approaches to this will be shown. The first requires the use of the Windows API, and the second uses only the VCL. The Windows API approach is more complex than using the VCL, but it enables the process to be understood better. There are two Windows API functions that help you subclass a control’s window procedure. The first, SetWindowLong(), allows you to specify a new window procedure for a window, returning the address of the previous window procedure for the window. The second, CallWindowProc(), allows you to send a method to a specific window procedure at a specific address. More details on both functions are given at the end of this section, and you can refer to the Win32 SDK help files. To use these two functions to send a control’s messages to a non-visual component, you must do three things: 1. Declare a window procedure to receive the control’s messages. Use the declaration void __fastcall WndProc(TMessage& Message);. 2. Declare a variable to hold the pointer to the control’s window procedure so that you can restore it when your component is destroyed: void* PreviousControlWndProc;.
11 MORE CUSTOM COMPONENT TECHNIQUES
void __fastcall TCustomComponent::WndProc(TMessage& Message); { //******************// // Process messages // //******************//
719
13 9721 CH11
11/13/00
720
9:52 AM
Page 720
C++Builder 5 Essentials PART I
3. Convert the address of WndProc() to a pointer that the SetWindowLong() function can accept. You do this by calling the MakeObjectInstance() function. This function is declared in $(BCB)\Include\Vcl\Forms.hpp. You pass the function name as its single argument, and it returns a void* that can be used in the call to SetWindowLong(). We therefore need another variable to store this value, such as void* NewComponentWndProc;. Now you almost have all you need to receive messages for another control. The last thing is to decide what control to receive messages for. This depends on the component you are making. For example, if you were making a tray icon component, you would want to monitor the messages sent to the form on which your component was placed. In that case the control would be the owner of the component. However, it is quite conceivable that your component may need to monitor the messages sent to different controls on the same form. Then it would be appropriate to include a TWinControl* property and replace the control’s window procedure with the component’s window procedure in the property’s set method. This is slightly more complex than with the control as the component’s owner, but the techniques are similar. In the following example, it is the component’s owner (obtained from the Owner property) whose messages we want to monitor. Listing 11.6 shows a class definition for a non-visual component that can monitor the messages of the Owner onto which it is placed. This will be a TWinControl descendant. We therefore dynamic_cast the Owner property (a TComponent*) to a TWinControl*. LISTING 11.6
Definition for a Component to Subclass a Control Using the Windows API
class PACKAGE TSubClassComponent : public TComponent { private: void* PrevControlWndProc; void* NewComponentWndProc; protected: virtual void __fastcall WndProc(TMessage& Message); virtual void __fastcall Loaded(); public: __fastcall TSubClassComponent(TComponent* Owner); __fastcall ~TSubClassComponent(); };
Listing 11.7 shows the implementation details of the methods in Listing 11.6.
13 9721 CH11
11/13/00
9:52 AM
Page 721
More Custom Component Techniques CHAPTER 11
LISTING 11.7
Implementation for a Component to Subclass a Control Using the
Windows API
__fastcall TSubClassComponent::TSubClassComponent(TComponent* Owner) : TComponent(Owner), PrevControlWndProc(0), NewComponentWndProc(0) { TWinControl* WinControl = dynamic_cast(Owner); if(!ComponentState.Contains(csDesigning) && WinControl) { NewComponentWndProc = MakeObjectInstance(WndProc); } } //---------------------------------------------------------------------------// // DESTRUCTOR __fastcall TSubClassComponent::~TSubClassComponent() { TWinControl* WinControl = dynamic_cast