TE AM FL Y
Kylix 2 Development ™
Eric Whipple Rick Ross Nick Hodges
Wordware Publishing, Inc.
© 2002, Wordware Publishing, Inc. All Rights Reserved 2320 Los Rios Boulevard Plano, Texas 75074 No part of this book may be reproduced in any form or by any means without permission in writing from Wordware Publishing, Inc. Printed in the United States of America
ISBN 1-55622-774-4 10 9 8 7 6 5 4 3 2 1
0201
Kylix is a trademark of Borland Software Corporation. Linux is a registered trademark of Linus Torvalds. Other product names mentioned are used for identification purposes only and may be trademarks of their respective companies.
All inquiries for volume purchases of this book should be addressed to Wordware Publishing, Inc., at the above address. Telephone inquiries may be made by calling: (972) 423-0090
Dedication For Niko and for my family Eric Whipple
For Clarissa
Rick Ross
Contents Dedication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iii Foreword . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvii Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xix Section 1—Introduction Chapter 1 Introduction. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Welcome to Kylix 2 Development . . . . . . . . . . . . . . . . . . . . . . . . 3 Welcome to Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 Welcome to Kylix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 Again, Welcome! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 Chapter 2 Installation. . . . . . . . . . . . . . . . . . . . . . . . . Introduction . . . . . . . . . . . . . . . . . . . . . System Requirements . . . . . . . . . . . . . . . Supported Distributions . . . . . . . . . . . . . . Pre-Installation Issues . . . . . . . . . . . . . . . Installing Kylix . . . . . . . . . . . . . . . . . . . Post-Installation Configuration and Testing . . . Running Kylix for the First Time . . . . . . . . . Additional Steps for VisiBroker/CORBA Users . Installing Interbase 6.0 - Kylix 2 . . . . . . . . . Installing Interbase 5.6 - Kylix 1 . . . . . . . . . Installing Interbase 6.0 - Kylix 1 Companion CD. Installing Interbase 6.0 - Internet Download . . . Summary . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
11 11 11 11 12 13 19 20 21 21 23 24 25 28
Chapter 3 The Kylix IDE. . . . . . . . . . . . . . Hello World . . . . . . . . . . . . The Kylix IDE from 10,000 Feet The Kylix IDE: A Closer Look . Controlling Your Environment. . Summary . . . . . . . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
29 29 29 30 49 62
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
v
Contents
Section 2—Building Applications Chapter 4 Working with Projects and Files . Introduction . . . . . . . . . Project Files . . . . . . . . Creating a New Project . . Working with Projects . . . Summary . . . . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
65 65 65 71 75 85
Chapter 5 Object Pascal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 Linux Issues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 Comments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 Variables. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 Assignment and Comparison . . . . . . . . . . . . . . . . . . . . . . . . . 91 Constants . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91 Typed Constants . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 Data Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 Program Flow. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98 Classes and Objects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115 Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 Chapter 6 Application Architecture . . . . . . . . . . . . . . . Introduction . . . . . . . . . . . . . . . . . . . A Comparison of Development Models . . . . Variable Scope: Global vs. Local Variables . . Using Scope Effectively in Kylix Applications Using Scope in Kylix Classes. . . . . . . . . . Summary. . . . . . . . . . . . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
123 123 123 125 129 137 139
Chapter 7 Object-Oriented Development . . . . . . . . Introduction . . . . . . . . . . . . . . . The Four Pillars of OOP . . . . . . . . Typecasting . . . . . . . . . . . . . . . Virtual Methods . . . . . . . . . . . . . Putting It All Together: Polymorphism Another Side of OOP . . . . . . . . . . Summary. . . . . . . . . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
141 141 142 145 148 150 155 161
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
Chapter 8 Shared Objects and Packages . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
vi
Contents
Shared Objects Exceptions . . . Packages . . . . Summary. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
163 171 174 192
Chapter 9 Compiler, Run-time Library, and Variants Introduction . . . . . . . . . . . . . Compiler Overview . . . . . . . . . Run-time Library . . . . . . . . . . Variants . . . . . . . . . . . . . . . Summary. . . . . . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
193 193 200 201 224
Chapter 10 Exception Handling and Resource Protection . . . . Introduction . . . . . . . . . . . . . . . . . . . History . . . . . . . . . . . . . . . . . . . . . . The Life of an Exception Object . . . . . . . . Exception Hierarchy . . . . . . . . . . . . . . Creating Custom Exceptions . . . . . . . . . . Customizing Application Exception Handling. Summary. . . . . . . . . . . . . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
225 225 225 226 228 236 238 239
Chapter 11 Debugging and the Debugger. . . . . . . . . . . . . . . Introduction . . . . . . . . . . . . . . . . . . . . . Programming Utopia . . . . . . . . . . . . . . . . Debugging Techniques . . . . . . . . . . . . . . . Debugging Applications. . . . . . . . . . . . . . . The Integrated Debugger. . . . . . . . . . . . . . Debugging Shared Object Libraries and Packages Summary. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
241 241 241 241 244 245 254 255
Section 3—Data Access Chapter 12 dbExpress and DataCLX Introduction . . . . dbExpress . . . . . DataCLX . . . . . . Summary. . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
259 259 260 265 277
Chapter 13 Client-Side Data Management . . . . Introduction . . . . . . . . . . . Organizing Client-Side Data . . Displaying Application Data . . Using Client DataSets . . . . . Manipulating Client-Side Data . Resolving Data Updates . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
279 279 279 282 284 287 294
vii
Contents
Dealing with Offline Data . . . . . . . . . . . . . . . . . . . . . . . . . . . 301 The Super Data Component: TSQLClientDataSet . . . . . . . . . . . . . 302 Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304 Chapter 14 Using Field Objects . . . . . . . . . . . . Introduction . . . . . . . . . . . . . Kylix Datasets, Revisited . . . . . . Field Objects in Kylix Applications Summary. . . . . . . . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
305 305 305 307 317
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
321 321 322 322 325 336 340 351 355
Chapter 16 Writing Custom Components . . . . . . . . . . . . Introduction . . . . . . . . . . . . . . . . . . Why Create Components? . . . . . . . . . . A Whole New Audience. . . . . . . . . . . . Component Properties . . . . . . . . . . . . Component Events . . . . . . . . . . . . . . Using Custom Classes . . . . . . . . . . . . Exposing Internal Component Members . . Building Data-Aware Components . . . . . . Writing Platform-Independent Components Building Custom Property Editors. . . . . . Deploying Your Components with Packages Summary. . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
357 357 357 358 358 362 366 370 374 377 378 382 385
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
389 389 389 389 390 394 396
Section 4—Components Chapter 15 CLX. . . . . . . . . . . . . . . . . . . Introduction . . . . . . . . . . . vCLX Background. . . . . . . . The vCLX Architecture. . . . . vCLX Events . . . . . . . . . . Graphics in vCLX . . . . . . . . Using Common vCLX Controls vCLX Tricks . . . . . . . . . . . Summary. . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
Section 5—Advanced Linux Development Chapter 17 Processes and Threads . . . . Introduction . . . . . . . Multitasking . . . . . . . Processes . . . . . . . . Creating a New Process Daemon Processes . . . Threads . . . . . . . . .
viii
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
Contents
Creating Threads . . . . . . . . . . . . Supporting TCanvas Methods . . . . . Exceptions . . . . . . . . . . . . . . . . Multithreaded dbExpress Applications Summary. . . . . . . . . . . . . . . . . Chapter 18 Synchronization IPCs . . . . Introduction . . . . . . Mutexes . . . . . . . . Condition Variables . . POSIX Semaphores . . System V Semaphores Read-Write Locks . . . Record Locking . . . . Summary. . . . . . . .
. . . . . . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
400 414 418 419 419
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
421 421 421 427 432 436 445 450 456
Chapter 19 Message Passing IPCs . . . . . . Introduction . . . . . . . . . Pipes . . . . . . . . . . . . . FIFOS (Named Pipes) . . . System V Message Queues Summary. . . . . . . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
457 457 457 466 471 480
Chapter 20 Shared Memory . . . . . . . Introduction . . . . . . Linux Shared Memory Summary. . . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
481 481 482 500
Chapter 21 Internet Applications — NetCLX . . Introduction . . . . . . . . . . . Types of Internet Applications . WebBroker Architecture . . . . Page Producers . . . . . . . . . Data-aware Producers . . . . . Other Internet Components . . Summary. . . . . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
503 503 504 505 515 518 526 528
Chapter 22 Introduction to WebSnap . . . . . . Introduction . . . . . . . . . . WebSnap Features. . . . . . . A Minimum Application. . . . A Little Deeper into WebSnap Anatomy of a WebSnap Page .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
. . . . . .
531 531 531 533 539 547
. . . .
. . . . . . . . .
. . . . .
. . . .
. . . .
Section 6—Enterprise Applications
. . . . . .
ix
Contents
Using TAdapter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 549 Displaying Data in a WebSnap Application . . . . . . . . . . . . . . . . . 559 Summary. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 561 Chapter 23 Advanced WebSnap . . . . . . . . . . . . . . . . . . . Introduction . . . . . . . . . . . . . . . . . . . . Granting Rights on Actions. . . . . . . . . . . . Granting Access to Specific Pages . . . . . . . . Persistent Sessions . . . . . . . . . . . . . . . . Image Handling . . . . . . . . . . . . . . . . . . File Uploading . . . . . . . . . . . . . . . . . . . Adding Components to TAdapterPageProducer. Summary. . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
563 563 563 564 568 575 576 578 581
Chapter 24 Web Services. . . . . . . . . . . . . . . . . Introduction . . . . . . . . . . . . . . Web Services Background . . . . . . Creating a Web Service . . . . . . . . Creating the Client . . . . . . . . . . Connecting to a Public Web Service . Summary. . . . . . . . . . . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
. . . . . . .
583 583 583 584 588 591 593
Chapter 25 CORBA Development with VisiBroker 4.5 for Kylix Introduction . . . . . . . . . . . . . . . . . . . Distributed Computing Background . . . . . . CORBA Basics . . . . . . . . . . . . . . . . . CORBA Independence . . . . . . . . . . . . . Kylix Support . . . . . . . . . . . . . . . . . . VisiBroker . . . . . . . . . . . . . . . . . . . . Simple CORBA Hello, World Example . . . . BOA vs. POA . . . . . . . . . . . . . . . . . . Summary. . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
595 595 595 596 597 602 602 603 612 616
TE
AM FL Y
. . . . . . .
References. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 617 Index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 619
x
Foreword To understand the importance of this work to the Kylix community, you need to understand a bit of history in Delphi’s importance to the Windows community — Delphi, of course, being the immediate ancestor of Kylix. The historical parallels are remarkable. On Valentine’s Day in 1995 a little software shop called Borland International released a product called Delphi, and Windows programming was forever changed. While a lot can be said about exactly what made Delphi into the success it ultimately became, an argument can be made for Delphi owing its success to three key factors: n
Delphi challenged the previously held belief that developers had to choose between run-time performance and ease of development. Prior to this, developers felt they had to choose between Visual Basic if they wanted rapid application development (RAD) or C/C++ if they wanted run-time performance. By combining a super-fast native code compiler with visual application design tools, Delphi demonstrated to the world that productivity and performance are not mutually exclusive.
n
Delphi also challenged the assumption that database features could not be a first-class feature in a high-performance development tool. Rather than work with databases as a disjointed VBX-style or C++ library bolt-on or in bulky 4GL tools, Delphi made database functionality a first-class citizen in the development tool, adding easy database programming to the already powerful combination of RAD and run-time performance.
n
A healthy third-party market grew around Delphi, consisting of publications, component vendors, and a devoted community of software developers. This enabled those in the know to distribute the knowledge virally across this worldwide community.
I was lucky enough to own not one but two front row seats to this revolution in Windows development. As a member of the Delphi team at Borland, I helped define, create, and launch the product into the market. As an author of the bestselling Delphi Developer’s Guide series of books, I was a part of the community that established the knowledge beachhead in the industry that provided the information folks needed to become effective Delphi developers. Prior to the introduction of Kylix, the Linux development community found itself in a position similar to that of the Windows community prior to the launch of Delphi. Projecting Delphi’s keys to success to the world of Linux and Kylix, you find: n
Linux was at a similar crossroad between performance and developer productivity. In fact, Linux was perhaps even further behind the developer productivity curve than Windows because Linux lacked a rapid application development tool with the usability and
xi
Foreword
critical mass that Visual Basic brought to Windows in the early days. A great deal of Linux development is still done using text-based tools such as EMACS and GCC, which — while lightweight and flexible — don’t begin to offer the performance/productivity combination of Kylix. n
Likewise, Linux database developers for the most part are put in the position of having to choose between C/C++-style libraries that lack productivity and integration or less efficient access via scripting languages such as Perl or PHP.
n
Kylix is now beginning to build community and third-party momentum on the Linux platform as Delphi did before it on Windows. Kylix has an opportunity to take even greater hold in the hearts and minds of Linux developers due to the propensity of Linux developers to be connected to the community and the lack of other dominant vendors in the tools market. You hold in your hand one of the vehicles driving this momentum: Kylix 2 Development is exactly what Kylix developers — and the Linux community in general — needs to raise the standard of development to the next level.
Kylix 2 Development is clearly written for developers by developers, with an eye toward giving Kylix programmers the exact information they require to first become productive with the tool, and then learn to make it sing for them in building more complex applications. The text is pithy and clear, and the code snippets provide great illustration for using Kylix and the Object Pascal language they way they are intended to be used. The maxim stating that “history repeats itself” is clearly manifesting itself in the parallels between the successes of Delphi in the Windows world and Kylix in the Linux world. Kylix 2 Development provides one of the keys to riding this next great wave of developer productivity. Steve Teixeira
xii
Acknowledgments We started out with a mission — to write a book. We were unaware of the daunting task ahead of us. Blindly, we leapt into unknown waters and almost instantly were in over our heads. We persevered through some rough times, both personally and professionally. The fruit of our labor resulted in Kylix 2 Development. Without Kylix, there would be no need for a book. To that end, our hats are off to the entire Kylix and Delphi teams at Borland for producing a superb RAD tool for Linux and Windows. Keep up the great work. We look forward to whatever else is up your collective sleeves! Similarly, without Linux, this book would not have been written. We would like to thank Linus Torvalds, Alan Cox, and the numerous volunteers who are constantly improving an incredible operating system. We lost count of the number of updates to the kernel that were made while we were writing this book. We would like to thank our technical editors, Nick Hodges and Xavier Pacheco, for their expertise in reviewing the manuscripts. They were instrumental in reviewing the book and offering valuable suggestions. Thanks also to Mark Duncan and Danny Thorpe. Our associates at PILLAR were helpful with ideas, suggestions, guidance, and encouragement, especially Ken Faw. A special thank you goes to Steve Teixeira for writing the foreword. The crew at Wordware Publishing — Jim Hill, Wes Beckwith, Beth Kohler, Kellie Henderson, and countless others whom we don’t even know — was incredible! Thank you for your patience, encouragement, and professionalism while helping us craft this book. Finally, but certainly not least, the following people helped in one way or another: Jayson Cavendish, Richard Vissers, Jeff Puschak, John Kaster, and others we are probably missing. If we have left anyone out, we are truly sorry. Eric Whipple and Rick Ross
xiii
Acknowledgments
I would like to give special thanks to my mentor, Ken Faw, for his years of persistence and insistence on technical excellence. Also, thanks to my programming partner and colleague, Ernie Mecham, for his friendship and encouragement. Thanks to my family and to the people at PILLAR for their patience and understanding of issues both within the book and without. Thanks to my coauthor, Rick, who taught me how to tie a bow. Finally, thanks to my savior, Jesus Christ, who was with me every step and continues to be. Eric Whipple I have been truly blessed by the giver of Life, Jesus Christ, who is my personal savior. I can’t imagine life without Him or his abundant blessings. I thank Jesus for my wife, Clarissa. She is an incredible woman. While I worked on this book, she managed to take care of our three (and then there were four!) beautiful children. In addition, she also reviewed portions of the book and offered many comments and suggestions that resulted in a more readable book. Thank you for your endless love! I thank Jesus for my parents who play an important part in my life. They have always encouraged me to reach for the stars. Thank you for your love and support. Finally, I would like to thank my high school English teacher, Mrs. Patricia Burkett, for her encouragement and teachings. Rick Ross
xiv
Chapter 1
Introduction
Welcome to Kylix 2 Development Welcome to Kylix 2 Development, the best thing to happen to development since the disappearance of the punch card. In this book, we examine the intricacies of using Borland’s Kylix to develop applications for the Linux platform. As you read this book, you will see that the qualities that have made the award-winning Borland development products successful were skillfully assembled to create the very best in visual Linux development tools.
Who Should Read This Book? One of the great subtleties of this book is that it brings together kids from every side of the playground. On the particular playground in which this book was written, there are three major groups of kids. Each group has their own particular styles and preferences, but we all have to get along somehow. The Windows are the most powerful and influential family in the neighborhood. They don’t make up a lot of the playground games, but they have a talent for refining and improving on them so that all the children are able to understand them. They are a large and close-knit family. They play together all day every day. Their father, Bill, has brought them up to believe that other families are dirty and foolish. They banter self-importantly with each other about who can climb the monkey bars faster but playing with other children makes them nervous. The Torvalds are new to the playground and are a different breed. Until recently, they were all home schooled and were never able to play with the Windows children. Their unique education has made them brilliant but most of them have developed a strong dislike for the Windows kids. Instead of using the playground monkey bars, the Torvalds children have decided to build their own, just to show that they can. Knowing that anyone could build monkey bars with rods, bolts, and wrenches, the Torvalds children have designed and assembled a cheaper and stronger set built out of twigs and grass. The Cios are an energetic and industrious group of kids. They show all the children how to get the most play for their day and how to play nice with each other. Unfortunately, the other kids don’t always cooperate. The Torvalds sometimes
3
4
Chapter 1: Introduction
refuse to play with the Windows children altogether, and the Windows kids won’t share their toys. This makes the Cios sad. They really want all the kids to have fun, but they’re frustrated because they know that there’s got to be a better way to do it. They sit at the top of the tall rocket ship overlooking the entire playground and wonder how to make everyone share and play nice. If you know anyone who resembles any of these groups of kids, you should recommend this book to them. Kylix 2 Development is great for Windows developers (especially Borland product developers) who are branching out into the Linux world. It eases you into the Linux world by first placing you in familiar territory (the Delphi IDE) and then introducing you to new techniques and development issues. Windows developers learn how to move away from proprietary Windows concepts and migrate and expand their applications to a whole new platform. Linux developers learn how to use the graphical two-way user interface and utilities to create applications quickly and easily. Finally, project managers and teams learn about the “big picture” issues, such as cross-platform components and application design techniques for porting existing applications to other operating systems.
How to Read This Book In addition to the main chapter text and examples, this book includes a large number of author notes. Author notes are designed either to draw the reader’s attention to points that the authors feel are especially important or to give further insight into a chapter topic. A specific icon identifies each note. A description of each is given below: Tips — Tips can be used to refresh the reader’s mind about a previously discussed topic or to help the reader anticipate the next step in a process. They usually refer directly to the topic at hand and are intended to help the reader “put the pieces together.” Warnings — Warnings are designed to help the reader avoid the pitfalls of a particularly tricky development technique or to make him aware of a commonly made mistake. Warnings typically refer to the situation at hand but may be used to point out the impact of a development choice on another part of the system or design. Alerts — Stop! If you see the alert sign, pay attention and take heed. Alerts are designed to make the reader aware of a particularly hazardous operation or side effect. They are used to refer to fatal errors within an application but may also be used to point out potentially dangerous differences in platforms. Don’t say we didn’t warn you! Notes — Notes allow the authors to deviate from a topic to discuss additionally relevant or related topics. Each note is featured in a shaded area and may include important links or references.
Chapter 1: Introduction
5
OS Notes — Author notes may also include icons that denote a particular operating system. This indicates that the note is meant to be particularly significant or helpful to anyone who is coming from a background in that OS. Warning: Notes make awfully good soapboxes! The authors of this book reserve the right to preach based on their own developmental styles and experiences at any time!
What to Expect Kylix 2 Development helps you master Kylix by giving you detailed information and instruction on a wide range of topics including: n
The Kylix IDE
n
Linux application architecture
n
Object-oriented programming
n
CLX components and architecture
n
dbExpress and Data Access with DataCLX
n
Advanced Linux development
n
Web development with Web Services and WebSnap
n
Cross-platform web development with NetCLX
n
Writing enterprise applications with XML and CORBA
Topics are covered through detailed descriptions and specific coding examples and techniques. Special notes from the authors help the developer make wise design and implementation choices to create sophisticated and flexible software for the Linux platform.
What Not to Expect Kylix 2 Development covers a wide range of topics from the simplest of client/server programs to the intricacies of writing complex, multithreaded and multiprocess applications. It is not an in-depth guide to the Linux operating system. A basic understanding of Linux is assumed including the installation, configuration, and navigation of whatever Linux implementation you choose. Appendix C, on the companion CD, lists some common Linux shell commands and concepts and gives a brief explanation of each. If you are new to Linux and want more information or a list of references, please check out http://www.linuxnewbie.org.
6
Chapter 1: Introduction
On the CD The CD that accompanies this book is a great resource of information on a number of topics and includes the following: n
All source code and examples contained in the book
n
Additional sample applications and utilities designed by the authors
n
Linux tutorial
n
Additional chapters (appendixes) not included in the book
Welcome to Linux What is Linux?
Why Linux?
AM FL Y
Linux is an operating system originally developed at the University of Helsinki by Linus Torvalds. Torvalds was interested in Minix, a small UNIX clone, and set out to write a system that exceeded the Minix standards. The first version of Linux was released in 1991 (version 0.02) and the first full-featured Linux kernel was released in 1994. As of January 2002, the current kernel version is at 2.4 and is continuing to evolve.
TE
Cost — One of the best reasons to move or expand to Linux is the cost. The Linux kernel and libraries are totally free for download from a number of web sites. However, this does not mean that Linux distributions are free. Linux vendors such as RedHat, SuSE, Mandrake, and Caldera (to name a few) each provide a more user-friendly installation of core Linux packages. In addition, they add onto the basic Linux code a set of configuration and installation utilities that are geared toward a particular distribution of Linux. These additional services can be downloaded freely or purchased off the shelf. Of course, as demand rises, prices will rise, but Linux distribution costs are still dramatically less than more commonly used corporate, web, FTP, and e-mail servers. Stability — The Linux operating system provides a much more stable platform for running services and applications than other commercially available systems. With proper installation and configuration of the Linux kernel and drivers, the system runs longer with fewer reboots. System crashes are few and far between and, if an application crashes, it is less likely to affect the overall stability of the operating system. Stability is a vital component to the survival of mission-critical systems. Application servers, Internet applications, data warehouses, and countless other applications demand a stable platform in order to provide the value for which they were designed.
Chapter 1: Introduction
7
Flexibility — Another great thing about Linux is that it is inherently configurable and extensible. All Linux kernels are required to be open source. The open source movement forces kernel implementers to make all of their source code available to developers for study and modification. This allows everyone in the global development community to extend Linux in whatever way is appropriate for their use. It encourages the continued growth and refinement of the Linux OS and, because the Linux kernel is required to be freely distributable, gives everyone the chance to use this great technology.
Welcome to Kylix What is Kylix? Kylix is the newest way to create sophisticated software for the Linux platform. According to Borland, “(Kylix). . . is a high-performance native Linux rapid application development tool that radically speeds and simplifies development through component based visual programming.” Ask just about anybody what the number one problem with Linux is, and they’re likely to answer you in one word: software. Linux has struggled to become a viable alternative to Windows in part because of the lack of Linux software. Kylix is here to change all of that. No other tool available has the tradition and refinement that Kylix gets from its popular Windows counterpart. Few companies have made as strong a commitment to the Linux platform as Borland.
Why Kylix? Kylix is a pioneer in the increasingly competitive field of visual Linux development tools. Its tested IDE and editor allow the quick creation of Linux utilities and applications. Kylix is the latest in Borland’s prestigious line of award-winning products. Its Windows counterpart, Delphi, has turned out a wide variety of components, services, and applications for the desktop, the server, and the Internet. It creates a fresh face in the world of Linux development, but also brings with it years of refinement and testing in the success of Delphi.
Kylix at a Glance Following is a summary of some of the notable features included in Kylix and how they are addressed in this book. Borland developers can look at this as the “What’s New” section for Kylix. It serves as a summary of some of the differences between Delphi and Kylix, as well as a summary of some of the more important topics to be covered in this book. Please note that this is not an exhaustive list and is meant only to provide a look ahead at some of the material we’ll be covering.
8
Chapter 1: Introduction
The IDE The Delphi for Windows integrated development environment is arguably the best on the market in terms of organization and ease of use. Even in its first release, Kylix preserves many of the things that make the Delphi IDE great. In Chapter 3, we discover the individual pieces of the Kylix integrated development environment (IDE), and how they work together to provide a high level of development productivity.
CLX Component Library for X (Cross) Platform is a set of visual and nonvisual components that are designed to promote reuse and rapid application development on the Linux platform. CLX components are used in the same way as VCL components are used in Delphi. They provide Object Pascal wrappers around and extensions of a C++ library (called Qt) produced by TrollTech (www.trolltech.com). The majority of component properties, methods, and events have been preserved in moving from the VCL to CLX, and some exciting new ones have been added. Throughout Kylix 2 Development, CLX is demonstrated in its most common uses such as to provide data access and Internet applications. Chapter 15 takes us further into CLX by explaining the CLX event model and the Pascal wrapping layer.
The Data Layer (dbExpress and DataCLX) One of Delphi’s greatest selling points is its ability to access, display, and navigate data. Kylix continues this tradition with its DataCLX components and an underlying architecture called dbExpress that uses Borland’s powerful DataSnap technology to provide efficient and powerful access to data. dbExpress represents the underlying data access architecture that is used by DataCLX components to natively access a wide array of database servers. It is a set of low-level interfaces on which dbExpress drivers are built. Database vendors can implement the dbExpress interfaces to provide the raw data retrieval needed for accessing data in applications. Chapter 12 describes the foundations and principles of the dbExpress architecture, as well as the basic steps of using DataCLX to bring data to the user interface. Chapter 13 continues the data access discussion with a closer look at how data is managed in client applications.
Component Development Component development is one of the fundamentally different pieces of Kylix. Traditional Windows component development relies heavily on proprietary Windows mechanisms that don’t exist in Linux (such as Windows messaging). Chapter 15 explains what’s going on beneath the covers of CLX and shows component developers how to create their own custom components.
Chapter 1: Introduction
9
Advanced Linux Development Unleash the power of Linux by examining the advanced features that Linux has to offer. Processes, daemons, threads, and interprocess communications are covered in depth to give you the knowledge needed to create sophisticated systems easily.
Enterprise Development Kylix provides an incredible collection of tools for writing sophisticated Internet and enterprise applications that can be developed, deployed, and managed from all over the world. Kylix makes quick work of web application development including CGI and Apache DSO modules. Chapter 21 describes and demonstrates the power of NetCLX, a powerful, cross-platform web development architecture. Chapters 22 and 23 go on to discuss the sophisticated and robust world of Borland’s newest accomplishment in web development, WebSnap. Finally, Chapter 24 covers the emerging world of Web Services, including the creation and consumption of internally created services, as well as the importing of already existing services. Chapter 25 details Kylix’s ability to create distributed, enterprise applications using the CORBA specification. Kylix imports pre-existing IDL directly, generating Pascal code for both clients and servers. By simply implementing the business logic of your application, you can produce fast and powerful CORBA objects that are ready to be accessed from anywhere.
Answering Some FAQs Q: Do I have to know Delphi for Windows first? A: No. Kylix 2 Development leads you step by step through the basics of using Kylix, including the basic pieces of the integrated development environment (IDE) and the use of components. Of course, any experience with Delphi for Windows or any other Borland IDE makes the transition to Kylix that much easier, but it is not necessary. No matter what your development background, Kylix 2 Development will have you churning out code in no time. Q: Do I have to be a Linux expert? A: Definitely not. One of the most beautiful things about this product is that it abstracts most of the gory details of developing in Linux. The CLX components wrap around all of the low-level libraries and enable you to create quick and practical development solutions. That being said, it should be noted that the more Linux experience you have, the easier it is to get behind the scenes and understand the underlying architecture and other foundational elements that help you to master more advanced development techniques. Q: Do I have the right flavor of Linux? A: Probably. Kylix supports all major implementations of Linux, but requires Kernel 2.2 or higher, libgtk.so version 1.2 or higher (used for graphical installation only), libjpeg.so version 6.2 or higher, and a compatible X11R6 terminal server like
10
Chapter 1: Introduction
XFree86. Fortunately, most of the recent releases of major Linux implementations support these requirements, but you should check your Linux documentation to be sure. Q: Do I have to use a particular desktop? A: No. Kylix supports both the KDE and GNOME desktops. I would like to be able to say that it supports them equally, but this is not precisely the case. Because the Kylix component library (CLX) is wrapped around the TrollTech Qt library (see Chapter 15), which supports KDE better than GNOME, there are some small differences in the graphical representation. These differences are not major and should not interfere with development. Borland is working closely with TrollTech to ensure higher levels of desktop independence in future versions of Qt. Q: Can I develop proprietary code, or does it have to be open source? A: This is not a law book, and as such should not be used as a replacement for understanding copyright laws or the GPL licensing specifics. However, if you write an application for commercial distribution that contains components or other source code that was distributed under the General Public License standard which is maintained by GNU (http://www.gnu.org), you are required to make all of your source code available as part of the open source initiative. Q: Can I develop Kernel device drivers? A: No. Kylix is geared for application developers and not kernel developers.
Again, Welcome! Kylix 2 Development is designed to answer the questions of the novice as well as the advanced developer. Its in-depth descriptions of the Kylix IDE and the Object Pascal language are sure to enhance the skills of experienced developers as well as enable new developers to master the techniques and shortcuts to proficiency in the number one visual development tool for Linux. Its detailed look at multithreaded and multiprocess applications is sure to help you solve some of your most intricate development issues. Finally, its discussion of NetCLX, Web Services, WebSnap, and CORBA provide the necessary tools to write scalable and reliable Internet and enterprise applications. So take the time to master visual Linux development with a tool that has the power to help you corner the Linux software market and to expand into whatever world your imagination can create.
Chapter 2
Installation
Introduction Installing Kylix is easy for recent Linux distributions. However, certain distributions need to be updated before Kylix can be installed. This chapter covers the installation of Kylix, system updates that are required, recommended installation locations, and troubleshooting tips to make installation a breeze. As an added bonus, installation of Interbase 5.6 and 6.0 are also detailed.
System Requirements Kylix can run on most modern Linux distributions. The minimum requirements for installing Kylix are: n
Kernel version 2.2 or higher
n
libjpeg version 6.2 (libjpeg.so.6.2) or higher
n
An X Window system compatible with X11R6, like XFree86
n
For graphical installations and WebSnap previews, libgtk version 1.2 or higher
The X Window requirement is needed even when only using the command-line compiler (dcc). Using only the command-line compiler is certainly an option, but once the IDE is used, the command line will be a distant memory. While not a system requirement from Borland, the authors would also add glibc 2.2 or higher to the list. It contains major bug fixes and is much more reliable than previous versions.
Supported Distributions While Kylix can run on numerous Linux distributions, Borland has certified Kylix 1 on the following distributions: n
Red Hat Linux 6.2 or higher
n
SuSE 7.0 or higher
11
12
Chapter 2: Installation n
Mandrake Linux 7.2 or higher
Kylix 2 has been certified on these distributions: n
Red Hat Linux 7.1
n
SuSE 7.2
n
Mandrake Linux 8.0
What does the certification mean? Borland will only support Kylix installed on the above-mentioned distributions. This does not mean that Borland will abandon you; rather, they have not tested Kylix extensively on every major distribution — yet.
Pre-Installation Issues Before installing Kylix, decide whether to install as a root user or a normal user. Installing as root has several advantages: n
Kylix needs to be available to all users of the system.
n
The distribution being used is RPM based and you want to use the Kylix RPMs.
n
Installation takes care of certain installation problems automatically.
n
The Kylix files are centrally located.
n
It is easier to share CLX objects with the Object Repository. Permissions do need to be granted to accomplish this.
It is the opinion of the authors that the only reason to install Kylix without using the root user is because the password is not available. In all other cases, it is strongly recommended that installation be performed as the root user. In the Install README file, additional advantages are discussed about installing as a normal, non-root user. During the development of Kylix, the team discovered a bug in the GNU C Libraries (glibc). This bug is tested during the installation of Kylix. If the bug is discovered, the appropriate patches need to be applied before Kylix will install. This is covered in the next section. Tip: Installing Kylix as a normal user forces the installation to use the tar files instead of the RPM method. On distributions that do not have a Red Hat Package Manager (RPM), the Kylix installer will use the tar files.
Chapter 2: Installation
13
Installing Kylix Installing Kylix is straightforward. If an X Window System is currently running, bring up a terminal window. Switch to the root user, using the su command, if a root installation needs to be performed. The first step is to place the Kylix CD-ROM into the drive. Then, if the CD-ROM drive is not automatically mounted, mount the drive using a command similar to: mount /dev/cdrom
Tip: For more information on using the mount command, see Appendix F: “Linux Commands,” use the man pages (e.g., man mount), or consult a Linux reference book. Mounting may require root privileges. If so, log in as root, if needed, and mount the drive. Once mounted, change the current directory to the mount point of the CD-ROM. At the command prompt type: sh setup.sh
Now the Kylix installation program will begin. Before performing the installation, a pre-installation test is performed that verifies if the system has the proper requirements. For most distributions the pre-test will fail. Do not panic, as this is normal. The failure will look something like the following: [root@lnxbox cdrom]# sh setup.sh BORLAND KYLIX Checking dependencies... Kernel version >= 2.2.0....OK Glibc version....FAILED Setup has determined that your system needs an updated version of glibc (C runtime library). The version of glibc that is required varies by distribution. Please read the document "PREINSTALL" on this disc. X11 Server....OK Libjpeg version >= 6.2.0....OK Libgtk version >= 1.2.0....OK Your system does not meet the minimum system requirements. Setup cannot continue.
14
Chapter 2: Installation
As the message indicates, there are detailed instructions found in the PREINSTALL text file located on the CD-ROM. The best way to continue is to look for an official update for glibc from the specific distribution that is being run. However, Borland has placed patches for several distributions on the Kylix installation CD-ROM. These are not supported and are only there for convenience. Distributions that have patches on the installation CD-ROM are Mandrake 7.2, Red Hat 6.2 and 7.0, and SuSE 7.0. For further information on the installation of these patches, consult the PREINSTALL file. The following table shows numerous distributions and the versions of the kernel and glibc of the default, out-of-the-box installations. In addition, the table shows if patches are required to install Kylix and the recommended installation directory, when installing as root, using the distribution’s default installation. Table 2-1 Distribution
Kernel Version
Glibc Version
Patches Required?
Recommended Install
Red Hat 6.2 Red Hat 7.0 Red Hat 7.1 Red Hat 7.2 SuSE 7.0 SuSE 7.1 SuSE 7.2 SuSE 7.3 Mandrake 7.2 Mandrake 8.0 Mandrake 8.1
2.2.14-5 2.2.16-22 2.4.2-2 2.4.7-10 2.2.16 2.2.18 and 2.4.0 2.4.4 2.4.10 2.2.17-21-mdk 2.4.3-20mdk 2.4.8
2.1.3-15 2.1.92 2.2.2-10 2.2.4-13 2.1.3-143 2.2-7 2.2.2 2.2.4 2.1.3-16mdk 2.2.2-4mdk 2.2.4
Yes Yes Yes* No Yes No No No Yes No No
/opt/kylix2 /opt/kylix2 /opt/kylix2 /opt/kylix2 /opt/kylix2 /opt/kylix2 /opt/kylix2 /opt/kylix2 /usr/kylix2 /usr/kylix2 /usr/kylix2
*Installing on Red Hat 7.1 as a normal user does not require any patches. However, due to a bug in the RPM tool that ships with Red Hat 7.1, an update is required if Kylix will be installed as root. Another alternative is to specify the –m option when running the setup.sh, forcing the install to use the tar files instead of the RPM packages.
Tip: Make sure that there is enough room to install Kylix before running the setup script. Use the command df -h to list the available file systems and how much space is available on each of them. Once the appropriate patches are applied, run the setup script again. If the installer detects an X Window system, the graphical install runs, otherwise the console version runs.
Chapter 2: Installation
15
Tip: Certain distributions, SuSE in particular, have locked down the security of X Windows. During the installation, instead of the graphical install the text version will be used. If you would rather use the graphical install, execute the command xhost localhost as the user who started the current X Window session and not as the root user. Warning: For almost all installation programs, accepting the default options is the preferred method of installation. When installing Kylix 1 as the root user, do not accept the default install path of /root. This directory is the root user’s home directory and is the only user that has access to it. Instead, choose another directory, preferably /opt/kylix or /usr/kylix.
Console Install Now that the patches have been installed, the setup script continues executing. After agreeing to the license, the installation prompts for questions as shown below. [root@lnxbox cdrom]# sh setup.sh
BORLAND KYLIX 2 Checking dependencies... Kernel version >= 2.2.0....OK Glibc version >= 2.1.2....OK X11 Server....OK Libjpeg version >= 6.2.0....OK Libgtk version >= 1.2.0....OK
----====== Borland Kylix 2 installation program ======---You are running a x86 machine with glibc-2.1 Hit Control-C anytime to cancel this installation program. BORLAND SOFTWARE CORPORATION LICENSE TERMS KYLIX 2 ENTERPRISE The license would be displayed here, but was removed for space limitations. Borland Software Corporation 100 Enterprise Way
Chapter 2: Installation Scotts Valley, CA 95066-3249 Do you agree with the license? [Y/n] Y Would you like to read the /mnt/cdrom/README file ? [Y/n] Y KYLIX 2 RELEASE NOTES The Release Notes would be displayed here, but was removed for space limitations.
AM FL Y
------------------------------------------------------Copyright (c) 2001 Borland Software Corporation. All rights reserved. ------------------------------------------------------Please enter the installation path [/usr/local/kylix2] /opt/kylix2 Please enter the path in which to create the symbolic links [/usr/local/bin] Install Main Program Files? [Y/n/?] Y Install dbExpress (includes InterBase and MySQL drivers)? [Y/n/?] Y Install dbExpress Oracle Driver? [Y/n/?] Y Install dbExpress DB2 Driver? [Y/n/?] Y Install dbExpress Informix Driver? [Y/n/?] Y Install Help Files? [Y/n/?] Y Install Internet Components? [Y/n/?] Y Install WebSnap? [Y/n/?] Y Install Mozilla Preview Widget? [Y/n/?] Y Install Web Services? [Y/n/?] Y Install VisiBroker/CORBA Components? [N/y/?] Y (Defaults to N) Do you want to install GNOME/KDE menu items? [Y/n] Y Installing to /opt/kylix2 5641 MB available, 243 MB will be installed.
TE
16
Continue install? [Y/n]Y Installing Main Program Files ... 100% - setup.data/packages/kylix2_main_program_files-1.0-1.i386.rpm 100% - setup.data/packages/kylix2_ide-1.0-1.i386.rpm 0% - Running script Installing dbExpress (includes InterBase and MySQL drivers) ... 100% - setup.data/packages/kylix2_db_express-1.0-1.i386.rpm 0% - Running script Installing dbExpress Oracle Driver ... 100% - setup.data/packages/kylix2_db_oracle-1.0-1.i386.rpm Installing dbExpress DB2 Driver ... 100% - setup.data/packages/kylix2_db_db2-1.0-1.i386.rpm Installing dbExpress Informix Driver ... 100% - setup.data/packages/kylix2_db_informix-1.0-1.i386.rpm Installing Help Files ...
Chapter 2: Installation
17
100% - setup.data/packages/kylix2_help_files-1.0-1.i386.rpm 0% - Running script Installing Internet Components ... 100% - setup.data/packages/kylix2_internet-1.0-1.i386.rpm 0% - Running script Installing WebSnap ... 100% - setup.data/packages/kylix2_web_snap-1.0-1.i386.rpm 0% - Running script Installing Mozilla Preview Widget ... 100% - setup.data/packages/kylix2_mozilla-1.0-1.i386.rpm 0% - Running script Installing Web Services ... 100% - setup.data/packages/kylix2_web_services-1.0-1.i386.rpm 0% - Running script Installing VisiBroker/CORBA Components ... 100% - setup.data/packages/kylix2_visicorba-1.0-1.i386.rpm 0% - Running script 100% - /opt/kylix2/README 100% - /opt/kylix2/license.txt Installation complete. **** IMPORTANT **** If you installed the GNOME/KDE menu items, please restart X Windows to make the menu items appear. To ensure that the runtime environment is set up properly, always start Kylix from the GNOME/KDE menu or with this command: "startkylix".
GUI Install The graphical installation needs the same information as the non-graphical installation does. Kylix 1 users should note that the default install path is /root/kylix when installing as root. During the installation of Kylix, the installer displays a progress bar. One nice feature is that during the installation, the View Readme button is enabled, even when the installer doing its work.
18
Chapter 2: Installation
Figure 2-1: The Kylix GUI installation screen
Install Options Regardless of how the installation is performed, the options are the same. The following table lists a summary of the options that need further explanation. Table 2-2 Installation Option
Description
Install path
Where the actual Kylix files will be installed. Make sure that the user running the installation has permissions for this directory. The directory where links are placed to point to the command-line compiler, the help system, and the startkylix script. This directory should be in the default path of users who will be running Kylix. Adds an entry for Kylix to the menu system.
Link path
Desktop menu items
Warning: Installing Kylix as the root user is recommended. However, running Kylix as the root user is very dangerous. Instead, run Kylix as a normal user.
Chapter 2: Installation
19
Post-Installation Configuration and Testing After Kylix is installed, exit out of the root shell, if appropriate. Before starting Kylix for the first time, make an entry into a file named .bashrc. Notice the period before the name of the file. This file is executed each time a new shell is started. Simply adding one line to this file will make life easier, since it sets up the environment variables for compiling from the command line and for running applications that need certain shared object files. These needed files are discussed in detail in Appendix A, “Deploying Kylix Applications.” Change to the home directory using the cd command without any arguments. Edit the file .bashrc to include the following lines: source /bin/kylixpath # the next line is only necessary if you have installed the Visibroker/ CORBA components source /vbroker/vbroker.sh
Where is /opt/kylix2 or /usr/kylix2 or whatever directory Kylix was installed in. Save the file and exit the editor. Now exit the shell and bring up a new shell. You should see some messages that look similar to the following: PATH is now set to : /opt/kylix2/bin:/opt/kylix2/lib:/opt/kylix2/help:/opt/kylix2/vbroker:/usr/ local/bin:/bin:/usr/bin:/usr/X11R6/bin LD_LIBRARY_PATH is now set to : /opt/kylix2/bin:/opt/kylix2/vbroker: XPPATH is now set to : /opt/kylix2/help/xprinter HHHOME is now set to : /opt/kylix2/help auser@lnxbox:~ >
When convinced that the script file is working properly, edit the .bashrc file again and append “> /dev/null” to the end of the source command, so that the output is redirected elsewhere. The command should now look like this: source /bin/kylixpath > /dev/null
The next time a shell is started, no output will be displayed.
20
Chapter 2: Installation
Running Kylix for the First Time Now it is time to start Kylix. There are two ways to start Kylix. If, during the installation, the “Desktop menu items” option was selected, located in the menu will be a menu option titled “Borland Kylix 2.” Remember that X Windows needs to be restarted in order for the menu item to appear. Alternatively, bring up an x-terminal and use the startkylix script. Tip: The authors highly recommend starting Kylix from a terminal window. This helps viewing in messages and errors displayed from your applications and from Kylix. Regardless of the method used to start Kylix for the first time, a small dialog box will appear that displays a message stating that it is generating the font matrix. Once complete, the Kylix licensing dialog is displayed. After filling out the registration information, Kylix appears. You are now ready to code the next killer Linux application! The first time Kylix is run for a user, it creates a .borland directory underneath the home directory of the user. In this directory it creates four configuration files and other files and directories needed by the wine libraries. Wine libraries are a set of shared object libraries that Kylix uses internally. They contain a set of Win32 routines that are translated into the appropriate Linux versions. It is important to note that while Kylix uses the wine libraries, applications generated by Kylix do not. Table 2-3 explains these configuration files. Table 2-3 Configuration File Name
Description
delphi65dci
Any changes that the user makes to the Code Template macros are stored here. Any changes that the user makes to the Menu Templates are stored here. Any changes that the user makes to the Object Repository are stored here. IDE options are stored in this file.
delphi65dmt delphi65dro delphi65rc
In addition to the files mentioned above, the .borland directory of the user who installed Kylix (e.g., /root/.borland) contains the two files described in the following table.
Chapter 2: Installation
21
Table 2-4 Configuration File Name Description dbxconnections dbxdrivers
List of available dbExpress connections. List of the available dbExpress drivers that have been installed and the default parameters for a particular driver.
For further information on dbExpress, see Chapter 12.
Additional Steps for VisiBroker/CORBA Users VisiBroker requires an installed Java™ Runtime Environment (JRE) in order to execute itself. Located on the Kylix installation CD in the /jre directory is a version that can be installed. Detailed instructions for installing the JRE are found in the INSTALL file. After the JRE is installed, make sure that the jre1.3.1_01/bin directory is added to the PATH environment variable. The easiest way to verify the PATH environment variable is properly set is to type “java” at a shell prompt. If a Java usage screen appears, the PATH environment variable has been properly set. Some of the VisiBroker utilities use the Korn Shell for executing. Unfortunately, not all Linux distributions ship with a Korn Shell or a clone. Fortunately, there is a public domain Korn Shell available for a variety of distributions at http://rpmfind. net/linux/rpm2html/search.php?query=pdksh.
Installing Interbase 6.0 - Kylix 2 Included on the Kylix 2 installation CD-ROM is a database called Interbase. The setup script is located in the /interbase/IB60_linux directory. Run the setup script as the root user. If a previous version of Interbase is installed, consult the Install.txt file before continuing. root@lnxbox:~ > cd interbase/IB60_linux/ root@lnxbox:~/IB60_linux > ./setup 1. 2. 3. 4. 5. 6.
Install Install Install Install Install Exit
InterBase Client and Server software using RPM InterBase Documentation in PDF format InterClient JDBC software Easysoft ODBC driver software Adobe Acrobat(R) Reader software
Enter selection. (default 1) [1-6] : 1 INTERBASE MEDIA KIT LICENSE STATEMENT AND LIMITED WARRANTY
22
Chapter 2: Installation The license would be displayed here, but was removed for space limitations. Please select an option below to accept or decline the terms and conditions of the license 1. I Accept 2. I do not Accept Enter selection [1-2] : 1 License accepted Enter the absolute path name of the install directory [default: /opt] : Starting InterBase Client and Server RPM Install, please wait... The installation completed successfully 1. 2. 3. 4. 5. 6.
Install Install Install Install Install Exit
InterBase Client and Server software using RPM InterBase Documentation in PDF format InterClient JDBC software Easysoft ODBC driver software Adobe Acrobat(R) Reader software
Enter selection. (default 1) [1-6] : 6
Before running a test to verify the installation, the Interbase server needs to be started. Use the ibmgr application to start the Interbase server. See the example shown below. root@lnxbox:/opt/interbase/bin > ./ibmgr -start -forever server has been successfully started
Tip: If the message does not appear when starting the Interbase or ibmgr gives an error message when starting up that indicates the server could not be started, add an entry to the /etc/hosts.equiv file that gives permissions to localhost. The file should look like similar to this: # hostname localhost
Use caution when modifying this file. Adding additional entries can cause major security holes. Only the root user can modify this file. For RedHat 7.2 installations, the line “localhost.localdomain” should be added to the /etc/hosts.equiv file.
Chapter 2: Installation
23
Now verify that the installation is working property by executing the following: root@lnxbox:/opt/interbase/bin > ./isql /opt/interbase/examples/employee.gdb -u sysdba -p masterkey Database: /opt/interbase/examples/employee.gdb, User: sysdba SQL> select count(*) from employee; COUNT ============ 42 SQL>
Interbase has now been installed! The remaining sections include installation instructions for Kylix 1 owners who want to install other versions of Interbase.
Installing Interbase 5.6 - Kylix 1 Included on the Kylix 1 installation CD-ROM is a database called Interbase. The setup script is located in the interbase/linux directory. Run the setup script as the root user. If a previous version of Interbase is installed, consult the Install.txt file before continuing. Shown below is an installation session. The installation is straightforward. Notice that the installation uses the /opt directory. root@lnxbox:/cdrom/interbase/linux > ./setup 1. Install InterBase Client and Server software using RPM OR 2. Install InterBase Client and Server software using TAR 3. Install InterClient JDBC software 4. Install Adobe Acrobat(R) Reader software 5. Exit Enter selection. (default 1) [1-5] : 1 Starting InterBase Client and Server RPM Install, please wait... Enter the absolute path name of the install directory [/opt] : 1. Install InterBase Client and Server software using RPM OR 2. Install InterBase Client and Server software using TAR
24
Chapter 2: Installation
3. Install InterClient JDBC software 4. Install Adobe Acrobat(R) Reader software 5. Exit Enter selection. (default 1) [1-5] : 5 Quitting
After installing, a key needs to be added before using Interbase. Change to the /interbase/bin directory. Execute the following command: root@lnxbox:/opt/interbase/bin > ./iblicense -add -key eval -id eval The operation was completed successfully. Please restart the server for the changes to take effect.
Now verify that the installation is working property by executing the following: auser@lnxbox:/opt/interbase/bin > ./isql /opt/interbase/examples/employee.gdb -u sysdba -p masterkey Database: /opt/interbase/examples/employee.gdb, User: sysdba SQL> select count(*) from employee; COUNT =========== 42 SQL>
Interbase 5.6 is now installed; however, Kylix needs one additional step. Execute the following commands: auser@lnxbox:/ > rm /usr/lib/libgds.so auser@lnxbox:/ > ln -s /usr/lib/libgds.so.0 /usr/lib/libgds.so
For information about using Kylix and Interbase together, see Chapter 12.
Installing Interbase 6.0 - Kylix 1 Companion CD The companion CD includes Interbase SuperServer 6.0. The installation is found in the interbase_6 directory. Interbase SuperServer is installed by running the following command: root@lnxbox:/cdrom/interbase_6 > rpm -I InterBaseSS_LI-V6.0.1-1.i386.rpm
Before running a test to verify the installation, the Interbase server needs to be started. Use the ibmgr application to start the Interbase server, as shown on the following page.
Chapter 2: Installation
25
root@lnxbox:/opt/interbase/bin > ./ibmgr -start -forever server has been successfully started
Testing the installation is identical to the Interbase 5.6 example. Shown below is an example that demostrates if Interbase SuperServer 6.0 was installed properly. root@lnxbox:/opt/interbase/bin > ./isql /opt/interbase/examples/employee.gdb -u sysdba -p masterkey Database: /opt/interbase/examples/employee.gdb, User: sysdba SQL> select count(*) from employee; COUNT ============ 42 SQL>
Tip: If ibmgr gives an error message when starting up that indicates the server could not be started, add an entry to the /etc/hosts.equiv file that gives permissions to localhost. The file should look like similar to this: # hostname localhost
Use caution when modifying this file. Adding additional entries can cause major security holes. Only the root user can modify this file.
Tip: The downloaded version of Interbase 6.0 is installed with an evaluation license that limits the use of Interbase to 90 days. However, the version that is included on the companion CD has developer licenses for five users.
Installing Interbase 6.0 - Internet Download Another alternative is to download Interbase 6.0 from the Internet. Located at http://www.borland.com/interbase/downloads, this version will install on systems that do not have RPM installed. The file that was available for download at the time of this writing is IB601_linux_eval.tgz. Once Interbase has been downloaded, the files need to be extracted as shown below. root@linuxbox:~ > tar -xvzf IB601_linux_eval.tgz IB60_linux/ IB60_linux/Install.txt IB60_linux/IC20105LinuxJRE13.tar IB60_linux/License.txt IB60_linux/Readme.txt IB60_linux/ReleaseNotes.pdf
Chapter 2: Installation IB60_linux/InterBaseSS_LI-V6.0.1-1.i386.rpm IB60_linux/setup IB60_linux/odbc/ IB60_linux/odbc/unixODBC-2.0.5-1.i386.rpm IB60_linux/odbc/unixODBC-2.0.5-1.src.rpm IB60_linux/odbc/unixODBC-gui-qt-2.0.5-1.i386.rpm IB60_linux/odbc/interbase-odbc-1.1.57-1.i386.rpm IB60_linux/odbc/interbase-odbc-gui-qt-1.1.57-1.i386.rpm
After the files are extracted, run the setup script from the IB60_linux directory. If a previous version of Interbase is installed, consult the Install.txt file for further details. Run the setup script as the root user. Shown below is an installation session. It is similar to the Interbase 5.6 installation. root@yahwehlnx:~ > cd IB60_linux/ root@yahwehlnx:~/IB60_linux > ./setup Install InterBase Client and Server software using RPM Install InterClient JDBC software Install Easysoft ODBC driver software Exit
AM FL Y
1. 2. 3. 4.
Enter selection. (default 1) [1-4] : 1 INTERBASE MEDIA KIT LICENSE STATEMENT AND LIMITED WARRANTY
TE
26
The license would be displayed here, but was removed for space limitations. Upon your acceptance of the terms and conditions of this License Agreement, Please select an option below to accept or decline the terms and conditions of the license 1. I Accept 2. I do not Accept Enter selection [1-2] : 1 License accepted Enter the absolute path name of the install directory [default: /opt] : Starting InterBase Client and Server RPM Install, please wait... The installation completed successfully
Chapter 2: Installation
27
Adding 90 days evaluation license, please wait... The operation was completed successfully. Please restart the server for the changes to take effect. 1. 2. 3. 4.
Install InterBase Client and Server software using RPM Install InterClient JDBC software Install Easysoft ODBC driver software Exit
Enter selection. (default 1) [1-4] : 4
Notice that the installation automatically adds an evaluation license. Before running a test to verify the installation, the Interbase server needs to be started. Use the ibmgr application to start the Interbase server as shown below. root@lnxbox:/opt/interbase/bin > ./ibmgr -start -forever server has been successfully started
Testing the installation is identitical to the Interbase 5.6 example. Shown below is an example that demostrates if Interbase 6.0 was installed properly. root@lnxbox:/opt/interbase/bin > ./isql /opt/interbase/examples/employee.gdb -u sysdba -p masterkey Database: /opt/interbase/examples/employee.gdb, User: sysdba SQL> select count(*) from employee; COUNT ============ 42 SQL>
Tip: If ibmgr gives an error message when starting up that indicates the server could not be started, add an entry to the /etc/hosts.equiv file that gives permissions to localhost. The file should look like similar to this: # hostname localhost
Use caution when modifying this file. Adding additional entries can cause major security holes. Only the root user can modify this file.
28
Chapter 2: Installation
Tip: Interbase 6.0 requires the server to be running in order to access any databases. Automatically starting Interbase when Linux boots is accomplished by creating a shell script that can start and stop Interbase on demand. For more information, check out these Borland newsgroups: borland.public.interbase borland.public.interbase.linux borland.public.linux
Summary Installing Kylix is easy with a system that is up to date. For the supported systems that are not current, Borland has supplied patches to make the installation easier. When updating a Linux system, first check for updates with the Linux distribution vendor. Use these patches, as they are supported and the Borland supplied patches are not. Installing either version of Interbase is easy too. For more information on Kylix and Interbase, see Chapter 12. Now that Kylix is installed, get ready to write the next great Kylix application that sells millions!
Chapter 3
The Kylix IDE
Hello World Everyone knows that a developer’s guide just isn’t worth the paper it’s printed on without a Hello World program. It’s usually done at the beginning of the book and serves to show the reader that programming in the tool of choice is as easy as one, two, three. In this book, we feel like “Hello World” doesn’t do justice to what we are accomplishing. For Windows programmers, we are stepping out of the safe zone and into the eerie and uncharted territory of a whole new platform. For Linux developers, we are emerging from the jungle of command-line tools into the world of graphically based rapid application development. Either way, it’s a new and exciting universe, filled with potential. The Kylix IDE begins its career as the best development environment anywhere on Linux. Its Windows counterpart, Delphi, has years of experience and refinement that are transferred elegantly and robustly to the Linux platform. This chapter focuses on familiarizing the developer with the basic parts of the Kylix IDE and their purpose, use, and interaction with Kylix and the Linux environment.
The Kylix IDE from 10,000 Feet Before the specific elements of the Kylix IDE are discussed, it is a good idea to get a general picture of the Kylix IDE and of the basic elements used within it. With that in mind, the following describes what a user can expect to see in the IDE. While working with Kylix, developers deal with Kylix projects. “Project” does not necessarily refer to an application, but could also indicate a shared object (SO) or another non-executable type of project. Kylix projects are made up of Kylix units. A unit is a basic source code file, written in Object Pascal. It is easy to become overwhelmed with the rapid application development, GUI nature of Kylix and think that applications are made up of forms. Every piece of code in an application, however, is not necessarily form based, and in fact may not be a visual element of any kind. A Kylix application is comprised of a collection of units, some of which may contain code that declares and controls the behavior of a form.
29
30
Chapter 3: The Kylix IDE
When a developer is working with a form, that form will typically contain one or more components. A component (from the 10,000-foot level) is any element that can be dropped onto a form, such as an edit box or a button. Again, it is important to keep in mind that even though Kylix makes GUI development as easy as drag and drop, the real work is being done by the code editor, which adds the appropriate declarations and initialization code. Components on a form are made up of properties, methods, and events. The Kylix IDE also makes it easy to set values for the components’ persistent (design-time) properties and to create event handler code that is used to respond to an event in a component. The IDE does not include a visual tool for using component methods, because methods are typically coded directly into the Kylix code editor.
The Kylix IDE: A Closer Look Understanding the Kylix IDE (integrated development environment) is key to becoming proficient in the development of Kylix applications. For Delphi developers, using Kylix is virtually identical to using Delphi. For developers coming from a Linux background of command-line and text-editor programming environments, the Kylix IDE is a whole new way to develop sophisticated applications using rapid application development techniques and a rich set of reusable components. The Kylix IDE is primarily composed of five parts: the Speedbar, the Component Palette, the Forms Designer, the Object Inspector, and the Code Editor. This chapter describes each of these parts in detail and enables the developer to master the use of this powerful tool.
Speedbar Kylix’s title bar window includes the Kylix speedbar on its left side. It is essentially a toolbar that provides shortcuts to commonly used menu items. Following is a list of some of the more commonly used items on the speedbar.
Figure 3-1: The Kylix speedbar
Open/Reopen — The Open button is used like most software open buttons. It brings up the standard Open File dialog and allows the user to choose a file or a project to open. Notice the drop-down arrow next to the icon. Instead of launching the Open dialog, it displays a list of recently opened items. The list is split into two sections. At the top is a list of the last projects that were opened, with the most recently opened appearing at the top. The second section is a set of the last individual files that have been opened, with the most recent appearing at the top. These lists allow
Chapter 3: The Kylix IDE
31
the developer to quickly open commonly used projects and files without having to search the file system for them with the standard dialog. Save All — The File menu includes four different options for performing save operations in Kylix. Many beginners find themselves lost in choosing the right one to suit a particular occasion. No one wants to accidentally forget to save a piece of work. In addition, it is uncommon to want to save only a piece of a developer’s work. The Save All option saves everything that is currently opened (all of the files and projects). Once executed, the Save All icon will be grayed out. This is a sign to the developer that everything that has been done to this point is safely saved away. Certainly, the other save options have their uses (e.g., use Save As… to rename a file), but Save All is the best way to save all of your work in one click. Forms List/Units List — Selecting the Forms List button opens a dialog with a list of the forms in the current project. It is good practice to keep a tidy development desktop and to close any files that are not currently being edited. Instead of leaving every form in the project open, close forms when they are no longer being used and retrieve them later with the forms list. A common mistake is for beginners to open every file directly from the File|Open… menu item. Not only does that require navigating the file system to find a file, but there is a real potential that you could open the wrong file or the wrong version. You can imagine (or maybe remember!) the frustration of spending hours editing a file and then running the program to find the edits mysteriously missing, because you were editing a backup copy. All of this is said to demonstrate the importance and convenience of the forms list. When a form from the current project is needed, use the forms list to retrieve it.
Figure 3-2: The Forms list and Units list
Of course, not every file in a project is form based. It is very common for a unit of code to contain business logic, database code, or anything else that is nonvisual in nature. Nonvisual units can be found via the units list button (which is located right next to the forms list button.) The units list displays all units in the project as well as the main project file (.dpr). Project files can also be opened by selecting the Project|View Source menu item.
32
Chapter 3: The Kylix IDE
Toggle — The Toggle button is one of the most important buttons on the speedbar. It has a basic job, but it is used so frequently that it is imperative that you understand its use. The Toggle button’s job is to transfer the view of form-based units from the form designer to the code editor. In other words, if you are looking at a particular form, pressing the Toggle button will display the unit associated with that form. If you are viewing the code for a form-based unit, pressing the toggle button displays the form declared by that unit. Because of the amount of use the toggle button gets, it is a good idea to learn the shortcut key for executing it. The toggle function is executed by hitting the F12 key. Developers commonly need to view the form in which they are coding. Instead of stopping typing to move the mouse, click the Toggle button (or press F12), then click once more to start typing again. It sounds like a small perk but it is a great time-saver. Tip: If any of the function keys do not appear to work, the window manager probably has them assigned to other functionality. Pressing the scroll lock button will force the window manager to release the key from its bindings. Alternatively, modify bindings with the appropriate configuration utility. Run — The Run button executes the current project. Running a project could fail for two reasons. Either it is something other than a stand-alone application (like a shared object project without a host application) or it contains compile-time errors, in which case the application is not executed and the errors are displayed. Running the current application is a common task, and it is helpful to learn the run shortcut key, which is F9. Similarly to the Open button, there is a drop-down list of projects that can be executed. This is especially helpful when there are multiple projects open. For more on using multiple projects, see Chapter 4. There are a number of buttons on the speedbar that have not been discussed, and a much larger number that are not displayed by default on the speedbar. Spend some time customizing your speedbar to display those items you use the most (see the customization section below). Common additions to the speedbar include the Cut, Copy, and Paste buttons or the Search button. Following is a list of what we consider to be some useful additions to the default toolbar. Close All — Closing can be done from two different items in the File menu. It frequently causes problems for new Kylix users because they may be unsure what they have just closed. A common source of confusion comes from closing the editor window. Since Kylix uses a tab sheet style code editor for editing multiple files, closing the editor window makes it appear that everything has been closed. If the files are opened in the context of a project, this is not the case. Closing the editor window closes only the open units; it does not close the current project! To avoid the confusion of having to close all of the items individually, use the Close All button. Close All closes all open files as well as the current project. It gives the Kylix developer a fresh start from which to open or create a new project.
Chapter 3: The Kylix IDE
33
Compile/Build All — Adding Compile or Build All to the speedbar is a good idea, because of the frequency with which this command is used. Compile recompiles any files that have changed since the last run of the application. Build All, on the other hand, forces every file in the project to be recompiled regardless of whether they have changed. Some developers prefer to build the project from scratch every time (though this is not necessary). With other development tools, building a project can be a painfully long process, but with the speed of the Kylix compiler, even the largest projects can be built in a fraction of the time of other tools. Project Manager — The Project Manager button gives you a convenient way to bring up the Project Manager window. The Project Manager displays a list of forms and units for one or more projects, so the developer can easily navigate within a project or between projects. The Project Manager is discussed in detail in Chapter 4, “Working with Projects and Files.”
Customizing Your Speedbar Customizing the speedbar is a simple process. To begin, right-click anywhere in the speedbar area. A pop-up menu is displayed that contains a number of check boxes allowing the developer to select which toolbars are displayed in the speedbar. To create a customized speedbar, select the Customize… option. The speedbar editor is shown with three tabs. The Toolbars tab is essentially a re-creation of the pop-up menu and allows you to determine which toolbars are visible. Jumping to the third tab, labeled Options, two more check boxes are available that set options for the speedbar’s behavior within the IDE. Show tooltips toggles whether or not each button on the speedbar shows a tooltip with the name of the IDE operation when the mouse hovers over it. Show shortcut keys on tooltips actually changes the tooltip that will be shown. When it is checked, speedbar tooltips shows the name of the button and its shortcut key (if one exists). This is extremely helpful to developers who are new to the Kylix environment. The Commands tab of the speedbar editor lets the developer control exactly what is displayed on the speedbar. The left side lists each of Kylix’s main menu categories. When a category is selected, each of its items is displayed in the list box on the right. To add an item to the speedbar, simply drag that item up into the speedbar area. A dragging rectangle displays the insertion position of the item. When the mouse is released, the button is added to the speedbar and is ready to Figure 3-3: Customizing the speedbar use. To remove a speedbar item, simply drag it off of the speedbar while the
34
Chapter 3: The Kylix IDE
editor is open. Using this editor each time you install Kylix is a great way to place your most commonly used items within easy reach.
Component Palette To the right of the speedbar on the Kylix title bar is the component palette. The component palette is the main location for a developer to find components for use in a Kylix application. What’s a component? A component is a Kylix object that adheres to some specific principles with regard to its internal behavior and its external interface to other objects. To discuss components further, we would have to delve into the intricate world of object-oriented methodology (which is the primary purpose of Chapter 7). For now, let’s just say that a component is anything that can be dropped onto a form in a Kylix project (such as an edit box or a label). For a more in-depth discussion of components, see Chapter 16, “Writing Custom Components.” Figure 3-4: Kylix component palette
The component palette contains all of the Kylix components that are installed by default, as well as any third-party components and components that are written by the developer. The components are separated into logical groups (according to general usage and purpose) and organized into a set of tabs. An explanation of using and manipulating individual components on the palette is better addressed by example. For more information, see the form designer section later in this chapter. For now, we will limit our discussion to the navigation and customization of the component palette. There are a couple of circumstances under which all of the available components may not be visible to the developer. Especially if you are using a number of third-party components or personally created components or if you are working at a low resolution, you may not be able to see all of the tabs of the component palette at one time. To navigate to the unseen tabs, use the small arrows located next to the rightmost tab on the palette. Note that navigating the visible tabs does not change the currently selected tab. You must select a new tab in order to see its components. The second navigation technique revolves around individual tabs of the palette. If a particular tab contains a large number of components, you may not be able to see all of the components at one time. To navigate to unseen components on a tab, use the larger arrow buttons to the left and right of the first and last visible component on a tab. These arrow buttons will only be visible if the tab contains unseen components.
Chapter 3: The Kylix IDE
35
Customization The component palette, like most other parts of the Kylix IDE, can be customized for your convenience. Again, if you are using a large amount of third-party components, the tabs that are added to the component palette will most likely be added to the end of the set of tabs. To move them to the front (for easier access), right-click on the component palette and select Properties. The Palette Properties dialog allows the developer to: n
Rearrange the tab order
n
Rearrange component order (per tab)
n
Rename tabs
n
Hide unused components
Customization of these elements is achieved through drag and drop and through the use of buttons located at the bottom of the dialog. The component palette dialog can also be accessed through the Tools|Environment Options menu item. For more on environment options, see the Environment Options section, below.
Figure 3-5: Customizing the component palette
Object Inspector The Object Inspector is the primary location in the IDE from which to edit component property values, as well as to assign handler code to the various component events. Each component used in the designer displays a different set of properties and events; however, the process of editing them is consistent and simple.
Properties Tab Component Selection Box
The component selection box at the top of the Object Inspector specifies the name and type of the component that currently has the focus in the designer. The component selection box is a drop-down box that contains a list of every component on the form, including the form. As we will see later, there are times in which a component is completely covered by other components and cannot be focused by clicking on it directly. In such cases, simply select the component in the Object Inspector and the focus will be switched to that component.
Chapter 3: The Kylix IDE Property Types
The Object Inspector allows developers to quickly set component property values at design time. Because properties are of varying types, the Object Inspector provides a different way to edit different properties. Properties in the Object Inspector can be broken down into four types, according to the way they are set in the Object Inspector. Simple
Enumerated
AM FL Y
A simple property is any property whose value is directly entered into the Object Inspector. The properties do not necessarily have to be string types. They can be a number of types including character, integer, and floating point. Examples of simple properties include the text property of an edit box, the item index of a radio group, or the interval property (an integer which measures milliseconds) of a timer. When a simple type is changed in the Object Inspector, you must leave its editor by tabbing, hitting Enter, or changing the currently focused component in order for the changes to take effect.
Figure 3-6: The Properties tab
Enumerated types include any properties for which the developer must choose a particular value from a specified, finite list of values. Only one value can be chosen, and it must fall within the set of values predeclared by the type definition. Enumerated properties are typically set by selecting a value from a drop-down list in the Object Inspector. A good example of an enumerated property is the WindowState property of the TForm class. The WindowState property determines the visual state of the form when it first appears. WindowState is of type TWindowState, which includes the values wsMinimized, wsNormal, and wsMaximized. The WindowState property of a form can only be one of those three values. In addition, WindowState values are mutually exclusive. A form can only have one of those values at a time. Another good example of an enumerated property is any Boolean property. Such properties can only have one value and that value must be either true or false.
TE
36
Tip: When an enumerated property is being edited, the members of its drop-down editor can be traversed by double-clicking in the Object Inspector. Even though it involves more clicks, this can be a much faster way of getting to the correct value. This is most effective with Boolean properties. Simply double-clicking the property will flip its value between true and false.
Chapter 3: The Kylix IDE
37
Set
Set type properties are similar to enumerated properties in that the values of a set type property come from a predetermined and finite list of values. However, unlike enumerated properties, a set type property can contain any number of values from zero to the number of items defined by the specific set type being edited. Set types can be partially identified by the plus sign found to the left of the property name (though the plus sign does not necessarily indicate a set type). For more info, see the Sub-Properties section below. Another indication of a set type is that the list of values in the Object Inspector is surrounded by square brackets ([]). A good example of a set type property is the BorderIcons property of a form. The BorderIcons property is of the type TBorderIcon. The TBorderIcon type includes the values biSystemMenu, biMinimize, biMaximize, and biHelp. Collectively, they help to define the functionality of a form’s title bar. Any number of these values (including none) can be used in the BorderIcons property. Property Editor (Ellipsis)
Property Editor types are used for those properties that include a specialized editor in the form of a dialog that helps the developer to set a value for a particular property. When a property editor type is selected, an ellipsis button appears at the right side of the Object Inspector. Pressing this button brings up the property’s design-time editor and typically allows for a much more convenient way of setting a property’s value. A good example of the property editor type is the TImage component’s Picture property. Pressing the ellipsis button in the Object Inspector brings up the picture editor, which allows the developer to search the system for an image type to load into the image component. Sub-Properties
Both set types and property editor types will sometimes have a plus sign located to the left of the property name. Such a sign indicates the presence of sub-properties. That is, the property’s value is comprised of a collection of other values. Clicking the plus sign expands the property to show its sub-properties in the Object Inspector. For set types, sub-properties are most often implemented as a collection of boolean properties, indicating whether that value should be included in the set.
Events Tab In addition to the Properties tab, the Kylix Object Inspector includes an Events tab for easily creating event handlers from within the IDE. Each component has a different set of published events that provide developer hooks into the user’s interaction with the application. To respond to a particular event, a procedure is created and assigned to the particular events for which it is designated as a response. To create an event handler for a particular event, declare and implement a method that will be used to respond to the event and then assign that response to the event. EventHandler methods are normal Object Pascal procedures. There is
38
Chapter 3: The Kylix IDE
nothing special about such a procedure that makes it an event handler, save that it conforms to a particular method signature. The OnClick method of the TButton class is a type of event known as a TNotifyEvent. TNotifyEvents are procedures that take one parameter of type TObject. To create an event handler for a published event, simply double-click the event in the Object Inspector. The Kylix IDE creates a procedure that conforms to the type of event selected. The procedure is created as a method of the form class of which the button is a member.
Figure 3-7: The Events tab
type TfrmWelcome = class(TForm) btnWelcome: TButton; lblWelcomeMessage: TLabel; procedure btnWelcomeClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var frmWelcome: TfrmWelcome; implementation {$R *.xfm}
Chapter 3: The Kylix IDE
39
procedure TfrmWelcome.btnWelcomeClick(Sender: TObject); begin lblWelcomeMessage.Caption := 'Welcome to Kylix!'; end;
Once the procedure has been created, it is listed in the Object Inspector as the value for the particular event for which it was created. A reference to the procedure is internally stored by the component and is automatically invoked whenever the event occurs. Notice that the OnClick event editor in Figure 3-7 is actually a drop-down, combo box style. If the drop-down list is invoked instead of the developer double-clicking, it will show a list of the unit’s eligible methods for responding to the event. An eligible method is any one that follows the declared event signature. This is extremely convenient for creating common event handlers for a collection of events. To use an already implemented event handler, select a method from the event’s drop-down list instead of double-clicking.
Subcomponents Properties of a component often refer to other components. A form component, for instance, contains a menu property that refers to a main menu component. When a component is assigned a value that refers to another component, a plus sign (+) appears next to it in the Object Inspector. Clicking the plus sign expands the property to include the properties (or events) of the component that the property is referring to, as shown in Figure 3-8.
Figure 3-8: A menu component displayed from the Object Inspector’s view of a form.
40
Chapter 3: The Kylix IDE
Customization There are a number of customizations that can be made to the Object Inspector. Popular items include Stay on Top and Dockable, which allows the Object Inspector to be docked to other elements of the IDE. In addition, object properties have been gathered into logical groupings according to the component elements they control. Developers may choose to view or arrange properties according to the category to which they belong instead of the default alphabetical listing. Such an arrangement is shown in Figure 3-9.
Form Designer The form designer is responsible for the design-time development and manipulation of form-based units. It allows the developer to add visual and nonvisual components to a form in the Kylix designer and to set their persistent (design-time) properties.
Adding Components
Figure 3-9: Object Inspector customization
To add a component to a form, simply select that component in the component palette and then click anywhere on the form to drop the component at that location. Unlike some other environments, every visual component has a default size. In other words, you are not required to dynamically set a component’s boundaries by dragging it into a specific size. However, this option is also available at your discretion.
Adding Multiple Components
Figure 3-10: The Hello World form
It is very common to require several instances of the same component on a form. For example, a data input form is likely to require many different TEdit (or TDBEdit) components. One way to do this is to repeatedly go to the component palette and select the TEdit component and then drop it onto the form designer. However, not only is this time-consuming but inevitably you will eventually select the wrong component and will have to stop and delete it before you go on adding TEdits. Instead of this hassle, Kylix allows you to “lock in” the selection of the TEdit component so that you can add as many copies as you want at one time. To “lock in” the selection of a certain component, hold down the Shift key while you are selecting it in the component palette. You will know you have succeeded because the component’s border in the component palette will be a slightly different color, and because when you drop that component onto the form designer, the component palette will continue to show that component as currently selected in the
Chapter 3: The Kylix IDE
41
component palette. Once a component is permanently selected, you can simply click the form designer each time you want to drop a new instance of that component. When the desired number of components has been dropped onto the form, clear the component in the component palette by selecting the cursor button. The cursor button appears on the very left side of every tab on the palette. Once this button has been pressed, the component palette is back to a normal state and can be used as described above. Tip: If you are not satisfied with a component’s default size and you are going to use multiple instances of that component, it is much quicker to simply drop several copies of that component onto the form designer and then set all of their boundaries at the same time. Another quick solution is to drop one component, set the property values that the components will have in common, and then copy and paste that component several times!
Moving Components The Kylix form designer contains a grid of reference points that allows the developer to place components at specific points on the form. An environment option called Snap to Grid helps the user align components by automatically aligning new components with the edges of whatever set of four points the developer drops the component within. (For more on environment options, see the “Environment Options” section later in this chapter.) Moving components is a simple process of dragging a component to its desired location. As mentioned before, the Snap to Grid environment option will automatically set your component in alignment with the nearest set of grid points. To set a component’s position explicitly, simply set its Left and Top properties in the Object Inspector.
Grouping Components Setting the common properties of a group of similar components can be excruciatingly time-consuming. It is much more convenient to be able to set the value of a common property for a group of components at one time. To do this, you must first select all of the components simultaneously by putting them into a selection group. Accomplishing this task is very simple and can be done in two ways. The first method of selecting a group is to individually include them in a group. Once the initial component is selected, simply hold down the Shift key while you are selecting additional components. As long as you are holding the Shift key down, clicking a component will continually toggle its membership in the group. If a component is part of a group, its focus points will be gray instead of black. Another way to group components that are sitting on a form is to simply drag the mouse across them. As you drag the mouse across the form, a rectangle outline with a broken line edge expands in the direction of your drag. Any component that is touched or surrounded by the rectangle will be included in the group when the
42
Chapter 3: The Kylix IDE
mouse is released. This is very often a much faster way of selecting a group of components. The tricky part comes when a set of components is placed in a container. Suppose that there is a group of buttons set on a panel. Creating the drag rectangle around the panel will result in only the panel being selected. If you try to create the drag rectangle within the panel, Kylix thinks that you are trying to simply move the panel by dragging it to a new location. The solution is to create a localized drag so that the components on the panel are selected and not the panel itself. To do this, hold down the Ctrl key while you are dragging within the panel. This will create a drag rectangle that is localized to whatever container you are dragging. Tip: It’s important to be familiar with both grouping techniques. Even knowing about localized dragging does not allow you to use the drag in every situation. If, for instance, there is a group of labels that are aligned vertically, and you want to set the font of certain, more important, labels to bold, you must select the individual labels using the Shift-click technique. (However, if you are bolding the majority of the labels, it is perfectly valid to use the drag to select them all and then hold down the Shift key to unselect the labels that should not be included in the group.)
Setting Common Properties Whichever technique is used to select a group of components, they can now be treated a group. You can drag the entire group to a new location on the form as well as set the common properties all at one time. When more than one component is selected, the Object Inspector changes to reflect the new group. The component selection box no longer displays the name and type of the component because the selected group contains components of various names and possibly different types. The properties that are shown in the Object Inspector are those components that are in common between all components in the group. (This is not strictly true. Components have the ability to declare what properties will be available in the Object Inspector during a group selection. Certain properties, like the Name property, will not be displayed. The more similar a group of components are, the more properties will be displayed.) Setting a property in the Object Inspector while it is in this state will set the value for the specified property in each of the selected components. This is a very useful way to promote efficiency in the design process. If a certain section of a form (say a group of labels and edits) must be initially disabled, simply drag a rectangle around them and set the enabled property to false. Obviously, this is much faster than selecting each component individually and setting its enabled property.
Chapter 3: The Kylix IDE
43
Aligning/Sizing Components A common reason for selecting a group of components is to align or size them to be the same. This can certainly be done through the Object Inspector by setting their Top, Left, Height, and Width properties but can be accomplished more easily by using tools provided by the Kylix environment. To align a group of components, select them in a group, right-click over any member of the group, and select the Align… menu item. Select the appropriate options for both vertical and horizontal alignment. Select OK to close the dialog and update the position of the components. If the components have been selected by using the Shift key to individually group the components, they are aligned according to the first member of the group. If they are grouped by a drag operation, they are aligned according to the component creation Figure 3-11: Aligning grouped order, that is, the alignment will take place rel- controls ative to the first component dropped onto the form. After the components have been aligned, they continue to maintain their group status. They then can be moved as a group to the desired location. Notice that the components can also be spaced equally. If this option is selected, the components will be evenly spaced in the area between the first and last component, geographically speaking. In other words, a group of components that is vertically spaced equally will spread the components out between the vertical position of the topmost component and the bottommost component. To resize a group of components, create a component group, right-click and select Size… Most of the options in the Size dialog are relative to other components in the group. Both the width and the height of components can be sized to match the size of another component in the group. Size can also be explicitly specified by selecting the width and/or height options Figure 3-12: Sizing grouped controls and entering a specific size.
Code Editor In addition to the drag and drop, visual nature of the Kylix environment, a large emphasis is placed on textual development. Kylix is referred to as a two-way tool, allowing changes to be made to either the visual or the textual environment, with one having an effect on the other. Kylix gives the developer a lot of freedom with respect to the editing of project code. Unlike some tools that place proprietary tags into the code with menacing comments that say things like DON’T TOUCH THIS CODE, Kylix permits the user full access to a project’s source code. Anything that
44
Chapter 3: The Kylix IDE
the visual environment creates or manipulates can be altered by simply changing the project’s code in the Kylix code editor. The Kylix code editor window is split into three sections: the code editor, the code explorer, and the message view. Kylix source code is just text and can be edited in any text editor, but the Kylix code editor makes code editing easier by providing more readable, formatted text and by supplying navigational tools and keystrokes to increase the speed of development.
Figure 3-13: The Kylix code editor
Editor The Kylix code editor is one of the most important parts of the IDE, given the amount of time that a developer spends interacting with it. In addition to a basic text editor, the Kylix editor provides a rich font editing environment and a number of editing tools that make development both faster and easier.
Code Browser Another great feature of the editor is the code browser. Place the mouse over any class or identifier while holding down the Control key. The identifier is highlighted and underlined (like a URL link). Click on the link and the Kylix editor will automatically open the unit in which that identifier is declared, at the spot at which its declaration begins. This can also be accomplished by right-clicking on any identifier and selecting Find Declaration from the code editor context menu.
Chapter 3: The Kylix IDE
45
Module Navigation In a large unit, navigating to various pieces of code can be a trying task. Scrolling past code is an inaccurate way to find a particular section. Module navigation allows developers to quickly find code. To quickly find a method implementation, place the cursor on the method prototype, listed within the class declaration. Press Ctrl+Shift+Up Arrow or Ctrl+Shift+Down Arrow to jump from the prototype of a method to its implementation. The same key combination will take you from the implementation of a method back to its declaration. In addition, up to ten locations per source code file can be bookmarked for faster access. To set a bookmark, place the cursor on the desired line of code. Right-click and select Toggle Bookmarks to turn a particular bookmark on or off. Then, to return to the location of that bookmark, right-click again and select Goto Bookmarks, choosing the appropriate bookmark. Returning to bookmarks can also be accomplished by pressing Ctrl+.
Class Completion In a visual environment, a large amount of code is written automatically by the Kylix editor. Event handlers and their prototypes, for instance, are generated when a particular event is double-clicked in the Object Inspector. However, it is necessary much of the time to write class methods that are unrelated to any part of the user interface. When such a method is declared, it must then be written out in the implementation section. This causes a lot of time to be wasted by the developer, writing implementation skeletons for the methods declared by the class. Imagine an alternate implementation of the Hello World program. unit HelloWorld; interface type THelloWorld = class public property CurrentLanguage:String; procedure SayHello; procedure SayWelcome; end; implementation end.
The THelloWorld class includes a collection of properties and methods that provide the implementation of the application. These properties and methods are referenced from the event handlers of the main HelloWorld form. Because these elements are not visual in and of themselves, it would be up to the developer to
Chapter 3: The Kylix IDE
create implementation and support code for their use. Instead, Kylix allows the developer to automatically complete class elements by right-clicking and selecting Complete class at cursor (Shift+Ctrl+C). Kylix will complete the code, not only by providing implementations for the methods but also by finishing the implementation of the property, including the implementation of the property’s accessor methods (for more on properties, see Chapter 16, “Writing Custom Components”). Tip: In order for Kylix to be able to complete property definitions, be sure that Finish Incomplete Properties is selected in Tools|Environment Options. unit HelloWorld;
AM FL Y
interface type THelloWorld = class private FCurrentLanguage: String; procedure SetCurrentLanguage(const Value: String); public property CurrentLanguage:String read FCurrentLanguage write SetCurrentLanguage; procedure SayHello; procedure SayWelcome; end; implementation { THelloWorld }
TE
46
procedure THelloWorld.SayHello; begin end; procedure THelloWorld.SayWelcome; begin end; procedure THelloWorld.SetCurrentLanguage(const Value: String); begin FCurrentLanguage := Value; end; end.
Chapter 3: The Kylix IDE
47
In addition to its great editing capabilities, the editor contains a context menu (right-click menu) with a variety of additional code writing and navigation abilities. The context menu items cover a wide range of topics, some of which are covered in the applicable sections of this book. Following is a summary of the tools available through the context menu.
Figure 3-14: The Editor context menu
Table 3-1 Context Menu Option Find Declaration
Description
This item allows the developer to quickly find source code for a used object. For instance, if a class is using a TButton object and the developer needs to see the source code for the TButton class, he or she can simply right-click on the desired item and select Find Declaration. Kylix will open the source code file that contains the declaration of the target object at the location of its declaration. Close Page Allows the developer to close a single open page without closing the entire editor window. Open File at Cursor Opens the file at the location of the cursor. This is especially useful for navigating into the units that a particular code file is using. For more on using units, see Chapter 6. New Edit Window Opens the currently focused file in a new window. Edits to one window are duplicated in the other (they are actually views of the same file). This is useful for comparing/viewing two sections of a large file and eliminates the need to scroll back and forth within a particular file. Browse Symbol at Cursor Opens the symbol explorer. For more on the symbol explorer and the project browser, see Chapter 4. Topic Search Opens contextual help on the element at the cursor’s location. This functionality can also be executed by pressing the F1 key. Complete Class at Creates implementation skeletons and certain interface Cursor elements for partially declared properties and methods. More on this can be found in Chapter 7. Add To-Do Item Allows the developer to add To-Do items to the current project. For more on To-Do lists, see Chapter 4.
48
Chapter 3: The Kylix IDE Context Menu Option
Description
Cut, Copy, Paste
Performs standard Cut, Copy, and Paste operations on text in the code editor. Allows the developer to bookmark the editor in ten different locations per file. This makes returning to important lines of code faster and easier. Returns to the line number of the bookmark chosen (1–10). Provides quick access to debugging options. For more on debugging, see Chapter 11. Forms can be viewed visually or as text. If a form has been viewed as text, this option toggles the developer’s view back to a visual mode. For more on viewing forms, see Chapter 4. Toggles the editing status of a file. Toggles the visibility of the message window in the code editor. Toggles the visibility of the code explorer in the code editor.
Toggle Bookmarks
Goto Bookmarks Debug View as Form
Read Only Message View View Explorer
Code Explorer The code explorer gives the developer a quick list of elements contained in the currently displayed source code file. It is typically docked at the left side of the code editor but can be moved to a number of locations as well as being used independently of any other window.
Figure 3-15: Code explorer icons
When an item is selected in the code explorer, the editor jumps to its implementation in the source code file. The code explorer supports incremental searching, which is helpful in units that declare a large number of classes or other objects. Simply type in the name of the class and the explorer will jump to that location. A number of options can be set that determine how the explorer displays its data. For more on these options, see the “Explorer” section below.
Chapter 3: The Kylix IDE
49
Controlling Your Environment One of the best things about Kylix is that developers can create sophisticated applications in a fraction of the time required by command-line and text-only development. The Kylix environment options allow developers to customize the Kylix environment in a way that suits their development preferences. In older versions of Kylix for Windows (Delphi), environment options were all grouped together under one menu item. For this reason environment options are often spoken of as one group of options, when really they are split into three groups: environment options, editor options, and debugger options.
Environment Options Preferences The Preferences tab contains settings that affect some of the general functionality of the IDE as well as some specific behavior of the form designer. Taking the time to set these properties greatly increases a developer’s efficiency and can greatly decrease his frustration during development. Autosave Options
The Autosave options box contains two elements that deal with saving Figure 3-16: Preferences options items during development. Editor files automatically saves all modified files whenever the application is run and when you exit Kylix. The Project Desktop option saves the current state of the Kylix desktop whenever a project is closed or you exit Kylix. The next time the project is opened, all files that were open when the project was last closed will be reopened, including non-project related files. Tip: While the Editor files option may seem pretty slick, it is our opinion that it causes more trouble than it’s worth. Developers commonly solve problems in coding by writing a throwaway program designed to test a piece of code or a certain technique. Once the technique has been demonstrated correctly, the code is used in the main application and the test app is thrown away. When Editor files is on, Kylix will try to save the trash program files when the test application is run. Because the trash application contains new files, saving them will bring up the Save dialog. In other words, you have to save trash just to test a piece of code that will eventually be thrown away!
50
Chapter 3: The Kylix IDE Desktop Contents
Desktop contents consists of two options that help to determine exactly what information is stored when the Project Desktop AutoSave option is enabled. If Desktop only is selected, all open files, directory information, and editor windows that are open are stored and reopened the next time the project is opened. If Desktop and Symbols is selected, the IDE also stores symbol information for the project from the last time it was compiled successfully. Form Designer
The Form designer options allow the user to control the IDE’s interaction with forms. It includes a number of options that make using forms in the designer a lot easier. Table 3-2 Form Designer Option
Description
Default
Display grid
Displays a grid of visible points on the form to assist developers in placing and aligning/sizing controls. When a component is dropped onto a form, it is automatically aligned with the nearest gridline. This helps to automatically align components without the developer having to be accurate with the drop to the pixel. Provides a caption for nonvisual components such as ImageLists, queries, and menus that appears beneath the component in the designer. When a form is saved by Kylix, its information is written to a .dfm file in either binary or text format. Binary formats were used with earlier versions of Delphi, while text formats are easier to modify and read. Sets the default creation mechanism for new forms in the project. When this option is unchecked, new forms are placed in the source code such that each form is automatically created by the application upon startup. It is not typical for an application to create every form on startup, however there are certainly times when more than one form must be created automatically. Determines the distance in pixels between points on the grid. Change these values to increase or decrease the size of grid cells on a form.
On
Snap to grid
Show component captions New forms as text
Auto create forms & data modules
Grid size X / Grid size Y
On
Off
On
On
8, 8
Chapter 3: The Kylix IDE
51
Shared Repository
Kylix stores a number of premade forms, projects, and code snippets that can be accessed easily through the object repository. In addition to the premade code, developers can add their own forms and projects for future use. In a departmental environment, developers may want to share repository objects with others on the project. To share a repository of objects, set the Shared repository directory to the directory location of the repository objects. Once the shared repository directory has been Figure 3-17: Setting the shared set, choosing File|New… from the Kylix menu repository will bring up the object repository with the objects from that directory.
Object Inspector The Object Inspector tab contains a number of settings that control the display of property and event information in the Object Inspector. In addition to settings that control what information is displayed, the Object Inspector tab also allows developers to create a color scheme for items in the Object Inspector. Each type listed in the Colors list box can be assigned a different color for easier identification by the developer.
Figure 3-18: Object Inspector
Library The Library tab contains options that set Kylix options regarding the placement of all package files as well as the location in which the compiler should search for packages. A package is a collection of files used by an application or by the Kylix IDE. Set these options to explicitly set paths for various package files. For more on packages, see Chapter 16, “Writing Custom Components.”
Figure 3-19: Library options
52
Chapter 3: The Kylix IDE Table 3-3 Library Option
Description
Library path
Sets the directories in which the compiler will search for package source code. If the package uses any file that is not found in the search path, a compiler error will be thrown. Kylix defaults this option to search in the /bin and the /lib directories. Most of the default packages that are distributed with Kylix are located in /bin. Compiled packages are given an .so extension and serve a similar function to Windows DLLs. BPL Output directory specifies the directory in which compiled package files are placed. DCP files are binary images that contain header information as well as the complete set of DPU files (compiled source code files) that are contained by the package. Tells the project browser where to find source code files when they are not found in the project path. For more on the Project Browser see Chapter 4, “Working with Kylix Projects and Files.”
BPL output directory
DCP output directory
Browsing Path
Palette The Palette tab controls the appearance and configuration of the component palette. It allows a developer to decide on the order of components and tabs within the component palette. For more information on customizing the component palette, see the “Component Palette” section earlier in this chapter.
Figure 3-20: Palette options
Chapter 3: The Kylix IDE
53
Explorer The Explorer options help to determine what information is available in the code explorer during development. The left side contains a number of individual options that determine the amount and format of the information supplied in the designer.
Figure 3-21: Code explorer options
Table 3-4 Code Explorer Option
Description
Default
Automatically show Explorer Highlight incomplete class items Show declaration syntax
Determines whether the explorer is automatically displayed for each unit that is opened. Properties and methods that are not completely defined are shown in bold. The name and type of properties is displayed in the explorer (instead of just the names). Methods are displayed with their entire signature. Determines whether elements are sorted according to their names or their scope. The class completion option helps to determine what work the Complete Class at Cursor option will do. If this is unchecked, incomplete properties will not be completed. Determines the order in which classes, units, and globals are displayed in the explorer. Determines what symbols are displayed. With Project symbols only, only symbols from units in the current project are displayed. With All Symbols, symbols from every unit used by the project (both directly and indirectly) are shown.
On
Explorer sorting Class completion option
Initial browser view Browser scope
On Off
Alphabetical On
Classes Project symbols only
The right side of the Explorer tab shows a list of element groups as a collection of check boxes. Each element that is checked will be shown in the code explorer in a dedicated category (folder). When an element can appear in more than one folder, the elements in bold take precedence. Note the small icon between the check box and the element name. This icon represents a collapsed treeview. When the icon is clicked, it changes to an expanded treeview. Any element that shows an expanded treeview beside it will be automatically expanded by the explorer. This saves the
54
Chapter 3: The Kylix IDE
time of having to navigate through the explorer. If, for instance a component developer is extending existing components, he may want to auto-expand the protected section of each class. Selecting the icon beside the protected element accomplishes this goal (as long as the protected element is checked).
Environment Variables Environment variables can be changed from within the IDE. Changes made here affect the editor, designer, debugger, and compiler. In addition, these are the environment variables that are inherited by running applications within the debugger. The Environment Variables tab is comprised of two sections. Notice in Figure 3-22, that they are System variables and User overrides. System variables refer to the list of environment variables that had variables when Kylix was started. User overrides are either changes to existing environment variables or new enviFigure 3-22: Environment Variables options ronment variables. Overriding a variable is accomplished by selecting the environment variable and pressing the Add Override button. Another dialog will appear and display the variable name and the current value. Change the value appropriately and press the OK button. Tip: A common variable to override is LD_LIBRARY_PATH by adding the current directory which is denoted by a single period. In Chapter 7, the reasons for this will become apparent. Add a new variable by pressing the New… button and entering the name of the variable and the value. Tip: By convention, all environment variables in Linux are uppercase. They are delimited with the colon (:).
Chapter 3: The Kylix IDE
55
Internet The Internet tab displays options that set preferences for use in WebSnap applications. (For more on WebSnap, see Chapters 22 and 23.) It contains a collection of Internet file types that WebSnap uses to generate files. Each file type can be associated with particular file type extensions. In addition, the file listed in the Sample Image File text box is used by WebSnap to display a default image in the designer when the correct image is not available. Figure 3-23: The Internet tab
Editor Options Editor Options allow the developer to specify options that are directly related to the presentation and manipulation of the code editor. Spending time setting up a convenient environment is a good way to increase efficiency in development and decrease frustration caused by the time wasted in the formatting of code.
General The General tab specifies the general editing behavior of the code editor.
Figure 3-24: General options
56
Chapter 3: The Kylix IDE
Table 3-5 Description
Default
Auto indent mode
When the Enter key is pressed in the editor, the cursor is placed under the first character of the preceding line instead of at the beginning of the line. This is very helpful in keeping a standard of indenting for code of different levels. Functions similarly to the Ins key in other editors. When unchecked, code is written right over the top of existing text. Inserts a tab character. If Smart tab is enabled, this option is off. Tabs to the position of the first character of the preceding line. If Use tab character is turned on, this option is off. Begins each auto-indented line with a minimum number of characters. Aligns text to the previous level of indentation when the Backspace key is pressed. Allows the developer to use the arrow keys to tab around the editor. Undoes all editing commands of the same type as the last command. Places the cursor beyond the EOF marker. Tracks changes even when a save operation has been performed. Saves blank characters, if they exist, at the end of each line. Makes use of BRIEF regular expressions. For more on BRIEF regular expressions, see the Kylix help. When a block of text is selected, it remains selected even if the cursor is moved, until a new block is selected. When a block of text is selected, typing any new text will replace the entire block with what is entered. Selects the entire line of code when it is double-clicked in the editor. When this option is off, only the word under the cursor is selected. When the Search|Find menu item is selected, the text at the cursor’s position is automatically entered into the TextToFind box of the search dialog. The Edit|Cut and Edit|Copy menu items are enabled, even if there is no block of text selected. Enables syntax highlighting within the editor. For more on syntax highlighting, see the Color page of the Editor Properties dialog.
On
Use tab character Smart tab Optimal fill Backspace unindents Cursor through tabs Group undo Cursor beyond EOF Undo after save Keep trailing blanks BRIEF regular expressions Persistent blocks Overwrite blocks Double click line Find text at cursor
Force cut and copy enabled Use syntax highlight
TE
Insert mode
AM FL Y
Editor Option
On Off Off Off On On On Off Off Off Off Off On Off On
Off On
57
Chapter 3: The Kylix IDE
Display The Display page sets display options for the code editor, as well as the default font that is used.
Figure 3-25: Display options
Table 3-6 Display Option
Description
Default
Brief cursor shapes Create backup file
Uses BRIEF cursor shapes. Backs up files during a save operation by creating another file with a tilde (~) at the beginning of the file extension. Preserves the end of line position. Normally, when the code editor is maximized, it fills the entire screen except for the area occupied by the Kylix title bar (the speedbar and the component palette). When this option is turned on, the editor can be maximized to fill the entire screen. Displays a continuous vertical line at the right margin of the code editor, signifying the end of the printable page. Displays the gutter area on the left side of the code editor. The gutter is used to help display breakpoint and bookmark locations in the editor. Sets the right margin in the code editor. Valid values are between 0 and 1024. Sets the width of the code editor gutter. Selects the font type used by the editor. A sample of code in this font can be found in the sample box at the bottom of the dialog. Sets the font size used by the code editor. A sample of code in this size can be found in the sample box at the bottom of the dialog.
Off On
Preserve line ends Zoom to full screen
Visible right margin Visible gutter
Right margin Gutter width Editor font Size
On Off
On On
80 30 Courier 11
58
Chapter 3: The Kylix IDE
Key Mappings The Key Mappings tab sets the default keystroke mappings for the Kylix editor. It sets the shortcut keys for a collection of Kylix designer operations. Each key mapping module emulates a particular key configuration. The Enhancement modules list box can contain special packages that contain custom key bindings, developed using the OpenTools API. The OpenTools API is a set of objects that allows the developer to tap into and interact with the Kylix editor and is often used to create Kylix experts that provide custom functionality in the designer. The Figure 3-26: Key Mappings options OpenTools API is beyond the scope of this book, but more information can be found in the Kylix help.
Color The Color tab allows the developer to create a custom color scheme for the code editor. The list box on the left side contains a list of various elements found in the editor. Everything from reserved words to string literals to assembler code can be assigned a custom color scheme and style. The bottom of the dialog contains a scrollable region that shows the developer an example of each element type and how it will be displayed in the code editor. The Color SpeedSetting drop-down contains a number of premade color palette settings that Figure 3-27: Color options reflect the default color schemes of various development environments.
Chapter 3: The Kylix IDE
59
Code Insight Code Completion
The Code Completion feature assists the developer by providing a list of available coding choices for the developer to select from during the writing of application code. When any object name is typed into the code editor and the period (.) is pressed, a window pops up at the cursor location with a list of that object’s properties, methods, and events. The code completion window is color-coded by the object’s element type and can be resized to show more or fewer elements. The element list can be sorted alphabetically or by the visibility of the Figure 3-28: Code Insight options elements by right-clicking and selecting the appropriate option. The developer can select an item from the list or can continue to type while the list is incrementally searched by Kylix. At any time, the developer can press Enter or the Spacebar to choose the currently selected item from the list. Code completion is a great time-saver during development. Especially if you program in more than one language, it can be confusing to have to remember whether the button component’s display property is called caption or text or label. It is called one thing in Java and another in Pascal and another in C++. Code completion avoids the delay of going to the help or into the source code of the component to determine the name of a property or method. Also, if the component you are using is not a standard Kylix component but was developed by someone else in your company or by a third-party vendor, you would have to search their documentation or the source to determine what the component could do.
Figure 3-29: Code completion
60
Chapter 3: The Kylix IDE Code Parameters
Just like code completion gives the developer information about an object, code parameters gives the developer information about a particular method. If you have not used an object before, you are probably unfamiliar with the methods that it contains. Even if you do know the name of the method, it is sometimes difficult to remember the order of the parameters that the method takes. As we will see in Chapter 5, sometimes an object contains multiple versions of the same method. The only distinguishing characteristic of the various versions is the parameters that they take. When a developer types in a call to a particular method and presses the open parenthesis ( ( ) key, code parameters shows a list of all of the method’s parameters and their types. If an object contains several versions of a method, code parameters shows a list of all available parameter lists for that method. As the developer enters parameter values, code insight will delete from the list any parameter sets that are no longer valid. That is, once a parameter is entered that does not match one of the parameter sets, that set is removed from the list. So a developer always knows what sets of parameters he or she can enter, based on what has already been entered.
Figure 3-30: Code parameters
Tooltip Expression Evaluation and Symbol Insight
Tooltip expression evaluation is used to make debugging easier by providing tooltip information on the values of variables. Simply hold the cursor over a variable and its value is displayed for you. Debugging is outside the scope of this chapter, but more information can be found in Chapter 11. Tooltip Symbol Insight uses tooltips to display declaration information for any identifier when the cursor is placed over it in the code editor.
Chapter 3: The Kylix IDE
61
Code Templates
Code templates are a great way to store premade pieces of code that can be accessed and inserted easily into code. Especially for those developers who are new to Object Pascal, code templates are a great way to make coding easier by using code skeletons that conform to Pascal syntax specifications. Even after developers are comfortable with the Object Pascal language, code templates can be used to store commonly used code snippets that will save the developer the time of typing them from scratch. Each code template consists of a name, a description, and a piece of code. Once a code template has been added into Kylix, it can be retrieved and used with a few simple keystrokes. To access a code template, use the keystroke Ctrl+J. Locate the correct template by typing in the first few characters of the template’s nickname (which is generally the same as the first few characters of the Pascal code). Selecting a particular template inserts the code into the code editor at whatever location the cursor occupies. Suppose, for instance, that you are new to the language but an experienced programmer. You may have worked with a case statement before, but are unfamiliar with the Pascal syntax for it. Simply type in the word case and hit Ctrl+J. Kylix brings up a list of all the known templates that begin with that word. Notice that there are multiple templates for the same Pascal statement.
Figure 3-31: Using code templates
When a particular template is selected, Kylix inserts a code skeleton that conforms to the language’s rules. Once the code is inserted, all that remains is to fill in the missing values and the code is complete. The case statement (with else) code looks like the following: case | of : ; : ; else ; end;
Even for an experienced Pascal programmer, code templates provide great benefit. Any piece of code requiring several lines of typing can be entered as a code template to be retrieved quickly at a later time. Examples might include connecting to a
62
Chapter 3: The Kylix IDE
file stream, declaring an often-used custom array, or launching a form modally. Once a template has been entered, its code can be inserted quickly and with the ultimate precision.
Debugger Options Kylix debugger options allow the developer to specify the details of how the debugger interacts with the IDE. Kylix uses an integrated debugger by default, so every time an application is run, the debugger is run along with it. Debugging options are beyond the scope of this chapter and are covered in Chapter 11.
Summary The Kylix IDE provides developers with a fast and simple way to create powerful applications with a convenient drag and drop interface and a wide variety of functionalities that make everything from GUI design to hand coding easy and efficient. In addition, it provides a number of environment and editor settings that allow the developer to customize the Kylix environment to suit individual needs. Once the IDE has been mastered, developers can create robust applications in a fraction of the time that it takes with traditional Linux development tools.
Chapter 4
Working with Projects and Files
Introduction A Kylix project is a group of files that collectively provide some executable or otherwise useful functionality. Projects and applications are not the same thing. An application is a executable piece of compiled code, while a project is any set of Kylix source code. Projects can be anything from a GUI-based client/server application to an SO library to a web application (Apache Shared Module). A project is simply a logical collection of source code files and a project file that ties them together.
Project Files The Project (.dpr) File The .dpr file is responsible for keeping track of the location of source code files associated with the project and for executing the main program code. “dpr” originally stood for Delphi project file. Although Kylix project files could conceivably be called “kpr” files, Borland chose not to make the change. Because Kylix projects (using CLX) are designed to be cross-platform, they must be able to be loaded into the Delphi environment, hence the value of maintaining the original extension. Project files are typically very small because the functionality of the forms and other classes in the project is declared and implemented within the units in which those objects are declared. A typical project file is shown below. program OrderEntry; uses Forms, FormCustomer in 'FormCustomer.pas' {frmCustomer}, FormOrders in 'FormOrders.pas' {frmOrders}, FormInventory in 'FormInventory.pas' {frmInventory}, FormParts in 'FormParts.pas' {frmParts}, FormOrderEntryMain in 'FormOrderEntryMain.pas', {frmOrderEntryMain};
65
Chapter 4: Working with Projects and Files CustomerRules in 'BusinessObjects/CutomerRules.pas'; {$R *.RES} begin Application.Initialize; Application.CreateForm(TfrmOrderEntryMain, frmOrderEntryMain); Application.Run; end.
AM FL Y
The program keyword is used to denote that this project is an application. If it were instead an SO project, the keyword library would replace the word program. The uses clause of the project file contains a list of all of the files the project is aware of. Whenever the project is compiled, each of the files listed in the uses clause is linked into the executable. If any of the units include an initialization section, that code is run before the Application.Initialize routine in order of the listed units. Similarly, when the application terminates, every unit listed that has a finalization section is executed in reverse order of how they are listed. For more on the initialization and finalization sections of units, see Chapter 6, “Application Architecture.” If a particular file in the uses clause contains the declaration of a form, the form’s name is listed in a comment following the name of the file. In the example shown above, most of the files are saved in the same location as the .dpr file. The CustomerRules object, however, is found in a subdirectory of the application directory. Because the CustomerRules.pas file is not in the application directory, the path (both relative and absolute paths are acceptable) to the file is listed instead of just the name of the file.
TE
66
OS Note: The names of the files used by the project are case sensitive. If, for some reason, one of the .pas files was saved with a different case, the project would no longer be able to locate the file and an error would result. Just after the uses clause of the project file is a compiler directive {$R *.res}. This tells the compiler to link into the application a file of the same name as the project, but with a .res extension. The resource file contains resources for the project such as the project icon and any other allocated resources. The project file also contains the central execution point of the application. Kylix applications are run by the global application object. The application object is responsible for interacting with the operating system to launch and initialize processes. For more on the application object, see the “Using the Application Object” section later in this chapter. The application object provides a context in which the elements of an application run, as well as a central point through which to receive messages that apply to the application as a whole such as the OnActivate event (which occurs whenever the application gains the focus) and the OnIdle event (which allows the application to perform background operations when the application is not busy). Typically, the
Chapter 4: Working with Projects and Files
67
application object will simply create the main form of the app (if it is a GUI app) and then run. The logic for creating other forms and for providing functionality to the main form is not commonly contained in the project file; rather it is internal to the TFrmOrderEntryMain class.
The Form (.xfm) File GUI applications typically contain a good number of form-based units. Forms, in turn, usually contain controls of all types. Chapter 3 demonstrated the use of controls and various techniques for setting (among other things) their properties through the Object Inspector. When a form’s unit is saved, the property values of those controls must be stored as well as the form’s code. Instead of storing control property values in the source code file, Kylix writes them out to a form file that has the same name as the unit file and an .xfm extension. A typical form file is listed below (some components have been left out for brevity). Warning: The .xfm file is created and maintained by the Kylix environment. When changes to a form are saved, the .xfm file is updated with the necessary items. This file should not be edited directly. object frmCustomers: TfrmCustomers Left = 358 Top = 214 Width = 391 Height = 283 Caption = 'Customers' Color = clBtnFace Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = 'MS Sans Serif' Font.Style = [] OldCreateOrder = False PixelsPerInch = 96 TextHeight = 13 object spltrVerticalMain: TSplitter Left = 209 Top = 0 Width = 8 Height = 256 Cursor = crHSplit Beveled = True end object lstbxCustomerList: TListBox Left = 217
68
Chapter 4: Working with Projects and Files Top = 0 Width = 166 Height = 256 Align = alClient ItemHeight = 13 Items.Strings = ( 'PILLAR Technology Group, Inc.' 'Borland Corporation' 'Red Hat Linux') TabOrder = 0 end object pnlCustomerDetail: TPanel Left = 0 Top = 0 Width = 209 Height = 256 Align = alLeft TabOrder = 1 object lblCustomerName: TLabel Left = 8 Top = 8 Width = 75 Height = 13 Caption = 'Customer Name' end object lblContactName: TLabel Left = 8 Top = 56 Width = 68 Height = 13 Caption = 'Contact Name' end object lblAddress: TLabel Left = 8 Top = 104 Width = 38 Height = 13 Caption = 'Address' end object edtCustomerName: TEdit Left = 8 Top = 24 Width = 193 Height = 21 ReadOnly = True TabOrder = 0
Chapter 4: Working with Projects and Files
69
end object edtContactName: TEdit Left = 8 Top = 72 Width = 193 Height = 21 TabOrder = 1 end object edtAddress: TEdit Left = 8 Top = 120 Width = 193 Height = 21 TabOrder = 2 end end end
Notice that the form file stores values for every component on the form, including the form itself. Each control is identified by the object tag and is followed by a collection of values. The end keyword signifies the end of a control’s listing. Also notice that the listings for each control are not complete, that is, the .xfm file does not store every property of every control. It turns out that when a control declares a property, it can also declare a default value for that property. When a form is being written out to the .xfm, it uses the following pseudo-logic: If the value of property x is different from the property’s default value, write it out to the form file. When a form’s values are streamed back in, if a particular property is not listed in the .xfm for a certain control, Kylix uses the property’s default value, as declared by the component that the property is a member of. Take, for example, the ReadOnly property of the TEdit control. The edtCustomer control lists the ReadOnly property as true, while the other edit controls do not mention it at all. edtCustomer stores the value of ReadOnly because it is different from the default value of the ReadOnly property (which is false) declared in the TEdit class. This mechanism keeps .xfm files from becoming outrageously large. (Imagine if every property of every control on the form was stored!) Tip: To view a form file, right-click on any form and select View as Text. Kylix will open up the .xfm file for the form in the editor. There is a New forms as text environment option on the Preferences tab that determines the format of the .xfm file. If it is checked, the .xfm file has a text format. If not, the .xfm is stored as a binary file. Regardless of this setting, selecting View as Text from the form’s context menu will display the information as text.
70
Chapter 4: Working with Projects and Files
The Pascal (.pas) File Kylix projects are primarily composed of source code files, called units. Units have a .pas extension and contain almost all of the code in a Kylix project. Each unit used by a project is linked into the executable file through its listing in the main project file (.dpr). Although units commonly contain form declarations and implementations, they frequently contain business objects, utility code, and many other program elements. unit Unit1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs; type TForm1 = class(TForm) private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} end.
The code above displays a basic form unit. (Of course, not every unit is visual in nature.) This particular unit declares a form class along with a variable declaration. The unit file is the center of all Kylix coding and can hold very large amounts of code. Chapter 6 examines units and their structure in more detail.
Chapter 4: Working with Projects and Files
71
Generated Files In addition to the three files mentioned above, applications generate (upon compilation) a number of other files that assist Kylix in storing projects and other items. Following is a list of some of the files generated by a Kylix project. Table 4-1 File Extension
Description
conf
Project configuration file. This file holds project configurations such as the project’s compiler settings and memory resouce information. Project options file. This file contains settings from the project options such as directory information and any application parameters. The file is generated from settings in the Project Options dialog. The desktop file saves information regarding open units and forms and their positions. This file is only created if the Tools|Environment|Preferences|Autosave Desktop setting is enabled. Standard resource file that contains the project icon and other resources needed by the application. Stores the current ToDo list for the project.
kof
desk
res todo
Creating a New Project If the project that is being created is an application, select File|New Application from the Kylix main menu. The currently opened project is closed and a new application is created. The new application has, by default, a main form. This form can always be discarded but is more likely to be used as the main GUI form of the application. If the new project is not a GUI application (if it is an SO project or a console application), it can be created through the object repository. For more on the object repository, see the “Object Repository” section later in this chapter.
Using the Application Object As noted in the project file section above, Kylix applications are run by the global application object. The application object (an instance of the TApplication class or one of its descendants) interacts with the operating system to initialize, execute, and destroy an application (an OS process). Tip: Not all Kylix projects include an application object. SO projects (Linux libraries), for example, do not include a TApplication object. SOs are not executable by themselves and must be used within the context of a host application. Although the application object is not available to the developer at design time, it includes a number of properties, methods, and events that can be set at run time to
72
Chapter 4: Working with Projects and Files
influence the behavior of the application. The HelpFile property for instance, can be set to associate a help file with the application. Although this can also be done through the Project Options|Application page (see below), having access to the property allows the developer to dynamically assign a help file during execution. Application events are commonly used to process certain occurrences from a central location. The OnActivate or OnDeactivate events can be used to take action when the program gains or loses focus as the current application. OnException overrides the application’s default response to exceptions (for more on exceptions, see Chapter 10). As an example, let’s look at the application’s OnHint event. The application object’s Hint property automatically takes on the value of the current hint being shown in the application. Commonly that hint is also displayed in another control such as a status bar. To do this, write an OnHint event handler that passes the current hint to the status bar. Then dynamically assign the method to the OnHint event handler. It is important that the method signature match the signature required by the event. The OnHint event is of type TNotifyEvent and requires one parameter of type TObject. procedure TfrmMain.ShowGlobalHint(Sender:TObject); begin stsbrStatus.Caption := Application.Hint; end; procedure TfrmMain.CreateForm(Sender:TObject); begin Application.OnHint := ShowGlobalHint; end;
Tip: The code listed above is for example only. The status bar component already knows how to grab the application’s current hint. To do this, set the AutoHint property of the status bar to true.
Compiling and Running a Project Once the appropriate source code has been written and/or the visual elements have been arranged and configured to the developer’s specifications, the program can be compiled and run (if it is an application). There are three methods for compiling an application. Syntax Check — The syntax check option compiles all of the source code files in a project but does not link them. Syntax check is a faster option than the other two options; however, it does not fully prepare the application to be run. It is a good way to check for compiler errors in code. Compile — The compile option compiles and links all source code files that have changed since the last time the project was compiled. Compiled source code files are given a .dcu extension and are used to link the project into an application, SO, or another type of generated file. If the Environment option Show compiler progress is
Chapter 4: Working with Projects and Files
73
enabled, a compilation dialog appears during the compilation process, giving information on the project’s compilation status. Compile is such a frequently executed operation that it is helpful to memorize its shortcut key (Ctrl+F9). Build — Building a project forces every file in the project to be recompiled, even if they have not changed since the last compile. Even if the source code files have not changed, building should be done if environmental or compiler options have changed that will affect the compilation of units.
The Object Repository The object repository contains a collection of projects, forms, and wizards that help developer get started on a new element of their project. Each page in the repository contains a collection of items that are used to provide a head start in developing projects or project components. To use an object from the repository, double-click the item or select it and press OK. Depending on the type of the object selected, Kylix may or may not start a new project.
Figure 4-1: The object repository
Repository objects are stored in a particular location on the hard drive or on a network. The Preferences page of the Kylix Environment Options dialog contains a Shared Repository option that specifies the directory location of the source code for repository items. Tip: The Shared Repository option can be set to a network location, thus providing a network-wide share of objects. Be sure to enable the appropriate rights to allow those in the specific group read/write access and the world to only read them.
74
Chapter 4: Working with Projects and Files
Using Repository Objects Some objects can be inserted into the current project in three different ways. Forms, for instance, can be copied, inherited, or used in a project. Each option adds the form to the current project in a different way. Copy — The Copy option creates a new copy of the repository object’s source code file. Every element of the file is identical but it is re-created locally in the project space from which it was created. This is a good way to use a common starting point for an object so that it has similar properties to similarly used objects. Once the file has been copied, the developer is free to make any necessary customizations. Inherit — The Inherit option creates a new file, and defines a new class that is inherited from the repository object’s class. The advantage to the Inherit option is that the new object maintains all of the exposed functionality of the object that it inherits from, without clogging up the unit with all of that functionality’s source code. This is by far the most popularly used option. Note: Inheritance is an object-oriented concept that allows an object to automatically take on the attributes and functionality of its ancestor, and then to add on additional characteristics specific to itself. If you are unfamiliar with inheritance, see Chapter 7. Use — The Use option does not create a new object at all; rather it links the actual repository object into the application. Everyone who uses an object from the repository in their project is using the same object. The advantage to using an object is that if any changes are made to that object, everyone who uses it gets the changes. The disadvantage to using an object is that if any changes are made to that object, everyone who uses it gets the changes.
Adding to the Repository Kylix allows developers to add customized forms to the repository. Figure 4-2 shows a form template that could be used as a starting point for a large number of forms. Such a form might contain a toolbar, a navigation pane (in this case, a treeview), a status bar, and a content pane. Creating this base form over and over cumulatively wastes development time. It is much easier to simply add it to the repository and then let other developers copy, inherit, or use it for any form that fits this basic style. To add a form to the repository, right-click on the form and select Add to Repository… The Add to Repository dialog comes up and provides a way for the developer to enter information about the form. The form’s title, description, and author can be entered as well as the page in the repository on which the item should be placed. If desired, the object can use a particular icon to represent it in the object repository.
Chapter 4: Working with Projects and Files
75
Figure 4-2: Adding a form to the repository
Working with Projects This section explains the architecture and use of Kylix projects and describes project-specific options that affect the way projects are viewed, compiled, and run.
Tracking Project Progress While a project is in development, it is helpful to include development notes that track items that are yet to be done. Kylix includes for this purpose a To Do List that tracks outstanding items in the project code. Warning: To Do List items should not be confused with or substituted for documentation or versioning. They are only ways to temporarily keep track of items that must be completed or addressed. To add a To Do List item, right-click the code editor and select Add To-Do Item… The To Do Item editor comes up with a collection of elements used in the To Do Item. Developers can specify the item text, priority, owner, and category.
Figure 4-3: Adding a To Do List item
76
Chapter 4: Working with Projects and Files
AM FL Y
To Do Items are inserted into the code as formatted comments. The following comment, for instance, is the result of inserting a new To Do Item. The formatting helps the To Do List Viewer parse the comment. { TODO 1 -oEric -cValidations and Constraints : Check Customer Object for entry validation. } The To Do Items in a project can be viewed by selecting the View|To Do List option from the Kylix menu. The viewer shows each To Do Item in the project and allows the developer to mark the item as completed. Completing an item does not remove it from the code. It simply changes the first element of the embedded comment from TODO to DONE.
Figure 4-4: Viewing To Do List items for Project 1
The Project Manager
TE
The Project Manager displays file information for one or more Kylix projects. Each project has its own node on the Project Manager treeview and can be expanded to show each file included in the project. Double-clicking on any file opens that file in the code editor or the form designer, depending on its type. A collection of projects can be saved together in a project group (a .bpg file). The libStringFormat and StringEditorClient projects above are both a part of the StringGroup project group.
Figure 4-5: The Project Manager
Chapter 4: Working with Projects and Files
77
Note: The project group file (.bpg) can be viewed by right-clicking on the .bpg file and selecting View Project Group Source. The .bpg file is similar to a C++ make file and keeps track of the projects included in the project group. The .bpg file is maintained for you by Kylix and, in most cases, should not be edited manually. #---------------------------------------------------------------------VERSION = BWS.02.5 #---------------------------------------------------------------------MAKE = make -$(MAKEFLAGS) -f$** DCC =dcc $< #----------------------------------------------------------------------
PROJECTS = StringEditorClient libStringFormat.so #---------------------------------------------------------------------default: $(PROJECTS) #---------------------------------------------------------------------StringEditorClient: StringEditorClient.dpr $(DCC) libStringFormat.so: StringFormat.dpr $(DCC)
Notice in Figure 4-5 that the SO project (libStringFormat.so) is listed in boldface and is also listed in the drop-down project selector. This denotes the current project. Even though the Project Manager can view and control multiple projects at once, the Kylix IDE must deal with one project at a time. The current project is the one that is currently controlled by the Kylix IDE. Any action taken from the Kylix menu or speedbar is applied to the current project. To change the current project, double-click a new project in the Project Manager list, choose the new project in the project selector, or simply select a new project and click the Activate button in the Project Manager toolbar. The Project Manager’s context menus allow developers to control the elements of the project group or any of the projects in the group. By right-clicking on the project group file, the developer can add new or existing projects (this can also be done from the Project Manager toolbar), save changes to the group, and view the .bpg source code, as well as set the viewing options for the Project Manager. The Project Manager toolbar and status bar views can be toggled on and off, and the Project Manager window’s docking capabilities can be set. Right-clicking on a particular project launches a different context menu. From here, the developer can add and remove files, view the project source code (.dpr file), save, compile, or build the project, and launch the Project Options dialog.
78
Chapter 4: Working with Projects and Files
The Project Manager is a great help to the developer who is working on related projects. An SO project, for instance, cannot be run by itself. (An SO is basically the Linux version of a Windows DLL. For more on SOs, see Chapter 8.) It needs a host application to bind to it and make calls to it. Without the ability to manage multiple projects, this is a much more time-consuming task. If a developer is working on the SO project and is ready to test, he must compile and save the .so, close the project, open the host application, and run it. If there are errors, he must close the host application, reopen the .so, make the edits, recompile and resave, close the SO project, reopen the host, and rerun the application! That’s quite a bit of busywork and a great waste of time. With the Project Manager, when the SO is ready to be tested, simply switch the active project to the host application and run it. If there is an error, switch back to the SO project and make the appropriate edits. When the edits are complete, rebuild the SO, switch back to the host, and run it. The Project Manager provides a quick and easy way to make edits to multiple projects. When all edits have been made to each project, select Project|Build all Projects from the Kylix menu to rebuild all currently open projects.
The Project Browser The project browser shows elements of the current project, such as the units, classes, types, and variables in a tree structure. It allows the developer to drill down into the pieces of an application to determine their relationships, ancestry, and declarations. Before using the project browser, save and compile the application. There are several compiler options that affect what is seen in the project browser. In addition, a number of environment options also dictate the project browser view. To set these options, select Tools|Environment Figure 4-6: Viewing a project with the project options from the Kylix menu and browser go to the Explorer tab. The project browser is split into two panes. On the left, the Inspector pane shows the globals, classes, and units included in the project. The right side displays the currently selected element in more detail. The tabs on the right side will change, depending on the selected item on the left.
Chapter 4: Working with Projects and Files
79
Project Options In the last chapter, we studied the IDE options that Kylix offers to enable the developer to customize the way Kylix works. In this chapter, we continue that discussion by presenting a new set of options that are related specifically to a particular project. Project options do not naturally persist between two different projects but allow the user to set particular directions for Kylix to use when dealing with that project. To bring up the project options, select Project|Options… from the Kylix main menu. The Project Options dialog comes up with a collection of tabsheets, each related to the control of a different aspect of the current project. Tip: The Default check box appears at the bottom of the Project Options dialog and sets the default project options. In other words, if the Default check box is checked when the Project Options dialog is closed, the current settings become the default settings for all future projects.
Forms The first set of project options is found on the Forms tab. The Forms tab allows the developer to set preferences regarding form creation when a GUI application is run (of course, not all projects are GUI applications!). The tab contains a combo box and two list boxes, labeled Auto-create forms and Available forms. The Main Form combo box allows the developer to set which form will be the main form of the application. Each entry in the Auto-create forms list box represents one form or datamodule (which we’ll learn about later) that Figure 4-7: Project Options, Forms dialog will be automatically created upon application startup. Recall the environment option Auto create forms & data modules from the last chapter. If this option is selected, each new form or data module added to the project will appear in the Auto-create forms list. Typically, this is not the desired situation. If this setting is maintained, the application will create every form in the program when the program is started. This will cause the application to carry a heavy run-time footprint as it uses memory for every form that is created. If there are sections of the application that are less commonly used (such as user options, color scheme dialogs, etc.), they will more than likely be wasting space for no good reason; that is, you will create forms that the user will more than likely never use.
80
Chapter 4: Working with Projects and Files
On the other hand, there are times when you want more than one form to be created and shown upon startup. Take, for example, a graphics program that contains a user interface with multiple windows (main toolbar, graphics tools window, content window, etc.) Each of those windows must be created on startup and so would need to be listed in the Auto-create forms list. Regardless, the Forms tab lets the developer decide what forms and data modules will be created during the initial phase of program execution. Note: Each entry in the Auto-create forms list corresponds to a line of code in the project file. These lines can be edited manually but this is not recommended, as there is more chance of errors. We strongly suggest that you use the project options to alter the automatic creation of forms and data modules. Also, keep in mind that the order of creation can be important when one form depends on another’s existence. The order of creation can also be controlled through the project options. begin Application.Initialize; Application.CreateForm(TfrmMain, frmMain); Application.CreateForm(TfrmCustomer, frmCustomer); Application.CreateForm(TfrmOrder, frmOrder); Application.CreateForm(TfrmInventory, frmInventory); Application.Run; end;
Application The Application tab provides a few options for setting general application properties. Title — Sets the title that will be shown in the Linux Windows list while it is executing. Target file extension — Specifies the extension for the executable file. Typically, the default property is sufficient, although this property can be set if a custom extension is required. Although custom extensions are not usually used for executable files, Linux libraries are not required to have the .so extension (however, that is the standard). Figure 4-8: Project Options, Application dialog Many applications use custom library extensions. For instance,
81
Chapter 4: Working with Projects and Files
Kylix uses a bpl file to hold components and editors that are installed in the environment. A bpl is just an SO that by convention has a custom prefix (bpl). (Actually, a .bpl is an SO on steroids. For more information on .bpl files, see Chapter 8.)
Compiler The Compiler options tab sets the compiler directives that are applied to the project during the compilation process. Although compiler directives can be inserted into code directly (as with the {$R *.xfm} option), setting these options is the preferred way of controlling project compilation. Tip: To see a list of the current compiler option settings for a unit, press Ctrl+O+O in the code editor. The following table describes these options.
Figure 4-9: Project Options, Compiler dialog
Table 4-2 Option
Description
Directive
Default
Code generation Optimization
Toggles the use of compiler optimizations. This setting {$O} can affect debugging (some statements can’t be seen by the debugger because they are optimized out.) Stack Frames Generates stack frames for all procedures and functions. {$W} Record Field Alignment Aligns elements in structures to 32-bit boundaries. {$A} Syntax options Strict var-strings
Completes error checking on string-type parameters. This option is ignored if the Open Parameters option is enabled. Complete boolean eval Evaluates every condition of a Boolean expression even if one condition is known to be false. Extended syntax Enables functions to be defined as procedures with ignored results. Also enables Pchar support. Typed @ operator Determines the type of pointer returned by the @ operator. Open parameters Enables open string-type parameters in methods.
On
Off 8
{$V}
On
{$B}
Off
{$X}
On
{$T}
Off
{$P}
On
82
Chapter 4: Working with Projects and Files
Option
Description
Directive
Default
Huge strings
Determines the data type assigned to string-type variables. If enabled, string is associated with the AnsiString type. Otherwise, string is associated with the shortstring type. Allows assignments to be made to typed constants. (The default is Off, which is different from Delphi 5.)
{$H}
On
{$J}
Off
Assignable typed constants Run-time errors Range checking I/O checking Overflow checking Debugging Messages Show Hints Show Warnings
Checks that the access of array and string elements are {$R} within the defined bounds of the element. Checks for I/O errors after every call. {$I} Checks that integer operations do not overflow the stack. {$Q} (Debugging options are covered in Chapter 10.)
Off
Shows hints, generated by the compiler, to the developer. {$O} Shows warnings, generated by the compiler, to the {$O} developer.
On On
On Off
Linker The Linker tab specifies options for how compiled files are linked together. Map file — Specifies the type of map file produced by the application. By default a map file is not produced. Each option produces a map file with more information. Map files can include a list of segments, the program start address, any warnings or errors generated during the linking process, a list of public symbols (sorted alphabetically), and an additional detailed segment map with more detailed information. Map files are useful Figure 4-10: Project Options, Linker dialog when you want to translate an address to a public symbol or vice versa as well as seeing detailed technical information about your application. For those diehards who love assembler, the map file provides some insight on how the compiler works.
Chapter 4: Working with Projects and Files
83
Executable and SO Options Include STABS debug info — When a program includes debug information, it is included in the STABS format, which is compatible with GDB debugger. This provides the detailed information that GDB requires. Include external debug info — Includes debugging information in the executable file. This is different from the debug information compiler option. That option only includes debug info in the generated .dcu file. This option includes debugging information in the executable file. This option should be used if you are using an external debugger. The executable file grows larger with this option enabled and the compilation time of the project will grow, but the memory requirements and the performance of the application will not be affected. Memory sizes — The resource reserve option specifies the amount of virtual memory to reserve for an application’s resources. The default is 1 megabyte. Description — A string that is linked to the $D directive and embedded into the application. The string can include 255 characters and is usually used to store things like copyright information.
Directories/Conditionals Output directory — When a project is compiled, the compiler will put compiled units such as dcu’s, kof’s, elf’s, etc. into this directory. Unit output directory — Specifies a separate directory to contain the .dcu files. Note, .dcp files can be relocated by setting the DCP output directory path on the library page of the Tools|Environment Options dialog box. Search path — This path specifies where the compiler will look for a project’s source code files. The developer is allowed to specify multiple paths on which the Figure 4-11: Project Options, Directories/Condicompiler will attempt to locate tionals dialog files. Paths should be separated by semicolons and both relative and absolute paths are allowed. Debug source path — Search path for the debugger. The debugger searches paths defined by the compiler by default. If the directory structure has changed since the last compile, a path can be entered here to include a file in the debugging session. BPL output directory — Specifies where the compiler puts generated package files (BPL files).
84
Chapter 4: Working with Projects and Files
DCP output directory — Specifies where your DCP file is placed at compilation time. If left blank, the global DCP output directory specified in the Tools|Environment Options Library page is used instead. Conditional defines — Symbols referenced in conditional compiler directives. You can separate multiple defines with semicolons. See Chapter 9 for more information on conditional compilation. Aliases — Kylix allows an application to specify aliases for units. The Unit aliases option specifies the aliases for an application. It allows the developer to create aliases for unit names. Using aliases is a quick way to map Kylix units to Delphi units (QDialogs to the Dialogs unit from Delphi 5); however, for readability, using conditional compilation would be better. Tip: Each page of the Project Options dialog includes a Default check box at the bottom of the dialog. If this box is checked when the OK button is pressed, the current settings will become the default values for all new projects.
Packages The Packages tab provides the developer an interface for adding both design-time and run-time packages to the project. For more information on packages, see Chapter 8. Packages may contain compiled component code or design-time property editors, used in the Kylix IDE. The Components button displays a dialog that shows the developer what components are included in the selected package.
Figure 4-12: Project Options, Packages dialog
Chapter 4: Working with Projects and Files
85
Build with runtime packages dictates the code that is embedded into the project’s generated executable file. Because CLX code is used by all Kylix applications, it is sometimes preferable to compile the commonly used code into its own package(s), separate from the application-specific code. This allows several applications to access the same CLX packages and dramatically reduces the total amount of space used by all Kylix applications. A basic Kylix application containing only a single form with no components has a size of 286 K. Compiling the same application with the Build with Runtime Packages option selected reduces the size of the executable file to 14 K! This provides a tremendous benefit when the application is deployed. Since the CLX packages are not likely to change with each release of the program, deployments following the initial release only need to include the executeable file or its package and not the CLX packages.
Summary Kylix projects provide a context in which to develop and run applications and SOs. A project’s code is maintained for the developer by the Kylix environment, leaving him or her to focus on the creation of source code modules and the business logic of the project. Projects are compiled and linked into the runtime modules they represent (applications or shared objects). Kylix provides a large number of project options that control the structure and compilation of projects. This chapter presented the architecture and responsibilities of projects, as well as described some tools and techniques for managing them. The Project Manager and project browser are available to assist developers in creating, managing, and running their projects. Once a project is created, the developer is free to develop it to whatever level of sophistication is required.
TE AM FL Y
Chapter 5
Object Pascal
Introduction You can’t get very far with any development tool without knowing the language it uses. Kylix uses Object Pascal, which is eerily like regular old Pascal except that it includes some of the advancements necessary to survive in an object-oriented world. Most of the strictly language issues are unchanged from Delphi to Kylix. If you are already familiar with Object Pascal, you should probably skim this chapter. However, please make sure to look for author notes throughout which will highlight any differences that occur. In addition, please read the section titled “Linux Issues” for an overview of writing Pascal code on the Linux platform. This is not a comprehensive reference, but is intended to get you up and running with Object Pascal. For a more complete guide to Pascal, please consult an Object Pascal reference (a reference is provided on the Kylix CD under /runimage/kylix/documentation). Tip: Although Kylix uses Object Pascal as its base language, we reserve the right to refer to it in this chapter as simply Pascal. This is done primarily because…well…it’s shorter and because this is a Kylix book, not an Object Pascal book!
Linux Issues Fortunately for Delphi and other Windows developers, Object Pascal translates pretty smoothly from Windows to Linux. There are, however, a few rules that must be adhered to in order to survive on the Linux platform. Following are a few of the basic rules. More information can be found in Appendix E: “Linux Tutorial for Windows Developers.”
87
88
Chapter 5: Object Pascal
Case Sensitivity Pascal is not a case-sensitive language. The identifiers customerid, CustomerID, and CustomerId are all seen by the compiler as the same. Although it is not required to refer to a variable with the same case all the time, it is generally a good idea to do so from the perspective of readability. When dealing with Linux, however, it is important to remember that there are a few occasions on which Kylix will ask for something from Linux and must do so with the proper case. Any time the application refers to a specific file, such as in an SO or in the name of a unit, the proper case must be used. Without it, Linux will search for a file that does not exist (not with that case, anyway).
Using Separators Linux uses the forward slash ( / ) instead of the backslash ( \ ) as the default path separator. Whenever code refers to a specific filename, make sure to use the correct separator. Use the PathDelim constant found in SysUtils.pas for a portable way of referencing the proper separator.
File Permissions Before the Kylix compiler can access source code or generated files, make sure that you are logged onto Linux with the correct file permissions. For more on permissions and interacting with Linux files, see Appendix E, “Linux Tutorial for Windows Developers.”
Statements A Pascal program, like most languages, is made up of statements. Each statement (with very few exceptions) ends with a semicolon. Blank lines, tabs, and white space are all ignored by the Pascal compiler, except for the purpose of separating language tokens. However, good programming includes the creation of readable and maintainable code. It is a good idea to develop some kind of personal or corporate standard for code format, as this makes it easier for other developers to read and understand. A common Pascal standard is to indent two spaces between logically contained code blocks as in the following example. For more information on Pascal coding standards, check out the Delphi coding standard at http://www.econos.de/ delphi/cs.html. (Don’t worry if you don’t understand all of the elements of this code. That’s why you’re reading this chapter!) procedure TfrmSomething.btnDoSomethingClick(Sender: TObject); var MyBooleanVariable:Boolean; i:Integer; begin if (MyBooleanVariable) then for i := 1 to 10 do
Chapter 5: Object Pascal
89
begin ShowMessage('I am doing something now!'); CallSomeMethod; end; end;
If you are coming from a Java background, then you may already use the standard suggested by the Java coding conventions document (available from the SUN web site), which is to indent by four spaces. The fact is that it is irrelevant what standard you use as long as you follow it all the time and it makes it easier for others to read your code.
Statement Blocks Statements are commonly organized into collections called blocks. A block of statements has the same scope and is taken as a logical entity by the compiler. In a program loop, for instance, a block of statements can be executed instead of a single statement. For more on loop structure, see the “Loops” section later in this chapter. for I := 0 to 9 do begin end;
Normally, the for loop only includes the statement directly following the loop header in the loop execution. By creating a code block (which is delimited by a begin..end pair), the developer mandates that all of the statements in the block are executed for each iteration of the loop. In this chapter, a block of statements can be substituted for any .
Expressions An expression is anything that evaluates to a type and a value. Expressions can be a simple value or a variable, or can be a more complex entity such as a calculation or the result of a function. Regardless of the makeup of an expression, it always has a single value. In the following code, each of the following lines contains a valid Pascal expression to the right of the assignment (:=). begin a := 10; b := 15 + SomeVariable – 47; c := GetCustomerName(37); end;
90
Chapter 5: Object Pascal
Comments Comments are an important part of any language, not only for the information they provide during the maintenance phase of development, but also for temporary deletion of code during development. Comments are ignored by the compiler, but are not removed from the source file. Pascal supports three types of comments. n
The open/close braces ( { comment } ) are used for multi-line comments. Brace comments cannot be nested within other brace comments, but can be nested within comments of another type.
n
Parenthesis/star comments ( (* comment *) ) are also used for multi-line comments. Similarly to braces, paren/star comments cannot be nested inside other comments of the same type, but can be nested within comments of another type.
n
The double slash comment ( // comment ) indicates the beginning of a line comment. Line comments do not require a terminating character, but are automatically terminated at the closest following carriage return marker. Line comments can be nested inside of both of the other types of comments.
Variables Variables, or identifiers, allow applications to store values for use at a later time. They abstract the memory locations at which data is stored by giving them more readable names so that the programmer can refer to them more easily. Variables can hold data of any valid Object Pascal type, as well as any user-defined types. To declare a variable, create a variable clause (var) and place declarations underneath it in the form VariableName(s) : DataType; as in the following example. var CustomerName : String; CustomerAge, CustomerID : Integer; CurrentBalance : Double;
Notice that each variable does not have to be prefaced by the word var. Also, multiple variables can be declared in a comma-separated list. Because Pascal does not care about white space, the developer is free to declare each variable in a list on a separate line or as part of the same line. Each item in the list declares a new variable of the type declared at the end of the list.
Chapter 5: Object Pascal
91
Assignment and Comparison Assignments and comparisons can be a confusing part of the Pascal language, but only if you’re a C++ or Java developer! In Pascal, assignments are made with the := and comparisons are made with the = sign. (The confusing part is that in some other languages, such as the ones mentioned, the equal sign is used for assignments and the double equal ( == ) is used for comparisons.) In this book, whenever you see “a gets b”, it’s referring to an assignment.
Constants Constants are a mechanism by which values are hard coded into a system. They are declared in a const section and take the format constant = value. Unlike variables, constants’ values are written in stone. A constant’s value cannot change during the life of the program. Constants are typically declared in all uppercase and often use underscores to separate words. All of the following are valid constant declarations. const MAX_VALUE = 50; STD_ERROR_MSG = 'An error has occurred. Please try again.'; PI =3.14159;
Constants are useful for declaring commonly used values in a central location. This makes maintenance easier, requiring the developer to update the value in only one place. For example, imagine that an application calculates prices for retail goods. A price is augmented by the applicable sales discount that should be applied. These changes take place from a large number of methods throughout the form. Using constants, the form might be coded like this: const CUST_DISCOUNT = .15; BONUS_DISCOUNT = .10; BONUS_DISCOUNT_PRICE = 10000.00; { … } function CalculateSalesPrice(Amount:Double):Double; begin if Amount >= BONUS_DISCOUNT_PRICE then Result := Amount–(Amount*(CUST_DISCOUNT + BONUS_DISCOUNT)) else Result := Amount – (Amount * CUST_DISCOUNT); end;
The advantage to using constants in this situation is that when changes must be made to the system (which they always do), they only have to be made to the constants instead of in every place they are used. Without the use of constants, the
92
Chapter 5: Object Pascal
developer is forced to locate all places in which those values are changed and change them all manually.
Typed Constants Typed constants are a hybrid of variables and constants. They are declared in a const clause, but unlike constants, their values can change. In that way, they are more like variables. They differ from variables, however in one very important way: They remember their value, even after the method in which they’re declared goes out of scope! procedure TfrmDataExplorer.btnLoginClick(Sender: TObject); const LoginAttempts:Integer = 0; begin if not DoLogin('UserName','Password') then begin MessageDlg('Your login has failed.',mtWarning,[mbOK],0); inc(LoginAttempts); if LoginAttempts = 3 then begin MessageDlg('The application will now terminate',mtError,[mbOK],0); Application.Terminate; end; end; end;
In the example above, the LoginAttempts typed constant is declared and initialized to 0. If the DoLogin method fails (the attempt to login to the database is rejected), LoginAttempts is incremented by one. The method then continues until it ends. If the user clicks the Login button again, the method is executed again with one very important difference. This time, LoginAttempts starts at one! Each time the method is executed, LoginAttempts remembers its value at the end of the last execution. This is a great way to keep track of how many times an event has occurred or a method has been executed.
Data Types Data types in Object Pascal can be broken down into two categories: primitive types and class types. Primitive types are used to store simple values for use in the application. Class types are used to hold more complex types such as classes and objects.
93
Chapter 5: Object Pascal
Primitive Types Primitive data types are used for passing and storing values throughout the application. In addition to numeric values, Pascal also uses logical (Boolean), string, and single character types. Following is a summary of the various primitive types.
Integer Types Type
Range
Format
Integer Cardinal Shortint Smallint Longint Int64 Byte Word Longword
–2147483648..2147483647 0..4294967295 –128..127 –32768..32767 –2147483648..2147483647 –2^63..2^63–1 0..255 0..65535 0..4294967295
signed 32-bit unsigned 32-bit signed 8-bit signed 16-bit signed 32-bit signed 64-bit unsigned 8-bit unsigned 16-bit unsigned 32-bit
Real Types Type
Range
Significant Digits
Size in Bytes
Real48 Single Double Extended Comp Currency
2.9 x 10^–39 .. 1.7 x 10^38 1.5 x 10^–45 .. 3.4 x 10^38 5.0 x 10^–324 .. 1.7 x 10^308 3.6 x 10^–4951 .. 1.1 x 10^4932 –2^63+1 .. 2^63 –1 –922337203685477.5808.. 922337203685477.5807
11–12 7–8 15–16 19–20 19–20 19–20
6 4 8 10 8 8
The Char Type Pascal uses the char type to store a single character. Character literals can be assigned to a char type variable using single quotes. In the following example, the character ‘A’ is assigned to the FirstLetter variable; the second line assigns an apostrophe to a variable. Because Pascal uses apostrophes to delimit strings, two apostrophes must be used to signify a single apostrophe character. var FirstLetter : Char; begin FirstLetter := 'A'; Apostrophe := ''''; end;
94
Chapter 5: Object Pascal
The Boolean Type Boolean data represents logical values that evaluate to true or false. Booleans can be assigned and compared directly (using true and false) or can be used as the result of an expression or function call. Boolean values are often used as a success code returned from a function. Unlike some other languages, Boolean values do not equate and cannot be compared to 1 and 0. begin Truth := 1; // causes compiler error! AcctCurrent := GetBalance('Borland Corporation'); end;
Strings A string represents an ordered sequence of characters, stored as a single value. Object Pascal supports three types of strings. Type
Maximum Length
Memory Required
Used for
ShortString AnsiString WideString
255 characters ~2^31 characters ~2^30 characters
2 to 256 bytes 4 bytes to 2GB 4 bytes to 2GB
Backward compatibility 8-bit (ANSI) characters Unicode characters
Short strings were originally used in earlier versions of Turbo Pascal. A short string is statically allocated to hold 256 bytes. The first byte indicates the length of the string, while the remaining 255 bytes are available to hold individual characters. A short string uses only 8-bit ANSI characters and cannot store Unicode characters. It is maintained primarily for backward compatibility. ANSI strings (also called long strings) differ from short strings in that they are dynamically allocated, and their length is only limited by available memory. By default, when a string type variable is declared, an ANSI string is used. A compiler directive ({$H-}) can be set to change the default string type to a short string, but this would normally be used only in conversion or legacy code (for more on this compiler directive, consult the Kylix help files). ANSI strings also use 8-bit ANSI characters. The WideString type is used by Pascal to support multi-byte character sets. This allows wide strings to store Unicode characters and other multi-byte characters. Unicode strings are not made up of a collection of single byte characters. A Unicode character is actually a two-byte word. This is especially important for multi-language applications because while the English language has only 26 characters (letters), Asian languages (for instance) have thousands of printable characters. It is not possible for one byte of data to hold that many combinations, so it is essential that each character be able to be stored in a multi-byte format.
Chapter 5: Object Pascal
95
Strings are declared as other identifiers are in a var section. Once they are declared, they can be assigned a value either from another identifier or with a string literal. String literals are delimited by single quotes. var VendorName:String; begin VendorName := 'PILLAR Technology Group, Inc.'; end;
Class Types Class type variables are declared in the same way as primitives. In a var section, simply state the name of the variable, a colon, and a type which is the name of the class. The primary difference between a primitive variable and a class-type variable is that class-type variables do not actually store values, but rather pointers to objects. Once a class-type variable has been declared, an object instance must be created and assigned to it before it can be accessed. If a class-type variable is accessed before it has been assigned, an EAccessViolation occurs. See the Classes and Objects section later in this chapter. var Customer : TCustomer; begin Customer.GetCompanyName; // Causes an EAccess Violation! end;
Variants A variant is a generic data type that can hold a value of a large number of data types. Integers, strings, doubles, and chars are just a few of the types that can be stored as a variant. This can be very helpful when you don’t know what type the user is going to pass to you. A good example is searching for values from a database. If the application allows the user to select which field they will be searching on, the value that they provide might be a number of different types. In order to provide this functionality without variants, the developer would have to define a whole set of local variables of different types and then determine on the fly which variable to use based on what database field the user is searching on. This is, of course, not a very clean way to do things. Instead, the developer can take whatever field name and value the user enters and pass them (as a string and a variant) to a searching method. This way, no matter what type the field is, the same searching method can be used and unnecessary variable declarations are avoided. Variants are discussed further in Chapter 9.
Chapter 5: Object Pascal
Warning: Overuse of variants causes poor performance. Because a variant is not explicitly aware of its type, it must determine the data type on the fly each time it is accessed. This is not to say that variants don’t have their place, simply that they are not to be used lightly. Other development tools have suffered the fate of variant abuse…I won’t mention any names, but their initials are VB!
User-defined Types Kylix allows the creation of custom types, which aids readability of code. It’s pretty frustrating to see a function call and have no idea what the parameters mean. For example, suppose you are creating a drag and drop user interface and you use the BeginDrag method. The code would look something like this: lstbxDraggableList.BeginDrag(False);
AM FL Y
The problem here is that it’s not immediately evident what the impact is of passing False as a parameter to this method. (Incidentally, this parameter determines if the drag operation occurs immediately or if it waits until the control is dragged a certain distance.) It would be more useful to pass a value that gave some indication of its semantics to the developer. At the risk of creating confusion, allow me to mix reality and fiction. It would be much better if the method was defined this way: procedure BeginDrag(Immediate:TDragTiming);
Instead of using a Boolean parameter, we have defined the parameter to be of type TDragTiming. Unfortunately, TDragTiming doesn’t exist so we must create it. To define a custom type, simply declare it and list its values in the type section:
TE
96
type TDragTiming = (dtImmediate, dtDelayed); { later in the code… } lstbxDraggableList.BeginDrag(dtDelayed);
The use of a user-defined type here makes the semantics of this method much easier to determine for other developers who read this code. This can save a great deal of time, because developers don’t have to look through the Kylix source code to determine what passing the originally declared Boolean parameter means.
97
Chapter 5: Object Pascal
Operators Operators allow the developer to perform pre-known operations on one or more values. Each value is called an operand. Operators are used to evaluate expressions, as well as to affect the value of application identifiers. A summary of the various operator types and their meanings is listed below.
Arithmetic Operator
Operation
Operand Types
Result Type
Example
+ – * / div mod + (unary) – (unary)
addition subtraction multiplication real division integer division remainder sign identity sign negation
integer, real integer, real integer, real integer, real integer integer integer, real integer, real
integer, real integer, real integer, real real integer integer integer, real integer, real
X+Y Result – 1 P * InterestRate X/2 Total div UnitSize Y mod 6 +7 –XLogical
Relational Operator Operation
Operand Types
Result type
=
equality
Boolean
inequality
< > =
less than greater than less than or equal to greater than or equal to
simple, class, class reference, interface, string, packed string simple, class, class reference, interface, string, packed string simple, string, packed string, PChar simple, string, packed string, PChar simple, string, packed string, PChar simple, string, packed string, PChar
Boolean Boolean Boolean Boolean Boolean
Set Operator
Operation
Operand Types
Result Type
Example
+ – * = = in
union difference intersection subset superset equality inequality membership
set set set set set set set ordinal, set
set set set Boolean Boolean Boolean Boolean Boolean
Set1 + Set2 S–T S*T Q = S2 S2 = MySet MySet S1 A in Set1
98
Chapter 5: Object Pascal
Logical (Bitwise) Operator
Operation
Operand types
Result type
Example
not and or xor shl shr
bitwise negation bitwise and bitwise or bitwise xor bitwise shift left bitwise shift right
Integer Integer Integer Integer Integer Integer
Integer integer Integer Integer Integer integer
not X X and Y X or Y X xor Y X shl 2 Y shl I
Precedence An Object Pascal reference will contain a list of the order in which operators are applied in a complex operation. For instance, in the following expression, the multiplication and division operators are applied first, followed by addition and subtraction. Operators at the same level of precedence are evaluated from left to right. begin MyValue := 35 – AnotherVariable * 15 + 36/2.5; end;
At first glance, it is unclear how this expression is evaluated. A good rule of thumb is to use the “you get what you pay for” rule. Although the answer is the same, investing a few extra seconds in some delimiting parentheses makes this expression much clearer. begin MyValue := 35 – (AnotherVariable * 15) + (36/2.5); end;
Program Flow A good program is one that not only stores data, but also uses it in an efficient way. Designing a good flow of execution in an application is key to creating smaller, more organized, and more maintainable code. The following section describes some of the elements of program flow and demonstrates their use.
Decision Structures An application uses decision structures to make logical choices during its execution. Programs frequently require user input or a particular value that is not known ahead of time in order to choose an execution path. The programmer writes code to handle each individual situation (unless he or she is really slick, in which case they will write generic code that can successfully and accurately handle multiple situations).
Chapter 5: Object Pascal
99
The If Statement The most commonly used decision structure in programming is the if statement. The if statement can be short and simple or long and complex, and has infinitely many forms. It chooses an execution path based on the value of a Boolean expression. The basic structure of an if statement is: if then else ;
As we’ve already mentioned, Pascal does not care about carriage returns or white space. You are free to format the if statement however you wish, that is, some developers place then on the second line with its statement or similarly place the else clause all on one line. Notice the absence of a semicolon after the first . The if statement is one of the few places in Pascal where you may not put a semicolon at the end of a statement. This is because the compiler considers the entire construct to be one statement. Pascal does not have an endif, like Visual Basic. It relies on the semicolon to determine the end of the if statement (semicolons within blocks of statements don’t count!) You can see that the if statement can be very simple. Even the structure shown above is not the simplest form of the if statement. The else clause is completely optional. Without it, an if statement might look like this: if (UserCount > 10) then ShowMessage('Too many users are connected');
Conversely, the if statement can be very complex, as in this example: if(UserGender = 'male') then UserGreeting := 'Hello, Mr. ' + UserName else if(UserStatus = 'Single')or(UserStatus = 'Divorced') then UserGreeting := 'Hello, Ms. ' + UserName else UserGreeting := 'Hello, Mrs. ' + UserName;
In this example, the else clause uses as its statement another if statement. Although the second if statement is long and within itself has an else clause, it is still seen by the compiler as just one statement. The “Dangling” else
Occasionally, an if statement is needed that can’t be interpreted by the compiler, by itself. Consider the following if statement. var FirstName, LastName, FullName:String;
100
Chapter 5: Object Pascal begin if (FirstName '') then if (LastName '') then FullName := FirstName + LastName; else ShowMessage('You must enter your full name'); end;
In this case, which if statement does the else clause belong to? Given the way that it is formatted, it looks like the else clause belongs to the first if statement, but this is not the case. Pascal does not care about white space, so the compiler will assume that, since the else clause follows the second statement, they must belong together. The logic in this case calls for an else clause for only the first statement. How can the compiler be made aware that the else clause belongs with the first statement? A commonly suggested solution to the problem is to place a semicolon at the end of the second if statement. This will cause a compile-time error. Because Pascal does not have a terminating marker for if statements (like Basic’s ENDIF), the compiler interprets the entire statement, from the first if until the first semicolon, as one statement. Placing a semicolon after the second if statement makes the compiler think that the next statement begins with else (which is illegal). The solution to the problem is to add a begin/end pair to the first if statement that encompases the second statement. Now that the second if clause has been placed inside of a statement block, the compiler sees that block as a single statement (sort of) and therefore assigns the else clause to the if statement that is on the same logical level. if (FirstName '') then begin if (LastName '') then FullName := FirstName + LastName; end else ShowMessage('You must enter your full name');
The Case Statement Another way to make decisions in your application is with the case statement. The case statement compares a value to a set of mutually exclusive “cases” in which the value could fall. Each case executes a statement or block of statements if the value is found in the case’s value list. The case statement has the following form: case of : ; : ; : ; else ; end;
Chapter 5: Object Pascal
101
The value list of a case statement can contain any ordinal type (integer, Boolean, user-defined types, etc.). It cannot contain elements of an unordered nature such as strings and floating points. The else clause allows the declaration of a default case for circumstances in which no appropriate case is found. Notice that unlike the switch statement in C++ and Java, the case statement does not require a break statement between cases. When a particular case’s statements are completed, execution jumps to the first line following the case statement. Tip: When a case statement is used to distinguish between a set of components, it often uses the tag property when there are no other distinguishing characteristics (the name of course cannot be used because it is a string). The tag property is defined in the TComponent class and is included for use in whatever way the developer sees fit. Whenever the tag property is used to distinguish components, it is especially important to use comments so as to alert team members to the particular use of the case statement. case tlbtnMainToolBarButton.Tag 0 : DataTable.First; // move to 1 : DataTable.Prior; // move to 2 : DataTable.Next; // move to 3 : DataTable.Last; // move to end; // case statement
of the the the the
first record previous record next record last record
The With Statement By default, whenever an object property or method is referenced by itself (without an explicit reference to the object) there is an implicit reference to Self. Self refers to the object in which you are working. For example, in the following code, the form’s Close method is used by the form itself. It is understood that it actually means Self.Close. Because we are coding the btnCloseClick method of the TfrmMain class, Self refers to TfrmMain. procedure TfrmMain.btnCloseClick(Sender: TObject); begin Close; // actually refers to TfrmMain.Close or Self.Close end;
The with statement is a way of temporarily redirecting Self to refer to a particular object or code block. It allows the developer to refer to several properties and methods of an object without having to explicitly name the object every time. with Self,lstbxCustomer do begin // refers to Self (the form) Caption := 'Searching'; WindowState := wsMaximized;
102
Chapter 5: Object Pascal
// refers to the list box MultiSelect := True; Items.add('Customer Name'); Items.add('Customer ID'); // refers to the last item (the list box) Color := clBlack; Font.Color := clWhite; end;
In the code above, the with statement includes a reference to both Self and lstbxCustomer. The with statement may refer to multiple objects as long as the items referenced inside the with statement’s code block are not common to more than one of the objects. In the previous example, the first two statements refer to the form and the second two refer to the list box. This is because those properties are not common to both forms and list boxes. The third pair of statements uses properties that are common to forms and list boxes and so they will default to whatever is the last item in the with statement, in this case, the list box. Warning: It is actually legal to refer to a property that is contained by more than one of the listed objects in the with statement. When such a property is referred to, the compiler assumes that the reference is intended toward the last object in the list. This, however, is not clear to anyone who looks at the code. It is ambiguous programming at best and should be avoided. With statements are a great way to provide compact code and avoid the use of a lot of unnecessary if statements. It is valid to take advantage of the fact that common properties will be assumed to refer to the last item in the list; however, if this technique is used, it is especially important to comment your code so that another developer will be able to quickly determine your intent.
Loops Program loops are used to iterate over a statement or a block of statements multiple times. A loop is a set of statements that is repeated until an exit condition is met that terminates the loop. When a loop is finished, execution continues at the first line of code following the loop body.
For. . do The for..do loop executes a block of statements a specific number of times. The initialization variable is incremented consistently, executing the body of the loop each time. The initialization variable cannot be modified within the body of the loop. Doing so causes a compile-time error.
Chapter 5: Object Pascal
103
for := to do ;
Because the initialization variable cannot be modified in the body of the loop, the loop must execute once for each value in the defined range from to , inclusive. It is possible, however, to have the loop count down instead of up by replacing the word “to” with the word “downto.”
While. . do The while. . do loop executes a block of statements as long as a certain condition is met. The condition must evaluate to a Boolean expression. while do ;
Unlike the for. . do loop, the while. . do loop does not know ahead of time how many times it will execute. As long as the Boolean expression remains true, the loop will execute another time. It is the developer’s responsibility to make sure that the body of the loop contains sufficient logic to provide an exit condition. Warning: Be sure that your while loops will eventually satisfy the exit condition. If the loop condition never changes or reaches the exit state, the loop will continue infinitely. In the following example, the Counter variable is never incremented; therefore, the exit condition will never be satisfied and the loop will continue infinitely. var Counter:Integer; begin Counter := 0; while (Counter < 10) do // This loop will never end! begin ShowMessage('Looping again…'); end; end;
Repeat. . Until The repeat. . until loop is like the while. . do loop in that it executes the loop as long as a condition is true. Unlike the while loop, however, the repeat. . until loop does not check for the condition until the end of the loop. This means that the statements of the looping body will be executed at least once. This is also unlike the while loop, which may never be executed at all.
104
Chapter 5: Object Pascal repeat ; ; ; until ;
Tip: Notice that the repeat loop, even if it contains multiple statements, does not require a begin…end pair. This, however, does not mean that a pair cannot be supplied to aid the comfort level of a new Pascal programmer! Repeat loops are often used applications for prompting the user for information. The application must ask the user a question, and if the answer cannot be validated or is incorrect in some other way, the loop simply asks them again!
Continue The continue statement causes the program to skip the remaining statements in the current loop and immediately proceed to the next iteration of its enclosing loop. continue statements must be used within the context of one of the loops mentioned above. Calling continue outside of a valid looping structure causes a compiler error. procedure NotifyDelinquentCustomers(CustomerList: array[0..100] of TCustomerRec); var I:Integer; begin for I := 0 to 99 do if CustomerList[I].AccountBalance 100,000.00 then begin Result := CustomerList[I].CustomerID; Break; end; end;
Subroutines In production development, or even in relatively small programs, the execution of statements can become lengthy and complex. Furthermore, subsections of functionality are commonly repeated a number of times throughout the life of the application. Pascal subroutines allow the developer to execute a particular behavior through a collection of valid statements. Using subroutines makes the application easier to read by replacing lengthy code sections with simple calls to the application to perform a certain behavior.
Procedures Procedures are subsets of behavior that can be called multiple times from various places within the application. They are defined according to the following format. procedure ( :; ,:…:); begin ; end;
Note that multiple parameters of the same type can be passed in a list format, instead of being declared individually. Once a procedure has been declared and implemented, it can be called simply by referring to it from another area of the program. procedure SortNumbers(Numbers:array[1..10] of Double); var I,J:Integer; begin for I:= 1 to 9 do for J := I+1 to 10 do if NumArray[i] > NumArray[j] then begin Temp := NumArray[I]; NumArray[I] := NumArray[J];
Chapter 5: Object Pascal NumArray[J] := Temp; end; end;
Functions Functions are similar to procedures in that they are used to encapsulate a subset of functionality that can be called by or within a single statement of the application. The primary difference between functions and procedures is that functions return a value to the caller. Procedures execute a command, while functions answer a question. function (:; ,:…:):; begin ; end;
AM FL Y
Functions are declared almost identically to procedures. The difference between them is that following the parameter list, a function declares a return type, that is, the type of response data that will be returned to the caller. function GetRandomNumbers( NumCount:Double):array[1..10] of double; begin // GetRandomNumbers := ; alternate method of // returning a value. result := ; end;
TE
106
Functions can return a result in one of two ways. A return value can be assigned to the function itself by assigning a value to the name of the function. Alternatively (and more commonly) the keyword result is used (as in the example above) to return a result from a function. Unlike in other languages that use the keyword return, assigning a value to result does not end the function. Result can be used as a local variable inside the function, being assigned as many times as necessary.
Parameter Passing Understanding the details of passing parameters to functions and procedures can be a key to an efficient and secure programming style. By default, variables are passed by value. This means that when a variable is passed as a parameter to a function or procedure, the system makes a copy of the value and passes that copy to the method. Within the method, the parameter acts as a local variable and may be accessed and assigned to freely. However, when the method is completed, the copy is destroyed. From the programmer’s perspective, this means that the variable that was passed in goes back to its original value. Some people view this as good
Chapter 5: Object Pascal
107
security, some view it as inflexibility. The truth is, it totally depends on the intended goal of the subroutine. Sometimes, a parameter is passed in for information only, while at other times, the entire purpose of the method is to populate the parameter.
Figure 5-1: Passing parameters by value
The second method of parameter passing is called passing by reference. When a variable is passed by reference, the application passes the address of the variable directly to the method. Each time the method reads or assigns to the parameter, it is accessing the value at the address that was passed in. This means that whatever changes are made to the parameter are changing the original value and are permanent. To pass a parameter by reference, prepend the parameter with the var keyword. Again, some people see this as dangerous, but it may, in fact, be the very purpose of the method.
Figure 5-2: Passing parameters by reference
Looking again at the SortNumbers example in the “Procedures” section, notice that the code used to actually swap two numbers involves multiple lines of code. Even in this small example, it is a good idea to abstract this functionality into its own method, both because it involves multiple lines of code and because it encapsulates a logical unit of work that may be used in a number of situations. The SwapNumbers procedure shows the switching of two numbers.
108
Chapter 5: Object Pascal procedure SwapNumbers(var A,B : Integer); var Temp : Integer; begin Temp := A: A := B; B := Temp; end;
Note: You may think that because of their size, objects are often passed by reference. This, however, is not the case. Objects are already references. Even though they are treated as values, they are actually pointers under the covers. If an object is passed by reference, the function is receiving a pointer to a pointer. For this reason, objects are always passed by value or by const. (See the next section. If you are unfamiliar with objects, you may want to skip ahead to read the Classes and Objects section below.) procedure GetEmployee(Employee:TEmployeeInfo); begin // changes to Employee are permanent! Employee := TEmployeeInfo.Create; { we will discuss the Create method later. } end; { later that day… } procedure GetEmployeeAddress; var AnEmployee : TEmployeeInfo; begin GetEmployee(AnEmployee); Address := AnEmployee.Address; City := AnEmployee.City; State := AnEmployee.State; Zip := AnEmployee.Zip; end;
The third and final way to pass parameters is by const. This is accomplished by prepending parameters with the const keyword instead of the var keyword. When a value is passed in by const, it is still a reference (an address) to the original value; however, it makes it illegal to assign anything to that parameter within the method to which it is passed. If any line of code in the method attempts to assign a new value to the parameter, a compiler error occurs. Passing by const is usually a good choice for larger items, like an array or a large record, because it is a more efficient way to pass it around (extraneous copies do not have to be made), without the concern that the method will change its value.
Chapter 5: Object Pascal
109
Overloaded Subroutines The writer of a subroutine often does not know exactly what information the user (the calling block) will have when the method is called. For example, a customer object could be created with varying amounts of information. The developer doesn’t necessarily know ahead of time how the method will be used, that is, what ways the client application will allow the user to create a customer. Overloaded subroutines allow the programmer to declare multiple versions of the same-named method in the same code scope. This gives the client application developer more flexibility in how the user interface is designed (what information is required, etc.). function GetCustomer(CustomerId : Integer):TCustomer;overload; function GetCustomer(CustomerName : String):TCustomer;overload; function GetCustomer(OrderId : Integer;OrderDate : TDateTime):TCustomer;overload;
Overloaded subroutines are a great way to provide flexibility in your code. Without them, we would be forced to create similarly named subroutines and then remember which parameters each version required! The key to overloaded subroutines is creating distinguished parameter lists. The application dynamically determines which subroutine is appropriate by what parameters the caller sends to the subroutine. Each version of a subroutine must add the overload directive to the end of its declaration. This notifies the compiler that the subroutines should be treated individually (even though the names are the same.) It is vital that the call is not ambiguous; that is, the application must be able to clearly determine what subroutine call is intended. Say, for instance, that the application needs to retrieve the customer associated with a particular order. This version of the GetCustomer subroutine might be declared this way: // causes a compiler error! function GetCustomer(OrderId : Integer):TCustomer;overload;
This is an example of an ambiguous subroutine (when it is declared along with the other GetCustomer versions shown above). If this subroutine was called, the compiler would have no way of knowing whether the integer supplied was supposed to be an OrderId or a CustomerId. Note that neither the return type nor the subroutine type (procedure or function) is sufficient to clearly determine the subroutine. The following two subroutine declarations would also cause compile-time errors. // compiler error! Return type does not distinguish a subroutine. function GetCustomer(CustomerId : Integer):Integer;overload; // compiler error! subroutine type does not distinguish a subroutine. procedure GetCustomer(CustomerId : Integer);overload;
110
Chapter 5: Object Pascal
Default Parameters Kylix allows developers to provide default values for function and procedure parameters. This essentially creates optional parameters that the developer can supply or not supply at his or her discretion. This is a very powerful tool, given that the same method may be called by two different clients, each of whom has a different amount of information. For example, say that you have created a procedure that draws a rectangle on the screen. The method might be defined one of these ways: procedure TfrmMain.DrawRectangle();overload; procedure TfrmMain.DrawRectangle(Height, Width:Integer);overload; procedure TfrmMain.DrawRectangle(Height, Width, Border: Integer; Color:TColor);overload;
The third version of the method gives the developer the most choice about exactly how to create the rectangle, but the fact is that some callers of this subroutine might not care about the color of the rectangle. Perhaps to most callers, all rectangles should be red, and it is only in rare cases that a different color should be used. Instead of forcing all of the developers to remember what color the rectangles they create should be, the subroutine could be redeclared like this: procedure TfrmMain.DrawRectangle(Height, Width, Border: Integer; Color:TColor = clRed);
This declaration provides a default value for the Color parameter and gives the user (caller) the option of only sending two parameters and omitting the third. Warning: Default methods can be the cause of ambiguous function calls. If an overloaded method contains a default parameter that is the only distinguishing attribute between two methods’ parameter lists, then the call is still ambiguous! The following example would not compile: procedure TfrmMain.DrawRectangle(Height, Width, Border:Integer); procedure TfrmMain.DrawRectangle(Height, Width, Border:Integer;Color:TColor = clRed);
Data Storage Storing data is a vital task in almost every application. Data must be passed around and cannot always be broken down into a simple value, but may be comprised of a collection of values of a common entity. Pascal provides a number of ways to store complex data and to refer to and access that data simply and efficiently.
Sets A set is a collection of unique values that are of the same ordinal type. Sets are subsets of their base type and can include from 0 (the empty set) up to 256 values. Sets can be declared either by their type or by a specific set of values. If a set is declared with a base type with more than 256 elements, the compiler will throw an error.
Chapter 5: Object Pascal
111
Notice the use of the keyword in. The in operator determines if a value is currently included in the set. type TLetters = set of 'a'..'z'; TIntSet = set of Integer; //ERROR: Too many elements. TIntSet = set of 0..200; // Valid set declaration. { later in the code… } var MySet : TIntSet; begin // initialize the set with all values in both ranges (1..10 and // 100..110). MySet := [1..10,100..110]; if 55 in MySet then ShowMessage('55 is a member of the set.'); end;
Sets can be declared on the fly by explicitly listing their type in the variable declaration. var MyCharSet : set of 'a'..'z';
Arrays An array is a data structure that holds a finite number of elements and maintains a unique index. All elements must be of the same type (unless it is an array of variants, see Chapter 9) and may include both primitive and non-primitive types. Arrays are used for a variety of tasks from maintaining a list of strings to storing a group of object references. Array types can be declared explicitly in a type section or on the fly as they are used. Either declaration type is equally acceptable, but as a rule, should be determined based on the intended usage of the array. If you are planning on using an array that holds ten integers consistently throughout a section of source code, it is usually better to declare the array type explicitly in a type section. The new type can then be used to easily define array variables of that type. type TIntArray = array[1..10] of integer; { later that day… } var AgeArray : TIntArray;
112
Chapter 5: Object Pascal
On the other hand, if you are creating a localized array intended only for one use, the array may be defined on the fly. This will keep your type sections from filling up with a collection of seldomly or singularly used types. The following shows an example of an array that is declared on the fly: var AgeArray : array[1..10] of integer;
In both cases, the array is declared by using the keyword array followed by an index boundary. Index boundaries must be of an ordinal type (to guarantee a finite number of items). Unlike many other languages, Pascal does not require array indexes to be zero-based. The following array declarations are all legal. type TIntArray = array[1..10] of integer; TNameArray = array['A'..'Z'] of integer; TStringArray = array[false..true] of String;
Tip: Although Pascal does not require array indexes to be zero-based, it is important to be familiar with their use (off by one errors, etc.). Many Kylix components that include array-type properties are declared as zero-based arrays. Array Constants
Arrays can be initialized inline to their declarations. Array constants provide a list of values that are used to preset the elements of the array. To declare an array constant, list the values (separated by commas) in parentheses after the array declaration. var AgeArray : array[1..10] of integer = (1,15,23,27,32,32,40,41,42);
Static Arrays
To use an array, simply declare a variable of the array type and access an element at a particular index by specifying the index in brackets. This type of array is called a static array. Like primitive types, the memory for a static array is determined at compile time and cannot be adjusted at run time. No initialization of the array is necessary. Once a static array variable is declared, it can be used right away, as in the following example. var AgeArray : array[1..10] of integer; begin AgeArray[1] := 15; AgeArray[2] := 20; AgeArray[9] := 65;
Chapter 5: Object Pascal
113
{ … } end;
Dynamic Arrays
Object Pascal also supports dynamic arrays. A dynamic array has a size that is unknown at compile time. Dynamic arrays are used to store a variable number of elements. They differ from static arrays in one very important way: The developer must explicitly allocate memory for the array at run time. Similarly, the developer is responsible for destroying the array when he is done using it. This is accomplished by setting the array to nil (of course, if the array holds object references, you must destroy the objects first!). A dynamic array can also be destroyed by passing it to the Finalize procedure, setting it equal to 0, or using the SetLength function, passing zero as the NewLength parameter. The following code demonstrates the use of dynamic arrays: Warning: The following code would normally be surrounded by exception handling code. However, since we haven’t talked about exceptions yet, this seems premature. Exceptions and exception handling is covered in detail in Chapter 10. procedure TForm1.btnCustomerNamesClick(Sender: TObject); var CustomerNames: array of String; begin CustomerNames[7] := 'Bad Code'; // This line would blow up! SetLength(CustomerNames,15); // You must call SetLength first. CustomerNames[3] := 'Ken Faw'; ShowMessage(CustomerNames[3]); CustomerNames := nil; end;
Tip: Although dynamic arrays sound pretty slick, they are not used as frequently as you might think. This is because dynamic collections of objects are usually stored and managed by a TList or TStringList object Multidimensional Arrays
Multidimensional arrays store data in more than one dimension; that is, they have more than one index. Multidimensional arrays are defined by using two or more array indexes separated by commas. As an example, imagine that you want to store a group of screen locations for use with a drawing program. You might create an array that looks like this: implementation var PointArray: array[1..10,'x'..'y'] of integer;
114
Chapter 5: Object Pascal procedure TForm1.btnSetPointsClick(Sender: TObject); begin PointArray[1,x] := 10; PointArray[1,y] := 10; PointArray[2,x] := 10; PointArray[2,y] := 15; { More code goes here... } end; procedure TForm1.btnDrawObjectClick(Sender: TObject); begin for I := 1..10 do // See below for info on the for loop. DrawPoint(PointArray[I,'x'],PointArray[I,'y']); end;
Tip: To determine the length of any array, use the Length function, found in System.pas. Length returns the number of elements contained in the array. The Low and High functions return the lower and upper boundaries of the array. These functions can be used to protect against errors caused by accessing indexes which are out of bounds.
Records A record represents a collection of potentially diverse elements, called fields, stored in a single identifier. Records are more dynamic than arrays from the perspective of the data they can hold. A record can hold multiple data elements of varying types. Records are declared by declaring the record type, followed by the fields of the record. type TCustomer = record CustomerID:Integer; Name:String; Address:String; City:String; State:String; Zip:Integer; Phone:Integer; Balance:double; end; // TCustomer record.
Chapter 5: Object Pascal
115
Records are declared like any other variable. To assign or access a particular field in a record, use the dot (.) notation. var CustomerRec, CurrentCustomer:TCustomer; begin CustomerRec.CustomerID := 11354; CustomerRec.Name := 'PILLAR Technology Group, Inc.'; CurrentCustomer := CustomerRec; end;
Notice that records can be assigned to other records. The effect of assigning in this way is that all of the field values of the source record are copied to the target record.
Classes and Objects While it is important to understand the basic elements of the Object Pascal language, it is just as important to understand how and when they are applied in production systems. The preceding sections have been discussing various ways in which to store data within the application. However, these techniques are fairly rudimentary from the perspective of an application’s intent, which is to accurately represent something in a program that exists in the business world. A sales application, for instance, needs to be able to deal with customers and so should include an entity that accurately represents customers. This is extremely difficult, if not impossible, to do in an array, because array data must all be of the same type. Records help the situation by allowing fields of differing data types to be declared within the same storage device. However, even a record cannot fully encapsulate what it is to be a customer. Why? Behavior. In the real world, customers are not simply a collection of addresses and account numbers and phone numbers. They are a dynamic part of a process that runs every business. If an application cannot include in a customer the concepts of behavior, then it is not precisely representing what a customer is. Chapter 7 discusses the design of systems that are comprised of a set of collaborating objects. In this section, we focus on the structure, creation, and use of classes and objects. Developers often misunderstand objects and specifically have trouble identifying the relationship between classes and objects. The two terms are often used incorrectly in conversation and therefore have the potential to cause a great deal of confusion. It is correct to say that an object represents a particular example of (or more commonly an instance of) a class. That is, a class declares (or describes) the anatomy of an object.
Chapter 5: Object Pascal
AM FL Y
There are a number of popular analogies that help to explain the relationship of classes and objects. For example, say that you are building a new house. The first step is to have an architect develop a blueprint for the house. The blueprint is not a house. You cannot move into a blueprint. You cannot paint a blueprint (well, you could but it would probably be a bad idea!). The purpose of the blueprint is to precisely describe the dimensions, organization, and layout of the house that is going to be built. The blueprint is similar in function to a class. Classes do not take up any space. They are only declarations of an object that is yet to be created. Once the architect’s blueprint has been finalized, the next step is to actually build the house. First, land must be set aside and prepared for the house. Once the house has a piece of land to sit on, construction materials such as wood, brick, and shingles (and one or two other things!) must be gathered or created for the house. When all of the materials have been gathered, the house is actually built according to the blueprint’s specification. The process of constructing the house is analogous to instantiation. To create an object, you must instantiate its class. Instantiation is the process of requesting and initializing memory for an object according to the specification of its class. In Object Pascal, this is accomplished by calling the class’s constructor, which is a method of the class, usually called Create. The constructor’s job is to create an instance of an object based on its class declarations. Once an object has been created, its methods and properties (also called instance variables) are available for invocation and assignment. Each class also contains a destructor method normally called Destroy. The Destroy method is used for cleaning up resources used by the object. Constructors and destructors are further discussed in the following sections. The TEmployeeInfo class shown below demonstrates the declaration and implementation of a basic class and its constructor and destructor. The constructor’s job is to create an EmployeeInfo object by requesting memory from the system (that code occurs deeper within the CLX hierarchy) and to initialize the object’s instance variables. Notice that the TEmployeeInfo class declares a property of type TStringList. TStringList is a class that can be used to maintain a dynamic list of strings. Unlike primitive types, class-type variables like FamilyMembers are not automatically assigned memory by the system but must be explicitly instantiated by their owning object. TStringList is itself a class that must be instantiated before it can be used. The TEmployeeInfo constructor takes care of this by calling the constructor of the TStringList class. Once FamilyMembers has been created, the constructor initializes it with a few values. When any TEmployeeInfo type object is destroyed, its destructor cleans up the object by freeing the TStringList object that is owned by the EmployeeInfo object.
TE
116
type TEmployeeInfo = class Name : String; Age : Byte; Phone : String; FamilyMembers : TStringList;
Chapter 5: Object Pascal
117
constructor Create; destructor Destroy; end; constructor TEmployeeInfo.Create; begin Name := 25; Age := 25; Phone := 555 – 5555; FamilyMembers := TStringList.Create; // Create an instance of TStringList. FamilyMembers.Add('Bethany'); FamilyMembers.Add('Nicholas'); FamilyMembers.Add('Fido'); end; destructor TEmployeeInfo.Destroy; begin FamilyMembers.Free; end;
The above example shows not only the creation of constructors but also their usage (with the use of the TStringList class’s constructor). To create an EmployeeInfo object, simply call its constructor with the following syntax: := .; Another example is listed below. var CurrentEmployee : TEmployeeInfo; procedure GetEmployee; begin CurrentEmployee := TEmployeInfo.Create; end;
Tip: Constructors are not required to be named Create but that is the most commonly used convention. It makes the creation of objects somewhat uniform.
Class Scope The next chapter, “Application Architecture,” discusses the concept of scope from a variety of levels. Scope refers to the visibility of a piece of information or functionality. Objects contain their own scoping level and a collection of modifiers. Just because an object is public does not mean that users of that object should have access to every detail of that object. Class scoping modifiers help to define a precise interface through which users of an object interact with that object.
118
Chapter 5: Object Pascal
Private As the name indicates, the private modifier restricts the property or method to be accessed only from within that object. Even if another unit can access that object, it cannot access its private members. Tip: Many languages support the notion of objects being “friends.” Objects that are friends are able to access each other’s private identifiers and methods. In C++, two objects are friends only if they are declared to be so. In Kylix, any two classes that are declared in the same unit are automatically friends. Although there are times when friend objects can be useful, it is important to understand that by using the friendship concept, an object is allowing someone else to invade a private area that is not generally meant to be accessed from the outside.
Protected An object’s protected members are available for use by the class in which they are declared and by any object that is a descendant of that object. Protected members are accessed only from within the implementation of that object or a descendant (more on descendants in Chapter 7). Users of the object cannot access protected members even if they happen to be other instances of the same object! Protected members will be discussed in more detail in Chapter 16.
Public Public members are available to anyone who can access the object. Public members help to declare a specific interface through which users of the object interact with it. Commonly, public properties are actually stored as private members and accessed through a pair of Get and Set methods. Properties are discussed further in Chapter 16.
Published Published and public object members have the same level of visibility. Published members can also be seen by anyone who can access the object. The difference between public and published members is that runtime type information (RTTI) is generated for published members. RTTI allows other objects to dynamically determine the abilities of the object. Kylix uses RTTI information to determine the properties that should be displayed in the Object Inspector. More on published members can be found in the Kylix help files.
Chapter 5: Object Pascal
119
Object Methods Objects contain not only properties but also methods and events. A method is a procedure or function that belongs to (is a member of) a class. Kylix allows procedures and functions to be declared outside of a class, but as we will see in Chapter 7, it is generally better to declare them as part of the object for which they are performing a piece of functionality. Constructors and destructors are also considered object methods.
Instance Methods Typically, methods are declared in such a way as to belong to the instance of the object through which they are called. In other words, a method (more accurately known as an instance method) can only be invoked through a reference to a particular object instance of the class in which it was declared. In short, you must create an object before you can call its methods.
Class Methods Occasionally, it is helpful to declare a method that is invoked on the class itself, rather then on individual instantiations of the class. Such a method is called a class method. Many languages, including Java and C++, make use of the class method through the static modifier. In Object Pascal, class methods are designated by using the word class before the word function or procedure in the method declaration. Class Method Example: The Singleton Pattern
A great example of the use of class methods involves the implementation of an object-oriented pattern known as the singleton pattern. A singleton is an object that can only be instantiated once for a given system. Subsequent creations of the object result in the return of an object reference to the already existing instance. In this way, all clients of an object are communicating with the same object. unit LogFileWriter; interface type TLogFileWriter = class private constructor Create; public class function GetLogFileWriter:TLogFileWriter; end; implementation
120
Chapter 5: Object Pascal var FLogFileWriter:TLogFileWriter; { TLogFileWriter } constructor TLogFileWriter.Create; begin inherited; end; class function TLogFileWriter.GetLogFileWriter: TLogFileWriter; begin if FLogFileWriter = nil then FLogFileWriter := TLogFileWriter.Create; Result := FLogFileWriter; end; end.
The code shown above demonstrates the creation of a singleton object in Kylix. Notice that the constructor is declared to be private, which at first seems odd given that no one then is permitted to create a LogFileWriter object. TLogFileWriter also contains a public class function named GetLogFileWriter. This method can be called directly on the class instead of on a particular instance of the class. Finally, a variable of type TLogFileWriter is declared in the implementation section of the unit. The GetLogFileWriter method first checks to see if an object instance has already been created and, if not, creates one by calling its own private constructor. An object reference is then returned to the user, regardless of whether it was just created or already existed. In this way, the object can only be instantiated once and is shared by all clients. A typical request for a LogFileWriter object might look like this: procedure TClientObject.OpenLogFile; var LogObject : TLogFileWriter; begin LogObject := TLogFileWriter.GetLogFileWriter; end;
No matter how many times or by how many clients the GetLogFileWriter method is invoked, everyone will receive a reference to the same object. Singleton objects are extremely useful for avoiding certain types of concurrency issues and for centralizing access to a particular object or resource.
Chapter 5: Object Pascal
121
Tip: For more on the singleton and other object-oriented patterns, check out “Applying Design Patterns in Delphi and Kylix” by Xavier Pacheco.
Class References Sometimes, a class must be created whose type is not known at compile time. The application may create a different type of object depending on the state of the system. Kylix allows developers to create a class reference (also called a metaclass) in the following manner: type TControlClass = class of TControl;
Once the class reference has been declared, functions can call a constructor of that class, even though they do not know (at compile time) exactly what type the class is. In the following code, the CreateControl function creates and initializes any type of control class it is passed and adds it to an already existing form. function CreateControl(ControlClass: TControlClass; const ControlName: string; X, Y, W, H: Integer): TControl; begin Result := ControlClass.Create(MainForm); with Result do begin Parent := MainForm; Name := ControlName; SetBounds(X, Y, W, H); Visible := True; end; end;
Understanding classes and objects is vital to proficient use (and reuse) of application entities. In Chapters 6 and 7, we look at the use of classes and objects to create flexible and extensible applications that accurately represent the business needs of the application.
122
Chapter 5: Object Pascal
Summary The Object Pascal language provides programmers with an easy-to-learn, easy-towrite, and easy-to-read environment for the development of complex applications. Using the basic building blocks of Pascal, programmers are able to create logically intricate and reusable code elements that work together to perform an almost limitless set of behaviors. This chapter gave programmers a look at the basic pieces of Pascal, as well as the program flow and data storage elements that contribute to a sensibly written application. Now that the base language of Kylix has been discussed, we can move on to a discussion of the organization of applications and the types of programs that can be built with these simple concepts.
Chapter 6
Application Application Architecture Architecture
Introduction In any application, functions, procedures, and variables must interact in order to provide a set of data and behaviors that make the application run. The issue of scope is related to the concept of visibility or “who can see me?” Understanding scope is critically important to creating organized code and can make the maintenance and extension of an application much easier.
A Comparison of Development Models Methods of development have changed a great deal in the last ten years (actually, development methods are constantly changing!). Two often-used and often-contrasting styles of development are procedural and modular. Procedural programming (also called structured programming) is most often associated with legacy code and tends to focus more on functionality and less on the organization and refactoring of that functionality. Procedural programs typically contain a large number of global variables and procedures/functions. A main procedure (sometimes called a driver) is then used to define the logical flow of the application. A modular program, on the other hand, is made up of a collection of modules that each contain the declarations and behavior relevant to that particular piece of the system. Each module may or may not be communicating with other modules in the system, depending on the relationships of the business entities they represent. The modules themselves may be made up of a collection of sub-modules, each with their own set of scoping considerations.
123
124
Chapter 6: Application Architecture
Figure 6-1: Procedural and modular development methods
Modular programming is generally a more defensive style of programming in which information is provided on a “need to know” basis. The use of global variables and methods is strongly discouraged except in specific circumstances where it is required. The goal of modular development is to, as much as possible, provide modules that are decoupled from one another, in accordance with the “black box” theory. The black box theory says that as long as the inputs and outputs of an entity remain constant, the implementation details of the process should be irrelevant to its use. In other words, modules (which may be units, objects, functions, etc.) are responsible for implementing certain behaviors that facilitate a designed relationship between different pieces of an application. However, one module that requires the services of another should not be dependent on a particular implementation of those services, rather it should simply make a request and await a result. Once modules can interact independently of their implementation (once they are decoupled), they are more likely to be able to be used as a set of changeable parts. Updates to a module do not affect other modules as long as the interface (the externally exposed identifiers and method signatures) remains the same. Modular development places a high priority on the ability of objects to interact without regard to the implementation details of a particular object.
Chapter 6: Application Architecture
125
Note: Modular development and object-oriented development are not necessarily the same thing. It is certainly possible to create a set of modules that execute procedural code. In Kylix, a set of units do not necessarily make a program object oriented. The unit is used as a container for one or more objects that are interacting with other objects in the system. In fact, the organization of units is a key factor in developing an organized set of modules whose relationships can be mapped to existing business relationships. Chapter 7 discusses the relationship and interaction of objects and the modules in which they exist. The benefits of using modules as boundaries for logically related behaviors are further enhanced by the use of objects. A module is commonly composed of a collection of collaborating objects that work together to provide a particular set of application behaviors. The differences between procedural and modular development will become more evident as object-oriented techniques are further explored. The next chapter explores this topic further and describes the short- and long-term benefits of using objects effectively.
Variable Scope: Global vs. Local Variables The issue of scope often begins with a discussion of global and local variables. Traditionally, global variables are those that are visible to the entire application, while local variables are defined within a function or a procedure and are only visible to that routine. The fact is that in modular development, the terms global and local are useless in and of themselves. They are meaningless without a qualification. A variable could be called global to an application, a unit, a section, or a method. On the other hand, that same variable could be said to be local to the program, the object, the method, or just about anything else. Global no longer means application-wide and local no longer means confined to a specific procedure or function.
Nested Variables Variables can be declared at multiple scoping levels within an application. This can cause confusion as to which variable is being referenced during a particular operation. In the following code, the counter variable has been declared twice: once at the unit level and once inside a function. (In Kylix applications, variables can also be declared at the unit level. For more on this, see the “Kylix Unit Architecture” section below.) program StudentCounter; uses Dialogs, sysutils;
Chapter 6: Application Architecture var Counter:Integer; // Global student counter.
AM FL Y
function CountFreshmen:Integer; var Counter:Integer; begin Counter := 0; ShowMessage('I''m counting 9th graders.'); while (CountingCondition true) do begin { Counting Code here… } inc(Counter); // Freshmen counter. inc(StudentCounter.Counter); // Global student counter. end; ShowMessage(IntToStr(Counter)); ShowMessage(IntToStr(StudentCounter.Counter)); Result := Counter; end; var TotalFreshmen, TotalSophomores, TotalJuniors, TotalSeniors : Integer; begin Counter := 0; TotalFreshmen := CountFreshmen;
TE
126
// These function implementations left out for brevity. TotalSophomores := CountSophomores; TotalJuniors := CountJuniors; TotalSeniors := CountSeniors; ShowMessage('There are '+IntToStr(Counter)+' students.'); end.
Tip: Notice that the variables declared in the preceding code are all named the same thing. While this is legal as long as the variables are not declared in the same scoping level, it can certainly cause a great deal of confusion for the developer. Be sure to name your variables and methods precisely. The CountFreshmen method contains code to total and return the number of freshmen in school. Before it returns, the method shows a message indicating both the number of freshmen and the current total number of students. Because the counter variable is declared twice, the application must dictate the intended counter variable to use in the message display. The first statement shows the number of
Chapter 6: Application Architecture
127
freshmen. When the Counter variable is used, local scope takes precedence and the function’s counter variable is used. To specify the application’s counter variable, preface the counter with the name of the application. If a variable is declared to be global to a unit instead of the entire application (which is more common), preface the variable with its unit name. Tip: Hopefully, it is becoming obvious that using classes and objects provides a significant benefit in terms of organization and scope. Typically, there would be a FreshmenClass object declared that would hold information about the freshman class. The class might look like this: Type TFreshmenClass = class StudentCount:Integer; // Other class info declared here. end;
With this declaration, the StudentCount variable is accessed through the object that contains it and its scope is easily recognizable by other developers and more clearly used throughout the application.
Nested Routines Just like variable declarations can be nested, entire procedures and functions can be defined within the scope of other procedures and declarations. This can be an effective way of preserving the modularity of code without exposing it to parts of the application that should not be able to invoke behaviors directly. program StudentCounter; uses Dialogs, sysutils; var Counter:Integer; // Global student counter. procedure CountStudentBody; var Counter:Integer; function CountFreshmen:Integer; var Counter:Integer; begin Counter := 0; ShowMessage('I''m counting 9th graders.'); while (CountingCondition true) do begin { Counting Code here… } inc(Counter); // Freshmen counter. inc(StudentCounter.Counter); // Global counter. end;
128
Chapter 6: Application Architecture ShowMessage(IntToStr(Counter)); ShowMessage(IntToStr(StudentCounter.Counter)); Result := Counter; end; // CountFreshmen function. begin // CountStudentBody procedure. Counter := 0; Counter := Counter + CountFreshmen; // These functions left out for brevity. Counter := Counter + CountSophomores; Counter := Counter + CountJuniors; Counter := Counter + CountSeniors; Result := Counter; end; // CountStudentBody procedure. begin Counter := 0; Counter := CountStudentBody; ShowMessage('There are '+IntToStr(Counter)+' students.'); end.
Tip: In the above code, notice that there are now three Counter variables. The new variable is declared at the level of the outer procedure, CountStudentBody. This variable can be accessed from within its own method, but cannot be accessed from within the CountFreshmen method. A procedural variable cannot explicitly state its scope and is hidden by variables of the same name within internal procedures and functions. procedure CountStudentBody; // This variable cannot be accessed from within the // CountFreshmen function. var Counter:Integer; function CountFreshmen:Integer; var Counter:Integer; begin Counter := 0; end;
Notice in the code above that the CountFreshmen function is written entirely within the bounds of the CountStudentBody procedure. With this declaration, no one can see the CountFreshmen function except the CountStudentBody procedure and any procedure or function declared within CountStudentBody. However, in order to see another function at the same scoping level, a function must be declared after the function that it uses.
Chapter 6: Application Architecture
129
function CountFreshmen:Integer; var Counter:Integer; begin { Count Freshmen code here… } end; function DoRookieCount:Integer; begin ShowMessage(IntToStr(CountFreshmen)); end;
Using Scope Effectively in Kylix Applications Effective use of scope in Kylix applications is at the discretion of the developer. Even though Kylix strongly supports the modularization of application code, it does not force it. Using units does make a program modular, but it does not take advantage of the benefits that modular programming is designed to create. This section describes the use of scope with regard to unit communication and unit and class architecture.
Unit Communication (The uses Clause) In order for any modular application to be effective, modules must communicate with other modules in the application. As has already been stated, each module must communicate with a specific set of other modules to provide a particular set of behaviors. Modules that have no natural business relationship are not made available to each other. How does one unit gain access to another one? Through the uses clause. The uses clause lists the units that a unit has access to. Even though we have not interacted with the uses clause, we have been using it all along. Each new form unit already includes a uses clause that lists a number of units. Even though there is no apparent code related to it, the form unit depends on the uses clause in order to compile. unit Unit1; interface uses SysUtils, Types, Classes, Variants, QGraphics, QControls, QForms, QDialogs, QStdCtrls; type TForm1 = class(TForm) Edit1:TEdit; Button1:TButton; private
130
Chapter 6: Application Architecture { Private declarations } public { Public declarations } end; var Form1: TForm1;
The code above shows the interface section of a standard form. An edit control and a button have been placed on the form. Notice that the properties, methods, and events Edit1 and Button1 are not explicitly listed. Their class members are listed along with their declaration in the QStdCtrls unit. When the TForm1 constructor is called, it goes through the form class, calling the constructor of any objects that the form owns. In order to have access to the TEdit and TButton constructors that are needed for the TForm class to be created, Unit1 must have access to the QStdCtrls unit (where the TEdit and TButton constructors are declared). The unit gains access to the QStdCtrls unit by placing it in a uses clause. This tells the compiler, “If you can’t find something in this unit, look here (in this case, QStdCtrls) for its definition.” If an element is used but not defined in a unit, the compiler will generate an undeclared identifier error. Once the unit in which the element is declared is used, the compiler will be able to find the declaration of the element and will use it to build the application. Tip: Note that the QStdCtrls unit is not included with forms by default. When a component from the component palette is placed onto a form, Kylix automatically includes its unit in the form’s uses clause.
Circular Unit Reference In a modular application, units are using other units all the time. Each relationship demands unit communication in at least one direction. Sometimes, two units rely on each other for information. That is, they each must use the other. According to what we’ve said already, each unit should declare a uses clause in its interface section that includes a reference to the other unit. Unfortunately, this causes a compiletime error. When two units use each other in their respective interface sections, a circular unit reference error occurs. [Fatal Error] Unit1.pas(7): Circular unit reference to 'Unit1'
Fortunately, the uses clause (just like the var clause or the type clause) can also be declared in the implementation section. In the following code, the Unit1 reference to Unit2 has been moved to the implementation section. This avoids the circular unit reference. The rule about circular unit references does not apply to the implementation section. Any unit may reference any other unit in the implementation section regardless of who is using it.
Chapter 6: Application Architecture unit Unit1; interface uses SysUtils, Types, Classes, Variants, QGraphics, QControls, QForms, QDialogs, QStdCtrls; type TForm1 = class(TForm) Edit1:TEdit; Button1:TButton; private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation uses Unit2; {$R *.xfm} procedure TForm1.Button1Click(Sender:TObject); begin ShowMessage(Form2.Edit1.Text); end; end.
Unit2, as you might imagine, bears a striking resemblance to Unit1. unit Unit2; interface uses SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs, QStdCtrls; type TForm2 = class(TForm) Edit2: TEdit;
131
132
Chapter 6: Application Architecture Button2: TButton; procedure Button2Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form2: TForm2; implementation uses Unit1; {$R *.xfm} procedure TForm2.Button2Click(Sender: TObject); begin ShowMessage(Form1.Edit1.Text); end; end.
Notice that a unit does not have to be used until it is used; that is, since Unit1 does not refer to any elements of Unit2 until the implementation section, it is not required to include Unit2 in the interface section’s uses clause. QStdCtrls, on the other hand, must be used in the interface section because Unit1 refers to it (by using the TEdit class) in a declaration (the class TForm1) that is located in the interface section. Tip: It is generally a good idea to limit the number of unit names in the interface portion of a unit, and to put unit names in the uses clause in the implementation section if possible. This reduces the likelihood that a circular reference will occur and limits the scope of used units as much as possible.
Kylix Unit Architecture To understand the role that scope plays in Kylix applications, we must first understand the architecture of the basic Kylix module, the unit. A unit is one source code file that can contain just about any piece of functionality that the programmer desires. The following code is the default code for a unit that declares and implements a form. The unit is broken up into a number of sections and clauses, one of which houses the declaration of the form class. Notice that the class itself (TForm1) contains a private and a public section. Classes have their own set of scoping rules that are discussed later in this chapter. For now, we will focus on scoping as it relates to a Kylix unit.
Chapter 6: Application Architecture
133
Kylix units are divided into four major sections: Interface, Implementation, Initialization, and Finalization. Each section provides a different functionality and a different set of scoping rules. Unit sections do not use specific syntax to delimit their boundaries; that is, there is no begin and end to a section. Once a section keyword is encountered, the compiler applies its scoping rules until the next section keyword or the unit’s final end is found. unit StudentBodyCounter; interface type TStudentBodyCounter = class TotalStudentCount:Integer; function GetFreshmenCount:Integer; function GetSophomoreCount:Integer; function GetJuniorCount:Integer; function GetSeniorCount:Integer; end;
implementation { TStudentBodyCounter } function TStudentBodyCounter.GetFreshmenCount: Integer; begin { retrieve Freshmen data...} while not (true){end of list} do Result := Result + 1; end; function TStudentBodyCounter.GetSophomoreCount: Integer; begin { retrieve Sophomore data...} while not (true){end of list} do Result := Result + 1; end; function TStudentBodyCounter.GetJuniorCount: Integer; begin { retrieve Junior data...} while not (true){end of list} do Result := Result + 1; end;
134
Chapter 6: Application Architecture function TStudentBodyCounter.GetSeniorCount: Integer; begin { retrieve Senior data...} while not (true){end of list} do Result := Result + 1; end; end.
Interface Section After the name of the unit (unit StudentBodyCounter), the first element of the unit is the keyword interface. Anything that is declared in the interface section is visible to the entire unit in which it is declared, as well as any other unit that is communicating with (i.e., uses) this unit. It is important to note that this does not mean they are traditional global variables. Global variables are accessible to any part of the application. These elements are only accessible to units that reference this unit. For example, if this were the inventory unit, it is likely that the stock unit would be talking to it, but it is unlikely that the human resources unit would do the same. The stock unit and the human resources unit do not have a natural business relationship, so their program modules would not have one either. Warning: Form-based units always include a variable of the declared form class type in the interface section. If the form is auto-created by the application, the variable contains a reference to the created form. However, if the form is not auto-created (which is much more often the case), the variable has a nil value. That means that any reference to the variable without explicitly instantiating the form and assigning it to that variable will result in an EAccessViolation. In addition, the concept of a form variable just “hanging out” in the unit’s interface section with no implied semantics for its use breaks a number of the concepts of object-oriented development and does not follow the “need to know basis” principle on which most objects’ functionality is provided to outsiders. Some people go so far as to suggest that this variable should be deleted from the unit each time a form is created. At the very least, it is to be used with extreme caution! As a general rule, only objects that must be used by other units should be declared in the interface section. As the section titled “A Comparison of Development Models” described, modular or object-oriented development does not subscribe to the “just throw it up there and we’ll use it if we need it” attitude. Each relationship in a modular application has a specific purpose, and elements should not be visible to modules with which they have no business reason to interact. The form class, for instance, is an object that normally is instantiated by another unit. A main form’s menu item may instantiate another form when it is clicked. In order to create the
Chapter 6: Application Architecture
135
new form, the unit in which the menu is declared must have access to the unit that declares the form to be created. unit StudentBodyCounter; interface type TStudentBodyCounter = class function GetStudentBodyCount:Integer; function GetFreshmenCount:Integer; function GetSophomoreCount:Integer; function GetJuniorCount:Integer; function GetSeniorCount:Integer; end;
Implementation Section Following the interface section of the unit is the implementation section. The implementation section is where the actual work of the unit is done. While the interface section simply exposes the declarations and the external interface for a unit, the implementation section is where the specific execution of the functionality is coded. The implementation section can also contain variables, constants, and other type declarations, and is used primarily to implement the behavior described by the interface section. Elements declared in the implementation section can be seen only by that unit’s implementation section and only from the point at which it’s declared to the end of the section. implementation { TStudentBodyCounter } function TStudentBodyCounter.GetFreshmenCount: Integer; begin { retrieve Freshmen data...} while not (true){end of list} do Result := Result + 1; end; function TStudentBodyCounter.GetSophomoreCount: Integer; begin { retrieve Sophomore data...} while not (true){end of list} do Result := Result + 1; end;
Chapter 6: Application Architecture function TStudentBodyCounter.GetJuniorCount: Integer; begin { retrieve Junior data...} while not (true){end of list} do Result := Result + 1; end; function TStudentBodyCounter.GetSeniorCount: Integer; begin { retrieve Senior data...} while not (true){end of list} do Result := Result + 1; end; end.
AM FL Y
Initialization Section The initialization section contains statements that are executed when the application starts. Although it is not included in a unit by default, it can be defined by simply adding the word “initialization” to the end of the implementation section. This can be a good place to perform initializations and set up code. So if you have data structures that must be initialized, you can do this in the initialization section. This section is optional and executes statements in the order in which they appear in their unit. The initialization sections of a project’s units are executed in the order they are encountered by the compiler. Errors that occur in the startup of an application can often be traced to problems in the initialization section of a unit. (The same for finalization and errors at the close of an application.)
TE
136
Tip: The reason this section is not included by default is that it is not very common in general programming practice to use the initialization section. Most of the things that would be done in an initialization section could also be done in a constructor. Internalizing code to the class (instead of to the more generally scoped unit) is strongly encouraged.
Finalization Section As you might suspect, the finalization section performs the opposite task from the initialization section. Similarly, to define a finalization section, simply type “finalization” after the initialization section. This section of the unit can be used for cleaning up resources allocated in the corresponding initialization section. Finalization sections are executed when the main program terminates, and are executed in the opposite order from their corresponding initialization sections. In other words, if initialization sections execute for units 1, 2, and 3, then the finalization sections will
Chapter 6: Application Architecture
137
execute in the order 3, 2, 1. A finalization section must have a corresponding initialization section, but an initialization section can exist without a finalization section.
Using Scope in Kylix Classes Although Pascal allows the creation of variables, procedures, and functions outside of classes, they are discouraged in most situations. The last chapter discussed the value of using objects in applications. Just because an object is public (declared in the interface section) does not mean that users of that object should have access to every detail of that object. Class scoping modifiers help to define a precise interface through which users of an object interact with that object. This limits the class user’s view of the class to an externally exposed set of attributes and methods and keeps the developer from using the class in ways it was not intended to be used.
Private As the name indicates, the private modifier restricts the property or method to be accessed only from within that object. Even if another unit can access that object, it cannot access its private members. Tip: As noted in Chapter 5, some languages support the notion of objects being “friends.” Objects that are friends are able to access each other’s private identifiers and methods. In C++, two objects are friends only if they are declared to be so. In Kylix, any two classes that are declared in the same unit are automatically friends. Although there are times when friend objects can be useful, it is important to understand that by using the friendship concept, an object is allowing someone else to invade a private area that is not generally meant to be accessed from the outside. unit Bank; interface uses Classes; type TTeller = class private TellerID:Integer;// can be used by other classes in the unit. public TellerUserName:String; HireDate:TDateTime; Salary:Currency; end; TBank = class
138
Chapter 6: Application Architecture private public TellerList:TStringList; AccountList:TStringList; procedure Deposit(AccountNum:Integer; Amount:Currency;TellerObject:TTeller); end; implementation {TBank} procedure TBank.Deposit(AccountNum:Integer; Amount:Currency;TellerObject:TTeller); begin ShowMessage('A deposit of '+FloatToStr(Amount)+' has been made to account '+IntToStr(AccountNum)+ ' by teller number '+IntToStr(TellerObject.TellerID)); end; end.
Two classes are declared in the code above, TTeller and TBank. Although the ID of a particular teller is not public information, the bank object uses it to distinguish between tellers. Because TBank and TTeller are “friends,” bank objects can refer to the teller’s ID, even though it is a private property.
Protected An object’s protected members are available for use by the class in which they are declared and by any object that is a descendant of that object. Protected members are accessed only from within the implementation of that object or a descendant (more on descendants in the next chapter). Users of the object cannot access protected members even if they happen to be other instances of the same object! Protected members will be discussed in more detail in Chapter 16.
Public Public members are available to anyone who can access the object and to the entire unit in which they are declared. Public members help to declare a specific interface through which users of the object interact with it. Commonly, public properties are actually stored as private members and accessed through a pair of Get and Set methods. Properties are discussed further in Chapter 16.
Chapter 6: Application Architecture
139
Published Published class members have the highest level of visibility. For most situations, published members behave the same as public members. Any object that has access to a particular class also has access to its published members. The difference between public and published properties is that published properties include RTTI (runtime type information). RTTI allows an application to inspect an object to determine an object’s properties and their values. It is used, among other things, by the Kylix designer to display object properties in the Object Inspector and to attach event handlers to object events. In order to contain published members, a class must be compiled with the {$M+} compiler directive or descend from a class compiled with that directive. TPersistent is compiled with the {M+} directive, and thus all classes that descend from it publish RTTI information about its published class members.
Summary Using scope effectively is a vital part of creating clear and organized applications that only present information to those elements that should have access to it. This chapter discussed the techniques and constraints for defining program elements and for restricting their scope to the boundaries in which they are accessed. In the next chapter, we’ll see how to create objects that collaborate with each other intelligently to create well-designed systems. Each object has a specific set of attributes and behaviors that are only available to the other objects that require the information.
Object-Oriented Object-Oriented Development Development
Chapter 7
Introduction Chapter 6 explored the physical architecture and the issue of scope in units and applications. Chapter 7 continues our look at application architecture, but shifts our focus to address the logical and business design of a system. Part of creating a good application is allowing the business (and not the programming) to lead the way to success. This chapter takes a look at object-oriented techniques and at how they are used to create a flexible, extensible, and logically sound application. It’s important to understand that OOP (object-oriented programming) is not just a set of terms to debate, and it’s not just set aside for the analysis gods to hand down to the lowly programmers. It’s a mindset. It’s a way of thinking. It’s the key to successful development in the modern business world. Note: Everything You Ever Wanted to Know About Analysis Squeezed into Three Paragraphs It is important to note that the concepts of this chapter are not meant to replace good old-fashioned planning. The key element in the design of a good system is knowledge of the business. Not only how it is run, but also how it is likely to change or expand. A traditional approach to learning about a business involves the exploration and documentation of required data and data dependencies. Once a data model has been designed, a set of services and displays that manipulates that data can be built around it. For process flow applications and for most client/server programs, this can be an effective way to create code quickly and with minimal programming resources. When we look at a business, however, we see that it is not solely made up of data, but of people. What is important is not what we know about people, but rather how we interact with them. The roles we play within the system are defined by the interactions between both internal and external entities and by both the logistics and the semantics of those interactions. The key to the analysis of business systems lies in the documention of the interactions through the
141
142
Chapter 7: Object-Oriented Development processes by which the business runs and the entities and sub-processes that are required to accomplish those processes. The responsibility of the analyst is to document the processes and entities in a way that adheres to a more formally discovered software specification. Once the documentation has been generated, the application can be judged by its logistic and semantic implementation of the specification. One of the most common documentation methods is the Unified Modeling Language (UML). The specifics of UML are beyond the scope of this book, but more information can be found at http://www.omg.org/uml/.
The Four Pillars of OOP Understanding how classes and objects work and how they are used is vital to the use of Kylix and the Object Pascal language. But just because an application uses objects does not mean that it is an object-oriented application. To really apply object-oriented techniques in an application, you must first understand the basic principles of object-oriented methodology. The following section describes and demonstrates the use of object-oriented principles in a Kylix application. Tip: It is important to note that the following principles are not specific to Kylix but are able to be applied in any object-oriented language. Use of these techniques encourages good design and flexible and extensible applications.
Abstraction Abstraction is one of those OOP terms that people often regard lightly, but it actually provides one of the greatest benefits of OOP. (It’s part of that mindset thing we were talking about earlier!) Unfortunately, business analysts and programmers have not traditionally made the best of partners in the development process. The primary reason for this is that they tend to speak in completely different languages. Business people say things like “customer” and “inventory,” while developers tend to say things like “object”’ and “constraint” and “query.” You can see then why it is difficult for them to discuss the business intelligently. In order to create an application based on a business specification, there must be a mapping between business terms and development terms. This mapping is often responsible for inaccurate implementations of the business specification and can lead to multiple rewrites and increased development costs. Abstraction allows people from every department to come together on common ground. It is the ability of a language to accurately reflect the real-world entities and situations that it is trying to model. For example, the concept of a customer in an application might be represented by some 15 methods and 30 or so variables and constants. Under the procedural method of development, it is up to the developer
Chapter 7: Object-Oriented Development
143
to remember what things belong to the customer. Furthermore, none of the globally defined variables have any kind of inherent meaning. That is, nothing stops a developer from using the CustomerAddress variable to store, say, a product name or an order note. Misuse of identifiers is a common problem in procedural applications that can lead to “spaghetti code.” Even the most organized applications can be twisted out of shape as the application is maintained and extended. It is much easier to deal with customers if the developer can create a particular object, known as a customer. If developers can refer to customer, order, and inventory objects, then it is easier to think of the system in terms of business entities and their relationships.
Encapsulation Encapsulation is an often underestimated piece of OOP theory, especially if you come from a legacy or other type of procedural programming background. It is vital to successfully implementing object-oriented solutions. In general, encapsulation deals with the organization and internalization of storage and behavior. On a more practical level, it deals with the “single-mindedness” of an object. Encapsulation says that every object is responsible for a certain set of behaviors that programmatically define a process that occurs in the real world. Furthermore, it says that every piece of functionality in the system is some object’s responsibility to perform. To create a customer object, a TCustomer class is declared with methods like PlaceOrder, UpdateCustomerAddress, and PayInvoice. The customer class is not very likely to contain a method that reorders inventory items that are out of stock. This is because the job of the customer object is to fill the role of a customer, not to fill the roles of a customer and an inventory manager. As you can see, this would not only create confusion as to the location of functionality, but would also break apart the concept of abstraction. That is, the customer object would no longer accurately reflect the behaviors of a real-life customer. Asking, “Whose job is that?” is a good first step toward creating a well-encapsulated set of objects. Encapsulation is a difficult concept for many procedural programmers to accept. It directly conflicts with the school of “If it works, it must be right. Anything that gets the program working and out the door can’t be too bad.” Unfortunately, this train of thought is okay if your only concern is a timely deployment. But as many developers and managers have found, a poorly organized application can come back to haunt a department or a company for many months or even years. The cost of maintenance on poorly encapsulated systems is horrendous, especially in the software development industry, where the turnover rates are so high, and it is unlikely that the same developer will maintain an application for its entire lifetime.
Inheritance The third pillar of OOP is inheritance. Inheritance shows a much more practical side of object-oriented development in that it is more technique and less theory. Inheritance is the ability for an object to inherit the attributes and behaviors of another object. The descendant (also called a subclass or a child class) automatically
144
Chapter 7: Object-Oriented Development
takes on the declarations and implementations of its ancestor (also called a super class or a parent class) and then is free to add on specific behaviors and attributes of its own. The main advantage to this is in the concept of code reuse. Say, for instance, that you are building an application for a national auto dealer to keep track of its inventory. You would have to create classes for each kind of automobile that the dealer sells. There are thousands of different automobile types and for each type, there are hundreds of possible attributes and behaviors; but a very large number of these attributes and behaviors are common to all automobiles or at least a large subset. Without inheritance, each attribute and behavior for every class in the application would have to be explicitly declared. This scenario can lead to the “cut and paste” coding style, which can lead to a number of problems. Even if the common methods of the objects were coded identically (without mistakes), it would take up a great deal of time and money! Inheritance allows you to reuse previously defined code in descendant classes. In our automobile example, inheritance leads to a hierarchical structure of objects (classes). A small example of the resulting hierarchy is shown below: TAutomobile = class VIN:String; Make:String; Model:String; Year:Integer; Color:TColor; { lots more code here... } end; TCar = class(TAutomobile) BodyStyle:TCarStyle; EngineSize:TEngineSize; { lots more code here... } end; TTruck = class(TAutomobile) Length:Integer; TowingHitch:Boolean; LuggageRack:Boolean; EngineSize: TTruckEngineSize; { lots more code here... } end; TPickup = class(TTruck) BedSize:Integer; BedLiner:Boolean; BodySize:TPickupSize; { lots more code here... } end;
Figure 7-1: The automobile hierarchy
Chapter 7: Object-Oriented Development
145
Of course, this is a very small section of what would be a very large set of classes. Without inheritance, each class in the hierarchy would have to redeclare the same properties and methods. This is horribly redundant and a great waste of time for the developer. However, using inheritance, we can automatically give all of the functionality of the ancestor classes to their descendants, just by saying that the descendants inherit from the ancestors. Does a pickup truck have a VIN? Of course it does; it’s an automobile (and all automobiles have a VIN). Instead of specifically defining repeated functionality, we imply it by saying that a particular class takes on the characteristics of its ancestor class. Stated another way, the pickup and the automobile have an “is a” or a “kind of” relationship. That is, a pickup “is a” truck, but it is a particular “kind of” truck. It contains all of the things that we would expect a truck to have, but also defines specific features that only pickup trucks have.
Polymorphism The final pillar of object-oriented development is polymorphism. Polymorphism is the ability for different but related objects to interpret the same command in different ways. It is a mechanism by which a collection of related objects implement the same behavior in a way that is specific to each individual class from which they are created. In order to understand polymorphism, there are a couple of topics that must first be discussed. Once they are understood, we can look at the details of writing polymorphic objects and their methods.
Typecasting Typecasting is the ability to alter the perceived type of an object at run time. In keeping with the automobile theme, imagine that you are in a Kylix training class. Behind you is a picture window that looks out onto a busy road. If a Mercedes drives by, and the instructor says, “Hey, there goes a car,” is your first question likely to be “Was it the M class?” Probably not. Why? Because the instructor didn’t tell you that the car was a Mercedes. You were only told that there was a car going by. From your knowledge of cars, you might have asked about the make, model, or body style of the car, because those are things that all cars have. Was the instructor wrong? No. There certainly was a car going by. It just happened to be a very specific kind of car (unbeknownst to you). It’s important to note then, that it is perfectly legal to refer to an object as one of its ancestors. A Mercedes is certainly able to do at least all of the things that cars do. Because a Mercedes is a descendant of the car class, you can be sure that it has all of the things that cars have (through inheritance). The second point of note is that the “thing” that just drove by has two identities: its perceived type (car) and its actual type (Mercedes). The user of an object can only refer to that object in terms of its perceived type. However, a developer commonly has inside knowledge of the actual
146
Chapter 7: Object-Oriented Development
type of the object and must tell the application to treat it as what it is (instead of what the application thinks it is). On a primitive level, typecasting is accomplished by using common functions such as StrToInt and DateToStr. In each case the user passes in a value (of a certain type) and is returned the same value but of a different type. For instance, the expression IntToStr(35) evaluates to return the string “35”. Pascal supports two types of typecasting for class types, is/as and c-style. In both cases, typecasting allows the developer to alter the perceived type of an object.
Typecasting Example Most event handlers in Kylix are passed a parameter called Sender. Sender is of type TObject and refers to the object in which the event occurred. For instance in the following code, Sender tells you which item was clicked.
AM FL Y
procedure TForm1.MyButtonClick(Sender: TObject); begin { put some code here... } end;
TE
At first glance, it seems obvious that Sender refers to MyButton. However, this does not always have to be the case. It is important to remember that there is nothing special about this method that ties it to the MyButton button. Kylix simply uses the name of the component in the designer through which this code skeleton was created as a default for the name of the method. There are very likely other buttons (and menu items, list boxes, radio groups, etc.) that can be clicked and must perform the same functionality. This piece of the Kylix architecture allows the developer to write generic code that can be applied to more than one component. Take, for example, the maintenance of a toolbar and its associated menu items that are responsible for managing an open document. A typical toolbar includes buttons for opening, saving, printing, and closing a document (and probably several more!). Convention suggests that there would be a set of menu items that also provides this functionality. When the save button or the save menu item is clicked, it should be disabled until the document is modified again. One way to implement this functionality is to have two different event handlers that save the current document and disable the component (one for the toolbar button and one for the menu item). While this is a simple approach, it is remarkably wasteful in that the same logical statement is coded for both the button and the menu item. A more graceful solution would allow each component to share a common event handler that would save the document and disable itself. Remember that a reference to whatever component was clicked will be passed into the OnClick event through the sender parameter. From a logical perspective, it is not important whether the component is a menu item or a button. Whether the component is a button or a menu item, it is that component that must be disabled after saving the document. However, note that the sender parameter is of type TObject. For this reason, the following code would cause an error:
Chapter 7: Object-Oriented Development
147
procedure TForm1.MyButtonClick(Sender: TObject); begin CurrentDocument.Save; Sender.Enabled := False; // causes an error! end;
This is because the TObject class, unlike the TMenuItem and TButton classes, does not have an enabled property. Even though we happen to know that sender currently refers to a button (or a menu item), the application perceives sender’s type to be TObject. Therefore, it must be treated like a TObject. Typecasting allows us to change the application’s perceived type of Sender. The TObject class contains a method called as which allows us to dictate the perceived type of sender within a certain scope. Applying the as method to the object allows us to treat sender as its actual type. Once Sender has been cast as a TButton, it can then be treated as a button, that is, its enabled property can be accessed. procedure TForm1.MyButtonClick(Sender: TObject); begin CurrentDocument.Save; (Sender as TButton).Enabled := False; // does not cause an error! end;
Typecasting can also be implemented using the c-style casting method. C-style casting is done by simply prefacing the object with its type in parentheses: procedure TForm1.MyButtonClick(Sender: TObject); begin CurrentDocument.Save; (TButton(Sender)).Enabled := False; // does not cause an error! end;
As with most things in development, using the two casting styles involves a trade-off. C-style casting is generally considered to be a little faster than is/as casting. However, c-style casting can be dangerous. What would happen if an object was cast incorrectly? If the event handler above is attached to a menu item, then (Sender as TButton) would fail. Using as typecasting, this will generate an exception (which can be handled). If c-style casting is used, the result of the cast could be undefined, meaning you don’t necessarily know that the cast was unsuccessful and the response of the object to future access will have unpredictable results! Fortunately, the TObject class also provides the is method, which is a Boolean method through which an object may be asked what its type is. Once the type has been determined (and the cast is going to succeed), the c-style casting method is safer to use. The following code results and is a common typecasting method. procedure TForm1.MyButtonClick(Sender: TObject); begin CurrentDocument.Save; if (Sender is TButton) then
148
Chapter 7: Object-Oriented Development (TButton(Sender)).Enabled := False; // does not cause an error! end;
Warning: The more generic your code is, the more difficult it can be to read. If you commonly use this technique to reduce the amount of code in your application, it is your responsibility to comment that code appropriately to keep your teammates from wasting time figuring out where the rest of your code is!
Virtual Methods As a hierarchy of classes continues to grow, it is often necessary to augment or replace certain behaviors in descendant classes. Constructors provide a good example of the need for changes in behavior. Constructing an automobile requires the initialization of certain attributes, such as the VIN, make, and model. Each descendant of the TAutomobile class inherits this constructor along with its other methods and attributes. However, creating a truck is different than creating an automobile. TTruck declares attributes (and methods) that are not accounted for in the TAutomobile constructor. The TTruck class, then, must add on to the behavior of the constructor that it inherited from TAutomobile. In this case, the constructor of the TAutomobile class is said to be virtual, and once declared, it is allowed to be changed by any descendant class. In Kylix (Object Pascal), methods are static unless they are declared to be virtual. Once a method has been declared virtual, it is able to be overridden by any descendant for the remainder of the hierarchy. Each new descendant has the option (but not the requirement) of changing the last known implementation of the method. Descendant classes do not know, nor do they need to know, whether the method that they are overriding was itself overridden by the ancestor, or whether it was originally declared at that level. What is important is that the ancestor class includes a method that was originally declared to be virtual. Using virtual methods is a simple matter of understanding a pair of method directives. The virtual directive declares a method to be virtual, while the override directive states that a descendant class is, in fact, exercising the option to change the behavior of a previously declared virtual method. The following example demonstrates the use of these directives: unit AutomobileClasses; interface type TAutomobile = class VIN:String; Make:String; Model:String;
Chapter 7: Object-Oriented Development constructor Create; virtual; end; TTruck = class(TAutomobile) Length:Integer; TowingHitch:Boolean; LuggageRack:Boolean; EngineSize: TTruckEngineSize; constructor Create; override; end; implementation { TAutomobile } constructor TAutomobile.Create; begin inherited Create; // call to Tobject's constructor VIN := ''; Make := ''; Model := ''; end; { TTruck } constructor TTruck.Create; begin inherited Create; // call to Tautomobile's constructor Length := 96; TowingHitch := False; LuggageRack := False; EngineSize := V6; end; end.
Note: The Value of Virtual Methods The value of virtual methods is, at first, difficult to determine. Even if a method is not declared to be virtual, a descendant class can redeclare the method and make a call to the ancestor’s version of it by using the inherited keyword. It’s important to keep in mind that we are taking steps toward the goal of polymorphism and there are several pieces to the puzzle. The value of virtual methods will become clear when the specifics of how polymorphism works are explained in the next section.
149
150
Chapter 7: Object-Oriented Development
Putting It All Together: Polymorphism Remember that polymorphism is the ability for two or more different but related objects to interpret the same command in different ways. It allows an application to invoke the same method on a group of related objects, and to have them each perform the same task in a way that is specific to each individual class. Let’s change domains from the auto industry to the medical industry. Take, for example, a reporting system that is being implemented for a large hospital. There is a central reporting server that, among other things, makes requests to a collection of departmental reporting servers. One method that is required is a DoDaysEndReports method. Each department has a unique set of reports that must be generated at the end of every day. In addition, some departments may have month-end reports that need to be run if it is the last day of the month. Looking at this situation from a high level, it would be correct to say that every department must do the same things, but each department has a different way of doing them. One way to implement this requirement without polymorphism would be to create a collection of individually declared and implemented department servers, each of which knows its own reporting responsibilities. The problem is that in order for the corporate reporting server to notify each department that it is time to execute daily reports, it has to know the details of each department server and what methods to call in order to accomplish the required functionality. An example of the server’s dispatching method might look like this: procedure TCorporateReports.DispatchDailyReports; var DepartmentServers:TList; I:Integer; begin DepartmentServers := TList.Create; GetDepartmentServers(DepartmentServers); for I := 0 to DepartmentServers.Count-1 do with DepartmentServers.Items[i] do if ClassName = "AccountingReports" then (AccountingReports(DepartmentServers.Items[i])).DoAccountingReports else if ClassName = "HRReports" then (HRReports(DepartmentServers.Items[i])).DoHRReports else if ClassName = "OrderReports" then (OrderReports(DepartmentServers.Items[i])).DoOrderReports else { lots more code here... } end;
This implementation leads to headaches in both development and maintenance. DispatchDailyReports is just one example of a large number of methods that the server might have to invoke on all the departments. For each method, the corporate server has to check each departmental server for its type, cast it as such, and then know what method that server has implemented to achieve the required
Chapter 7: Object-Oriented Development
151
functionality. In addition, each time a new department is added, the corporate server would have to be altered in each method that referred to the individual departments. Now, using object-oriented techniques, let’s take another look at the reporting problem. The first thing to realize is that each department has basically the same responsibilities. Even though each one of them accomplishes those responsibilities in a unique way, they are all processing a collection of daily reports. Once the common set of responsibilities of departmental servers has been established, we can begin to see them as similar objects. Instead of creating a group of independent departmental servers that each has its own plan for doing its work, what if we created a collection of servers that is part of the same family, that is, the family of objects that must implement this common functionality. To begin, a basic departmental server is created that includes all the required declarations that each department will have to implement. type TDepartmentalReportingServer = class { ...other code goes here... } procedure DoDaysEndReports;virtual;abstract; { ...other reporting functions go here... } end;
Notice that the DoDaysEndReports method is declared as a virtual method. This is done because we know that every department must be able to implement this method differently, and we want to give descendants permission to alter the specific behavior of this method. What does it mean to DoDaysEndReports from a general perspective? Each department can define a set of objectives that must be satisfied at the end of each day, but what exactly does it mean to do days end reports at the base level? Can the method for accomplishing this reporting be precisely described for the general case? Probably not, because each department may have a completely different way of accomplishing this behavior (different reports, different storage, etc.). Notice that the base class’s DoDaysEndReports method is declared as abstract. This method directive makes it illegal for the method to be implemented in the TDepartmentalServer class. Why would we do this? Why would we declare a method that cannot be implemented and therefore can never be called? It is done because we know that this virtual method will have a different implementation for each descendant class and will be overridden. Its purpose is to provide a contract that says that every department (every object that descends from TDepartmentalServer) must know how to DoDaysEndReports.
152
Chapter 7: Object-Oriented Development
Tip: It is by no means a requirement for polymorphism that the methods of the base class be declared abstract. It is entirely possible that the ancestor class will implement a base functionality that can be added on to by its descendants. Many times, however, a class is declared as abstract in order to indicate functionality that should be provided by descendent classes. type TAccountingServer = class(TDepartmentalReportingServer) { ...other code goes here... } procedure DoDaysEndReports;override; { ...other reporting functions go here... } end; TAccountingServer = class(TDepartmentalReportingServer) { ...other code goes here... } procedure DoDaysEndReports;override; { ...other reporting functions go here... } end;
How Polymorphism Works The corporate reporting server is responsible for calling the DoDaysEndReports method for all department servers at the end of each day. Because each department reporting server is part of the reporting server family (that is, it is a descendant of the TDepartmentalReportingServer class), it is required to implement the DoDaysEndReports method. In addition, each server is able to be referred to as a TDepartmentalReportingServer. The corporate reporting server’s DispatchDailyReports method then can be reimplemented as follows: procedure TCorporateReports.DispatchDailyReports; var DepartmentServers:TList; I:Integer; begin DepartmentServers := TList.Create; GetDepartmentServers(DepartmentServers); for I := 0 to DepartmentServers.Count-1 do TDepartmentalReportingServer (DepartmentServers.Items[I].DoDaysEndReports); end;
Unlike the previously shown implementation, this version of the method does not need to check each server object for its type before calling the DoDaysEndReports method. The corporate reporting server can be certain of each department’s ability to execute this method because every descendant of TDepartmentalReportingServer is guaranteed to include this method. The remaining question, though, is
Chapter 7: Object-Oriented Development
153
why does this work? The TDepartmentalReportingServer’s DoDaysEndReports method is declared as an abstract method, meaning that it has no implementation. We know that the actual type of the objects in the list are descendants of the TDepartmentalReportingServer class and that they all have a validly overridden implementation of the method, but the code does not include a typecast that changes the perceived type of the object (which is TDepartmentalReportingServer) to its actual type. How then does the correct method implementation get called? The answer lies in the internal information that is embedded into every object instance. Each object instance in a Kylix application knows what class it was instantiated from. Each class in a hierarchy has a corresponding Virtual Method Table (VMT). The VMT is a table that contains a list of every virtual method that the class knows about, whether declared in the class or inherited from an ancestor. Methods of an object instance are dynamically dispatched at run time, according to the actual type of the object. In other words, the particular implementation of a method that is executed when a call is made depends on the actual type of the object instance that is being accessed and not on its perceived type (the type of the variable through which it is being referenced). The first 4 bytes of every object instance contain a pointer to the object’s class’s VMT.
Figure 7-2: Virtual Method Table references
154
Chapter 7: Object-Oriented Development
Tip: This is the value of using virtual methods in an application (see the note earlier in this chapter). If a method is not declared virtual, it will not be entered in its class’s VMT. Even if descendant classes call the method, it will not be able to be used polymorphically. Interaction with the VMT is done internally and understanding its use is not required for using polymorphism. However, it helps to explain not only what is done but why it is done. Think of all class-type variables as coat hooks. When an object reference is assigned to a variable, it is in effect hung on the hook and can be retrieved from that hook. Variable coat hooks have a special shape such that only certain types of coats (objects) and their descendants can hang on a particular type of hook. Even though many different kinds of objects (in this case, departmental reporting servers) can be hung on a particular hook, each object instance that might hang there is only aware of its own implementation of the DoDaysEndReports method. When the caller (in this case, the TCorporateReports.DispatchDailyReports method) invokes the DoDaysEndReports method, the object goes to its class’s VMT, looks up the location of the DoDaysEndReports method, and executes it. At another time, a completely different object instance is hanging on the same hook, but it is only aware of the DoDaysEndReports method as it exists in its class’s VMT. When the method is invoked, the object follows the same procedure but is looking at a completely different VMT. Therefore, a different implementation of the method is invoked. The value of this mechanism is that the caller (TCorporateReports.DispatchDailyReports) is totally unaware of the differences between the objects’ implementations of the method. Its job is only to invoke a particular method on a particular object, without worrying about the specifics of how that behavior is executed. Note: Dynamic Methods Methods can be declared with the dynamic modifier instead of the virtual modifier. Dynamic methods behave in exactly the same way as virtual methods but are stored differently. Every virtual method that a class knows about, whether explicitly overridden or simply inherited as is, is listed in the VMT. Dynamic methods, on the other hand, are only listed if they are explicitly overridden in the new class. When a dynamic method is invoked at run time, the application must propogate up the hierarchy looking through each class’s VMT for the last known reimplementation of the method. This can greatly decrease the size of each VMT but takes longer to locate a particular method implementation at run time. If a method is called frequently, or the execution time of the method is critical to the performance of the application, it is better to make a method virtual, rather than dynamic.
Chapter 7: Object-Oriented Development
155
Another Side of OOP While the preceding sections describe a very effective method of achieving polymorphic behavior in applications, it is important to realize that polymorphism can be achieved in other ways. The next sections describe and demonstrate another object-oriented construct, known as interfaces, and compares and contrasts them with the hierarchical methods shown above.
Abstract Classes As we have already seen, abstract methods are those that are not allowed to be implemented in the class in which they are defined. Instead, their methods must be declared as virtual or dynamic and be overridden by descendant classes. Why would we do that? What is the purpose of declaring an abstract method if it is just going to have to be redeclared and implemented? The value of abstract methods is that they provide a contract that future generations must follow. Even though the TDepartmentalServer’s DoDaysEndReports method doesn’t do anything, it forces the departmental servers (that descend from it) to implement the method. If a Kylix class is composed of nothing but a set of abstract method declarations, then it is (in effect) an abstract class and defines a contract for all of its descendants to sign, promising to implement a certain set of behavior. It is this contract that allows the compiler to be confident that no matter what the specific class type of an object instance in that family, the object will be able to perform any of the functionalities declared by the contract. Kylix provides a special element for defining such a contract, known as an interface.
Interfaces An interface is essentially a set of method declarations that collectively defines some set of functionality or services. Interfaces cannot be instantiated and do not provide an implementation for any of their declared methods. Their methods are by nature public and virtual. Looking again at the reporting server example, the Reports interface contains the following declarations: Reports = interface function GetReport(ReportName: string): Boolean; function GetReportList:TStringList; function CloseReport(ReportName: string): Boolean; function PrintReport(ReportName: string): Boolean; function DoDaysEndReports: Boolean; function GetReportCount: Integer; function ReportIsOpened(ReportName: string): Boolean; end;
The important difference between an interface and a class is that the interface only includes method declarations. Classes that implement the interface are free to do
Chapter 7: Object-Oriented Development
so in whatever manner is appropriate. In addition, application variables can hold a reference to any server object that implements that interface. This is illustrated in the next section. Tip: Interface names in Delphi have commonly been prefaced by the capital letter “I”. This primarily comes from a convention used in COM technology. Although this is a good convention to use (because it helps to distinguish classes and interfaces), it is by no means required. In the Java world, for example, this same convention is not generally applied.
Interfaces and Polymorphism
AM FL Y
Polymorphism has been as defined as “two different but related objects, interpreting the same command in different ways.” With classes, polymorphism rests on the fact that objects are descended from a common ancestor and have overridden methods in a unique way. However, polymorphism does not require that classes are implemented as part of the same hierarchy, only that they are related. This relationship is also established by two classes that implement the same interface as demonstrated below: TAccountsReceivableReports = class(TObject, Reports) function GetReport(ReportName: string): Boolean; override; function GetReportList:TstringList; override; function CloseReport(ReportName: string): Boolean; override; function PrintReport(ReportName: string): Boolean; override; function DoDaysEndReports: Boolean; override; function GetReportCount: Integer; override; function ReportIsOpened(ReportName: string): Boolean; override; end;
TE
156
THumanResourcesReports = class(TObject, Reports) function GetReport(ReportName: string): Boolean; override; function GetReportList:TstringList; override; function CloseReport(ReportName: string): Boolean; override; function PrintReport(ReportName: string): Boolean; override; function DoDaysEndReports: Boolean; override; function GetReportCount: Integer; override; function ReportIsOpened(ReportName: string): Boolean; override; end;
The TAccountsReceivableReports and the THumanResourcesReports classes both implement the Reports interface. This allows methods of both objects to be used by a client application polymorphically, even though the two classes have no family (hierarchical) relationship. For example, if the reporting system contained a management application that allowed reports from any department to be executed, it
Chapter 7: Object-Oriented Development
157
could be implemented generically, without concern for particular departments, as in the following example:
Figure 7-3: The Reporting Server Management application
// The ReportServers array can hold a reference to any // object that implements the Reports interface. var ReportServers : array[0..4] of Reports; procedure TfrmManagementReports.FormCreate(Sender:TObject); begin ReportServers[0] := TAcctsReceivableReports.Create; ReportServers[1] := TAcctsPayableReports.Create; ReportServers[2] := THumanResourcesReports.Create; ReportServers[3] := TPatientRecordsReports.Create; ReportServers[4] := THospitalAdminReports.Create; end; procedure TfrmManagementReports.btbtnExecuteReportsClick( Sender:TObject) begin with grpbxDaysEndReports do for I := 0 to ControlCount –1 do if ((Controls[i] as TCheckBox).Checked) then ReportServers[i].DoDaysEndReports; end;
In this example, the ExecuteReports button is used to generate days end reports for any selected department. The code used is similar to that used in the polymorphism section above but there is one important difference. In this case, none of the reporting servers have a common ancestor. They are not required to descend from the same class, rather they are only required to implement the same interface (namely, the Reports interface). The advantage to this approach is that as the system grows (more departments are added), new DepartmentalReportingServers are not required to be “retrofitted” into an existing hierarchy in order to be used by the application. Rather, any new object that implements the interface can be used by the calling code. An example of this is shown in the next section.
158
Chapter 7: Object-Oriented Development
Comparing Interfaces and Hierarchies Each department reporting server contains a collection of objects that it uses to collect and process data for its reports. The PatientReportingServer contains its own set of objects, including the TCustomer class and the TInPatient class, shown below. The TCustomer class is used for gathering general information, like a customer’s name, address, medical history, etc. It is also used to collect information for the accounts payable department and any available insurance information. Customer objects are used by the reporting system to gather information on outpatient customers. Outpatients are not in need of extended care and tend to have simpler hospital records. Inpatients, on the other hand, require some kind of extended medical care, and are more likely to have detailed medical chart information on diagnoses, prescriptions, and doctor’s orders that all needs to be stored by the system. Accordingly, the TInPatient class declares a number of additional fields and behaviors that relate only to Inpatients. The InPatient class, of course, descends from the TCustomer class because it also requires general, accounts payable, and insurance Figure 7-4: The initial information. Customer hierarchy So far, the difference between hierarchies and interfaces is not totally clear. The behavior of these classes could have been implemented through the use of interfaces, but what would the value be? The value of interfaces is shown when we begin to think about the maintenance of this system. The hospital decides to implement a new policy that each of its residents is required to spend one Saturday every six months at the hospital’s free homeless clinic. For legal reasons, the doctor is required to keep detailed records of the treatment given to each clinic patient seen. In these cases, however, the Accounts Payable and insurance information may not be applicable. In effect, all of the people the doctor sees are InPatients but none of them are Customers! Note: As another example, imagine that you are writing software for a travel agency. A travel agent talks to an individual person. Some of the people he or she talks to are customers. Some of the customers are passengers. Some of the passengers are AirlinePassengers. The resulting hierarchy is shown below.
Chapter 7: Object-Oriented Development
Figure 7-5: Travel agency hierarchy
An AirlinePassenger calls the travel agent and says that she is taking her dog “Fifi” with her to Boston, but is concerned about transporting her in the cargo hold. She would like to purchase a seat on the plane for Fifi’s kennel (yes, you can actually do that!). In order to reserve the seat, the travel agent must take passenger information about Fifi. But according to the hierarchy, Fifi can only be an airline passenger if she is a person! Since she is not, all of the memory about an AirlinePassenger that relates to a person or to customer information is unused and makes the required AirlinePassenger object a very inefficient solution to deal with Fifi in the application!
The clinic patient is actually not a TCustomer or a TInPatient but rather a hybrid of the two. The question to be answered in order to fix the problem in the integrity of the hierarchy is, “Where does the new TClinicPatient class fit into the family tree?” One solution would be to create a generic class, called TGeneral, that contained only the most basic of information. TCustomer and TClinicPatient both descend from TGeneral, and TInPatient still descends from TCustomer as shown at right. However, this scenario presents two major problems to the developers.
Figure 7-6: The revised Customer hierarchy
159
160
Chapter 7: Object-Oriented Development
The first problem is that TClinicPatient is just one example of a class that requires the attributes of the descendant (TInPatient), but none or few of the attributes of the ancestor (TCustomer). Another example might involve a patient who has no insurance and pays monthly installments toward his or her medical bills. For each new entity that is introduced into the system, the entire hierarchy must be rearranged so that all the classes can share common ancestors and behaviors. This can be done in two ways: n
Use a language that supports multiple inheritance.
n
For each new entity, create an ancestor (TGeneral) to the class with the functionality that must be split up (TCustomer) and factor out the common information; then create the new class (TClinicPatient) as a descendant of the new class (TGeneral).
Using either of these solutions, the hierarchy becomes a tangled web of countless generations of classes that represent an extremely high level of granularity and is a maintenance nightmare. The second problem with the solution shown above is its effect on polymorphism in the system. When the client application retrieves a customer based on a patientID, it currently stores the returned object in a TCustomer type variable. That variable must now be changed to TGeneral (or else it won’t be able to hold a reference to a TClinicPatient object). However, even if the variable’s type is changed, it would still be difficult to use the object returned polymorphically, because in order to use a method polymorphically in a hierarchy, the method must be originally declared in a common ancestor of the objects from which the polymorphic call might take place. The TInPatient and TClinicPatient classes contain common methods that the client application would like to call polymorphically, but in order to do this, those methods must be declared in the TGeneral class! This violates the encapsulation terribly in that it is not the job of the TGeneral class to provide methods that deal with patient behavior! So while hierarchies provide tremendous code reuse and can decrease initial development time, they can present difficult hurdles to those developers who are tasked with the maintenance of the application. Since applications must flex with the needs of the business, this is probably not the best design for the application. Note: The previous section is not intended to imply that inheritance and hierarchies are bad programming. Certainly, the original InPatient class could very well descend from the Customer class to take advantage of code reuse. The key is that the users of those objects (in this case, the corporate reporting server) do not rely on any familial relationship between the two but on what services they can provide (what interfaces they implement). In this way, new objects can be added to the system later without shaking up the organization of the classes.
Chapter 7: Object-Oriented Development
161
Summary The use of object-oriented methodologies takes practice! These concepts are not ones that developers usually hear for the first time and say, “I think I’ll go implement polymorphic behavior in my application right now!” Do not be discouraged if the use of these techniques seems difficult at first. The key is to recognize the commonalities of different objects in your system and to decide how best to organize a set of classes so as to embed the most code reuse, flexibility, and extensibility into your application. If this chapter seems muddled and confusing, go back and read it again. The more exposure you have to these techniques, the better prepared you’ll be to recognize similar situations in your own applications.
Chapter 8
Shared Objects and Shared Objects and Packages Packages
Introduction Shared object libraries and packages can dramatically reduce memory and disk space requirements for applications that share common code. Applications that need a plug-in architecture are another good example of using the power of shared object libraries and packages. Existing shared object libraries can be leveraged to reduce time to market. Kylix allows you to create Linux Shared Objects (SOs) as easily as creating DLLs in Delphi. Linux, however, has certain naming and versioning conventions that can make writing and using them tricky. Packages in Kylix are a shared object library that has additional functionality to overcome the limitations of normal Linux shared objects. Exception handling and displaying non-modal forms are two big reasons for using packages over shared object libraries. Packages are only usable with applications created with Kylix as they are not compatible with other languages or development environments. This chapter discusses shared objects and packages in depth, giving the background needed to write and use packages and shared objects quickly, efficiently, and effectively. Shared objects are explained first, then packages.
Shared Objects Before shared object libraries existed, applications were required to link in references to all of the functions used at compile/link time. The linker resolved all addresses when the files were combined or linked together. When two or more applications share the same code, they need to be linked to the same source files. Using this technique, the file sizes and memory requirements of the applications increased. While there is nothing wrong with using this approach, and it is still commonly used, shared object libraries were created to help reduce these memory and file size requirements. A shared object library is a group of functions that the linker does not resolve until it is linked at run time. Instead, these functions are specially marked, so that
163
164
Chapter 8: Shared Objects and Packages
when the program is executed, they are resolved (assigned an address) by loading the appropriate shared object library.
Versioning Windows DLLs and executables can embed version information into the resource section of the file. Unlike Windows, Linux has a built-in mechanism for identifying the version of a shared library. The file naming convention of a shared object takes the form: lib.so...
Library name indicates the name of the file, and for Kylix is the name of the project (.dpr) file. Major, minor, and micro refer to the release numbers of the library. The major release number indicates compatibility at the interface, or API, level. Any interface change in a library that requires applications to be recompiled in order to work should increment the major version number of the library. For example, in major release one a function requires an integer parameter, but in release two, the same function now takes a string as a second argument. In order for the application to work with release two, make the appropriate changes in the application, recompile, and relink. For minor releases, any changes refer to additional interfaces that have been added and are completely compatible with previous releases. All existing interfaces remain unchanged. Finally, a micro release does not add any additional functionality, but changes are made to the implementation. Typical changes that affect micro releases are bug fixes and performance tuning. Having a file naming standard is certainly a step in the right direction for different versions of shared objects to live peacefully with each other. But using all of the version numbers to identify a library is overkill. Also, any changes to a filename would bring disastrous results. Therefore, each library has an internal identifier that refers to the name and the version. This is called the soname. It is given to the linker when creating the shared object. The soname is of the form: lib.so.
Notice that the minor and micro releases are not part of the soname. This allows for bug fixes and performance enhancements to be released and should not affect any applications that link to it. Another benefit to having the version number as part of the filename is to keep older applications running, while improving or adding additional functionality. Linux allows older versions to remain alongside the latest versions by using file links to point to the proper current versions. Shown below is the output, from one of the author’s systems, using the ls -l /usr/lib/libmenu* command. -rw-r--r-lrwxrwxrwx
1 root 1 root
root root
44456 Aug 4 2000 libmenu.a 12 Oct 7 03:33 libmenu.so -> libmenu.so.5
Chapter 8: Shared Objects and Packages lrwxrwxrwx
1 root
root
-rwxr-xr-x lrwxrwxrwx
1 root 1 root
root root
-rwxr-xr-x
1 root
root
165
14 Oct 7 03:33 libmenu.so.4 -> libmenu.so.4.0 25168 Jul 12 2000 libmenu.so.4.0 14 Oct 7 03:24 libmenu.so.5 -> libmenu.so.5.1 23860 Aug 4 2000 libmenu.so.5.1
The output contains three links that are indicated by the -> character and the letter “l” as part of the permissions. There are two different major versions of libmenu — version four and five. By convention, the library name without a version number points to the highest major version, in this case, five. Then for each major version, there is also a link to the most current release. Version four is a zero release (libmenu.so.4.0) while version five is release one (libmenu.so.5.1). Using a link to point to the current version of a library can cause problems as well. Poorly written installation programs can change the link to point to a version that is incompatible with other installed applications. When loading an application that uses a library, the program loader loads the shared object filename listed in the application’s function import table (the soname). If the program loader cannot find the library, an error message is displayed that indicates what version of the library the application requires.
Search Order If two or more libraries contain the same function definition, the first library will be the one used. The search order that is used to look for any of the libraries needed is as follows. First, the directories specified by the environment variable LD_LIBRARY_PATH are searched. Second, the program loader searches a cache file named /etc/ld.so.cache that lists directories where previous libraries were found. Third, the program loader searches the list specified in the text file /etc/ld.so.conf. Finally, if the library has not been located, the dynamic linker searches the /usr/lib directory. Programs that have either the set user id or set group id bits set on an executable do not search the LD_LIBRARY_PATH environment variable, for security reasons.
New Compiler Directives Kylix has introduced several new compiler directives that are useful when working with shared object libraries. They are SOPREFIX, SOSUFFIX, SOVERSION, and SONAME. All of these directives take one string argument. SONAME is used to set the internal library name and the other three are used to change the name of the output file. When using these filename directives, they take the form: .so.
SOPREFIX defaults to “lib” for shared objects and “bpl” for packages. If the SONAME directive is used, it will create a symbolic link that points to the actual filename. For more information on these directives, look in the Kylix help file.
166
Chapter 8: Shared Objects and Packages
Calling Conventions
TE
AM FL Y
A calling convention is an agreement between the caller of a routine and the routine itself. All functions have a calling convention that indicates how parameters are passed, how parameters are cleaned up, how registers are used, and how errors and exceptions are handled. The default Kylix and Delphi calling convention is called register. For specifics regarding register and other calling conventions, search the Kylix help under the topic “Calling Conventions.” If a library is only going to be called by other Kylix applications, then there is no real need to change it. However, applications that are written in another language normally use a different method of passing parameters. Two of the most commonly used calling conventions are cdecl and stdcall. Both pass arguments backward, from right to left. The difference is who is responsible for cleaning up afterward. For cdecl functions, the caller is required the to clean up while the routine cleans up stdcall functions. Whatever method is chosen, both the caller and the routine need to be sure that both are using the same calling convention. Both calling conventions are fully supported by GNU C/C++ compiler on Linux. Another calling convention is safecall, which does more than either cdecl or stdcall. In Windows, they are used frequently in COM methods. All routines that are declared as safecall have an implicit return a value of type HResult. They also wrap the entire function with an implicit try..except block to catch any software exceptions that may occur and convert them to a HResult value. The safecall calling convention in Linux requires that all modules use the ShareExcept unit, described later in this chapter.
Creating Shared Objects
Kylix makes creating shared object libraries easy. They are created by choosing File|New and the SO option in the dialog that appears, which is shown here:
Figure 8-1: Creating a shared object library
Chapter 8: Shared Objects and Packages
167
Once selected, Kylix generates the following code: library Project1; { Important note about shared object exception handling: In order for exception handling to work across multiple modules, ShareExcept must be the first unit in your library's USES clause and your project's (select Project/View Source) USES clause if 1) your project loads (either directly or indirectly) more than one Kylix-built shared object, and 2) your project or the shared object are not built with run-time packages (baseclx). ShareExcept is the interface unit to the dynamic exception unwinder (libunwind.so.6), which must be deployed along with your shared object. } uses SysUtils, Classes; begin end.
Adding routines to a shared object is straightforward. Either add the function or procedure in the .dpr file, or include the appropriate unit by adding it to the uses clause. Once a routine is added, it is not automatically available to be called outside of the shared object. It must be explicitly exposed to the outside world by using the exports keyword. The code below shows an example of a function called MyCube that is exported. library Cube; uses SysUtils, Classes; function MyCube(x : integer) : integer; stdcall; begin Result := x * x * x; end; exports MyCube; begin end.
168
Chapter 8: Shared Objects and Packages
Here is an alternative way to export a function: library Cube; uses SysUtils, Classes; function MyCube(x : integer) : integer; stdcall; begin Result := x * x * x; end; exports MyCube name 'MyCube'; begin end.
When exporting overloaded routines, each routine must be given a different exported name. That way both functions can be visible to calling routines. Suppose, for example, the shared object contained two functions named MyCube with one version that takes an integer and the other a double. The exports section would look like this: exports MyCube(x : integer) name 'MyIntCube', MyCube(x : double) name 'MyDblCube';
In shared object libraries, function imports and exports are bound by name only and are always case-sensitive.
Using Shared Objects Using a shared object requires the binding of functions at either link time (statically) or dynamically at run time. Most often they are statically linked. There are several ways of binding to a routine that is in a shared object. They are: n
Use a single line definition.
n
Write an import unit that contains a collection of related routines (Libc.pas is a good example).
n
Dynamically bind to a routine at run time.
To demonstrate calling the MyCube function within the libCube.so library, examine the following code: program CubeTest; {$APPTYPE CONSOLE}
Chapter 8: Shared Objects and Packages
169
uses // pick one of the two listed below cubedef in 'cubedef.pas'; // cubedef2 in 'cubedef2.pas'; var x : integer; begin writeln('Enter a number to cube..'); readln(x); writeln(x,' cubed is ',MyCube(x)); end.
The example contains a simple console application that prompts for a number, then calls the MyCube function to calculate the value. When running the demo application, make sure that the environment variable LD_LIBRARY_PATH contains the current directory, a period. For users of the bash shell, use the command: export LD_LIBRARY_PATH=.:$LD_LIBRARYPATH to add the current directory to the path used to locate shared objects. Tip: Applications that use shared object libraries located in the same directory as the executable will not run automatically. This is true for applications running inside of the IDE or from a terminal prompt. In order to execute the applications, the LD_LIBRARY_PATH environment variable must be modified. Two options are available that can solve this dilemma. Bring up the Environment Options dialog located in the Tools menu. Add the current directory, specified by a single period, to the LD_LIBRARY_PATH environment variable. Alternatively, modify the startkylix shell script by adding the current directory to the environment variable before the call to kylixpath. Single line definitions work well when there are only a few functions that need to be referenced. An example for the MyCube import is show below: unit cubedef; interface function MyCube(x : integer) : integer; stdcall; external 'libCube.so' name 'MyCube'; implementation end.
170
Chapter 8: Shared Objects and Packages
Shown below is an example of how to use the import unit style of declaring external routines: unit cubedef2; interface function MyCube(x : integer) : integer; stdcall; implementation function MyCube; external 'libCube.so' name 'MyCube'; end.
An important item to note is that the function name that is used within a Kylix application does not need to be the same as the name of the routine in the shared object. This is accomplished by using the name directive to specify the exact, case-sensitive external name of the routine. Furthermore, if the name of the function used is the same as the routine in the shared object, the name directive is not needed. Using the index directive under Kylix generates a platform warning and binds to the routine by name, ignoring the index directive. Dynamic binding waits until run time to search the shared object for a specific routine. Linux provides five functions that are used for this purpose. They are dlopen, dlclose, dlsym, dlvsym, and dlerror. An example that uses dynamic binding is shown below. program DynamicCubeTest; {$APPTYPE CONSOLE} uses Libc; var x : integer; handle : Pointer; MyCubeFunc : function ( x : integer) : integer; stdcall; begin handle := dlopen('libCube.so',0); if handle = nil then begin writeln('dlopen failed. ',GetLastError); Exit; end; try dllerror; // dlsym doesn't clear the error on success!
Chapter 8: Shared Objects and Packages
171
mycubefunc := dlsym(handle,'MyCube'); if @mycubefunc = nil then begin writeln('Error loading function MyCube. ErrMsg:',dlerror); Exit; end; writeln('Enter a number to cube..'); readln(x); writeln(x,' cubed is ',MyCubeFunc(x)); finally dlclose(handle); end; end.
Although the binding to a routine occurs at run time, the application still needs to know the arguments, return value and calling convention at the time of the compilation. (Technically, all function imports are resolved when the function is actually called and not at load time. This is the default convention and can be changed by setting the environment variable LD_BIND_NOW.) Tips: To make it easier to port Windows DLLs to Linux, Kylix maps LoadLibrary, FreeLibrary, and GetProcAddress to the Linux equivalents: dlopen, dlclose, and dlsym. The authors recommend using the Windows equivalent as dlsym does not clear the error condition upon success. Use the Linux APIs when more control is needed when opening the shared object library. The dlsym function is capable of returning a pointer to a record as well as library routines. In fact, the .dpr file of an Apache DSO demonstrates how to export a record. An example that demonstrates these features is located on the CD. The dlvsym function adds an additional parameter in order to specify a version of a symbol. This function is used when a shared library contains routines and records with different versions of the same symbol. For most applications, this function should not be needed.
Exceptions There are two kinds of exceptions in Kylix. They are operating system, or external, exceptions and software exceptions. External exceptions are descendants of the class EExternal, found in SysUtils.pas. Certain Linux signals are mapped to language exceptions as shown below.
172
Chapter 8: Shared Objects and Packages Table 8-1 Signal
Description
Kylix Exception
SIGINT SIGFPE
User Interrupt (Control-C) Floating-point exception
SIGSEGV
Segmentation violation (AV)
SIGILL
Illegal instruction
SIGBUS
Bus error (Hardware Fault)
SIGQUIT
User Interrupt (Control Backslash)
EControlC EDivByZero, EInvalidOp, EZeroDivide, EOverflow, EUnderflow ERangeError, EIntOverflow, EAccessVioliation, EPrivilege, EStackOverflow ERangeError, EIntOverflow, EAccessVioliation, EPrivilege, EStackOverflow ERangeError, EIntOverflow, EAccessVioliation, EPrivilege, EStackOverflow EQuit
Notice that SIGSEGV, SIGILL, and SIGBUS signals are mapped to multiple exceptions, depending on the fault that occurs. By default, Kylix maps these signals to exceptions for applications, but not for libraries. However, there are a couple of run-time library functions that can be used to map the signals to exceptions. They are HookSignal and UnhookSignal and are found in SysUtils.pas. Two other functions also exist for more advanced signal handling, InquireSignal and AbandonSignalHandler. Information regarding these functions can be found in SysUtils.pas. Here are the rules for dealing with shared objects and hardware exceptions. Shared objects that are created with Kylix and are not built with packages should not trap any signals when they are loaded by a Kylix application. Building a shared object with packages that is loaded by a Kylix application that is also built with packages can trap hardware exceptions in the shared object. Finally, if a shared object that is created with Kylix is called by a non-Kylix application, it is able to trap hardware exceptions as long as the calling application and any other libraries do not install signal handlers. The reason that exception handling in shared objects is messy is because the POSIX thread (pthread) and signal standard does not provide adequate support for multiple bodies of code to participate in signal handling for the process. Unfortunately, hooking POSIX signal handlers is very much like installing interrupt service routines: It is easy to hook in and chain to the previously installed handler, but it is difficult to unhook without corrupting the chain. When robust exception handling is needed, consider compiling the shared object code as a package to remove these limitations.
Chapter 8: Shared Objects and Packages
173
ShareExcept The ShareExcept unit is used to handle exceptions between non-package Kylix applications and non-package Kylix shared objects. This unit is a wrapper for the libborunwind.so.6 that allows for exceptions to cross from shared objects to the application. ShareExcept must be the first unit listed in the uses statement for both the library and the application. Remember to deploy libborunwind.so.6 with any library that uses the ShareExcept unit. When dealing with libraries, especially with non-Kylix applications, make sure each exported library routine catches all software exceptions. A sample library that traps the external exceptions and catches all exceptions that may occur in an exported function is shown below. library SafeSO; uses ShareExcept, SysUtils, Classes; function SomeSOFunction : boolean; stdcall; begin try // do something Result := true; except on e : exception do begin // handle any exceptions Result := false; end; end; end; exports SomeSOFunction; procedure DLLHandler(Reason: Integer); begin // 0 means unloading, 1 means loading if Reason = 0 then begin // now we want to remove our signal handler UnhookSignal(RTL_SIGDEFAULT); end; end;
174
Chapter 8: Shared Objects and Packages begin // we want to map Linux Signals to Kylix Exceptions // so we call HookSignal to hook all of the default signals HookSignal(RTL_SIGDEFAULT); // install the Exit handler DLLProc := @DLLHandler; end.
Remember that this example should only be used if the calling application and any other libraries do not install signal handlers.
Packages Shared object libraries are useful for writing common functions that need to be shared between applications written in different languages. But unfortunately they have some disadvantages when integrating with Kylix applications, exceptions, and non-modal forms. To eliminate these problems, the designers of Kylix (and Delphi) developed packages. A package is a shared object library with additional, Kylix-specific, enhancements. They can only be used by applications that are written in Kylix. Packages, like shared object libraries, can be used to share common code. They are also used when distributing components (see Chapter 16 for more information). When multiple applications are properly designed to use packages, they save hard drive and memory space. Maintenance and updates to applications are easier, requiring only the affected packages to be installed.
Package Types There are different package types to distinguish what they are used for. Designtime packages are used for components, property editors, experts, and wizards. Run-time packages are used for adding additional functionality for applications. Packages can be design only, run time only, design and run time, or neither. Packages that are not design time or run time are uncommon and can be referenced only by other packages.
File Prefixes and Extensions The project source for a package is found in the .dpk file. In the following section, the layout of this file is discussed. Each unit that is in a package is compiled into a binary image file with a .dpu extension. All of the .dpu files are compiled into one package binary image file with a .dcp extension. Without changing any compiler directives, the output of the package is of the form: bpl.so
Chapter 8: Shared Objects and Packages
175
Structure of Packages Packages are comprised of one or more units that contain the functionality that the package provides. These units may require other packages. In the package source file (.dpk) two additional sections need to be specified. These are a Requires section that lists the packages that are needed for linking and a Contains section that lists the units that provide the functionality. With these new sections come a few rules that need to be observed. Applications may use multiple packages. However, an application must have only one reference to a unit. The same unit cannot be in the contains section for more than one package. Circular references need to be avoided. Packages cannot require themselves. In addition, if package X requires Y, then Y cannot require X. Any kind of circular loop is not allowed. The compiler will detect these errors and report them appropriately.
Compiler Directives Also found in the package source are a few more compiler directives. The table below lists these for easy reference. Table 8-2 Compiler Directive
Description
{$DESIGNONLY ON} {$RUNONLY ON}
When specified, the package is a design-time only package. When specified, the package is a run time only package.
Creating Packages Kylix makes creating packages easy. They are created by choosing File | New and selecting the Package option in the dialog that appears, which is shown here:
Figure 8-2: Creating a new package
Chapter 8: Shared Objects and Packages
Once selected, Kylix brings up the package editor, which looks like this:
Figure 8-3: The package editor
AM FL Y
Using the package editor, units and packages are added by using the Add button. If the current selection is in the contains section (as shown in Figure 8-3), a new unit or new component can be added. Similarly, if the current selection is in the Requires section, an existing package is added to the Requires section. The Remove button allows for packages and units to be removed from their respective sections. Design-time packages are added to Kylix by using the Install button. If the package has not been compiled, it is compiled, and if successful, is installed into Kylix. Removing packages is accomplished by using the Component | Install Packages dialog or by pressing the Options button and clicking on the Packages tab. Options for packages are similar to other projects. When the Options button is pressed, the following dialog appears:
TE
176
Figure 8-4: Package project options
Chapter 8: Shared Objects and Packages
177
Only the Description tab is specific to packages. The other tabs are the same as all other project types (Applications, Console Applications, Shared Objects, etc.). This dialog is where the package type is specified in the Usage options. Controlling the build is specified by selecting either Rebuild as needed or Explicit rebuild. When using packages that will not change frequently, select the Explicit rebuild option. For automatic compilation, select the Rebuild as needed option. Finally, the description is used to display a description in the IDE for the package once it has been installed. After a new package is selected from the New Items dialog, Kylix generates the following code: package Package1; {$R *.res} {$ALIGN 8} {$ASSERTIONS ON} {$BOOLEVAL OFF} {$DEBUGINFO ON} {$EXTENDEDSYNTAX ON} {$IMPORTEDDATA ON} {$IOCHECKS ON} {$LOCALSYMBOLS ON} {$LONGSTRINGS ON} {$OPENSTRINGS ON} {$OPTIMIZATION ON} {$OVERFLOWCHECKS OFF} {$RANGECHECKS OFF} {$REFERENCEINFO ON} {$SAFEDIVIDE OFF} {$STACKFRAMES OFF} {$TYPEDADDRESS OFF} {$VARSTRINGCHECKS ON} {$WRITEABLECONST OFF} {$MINENUMSIZE 1} {$IMAGEBASE $400000} {$IMPLICITBUILD OFF} requires baseclx, visualclx; end.
Notice that the baseclx and visualclx packages are automatically added to the Requires section. If the package being created does not use any visual components, these two packages can be removed. Another item to note is that there is no
178
Chapter 8: Shared Objects and Packages
Contains section. Once units are added to the package using the Add button in the package editor (shown in Figure 8-3), they are added to the Contains section.
Package Versioning and Naming Conventions When a package is compiled with a specific version of Kylix, the package can only be used with applications that are compiled with the same version of Kylix. For this reason, packages should be named in a way that identifies the version of Kylix with which they were compiled. For example, MyFirstPackage10 would indicate that the package was created with Kylix version 1.0. In addition, packages should also include a standard way of identifying whether they are design-time or run-time packages. Use an abbreviation like “Dsgn” for design-time packages and “Rtm” for run-time packages. Choosing a naming convention will save time in the long run, by not having to remember if it is a design-time or run-time package.
Creating a Package Let’s create a package that wraps the functionality shown in the cube unit below. Create a new package using the New Items dialog shown in Figure 8-2. Save the package and name it PkgCube10. Remove the two Requires packages by pressing the Remove button. These packages are not required since we are not using any of the functionality that they provide. Press the Add button, type in the name Cube.pas in the Unit file name edit box, and press OK. Kylix detects that the unit does not exist and asks what type of file needs to be created. Select new unit and click OK. Copy the code shown below into the unit and save the file. Now build the package. This will create a package named bplPkgCube10.so. If additional units had been needed, they could have been added like the Cube.pas unit. Existing units can be included by selecting the appropriate unit from the directory where it resides. The package source looks like this: package PkgCube10; {$R *.res} {$ALIGN 8} {$ASSERTIONS ON} {$BOOLEVAL OFF} {$DEBUGINFO ON} {$EXTENDEDSYNTAX ON} {$IMPORTEDDATA ON} {$IOCHECKS ON} {$LOCALSYMBOLS ON} {$LONGSTRINGS ON} {$OPENSTRINGS ON} {$OPTIMIZATION ON}
Chapter 8: Shared Objects and Packages {$OVERFLOWCHECKS OFF} {$RANGECHECKS OFF} {$REFERENCEINFO ON} {$SAFEDIVIDE OFF} {$STACKFRAMES OFF} {$TYPEDADDRESS OFF} {$VARSTRINGCHECKS ON} {$WRITEABLECONST OFF} {$MINENUMSIZE 1} {$IMAGEBASE $400000} {$IMPLICITBUILD OFF} contains Cube in 'Cube.pas'; end.
Functionality of the package is provided with the Cube.pas unit shown below: unit Cube; interface // notice that the calling conventions do not need to be specified. function MyCube(x : integer) : integer; overload; function MyCube(x : double) : double; overload; implementation function MyCube(x : integer) : integer; begin Result := x * x * x; end; function MyCube(x : double) : double; begin Result := x * x * x; end; // Exporting functions in packages are only needed // when the functions will be called dynamically. exports MyCube(x : integer) name 'MyIntCube', MyCube(x : double) name 'MyDblCube'; end.
179
180
Chapter 8: Shared Objects and Packages
Using Packages Now that the package is created, how are they used? Like shared objects, packages can be linked statically or dynamically. Statically linked packages must exist when the application is executed, otherwise an error message is displayed, indicating that the shared library could not be loaded. Packages are located in the same way as shared object libraries as discussed in the “Search Order” section earlier in the chapter.
Statically Linking to Packages Create a simple console application with the code shown below. From all appearances, it looks like a normal console program, with nothing indicating that it uses packages. In fact, if it were compiled, the package would not be used. program TestMyCube; {$APPTYPE CONSOLE} uses Cube; var x : integer; begin writeln('Enter a number to Cube'); readln(x); writeln(x,' cubed is ',MyCube(x)); end.
An option must be enabled in order for an application to use packages. In the Project Options dialog box, enable the Build with runtime packages option and specify PkgCube10 in the list of packages.
Figure 8-5: Building an application with packages
Chapter 8: Shared Objects and Packages
181
Attempting to run the TestMyCube program will not work without some further tweaking. Unless the directory where the bplPkgCube10.so is specified in the LD_LIBRARY_PATH, the test program cannot run. The easiest way to resolve this is to put the current directory in the LD_LIBRARY_PATH, found in Tools | Environment Options in the Environment Variables tab. Remember that the current directory is specified by using a single period.
Dynamically Linking to Packages Packages can be linked dynamically as well. This requires some additional code, as shown in the example program DynamicPackage. program DynamicPackage; {$APPTYPE CONSOLE} uses SysUtils; var x : integer; function CallIntCube(value : integer) : integer; type TMyCubeFunc = function (x : integer) : integer; var pkgHandle : HMODULE; fn : TMyCubeFunc; begin Result := -1; pkgHandle := LoadPackage('./bplPkgCube10.so'); try @fn := GetProcAddress(pkgHandle, 'MyIntCube'); if @fn = nil then raise Exception.CreateFmt('GetProcAddr failed. Error %d' , [GetLastError]) else begin // calling function Result := fn(value); end; finally UnloadPackage(pkgHandle); end;
182
Chapter 8: Shared Objects and Packages end; begin writeln('Enter a number to cube'); readln(x); writeln(x,' cubed is ',CallIntCube(x)); end.
Notice the calls to LoadPackage and UnloadPackage. LoadPackage calls the initialization routines, if present, for every unit that is listed in the contains clause of the package. Similarly, UnloadPackage calls the finalization routines, if present, for every unit that is listed. Tip: Notice in the code for LoadPackage, found in SysUtils.pas, that it attempts to load the shared object using the dlopen function. Then, it looks for an exported routine called Initialize. The compiler automatically adds the initialize routine to every Kylix package. Its purpose is to call the initialization section for every unit contained in the package. The code for UnloadPackage is found in SysUtils.pas as well. It searches the package for the exported routine named finalize. Generated automatically by the compiler and inserted into the package, finalize calls the finalization section for every unit contained in the package. Once the package is loaded, the address of the MyIntCube routine is retrieved. Notice that the variable is a function pointer, declared with the same parameters and return value as the function within the package. Finally, a check is made to ensure that an address has been assigned and the function is called.
Packages: A Bigger Advantage If a routine can be called dynamically from both a shared object library and a package, why use packages? Packages have an advantage that shared objects lack. In addition to dynamically loading routines, packages have the ability to dynamically load classes as well! The only restriction is that any dynamically loaded classes must be descendants of TPersistent. This includes components and a number of other classes. In order to demonstrate this powerful advantage, a package is created that contains a form. Since a form is a component, this example will show how to dynamically display the form and how to access properties and methods. The source for the package is shown below. package TestPackages; {$R *.res} {$ALIGN 8} {$ASSERTIONS ON} {$BOOLEVAL OFF}
Chapter 8: Shared Objects and Packages
183
{$DEBUGINFO ON} {$EXTENDEDSYNTAX ON} {$IMPORTEDDATA ON} {$IOCHECKS ON} {$LOCALSYMBOLS ON} {$LONGSTRINGS ON} {$OPENSTRINGS ON} {$OPTIMIZATION ON} {$OVERFLOWCHECKS OFF} {$RANGECHECKS OFF} {$REFERENCEINFO ON} {$SAFEDIVIDE OFF} {$STACKFRAMES OFF} {$TYPEDADDRESS OFF} {$VARSTRINGCHECKS ON} {$WRITEABLECONST OFF} {$MINENUMSIZE 1} {$IMAGEBASE $400000} {$RUNONLY} // Runtime only package {$IMPLICITBUILD OFF} requires baseclx, visualclx; contains frmPackageTest in 'frmPackageTest.pas' {frmPkgTest}; end.
Notice that the package is run-time only. Shown below is the code for the form within the package. Nothing is fancy about this particular form. In addition to a button and label, there is a count property and two methods. unit frmPackageTest; interface uses SysUtils, Types, Classes, Variants, QGraphics, QControls, QForms, QDialogs, QStdCtrls; type TfrmPkgTest = class(TForm) Button1: TButton; Label1: TLabel; procedure Button1Click(Sender: TObject);
184
Chapter 8: Shared Objects and Packages private { Private declarations } FCount : integer; public { Public declarations } constructor Create(AOwner : TComponent); override; published procedure ShowCount; procedure ShowAMessage(const str : string); property Count : integer read FCount write FCount; end; var frmPkgTest: TfrmPkgTest; implementation {$R *.xfm} constructor TfrmPkgTest.Create(AOwner: TComponent); begin inherited; FCount := 131; // arbitrary value end; procedure TfrmPkgTest.Button1Click(Sender: TObject); begin ShowMessage('Hello Package World! Count is %d', [FCount]); end; procedure TfrmPkgTest.ShowAMessage(const str : string); begin ShowMessage('Inside the package.. The message is %s',[ str ]); end; procedure TfrmPkgTest.ShowCount; begin ShowMessage('The value of Count in the package is %d ',[FCount]); end; initialization RegisterClass( TfrmPkgTest ); // Most important finalization UnRegisterClass( TfrmPkgTest ); end.
Chapter 8: Shared Objects and Packages
185
A couple of items are worth noting. First, any property or method that needs to be called dynamically must be declared in the published section. The reason for this is that Kylix uses runtime type information (RTTI) in order to access these properties and methods. Second, notice that the form has initialization and finalization sections. When the package is loaded, the form is registered with the system using the RegisterClass function. Similarly, UnRegisterClass removes the class reference from the system in the finalization section. Tip: Kylix has the ability to determine type information at run time similar to how reflection works in Java. However, RTTI in Kylix is limited to properties and methods declared in the published section. The Typinfo.pas unit contains a number of routines that are useful when needed to use RTTI in applications. Furthermore, the TObject class contains methods like MethodAddress that can be used as well. Now after compilation, the bplTestPackage.so file will contain the functionality found in frmPackageTest. The code shown below demonstrates how to create, get and set properties, and call methods dynamically using the power of RTTI and packages. It is important to compile the application with packages enabled; otherwise the application will not work. unit frmMain1; interface uses SysUtils, Types, Classes, Variants, QGraphics, QControls, QForms, QDialogs, QStdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation
Chapter 8: Shared Objects and Packages {$R *.xfm} uses Typinfo; procedure TForm1.Button1Click(Sender: TObject); const CLASS_NAME = 'TfrmPkgTest'; PROP_NAME = 'Count'; METHOD1_NAME = 'ShowCount'; METHOD2_NAME = 'ShowAMessage';
: : : : : : :
HMODULE; TPersistentClass; TComponent; Variant; procedure( obj : TObject); procedure( obj : TObject; const str : string); string;
AM FL Y
var PkgHandle PerCls DynComponent PropertyValue method1 method2 parmstr
begin PkgHandle := LoadPackage('./bplTestPackages.so'); try // There are two ways to retreive the class: FindClass and // GetClass. The only difference is that FindClass will // raise an exception if the class cannot be located. // GetClass returns nil if the class cannot be located.
TE
186
//PerCls := GetClass('TfrmPkgTest'); try PerCls := FindClass(CLASS_NAME); except on e : exception do begin ShowMessage('Exception %s while finding class %s ', [e.message,CLASS_NAME]); PerCls := nil; Exit; end; end; // // // //
now that we have a persistent class, we can create the object. This is accomplished by casting the persistent class and calling the constructor, passing an appropriate value for the owner. In this case, we will pass nil because
Chapter 8: Shared Objects and Packages // we will take care of the cleanup. DynComponent := TComponentClass(PerCls).Create(nil); try // now we can see if the component has a particular // property that we're interested in if IsPublishedProp( DynComponent, PROP_NAME) then begin // set the property to some arbitrary value SetPropValue( DynComponent, PROP_NAME, 7); // now retrieve the property value PropertyValue := GetPropValue( DynComponent, PROP_NAME); // and display the results ShowMessage('GetPropValue returned %d for property %s', [Integer(PropertyValue),PROP_NAME]); end else ShowMessage('The component does not have the %s property', [ PROP_NAME ]); // We can call methods too. In this first example, the method // does not take any parameters method1 := DynComponent.MethodAddress(METHOD1_NAME); if assigned(method1) then begin // But then what are we doing here??? // Any method actually has a hidden parameter, the "Self" // object is passed in. So in this case, we pass in the // DynComponent, as it is the object we need to call the // method on.. method1( DynComponent ); end else ShowMessage('Unable to locate method %s',[METHOD2_NAME]); // similarly, we can call methods with parameters as well method2 := DynComponent.MethodAddress(METHOD2_NAME); if assigned(method2) then begin parmstr := 'Calling from the Application'; // just like the first method, we pass the DynComponent // as the "Self" parameter, and then any additional parameters // that are required. method2(DynComponent, parmstr); end else ShowMessage('Unable to locate method %s',[METHOD2_NAME]);
187
188
Chapter 8: Shared Objects and Packages // finally, let's display the form as a modal dialog TForm(DynComponent).ShowModal; finally DynComponent.Free; end; // try/finally — freeing the component. finally UnloadPackage(PkgHandle); end; // try/finally — Package unloading end; end.
Walking through the code, the call to dynamically link the package is the same. After the package is loaded, the class that will be dynamically created needs to be located using either the FindClass or GetClass function. These functions return a TPersistentClass from which we can create the required class. The only difference between FindClass and GetClass is how errors are handled. FindClass throws an exception while GetClass returns nil. In order to create a component dynamically, the TPersistentClass is cast into a TComponentClass. Once cast, the constructor is called. The component has been created. Once the class is created, a search is made using the IsPublishedProp function to determine if the component has the specified property. If the property is found, it is set to an arbitrary value. Similarly, the value of the property is retrieved using the GetPropValue function, to verify that setting the property did actually work. In this example, the property has an integer data type. Information about a property is obtained by using one of the overloaded GetPropInfo functions. Calling methods dynamically requires a little additional work. This example shows two methods: that are ShowCount and ShowAMessage. ShowCount does not take any parameters, while ShowAMessage takes a string. But notice how the two variables needed to call these methods are declared. Method1 is defined as a procedure that takes a TObject parameter. Method2 is defined as a procedure that takes a TObject parameter and a string. Why do both methods have a TObject as the first parameter? The reason is the way method routines work behind the scenes. Method routines have a hidden first parameter. This first parameter is automatically passed to the method routine. It contains the “self” pointer of the object. In other words, the first parameter is a reference to the object that the method is acting upon. Built into the TObject class is the MethodAddress function that returns a pointer to a method, based on the string representation of that method. Since all objects in Kylix are descendants of TObject, calling DynComponent's MethodAddress returns a pointer to the requested method. Notice that DynComponent is passed as the “self” parameter. Finally, the DynComponent is cast into a TForm component and is displayed as a modal dialog box. Afterwards, the component is freed and the package is unloaded.
Chapter 8: Shared Objects and Packages
189
Displaying a Form Dynamically: An Easier Way The previous section demonstrated how to dynamically load a form that contained published properties and methods. This method works well when displaying a form that cannot be treated in a generic manner; in other words, a form that has additional properties or methods that need to be accessed. When a form can be used in a generic manner, an alternative method exists that is easier to read, maintain, and understand. To demonstrate this alternative method, another package has been created that contains a form with a label, edit box, and button. When the button is pressed, the form displays a message box that contains the contents of the edit control. The .xfm file for this form has been left out as well as the package contents for brevity, and can be found on the CD. Shown in the listing below is a portion of the application that calls the form dynamically. procedure TForm1.btnDispFormClick(Sender: TObject); var PC : TPersistentClass; aForm : TCustomForm; begin // find the form to display // remember that in order for FindClass to locate the class, // the class needs to be registered using the RegisterClass function. PC := FindClass('TfrmSimple'); // now create the form Application.CreateForm(TComponentClass(PC), aForm); try // show the form as a modal dialog aForm.ShowModal; finally aForm.Free; end; end;
Notice that FindClass is still used to retrieve the class from the package. Once located, the form is created using the CreateForm method of the Application object. This is the same method that is used to create forms in a graphical application project’s .dpr file. Since CreateForm expects a component class or descendant, the result from FindClass is cast to an expected class type. The form is created and assigned to the aForm variable. From this point on, the code looks just like any code that needs to manipulate a form. Left out from this listing are the calls to dynamically load and unload the package. These calls are located in the OnCreate and OnDestroy event handlers.
190
Chapter 8: Shared Objects and Packages
The Interface Advantage In the previous chapter, interfaces were introduced as a method of providing a set of functionality or services. Packages may contain classes that implement interfaces. The methods and properties of the interface can be dynamically called at run time, provided certain rules are followed. Recall that in order for the FindClass and GetClass functions to work, the class must be a descendant of TPersistent. In order for interfaces to work, classes must implement the IUnknown interface. Fortunately, there is a class that does both: TInterfacedPersistent. Using the TInterfacedPersistent class provides the necessary methods needed by the IUknown interface, allowing the developer to concentrate on writing the functionality needed by the application. Classes, however, are not required to descend from TInterfacedPersistent. Existing class hierarchies can be used, as long as the class implements the required IUnknown methods: QueryInterface, _AddRef, and _Release. Examples of these method implementations are found in the TInterfacePersistent class. To demonstrate how to dynamically use an interface in a package, the IMyInterface has been defined as follows: unit MyInterface; interface type IMyInterface = interface (IUnknown) // GUID automatically generated by using Shift-Ctrl-G in IDE ['{5E545AD1-48B1-D511-911C-00D0B725EC52}'] procedure ShowCount; procedure ShowAMessage(const str : string); function GetCount : integer; procedure SetCount(aCount : integer); property Count : integer read GetCount write SetCount; end; implementation end.
Notice that the interface has a count property. Properties are only allowed if accessor methods are used. Once the interface is defined, a class is created that is a descendant of TInterfacedPersistent and implements the IMyInterface: TMyInterfaceClass = class(TInterfacedPersistent, IMyInterface) private FCount : integer; public function GetCount : integer; procedure SetCount(aCount : integer); procedure ShowCount;
Chapter 8: Shared Objects and Packages
191
procedure ShowAMessage(const str : string); end;
One important item to note is that the methods of the class are defined to be public and not published. Interfaces do not contain RTTI information, only classes. Tip: One of the new features in Kylix 2 (and Delphi 6) is runtime type information for interfaces. Web Service applications make extensive use of interface RTTI in order to expose methods. Any interface that descends from the IInvokeable interface automatically includes RTTI data. See Chapter 24 for more information on Web Services. Additionally, the TMyInterfaceClass is registered with RegisterClass in the initialization section of the unit. The code that implements these methods has been left out for brevity. See the CD for the complete source code. In the application that dynamically loads the package, the code is drastically reduced. This application uses the MyInterface unit, since it needs to know methods that are within the interface. Shown below is the source for dynamically accessing properties of the TMyInterfaceClass. procedure TForm1.FormCreate(Sender: TObject); begin aHandle := LoadPackage('./bplTestPackagesWithInterfaces.so'); end; procedure TForm1.btnDynIntfClick(Sender: TObject); var PC : TPersistentClass; MyIntf : IMyInterface; begin // now let's locate the Interface.. PC := FindClass('TMyInterfaceClass'); // create the class, and cast it to our interface MyIntf := TInterfacedPersistent(PC.Create) as IMyInterface; // now we can call the various methods and access properties.. MyIntf.Count := 35; MyIntf.ShowCount; MyIntf.ShowAMessage('hello from package world!'); ShowMessage('Count is %d',[MyIntf.Count]); end; procedure TForm1.FormDestroy(Sender: TObject); begin UnloadPackage(aHandle); end;
192
Chapter 8: Shared Objects and Packages
Notice that the class is still located using the FindClass function. Once retrieved, the class is created, cast as a TInterfacedPersistent class, then cast again as the IMyInterface. This results in an interface to the desired class. Afterwards, accessing the methods and properties are the same as any other class.
Dynamic Packages: Which Method to Use? Now the question is “Which method should be used when dynamically loading packages?” The answer is “It depends.” When designing new applications, it is easier to put the framework in ahead of time; thus, the authors strongly recommend the interface approach. It is easier to read, maintain, and use. For applications that already exist, it may be easier to use the runtime type information. Regardless of the method chosen, both have advantages and disadvantages. Using runtime type information limits access to published methods and properties, whereas the interface method is limited by the methods and properties that exist within the interface.
Summary When designing and writing shared objects and packages, remember to version them properly. This will allow for the coexistence of new and existing applications that use them. Compiler directives play an important role with versioning shared objects and packages. Choose the best method for your application when calling routines in shared objects. Recall that there are two ways of referring to routines in a package or a shared object: statically and dynamically. Make sure that the calling conventions are identical when calling routines. Dynamic component creation can only occur in packages. Packages provide the framework for accomplishing the creation of dynamic components. Two methods are available for accessing methods and properties. Runtime type information is built into published properties and routines can be used to call methods and get and set property values. Interfaces provide an alternative way that is easier to read and understand. Exceptions need to be handled properly with shared objects. The general rule is to not allow exceptions to escape exported functions and to put the ShareExcept unit in as the first unit in the uses statement. Packages overcome the limitations of exceptions in shared objects. Shared objects and packages both allow for the division of large applications into several functional modules. When multiple applications need the same functionality, the result is increased reusability. Kylix provides the tools necessary to unleash the power of shared objects and packages quickly, easily, and effectively.
Chapter 9
Compiler, Run-time Library, and Variants
Introduction At the heart of Kylix is the blazingly fast compiler. It can be used not only within the IDE, but also from the command line. Furthermore the run-time library is full of cross-platform routines that help writing sophisticated applications. In this chapter, the compiler and run-time library are discussed in depth.
Compiler Overview Kylix generates native Linux applications and shared objects from a project’s source files. It takes those source files and generates the zeroes and ones that the computer needs in order to execute the application. In addition, Kylix provides options for compiling from the command line, options that control how source files are compiled, inline assembler, optimization to generate high-performance code, and conditional compilation.
Command-Line Options While the Kylix IDE provides a great environment for writing applications quickly, the compiler is also available for use from the command line. The compiler is invoked with the dcc command, and has numerous options as shown below: Borland Delphi for Linux Version 14.1 Copyright (c) 1983,2001 Borland Software Corporation Syntax: dcc [options] filename [options] -A= = Set unit alias -O = Object directories -B = Build all units -P = Generate PIC code (DPU) -D = Define conditionals -Q = Quiet compile -E = EXE output directory -R = Resource directories -F = Find error -U = Unit directories -GD = Detailed map file -V = Debug information in EXE -GP = Map file with publics -VR = Generate remote debug (RSM) -GS = Map file with segments -W = Output warning messages
193
194
Chapter 9: Compiler, Run-time Library, and Variants -H = Output hint messages -I = Include directories -LU = Use package -M = Make modified units -N = DCU/DPU output dir
-Z = Output 'never build' DCPs -$ = Compiler directive –help = Show this help screen –version = Show name and version
Compiler switches: -$ A8 Aligned record fields B- Full boolean evaluation C+ Evaluate assertions at runtime D+ Debug information G+ Use imported data references H+ Use long strings by default I+ I/O checking J- Writeable structured consts L+ Local debug symbols M- Runtime type info
(defaults are shown below) O+ Optimization P+ Open string params Q- Integer overflow checking R- Range checking T- Typed @ operator V+ Strict var-strings W- Generate stack frames X+ Extended syntax Y+ Symbol reference info Z1 Minimum size of enum types
If the command-line compiler is not found when invoked, be sure to add the /bin directory to your PATH environment variable by running the kylixpath script. See Chapter 2 for more information.
Compiler Directives Compiler directives are used to force specific options while compiling source code. Most compiler directives have two ways of being specified, a short form and a long form. The long form is verbose and is usually self-explanatory, while the short form requires looking in the help file or memorization to remember what it is used for. For example, the following two compiler directives are functionally equivalent, and will turn range checking on: {$R+} {$RANGECHECKS ON}
Compiler directives can be specified in two ways, using either curly bracket style as in: {$(compiler directive) (arguments if necessary)} {$R+}
or using the parenthesis style like this: (*$(compiler directive)(arguments if necessary)*) (*R+*)
Notice that both styles are really a special form of comments. Some users prefer to use one style for compiler directives and another for regular comments to make it easy to distinguish between them symantically. If the directives need to be
Chapter 9: Compiler, Run-time Library, and Variants
195
commented out so that they are ignored, add a few spaces before the dollar sign like this: { $A-} // commented out compiler directive.
If directives are commented out, make sure to add a comment to explain the reason why they were commented out. See Appendix G for a complete list of all of the compiler directives.
Inline Assembler Kylix provides the ability to embed assembly code within a routine. The keyword asm is used to start a block of assembly code and is terminated with an end statement. One of the nice features of using embedded assembly is that most parameters can be referenced directly, except for strings, floating-point, and set constants. If this feature was not available, arguments would be referenced via registers or stack offsets, making the code harder to read and maintain. An example that demonstrates both a Pascal and an assembly version of squaring an integer is shown below. // Pascal version function MySquare(x : integer): integer; begin Result := x * x; end; // Assembly version function MySquare(x : integer): integer; begin asm mov EAX, x // put the value of X in the EAX register mul EAX, EAX // Square EAX mov @Result, EAX // move the result of the squaring to Result end; end;
While most applications will never need to write assembly code, the ability to write hand-coded assembly when every ounce of performance is needed is available without having to maintain and link in additional assembler source code files.
Compiler Optimization Kylix generates applications that perform quickly. Code optimization is enabled by default. Disable code optimization with the check box located on the Project Options dialog, under the Compiler tab. When optimizations are enabled, Kylix looks at several areas looking for ways of increasing performance. They are Register Optimizations, Call Stack Overhead Elimination, Common Sub-expression elimination, and Loop Induction Variables. For further information, read the article
196
Chapter 9: Compiler, Run-time Library, and Variants
titled “Technical Overview of the Kylix Compiler” located at http://www.borland.com/kylix/papers/.
Conditional Compilation Conditional compilation is the ability of the compiler to test a static condition at compile time and decide whether a block of code is included or not. It is often used to include additional debugging code, identify cross-platform code, build different versions of the application using the same source code base, or take advantage of additional features based on the version of the compiler. The condition or test used to determine if the block of code is included can be a symbol, an expression that can be determined at compile time, or the state of a compiler option.
Symbols
Table 9-1 Predefined Symbols BCB
CONSOLE
Definition
Defined when C++ Builder IDE invokes the Pascal compiler Use to determine if expressions can be used, for example the {$IF expression} directives. This is new for Kylix and Delphi 6. Defined when the {$APPTYPE Console} directive is used, but only available in the project source file (.dpr) and libraries. Defined when the target platform is the Intel 386 family of processors. Defined when targeting Executable and Linkable Format (ELF) files. Defined only when using the Open Edition of Kylix. Defined when compiling on any Linux platform. Defined when compiling on any 32-bit Linux platform. Defined when compiling on any Windows platform. Defined when compiling on platforms that use address maps instead of stack frames to unwind exceptions. Currently only defined for Linux platforms; however, do not assume that this excludes Windows platforms.
TE
ConditionalExpressions
AM FL Y
Symbols used for controlling conditional compilation are either created by the developer or predefined by the compiler. By convention, symbols are usually in uppercase, although they are not case sensitive. A list of predefined symbols is shown in the following table.
CPU386 ELF GPL LINUX LINUX32 MSWINDOWS PC_MAPPED_EXCEPTIONS
Chapter 9: Compiler, Run-time Library, and Variants
197
Predefined Symbols
Definition
PIC
Defined when compiling on platforms that require Position Independent Code (PIC). Currently it is only defined when compiling shared objects or packages on Linux and POSIX platforms, but do not assume that this excludes Windows platforms. Defined when compiling on any POSIX platform. Defined when compiling on any 32-bit POSIX platform. Each version of the run-time library is defined. This is the version for Kylix 1.0. Defined when compiling on any 32-bit Windows platform.
POSIX POSIX32 VER140 WIN32
A symbol is defined by using one of the following methods. 1. Use the {$DEFINE SymbolName} declaration before any tests for the symbol. 2. Add the symbol to the list of conditional defines in the Directories/Conditionals tab in Project Options. 3. When compiling using the command-line compiler, dcc, add the option -DSymbolName to the options. Testing for Symbols
Symbols are tested by using the {$IFDEF SymbolName} directive, the {$IFNDEF SymbolName} directive, or the {$IF Defined(SymbolName)} directive. {$IFDEF} Directive — The {$IFDEF}directive is used to test the existence of a symbol and is terminated by the {$ENDIF} directive. If an else clause is needed, use the {$ELSE} directive. The following example illustrates these directives. {$IFDEF LINUX} // place Linux code here {$ELSE} // place all NON-Linux (does not imply only Windows!) {$ENDIF}
Be careful when using the {$ELSE} directive. In the example shown above, the else does not imply the Windows platform only. If Kylix is ever ported to another platform, the else condition will be true for that platform as well. Instead of using the else, use two separate tests as shown below: {$IFDEF LINUX} // place Linux code here {$ENDIF} {$IFDEF MSWINDOWS} // place Windows code here {$ENDIF}
198
Chapter 9: Compiler, Run-time Library, and Variants
{$IFNDEF} Directive — Use the {$IFNDEF} directive whenever a symbol is not defined. The {$IFNDEF} is terminated by the {$ENDIF} directive. The {$ELSE} directive is also available to be used. Shown below is an example that illustrates the {$IFNDEF} directive. {$IFNDEF LINUX} // Linux is NOT defined (does not imply that we are on a Windows!) {$ELSE} // Linux IS defined {$ENDIF}
{$IF} Directive — New to Kylix and Delphi 6, the {$IF} directive tests for symbols using the Defined function as shown in the example below: {$IF Defined(LINUX)} // Place Linux code here {$IFEND}
Notice that the {$IF} directive is terminated by a different directive than the two previous directives. It uses the {$IFEND} directive to terminate the block. An additional directive {$ELSEIF} is available as well as the {$ELSE} directive. These will be discussed in the next section. {$UNDEF} Directive — Symbols are undefined by using the {$UNDEF} directive. Normally, the {$UNDEF} directive is used to quickly prevent a section of code from being included. Here is an example: {$UNDEF DEBUG} // turn of the debug stuff for now.. {$IFDEF DEBUG} // do some debug stuff {$ENDIF} {$DEFINE DEBUG} // turn it back on now..
Expressions Expressions are new for Kylix and Delphi 6. Used with the new {$IF} compiler directive, any constant expression at the time of compilation is available for testing. Introduced with expressions, the symbol ConditionalExpressions is defined when the {$IF} directive is available. This is very useful for retaining compatibility with older versions of Delphi, since the {$IF} directives can be wrapped up in an {$IFDEF} directive as shown below: {$IFDEF ConditionalExpressions} {$IF // // //
MyCondition} DO NOT use a {$ELSE} directive when nesting the new style {$IF} directives inside the old {$IFDEF} style, as the compiler will match the {$ELSE} with the {$IFDEF} instead of the {$IF}.
Chapter 9: Compiler, Run-time Library, and Variants
199
{$ELSEIF AnotherCondition > 10} // more appropriate code {$IFEND} {$ENDIF}
It is important to note that when nesting the {$IF} directives inside either {$IFDEF} or {$IFNDEF} directives, you should not use an {$ELSE} directive with the {$IF} directive, since the compiler will match the {$ELSE} with the traditional directives. Two functions are available for use within the {$IF} directive, Defined and Declared. Defined, as mentioned previously, returns true when a symbol is defined. Declared returns true when a Pascal identifier is visible within the current scope. An expression can use Boolean operations to form a complex boolean expression. For example the following is valid: {$IF Defined(DEBUG) and Declared(DebugVar)} // dump out the debug variable ShowMessage('DebugVar is ' + IntToStr(DebugVar)); {$ELSE} ShowMessage('Sorry. DebugVar is not available'); {$IFEND}
An alternative to the {$ELSE} directive, the new {$ELSEIF} directive is available, but only with the new {$IF} directive. {$ELSEIF} takes an expression as well, similar to the {$IF} directive. Using the {$ELSEIF} directive is much nicer as it does not require nesting {$IF} inside of {$ELSE} directives. {$IF Defined(LINUX)} // put linux code here {$ELSEIF Defined(MSWINDOWS) // put Windows code here {$ELSE} {$MESSAGE FATAL 'No code for this platform!'} {$IFEND}
Tip: The {$MESSAGE MsgType 'string'} directive prints out a message when compiled. The string must be in quotes. MsgType is one of the following: HINT - Displays a hint and continues to compile WARN - Displays a warning and continues to compile ERROR - Displays an error and continues to compile FATAL - Displays an error and stops the compilation
200
Chapter 9: Compiler, Run-time Library, and Variants
Options Kylix allows for determining the state of compiler options by using the {$IFOPT} directive. Some functions behave differently, depending on the state of compiler options. A good example is the input and output functions. In the {$I-} state, input and output errors are detected by using the IOResult function. When using the {$I+} state, input and output errors raise exceptions. The following code demonstrates how a procedure could use the {$IFOPT} directive to handle both cases of the {$I} compiler option. var f : file; {$IFOPT I-} // only need this when using IOResult.. ioError : integer; {$ENDIF} begin AssignFile(F,'ANonExistantFile'); {$IFOPT I+} try {$ENDIF} Reset(F); {$IFOPT I+} // Catch any IO exceptions except on e : EInOutError do ShowMessage('Exception: ' + e.message); end; {$ELSE} // Not using exceptions so detect errors using IOResult IOError := IOResult; if IOError 0 then ShowMessage('I/O Error: ' + IntToStr(IOError)); {$ENDIF} // more code here.. Should really wrap this in a try/finally to // close the file when finished. end;
{$IFOPT} uses the {$ELSE} and the {$ENDIF} directives when appropriate.
Run-time Library The run-time library is a set of cross-platform routines and a special data type called a variant. By using these routines over those found in Libc.pas or other operating system units, applications become easier to port between Linux and Windows. These routines range from allocating memory to complex mathmatical functions to
Chapter 9: Compiler, Run-time Library, and Variants
201
string manipulation. A variant is a special data type that allows for the same variable to be assigned different data types at run time. Kylix has twenty units that make up the run-time library. They are listed in the following table: Table 9-2 Unit
Description
System
Implicitly included in every program, System.pas contains routines that the compiler needs to handle in a specific way. Some functions pass parameters in non-standard ways for performance reasons. Others require the special knowledge by the compiler. SysUtils.pas contains the bulk of the run-time library. Exceptions are declared in this unit. Contains various data types and interface declarations for streams. Container resources like Lists, Stacks, Queues, and Buckets. Conversion support routines. Provides many useful date and time related routines. Interfaces for VCL/CLX help system. Classes for manipulating ini/config files. Mask support routines. Mask matching routine. Math.pas contains mathematical and financial routines. Resources for runtime library errors, warnings and messages. Conversion routines for physical, fludic, thermal and temporal units. String utility routines. Synchronization objects used in multi-threaded applications. Variant type support functions. Contains routines for complex numbers custom variant type. Provides routines for the measurement conversion custom variant type. Has useful variant related functions, and a couple of classes and interfaces as well. Used only for operating systems that are not Windows, it contains platform-neutral routines that manipulate variants.
SysUtils Types Contrns ConvUtils DateUtils HelpIntf IniFiles MaskUtils Masks Math RTLConsts StdConvs StrUtils SyncObjs TypInfo VarCmplx VarConv Variants VarUtils
The numerous routines found in the runtime library are listed in Appendix H, “Routines of the Run-time Library.”
Variants Originally variants were used with OLE Automation on the Windows platform. In order to support variants on the Linux platform, Borland wrote a platformindependent version. Variants are a special data type that can hold many different types of data and whose actual type is determined at run time. This run-time data type information comes at a cost, however. Applications will be slower and larger
202
Chapter 9: Compiler, Run-time Library, and Variants
due to the increased code size. Whenever the data type is known ahead of time, and the data type cannot change, use a specific data type rather than a variant.
Variant Types Variants can contain many different data types. Internally, they are represented by the following constants. const varEmpty = $0000; varNull = $0001; varSmallint = $0002; varInteger = $0003; varSingle = $0004; varDouble = $0005; varCurrency = $0006; varDate = $0007; varOleStr = $0008; varDispatch = $0009; varError = $000A; varBoolean = $000B; varVariant = $000C; varUnknown = $000D; //varDecimal = $000E; {UNSUPPORTED} { undefined $0f } {UNSUPPORTED} varShortInt = $0010; varByte = $0011; varWord = $0012; varLongWord = $0013; varInt64 = $0014; //varWord64 = $0015; {UNSUPPORTED} varStrArg varString varAny varTypeMask varArray varByRef
= = = = = =
$0048; $0100; $0101; $0FFF; $2000; $4000;
In the following sections, these constants are used when specific data types need to be referenced. Furthermore, Kylix allows for the creation of custom variants. This topic is discussed at length in a later section.
Internal Representation of a Variant The variant data type is built into the Kylix compiler. In System.pas, the definition of how the compiler treats a variant is found by looking at the TVarData record.
Chapter 9: Compiler, Run-time Library, and Variants TVarData = packed record VType: TVarType; case Integer of 0: (Reserved1: Word; case Integer of 0: (Reserved2, Reserved3: Word; case Integer of varSmallInt: (VSmallInt: SmallInt); varInteger: (VInteger: Integer); varSingle: (VSingle: Single); varDouble: (VDouble: Double); varCurrency: (VCurrency: Currency); varDate: (VDate: TDateTime); varOleStr: (VOleStr: PWideChar); varDispatch: (VDispatch: Pointer); varError: (VError: LongWord); varBoolean: (VBoolean: WordBool); varUnknown: (VUnknown: Pointer); varShortInt: (VShortInt: ShortInt); varByte: (VByte: Byte); varWord: (VWord: Word); varLongWord: (VLongWord: LongWord); varInt64: (VInt64: Int64); varString: (VString: Pointer); varAny: (VAny: Pointer); varArray: (VArray: PVarArray); varByRef: (VPointer: Pointer); ); 1: (VLongs: array[0..2] of LongInt); ); 2: (VWords: array [0..6] of Word); 3: (VBytes: array [0..13] of Byte); end;
Tip: Variants can be typecast into the TVarRec record. For example, to access the Vtype field, the code looks like this: TVarData(MyVariant).VType
Be careful when accessing variants in this manner as it is easy to interfere with the low-level workings of variants. The authors strongly recommend that this method should be avoided, unless creating a custom variant as shown in a later section. More than likely, the functionality that is needed is found in a variant support function. Look at the variant categories found in the next section.
203
204
Chapter 9: Compiler, Run-time Library, and Variants
A variant is made up of a type and data. Notice the different data types that the variant can be, ranging from integers and strings to arrays. Take a close look and notice that variants cannot contain classes, class references, records, sets, static arrays, files, or pointers. They can contain a special dynamic array — a TVarArray — which is discussed a little later. The VType field is used by the compiler to determine how the data for the variant is accessed. Tip: A record named TVarRec is also declared in the System.pas unit. It is not used for variant data types; rather, it is used by the compiler for parameters that take array of constants. Variants are declared by using the word variant as the data type as shown below: var MyVariant : Variant;
Empty Variants When declared, variants are assigned a special value — Empty. Empty means that the variant has not been assigned a value. Use the VarIsEmpty function to determine if a variant is empty. A variant can be unassigned by using the VarClear function. if VarIsEmpty(MyVariant) then MyVariant := 'whatever I want to put here!' else VarClear(MyVariant);
Null Variants Another special value that variants can have is Null. Null is different from empty, as it is an actual assigned value. The function VarIsNull is used to determine if a variant is assigned a null value. MyVariant := null; if VarIsNull(MyVariant) then // handle appropriately
Null is not built into the compiler, nor is it a global variable. Rather, it is a function defined in Variants.pas. Variant expressions that contain one or more variants that are null always result in a null variant. However, expressions that contain any variants that are not assigned will generate an Invalid Variant Operation exception (EVariantError).
Chapter 9: Compiler, Run-time Library, and Variants
205
Variants in Expressions Kylix allows for variants to be placed into expressions. Valid operations on variants, depending on the underlying data type, are grouped into four categories: assignment, comparison, bitwise, and mathematical. The following table shows the operations available for each category. Table 9-3 Category
Operations
Comments and Notes
Assignment
:=
Comparison
=, , , =
Bitwise Mathematical
shl, shr, and, or, xor, not +, *, /, div, mod
Variants can be set to other variants, variables, and constants. Variants can be compared to other variants, variables, and constants. These require integer data types. These require numeric data types. Some, like div, require integer data types. The + operation also concatenates strings together.
For example, suppose that the following code is executed: var x,y,z : variant; begin x := 55; y := 100; z := x + y; // performs an integer addition and z be assigned the value 155 end;
The application will determine at run time to do an integer addition, based on the values in the variables x and y. Instead, if the following code is executed: var x,y,z : variant; begin x := 'Hello '; y := 'World!'; z := x + y; // concatenates the strings together forming "Hello World!" end;
The application will determine at run time to concatenate the strings together. Both of these examples have obvious results. But what happens when a string and an integer are added together? Suppose the following code is executed: var x,y,z : variant;
206
Chapter 9: Compiler, Run-time Library, and Variants begin x := '10'; y := 25; z := x + y; // what happens here? à Z is a integer type with the value 35! end;
At run time, the application does an implicit conversion of the variable x to an integer value, thereby allowing the application to determine the answer. Rules for conversions of variants are found in the help system under the topic “variant type conversions.” Remember that empty variants that are used in expressions will produce an Invalid Variant Operation exception (EVariantError) and null variants will produce a result that is null.
Arrays of Variants
AM FL Y
An array cannot be directly assigned to a variant. That is, the following code is illegal: var v : variant; x : array[0..9] of integer;
TE
begin z := x; // illegal, generates a compiler error. end;
However, variants can be assigned an array by using either the VarArrayCreate function or the VarArrayOf function defined in Variants.pas. The following code demonstrates how to do this: var v : variant; i : integer; begin v := VarArrayCreate([0,9], varInteger); // creates an array that can hold 10 integers for i:=0 to 9 do v[i] := i; end;
Now the variable v refers to a variant array that can be indexed just like any other array. In fact, a variant can be indexed at any time without causing a compiler error. However, if the variant has not been created, indexing a variant will generate a Variant is not an array exception (EVariantError).
Chapter 9: Compiler, Run-time Library, and Variants
207
Tip: Typically, strings are indexed using MyString[x] to look at individual characters. When dealing with variant arrays, the indexing operation (e.g., [x] ) assumes that the variant is an array. So a variant that contains a string cannot be indexed using the [x] construct. However, if this is necessary, assign the variant to a string and perform the indexing operation on the string instead of the variant. The first parameter of VarArrayCreate defines the bounds of the array. They are grouped in sets of two, with each pair specifying the upper and lower bounds of the array. So if a two-dimensional five by five array is needed, the first parameter would look like [0,4,0,4]. Now look at the second parameter of VarArrayCreate. It specifies the variant type of the array. Arrays are not confined to one specific type but can also be variant by using the varVariant type code. So if the code for calling VarArrayCreate is changed to look like this, v := VarArrayCreate([0,9], varVariant); // creates an array that can hold 10 variants
each position in the array can hold a different data type. An example that illustrates this is shown below. var v : variant; begin v := VarArrayCreate([0,9], varVariant); // creates an array that can hold 10 variants v[0] := 'The First Slot'; v[1] := 77; v[2] := 7731.53; v[3] := now; v[4] := True; v[5] := null; // etc, etc, etc. end;
Tip: Not every variant type is capable of being used with VarArrayCreate. Variants of type varEmpty, varNull, varStrArg, varString, varAny, varTypeMask, varArray, and varByRef cannot be used. If they are, an EVariantError will be generated, giving a message of “Error creating variant array.” For strings arrays, use varOleString or varVariant.
208
Chapter 9: Compiler, Run-time Library, and Variants
Alternatively, one-dimensional arrays can also be created by using the VarArrayOf function as the following code demonstrates. var v : variant; begin v := VarArrayOf(['The First Slot', 77, 7731.53, now, True, null]); // etc, etc, etc. end;
Creating Customized Variant Types Using the built-in variants to enhance applications is a nice tool to add to the collection. But, unfortunately, Borland could not think of every single variant that might be used in applications. Rather than write thousands of lines of code to write every possible variant type that anyone would ever want, they opted to provide the hooks necessary to implement a custom variant type. By following the rules, implementing a domain-specific variant is straightforward. There are two ways to create a custom variant type. Both methods require the creation of a class that descends from either TCustomVariantType or TInvokeableVariantType. The only difference is that classes that descend from TInvokeableVariantType can also have additional properties and methods. Otherwise, they both follow the same set of rules as listed below. 1. Determine where the data will be stored. If the size of the data is small enough (e.g., less than 16 bytes), choose the appropriate field of the TVarData record. When representing larger data, use one of the pointer fields. When allocating memory, remember to release memory when it is no longer needed. 2. Write a function that creates the custom variant, setting the VType field to the value of the custom variant, and also the data field(s) chosen in step 1. This function will need to be in the interface section of the unit, as this is the only way to create the custom variant without using a hack. Depending on the custom variant, additional functions may be needed if different types of data can be used to create the variant. 3. In the implementation section, write a class that descends from either TCustomVariantType or TInvokeableVariantType. 4. Override the virtual methods for the custom variant as appropriate. At a minimum, these should include the abstract methods Clear and Copy for TCustomVariantType descendants. Most likely, BinaryOp, UnaryOp, CompareOp or Compare, Cast, CastTo, IsClear, LeftPromotion, and RightPromotion will need to be overridden as well. In addition, DoFunction, DoProcedure, GetProperty, and SetProperty will need to be overridden for TInvokeableVariantType descendants.
Chapter 9: Compiler, Run-time Library, and Variants
209
5. Declare a private unit global variable (e.g., in the implementation section) of the class type created in step 3. In the initialization section of the unit, instantiate the class and assign it to the private unit global variable. This is where the custom variant is registered to the system. Declare a public global variable (e.g., in the interface section) of type word. In the initialization section of the unit, this variable needs to be assigned the VarType field of the newly instantiated class. Use this variable wherever a TVarType value is needed. 6. Unregister the custom variant with the system by destroying the class created in step 5. This occurs in the finalization section of the unit. An example that demonstrates how to write a custom variant is shown in the next section.
Creating a Roman Numerals Variant A Roman numeral variant type will be created to demonstrate how to write a custom variant. Roman numerals are represented by letters instead of numbers as shown in the table below: Letter
Value
I V X L C D M
One (1) Five (5) Ten (10) Fifty (50) One hundred (100) Five hundred (500) One thousand (1000)
Notice that Roman numerals have no way of representing zero or any negative number. Furthermore, they are all integer values. Certain combinations also perform subtraction as the next table reveals. Letter
Value
IV IX XL XC CD CM
Four (4) Nine (9) Forty (40) Ninety (90) Four hundred (400) Nine hundred (900)
The algorithm for converting a Roman numeral string to an integer and back into a string is not shown here, but can be found on the CD.
210
Chapter 9: Compiler, Run-time Library, and Variants
Step 1 – Define storage location Internally, the Roman numeral will be converted to an integer for easier manipulation. Looking at the TVarData record, the first position of the VLongs array will be used for this purpose. Since most of the methods that need to be overridden will need to use this location, two helper routines are written to enhance readability — a get method and a set method. These functions look like this: interface … type // Roman Numerals can only be positive TRomanNumeralInteger = 1..High(longint); … implementation … function GetRomanNumeralValue(const V : TVarData) : TRomanNumeralInteger; begin // we map the storage to the first location in the VLongs member array. Result := V.VLongs[0]; end; procedure SetRomanNumeralValue(var V : TVarData; data : TRomanNumeralInteger); begin V.VLongs[0] := data; end; …
Step 2 – Write functions to create custom variant In order for other units to create a custom variant, additional functions are written. For the example, two functions are written. One function takes a parameter of type TRomanNumeralInteger and the other a string that needs to conform to a valid representation of a Roman numeral. They are shown below: … interface … // These functions allow for a RomanNumeral Variant to be created
Chapter 9: Compiler, Run-time Library, and Variants
211
// Either pass in a non-zero long integer value or a string in a // proper roman numeral format function RomanNumeralsCreate( intData : TRomanNumeralInteger ) : Variant; overload; function RomanNumeralsCreate( strData : string ) : Variant; overload; … implementation … // // Step 2. Write a method that creates instances of your custom Variant. This // fills in the Variant's data as defined in step 1. // // The first two RomanNumeralsCreate procedures are used within this unit only procedure RomanNumeralsCreate( var Result : TVarData; data : TRomanNumeralInteger); overload; begin Result.VType := varRomanNumerals; SetRomanNumeralValue( Result, data); end; procedure RomanNumeralsCreate( var Result : TVarData; const data : string); overload; begin Result.VType := varRomanNumerals; SetRomanNumeralValue( Result, RomToInt(data) ); end; // These two RomanNumeralsCreate procedures are accessible from other units // They just call the internally used procedures. function RomanNumeralsCreate( intData : TRomanNumeralInteger ) : Variant; overload; begin RomanNumeralsCreate( TVarData(Result), intData ); end; function RomanNumeralsCreate( strData : string ) : Variant; overload; begin RomanNumeralsCreate( TVarData(Result), strData ); end; …
212
Chapter 9: Compiler, Run-time Library, and Variants
Step 3 – Create the descendant class For this example, the Roman Numerals variant will descend from TCustomVariantType as there is no need for additional properties or methods. Since this class should never be used by other units, it is placed within the implementation section. The class definition looks like this: implementation … type // This is the class that implements the methods for // our RomanNumeral variant (varRomanNumerals) // // Notice that this is not in the interface section, as it is created // only once, in the initialization section of this unit. TRomanNumeralType = class(TCustomVariantType) protected function LeftPromotion(const V: TVarData; const Operator: TVarOp; out RequiredVarType: TVarType): Boolean; override; function RightPromotion(const V: TVarData; const Operator: TVarOp; out RequiredVarType: TVarType): Boolean; override; public procedure Cast(var Dest: TVarData; const Source: TVarData); override; procedure CastTo(var Dest: TVarData; const Source: TVarData; const AVarType: TVarType); override; procedure Clear(var V: TVarData); override; procedure Copy(var Dest: TVarData; const Source: TVarData; const Indirect: Boolean); override; procedure BinaryOp(var Left: TVarData; const Right: TVarData; const Operator: TVarOp); override; procedure UnaryOp(var Right: TVarData; const Operator: TVarOp); override; procedure Compare(const Left, Right: TVarData; var Relationship: TVarCompareResult); override; end;
Step 4 – Override appropriate virtual methods In this step, the appropriate methods are overridden. Since the TRomanNumeralType is a descendant of the TCustomVariantType, the abstract methods Clear and Copy must be implemented. Furthermore, each of the overridden methods will be explained.
Chapter 9: Compiler, Run-time Library, and Variants
213
Step 4a – Override the Clear method The Clear method is called when the variant needs to be wiped clean. For variant types that store the data within the TVarData record, use the VariantInit run-time library routine. When a variant allocates additional memory for storing the data of the variant, be sure to release the memory and set the TVarData.VType field to varEmpty. procedure TRomanNumeralType.Clear(var V: TVarData); begin VariantInit(V); end;
Step 4b – Override the Cast method Cast is called when a built-in variant needs to be converted to the custom variant type. Any conversions that are not supported should call the run-time library routine RaiseCastError. procedure TRomanNumeralType.Cast(var Dest: TVarData; const Source: TVarData); begin case Source.VType of varByte : RomanNumeralsCreate( Dest, Source.VByte ); varShortInt : RomanNumeralsCreate( Dest, Source.VShortInt ); varSmallInt : RomanNumeralsCreate( Dest, Source.VSmallInt ); varWord : RomanNumeralsCreate( Dest, Source.VWord ); varInteger : RomanNumeralsCreate( Dest, Source.VInteger ); varString : RomanNumeralsCreate( Dest, string(Source.VString) ); varOleStr : RomanNumeralsCreate( Dest, WideString(Pointer(Source.VOleStr))); else // Not supported RaiseCastError; end; end;
Step 4c – Override the CastTo method CastTo is called when an attempt is made to convert a custom variant to another variant type. These variants can be either the predefined variants or other custom variants. If the cast is allowed, the custom variant is converted to the requested variant. When the cast is not allowed, use the RaiseCastError run-time library routine to inform the caller that it is not supported. procedure TRomanNumeralType.CastTo(var Dest: TVarData; const Source: TVarData; const AVarType: TVarType); begin case AVarType of
214
Chapter 9: Compiler, Run-time Library, and Variants varSingle varDouble varByte varShortInt varSmallInt varWord varInteger varInt64 varString varOleStr
: : : : : : : : : :
Dest.VSingle := RomanNumeralsToFloat(Source); Dest.VDouble := RomanNumeralsToFloat(Source); Dest.VByte := RomanNumeralsToInt(Source); Dest.VShortInt := RomanNumeralsToInt(Source); Dest.VSmallInt := RomanNumeralsToInt(Source); Dest.VWord := RomanNumeralsToInt(Source); Dest.VInteger := RomanNumeralsToInt(Source); Dest.VInt64 := RomanNumeralsToInt(Source); string(Dest.VString) := RomanNumeralsToString(Source); WideString(Pointer(Dest.VOleStr)) := RomanNumeralsToString(Source);
else // sorry charlie, we don't support it! RaiseCastError; end; // finally, set the destination type to be what the requested type was Dest.VType := AVarType; end;
Step 4d – Override the Compare or CompareOp method In order to support comparing custom variants, either the Compare or CompareOp method must be overridden. Most variants, including the Roman Numerals variant, will override the Compare method. Use Compare when a custom variant has an ordering, that is when a custom variant knows when two variants are less than, greater than, or equal to each other. procedure TRomanNumeralType.Compare(const Left, Right: TVarData; var Relationship: TVarCompareResult); var l,r : TRomanNumeralInteger; begin l := GetRomanNumeralValue(Left); r := GetRomanNumeralValue(Right); if l = r then RelationShip := crEqual else if l < r then Relationship := crLessThan else Relationship := crGreaterThan; end;
Use CompareOp when finer control is needed over all of the comparison operations. Suppose a custom variant does not support ordering but does support equality or inequality. CompareOp provides the method of controlling each individual comparison operation.
Chapter 9: Compiler, Run-time Library, and Variants
215
Step 4e – Override the Copy method When another variant needs to be assigned the value of the custom variant type, the Copy method is called. If the Indirect parameter is set to true, and the source variant uses a pointer to store the data of the variant (e.g., VarDataIsByRef(Source) returns true), an indirect copy needs to be made by calling the VarDataCopyNoInd method of the variant’s ancestor class. This method does the actual indirect copying. Indirect copying needs to occur when dealing with variant types that store data as a pointer and not the actual value. In contrast, direct copying merely copies the data in the variant. When Copy is called with Indirect set to false, a direct copy needs to be performed. Allocate any additional memory if needed. The Roman Numerals variant example does not allocate memory since it uses a field of the TVarData record for storing the value of the variant. procedure TRomanNumeralType.Copy(var Dest: TVarData; const Source: TVarData; const Indirect: Boolean); begin if Indirect and VarDataIsByRef(Source) then VarDataCopyNoInd(Dest, Source) else begin Dest.VType := Source.VType; SetRomanNumeralValue(Dest, GetRomanNumeralValue(Source)); end; end;
Step 4f – Override the LeftPromotion and RightPromotion methods Some of the magic of variants comes with the automatic promotion of variant types to different variant types at run time. For custom variants, this is controlled by two promotion methods: LeftPromotion and RightPromotion. When an expression of the form: Variant1 operation Variant2 is encountered, what determines how variants are promoted to other variant types? The answer lies in LeftPromotion and RightPromotion. In the expression above, Variant1 is the left side and Variant2 is the right side. When an expression involving two variants is encountered, the RightPromotion method is called first to determine if the right side of the expression can be cast into the Variant on the left. If the RightPromotion method indicates that it cannot be coerced, the LeftPromotion method is called to see if it can be cast into the variant on the right. // LeftPromotion //
Chapter 9: Compiler, Run-time Library, and Variants
AM FL Y
// This method answers the question of what happens when you have the following // scenario: X op RomanNumeral. Returns true if the promotion is allowed, false otherwise. function TRomanNumeralType.LeftPromotion(const V: TVarData; const Operator: TVarOp; out RequiredVarType: TVarType): Boolean; begin case V.VType of varSingle, varDouble : // convert the rational to a float begin // The RequiredVar type is whatever the incoming var type is RequiredVarType := V.VType; // and yes we do allow the promotion Result := true; end; varString, varOleStr : begin // if we have a string type, we force the rational to a string // but the only operation supported is opAdd RequiredVarType := V.VType; Result := Operator = opAdd; end;
TE
216
varByte, varWord, varSmallInt, varShortInt, varInteger, varLongWord : // convert the incoming to a roman numeral.. begin RequiredVarType := varRomanNumerals; Result := true; end; else begin // the only thing we need to capture is if it is already a // roman numeral type. Result := V.Vtype = varRomanNumerals; if Result then RequiredVarType := v.VType; end;
Chapter 9: Compiler, Run-time Library, and Variants
217
end; // case end;
// RightPromotion // Similar to LeftPromotion, this method asks the question what happens when I // have the following scenario, RomanNumeral op X? Returns true if allowed, false if not. // When two variants are used in an expression, RightPromotion is called first and // only if it returns false will the LeftPromotion method be called. // operation can be opAdd, opSubtract, opMultiply, opDivide, opIntDivide, // opModulus, opShiftLeft, opShiftRight, opAnd, OpOr, opXor, opCompare function TRomanNumeralType.RightPromotion(const V: TVarData; const Operator: TVarOp; out RequiredVarType: TVarType): Boolean; begin case V.VType of varSingle, varDouble : // convert the rational to a float begin // The RequiredVar type is whatever the incoming var type is RequiredVarType := V.VType; // and yes we do allow the promotion Result := true; end; varString, varOleStr : begin // the required type is the same that is coming in RequiredVarType := V.VType; // we only allow the add operation for strings Result := Operator = opAdd; end; varByte, varWord, varSmallInt, varShortInt, varInteger, varLongWord : // convert the incoming to a roman numeral.. begin RequiredVarType := varRomanNumerals; Result := true;
218
Chapter 9: Compiler, Run-time Library, and Variants end; else begin // the only thing we need to capture is if it is already a // roman numeral type. Result := V.Vtype = varRomanNumerals; if Result then RequiredVarType := v.VType; end; end; // case end;
Step 4g – Override the UnaryOp method The UnaryOp method is called when a unary operation (e.g., – or not) needs to be performed on a custom variant. Using the Roman Numerals variant, these operations did not make sense to support. While this method did not need to be overridden, it is shown here for completion. // UnaryOp (Negate and Not) procedure TRomanNumeralType.UnaryOp(var Right: TVarData; const Operator: TVarOp); begin // only called with opNegate, and OpNot // For Roman numerals, both are not supported. // so we defer handling to the base class where an exception will be raised. inherited; end;
Step 4h – Override the BinaryOp method The BinaryOp method is called when a binary operation (e.g., +, –, div, mod, etc.) needs to be performed on an expression involving variants. However, before BinaryOp is called, the LeftPromotion and RightPromotion methods will be called to determine if and how the variants are promoted. For the Roman Numerals variant, if either the left or right side is a floating-point type, the Roman numeral will be converted to the floating-point version of the number. When one of the arguments is a string type, the Roman numerals are converted to a string and concatenated together. Finally, if both arguments are Roman numerals, then almost all operations are allowed, with the exception of floating-point division, as there is no representation for fractional numbers. // BinaryOp is called with operations to // operations. However, it will only see // and RightPromotion will allow through procedure TRomanNumeralType.BinaryOp(var const Right: TVarData; const Operator: begin
work on the various binary those operations that LeftPromotion Left: TVarData; TVarOp);
Chapter 9: Compiler, Run-time Library, and Variants
219
// for the left side, only RomanNumerals are not created. For the other // numeric types, we convert the RomanNumerals to the appropriate type and // do the operation. For strings, we convert the roman numeral to a string // and only allow the add operation, performing concatenation. case Left.VType of varSingle : case Operator of opAdd : Left.VSingle := Left.VSingle + RomanNumeralsToFloat ( Right ); opSubtract : Left.VSingle := Left.VSingle - RomanNumeralsToFloat ( Right ); opMultiply : Left.VSingle := Left.VSingle * RomanNumeralsToFloat ( Right ); opDivide : Left.VSingle := Left.VSingle / RomanNumeralsToFloat ( Right ); else inherited; end; varDouble : case Operator of opAdd : Left.VDouble := Left.VDouble + RomanNumeralsToFloat ( Right ); opSubtract : Left.VDouble := Left.VDouble - RomanNumeralsToFloat ( Right ); opMultiply : Left.VDouble := Left.VDouble * RomanNumeralsToFloat ( Right ); opDivide : Left.VDouble := Left.VDouble / RomanNumeralsToFloat ( Right ); else inherited; end; varString : begin if Operator = opAdd then string(Left.VString) := string(Left.VString) + RomanNumeralsToString( Right ) else begin // the base class will throw an exception // this should never happen since Left & Right Promotion only allow opAdd // to be passed on through.. inherited; end; end;
220
Chapter 9: Compiler, Run-time Library, and Variants varOleStr : begin if Operator = opAdd then WideString(Pointer(Left.VOleStr)) := WideString(Pointer(Left.VOleStr)) + RomanNumeralsToString( Right ) else begin // the base class will throw an exception // this should never happen since Left & Right Promotion only allow opAdd // to be passed on through.. inherited; end; end; else // not Single, Double, String or OleStr begin if Left.VType = varRomanNumerals then begin case Right.VType of varString : begin if Operator = opAdd then Variant(Left) := RomanNumeralsToString(Left) + string(Right.VString) else begin // the base class will throw an exception // this should never happen since Left & Right Promotion only allow // for opAdd to be passed through. inherited; end; end; varOleStr : begin if Operator = opAdd then else Variant(Left) := RomanNumeralsToString(Left) + WideString(Pointer(Right.VOleStr)); begin // the base class will throw an exception // this should never happen since Left & Right Promotion only allow
Chapter 9: Compiler, Run-time Library, and Variants
221
// for opAdd to be passed through. inherited; end; end; varSingle : begin case Operator of opAdd : Left.VSingle := RomanNumeralsToFloat( Right.VSingle; opSubtract : Left.VSingle := RomanNumeralsToFloat( Right.VSingle; opMultiply : Left.VSingle := RomanNumeralsToFloat( Right.VSingle; opDivide : Left.VSingle := RomanNumeralsToFloat( Right.VSingle; else inherited; // let the base class deal with this.. end; end;
Left ) + Left ) Left ) * Left ) /
varDouble : begin case Operator of opAdd : Left.VDouble := RomanNumeralsToFloat( Left ) + Right.VDouble; opSubtract : Left.VDouble := RomanNumeralsToFloat( Left ) Right.VDouble; opMultiply : Left.VDouble := RomanNumeralsToFloat( Left ) * Right.VDouble; opDivide
: Left.VDouble := RomanNumeralsToFloat( Left ) / Right.VDouble;
else inherited; // let the base class deal with this.. end; end; else begin if Right.VType varRomanNumerals then inherited // let the base class deal with this else begin case Operator of opAdd : SetRomanNumeralValue( Left, GetRomanNumeralValue( Left ) +
222
Chapter 9: Compiler, Run-time Library, and Variants GetRomanNumeralValue( Right )); opSubtract : begin // There is no representation for negative or zero roman numeral // values so only allow subtraction with positive results if GetRomanNumeralValue( Left ) < GetRomanNumeralValue ( Right ) then SetRomanNumeralValue (Left, GetRomanNumeralValue (Left) – GetRomanNumeralValue (Right)) else raise EVariantError.Create('Result of subtraction would result' + ' in a negative or zero value .'); end; opMultiply : SetRomanNumeralValue( Left, GetRomanNumeralValue ( Left ) * GetRomanNumeralValue( Right )); //opDivide : // not supported opIntDivide : SetRomanNumeralValue( Left, GetRomanNumeralValue( Left ) div GetRomanNumeralValue( Right )); opModulus : SetRomanNumeralValue( Left, GetRomanNumeralValue( Left ) mod GetRomanNumeralValue( Right )); opShiftLeft : SetRomanNumeralValue( Left, GetRomanNumeralValue( Left ) shl GetRomanNumeralValue( Right )); opShiftRight : SetRomanNumeralValue( Left, GetRomanNumeralValue( Left ) shr GetRomanNumeralValue( Right )); opAnd : SetRomanNumeralValue( Left, GetRomanNumeralValue( Left ) and GetRomanNumeralValue( Right )); opOr : SetRomanNumeralValue( Left, GetRomanNumeralValue( Left ) or GetRomanNumeralValue( Right )); opXor : SetRomanNumeralValue( Left, GetRomanNumeralValue( Left ) xor GetRomanNumeralValue( Right )); else inherited; // let the base class deal with this (throw an exception) end; // case end; end;
Chapter 9: Compiler, Run-time Library, and Variants
223
end; // case Right.VType end; end; end; // case end;
Step 5 – Declare two unit global variables Two variables need to be created. One is located in the interface section and the other in the implementation section. Take a look at the Roman Numerals variant example: interface … var // declare a variable that will be filled later in the initialization // section to have the next VarType automatically assigned when the // TRomanNumeralType class is created. varRomanNumerals : Word; … implementation … var RomanNumeralType : TRomanNumeralType; … initialization RomanNumeralType := TRomanNumeralType.Create; varRomanNumerals := RomanNumeralType.VarType;
Notice that only one object of type TRomanNumeralType is ever created. The varRomanNumerals can be used wherever a TVarType value is expected. It is automatically assigned after the creation of the TRomanNumeralType class. Step 6 – Unregister the custom variant The last step is to release the resources of the only instantiation of TRomanNumeralType. Using the finalization section of the unit is the logical choice for this code. finalization FreeAndNil(RomanNumeralType);
224
Chapter 9: Compiler, Run-time Library, and Variants
Custom variants provide a rich set of run-time functionality. Included on the CD is an example of using the Roman Numerals variant in expressions and comparison operations.
Summary Most of the time, the compiler is taken for granted. It gracefully compiles code into the most basic representation needed by the computer. This code that is generated can be controlled using compiler directives, inline assembler, and conditional compilation, all of which was detailed in this chapter. Kylix makes writing cross-platform code easier by providing a plethora of run-time library routines ranging from statisical routines to trignometric routines. Variants are useful when the data type of a variable changes when a program executes. Use variants sparingly as they increase the generated code size and decrease performance. When custom variants are needed, follow the rules listed earlier in this chapter to write sophisticated applications. Whenever possible, search for routines in the run-time library rather than using platform-specific code. This will pay huge dividends when porting to another platform.
Exception Handling Chapter 10 and Resource Protection Introduction One thing is certain when writing software — the unexpected happens. Writing code that expects the unexpected is challenging. The resulting code is usually full of lots of error checking and makes maintenance a nightmare. Fortunately, Kylix helps in this regard with exceptions. By providing an easy-to-use construct to trap unexpected errors, the resulting code is easier to understand and maintain. Furthermore, these same constructs are used to assure that resources are cleaned up even if something goes awry.
History Before there were exceptions, programmers were required to test the return values of functions and propagate them up to a routine that would eventually handle the error. This was a maintenance nightmare, and there usually was more code to handle errors than to do the actual program logic. Then, setjmp/longjmp were used to implement a non-local goto. While these functions are still available in almost all standard C libraries, they are crude and not optimal. The biggest problem with using the setjmp/longjmp functions is that they do not clean up any resources while jumping to the designated location. Tip: A non-local goto would look something like this: // // For illustration purposes only. Does not actually work. // label JmpToMe; procedure Foo2;
225
226
Chapter 10: Exception Handling and Resource Protection begin // do something // detect some error goto JmpToMe; end; procedure Foo1; begin // do something Foo2; end;
Foo1;
AM FL Y
begin
// more stuff JmpToMe: // handle the error and exit end;
TE
Enter exceptions. Exceptions encapsulate the low-level functionality required to implement a non-local goto in a clean, object-oriented manner. When exceptions are properly used, they help in producing code that is easier to write, understand, and maintain. In this chapter, exceptions are explained in detail. Tip: The Exception object is defined in SysUtils.pas.
The Life of an Exception Object Exceptions, like all other objects, have a lifetime. They are created, used in some manner, then are eventually destroyed. This section explains the life of the exception object and how it differs from normal objects.
Creation An exception object is usually constructed by the application, whenever it is requested to do so. Exceptions can be caused by math errors like dividing by zero, user intervention (like pressing Ctrl+C), programming errors (e.g., segmentation violations), or hardware problems. Kylix maps certain Linux Signals to exceptions, as shown in the following table:
Chapter 10: Exception Handling and Resource Protection
227
Table 10-1 Signal
Description
Kylix Exception
SIGINT SIGFPE
User Interrupt (Ctrl+C) Floating-Point Exception
SIGSEGV
Segmentation Violation (AV)
SIGILL
Illegal Instruction
SIGBUS
Bus Error (Hardware Fault)
SIGQUIT
User Interrupt (Ctrl+\) (backslash)
EControlC EDivByZero, EInvalidOp, EZeroDivide, EOverflow, EUnderflow ERangeError, EIntOverflow, EAccessViolation, EPrivilege, EstackOverflow ERangeError, EIntOverflow, EAccessViolation, EPrivilege, EStackOverflow ERangeError, EIntOverflow, EAccessViolation, EPrivilege, EstackOverflow Equit
Notice that SIGSEGV, SIGILL, and SIGBUS signals are mapped to multiple exceptions, depending on the fault that occurs. Note: What is a Linux Signal? A signal is a software interrupt used to handle asynchronous events. Fortunately, the designers of Kylix have simplified dealing with signals for applications by mapping most of them to exceptions. Shared objects do not normally map signals to exceptions. For more about shared objects and exceptions, see Chapter 8. Once an exception is created, there is no need to destroy them, as they are implicitly destroyed when they are handled.
Propagation When an exception object is created, it searches for an exception handler. If the current block does not handle the exception, the object will begin to unravel the call stack looking for an exception handler that will. The object is said to be propagating or walking back up the call stack until it is handled. If the object propagates all the way back to the application object, the application will handle the exception by displaying the error message that was sent by the system. For more on the application exception handler, see the “Customizing Application Exception Handling” section later in this chapter. By default the Application Exception Handler displays the exception, unless it is EAbort. With that in mind the following code is unnecessary: try // code // code
228
Chapter 10: Exception Handling and Resource Protection except on e : exception do ShowMessage(e.Message); end;
The reason the code is unnecessary is because it is duplicating the same code as the the default exception handler. Console applications do not have an application object and are handled by the procedure pointed to by the ExceptProc variable found in SysUtils.pas. If ExceptProc does not have a function assigned to it, Kylix will display a run-time error of 230 and halt the application.
Destruction Exceptions should never be destroyed manually. Any code that handles an exception will also destroy the exception object implicitly.
Exception Hierarchy Like all Kylix classes, the Exception class descends from TObject. Similarly, other units create their own exceptions as descendants of an Exception. While the full class hierarchy will not be discussed here, let’s take a look at a small portion of it. EExternal is a subclass of Exception that deals with external occurances like access violations. The hierarchy for EExternal is shown in Figure 10-1. Looking at the partial class hierarchy, exceptions go from generic (Exception) to specific (EDivByZero).
Figure 10-1: The CLX exception hierarchy
Chapter 10: Exception Handling and Resource Protection
229
Handling Exceptions: Try/Except The first exception handling construct is the try/except statement. Try/except lives by this motto: “Try this code. If an exception occurs, do this.” More formally, the try/except block looks like this: try // make sure this starts the code block to protect ; ; except on do ; on do ; else end; // try/except.
Notice that the try block does not require a begin and end in order to perform multiple statements. In the except block, a begin and end is required when multiple statements are used. Also, the try/except statement is one of the places in Pascal where you will see an end without a begin. Because of this, it is a good idea to comment the end line to avoid confusion, especially if it occurs at the end of a method. Finally, an else can be used to trap exceptions that have not been previously handled. Exception blocks are able to handle multiple exceptions by using multiple exception handlers. An exception handler specifies which exception that it is responsible for dealing with and is denoted by the keyword on. In order for an exception block to handle multiple exceptions, they must be ordered from specific to general. In other words, if two exceptions are being caught, make sure the general exception handler (Exception) is last. Furthermore, exception handlers will catch any descendant class. That is, if an exception handler catches the parent, any descendants will also be caught, unless there is a specific handler before the parent’s handler. try // do something that may cause an exception except on EDivByZero do ShowMessage('Uh oh.. Div by Zero Exception'); // EExternal is the ancestor to EDivByZero. on EExternal do ShowMessage('Uh oh.. External Exception'); on EDatabaseError do ShowMessage('Uh oh.. Database Exception'); on Exception do ShowMessage('Uh oh.. General Exception!'); end;
If the general exception handler is first, it will receive all exceptions and the specific exception handlers will never be invoked.
230
Chapter 10: Exception Handling and Resource Protection
Getting the Inside Info In an exception handler, a variable can be declared to access the various properties and methods of the exception class. An example of this is shown in the following code fragment: try // do something that may cause an exception except on e : EDatabaseError do ShowMessage('Db Exception' + e.message); on e : Exception do ShowMessage('Gen Exception' + e.message); end;
Notice that the scope of the variable is local to the exception handler. The same variable can be used in multiple exception handlers. When using an else statement, the only access to the exception object is through the function ExceptObject, which returns the object that is associated with the current exception. See the Tip in the “Programatically Creating Exceptions” section for an example of how to use this function.
Resource Protection with Try/Finally The second construct to discuss is the try/finally statement. Try/finally is often compared to try/except, when, in reality they are two different things. As mentioned before, try/except says, “Try this code. If an exception occurs, do this.” Try/finally, on the other hand, says this: “Try this code. Even if an exception occurs, do this.” The difference is that in the second statement, the do this section takes place whether an exception occurs or not. Try/finally is commonly used to ensure that resources are released after they are used. A typical use looks like this: var MemBlock : pointer; begin GetMem(MemBlock,1024); try // do something with the chunk of memory finally FreeMem(MemBlock); end; // try/finally end;
Using a try/finally block is also a good way to clean up when a subroutine has multiple exit points. Consider the following code: GetMem(MemBlock,1024); try // do something with the chunk of memory
Chapter 10: Exception Handling and Resource Protection
231
if SomeCondition then Exit; // more stuff if AnotherCondition then Exit; // more stuff finally FreeMem(MemBlock); end;
The memory will be released even if SomeCondition or AnotherCondition are true. A finally block is guaranteed to be executed, no matter how the subroutine is terminated. An important point to remember is that if an exception occurs in a finally block, the new exception overwrites the old exception and the unwinding process starts again. If there is any remaining code in the finally block after an exception fired, that code is not executed.
Programmatically Creating Exceptions When exceptions need to be created within a subroutine, use the keyword raise to create them. Normally, raise is passed an exception object to create, as shown in the code fragment below: // Detected some error condition raise Exception.Create('Danger Will Robinson!');
Another common way of raising exceptions is to use the CreateFmt constructor as shown here: // Detected some error condition raise Exception.CreateFmt('Danger %s',[Person]);
The constructor is not assigned to a variable because the exception object does not need to be referred to again. It will be handled and destroyed by whatever exception handler it encounters first. Occasionally, an exception must be handled by more than one code block. That is, the exception must be dealt with not only by the block in which it’s created, but also in the calling block. Reraising exceptions is accomplished by calling raise within an exception handler but without a parameter like this: try // something that causes an exception except on exception do begin // other exception handling here
232
Chapter 10: Exception Handling and Resource Protection // possibly clean up, reporting, etc. raise; // re-raises the same exception end; end;
Tip: While raise is normally used with an Exception object or one of its descendants, it is perfectly legal to raise any object. The only difference is that the default exception handlers will only show that an exception occurred at an address, with no additional information. For example, suppose there is a need for additional debugging information that is useful to have (e.g., a stack trace, certain variable contents, user name, etc.) when a user reports a problem with an application. The example below demonstrates how this could be implemented by throwing a TStringList object that contains the information that is needed. unit frmMain; interface uses SysUtils, Types, Classes, Variants, QGraphics, QControls, QForms, QDialogs, QStdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); procedure FormCreate(Sender: TObject); procedure FormShow(Sender: TObject); private { Private declarations } Flist : TStringList; procedure Foo1; procedure Foo2; procedure Foo3; public { Public declarations } end; var Form1: Tform1; implementation {$R *.xfm}
Chapter 10: Exception Handling and Resource Protection procedure TForm1.FormCreate(Sender: TObject); begin Flist := TStringList.Create; Flist.Add('FormCreate'); // possibly more code here end; procedure Tform1.FormShow(Sender: TObject); begin Flist.Add('OnShow'); // possibly more code here end; procedure Tform1.Button1Click(Sender: TObject); begin Flist.Add('ButtonClick'); try Foo1; except on e : exception do begin // in case a "normal" exception was thrown… ShowMessage('Exception was fired: ' + e.message); end; else if ExceptObject is TStrings then begin ShowMessage('TStrings object was fired: ' + #10 + (ExceptObject as TStrings).Text); end; end; end; procedure Tform1.Foo1; begin Flist.Add('Foo1'); // do some stuff Foo2; end; procedure Tform1.Foo2; begin Flist.Add('Foo2'); // do some more stuff Foo3; end;
233
234
Chapter 10: Exception Handling and Resource Protection procedure Tform1.Foo3; begin Flist.Add('Foo3'); // do even more stuff; raise Flist; end; end.
When this example is run and the button is pressed, instead of the normal exception dialog box, the dialog in Figure 10-2 appears. Notice in the exception handler the code to display this dialog is caught in the else clause. If an object other than an Exception object is raised, make sure that the appropriate handler(s) are in place or use the else clause in the exception handlers.
Figure 10-2
Exceptions and the Afterlife As mentioned previously, exceptions continue to unwind the stack until they locate a handler. If an exception handler takes care of the exception, the code will continue to the end of the exception block. Suppose the following code is executed: writeln('one'); try writeln('two'); raise Exception.Create('MyException'); except on e : exception do writeln('exception fired ', e.message); end; writeln('three');
The output of the program will look like this: one two exception fired MyException three
Notice that even though the exception fired, three is still displayed. Now if the code is changed to use a try/finally statement: writeln('one'); try writeln('two'); raise Exception.Create('MyException');
Chapter 10: Exception Handling and Resource Protection
235
finally writeln('finally fired'); end; writeln('three');
The output will look like this: one two finally fired Exception Exception in module at 085168B MyException
Finally, both constructs can be nested to get the best from both worlds: writeln('one'); try try writeln('two'); raise Exception.Create('MyException'); finally writeln('finally fired'); end; except on e : exception do writeln('exception fired ',e.message); end; writeln('three');
And now the output looks like this: one two finally fired exception fired MyException three
Remember that using a try/except block handles exceptions while try/finally ensures that any code in the finally block gets executed even if an exception is raised. Tip: What not to do with exception handlers! Never, never use an exception handler in this way: try // // // //
code code code etc,
- line 1 - line 2 - line 3 etc, etc..
236
Chapter 10: Exception Handling and Resource Protection except // do nothing end; // continuing with the program…
Why? Since the exception handler does not do anything, it literally “eats” the exception. Nothing is displayed, logged. Nothing. Chances are very good that someday, something unexpected will happen, and you will spend hours trying to track it down. Suppose that the code above is doing some long, involved calculations and for whatever reason, an exception fires on line two. Now, execution of the program continues along as if nothing has happened. This is bad programming practice. The moral of this story is to either report the exception in some manner, or remove the exception handler and let the default handler deal with it.
AM FL Y
Nesting Exceptions Sometimes, it is necessary to trap a low-level exception and replace it with a more “user-friendly” version. This is done easily by capturing the exception in a handler and raising a new exception to replace it. The following code demonstrates this concept:
TE
try GetMem(MemBlock,1024); try // do something with the chunk of memory finally FreeMem(MemBlock); end; // try/finally except on EoutOfMemory do raise Exception.Create('Unable to allocate memory. ' + 'Contact technical support'); end;
Creating Custom Exceptions Creating new exceptions is no different from creating new classes. In fact, a large number of exceptions are simply aliases to the Exception class. Using an alias to create a custom exception is as easy as this: EMySimpleException = Exception;
Chapter 10: Exception Handling and Resource Protection
237
Instead of raising a standard exception, use the alias that was created like this: raise EMySimpleException.Create('Boom!');
Aliases provide a placeholder for future enhancements. If in the future, the need arises to make an actual exception class, the alias makes for an easier conversion, since all of the places the exception has been used have already been identified. Another alternative is to create a new class that descends from the appropriate exception. Because exception attributes are populated with information from the system, it is a little unusual to define additional properties or methods. For instance, you may want to add information about the user that caused the exception as shown in this example: program ComplexException; {$APPTYPE CONSOLE} EMyComplexException = class(Exception) public constructor CreateUser(const UserName,Msg : string); end; constructor EMyComplexException.CreateUser(const UserName, Msg : string); begin inherited Create(Msg); Self.Message := 'User: ' + UserName + ' ' + Self.Message; end; begin writeln('one'); raise EMyComplexException.CreateUser('John Doe','Broke it'); writeln('two'); end.
The output from this program looks like this: one Exception EMyComplexException in module at 08051762. User: John Doe Broke it.
For most applications, the default Exception object works fine. Component builders and other developers who wish to have more control over exceptions will want to create their own exception classes.
238
Chapter 10: Exception Handling and Resource Protection
Customizing Application Exception Handling Handling exceptions at an application level is the last line of defense and usually involves more than just displaying the standard message. An application may want to log exceptions in a file. In order to acomplish this, assign an event handler to the application’s OnException event. The application’s OnException event handler has the following signature: procedure Application.OnException(Sender:TObject;E:Exception);
In order to dynamically assign an application’s event handler, simply create a method with the same signature and assign it to the application’s OnException handler. procedure TfrmAppHandler.Button1Click(Sender: TObject); begin Application.OnException := LogException; end; procedure TfrmAppHandler.LogException(Sender:TObject;E:Exception); var f:TextFile; begin AssignFile(f,'TestFile.txt'); try if FileExists('TestFile.txt') then Append(f) else Rewrite(f); Writeln(f,DateTimeToStr(Now) + ' - ' + E.ClassName + ' : ' + E.Message); finally CloseFile(f); end; // try/finally end;
Chapter 10: Exception Handling and Resource Protection
239
Summary Unexpected errors occur in almost every program. Exceptions provide a convenient method of handling these unexpected errors in a clean, predictable manner. They should be used when unexpected situations occur and should not be used for errors that are expected and can be handled. Capture exceptions using a try/except block using an exception handler. Protect resources by using a try/finally block to ensure that resources are returned. Exceptions are created by using raise, and are destroyed automatically when an exception handler is finished with it. Exceptions are a powerful tool. Use them wisely to create polished and professional software.
Debugging and the Chapter 11 Debugging and the Debugger Debugger
Introduction Undocumented program feature. Anomaly. Bug. No matter what they are called, they inevitably show up when developing applications. Debugging, or more accurately preventing bugs, actually begins in the design phase of software development. But preventing bugs certainly does not stop at the design phase. It is, rather, a frame of mind or a debugging attitude that actively looks for and prevents those little critters from creeping out of the woodwork. This chapter demonstrates several techniques for preventing bugs. Furthermore, the integrated debugger will be discussed, covering the various tools that make finding and squashing bugs easier.
Programming Utopia In a perfect world, developers would work from an exact specification that lays the foundation for the code that they are writing. Taking the specification, the developer would skillfully write code that would not need any testing. In the real world, developers are human and make mistakes that cause applications to act in an unexpected way. By using the proper techniques, developers can write software that is close to perfection.
Debugging Techniques Debugging techniques, when used in a proper manner, can prevent or catch many bugs before even needing to use a debugger. These techniques are divided into two categories that are discussed in the following sections.
241
242
Chapter 11: Debugging and the Debugger
Design Phase Debugging begins in the design phase. What exactly does that mean? Detecting potential problems when designing applications will greatly reduce the amount of time required after the code has been written. While designing applications is outside the scope of this book, here are several design tips: n
Know the requirements of the application that is being designed.
n
Look at the big picture.
n
Complex designs usually signify trouble. Keep it simple.
n
Design several approaches, then choose the design that fits best.
n
Do not rush through design. Take time to really think it through.
n
Bounce ideas off of other team members, especially those who have been around the block.
n
When all else fails, use your instincts. With time and experience, they will usually lead you down the correct path.
In the mad rush to get an application out the door, do not skip the crucial step of designing the application. Like the Boy Scout motto, “be prepared” before starting to code. It will be an investment that will save countless hours of debugging.
Coding Phase Having a debugging attitude continues when writing the actual code. When writing code, think defensively. Expect the unexpected. Continually ask yourself, “What would cause this to fail?” The list below reveals several tips that will help with writing defensive code. 1. Always check the return value of every function. Never assume that the function or method will not fail. Otherwise, Murphy’s Law will raise its ugly head, and the one time that the function fails comes at the most inopportune time. (Usually while performing a demonstration for your customer or your boss!) Decide how the failure will be handled. Should an exception be raised? Or can the function deal with the failure? 2. Validate the input parameters of all subroutines. Never take for granted that the subroutine will be called with the proper arguments. For var and object parameters, make sure to verify that the parameters are not nil by using the Assigned function. procedure TForm1.Button1Click(Sender: TObject); begin if Assigned(Sender) then begin // do something here..
Chapter 11: Debugging and the Debugger
243
end; end;
3. Use Assert to verify conditions that are assumed to be true. Assertions, when used properly, are very useful. Assert tests a condition, and if the condition is false, either raises an EAssertionFailed exception or generates runtime error 227. Exceptions are raised when the SysUtils unit is included in the application. // for demonstration purposes only // when this is compiled with assertions turned off, // this function still could fail! // function SafeIntegerDivide (num, denom : integer) : integer; begin Assert(denom 0,'Denominator cannot be zero!'); Result := num div denom; end;
When Assert detects that the condition is false, it raises an exception that contains the complete path and filename and line number where the assertion failed. The example shown above shows how to use assertions in an application. Assertions are removed from generated code by specifying the appropriate compiler directive ({$C-} or {$ASSERTIONS OFF}). Since they are typically removed from a finished build, assertions should not take the place of valid error checking. The example above illustrates this point. What happens when the assertions are turned off? If the denom variable is zero, the function will still break; instead of raising an assertion exception, it will raise a divide by zero exception (EDivByZero). 4. Use try/except blocks to capture and handle exceptions appropriately. Get into the habit of expecting the unexpected; catch those exceptions. try // do some stuff // more stuff here // something in here is bound to throw an exception // somewhere, sometime… except // more specific exception handlers here on e : exception do // handle exception appropriately. Report it, Log it, etc.. end;
5. Use try/finally blocks to guarantee that resources are released, even if an exception is raised. Anytime a resource is created or opened, automatically add a try/finally block. frm := TMyForm.Create(nil); try if frm.ShowModal = mrOK then begin
244
Chapter 11: Debugging and the Debugger // take the appropriate action end; finally frm.Free; end;
6. Eliminate compiler warnings and hints. They usually indicate a potential problem. 7. Use code walkthroughs when a unit is completely written. Have other developers look at the code. 8. Use trace logging to help track down elusive bugs. Trace logging is especially useful for debugging multithreaded and long running applications, 9. Use the debugger to step through any new code, validating that the algorithm works as intended. Some of these techniques may seem simple and obvious, but they can save countless hours when they are consistently and properly used. Remember this: Program defensively. The goal is to write code that works the way it is supposed to the first time!
Debugging Applications Using the techniques described previously, the application has been defensively designed and coded. Now it is ready for end-user testing. While you’re anxiously waiting for those positive reports on how much the users love the application, the phone rings. Several bugs have been found. Now what? First thing is to take a deep breath and relax. Determine the steps necessary to reproduce the bug. Listen to what the person who found the bug says. Then try to duplicate the bug. If the steps do not cause the bug to reappear, observe the person who found the bug. Or if trace logging is being used, examine the log file. Now that the steps needed to duplicate the bug are determined, the battle is half over. Time to roll up the sleeves and dig into the code to locate the pesky little critter. Based on the behavior of the bug, make an educated guess where the bug is probably located. Usually, the most recently added code should be the first guess. Locating the bug is the other half of the battle. Sometimes the location of the bug is obscured, or worse, it is a symptom of another problem. Tracking bugs down requires patience and perseverance. One technique that works effectively to help locate bugs is divide and conquer. Divide and conquer works by systematically taking the code being debugged and dividing it into two halves. The bug will appear in one of the halves. Now repeat the process, ignoring the portions of code that do not affect the bug. Eventually the bug will be evident and ready to be fixed.
Chapter 11: Debugging and the Debugger
245
Now that the bug has been reproduced and located, it is time to actually fix the bug. Carefully study the code and determine what the code is supposed to do that it is not doing. Most importantly, slow down! Do not rush and throw in a hack that will be only a quick fix and likely cause another bug or two. The most important step is to thoroughly retest the application to validate that the changes did not introduce additional bugs or even make things worse! Ideally, go through an entire set of regression tests to perform the validation. Finally, take some time to learn from the bug. Determine what could have been done to prevent this bug from occurring. Look at the experience with an attitude of learning what not to do the next time. Bug hunting should be viewed as a way to improve coding skills. The best developers are those who have made the most mistakes and learned what not to do. Tip: When debugging an application that is being developed by multiple developers or has third-party components, assume that the bug is yours. Then prove or disprove your theory. Even if the bug is not something that you wrote, you will be able to give detailed steps to the appropriate person to help them fix the bug, or perhaps even tell them how to fix the bug!
The Integrated Debugger One of the most useful features of Kylix is the debugger. It provides the necessary tools to quickly locate those pesky bugs. The debugger allows for setting breakpoints, watching variables, modifying values while the process is running, inspecting variables, and monitoring the call stack viewer, modules that are loaded by the process, threads of the process, an event log window, and the CPU window. These powerful tools are just a click or two away.
Global Debugging Options Options are available for controlling how Kylix handles exceptions and signals, what messages are logged to the event log, debug inspector defaults, and other general debugging options. These options are located in the Tools menu under Debugger Options. Tip: By default, when the debugger detects an exception Kylix halts the application being debugged. If the application throws many exceptions, the repeated halting can get irritating. Use the Language Exception tab in Debugger Options and either disable Delphi Exceptions or add the specific exception(s) to the Exception Types to Ignore section.
246
Chapter 11: Debugging and the Debugger
Project Debugging Options Before debugging an application, make sure that certain options are enabled. These options are located in the Project Option dialog’s Compiler tab. The table below lists the debugger related options and explains them. Table 11-1 Option
Equivalent Compiler Directive
Description
Debug Information
{$D +|-}
Local Symbols
{$DEBUGINFO ON|OFF} {$L+|-}
When enabled, includes line number information in generated files.
Reference Info
{$LOCALSYMBOLS ON|OFF} {$Y+|-}
{$YD}
Assertions
{$DEFINITIONINFO ON|OFF} {$C+|-}
TE
Definitions Only
{$ASSERTIONS ON|OFF}
Use Debug DCUs
Requires that Debug Information and Local Symbols be enabled. Used by the Code Browser, Code Explorer, and Project Browser; when checked contains information regarding where identifiers are declared and referenced. Similar to Reference Info, but when checked only contains information where identifiers are declared.
AM FL Y
{$REFERENCEINFO ON|OFF}
When enabled, includes information about local symbols.
Not applicable
When enabled, includes code that checks to see if the assertion condition is false and raises an assertion exception with the given information. When enabled, allows for stepping into the code that comes with Kylix.
Walking Through Code, Step by Step Normally when testing an application, it is started within the Kylix environment. By pressing the F9 key, using the Run button in the toolbar, or selecting Run from the Run menu, the application is started. Running an application with Kylix is often used to see what causes an unexplained exception. Sometimes, however, there is a need to examine each line of code as it executes. Kylix provides additional features that allow for stepping over and tracing into functions, tracing into the next source line and running to the current cursor location. Pressing F8 steps over functions, F7 traces into functions, Shift+F7 traces to the next source line, and F4 runs to the current cursor location. These features are also found in the Run menu. The stepping over and tracing into functions are so common that they appear in the toolbar as well.
Chapter 11: Debugging and the Debugger
247
Tip: To step into the source code that comes with Kylix, check the use debug dcu check box in the Project Options dialog’s Compiler tab. One of the best ways of understanding how Kylix does its “magic” is by stepping into the source code. Not sure what an object, method, or property does or how it works? Turn on the debug dcu option and step into the source. To paraphrase a popular movie line: “Use the code, Luke!” Understanding the inner details of Kylix provides the knowledge necessary to quickly, efficiently, and effectively debug applications.
Breakpoints There are four types of breakpoints: source breakpoints, address breakpoints, data breakpoints and module load breakpoints. All breakpoints, have the following features in common: 1. Condition: If specified, the breakpoint will stop execution when the condition evaluates to true. They can be specified by right-clicking on a breakpoint and choosing Properties. 2. Pass Count: Will stop execution when the specified count is reached. 3. Group: Breakpoints can be grouped for organizational purposes. Once grouped, all of the breakpoints can be enabled or disabled in the View | Debug Window | Breakpoints window by right-clicking and selecting the appropriate action.
Source Breakpoints Source breakpoints are the most commonly used feature of any debugger. They provide a way to pause a process to examine variables and other useful information. The easiest way to define a breakpoint is to click in the gutter of the code editor as shown in Figure 11-1. Alternatively, use the Source Breakpoint command found in the Add Breakpoint submenu of the Run menu.
Figure 11-1: A source breakpoint
248
Chapter 11: Debugging and the Debugger
Address Breakpoints Address breakpoints are only enabled when an application is currently running within the Kylix IDE. They too are found in the Add Breakpoint submenu of the Run menu. Similar to source breakpoints, they allow the developer to specify an address instead of a source line number. Kylix will attempt to resolve the address to a source line number, and if successful will show as a source breakpoint.
Data Breakpoints Data breakpoints are only enabled when an application is currently running within the Kylix IDE. They are also located in the Add Breakpoint submenu of the Run menu. A variable or memory address is specified that when it is written to will pause the execution of the application.
Module Load Breakpoints Module load breakpoints are only enabled when an application is currently running within the Kylix IDE. Located in the Add Breakpoint submenu, they allow the application to pause when a module is loaded. Modules include both shared object libraries and Kylix packages.
Debug Fly-by Tips When you are debugging an application and would like to examine the value of a variable, hold the mouse over the variable. A debug tooltip pops up and its current value is displayed if the variable is within scope or has not been optimized out of the program. In order to see optimized variables, in the Project | Options dialog, uncheck the Optimization check box in the code generation section or use the {$O-} option.
Debugger Windows Kylix provides a number of debugger windows that assist the developer in monitoring information about the application while it is running. Following is a discussion of the debug windows that are available.
Watches Watches are useful for determining the value of variables while an application is being debugged. The easiest way to add a variable to the watch window, is to click on the variable to be added, right-click to bring up the editor’s context menu, and choose Debug | Add Watch at Cursor. Alternatively, press Ctrl+F5. Variables can also be dragged into the watch window. Certain optimizations can cause the
Figure 11-2: The Watch List window
Chapter 11: Debugging and the Debugger
249
watch window to display a message stating that the variable is inaccessible here due to optimizations. Like the debug fly-by tips, enable the variables by turning the optimizations off.
Evaluate/Modify The Evaluate/Modify window has many uses. Not only can the value of a variable be examined, it can also be changed. This allows for testing conditions that normally do not occur while the application is running. Furthermore, variables can be added to the watch window or inspected. It can only be invoked when the cursor is on a variable that is accessible with the current scope, using the code editor’s context Debug | Evaluate/Modify menu.
Figure 11-3: The Evaluate/ Modify dialog
Debug Inspector Examining the contents of objects while debugging is painful using the debug fly-by tips, watch window, or the Evaluate/Modify window. Use the Debug Inspector, shown in Figure 11-4, to see the data, methods, and properties of an object while debugging. Think of it as an object inspector on steroids used for debugging. The Inspector can be invoked from the Run menu or from the Evaluate/Modify window by pressing the appropriate button. Figure 11-4: The Debug Inspector
250
Chapter 11: Debugging and the Debugger
In each tab, the data, methods, and properties are listed in order of the hierarchy, starting with TObject and working on down to the actual class type. The Data tab contains class data members, the Methods tab contains class member functions, and the Properties tab contains the properties. Note that you can inspect many different data types, but only the Methods and Properties tabs appear if the inspector is looking at a class or interface.
Call Stack In the heat of debugging, it is easy to get lost while stepping into methods and subroutines. Use the Call Stack window, shown in Figure 11-5, to see the complete list of methods, functions, and procedures that have been executed to get to where you are now. Located in the View | Debug Windows menu, the Call Stack window can also be invoked by pressing Ctrl+Alt+S.
Figure 11-5: The Call Stack debug window
Modules Want to know what shared objects and packages an application requires? Use the Modules window, located in the View | Debug Windows menu, (or press Ctrl+Alt+M). Notice in Figure 11-6 that the window is divided into three sections. The top left section lists any processes that are running and the shared objects and packages that it references. Highlighting an entry in this section displays the list of subroutines that are available in the section on the right. A list of units and source files, if known, are displayed in the bottom section. Module breakpoints can be set by clicking on the shared object or package, right-clicking, and selecting Break on Load from the menu. Right-clicking on a unit allows for the viewing or editing of the source file.
Chapter 11: Debugging and the Debugger
251
Figure 11-6: The Modules window
Threads Debugging applications that contain multiple threads is not trivial. Kylix makes it easier by providing a Thread Status window to examine the state of each of the threads while it is running or paused by a breakpoint. Right-clicking on the process, the first line in the window, allows either terminating the process or temporarily changing the properties of the process. For a thread, right-clicking allows for viewing the source of the thread, if available, going to the source of the thread, if available, or forcing a thread to be the currently running thread. Figure 11-7 shows an example of the ThrdDemo that ships with Kylix.
Figure 11-7: The Thread Status Window
252
Chapter 11: Debugging and the Debugger
Event Log Found in View | Debug Windows, the Event Log can also be brought up using the Ctrl+Alt+V keyboard shortcut. In it, there are four types of messages that are displayed. Breakpoint messages detail where the breakpoint occurred. Process messages show when they are loaded and unloaded. Thread messages reveal when they are created and destroyed. Finally, module messages detail when packages and shared objects are loaded and unloaded, as well as the base address of the module and if debugging information is available. See Figure 11-8. All messages can be enabled or disabled by right-clicking in the Event Log and choosing Properties. Alternatively, they can be changed in the Tools | Debugger Options | Event Log tab.
Figure 11-8: The Event Log window
Also found in the Event Log context menu are options to clear the Event Log and save the log to a file.
CPU Window The CPU window is the powerhouse of the debugging windows. Located in View | Debug Windows | CPU, it can also be brought up by using the keyboard shortcut Ctrl+Alt+C or by right-clicking in the editor and clicking on View CPU in the Debug submenu. Divided into five windows, the CPU window contains a disassembly pane in the top left. Below it is the memory dump pane. In the upper right, there is a CPU registers pane and a Flags pane. Underneath is the machine stack pane.
Chapter 11: Debugging and the Debugger
253
Figure 11-9: The CPU window
It helps to understand assembly language to fully utilize the CPU window. Within the disassembly pane Kylix will show both Pascal and assembly, if possible. Make sure that debugging information is enabled and the source path and debug source path is properly set in the Directories/Conditionals tab of Project Options. This ensures that the CPU window can show Pascal source code as well as the corresponding assembly source code.
FPU Window When dealing with floating-point operations, the FPU window is invaluable. It is divided into three sections, from left to right, the FPU Registers pane, the Control Flags pane, and the Status Flags pane.
Figure 11-10: The FPU window
Bring up the FPU window by selecting View | Debug Windows | FPU or by using the Ctrl+Alt+F keyboard shortcut.
254
Chapter 11: Debugging and the Debugger
Docking One nice feature of Kylix is that almost all of the debug windows can be docked to save space on the screen. Shown in Figure 11-11 is one possible way of docking the debugging windows.
Figure 11-11: Docked debugging windows
Debugging Shared Object Libraries and Packages Kylix makes debugging shared object libraries and packages easy. First, check the environment variables in Tools | Environment Options to ensure that the current directory (the shorthand for current directory is a period) is set in the LD_LIBRARY_PATH. Then, open up the project containing the shared object or package. Choose Run | Parameters from the menu. Pick the appropriate host application (the executable) that will use the shared object or package. Set any breakpoints as appropriate. Run the host application using the Run command in the
Chapter 11: Debugging and the Debugger
255
menu. Now, any breakpoints will stop execution when the application uses either the shared object or package.
Summary Debugging starts with a proper mentality. Once obtained, it carries over into all aspects of development, from design to testing. When coding, remember to program defensively. Continually ask yourself “What would happen if…” and take measures to prevent or detect those conditions. When those pesky critters do sneak past these first defenses, Kylix steps up and provides the tools necessary to track them down and help to exterminate them. Take the time to learn how to use these powerful tools so that when they do arrive, any bugs are eliminated quickly.
TE AM FL Y
dbExpress and Chapter 12 dbExpress and DataCLX DataCLX Introduction One of Delphi’s greatest selling points is its ability to access, display, and navigate data. Kylix continues this tradition with its DataCLX components and an underlying architecture called dbExpress that provides efficient and powerful access to data. This chapter explores the Kylix data access architecture and demonstrates the power of dbExpress and DataCLX components. dbExpress and DataCLX together represent the Kylix and Delphi 6 data access and display layer. A common misconception is that the SQLComponents (SQLConnection, SQLDataSet, etc.) are part of dbExpress because they live on the dbExpress tab in the component palette. This is actually not the case. All of the components on the component palette are part of DataCLX. dbExpress refers to the underlying drivers and data access architecture that provides the communication layer between a datasource and DataCLX.
Figure 12-1: The DataCLX architecture (used with permission of Borland Software Corporation)
259
260
Chapter 12: dbExpress and DataCLX
Note: This chapter contains a number of references to the Borland Database Engine (BDE). The BDE was the main way to get data to an application before Delphi 6. Although the BDE does not have anything to do with Kylix directly, it is the forerunner to dbExpress and has had a lot of influence over the goals, implementation, and use of the dbExpress architecture. If you have not used the BDE, just keep in mind that the qualities of dbExpress are in part the result of what Borland learned from the BDE.
dbExpress dbExpress represents the underlying data access architecture that is used by DataCLX components to natively access a wide array of database servers. It is a set of low-level interfaces on which dbExpress drivers are built. Database vendors implement the dbExpress interfaces to provide the raw data retrieval needed for accessing data in applications. According to Borland documentation, “dbExpress is a cross-platform, database-independent and extensible interface for processing dynamic SQL.” dbExpress has been largely attached to the release of Kylix but is also available in Delphi 6. It represents a significant advance and reorganization of the Borland data access mechanism. dbExpress is designed to provide an underlying architecture that is: n
Smaller
n
Faster
n
Cross-platform
n
Easier to configure/deploy
n
Easier to extend/implement
Note: These things are all nemeses of the BDE and, to varying degrees, hurt the value and efficiency of Delphi applications (that use the BDE) in a production environment. The biggest limitation of the BDE is that it is a Windows-only solution. dbExpress is architected in such a way that the code used to build a dbExpress driver — usually C code — can be platform independent. Thus, the same code can compile a dbExpress driver on Windows and Kylix, and theoretically to any other platform where DataCLX may end up.
Chapter 12: dbExpress and DataCLX
261
Complexity Note: One of the goals of dbExpress is to implement a thin, fast data access layer with minimal configuration and deployment issues in comparison to the BDE. One of the biggest complaints about the BDE was the time-consuming installation and configuration issues that were always present in Delphi application deployment.
Configuration Configuration of dbExpress is done primarily through two files: dbxdrivers and dbxconnections. The dbxdrivers file contains information on all installed dbExpress drivers. The LibraryName and VendorLib attributes specify the names of vendor-specific libraries through which requests and responses to and from the database are sent. In addition to those attributes, each section of the dbxdrivers file contains default parameter values for database connections. Each time a new connection is created using this driver, these values are inserted as connection defaults. The dbxdrivers configuration file is shown below: [Installed Drivers] INTERBASE=1 [INTERBASE] GetDriverFunc=getSQLDriverINTERBASE LibraryName=libsqlib.so.1 VendorLib=libgds.so.0 BlobSize=-1 CommitRetain=True Database=database.gdb Interbase TransIsolation=ReadCommited Password=masterkey RoleName=RoleName ServerCharSet=ASCII SQLDialect=1 User_Name=sysdba WaitOnLocks=True
Note: The driver information shown above is specific to Interbase. Each database server requires different parameters to obtain and manage a connection. The dbxconnections file contains information on already defined connections to a database. Each entry (known as aliases in the BDE) describes a different connection profile for any database that dbExpress can connect to (that is, any database that has a dbExpress driver). Configuring dbExpress is simply a matter of editing
262
Chapter 12: dbExpress and DataCLX
this file. This could conceivably be done manually but will more commonly be done through the TSQLConnection component. For more on establishing a connection with a dbExpress driver, see the “Establishing a Connection with TSQLConnection” section later in this chapter. The dbxconnections configuration file is shown below: [OracleConnection] DriverName=ORACLE BlobSize=-1 BlockingMode=True DataBase=Database Name ErrorResourceFile=./DbxOraErr.msg Oracle TransIsolation=ReadCommited Password=password User_Name=user [IBLocal] DriverName=INTERBASE BlobSize=-1 CommitRetain=True Database=/opt/interbase/examples/employee.gdb Interbase TransIsolation=ReadCommited LocalCode=0x0000 Password=masterkey RoleName=RoleName ServerCharSet=ASCII SQLDialect=1 UserName=sysdba WaitOnLocks=True
Deployment Deploying applications that use dbExpress is easy. To deploy dbExpress, three files are required. For example, to deploy dbExpress for an application that uses Interbase, you would deploy these files in addition to your application: Table 12-1 On Linux
On Windows
Description
libsqlib.so
dbexpint.dll
libmidas.so
midas.dll
Libgds.so
gds32.dll
This is the implemented Interbase driver for dbExpress. This file provides functionality for data manipulation in the ClientDataSet. The Interbase (or other database) client library. (Note: This file is usually installed as part of the database client installation and would have to be installed with any application that uses the client software.)
Chapter 12: dbExpress and DataCLX
263
Performance Probably the most important factor in a data access architecture is its performance. Efficiency makes a big difference in how well an application is received by its audience. Performance can also affect the accuracy of data by ensuring that updates are applied quickly. dbExpress was created with maximum performance as a central goal.
Metadata Caching Unlike the BDE (see the note below), the main use of metadata in a dbExpress application is for the creation of field objects. Because dbExpress applications are generally updated through direct SQL statements rather than through data set components, it is not necessary for copies of metadata to be stored in as many places in the application. It makes sense to only retrieve the metadata for the client where the structure of the data is used. Keeping the metadata out of the dbExpress layer creates a thinner and more efficient application and lowers the in-memory size of the program.
Internal Query Generation Another factor that affects performance is the number of internal queries that are executed by the database driver. dbExpress only executes queries that are requested by the user, which greatly increases its performance. Note: In BDE applications, multiple copies of the database metadata are retrieved. The application and the BDE both retrieve and maintain at least one copy of the metadata and very often more than one. This greatly increases the size of BDE data access and greatly decreases the efficiency with which it operates. The BDE also introduced a number of internal queries that it used to retrieve metadata, access blob data, and perform other internally necessary functions.
Developing dbExpress Drivers An example of creating dbExpress drivers is beyond the scope of this book; however, it is important to understand that dbExpress has been designed to provide a simpler way to create database drivers. There are five core interfaces needed for dbExpress data access: SQLDriver, SQLConnection, SQLCursor, SQLCommand, and SQLMetaData. Classes that implement these interfaces are used to provide a dbExpress driver for a particular database. Following is a summary of their methods. For more information on these interfaces and their methods, consult the Kylix help files.
264
Chapter 12: dbExpress and DataCLX
Table 12-2 dbExpress Interface
Description
Method List
SQLDriver
Provides initialization functionality like loading the database client library and setting up the environment. Used to retrieve a SQLConnection object.
GetOption, GetSQLConnection, SetOption
SQLConnection Used to retrieve a connection to the database and a SQLMetaData object for retrieval of database metadata as well as a SQLCommand for query processing. The SQLConnection object also controls database transactions.
BeginTransaction, Commit, Connect, Disconnect, GetErrorMessage, GetErrorMessageLen, GetOption, GetSQLCommand, GetSQLMetaData, Rollback, SetOption
SQLCommand
Provides query and stored procedure processing. If the query or stored procedure returns a resultset, a SQLCursor object is returned.
Close, Execute, ExecuteImmediate, GetErrorMessage, GetErrorMessageLen, GetNextCursor, GetOption, GetParameter, GetRowsAffected, Prepare, SetOption, SetParameter
SQLCursor
Controls both the data and the metadata returned from a query or a stored procedure. The SQLCursor is used to retrieve data and data constraints described by the metadata. Notice that the SQLCursor object is a unidirectional cursor, providing a Next method but not a Prior method.
GetBcd, GetBlob, GetBlobSize, GetBytes, GetColumnCount, GetColumnLength, GetColumnName, GetColumnNameLength, GetColumnPrecision, GetColumnScale, GetColumnType, GetDate, GetDouble, GetErrorMessage, GetErrorMessageLen, GetLong, GetOption, GetShort, GetString, GetTime, GetTimeStamp, IsAutoIncrement, IsBlobSizeExact, IsNullable, IsReadOnly, IsSearchable, Next, SetOption
Chapter 12: dbExpress and DataCLX
265
dbExpress Interface
Description
Method List
SQLMetaData
Retrieves database metadata used by Kylix (and Delphi GetColumns, and C++ Builder) for such things as creating field objects. GetErrorMessage, A SQLMetaData object can be obtained from the GetErrorMessageLen, SQLConnection object. It is important to note that the GetIndices, GetObjectList, SQLMetadata object retrieves only a subset of the GetOption, metadata provided by most databases. This is because GetProcedureParams, dbExpress only uses a certain collection of metadata GetProcedures, GetTables, properties to pass on to other Kylix objects like SetOption TSQLQuerys and TSQLStoredProcs. Most of the additional metadata properties can be obtained through methods of the SQLMetaData object like GetOption and SetOption.
DataCLX DataCLX represents the components used to access, retrieve, and update data from a Kylix application. They are located on the dbExpress tab, because they use the underlying dbExpress architecture to deal with data. DataCLX is a completely new architecture compared to the traditional Dephi architecture and uses dbExpress to provide faster, thinner access to data.
Figure 12-2: The dbExpress tab
The TSQLConnection Class The SQLConnection component provides a central point of connection to a data store. It describes the application’s connection to the database through the parameters required by the dbExpress driver of the user’s choosing. The SQLConnection class provides a number of benefits to the application including: n
Managing database handles
n
Custom login functionality
n
Explicit transaction control
Chapter 12: dbExpress and DataCLX
Managing Database Handles When data sets connect through a SQLConnection, the SQLConnection’s database handle is used by everyone as an avenue for database communication. The KeepConnections property allows the SQLConnection to decide whether or not it should drop the database connection when there are no application data sets open. Setting this property involves the traditional argument of performance vs. resources. If KeepConnections is true, the application maintains an unnecessary connection to the database at all times. If it is false, then when no data sets are opened, the application drops its database connection and must wait for it to be reestablished when another data set is opened. Warning: Some database vendors charge for licenses on a per connection basis or limit the number of connections that the database can maintain. This can be another important reason to minimize and consolidate connections wherever possible!
AM FL Y
Providing Login Functionality
TSQLConnection allows the developer to create a customized login process, instead of relying on the database server’s default login process. Kylix provides a PasswordDialog in the object repository; however, the developer is free to create his or her own login screen. Providing customized login functionality is a nice, tight piece of coding. The TSQLConnection component provides an OnLogin event that is fired just before the application attempts to login to the database. The OnLogin event takes two parameters: the database component (which acts like the sender parameter) and the LoginParams property, which is a TStrings object that is pre-filled with a collection of name/value pairs that are required for your connection (the actual strings depend on what database you are connecting to).
TE
266
procedure TfrmEmployee.sqlcnctnMainLogin(Database: TSQLConnection;LoginParams: TStrings); begin with TfrmLogin.Create(Self) do try if ShowModal = mrOk then begin LoginParams.Values['User Name'] := edtUserName.Text; LoginParams.Values['Password'] := edtPassword.Text; end; finally Free; end; // try..finally end;
Chapter 12: dbExpress and DataCLX
267
Providing Explicit Transaction Control Maintaining transactional integrity is a vital part of most systems. Maintaining proper foreign key relationships and avoiding orphaned records are critical to sustaining properly normalized and useful data. A transaction is a group of database statements that must act as a logical unit. In other words, none of the statements succeed unless all of the statements succeed. If one fails, they all fail. SQLConnection provides three methods (StartTransaction, Rollback, and Commit) for controlling transactions that surround any operations on data sets that use its connection to talk to the database. SQLConnection also has a public TransactionsSupported property that provides the developer knowledge of whether the connection supports transactions. The TransactionsSupported property is read-only because it is up to the individual dbExpress driver to determine if transactions are supported for a particular connection. Note: Transactions are used a little bit differently in Kylix than in Delphi. Each of the three transactional control methods now takes a parameter of type TTransactionDesc. TTransactionDesc is a packed record that allows the developer to specify transaction-specific information including a unique transaction ID and an isolation level for the transaction. TTransactionDesc = packed record TransactionID : LongWord; {Transaction ID} GlobalID : LongWord; {Global transaction ID} IsolationLevel : TTransIsolationLevel{Transaction Isolation Level.} CustomIsolation : LongWord; {DB specific custom Isolation} end;
Isolation levels determine what data a transaction can see in the database. It can be set to one of four levels: Table 12-3 Isolation Level
Description
xiDIRTYREAD
The current transaction can see all changes made to data, even if the changes are part of another transaction that has not yet been committed. The current transaction can only see changes made by other committed transactions. This can create an incorrect view of data if other transactions are committed during this transaction. The current transaction sees a “snapshot” of the database at the moment at which it is started. It cannot see any subsequent changes to the database. This level is reserved for database-specific isolation levels.
xiREADCOMMITTED
xiREPEATABLEREAD
xiCUSTOM
268
Chapter 12: dbExpress and DataCLX
The key to transactions is deciding where to use them. For example, if a team of employees is working on a project that changes their job grade (which I am interpreting to be their relative priority for budgeting, time, etc.), it is important that they all transfer successfully. If any of the team members fails to have their job grade changed, business expenses and time allocations could be mistakenly assigned to the wrong project, etc. To make sure that every employee on the team makes the move as a logical unit, the application begins a transaction. When all of the team members have made the change, the transaction is committed. var TransRepeatable:TTransactionDesc; TransDirty:TTtransactionDesc; procedure TfrmEmployee.FormCreate(Sender:Tobject); begin Transrepeatable.IsolationLevel := xilRepeatableRead; TransDirty.IsolationLevel := xilDirtyRead; end; { … } procedure TFrmEmployee.StartEmpTransaction; begin TransRepeatable.TransactionID := GetTransactionID; sqlcnctnEmployee.startTransaction(TransRepeatable); end; procedure TFrmEmployee.RollbackEmpTransaction; begin sqlcnctnEmployee.Rollback(TransRepeatable); end; procedure TFrmEmployee.CommitEmpTransaction; begin sqlcnctnEmployee.Commit(TransRepeatable); end;
In the code above, the application has defined three methods for dealing with transactions. During the StartTransaction method, the TransactionID is assigned randomly by another user-defined method. Two different transaction description objects are declared to be used by the application. There are a number of strategies that can be used to create and assign transaction description objects. In this case, because of the location of the transaction description declaration, there can only be one Transrepeatable transaction at a time. Another method would be to keep a list of these objects to be used at different times in the application.
Chapter 12: dbExpress and DataCLX
269
Note: In this example, a transaction is used to apply multiple updates to a single table as a logical unit. Normally, a transaction is used to encompass heterogeneous statements, covering any database activity. As we will see in the next chapter, encapsulating several statements from the same data set is accomplished by using the principle of cached updates.
Establishing a Connection with TSQLConnection To define a database connection, right-click on the connection component and select Edit Connection Properties (or simply double-click on the connection component). The connection editor shows a list of already existing connections on the left and also provides a Driver Name box that allows the developer to filter the Connection Name list according to driver type. To use a particular connection, simply select it from the Connection Name list and click OK. If a new connection is desired, click the Add Connection button (+) on the editor’s toolbar. Fill in the appropriate parameter values on the right and the connection is ready to go. To test a connection, create or select it and click the Test Connection button (check mark).
Figure 12-3: The TSQLConnection component editor
When a connection is made using this technique, the connection information is embedded into the application; specifically it is added to the .xfm file where it is placed. The advantage to this is that the application does not rely on external files (like the dbxconnections file) to retrieve connection information. The disadvantage to this is that the application does not rely on external files to retrieve connection information! Following is the frmEmployee.xfm file with TSQLConnection component information.
270
Chapter 12: dbExpress and DataCLX object SQLCnctnEmployee: TSQLConnection ConnectionName = 'employee' DriverName = 'INTERBASE' GetDriverFunc = 'getSQLDriverINTERBASE' LibraryName = 'libsqlib.so.1' Params.Strings = ( 'DriverName=INTERBASE' 'BlobSize=-1' 'CommitRetain=True' 'Database=/opt/interbase/examples/employee.gdb' 'Interbase TransIsolation=ReadCommited' 'Password=masterkey' 'RoleName=RoleName' 'ServerCharSet=ASCII' 'SQLDialect=1' 'User_Name=sysdba' 'WaitOnLocks=True') VendorLib = 'libgds.so.0' Left = 88 Top = 56 end
Embedding connection information in the application is good from the perspective of deployment. The TSQLConnection component has a property called LoadParamsOnConnect (which has a default value of false). If this property is set to true, the application will load its connection parameters just before it needs them at run time. The application retrieves connection parameter information from the dbxConnections file. This provides a good deal of flexibility because the dbxConnections file can be edited externally if the database password or the location of the data changes. However, the application is now dependent upon the existence of a particular file (dbxConnections) that must be deployed along with the application. If LoadParamsOnConnect is false, the application embeds into the .xfm file all of the information necessary to connect to the database. However, since the connection information is “hard wired” into the application, it cannot be changed after the program is deployed. If the location of the data changes, the program must be redeployed. Another solution is to write an application-specific configuration file that contains the application’s configuration options (which may include but is not limited to the database connection information). Such information might be stored in an .ini file or another type of file. You may ask yourself, if dbExpress is configured by simply editing dbxconnections, and if I am going to have to write a configuration file for my application anyway, where is the advantage in creating an application-specific file? Answer: If you rely on the more general dbxconnections file, you are taking a risk that another application will corrupt the file or attempt to define a connection alias
Chapter 12: dbExpress and DataCLX
271
with the same name. If the information is stored in an application-specific file, this will not be a problem.
Retrieving Data with TSQLDataSets Now that a connection has been made to the database, the necessary data can be retrieved for use in the client application. dbExpress differs significantly from the Borland Database Engine in how it retrieves data for use in the client. Data is sent through the dbExpress driver to the application where it is typically stored in a data store called a SQLDataSet.
What are SQLDataSets? The dbExpress tab contains a number of components referred to as SQLDataSets. Included in this group are TSQLDataSet, TSQLTable, TSQLQuery, and TSQLStoredProc. These components represent local data sets that store data retrieved from a database (though TSQLStoredProc and TSQLQuery do not always return records). SQLDataSets differ from Delphi’s data set components in that they are unidirectional. SQLDataSets do not maintain an in-memory buffer of data and can only be navigated from top to bottom. Navigation methods include only First and Next. Calling Prior or Last on a unidirectional data set raises an EDatabaseError exception. In addition to their navigational constraints, SQLDataSets are non-updateable (read-only). This follows from the fact that they do not keep an in-memory copy of data and cannot keep track of changes made to multiple records.
Why Do We Need Them? SQLDataSets provide a significant performance advantage in retrieving data from the database. Because they are unidirectional and non-updatable, SQLDataSets do not require the retrieval and dynamic re-creation of database metadata. In other data access architectures (I won’t mention any names, but their initials are BDE!), data set components are responsible for updating data via the metadata they have obtained. Knowing the metadata allows data sets to send updates to the database. dbExpress DataSetProviders resolve updates to data via direct SQL calls and only use the SQLDataSets for retrieval of data. Because of their independence from additional overhead, SQLDataSets provide the ability to create extremely thin applications in situations where data updates are not needed. For example, if the purpose of data retrieval is only for publishing or reporting of data, it is much more efficient to use SQLDataSet. Suppose an application includes a form that displays a list of employee phone numbers. The numbers cannot be edited and are used for display, that is, they are simply retrieved from the database and “stuffed” into a list box.
272
Chapter 12: dbExpress and DataCLX
Figure 12-4
In such a case, it is not necessary for the data to be manipulated in a sophisticated way. Once the data is used for its initial purpose (to be published to the phone list), the application is done with it. SQLDataSets are a great choice for dealing with data in this way. The unidirectional, read-only data sets provide a remarkably efficient way to use data in the application, without introducing a large amount of overhead. Note: Comparing BDE Datasets and SQLDataSets SQLDataSets are significantly different from traditional BDE data sets, not only in data retrieval but in data manipulation. We have already mentioned that they are unidirectional and read-only. There are a number of other functionalities that SQLDataSets do not support. Most of them are related to the fact that SQLDataSets do not keep a cache or buffer of data in memory. Filters, bookmarks, and lookup fields are a few of the functions that cannot be performed by SQLDataSets, but are reserved for ClientDataSets. This is in keeping with the role that ClientDataSets are meant to play as the primary controller of client-side data. Manipulation of client-side data is covered in detail in the next chapter.
A Note about SQLDataSets’ Ancestry Looking into the DataCLX hierarchy, we see an unexpected twist in that SQLDataSets are derived from TDataSet. This is unexpected because TDataSets are bidirectional and buffer data. Object-oriented theory suggests that functionality should not be taken away as a hierarchy progresses but rather be added. There have been a number of discussions and opinions on this topic.
Chapter 12: dbExpress and DataCLX
273
Figure 12-5: The Kylix DataSet hierarchy
OOP purists might say that TSQLDataSets should be placed above the traditional data sets in the hierarchy, in keeping with OOP principles. Placing them above TDataSet would allow for a more natural object-oriented progression in that the functionality related to keeping a cache of data within the data set could be added on further down the family tree. Others would argue that changing TDataSet’s position in the CLX hierarchy would simply break too much existing code. Given the fact that porting Delphi and C++ applications to Linux is a top priority among a large percentage of the Kylix audience, this perspective eventually prevailed and TSQLDataSet was derived from TDataSet. So how did they do it? How did they take away functionality inherited from the base class? Most of the functionality the TSQLDataSet does not support generates an exception. The TDataSet class now contains a method called CheckBiDirectional, which is defined this way: procedure TDataSet.CheckBiDirectional; begin If IsUniDirectional then DatabaseError(SDataSetUniDirectional, Self); end;
IsUnidirectional is a public property of data sets. As the name suggests, it is a Boolean property that determines if the data set is unidirectional. Methods of TDataSet that perform the functionalities listed above call this method before they continue. If the data set is unidirectional, a DatabaseError is thrown and the procedure exits.
274
Chapter 12: dbExpress and DataCLX
Using SQLDataSets SQLDataSets come in four flavors: SQLDataSet, SQLTable, SQLQuery, and SQLStoredProc. The preferred method of using these components is to use the SQLDataSet. SQLTable, Query, and StoredProc are included primarily for backward compatibility. That is, they are used to migrate existing data applications to dbExpress. The first step to using SQLDataSets is to connect to the database. Using the SQLConnection property, select the SQLConnection object to use for your connection. Once a connection has been made, the next step is to select the use of the SQLDataSet. The CommandType property allows you to specify which type of data set you want to open. Set the CommandType property to ctQuery, ctTable, or ctStoredProc to select a retrieval technique. Once the CommandType has been set, you must set the CommandText property. Setting CommandText will vary, depending on the CommandType setting. If CommandType is set to ctTable or ctStoredProc, CommandText will present a drop-down list of tables or stored procedures for you to choose from. Stored procedure parameters can be set in the Params property. If CommandType is set to ctQuery, CommandText will provide a property editor for creating a SQL statement.
Figure 12-6: The SQLDataSet Command Text Editor
Chapter 12: dbExpress and DataCLX
275
Parameterized Queries Queries are often dependent upon the state of an application. The values of a query may not be able to be predetermined at design time, so it must fill in the particular parameters of the query at run time. To create a parameterized query, write a SQL statement that includes a parameter. Parameters are denoted by a preceding colon ( : ) as in the following example. Select * from employee where dept_no = :param1
The param1 parameter must have its value supplied at run time. To supply a value for the parameter, either set the correct element of the params array (of course, then you must know the index of the parameter) or call the ParamByName method. Using parameterized queries, developers can facilitate a master/detail relationship. Master/detail queries are discussed further in the next section. sqldtstDeptEmployees.Close; sqldtstDeptEmployees.ParambyName('param1').value := 630; sqldtstDeptEmployees.Open;
Tip: When parameters are re-bound at run time, it is a good idea to use the DisableControls and EnableControls methods. Because the data set must be closed in order to reset the parameter value, the application may experience a slight flicker; that is, the data will momentarily disappear. DisableControls temporarily (until EnableControls is called) disrupts the data-aware controls from receiving data updates from their data source. Both ClientDataSets and SQLDataSets contain these methods. sqldtstDeptEmployees.DisableControls; try sqldtstDeptEmployees.Close; sqldtstDeptEmployees.ParambyName('param1').value := 630; sqldtstDeptEmployees.Open; finally sqldtstDeptEmployees.EnableControls; end;
Master/Detail Relationships Applications often make use of existing database relationships. A one-to-many relationship is often called a master/detail relationship. Both SQLDatasets and ClientDataSets can maintain a master/detail relationship between two data sets, making sure that a change to the position of the record pointer in the master data set is automatically reflected in the detail data set. The selection of where the relationship is maintained is an important one for defining the scope of the relationship.
Chapter 12: dbExpress and DataCLX
Tip: It is important to note that master/detail relationships are always facilitated by the detail data set. Nothing has to be done to a data set to make it a master. The detail data set decides to “follow” the master. Master/Detail with ClientDataSets
The master/detail relationship is commonly established at the ClientDataSet level. This provides a more localized relationship in cases where the relationship does not make sense in every view of the data. Creating master/detail relationships with ClientDataSets is discussed in Chapter 13, “Client-Side Data Management.” Master/Detail with SQLDataSets
AM FL Y
Creating a master/detail relationship in SQLDataSets is actually very similar to doing so in ClientDataSets. The procedure, however, for setting up such a relationship is somewhat different. SQLDataSets include a property called DataSource. The DataSource property of data sets can be confusing because the DataSource property is used for a totally different purpose in data-aware controls. In SQLDataSets, the DataSource property is used to determine the data set (via its data source) that the detail table will use as its master. In other words, the DataSource property of the SQLDataSet serves exactly the same purpose as the MasterSource property of the ClientDataSet! The other element of master/detail relationships in SQLDataSets involves the SQL statement used in the detail table. Master/detail data sets use a parameterized query in the detail data set to determine what field(s) the tables will be joined on. Using the department example from the “Parameterized Queries” section, the SQL property must be changed to the following.
TE
276
Select * from employee where dept_no = :dept_no
In this case, the name of the parameter has been changed to the name of the field on which the tables should be joined. This is how the joining of the tables is declared. When the detail query is opened, it determines if it should follow a data set (by determining if its DataSource property has been set). If so, the detail data set looks to see if the master data set contains a field that has the same name as the parameter of the detail query. If it does, the value from the master data set is automatically inserted as the value of the detail query’s parameter. Each time the value changes in the master (perhaps because the record pointer moves), the detail parameter is re-inserted and re-executed.
Chapter 12: dbExpress and DataCLX
277
Executing SQL Commands Queries are not always used to retrieve resultsets. A query can execute other SQL commands such as INSERT, UPDATE, and DELETE. When a query executes a command that does not return a resultset, use the ExecSQL method. ExecSQL takes one parameter that determines if the query is to be prepared before it is executed. Set the ExecDirect parameter to true if the query does not take any parameters.
Summary The dbExpress data access layer is a thinner, faster, and more easily configurable and deployable mechanism for creating efficient and flexible connections to a data store. It is a huge leap over the functionality of the Borland Database Engine and will help to propel both Delphi and Kylix to new heights of application development. Overall, the Kylix data architecture is a giant step in the evolution of Borland’s robust and well-respected suite of desktop development tools, and surely makes both Delphi and Kylix even more effective in creating smaller, faster, and better database applications.
Client-Side Data Chapter 13 Management Introduction In the last chapter, we looked at how to retrieve data from the database, with the thin and powerful dbExpress architecture and the DataCLX components that use it. Using SQLDataSets, data can be retrieved and sent to the application quickly and with very little overhead. SQLDataSets, though, have limited functionality to manipulate data in a sophisticated way. Every application has specific needs with regard to the display, manipulation, and resolution of data. In this chapter, we focus on the organization of data in the client and on the manipulation and resolution of data with the TDataSetProvider and the TClientDataSet components.
Organizing Client-Side Data Most production-level applications pass around a great deal of data. Data for customers, orders, employees, inventory, and a countless list of other things are common types of data that must be managed and organized for use by the program. But where do they go? How does a developer make decisions about where to place the resultsets that hold application data? One solution is to place data sets, stored procedures, and other data elements on the forms or in the units in which they are used. In many cases, this would work just fine. But what about shared data? For example, imagine a customer form that shows a grid of a company’s current customers and their information. From this form, the user can pop up a modal search dialog to locate a specific customer. Although the display of data differs, the search dialog can use the same data set as the main customer form (since this is much more efficient than querying the database a second time for the same information). If the customer data set is placed on the main customer form, the search dialog would have to use the main customer form and retrieve its data from the main form class (because the data set would be a property of the form class). Although they are related, it is not the main customer form’s job to supply data to the search dialog. The customer form’s job is to display customer data. In addition to questions of
279
280
Chapter 13: Client-Side Data Management
encapsulation, keep in mind that data sets have a number of methods and events that are used to manipulate data, set appropriate behaviors, and provide constraints on data. A good amount of data management code is commonly written in and around the events of data access components. The customer user interface module (the form) should not be responsible for managing customer data, only for displaying it to the user. Data management is a separate responsibility that should be handled by another module. In Kylix, data is managed by components called data modules.
Data Modules A data module is a non-visual object that holds and manages a collection of other non-visual components. While you are free to place any non-visual component on a data module (such as an image list or a timer), it is typically used to hold data access components, such as TSQLConnections or TClientDataSets. Unlike some other tools, the Kylix designer uses a separate window for each data module. The developer is able to drag and drop components directly into any data module window.
Why Data Modules? Many people ask the question, why data modules? Why not just drop data access components onto the form that is using the data? The answer to these questions is threefold. First, suppose that you are developing an employee maintenance screen for a human resources application. You have placed the necessary data access components on the form that retrieves the data needed. Now you must create an employee search dialog that accesses the same data. Since the search screen is launched as a modal dialog, the employee data set will not be changed during the life of the search screen. Because the data has already been retrieved by the application, it would be wasteful to create a new employee data set just for the search screen. The search dialog and the employee screen will simply use the same data set. Since the employee data set is on the main screen, the dialog would be forced to use the main screen. Encapsulation rules dictate that it is not the employee screen’s job to supply data to the search dialog. Remember that encapsulation deals in a large part with the “single-mindedness” of an object. It enforces the idea that each object in the system has a natural set of responsibilities defined by its role in the system. The second benefit to using data modules involves data management. Data modules are a great place to implement data-specific business rules, validations, updates, etc. If data access components are placed on the form, all of the data management code will be mixed in with the form management code. Again, the form unit’s job is to hold the code that is necessary to manipulate and display user interface items. Its job is not to hold data management code.
Chapter 13: Client-Side Data Management
281
Finally, because data modules are often used to create client-side business objects, they are likely candidates for inheritance and polymorphism. An employee business object may be used differently by a human resources manager than it would be by a payroll manager. Such an object might look like this: TEmployeeBusinessObject = class(TDataModule) ClientDataSetEmployee: TClientDataSet; DataSetProviderEmployee: TDataSetProvider; procedure GetEmployeeData;abstract; function GetEmployeeList:TList;abstract; function GetEmployee(Emp_ID:Integer):TEmployee;abstract; procedure SetEmployeeStatus(Emp_ID:Integer; Status:TEmpStatus);abstract; end; THrEmployee = class(TEmployeeBusinessObject) { overridden methods here. } TPayrollEmployee = class(TEmployeeBusinessObject) { overridden methods here. }
Embedding the employee object’s responsibilities in the employee form makes it extremely difficult to efficiently reuse the employee object in an object-oriented hierarchy. So instead of dropping data access components directly onto the employee screen, they should be placed in the employee data module and should be used by both the employee screen and the search dialog.
Figure 13-1: Developing a data module strategy
282
Chapter 13: Client-Side Data Management
Using data modules allows the developer to (among other things) embed data-related business logic and other rules into the application without constraining them to a particular GUI element. This allows business rules to be reused throughout the application and provides a more flexible and less redundant set of application constraints, validations, and other rules.
Displaying Application Data Displaying data in a Kylix application requires the use of a number of components. Each element of the group is responsible for laying another piece of the path from the application to the database. The last chapter discussed the role of the TSQLConnection and TSQLDataSet components as well as the basics of TDataSetProvider and TClientDataSet. In this section, we examine these two components in detail as well as describing the remaining pieces of the puzzle: the TDataSource and the Kylix data-aware controls.
The Data Access Tab The Data Access tab includes three components: TDataSource, TClientDataSet, and TDataSetProvider. The latter two are discussed at length later in this chapter. The TDataSource component provides a connection between a data set and a group of data-aware controls. It centralizes control of display, navigation, and editing for a group of controls that are connected to a single data set. To help us understand the advantage of the TDataSource component, imagine that data-aware controls (also called dbcontrols) are directly connected to a data set with no datasource between them (this of course cannot be done). The form on which the controls are placed has two queries Figure 13-2 to the same table, one in the active database and one in the archive database. Because the fields in the archive table are the same, the same controls can be used to view both sets of data. Unfortunately, the developer has to manually change the data set for each control that displays its data. By using a datasource component the controls can be connected to the data set via a middle man. The datasource then is connected to the desired data set. Under this scenario, if the query that is being viewed changes, the datasource simply changes what data set it points to. The dbcontrols simply display what the datasource tells them to!
Chapter 13: Client-Side Data Management
283
The Data Controls Tab Components on the Data Controls tab are responsible for displaying and/or manipulating data provided from local data storage such as a TClientDataSet or a TSQLDataSet. Data-aware controls come in two flavors: those that display an entire data set and those that display a particular field in a data set. (To be completely accurate, not all data-aware components display data. The DBNavigator, for instance, does not display data, but it does manipulate the data set directly.) They connect to data via a TDataSource component that notifies the DataCLX component of any changes in the data set. The use of data-aware controls is one of the things that separates Kylix from a tool like Visual Basic (plus the ability to write cross-platform applications, support for inheritance, and a bunch of other stuff). Unlike Kylix, VB does not distinguish between data-aware and non-data-aware controls. For example, the VB text box acts very similarly Figure 13-3: Using dbcontrols to the Kylix TEdit component. However, it can be used independently or can be connected to a field of a data set. While this at first seems more convenient, it actually hurts the application. Components that connect to data require the use of a number of other properties and internal objects. These extra objects make the components in which they reside larger than non-data-aware components. This makes the application larger in general. Kylix uses data-aware controls only when they are necessary, that is, when the control must display data directly from the database. Otherwise, the control’s non-data-aware counterpart is used, creating a leaner application. Using data-aware controls is almost identical to using their non-data-aware counterparts. TDBEdit has most of the same properties, methods, and events that TEdit does. However, data-aware controls have one or two extra properties in comparison to their non-data-aware siblings. Dbcontrols that manipulate an entire data set (like DBGrids and DBNavigators) include a DataSource property that tells the component which data set it is connected to (remember that the datasource is connected to a specific data set). If the component (e.g., DBEdit or DBText) displays a particular field value, set the DataField property to select which field from the data set will be displayed by the control.
284
Chapter 13: Client-Side Data Management
Using Client DataSets As the last chapter discussed, applications that retrieve data simply for publishing or reporting purposes can use SQLDataSets. In these situations, data does not need to be updated or managed in a sophisticated way. Data is simply brought to the application, cycled through once, and then cannot be cycled through again without requerying the database. However, a large majority of applications need not only to retrieve data, but to process it in a complex way that includes updating the database. In such an application, ClientDataSets are the primary data storage component. ClientDataSets maintain an in-memory cache of data as well as a record of all updates that need to be applied (since the last time the database has been updated). To compare to Delphi’s BDE, ClientDataSets behave like TTables and TQueries that have the CachedUpdates property set to true. The difference is that ClientDataSets always use cached updates. That is, as edits are posted, they are actually posted to the local, in-memory copy of data that the ClientDataSet maintains. When the application is ready to make changes permanent, the entire cache is applied to the database at one time. Subsequent edits begin to fill the cache again. Using cached updates has a positive effect on the overall performance of an application in a networked, multiuser environment. Note: Although this chapter talks about the use of ClientDataSets and Providers in the client, nothing stops an application from using them in the middle tier of a multitiered application. The application server layer might very well use ClientDataSets in its processing and providing of application data. Also, DataSetProviders may be located in a middle layer, while ClientDataSets are used in the client. However, in order for a ClientDataSet to connect to a DataSetProvider through the Kylix IDE, they must have the same owner. If a ClientDataSet needs to connect to a DataSetProvider that has a different owner (for instance, if the provider is on another data module), the connection must be made via the ClientDataSet’s SetProvider method. The Object Inspector will not list the provider in the ClientDataSet’s ProviderName property, even if the ClientDataSet’s unit is using the provider’s unit. ClientDataSets store data in a variant property named Data. The Data property represents the ClientDataSet’s in-memory copy of data from the provider. As edits are made to the data cache, those edits are stored in another variant property called Delta. The Delta property includes all inserts, modifications, and deletions of local data. Figure 13-4 shows a visual representation of the delta packet.
Chapter 13: Client-Side Data Management
285
Figure 13-4: The Data and Delta properties of ClientDataSets
In the figure, the dbgrid on top of the form shows the ClientDataSet’s Data. Beneath it, a second ClientDataSet shows the first ClientDataSet’s Delta property. (Because Data and Delta are both Variants, one man’s Data can be another man’s Delta!) Notice that for each record that has been changed and posted to the primary data set, the delta contains two records, one with the original values and one with the new values of the fields that have been changed. In the case of Leslie Jones, two different changes were made to the same record (last_name and hire_date). To permanently apply changes to the local data only, call the MergeChangeLog method. MergeChangeLog places the changes stored in Delta into the Data property. Thus, those changes cannot be undone. After MergeChangeLog has been called, the Delta property is empty. Warning: Do not call MergeChangeLog when a ClientDataSet is connected to a provider. The ApplyUpdates method (see the “Resolving Data Updates” section) calls MergeChangeLog under the covers.
Retrieving SQL Data DataSetProviders retrieve their data from any TSQLDataSet component or from any ClientDataSet. Remember that SQLDataSets are stateless. When a provider asks a SQLDataSet for data, the SQLDataSet opens, grabs the data, passes it on to the ClientDataSet (via the provider), and closes. So even though the application is seeing data, the SQLDataSet may not (and usually should not) be open. The DataSetProvider can augment the data before it is sent to the ClientDataSet by using the OnGetData event. OnGetData is fired whenever the ClientDataSet requests data from the provider. For example, an application provides an employee list to a ClientDataSet. The SQLDataSet that is used in the employee data module retrieves information for all employees. However, the ClientDataSet that is used on this form should not display information about the salaries of employees at the vice
286
Chapter 13: Client-Side Data Management
president level. To accomplish this, the ClientDataSet could set a filter on its information. It makes more sense though to only provide information to the form that it is able to use. In this case, the provider encrypts the data before it is even sent to the ClientDataSet (of course, the data would have to be unencrypted when ApplyUpdates is called but before the data is sent to the database).
Retrieving XML Data
AM FL Y
procedure TdtmdlEmployee.dtstprvdrEmployeeViewGetData(Sender: TObject; DataSet: TCustomClientDataSet); var Salary:double; begin while not DataSet.Eof do begin if DataSet.FieldByName('JOB_CODE').AsString = 'VP' then begin Salary := DataSet.FieldByName(‘Salary’).Value; DataSet.FieldByName(‘Salary’).Value :=EncryptValue(Salary); end; DataSet.Next; end; end;
TE
In addition to dealing with traditional database server data, ClientDataSets can load, manipulate, and update XML data. At design time, XML data can be loaded by right-clicking the ClientDataSet and selecting Load from MyBase table. MyBase is an XML database that follows a specified schema and format. To load XML data programmatically, use the LoadFromFile method. LoadFromFile takes one parameter naming the file from which to load the data. If the XML is passed in directly as a string instead of as a file, set the xmlData property of the ClientDataSet. Note: Kylix 2 Update In Kylix 1.0, ClientDataSets cannot load any XML file. Only those that follow a certain format can be imported into the ClientDataSet. Kylix 2 also has this limitation. However, Kylix 2 includes a tool called the XMLMapper that allows developers to create mappings between any xml file and a data packet format that is understood by ClientDataSets. This makes the use of XML in ClientDataSets much more widely open and extensible. For more on this, see Appendix B, “Using the XML Mapper” on the companion CD.
Chapter 13: Client-Side Data Management
287
Note: MyBase MyBase is an XML database that can be used to load and move data within an application. Because it is pure XML, MyBase does not create the connectivity and configuration hassles that can come with SQL database deployment within applications. It is a great way to store data locally or to send it across the enterprise.
Manipulating Client-Side Data Master/Detail Revisited At the ClientDataSet level, master/detail relationships are created through the use of two properties: MasterSource and MasterFields. To create the department/ employee relationship, begin by setting the MasterSource of the detail data set (in this case, clntdtstDeptEmployees) to the datasource component that points to the master data set (in this case, dtsrcDepartments). The MasterSource property determines which data set the detail data set will follow. Setting this property is analogous to setting the DataSource property of a SQLDataSet. The second property that must be set is the MasterFields property. This property cannot be set until the MasterSource property is set. MasterFields uses a property editor called the Field Link Designer, as shown in Figure 13-5. The Field Link Designer is used to create a SQL join between the two tables. This setting is used to help create the underlying SQL statement that retrieves the data. With SQLDataSets, this is accomplished through correct naming of the SQL parameter. For more information on creating master/detail relationships with SQLDataSets, see Chapter 12. The Field Link Designer creates a SQL join on two data sets. Two list boxes show a list of fields from each of the tables. Add a join by selecting a field from each list and clicking the Add button, located between the two lists. An indication of the relationship is listed in the Joined Fields box at the bottom of the dialog. Note that it is not necessary for the two fields to have the same name (though this is common), only that they are of the same type. Once the Field Link Designer has joined the two tables, the master/detail relationship is complete. The relationship that is created by the MasterSource and MasterFields properties is automatically maintained by the ClientDataSets. Whenever the record pointer changes position in the master table, the detail table is automatically closed, given a new parameter, and re-executed.
288
Chapter 13: Client-Side Data Management
Figure 13-5: The Field Link Designer
Working with DataSet Indexes Indexing data in an application is an important step in providing fast access to the data that a user wants. ClientDataSets provide for custom indexing with the use of a few properties. To create a custom index, use the IndexFieldNames property. Create a client-side index by specifying a list of field names, separated by commas. For instance, to create an alphabetical list of employee names ordered by last name, use the string Last_Name;First_Name in the IndexFieldNames property. Multiple indexes can be created through the IndexDef property. IndexDef contains a collection of TIndexDef objects. Each IndexDef contains a number of properties that help to specify the parameters of the index. The Fields property is used in the same way as the IndexFieldNames property of the ClientDataSet itself. In addition, the Options property specifies index options. Figure 13-6 shows an index based on the employee job code. The index is descending and case insensitive. To use an index in the client data set, select it from a list of indexes in the IndexName property. (This is a good reason for you to name your indexes!) The IndexName property and the IndexFieldNames property are mutually exclusive. Setting one clears the other.
Figure 13-6: Creating a client-side index
Chapter 13: Client-Side Data Management
289
Navigation Client data sets are commonly navigated through the user of the DBNavigator component. They can, however, be manipulated programmatically through the use of a collection of common methods, defined in the TDataSet class. TClientDataSet (which is a descendant of TDataSet) is programmatically navigated through the use of the First, Prior, Next, Last, and MoveBy methods. Note: Remember that Kylix uses the ClientDataSet as the primary navigation component. These methods do not all work in the TSQLDataSet components. For example, if you attempt to call the Prior method of a SQLDataSet, a database exception is raised. unit FormDataAccessMain; interface uses SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs, DB, SQLExpr, FMTBcd, Provider, DBClient, QextCtrls, QDBCtrls, Qgrids, QDBGrids, DBXpress, QcomCtrls; type TfrmCustomer = class(TForm) dbgrdEmployee: TDBGrid; dtsrcEmployee: TDataSource; clntdtstEmployee: TClientDataSet; dtstprvdrEmployee: TDataSetProvider; tlbrMain: Ttoolbar; tlbtnFirst: TToolButton; tlbtnPrior: TToolButton; tlbtnNext: TToolButton; tlbtnLast: TToolButton; tlbtnPageUp: TToolButton; tlbtnPageDown: TToolButton; procedure tlbtnPageUpClick(Sender: TObject); procedure tlbtnPageDownClick(Sender: TObject); private { Private declarations } public { Public declarations } end; const PAGE_DOWN = 8;
290
Chapter 13: Client-Side Data Management PAGE_UP = -8; var frmCustomer: TfrmCustomer; implementation uses DataModuleEmployee; {$R *.xfm} procedure TfrmCustomer.tlbtnFirstDownClick(Sender: TObject); begin // This method is used as the event handler for the four // navigation buttons. case (Sender as TButton).Tag of 0 : clntdtstCustomer.First; 1 : clntdtstCustomer.Prior; 2 : clntdtstCustomer.Next; 3 : clntdtstCustomer.Last; end; end; procedure TfrmCustomer.tlbtnPageDownClick(Sender: TObject); begin // This method is used as the event handler for the page up // and page down buttons. case (Sender as TButton).Tag of 4 : clntdtstCustomer.MoveBy(PAGE_UP); 5 : clntdtstCustomer.MoveBy(PAGE_DOWN); end; end; end.
Filtering Data from the server often must be filtered on the client. ClientDataSets provide for filtering through the Filter and Filtered properties. Filter is a string property that has a similar syntax to the Where clause of a SQL statement. The following table shows the valid operators for use in a filter string.
Chapter 13: Client-Side Data Management
291
Table 13-1 Operator
Description
, =
Less than, greater than, less than or equal to, greater than or equal to Equal to, not equal to Tests that expressions are true or false Tests if a value is null Performs arithmetic operations on numerical values (+ and – also can be used with date/time values) Changes the case of a string value Trims extra spaces from string values Returns a substring of a string starting at a particular index Returns a particular element from a date/time value Returns a particular element from a time value Returns the current date or time from a date/time value Searches for a string pattern in another string Tests for a value’s inclusion in a set Wildcard character. Allows for partial matches in comparison
=, AND, NOT, OR IS NULL, IS NOT NULL +, -, *, / Upper, Lower Trim, TrimLeft, TrimRight, Substring Year, Month, Day Hour, Minute, Second Date, Time Like In *
Using these operators, developers can specify complex filters for a particular data set. To apply a filter at run time, set the Filtered property to true. The Filtered property can also be used to determine if the current filter is applied. To keep users from seeing information on company vice presidents, set the Filter property to: JOB_CODE ‘VP’
Once the Filtered property is set to true, the filter will take effect. Tip: Keep in mind that since the Filter property is used like the Where clause in an SQL statement, the values of some data types may need to be surrounded by quotes in order to be a part of a valid filter string.
Searching Searching for data in a client data set is done in one of two ways: using the FindKey and FindNearest method or using the Locate method.
FindKey/FindNearest Methods FindKey and FindNearest allow the user to specify a comma-delimited list of values that identify a record. The most common use is to supply the value of the primary key of the data set. If the record is found, the cursor is placed on that record. If not, the cursor remains at its current position and the function returns false. The primary difference between FindKey and FindNearest is that FindKey searches for perfect matches, while FindNearest searches for the closest match.
292
Chapter 13: Client-Side Data Management procedure TfrmEmployee.edtJobCodeSearchChange(Sender: TObject); begin clntdtstEmployee.IndexFieldNames := ‘Job_Code’; if not clntdtstEmployee.FindNearest([edtSearch.Text]) then raise EjobNotFound.Create(‘That job code was not found.’); end;
In this example, the FindNearest method is used to provide incremental searching. Because the function is used in the Edit control’s OnChange event, it is called each time the user types (or deletes) a character in the control. Tip: The FindKey and FindNearest methods automatically search on the current index. To search on a particular field, the index must be explicitly set as in the example above. The Locate method (see the next section) allows the user to specify the search field as well as the search value and is the more common way to search for data values.
Locate Method The Locate method is a much smoother way of finding result-set data. Unlike the FindKey and FindNearest methods, Locate allows the developer to specify not only what value to find but also the fields in which to find it. Locate is a Boolean method that returns a success code upon completion. If the record is found, the record pointer is moved to that location and Locate returns true. If the record is not found, the record pointer stays at its current location and Locate returns false. Locate takes three parameters: the name of the search field, the value to be searched for, and a set of search options (TLocateOptions). procedure TfrmEmployee.spdbtnNameFindClick(Sender:TObject); var SearchValue:Variant; SearchFields:String; begin // this method is used as the event handler for both the name and // ID search buttons. SearchValue := edtFind.Text; case (Sender as TspeedButton).Tag of 0 : SearchFields := ‘Last_Name’; 1 : SearchFields := ‘Emp_No’; end; // case statement. if not clntdtstEmployee.Locate(SearchFields, SearchValue,[loCaseInsensitive,loPartialKey]) then MessageDlg(‘Employee not found’,’The selected employee could not be found.’,mtError,[mbOK],0); end;
Chapter 13: Client-Side Data Management
293
The code above shows how an employee search is executed, either by last name or by employee ID. The variable SearchFields specifies what field in the resultset should be searched. Notice that the name of the variable is SearchFields and not simply SearchField. This is because the KeyFields parameter of the Locate method can take a semicolon-delimited list of fields on which to search. This is especially helpful if a search is performed on a multiple field index. To pass in multiple values to be searched for, you must create a variant array of values. For example, to search for all employees named “Smith” in a given department, the Locate method is used as follows: if not clntdtstEmployee.Locate(‘Last_Name;Dept_No’, VarArrayOf([‘Smith’,600]),SearchValue,[loCaseInsensitive, loPartialKey]) then MessageDlg(‘Employee not found’,’The selected employee could not be found.’,mtError,[mbOK],0);
The third parameter of the Locate method is the LocateOptions parameter. LocateOptions is a set of TLocateOption values, of which there are two. loCaseInsensitive causes a search to be case insensitive. loPartialKey allows for partial matches of a search. It behaves in the same way as the FindNearest method. So by default searches are case-sensitive, exact matches. Developers must include one or both of these options to alter the searching behavior.
Editing Data Most of the time, data is retrieved from the database for editing purposes. The ClientDataSet that holds the data on the client side is always in one of several states (one of which is dsEdit). A read-only property called State can be checked at any time to determine the state of the data set. Although there are a number of states, the three most commonly used by the developer are dsBrowse, dsEdit, and dsInsert. When a data set posts or cancels edits or an insert, the table is placed into browse mode. While edits are being made or a record is being inserted, the data set is in dsEdit or dsInsert respectively. (For more on the State property of data sets, see the Kylix help files.) The data-aware controls in Kylix often control the changing of a data set’s state. The DataSource component has a property called AutoEdit. When AutoEdit is set to true (which is the default), any changes to data-aware controls automatically place the table in a state of dsEdit (if it is not already in that state). This alleviates the developer from having to manually switch the state of the table through its methods (insert, edit, etc.). In some applications, the user is required to explicitly place the data set into edit mode via a keystroke or a button click. To programmatically change the state of the table, use the Insert, Append, or Edit methods. Once a data set has been placed in the dsInsert or dsEdit state, the developer calls the Post or Cancel method to return the data set to browse mode.
294
Chapter 13: Client-Side Data Management
Tip: If a record must be added behind the scenes (without the user explicitly adding values), the InsertRecord method can be used. The InsertRecord method takes an array parameter that represents the values of the new record. The record is inserted to the data set and a post is executed. procedure InsertRecord(const Values: array of const);
Resolving Data Updates Cached Updates When edits are posted to a ClientDataSet, the changes are placed into a local cache on the client. The Delta property of the ClientDataSet keeps track of each change to the data. To permanently apply changes to a ClientDataSet’s cache, call the ApplyUpdates method. ApplyUpdates attempts to save the entire contents of the cache to the database. It takes one parameter, MaxErrors, that determines how many errors the update allows before stopping the update process and returns the number of errors that occurred. To allow an infinite number of errors during an update, set the MaxErrors parameter to –1. Note: Cached updates are to data sets what transactions are to databases. They treat a collection of statements as a logical unit; either all succeed or all fail. The difference is that cached updates apply to statements made against a single data set and not the entire database. Unlike the TTable and TQuery components of the BDE, ClientDataSets always use cached updates. Once changes have been made to the ClientDataSet, they can be canceled with the use of two methods. CancelUpdates discards all local updates to application data. Once CancelUpdates has been called, the Delta property of the ClientDataSet is empty. To cancel changes to one record, call the RevertRecord method. RevertRecord only affects changes that still exist in the change log. Once a record has been applied by a provider or merged into the ClientDataSet’s Data property, RevertRecord cannot affect it.
Applying Multiple Caches When an application edits a collection of tables simultaneously (for instance, if the user can make edits to two different tables from the same form in the GUI), multiple caches are updated. If the caches must be applied as a logical unit, a transaction is required. In the following example, a salesperson returning from a road trip enters a collection of new customers and their sales records. It is easier for the user to enter all the customers at one time and then enter all of the sales. When the updates are applied, it is important that both all of the customers and all of the sales
Chapter 13: Client-Side Data Management
295
are successful. If the customer updates succeed and not the sales, then there will be inactive customers in the database. If the sales succeed and not the customers, the sales will be missing required references. To apply multiple caches as a logical unit, call the ApplyUpdates method of each cache within a single transaction. procedure TfrmSalesEntry.ApplyChanges; begin SqlcnctnMain.StartTransaction(Trans); try clntdtstCustomers.ApplyUpdates(0); clntdtstSales.ApplyUpdates(0); sqlcnctnMain.Commit(Trans); except on EDatabaseException do sqlcnctnMain.Rollback(Trans); end; // try..finally. end;
Note: In the BDE, the TDatabase component includes an ApplyUpdates method that takes a collection of data sets whose cached must be applied. The database’s ApplyUpdates method applied all of the listed caches in a transaction. The SQLConnection component does not include such a method. An explicit transaction is required.
Passing Update Parameters During the data resolution process, Kylix allows ClientDataSets and providers to pass custom information to each other regarding their interaction with data. The data set provider and the client data set generate events for Before/After GetParams, GetRecords, RowRequest, ApplyUpdates, and Execute. (The data set provider also generates an event for AfterUpdateRecords). Each of the event handlers for these methods specifies an OwnerData parameter. OwnerData is an OleVariant and can hold a variety of data types. Using OwnerData, developers can pass custom information from one component to the other and back again during data operations. For instance, a ClientDataSet may want to pass the ID of its current user along with the updates to assist the DataSetProvider in determining how updates should be applied. Once the updates have been applied, the DataSetProvider may want to notify the ClientDataSet of some piece of custom information such as the number of rows affected by the operation. procedure TForm1.ClientDataSet1BeforeApplyUpdates( Sender: TObject; var OwnerData: OleVariant); begin OwnerData := GetCurrentUserId; end;
296
Chapter 13: Client-Side Data Management procedure TForm1.DataSetProvider1BeforeApplyUpdates( Sender: TObject; var OwnerData: OleVariant); var RowsAffected:Integer; begin if OwnerData = ‘ewhipple’ then {… implement user specific logic …} OwnerData := RowsAffected; end; procedure TForm1.DataSetProvider1AfterApplyUpdates( Sender: TObject; var OwnerData: OleVariant); begin MessageDlg(OwnerData + ‘rows affected’,mtInformation,[mbOk],0); end;
Dealing with Concurrency During Updates
Table 13-2
TE
AM FL Y
In multiuser environments, concurrency is a major issue and must be addressed. When two users (for instance, user A and user B) grab the same data and then both try to apply the updates, who wins? If user A grabs the data and posts changes to the local cache but does not apply the updates, then the database values have not changed. If user B grabs the same records after user A but commits the changes to the database before user A does, then what happens when user A tries to commit? The DataSetProvider includes a property called UpdateMode to help deal with this problem. The UpdateMode can be set to one of three values:
UpdateMode Setting
Description
upWhereAll
The upWhereAll setting is the most restrictive of the three. Under this setting, dbExpress uses every field in the data set when locating a record to apply updates to. If any of the fields have been changed by another user (that is, if the values have changed since this user retrieved them), an OnUpdateError event is thrown and the update is rejected. upWhereChanged checks only the fields that the current data set is applying when locating records. As long as the user doesn’t try to change any field that had been changed by someone else, the update succeeds. Suppose that an HR manager and a project manager both are editing the same employee record. They are likely to be editing totally different fields in the employee’s record. Since the edited fields do not conflict, both updates are successful.
upWhereChanged
Chapter 13: Client-Side Data Management
297
UpdateMode Setting
Description
upWhereKeyOnly
This is the least restrictive of the UpdateMode settings. Only the primary key of the data set must be the same as it was when originally retrieved. Since most applications don’t allow the user to edit primary keys, any edits to the record will succeed (so long as the record has not been deleted). upWhereKeyOnly generally supports the “Last in wins” methodology.
Once UpdateMode is set, the concurrency resolution process is handled for the developer. If the updates fail as dictated by the UpdateMode setting, an exception is generated.
Figure 13-7: UpdateMode resolution failure
When updates are applied to a data set, the developer may want more granular control over how updates are applied. Each field object contains a set of provider flags that help to determine how that field is used during an update. ProviderFlags is a set property that can contain any of the following values: Table 13-3 Provider Flag
Description
pfInWhere
Fields that include this setting appear in the Where clause of Insert, Update, and Delete statements if the UpdateMode is set to upWhereAll or upWhereChanged. The field appears in the update clause of internally created Update statements. This can be important for efficient updates. If a field is never going to be updated by a data set (due to application constraints), it is unnecessary and inefficient to pass it back to the database. Fields that include this setting appear in the Where clause of Insert, Update, and Delete statements if the UpdateMode is set to upWhereKeyOnly. When the field is provided to the client, this value is included in the record to ensure uniqueness but is not visible to the client.
pfUpdate
pfKey
pfHidden
298
Chapter 13: Client-Side Data Management
Resolution Error Handling Resolution errors, errors that occur when changes to a data set are applied, can occur for a number of reasons. Even if updates do not conflict with database constraints, the application may implement business rules that affect the ability to update data. Both the ClientDataSet and the DataSetProvider include a number of events that help to provide error handling for data updates.
Handling Errors with ClientDataSets The ClientDataSet includes the OnDeleteError, OnPostError, OnEditError, and OnReconcileError events that allow the application to respond to update errors that occur in those conditions. Suppose that an application allows only for the editing and insertion of customers. Customers cannot be deleted from this part of the application (this is accomplished by setting the DataSetProvider’s poDisableDeletes option to true). When the ClientDataSet attempts to delete a record, an EDatabaseError is generated, causing the OnDeleteError event to be called. procedure TForm1.ClientDataSet1DeleteError(DataSet: TDataSet; E: EDatabaseError; var Action: TDataAction); begin MessageDlg(‘You are not authorized to delete ‘+ ‘records.’,mtError,[mbOk],0); Action := daAbort; end;
The OnDeleteError event takes as parameters the data set in which the error occurred, the exception object, and an Action parameter which is passed by reference. The Action parameter can be set to daFail, daRetry, and daAbort. daFail aborts the update (in this case, a deletion) and displays an error message. daAbort also aborts the update but does not display a message. This is a common setting to use if the developer wants to use an application-specific message instead of the default message sent with the exception. Finally, daRetry attempts the same update that caused the current error. Note: Be very careful when using the daRetry option. If the error condition is not corrected during the error handler, the daRetry option will cause a neve ending loop of update exceptions.
Chapter 13: Client-Side Data Management
299
The OnDeleteError, OnEditError, and OnPostError handlers deal with each error in a similar manner as described above. Notice that these errors are generated at the time of the operation and not upon the application of updates. In other words, the ClientDataSet is aware of the options set by the DataSetProvider (such as poDisableDeletes). When a restricted operation is attempted, the ClientDataSet generates an error when the changes is posted and does not wait for the ApplyUpdates method to be called. The other update error handler of the ClientDataSet is the OnReconcileError event. OnReconcileError occurs when the ClientDataSet needs to resolve a record that could not be applied during an update. In the following example, the update failure comes as a result of application logic, placed in the DataSetProvider’s BeforeUpdateRecord event, preventing the application from updating the records of employees with a job code of “VP.” procedure TForm1.DataSetProvider1BeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean); begin if DeltaDS.FieldByName(Job_Code').OldValue = ‘VP’ then raise EDatabaseError.Create(‘VP records cannot be modified.'); end;
When this event raises an exception, it is propagated back to the ClientDataSet that attempted the update. The ClientDataSet then generates an OnReconcileError event. OnReconcileError is generated for each record that fails during an update. This event can be used to validate values, log errors, rollback transactions, and a host of other operations. procedure TForm1.ClientDataSet1ReconcileError( DataSet: TCustomClientDataSet; E: EReconcileError; UpdateKind: TUpdateKind; var Action: TReconcileAction); begin LogUpdateRejection; Action := raCancel; end;
OnReconcileError includes a number of parameters including the data set, the error object, the update kind (ukModify, ukInsert, ukDelete), and an Action parameter that resolves the operation. Action can be set to a number of values, as shown in the following table. Table 13-4 TReconcileAction
Description
raSkip
Skip the updates to this record but leave the changes in the ClientDataSet cache. This is the default value. Cancel all updates to the record, reverting back to its original values. Abort the entire reconcile operation.
raCancel raAbort
300
Chapter 13: Client-Side Data Management TReconcileAction
Description
raMerge raCorrect
Merge the updated record with the data on the server. Replace the current updated record with the record values in the event handler. Cancel all updates to the record, replacing them with the server’s values.
raRefresh
Handling Errors with DataSetProviders At the DataSetProvider level, update errors are handled through the OnUpdateError event. Like the OnReconcileEvent, the OnUpdateError event is generated for each record whose update fails. OnUpdateError is of type TResolverErrorEvent and includes parameters similar to the OnReconcileEvent. However, instead of the Action parameter, OnUpdateError has a Response parameter. Response dictates the DataSetProvider’s response to the error condition and can be set to one of five values described below. procedure TForm1.DataSetProvider1UpdateError(Sender: TObject; DataSet: TCustomClientDataSet; E: EUpdateError; UpdateKind: TUpdateKind; var Response: TResolverResponse); begin ShowMessage('DSP - UpdateError'); Response := rrAbort; end; Table 13-5 TResolverResponse
Description
rrSkip
Skip the updates to this record but leave the changes in the ClientDataSet cache. Note that this will also cause the ClientDataSet’s OnReconcileError to be raised. This is the default value. Abort the operation without displaying an error message. This value does not stop the ClientDataSet’s OnReconcileError event from occurring. Merge the updates from the ClientDataSet’s delta packet with the new underlying database value. This will not succeed if another user has changed the value of the field in the database. Apply the current record value (after it has been edited in the OnUpdateError event) instead of the value sent to the event. Ignore the error. The record is not applied or returned to the ClientDataSet for resolution.
rrAbort
rrMerge
rrApply rrIgnore
Chapter 13: Client-Side Data Management
301
Dealing with Offline Data Application users (like a remote sales force) are often separated from the central source of data. Kylix gives developers the ability to create fully functional offline data access programs that allow data to be downloaded, stored, accessed, and updated offline, and then resolved to the live database upon reconnection. This type of application is referred to as the briefcase model.
Connecting to Data The application gives the user a choice of loading data from the database server or from a local file. If the user chooses a database connection, a provider is set for the ClientDataSet which is then opened. If a local connection is desired, call the LoadFromFile method, specifying the data file. procedure TfrmMainScreen.FormCreate(Sender: Figure 13-8: Connecting to data TObject); begin // Determine how we need to connect to the ClientDataSet with TfrmConnectionOptions.Create(nil) do try // subtle bug here. If the user doesn't okay the dialog, // nothing happens! if ShowModal = mrOK then if rdgrpConnectionOptions.ItemIndex = 0 then begin clntdtstOrders.ProviderName := dtstprvdrOrders.Name; clntdtstOrders.Open; end else clntdtstOrders.LoadFromFile(LOCAL_FILENAME); finally Free; end; end;
Once data has been retrieved from the database, it can be saved locally by calling the ClientDataSet’s SaveToFile method. SaveToFile takes two parameters, specifying the name of the local file and the format of the storage. Storage format can be binary (the default), XML, or XMLUTF8.
302
Chapter 13: Client-Side Data Management const LOCAL_FILENAME = 'orders.cds'; procedure TfrmMainScreen.btnSaveLocallyClick(Sender: TObject); begin clntdtstOrders.SaveToFile(LOCAL_FILENAME); end;
Resolving Offline Data When a remote application connects to the remote database, it must resolve changes from its local cache to the database. First, the application connects to the database. Once the connection has been made, the changes in the local data set’s cache must be applied to the remote database. procedure TfrmMainScreen.btnUpdateChangesClick(Sender: TObject); begin clntdtstOrders.Close; clntdtstOrders.ProviderName := dtstprvdrOrders.Name; clntdtstOrders.Open; clntdtstOrders.LoadFromFile(LOCAL_FILENAME); clntdtstOrders.ApplyUpdates(0); ShowMessage('The records have been updated'); end;
Alert: Notice that in the preceding code, the MergeChangeLog method was not called. ApplyUpdates calls MergeChangeLog for you after the updates are applied. If MergeChangeLog is called before ApplyUpdates, the Delta property (the cache) is emptied and ApplyUpdates thinks there is nothing to apply!
The Super Data Component: TSQLClientDataSet With all the data access components being used to retrieve, manipulate, and resolve data, the neighborhood can get crowded pretty quickly. Each time we want to retrieve a data set for manipulation and updating, we have to drop no less than three components into the application, not including the SQLConnection, the datasource, or the dbcontrols! Wouldn’t it be great (WARNING: I’m setting you up) if we could wrap all of that functionality into one component? A component that uses dbExpress for quick access to unidirectional data, and then turns around and provides that data to its own cache. Fortunately, such a component already exists. The TSQLClientDataSet is a DataCLX component that is a ClientDataSet with a SQLDataSet and a DataSetProvider built in.
Chapter 13: Client-Side Data Management
303
Warning: Although the TSQLClientDataSet provides all of the necessary components for connecting to and displaying data, it must be noted that it does not expose all of the properties of its internal components (the SQLDataset and the DataSetProvider). For example, the ResolvetoDataSet property of the TDataSetProvider component is not exposed to the developer. In applications that involve more than the most trivial of data management, it is usually better to use each component explicitly.
Figure 13-9: The SQLClientDataSet component
Establishing a Connection The SQLClientDataSet provides two ways to establish a connection. The DBConnection property allows the user to set a SQLConnection component as the connection component. It is used in the same way as a SQLDataSet uses the SQLConnection property. The ConnectionName property allows the user to select a predefined connection (from the same ones that appear in the SQLConnection editor) as the method of accessing the database.
304
Chapter 13: Client-Side Data Management
Tip: In applications that use more than one SQLClientDataSet in the same context, using the ConnectionName property is not the best option. When this property is set, each SQLClientDataSet uses its own connection to the database. The mutator method (the component’s internal method that sets the property’s value) of the ConnectionName property explicitly creates a TSQLConnection object (after freeing the current one) each time the property is set. Thus, each data set that the application used would use another database connection. Overall, while the SQLClientDataSet saves a small amount of time in the development of data-aware modules, it is generally better to use each component explicitly to perform the job it is intended to do. A few extra moments during development provides the developer with a wealth of functionalities to control and manage the movement and manipulation of data in the application.
Summary Manipulating data in the client is a central task of an overwhelming number of applications. Although the SQLDataSets of the last chapter are remarkable efficient at retrieving data, they rely on the DataSetProvider and the ClientDataSet to perform the specific nuances of application-level data manipulation. Studying these two components is key to understanding not only how data is accessed in a Kylix application but how it is used.
Chapter 14
Using Field Objects
Introduction In the last chapter, we looked at managing data sets in Kylix applications. In this chapter, we look at data sets in more detail. A data set is a collection of heterogeneous data. Each element (field) of a data set can have its own rules for providing validations and constraints on the data that it holds. Kylix field objects allow the developer to specify the way in which a particular field of a data set is treated within the application.
Kylix Datasets, Revisited Most people think of a data set as a grid of records (with a certain number of rows and columns). Of course, this is not actually how data is stored on the client. ClientDataSets keep a local cache of data on the client for use by the application and store the information for the current record in a local record buffer. When a data set is navigated, the next set of data (a record) is retrieved from the local cache and loaded into the buffer. Datasets are usually thought of as a collection of records, with the cursor moving through them, when it is actually more accurate to say that a resultset is a cursor with data moving through it. In other words, a data set and a record are the same thing. The real difference is that the term “data set” implies the existence of a local cache from which the record buffer is populated.
Figure 14-1: Client-side data storage
305
306
Chapter 14: Using Field Objects
Field Objects Once a resultset is understood in this way, it makes more sense then to say that a data set is made up of a collection of field objects. Each field object is used to manage a particular field of the data set. When a data set is opened, the application dynamically creates a set of field objects that help to manage the current data. Field objects are a great tool for implementing client-side data rules in an application. The next sections describe and demonstrate the use of field objects in a Kylix data application.
Field Object Ancestry
TE
AM FL Y
Each field object is a descendant of the TField class (which descends directly from TComponent). When the field objects are created, Kylix determines from the metadata provided by the database driver what type of field object to create. Each field object type contains different properties depending on the type of data that is being managed. Figure 14-2 shows a partial view of the CLX field object hierarchy.
Figure 14-2: The CLX field object hierarchy
Chapter 14: Using Field Objects
307
Field Objects in Kylix Applications When a Kylix data set is opened, the application dynamically creates a collection of field objects, based on the schema information of the data it is retrieving. Although it is convenient for Kylix to do this for you, it presents a problem. In order to provide data constraints, validations, and other property settings of the field objects, the developer is forced to programmatically alter the field objects. This can add a good deal of code to an application. In addition, dynamically created field objects are temporary. They only exist while the data set is open. Each time a data set is opened, the field objects are re-created. Properties and events must be reassigned to the new field objects.
Creating Persistent Field Objects A better solution is to create a collection of persistent field objects. Persistent field objects are explicitly declared by the class and so can interact with the design-time environment. This allows the developer to set properties of the field objects without adding a large amount of code. When field objects are persisted, they are declared by the class in which the data set exists. In the following example, notice the field objects declared at the bottom of the class declaration. Each field object is named after both the data set it represents and the name of the field that it manages. Keep in mind that even though the class declaration is slightly larger, the footprint of the application is not increased by these declarations. This is because those field objects always exist, whether or not they are declared at design time. Even if field objects are not made persistent, they are created by the application dynamically, at run time. type TfrmEmployeeMain = class(TForm) SQLConnection1: TSQLConnection; SQLDataSet1: TSQLDataSet; DataSetProvider1: TDataSetProvider; ClientDataSet1: TClientDataSet; DataSource1: TDataSource; DBGrid1: TDBGrid; DBNavigator1: TDBNavigator; ClientDataSet1EMP_NO: TSmallintField; ClientDataSet1FIRST_NAME: TStringField; ClientDataSet1LAST_NAME: TStringField; ClientDataSet1PHONE_EXT: TStringField; ClientDataSet1HIRE_DATE: TSQLTimeStampField; ClientDataSet1DEPT_NO: TStringField; ClientDataSet1JOB_CODE: TStringField; ClientDataSet1JOB_GRADE: TSmallintField; ClientDataSet1JOB_COUNTRY: TStringField; ClientDataSet1SALARY: TFMTBCDField;
308
Chapter 14: Using Field Objects ClientDataSet1FULL_NAME: TStringField; private { Private declarations } public { Public declarations } end;
Using the Fields Editor Persistent field objects are created using the fields editor. To launch the fields editor, right-click the data set (or double-click) and select Fields Editor. The fields editor window is initially blank. When no persistent field objects are explicitly declared, the data set assumes that field objects should be generated for every field of the data set. To choose the field objects that the data set should contain, right-click the fields editor and select Add Fields… A list of the available fields is displayed and individual fields can be selected and added to the class. Any fields not included in the fields editor (in the declaration of the class) cannot be accessed by any data-aware component. Once the fields are made persistent, the fields editor is used to access each field object in the designer. Selecting any field object in the editor loads its properties into the Object Inspector.
Figure 14-3: The Kylix fields editor
Note: It’s important to recognize that using field objects allows the application to deal with data at the data set level. In other words, the display properties, constraints, and validations that are set for a field object are provided uniformly to any control that accesses the data set for which the field object is created. This prevents the code from having to be rewritten for every control that deals with that field. This, however, does not prevent the developer from using another data set that points to the same data and provides completely different field-level code.
Chapter 14: Using Field Objects
309
Using the DBGrid Columns Editor Another way to persist the use of data in an application is to use the columns editor of the dbgrid component. The columns editor allows the developer to persist the dbgrid’s display properties. Although some of the properties are the same, the columns editor is not nearly as powerful as the fields editor. Changes only affect the dbgrid for which they are set. To launch the editor, right-click on the dbgrid and select Columns Editor…
Figure 14-4: The dbgrid columns editor
Figure 14-5 shows an example of the effects of the columns editor.
Figure 14-5: Comparison of formatted and unformatted grids
310
Chapter 14: Using Field Objects
With it, developers can create much more attractive and usable grids. Although most properties of a column are related to display only, there are some that are more central to functionality. For instance, each column has a ReadOnly property that can be used to keep a column from being edited. This is a good example of why a developer might use the columns editor instead of, or in addition to, the fields editor. If the ReadOnly property is set to true in the fields editor, then no control (that used that data set) would ever be able to change the value. If the field is only non-editable in one part of the application, the columns editor is a better choice to restrict its use.
Setting Field-level Properties Each field object type includes its own set of properties that help to manage its data. Because data types use different constraints, formats, and policies, each field object type is different. Note: The following section demonstrates the use of a few common field object properties, but there are many more. You should take time to get to know what field object properties can do for your application. The primary key of a data set is usually not able to be changed by the user. As such, its ReadOnly property is commonly set to true. Keep in mind that by setting it to true, edits cannot be made by any component using that field of the data set. In addition, the display of the data may be standardized by setting its Alignment and DisplayLabel properties.
Edit Masks TStringField objects include a property called EditMask that allows the developer to specify a format for entering data. Edit masks have their own little language, that is, a set of characters that specify the rules for each character of the mask. A common example of edit mask use is in entering phone or fax numbers. There are a number of ways that numbers can be entered (with area codes, with dashes, etc.) Application data is much easier to deal with if the format of the phone number data is consistent. To accomplish this, the EmployeePhone field object (which is of type TStringField) must set an edit mask to force users to include numbers in a certain format. The Input Mask Editor is shown in Figure 14-6. The input mask itself is listed at the top left of the editor. The Character for Blanks option determines what character will be put in place of unentered characters in the mask. Save Literal Characters determines if the literal characters (in this case, the parentheses) are part of the control’s text or if they are filtered out when the value is saved. On the right side of the editor is a list of commonly used masks. Selecting one of these automatically creates the mask settings for the developer.
Chapter 14: Using Field Objects
311
Figure 14-6: The Input Mask Editor
Another example of a common field object property is included in the TFloatField. The TFloatField type has a Boolean currency property that automatically formats the value as a currency value.
Comparing Changed Field Values The last chapter showed an example of a field object’s “old value.” Field objects can contain multiple values for a particular field to keep track of any changes that are made to it, while maintaining the field’s original value in the data set. procedure TForm1.DataSetProvider1BeforeUpdateRecord(Sender: TObject; SourceDS: TDataSet; DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean); begin if DeltaDS.FieldByName(Job_Code').OldValue = ‘VP’ then raise EDatabaseError.Create(‘VP records cannot be modified.'); end;
Field objects actually contain three properties related to a field’s value. NewValue refers to the value of the field object at the current moment. OldValue refers to the value of the field at the time the data set was queried from the database (actually, it refers to the value at the time the current record was loaded into the record pointer). Use NewValue and OldValue to compare a field’s original value with its current value. This can be effective in determining what changes have been made to the current record. Finally, field objects include the CurValue property. CurValue represents the current value in the database. When CurValue is accessed, the data set goes back to the database to retrieve a record’s current value. The entire record (not just one field) is read into an internal record buffer in the data set. CurValue then holds a snapshot of the current value of the record in the database. This is commonly used to compare a database’s current value against NewValue and OldValue during an OnUpdateError event. It allows the application to determine the current state of a field so that the record can be fixed and reapplied.
312
Chapter 14: Using Field Objects
Warning: The current release of dbExpress contains a bug that keeps the CurValue property from being populated. It always has an unassigned value. Look for a patch in upcoming releases of dbExpress. Note: Some field object display properties are only applicable to the dbgrid control. The DisplayLabel property, for instance, dictates the caption used in the header of the dbgrid control. No other control includes the use of a column header, so that property is not needed by any other component. Because of the number of dbgrids that are used in applications, these field object properties are included even though they are not applicable to all of the dbcontrols.
Field Object Events Each field object includes a set of events that makes field-level processing easier for the developer to implement. One of the most commonly used events is OnValidate. OnValidate is called just before the new field value is written to the record buffer. Again, it’s important to note that this allows the program to validate at the data set level, ensuring cohesive validations for any augmentation of that particular field. If a particular value is rejected by the validation code, the developer should raise an exception. procedure TfrmEmployee.clntdtstEmployeeEmpNoValidate (Sender:TObject); begin if clntdtstEmployeeEmpNo.Value < 0 then raise EinvalidEmpNo.Create(‘Invalid employee ID’); end;
Another useful event is the OnChange event. OnChange is fired just after the OnValidate event (if OnValidate does not raise an exception). In other words, the OnChange event allows the application to respond to successful changes in the data for a particular field. Finally, field objects have two events, OnGetText and OnSetText. These events allow the application to distinguish between a field’s actual data and the data that is displayed in the application.
Creating Custom Field Objects Although most field objects come directly from the database, there are a number of occasions when it is helpful to manufacture custom field objects. Although the types of field objects are slightly different, both SQLDataSets and ClientDataSets can create custom field objects. In this section, we look at the various custom objects that can be created within a Kylix data set.
Chapter 14: Using Field Objects
313
Calculated Fields A calculated field is a field that does not actually exist in the database schema; that is, it is not physically stored in a database. Calculated fields are fields whose values are dynamically derived, usually from other fields in the data set. For instance, an application that displays account balances for a list of customers might also show the current payment due. Because monthly payments are constantly changing and because their value can be determined directly from the current balance, they do not need to be stored in the database. Reducing the number of stored fields lessens network traffic and needless data retrievals. To create a calculated field, right-click (or double-click) the data set to display the fields editor. Right-click the fields editor and select New Field. The New Field dialog is displayed.
Figure 14-7: The New Field editor
Begin by giving the calculated field a name. Next, select a type for the field object (for the monthly payment field, select currency). Based on this selection, Kylix will add a field object of the correct class type (a descendant of TField). If the type warrants it, a size for the field can be specified. Make sure that the Field Type attribute is set on Calculated (other types will be discussed later on in this chapter). When the OK button is pressed, the fields editor displays the new field object along with the other field objects. Select the new field object to display its properties and events in the Object Inspector. Calculated fields must have their values set at run time. To set the value of a calculated field, use the OnCalcFields event of the data set (note that the OnCalcFields event is an event of the data set, not the field object). All of the calculated fields of a data set are calculated at the same time, that is, in the same event. procedure TfrmEmployee.clntdtstEmployeeCalcFields(Sender:TObject); begin clntdtstEmployeeProposedIncrease.value := clntdtstEmployeeSalary.Value * 1.03; end;
314
Chapter 14: Using Field Objects
Note: When two or more calculated fields exist in the same table, the logic for determining their values will all appear in the data set’s OnCalcFields event. The values for calculated fields are all calculated at the same time.
Lookup Fields Lookup fields are used to provide a value based on search criteria, usually another field from the table. For instance, an application that takes orders from a customer requires a customer number for each order. Unfortunately, this requires the user to know the number of every customer. An application that is easier to use allows the user to select a customer number from a list of customer names and other useful information. Even though the value applied to the record is a customer number, the user selects from a readable list of customer information. To create a lookup field, right-click the fields editor and select New Field. Enter a name for the field, the data type of the field, and the size of the field, if applicable. Choose the Lookup option for the field type. Once the Lookup option is selected, the bottom portion of the dialog is enabled for entering the additional information required for a lookup definition. In the Key Fields list, choose the field(s) that comprise the primary key field needed to perform the lookup. Multiple fields are separated with a semicolon. From the Dataset list, choose the data set that provides the lookup values. Note that this must be a bi-directional data set. Similarly, in the Lookup Keys list, choose the field(s) that make up the primary key from the data set specified previously. Finally, choose the field that will be displayed in place of the primary key value for this field object. For example, in the dbdemos database located in the /kylix2/demos/db/data directory, there are two tables that will be used to demonstrate how to create a lookup field. In order to avoid any file permission problems, copy the dbdemos.gdb file into a working directory. Be sure to save the project files in this same directory since the application will expect the dbdemos.gdb file to be in the same directory as the executable. Tip: Delphi supports dragging a field object from the fields editor and dropping it onto a form. In Kylix 1 and Kylix 2, this feature is not currently implemented.
Chapter 14: Using Field Objects
315
Field Objects and ClientDataSets When the New Field dialog is displayed for a ClientDataSet (instead of a SQLDataSet), the field types include two new options: InternalCalc and Aggregate. These options do not make sense for a SQLDataSet (given the nature of a SQLDataSet) but only apply to ClientDataSets.
InternalCalc Fields An internal calculated field is a calculated field that is stored within the data set but not persisted back to the database. Since the internally calculated field is stored within the data set, this allows for indexing, filtering, and aggregating on calculated fields. As with calculated fields, compute the value in the data set's OnCalcFields event.
Aggregate Fields An aggregate field is a special kind of calculated field. Like calculated fields, they are not stored with the database. Create aggregate fields by right-clicking in the fields editor (Figure 14-3) and selecting the New Fields menu item. Set the field type to Aggregate, give the aggregate a name, and press the OK button.
Figure 14-8: Field Editor with aggregate fields
The field editor changes slightly when aggregates exist in the data set (Figure 14-8). Notice that there are two sections. Aggregate fields are those listed in the lower section. After creating an aggregate field, set the expression property to the calculation for the field. For example, if the aggregate were calculating the average salary, the expression would be: AVG(SALARY)
Chapter 14: Using Field Objects
Warning: Kylix 1 contains a bug when aggregating bcd fields. Any aggregate computation performed on an bcd field is calculated incorrectly. This bug has been fixed in Kylix 2. Only certain types of calculations are allowed for aggregate fields. These calculations are shown in the following table: Table 14-1 Description
Count(*) or Count(fieldname)
Returns the number of records that are not blank. Works with any field type. Calculates the average for the field. Fields can be numeric or date-time. Calculates the sum of the field. Must be a numeric field. Calculates the minimum value for the field. Works on string, numeric, and date-time fields. Calculates the maximum value for the field. Works on string, numeric, and date-time fields.
AVG(fieldname) SUM(fieldname) MIN(fieldname) MAX(fieldname)
AM FL Y
Function
While only the functions shown above are allowed, they can be combined to form complex expressions like this: (AVG(fieldname) + Count(fieldname) * MIN(fieldname)) / MAX(fieldname)
A field name cannot be used in an aggregate expression unless one of the functions is performed on the field. For example, the following expression would be illegal since fieldname2 stands alone. Another way of looking at this is that aggregate fields calculate over the entire set of records, so which value of fieldname2 would be used?
TE
316
AVG(fieldname) * 0.05 + fieldname2
In addition to calculating on the entire set of records, aggregates have the ability to calculate the expression based on a grouping level of the data. Consult the Kylix help file for further details. After an aggregate field has an expression, activate the aggregate field by setting the Active property to true. Similarly, the ClientDataSet has an AggregatesActive property that must be set to true as well. The easiest way to see the value of an aggregate field is to hook it up to a TDBEdit field. Tip: A calculated field cannot be used as the field name for any of the aggregate functions. Instead, create an InternalCalcField, and perform the appropriate aggregate function on this internal calculated field.
Chapter 14: Using Field Objects
317
Summary Kylix data sets offer a wealth of functionality and allow developers to centralize the settings, constraints, and events surrounding the fields of a resultset. Persistent field objects give the developer a place from which to control and manage resultset fields from within the Kylix IDE. Using field objects is a great way to introduce business rules in a central location and avoid the unnecessary work of having to write the same code over and over again.
Chapter 15
CLX
Introduction Throughout the book to this point, there have been many references to the Kylix component architecture, called CLX. In this chapter we take CLX to the next level by looking inside of this powerful and extensible architecture. By the end of this chapter, the internals of CLX will have been exposed for developers to exploit in their applications. CLX stands for Component Library Cross(X)-Platform. It is a broad term for the set of classes, objects, and types that make up the Kylix library. CLX takes on many forms but can be broken down into four primary areas: n
BaseCLX includes all elements of the Kylix run-time library. Details of BaseCLX can be found in Chapter 9, “Compiler, Run-time Library, and Variants.”
n
DataCLX components use the new dbExpress data architecture, found in Kylix and Delphi 6, to provide powerful and efficient native access to a variety of today’s major database servers. DataCLX is the focus of Chapter 12 (“dbExpress and DataCLX”) and continues into Chapter 13 (“Client-Side Data Management”).
n
NetCLX includes the cross-platform Internet architecture and component layer. NetCLX is a great way to provide Internet connectivity and functionality whether your web server runs on Linux or Windows. NetCLX is discussed in detail in Chapter 21, “Internet Applications — NetCLX.”
n
Finally, VisualCLX, also called vCLX, refers to the most commonly used classes and components that provide incredibly sophisticated GUIs as well as a number of other functionalities. This chapter focuses on vCLX, both its rich set of components, visual and non-visual, and its architecture and internal mechanisms that make it great.
321
322
Chapter 15: CLX
vCLX Background vCLX is a cross-platform component architecture, based on the functionality of Delphi’s VCL. However, the library is not simply the VCL, ported to Linux. vCLX is actually based on a cross-platform C++ library called Qt (pronounced “cute”). Qt, which was first developed in 1992, is maintained by Trolltech, a software company located in Norway. vCLX is a wrapper around the Qt library. Although it is not the VCL on Linux, vCLX was designed to support most of the functionality of the VCL, and Borland made every effort to keep the interfaces of the two libraries as similar as possible. In fact, the interfaces are identical up to the TControl level of the hierarchy.
Figure 15-1: Overview of CLX architecture
Figure 15-1 shows how CLX interacts with the Linux environment. Each of the top three CLX subsets (DataCLX, NetCLX, and VisualCLX) is built on top of the BaseCLX layer, which interacts with Qt and the environment. VisualCLX, the subject of this chapter, also works with the X Window System to provide graphical representation of application widgets.
The vCLX Architecture Fortunately, understanding the details and underpinnings of vCLX is not necessary for the average developer. Borland has wrapped vCLX around the Qt layer, so that in most situations vCLX developers do not have to deal with the translation and handling of event processing, graphic functionality, and other elements. However, even if you are not taking vCLX beyond the surface layer, it is a good idea to get an understanding of exactly what is going on “beneath the covers” of this component library.
Chapter 15: CLX
323
Figure 15-2: vCLX and Linux
VCLX Files Much of the discussion of vCLX involves a description of concepts and strategies of wrapping the Qt (C++) layer. Understand though that there is a place in Kylix where concepts meet reality. Figure 15-2 describes the interaction between vCLX and Linux. vCLX was designed to minimize the dependency on any specific operating system. In reality, however, vCLX does make operating system-specific calls, but only when absolutely necessary. There is a layer called DisplayCLX that interacts with Qt’s run-time library on the application’s behalf. DisplayCLX is comprised of two files: libqtintf.so and Qt.pas.
libqtintf.so libqtintf.so is a Linux shared object library (see Chapter 8 for more information on shared object libraries) that exposes the functionality of the Qt (C++) layer. It binds dynamically to libQt.so, the Qt library. libqtintf.so gives vCLX access to the run-time library of Qt. Of course, vCLX does not require every function that Qt provides. Only those methods used by vCLX are exposed through this library.
Qt.pas Because libqtintf.so is a library that contains C and C++ code, Kylix must be able to dynamically bind to various routines and import them into the application. To do this, Kylix uses the Qt.pas file. Qt.pas imports the functionality of libqtintf.so so that its functions can be called from within vCLX applications. It contains a long list of type declarations and function imports. Qt is actually a set of C++ objects that manage the visual controls on the system where Qt is deployed. Qt.pas is actually a flattening out of the objects as a large collection of function calls. Sample sections of Qt.pas are shown below. Notice that all of the calls — except for the calls that create objects — take as their first parameter a handle to an existing Qt object.
324
Chapter 15: CLX type // class declarations QWidgetH = class(QObjectH) end; QButtonH = class(QWidgetH) end; QCheckBoxH = class(QButtonH) end; QPushButtonH = class(QButtonH) end; QClxBitBtnH = class(QPushButtonH) end; QRadioButtonH = class(QButtonH) end; QToolButtonH = class(QButtonH) end; {…} // method declarations function QButton_create(parent: QWidgetH; name: PAnsiChar; f: WFlags): QButtonH; cdecl; procedure QButton_destroy(handle: QButtonH); cdecl; procedure QButton_text(handle: QButtonH; retval: PWideString); cdecl; procedure QButton_setText(handle: QButtonH; p1: PWideString); cdecl; {…} // external method references. These refer to the corresponding // Qt method in libqtintf.so procedure QButton_destroy; external QtShareName name QtNamePrefix + 'QButton_destroy'; function QButton_create; external QtShareName name QtNamePrefix + 'QButton_create'; procedure QButton_text; external QtShareName name QtNamePrefix + 'QButton_text'; procedure QButton_setText; external QtShareName name QtNamePrefix + 'QButton_setText';
Note: Qt.pas is dynamically linked to libqtintf.so, which is dynamically linked to to libqt.so. This can have a great impact on deployment because some systems may already have libqt.so (which is approximately 7 MB). If the correct version of libqt.so already exists on a system, it is only necessary to distribute libqtintf.so.
Chapter 15: CLX
325
Widgets and Other Critters All Kylix controls descend from the TControl class which, in turn, descends from TComponent. A control is any visual component (one that has a visual representation in the run-time environment). Controls come in two flavors: TGraphicControls and TWidgetControls. The primary difference between them is that whereas TGraphicControls are simply painted on the screen, TWidgetControls are those that require a handle from Qt, accessible through the Handle property. The handle of a vCLX object is actually a pointer to its underlying Qt object (commonly called a widget). Handle is a read-only property. The following code shows an example of using the Handle property to get information about the underlying Qt object, in this case, the name of the class. procedure TForm1.Button1Click(Sender : TObject); begin ShowMessage(QObject_className( (Sender as TButton).Handle )); end;
TWidgetControl is the foundation for a large number of classes in vCLX. Controls that descend from TWidgetControl share a number of common attributes including the ability to receive focus at run time and the ability to contain other controls. In addition, descendants of TWidgetControl automatically take on the following collection of events: OnEnter, OnExit, OnKeyDown, OnKeyPress, OnKeyUp, OnMouseWheelDown, OnMouseWheel, OnMouseWheelUp. CLX events are somewhat more complex than in other environments and are discussed in detail in the next section.
vCLX Events Events in Kylix occur at several levels including the operating System, the X Window system, the Qt run-time library, inside DisplayCLX, and inside the vCLX controls themselves. The following sections describe the interactions between these various layers and how the developer can control event processing in Kylix applications. Normally, components receive events (via the application) from the operating system. However, vCLX very rarely talks to the operating system. Instead, it talks to and receives event information from Qt. In this way, Qt can be thought of as the vCLX operating system. Whether the event is originally generated in the operating system or in the Qt run-time library is irrelevant to the vCLX control. To understand how vCLX events work, we must look at the various pieces of the event structure and how they interact. Kylix events involve four basic entities: the operating system, the Qt object (widget), a Kylix specific object called a hook, and the vCLX component (TWidgetControl).
326
Chapter 15: CLX
Qt Events Low-level events such as mouse and keypress events are generated by the X Window System and the Window Manager. Widgets in the Qt run-time library wrap up and expose those events as descendants of the QEvent type. These events include those from the operating system as well as most of the common events generated by the X Window System. Qt declares a number of additional events internally and exposes them to clients of Qt (in this case, vCLX). Figure 15-3 shows a general overview of Qt events.
AM FL Y
Figure 15-3: Qt’s internal event structure
TE
Events generated from Linux or from the X Window System are sent to the QApplication object of the program that is being run. QApplication then sends the event on to the widget that caused the event. The correct widget is determined by the QApplication object, based on contextual information sent along with the event. Once the widget receives the event (and some additional information) it executes the event handler that is assigned to that event (if it exists).
Event Filters At both the application level and the widget level, there is an event filter that allows the object to preprocess events that are sent to it. For instance, if an event is sent to a component, it can be sent on to the component’s event handlers, or it can be rejected by either the component’s or the application’s event filter. An example of using EventFilter for processing custom events is shown in the listing below. There are two ways of sending an event: using either the QApplication_sendEvent routine or the QApplication_postEvent routine. The only difference between the two is that sendEvent waits until the receiver has handled the event, while postEvent does not wait.
Chapter 15: CLX unit frmMain; interface uses SysUtils, Types, Classes, Variants, QGraphics, QControls, QForms, QDialogs, QStdCtrls, Qt; const MY_MSG_NO = 2; // arbitrary type TForm1 = class(TForm) btnSendEvent: TButton; btnPostEvent: TButton; btnCallMsgProc: TButton; procedure btnSendEventClick(Sender: TObject); procedure btnPostEventClick(Sender: TObject); procedure btnCallMsgProcClick(Sender: TObject); private AnotherEvent : QCustomEventH; protected function EventFilter(Sender: QObjectH; Event: QEventH): Boolean; override; procedure MyMessageProc(var Msg); message My_MSG_NO; public end; var Form1: TForm1; implementation {$R *.xfm} const // Define a couple of events that will be used later MyCustomEvent = QEventType(Ord(QEventType_ClxUser) + 1); MyMsgProcEvent = QEventType(Ord(QEventType_ClxUser) + MY_MSG_NO); type TCustomMessage = record // The first two bytes indicate the Message Number associated // with this message. This is how the Dispatch method knows which // method to call, by searching the VMT table for the method that // is assigned this value.
327
328
Chapter 15: CLX MsgID : Word; // anything else can be placed here MsgInt : integer; // etc... end; { TForm1 } procedure TForm1.btnSendEventClick(Sender: TObject); var myEvent : QCustomEventH; begin // since we are using the "sendEvent" method, pass in nil // for the data. We will destroy the event here myEvent := QCustomEvent_create( MyCustomEvent, nil ); try // Since sendEvent waits until the event has been handled QApplication_sendEvent( Self.Handle , myEvent); finally // we can destroy the event when we're finished QCustomEvent_destroy( myEvent ); end; end; procedure TForm1.btnPostEventClick(Sender: TObject); begin // Create the event, passing the pointer of this event as the // data. When the EventFilter method processes this event, // the EventFilter method will take care of cleaning up this event. AnotherEvent := QCustomEvent_create( MyCustomEvent, Pointer(AnotherEvent) ); // post does not wait for the event to be handled. QApplication_postEvent( Self.Handle, AnotherEvent); end; procedure TForm1.btnCallMsgProcClick(Sender: TObject); var Event : QCustomEventH; begin // create the custom event, passing nil as the data paramter. // This indicates to EventFilter that the cleanup will occur // here in this method. Event := QCustomEvent_create( MyMsgProcEvent, nil); try
Chapter 15: CLX // sendEvent waits for the event to be handled. QApplication_sendEvent( Self.Handle, Event); finally QCustomEvent_destroy( Event ); end; end; procedure TForm1.MyMessageProc(var Msg); var theMsg : TCustomMessage; begin // The "message" procedure handler // convert the message into the message type theMsg := TCustomMessage(Msg); // and display the results ShowMessage('Hello from MyMessageProc! ID is %d, Int value is %d', [theMsg.MsgID,theMsg.MsgInt]); end; function TForm1.EventFilter(Sender: QObjectH; Event: QEventH): Boolean; var EvntType : QEventType; MyEvntHandle : QCustomEventH; msg : TCustomMessage; begin EvntType := QEvent_type(Event); // determine if this is an event that we're intereseted in if EvntType = MyCustomEvent then begin // yes we are interested. Display a message ShowMessage('My Custom Event fired!'); // get the data portion of the event MyEvntHandle := QCustomEvent_data( QCustomEventH(Event) ); if assigned(MyEvntHandle) then begin // we handled the event and it was posted so // clean up this event QCustomEvent_destroy( MyEvntHandle ); end; Result := true; end else if EvntType = MyMsgProcEvent then begin // for this example, we have assumed that the caller will clean // up the event when they are done with it. msg.MsgID := Ord(EvntType) - Ord(QEventType_ClxUser);
329
330
Chapter 15: CLX msg.MsgInt := 77; // arbitrary.. Dispatch( msg ); Result := true; end else begin // we will let the normal EventFilter handle this event Result := inherited EventFilter(Sender,Event); end; end; end.
EventFilter first extracts the event type and determines if the event needs to be handled. When the MyCustomEvent is fired, a message is displayed; if the message was sent via postEvent, it is destroyed. An event that is posted instead of sent must be destroyed somewhere. In this example, the destruction of the custom event occurs in the EventFilter method. For messages sent using the sendEvent routine, the caller is responsible for destroying the event. Note: The VCL based almost all of its events on the Windows messaging system. In the VCL, the event filter concept was accomplished by overriding the WndProc method. There is no messaging system in Qt as there is in Windows. Instead, Qt uses what it calls signals to indicate events have happened. Therefore, all “message handling” in vCLX is done via the EventFilter method, which is analogous to the WndProc method in the VCL. EventFilter is usually overridden when the application or a TWidgetControl needs to handle events in a non-uniform way, to enhance or override the standard behavior, or when events must be intercepted early. For instance, EventFilter might be used to prevent an event from being dispatched to a widget’s event handlers or to provide additional functionality above the standard behavior.
Signals and Slots Two important players in the Qt event architecture are slots and signals. Slots are Qt’s event handlers. They are callback methods of Qt objects that respond to a particular event. Signals are notifications that are emitted when the state of a widget changes in a meaningful way. When a signal is emitted, it executes every slot that it is connected to. A single signal can be connected to multiple slots. Each slot is executed when the signal is emitted. When an event is sent from the QApplication object to a widget, it is sent through the widget’s QWidget::event() method. When a widget receives an event, it determines from contextual event information if it owns a signal that is associated with that event. If it does, it triggers that signal (if no event filters stop it from executing). Each widget contains a list of slot method references. These slots are
Chapter 15: CLX
331
executed whenever the signal is broadcast by the widget (whenever the event occurs). Note: Signals are remarkably important because they allow multiple slots to be registered for each event (signal). However, in the vCLX layer, there is typically a one-to-one relationship between signals and slot methods (event handlers).
Hooks Keep in mind that Qt widgets are C++ objects. Of course, vCLX objects are written in Object Pascal. This presents a communication problem in that Qt objects can only talk to other C++ objects. There must be a mapping layer that allows messages from the Qt run-time library to be passed on to the vCLX layer. For this reason, Kylix uses hook objects. Hooks are basically used to pass through events from Qt objects to vCLX objects. A hook connects its slots to a widget’s signals, then the vCLX objects connect their event handlers to signals of the hook object. So a Qt event actually generates a chain of callbacks from the Qt object, to the hook, to the vCLX object. In addition to the individual event callbacks, the hook object also includes a reference to the widget’s EventFilter method. As noted before, EventFilter allows a widget (or the application) to preprocess events, before they are sent on to the event handlers.
Figure 15-4: Understanding signals and slots
The remaining question regarding hooks is, why do we need them? What value is there in an extra level of indirection? The answer is the the hook is used not to add value but as a necessity. Qt objects are C++ objects that live in the Qt run-time environment. Qt objects are only able to talk to other C++ objects. vCLX components are not C++ objects. They are written in Object Pascal. So a mapping layer is needed to pass through information between the two layers. This is the main service of the hook object.
332
Chapter 15: CLX
Note: vCLX is responsible for destroying hook objects and for detecting when they are released by Qt (when the Qt object has been destroyed behind the scenes).
Handling “Special Keys” In order for vCLX to handle keys with the level of complexity achieved by Delphi’s VCL, special processing is required. Qt does not provide special key messages like the Windows API. For this reason, vCLX introduces the NeedKey and WantKey functions. These functions are used to provide advanced key handling functionality within vCLX applications. The NeedKey method is used to determine whether or not the currently focused control will provide the only handling for the key event. If NeedKey handles the event (by returning true), the control’s key event handlers are called (e.g., OnKeyPress, OnKeyDown, etc). NeedKey takes three parameters including the integer value of the key, the shift state (whether special keys such as Ctrl or Alt were held down at the time of the keypress), and the text of the key pressed. function NeedKey(Key: Integer; Shift: TShiftState; const KeyText: WideString): Boolean; dynamic;
If NeedKey returns false, a menu accelerator that corresponds to the key is executed (if one exists). If an accelerator is not found, the parent form’s WantKey method is called. This begins a search for any controls that implement a WantKey method and calls each of them until the event is handled. If no WantKey methods handle the event by returning true, control is passed back to the initial control whose key event handlers are then called. WantKey is a method that allows controls to register that they would like to respond to the key event. This allows other controls to respond to process another control’s key events. Keep in mind that the WantKey method of other controls is only called if the NeedKey method of the control in which the event occurred returns false. function WantKey(Key: Integer; Shift: TShiftState; const KeyText: WideString): Boolean; dynamic;
Warning: Like the NeedKey method, WantKey also returns a Boolean result. If it returns true, no other WantKey methods are called. The WantKey methods of a form’s controls are called in an order that is not guaranteed. Returning true in any WantKey method preempts other controls from responding to the event. It also keeps the original control’s key event handlers from executing.
Chapter 15: CLX
333
Figure 15-5 shows the flow of events related to the NeedKey and WantKey methods. Notice that if the initial control returns false, any other control (that has a WantKey method) can kill the event, stopping even the initial control’s key event handlers from firing.
Figure 15-5: The process flow of the NeedKey and WantKey methods
The following example demonstrates the NeedKey and WantKey methods. It creates two custom edit boxes, one that only accepts letters and one that only accepts numbers. unit frmMain; interface uses SysUtils, Types, Classes, QGraphics, QControls, QForms, QDialogs,
334
Chapter 15: CLX QStdCtrls; type TMyAlphaEdit = class(TEdit) private function IsAlpha(Key : integer) : Boolean; protected function NeedKey(Key: Integer; Shift: TShiftState; const KeyText: WideString): Boolean; override; function WantKey(Key: Integer; Shift: TShiftState; const KeyText: WideString): Boolean; override; end; TMyNumericEdit = class(TEdit) private function IsNumeric(Key : integer) : boolean; protected function NeedKey(Key: Integer; Shift: TShiftState; const KeyText: WideString): Boolean; override; function WantKey(Key: Integer; Shift: TShiftState; const KeyText: WideString): Boolean; override; end; TForm1 = class(TForm) lblAlpha: TLabel; lblNumeric: TLabel; procedure FormKeyPress(Sender: TObject; var Key: Char); procedure FormCreate(Sender: TObject); private AEdit : TMyAlphaEdit; ANumEdit : TMyNumericEdit; end; var Form1: TForm1; implementation {$R *.xfm} function TMyAlphaEdit.IsAlpha(Key: integer): Boolean; begin Result := (key in [Ord('a')..Ord('z')]) or (key in [Ord('A')..Ord('Z')]); end;
Chapter 15: CLX function TMyAlphaEdit.NeedKey(Key: Integer; Shift: TShiftState; const KeyText: WideString): Boolean; begin inherited NeedKey(Key,Shift,KeyText); Result := IsAlpha(key); end; function TMyAlphaEdit.WantKey(Key: Integer; Shift: TShiftState; const KeyText: WideString): Boolean; begin inherited WantKey(Key, Shift, KeyText); Result := not IsAlpha(key); end; function TMyNumericEdit.IsNumeric(Key: integer): boolean; begin Result := key in [Ord('0')..Ord('9')]; end; function TMyNumericEdit.NeedKey(Key: Integer; Shift: TShiftState; const KeyText: WideString): Boolean; begin inherited NeedKey(Key,Shift,KeyText); Result := IsNumeric(key); end; function TMyNumericEdit.WantKey(Key: Integer; Shift: TShiftState; const KeyText: WideString): Boolean; begin inherited WantKey(Key, Shift, KeyText); Result := not IsNumeric(key); end; procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char); begin ShowMessage(Sender.ClassName + ':'+Key); end; procedure TForm1.FormCreate(Sender: TObject); begin AEdit := TMyAlphaEdit.Create(Self); AEdit.OnKeyPress := Self.OnKeyPress; AEdit.Parent := Self; AEdit.Left := 20; AEdit.Top := 20; AEdit.Show;
335
336
Chapter 15: CLX ANumEdit ANumEdit.OnKeyPress ANumEdit.Parent ANumEdit.Left ANumEdit.Top ANumEdit.Show;
:= := := := :=
TMyNumericEdit.Create(Self); Self.OnKeyPress; self; 150; 20;
lblAlpha.Left := AEdit.Left; lblNumeric.Left := ANumEdit.Left; end; end.
TE
Graphics in vCLX
AM FL Y
The NeedKey methods of TMyAlphaEdit and TMyNumericEdit return the value of IsAlpha and IsNumeric respectively. These methods determine if the keystroke is appropriate for each custom edit box. Similarly, the WantKey method reverses the logic, thereby blocking all non-appropriate input. A final note about the NeedKey method: This method is called before the control’s state has been changed. In other words, if the WantKey methods of other controls want information about the current state of the initial control, they must be aware that the current state of the initial control is its state before the key was pressed.
Graphics can be a difficult area to master. Especially when a developer is coordinating the properties and events of the Kylix controls, the Qt library, and the X Window System, even simple tasks can be cumbersome. Fortunately, Kylix provides a number of objects whose properties, methods, and events ease the task of developing graphical applications.
Using the Canvas Like a painter’s canvas, the canvas object gives developers a space on which to draw and render images. The canvas object in Kylix maps to Qt’s QPainter object. Properties like Pen, Brush, and Font objects of the Windows device context (as well as some others) each have corresponding objects in QPainter. Similar functionality is found in the canvas in Delphi’s VCL. Warning: VCLX’s TCanvas object is not thread-safe. Multiple code segments running in different threads can access the same canvas object simultaneously. To protect against this, use the Lock and Unlock methods of the canvas. See Chapter 17 for more information.
Chapter 15: CLX
337
Located on the CD is a project called CanvasDemo.dpr. It demonstrates some of the features of drawing on a canvas. CanvasDemo allows for the selection of the color and style of the pen and the brush. The following section of code demonstrates the OnPaint event from this project, which draws on the canvas of the TPaintBox component. procedure TForm1.PaintBox1Paint(Sender: TObject); var i : integer; height : integer; midheight : integer; width : integer; midwidth : integer; tmpInt : integer; procedure DrawFlower( x, y : integer); var AdjX : integer; AdjY : integer; begin // centered on x,y so we need to adjust x and y AdjX := x - 25; if AdjX < 0 then AdjX := 0; AdjY := y - 25; if AdjY < 0 then AdjY := 0; PaintBox1.Canvas.Pie(AdjX, PaintBox1.Canvas.Pie(AdjX, PaintBox1.Canvas.Pie(AdjX, PaintBox1.Canvas.Pie(AdjX, PaintBox1.Canvas.Pie(AdjX, end;
AdjY, AdjY, AdjY, AdjY, AdjY,
50, 50, 50, 50, 50,
50, 50, 50, 50, 50,
0, 1152, 2304, 3456, 4608,
576); 576); 576); 576); 576);
procedure WriteMessage(x, y : integer; const msg : string); var msgwidth : integer; msgheight : integer; begin msgwidth := PaintBox1.Canvas.TextWidth( msg ); msgheight := PaintBox1.Canvas.TextHeight( msg ); // adjust x and y for the width and height of the text
338
Chapter 15: CLX x := x - (msgwidth div 2); y := y - (msgheight div 2); PaintBox1.Canvas.TextOut( x, y, msg); end; procedure DrawRect(x, y : integer); var rect : TRect; begin // now draw some rectangles.. rect.Left := x - 30; rect.Top := y - 20; rect.Right := rect.Left + 60; rect.Bottom := rect.Top + 40; PaintBox1.Canvas.FillRect( rect ); end; begin width := PaintBox1.Width; midwidth := width div 2; height := PaintBox1.Height; midheight := height div 2; if not bClearCanvas then begin // set the brush and pen properties to the selections PaintBox1.Canvas.Brush.Style := SelectedBrushStyle; PaintBox1.Canvas.Brush.Color := SelectedBrushColor; PaintBox1.Canvas.Pen.Style := SelectedPenStyle; PaintBox1.Canvas.Pen.Color := SelectedPenColor; // Draw some lines PaintBox1.Canvas.MoveTo(0,0); PaintBox1.Canvas.LineTo(Width-1,0); PaintBox1.Canvas.LineTo(Width-1,Height-1); PaintBox1.Canvas.LineTo(0,Height-1); PaintBox1.Canvas.LineTo(0,0); // Draw an "X" PaintBox1.Canvas.LineTo(Width-1,Height-1); PaintBox1.Canvas.MoveTo(0,Height-1); PaintBox1.Canvas.LineTo(Width-1,0);
Chapter 15: CLX
339
// Draw some circles for i:=1 to 10 do begin tmpInt := i * 10; PaintBox1.Canvas.Arc( midwidth-tmpInt, midheight-tmpInt, tmpInt*2, tmpInt*2, 0, 5760); end; // Draw a "Flower" DrawFlower(midwidth, midheight div 4); DrawFlower(midwidth, height - (midheight div 4)); // Place some text on the canvas WriteMessage( midwidth, height div 4, edtMessage.Text ); // now draw some rectangles.. DrawRect( midwidth div 4, midheight); DrawRect( width - (midwidth div 4), midheight); end else begin WriteMessage( midwidth, midheight, 'Press the Draw button'); bClearCanvas := false; end; end;
Using Linux Styles Linux embraces the concept of user interface “styles.” The style of an application or a widget controls how that object is drawn within the application. To utilize this concept in Kylix, the global application object as well as each widget has a Style property of type TStyle or one of its descendants. This type includes a number of properties that help define how controls are drawn within the application. TApplicationStyle = class(TStyle) private FRecreating: Boolean; FOnPolish: TPolishEvent; procedure PolishHook(p1: QApplicationH) cdecl; procedure SetOnPolish(const Value: TPolishEvent); protected procedure DoPolish(Source: TObject); virtual; procedure CreateHandle; override; procedure SetDefaultStyle(const Value: TDefaultStyle); override; procedure StyleDestroyed; override; public destructor Destroy; override;
340
Chapter 15: CLX property OnPolish: TPolishEvent read FOnPolish write SetOnPolish; end;
Notice the DefaultStyle property. This is a property that TApplicationStyle inherits from the TStyle class. The TDefaultStyle type includes a list of commonly used styles that can be automatically taken on by the application or by an individual widget. To change the default style of an application, create a style object and assign it to the DefaultStyle property of the Style property of the application (or the widget).
Figure 15-6: Using application styles
Figure 15-6 demonstrates the effects of using styles. The first form is in an application that has its DefaultStyle property set to dsMotif. The second form uses the dsQtSGI setting. This is an effective way to set the “look and feel” of an application. In addition to the TApplicationStyle type, there is also TWidgetStyle. TWidgetStyle also controls how objects are painted within the application; however, it is used to create a style for individual widgets (of course, a particular style can be created and dynamically assigned to the DefaultStyle property of a collection of widgets).
Using Common vCLX Controls Although the topics to this point have been somewhat complex, it is important to remember that the average Kylix application will probably never touch these issues. Kylix gives the developer great power but also wraps up functionality in easy-to-use objects. To become truly great at vCLX, developers must spend time getting to know vCLX controls. There are so many controls that it takes time and study to gain an intimate understanding of all of the things vCLX can do for you. With that said, this section discusses some of the more commonly used vCLX objects and what they can do for you.
Chapter 15: CLX
341
Forms Any GUI application is going to use forms of some kind. A form is a window that is used to display other controls and information to the end user. Like any other control, forms have a large number of properties, methods, and events that could not possibly be described in one chapter, much less this section. But there are some key elements that can help developers get a head start into using forms in applications. With that in mind, the following is a list of common form attributes. ActiveControl — Determines which control has the focus when the form is created. It is an enumerated property that contains a list of the controls owned by the form. Tip: Focus can be set to any control at run time by calling .SetFocus. However, this method will cause an error if the form on which the control is sitting is not visible. BorderIcons — A set type property that determines what icons are displayed on the form’s title bar. This is an important part of professional applications. Dialogs, for instance, should be considered carefully, whether they can be minimized or mazimized. BorderIcons can include the values in the following table: Table 15-1 BorderIcons Value
Description
biSystemMenu biMinimize biMaximize biHelp
A system menu appears when the form’s icon is clicked. The title bar contains a minimize button. The title bar contains a maximize button. The title bar contains a question mark button. When clicked, the icon changes to crHelp.
Note: On Linux, BorderIcons only requests that the Window Manager add or remove a button from a form. The Window Manager may refuse the request. Unlike in Windows, the non-client area of a window is totally controlled by the Window Manager. Hence, there is no way to control the painting of the non-client area of forms as there is in Windows. BorderStyle — Sets the appearance of the form’s title bar. It can be set to the following values: Table 15-2 BorderStyle Value
Description
bsDialog
The form is not resizable; standard dialog box border with only a close icon. The form is not resizable; single-line border.
bsSingle
342
Chapter 15: CLX BorderStyle Value
Description
bsNone
The form is not resizable; and does not include a border of any kind. This setting is often used for splash screens. Standard resizable border. This is the default setting. Similar to bsSingle but with a smaller title bar. Similar to bsSizeable but with a smaller title bar.
bsSizeable bsToolWindow bsSizeToolWin
Note: Certain settings of the BorderStyle and BorderIcons properties are mutually exclusive. For instance, if BorderStyle is set to bsDialog, minimize and maximize buttons will not be displayed, even if they are included in BorderIcons. ClientHeight/ClientWidth — Determines the working client area of the form. This does not include the form’s title bar or scroll bars; only the usable area of the form. To set the ultimate dimensions for the entire form, use the Height and Width properties. FormStyle — Sets the usage style of the form. FormStyle determines how the form behaves in the application. It can be set to any of these values. Table 15-3 FormStyle Value
Description
fsNormal fsMDIForm
The form behaves normally. This is the default setting. The form is used as an MDI frame window. MDI (multiple document interface) applications are those in which all of the forms (called child forms) are physically contained within a frame form. Older versions of Microsoft Word and other applications used this style. The form is an MDI child window, contained within a frame (fsMDIForm) window. The form is consistently the top window. This behavior will force the window to float on top of all other windows, except those that also have an fsStayOnTop setting. Among those windows, noone will remain consistently on top.
fsMDIChild fsStayOnTop
Menu/PopupMenu — Determines the menu or pop-up menu component that is associated with this form. Menus are discussed in the next section. Note that the PopupMenu property is not specific to forms. It is part of the declaration of the TControl class. Position — The Position property sets the initial position of the form upon creation as well as its size. It is a useful way to create uniformity across screen sizes and resolutions. It can be set to the following values.
Chapter 15: CLX
343
Table 15-4 Position Value
Description
poDesigned
The form is positioned at the location specified in the designer and with the height and width set in the designer. This is the default setting. Keep in mind that the position of a form will be different with different resolutions. The form’s position and size is determined by the operating system. The form’s size is set to its design-time specification, but the operating system chooses its position on the screen. The form appears in the position specified at design time, but the size is chosen by the operating system. The form remains the size specified at design time, but is positioned in the center of the screen. Note that because CLX does not support multi-monitor applications, a form with this setting may not fall entirely on one monitor. The form remains the size specified at design time, but is positioned in the center of the screen. The form remains the size specified at design time, but is positioned in the center of the application’s main form. This position should only be used with secondary forms. If this setting is used for a main form, it behaves like poScreenCenter. The form remains the size specified at design time, but is positioned in the center of the form specified by the Owner property. If the Owner property does not specify a form, this position acts like poMainFormCenter.
poDefault poDefaultPosOnly poDefaultSizeOnly poScreenCenter
poDesktopCenter poMainFormCenter
poOwnerFormCenter
WindowState — Determines the state of the window. Windows can be created with their normal size (set in the designer), or in a minimized or maximized state. Some applications will create the main form with a wsMaximized setting, enabling the form to take over the entire screen. Other forms, such as visual logs and progress indicators, may be created with a wsMinimized setting. These are only a few of the properties used with forms. Developers should spend time experimenting with various settings to achieve a desired result.
Menus The first thing to note about menus is that they are actually not controls at all. They are non-visual components. Even though menus produce a visual representation, the menu component itself does not show up at run time. Fortunately, menus are a pretty simple concept. They tend to have similar functionality in most applications. The key to menus is understanding the menu editor. Menus come in two flavors: MainMenu and PopupMenu. MainMenus are the main menus associated with a given form. PopupMenus are right-click or context menus. Fortunately, the editor for the two is used virtually identical.
344
Chapter 15: CLX
Note: Menus are made up of a collection of menu items. However, the menu items are not owned by the menu. They are owned by the form just as the other components are. TForm1 = class(TForm) MainMenu1: TMainMenu; mnitmFile: TMenuItem; mnitmNew: TMenuItem; mnitmOpen: TMenuItem; mnitmSave: TMenuItem; mnitmExit: TMenuItem; mnitmEdit: TMenuItem; mnitmCut: TMenuItem; mnitmCopy: TMenuItem; mnitmPaste: TMenuItem; mnitmHelp: TMenuItem; mnitmAbout: TMenuItem; end;
The Menu Editor Drop a MainMenu component onto a form and double-click it or select the Items property to bring up the menu editor. When the menu editor appears, it is initially blank with a single box focused. This represents the first menu item. Set the menu item’s Name to mnitmFile and the Caption to File.
Figure 15-7: Creating a main menu with the menu editor
Each time a menu item is created, the editor provides a new menuitem box beneath the last one entered. In addition, there is a menuitem box to the right of the current category for creating new categories. Even when the menu is complete, the editor will show additional menu item placeholders. These are optimized out by the menu. Continue creating items until the following menu is created: File|New File|Open File|Save File|Exit
Chapter 15: CLX
345
Edit|Cut Edit|Copy Edit|Paste Help|About
Figure 15-8: A completed menu
Although this menu is considerably more complete, there are a number of things missing that production menus typically include. The following section describes these common menu elements and how they are created.
Menu Elements Note: The great thing about menus is that they are really easy to create. There are a finite number of common menu item attributes, and Kylix makes them all fast and simple. Accelerator keys — Accelerator keys enable the menu to be navigated from the keyboard. Each menu item contains a single letter in its caption that is underlined. Pressing Alt and that letter executes that menu item. To specify an accelerator, precede the intended letter with an ampersand (&) character. To access the New item via the letter “N”, type its caption as &New. That’s it! The actual execution of accelerators is taken care of for you by Kylix. Shortcuts — Some menu items are used so frequently that it is helpful to be able to execute them without navigating the menu. The Save operation, for instance, is commonly executed by pressing Ctrl+S. Kylix menu items contain a ShortCut property that specifies a collection of common key combinations. Once the ShortCut has been set for a menu item, the task is complete. If the user presses the correct key combination while the form has focus, that menu item is executed.
Figure 15-9: Setting menuitem shortcuts
Chapter 15: CLX
Tip: The list of valid ShortCuts is not limited to Ctrl+. There are a large number of combinations in the editor list. However, even this is not a complete list. The ShortCut property can be set to any key combination by typing ++ …. Even single letters can be used, though this is not recommended. If the letter “S” is used for save, then the menu item will be executed every time an S is typed anywhere on the form!
AM FL Y
Submenus — Menus often contain subcollections of menu items, known as submenus. The Open menu might be used to open a variety of different things in an application. To create a submenu of items that can be opened, right-click on the Open menuitem in the editor and select Create Submenu. This creates a new menu to the right of the item selected. The item selected now contains a small arrow showing that there is a submenu. Submenu items are no different than any other menu item and are created in the same way.
Figure 15-10: Creating a submenu
TE
346
Notice that in Figure 15-10, the Report item is followed by three periods. This is known as an ellipsis. Menus use the ellipsis to alert the user that the menu item leads to a dialog of some kind. Using an ellipsis is a standard rather than a requirement. There is no trick to creating an ellipsis; simply add three periods to the end of the menu item’s caption. Tip: Another commonly used attribute of menu items is the Checked property. This Boolean property adds a check to the left of the menu item. Separators — In larger menus, it is helpful to group menu items by their functionality. A clear indicator of groups is shown through menu separators. For instance, in the Edit category, there are operations that are grouped together such as Cut, Copy, Paste and Undo, Redo. To separate them with a single, horizontal bar set the caption of a new menu item to a single dash ( - ).
Chapter 15: CLX
347
Figure 15-11: Grouping menu items
Images — Menu items usually correspond to buttons on a toolbar. It is helpful to have the buttons and the menu items use the same image. Each menu item can be assigned a separate image, but it is much more common to use an ImageList component. The ImageList component holds a collection of images for use by components. That way, the toolbar and the menu don’t load two copies of the same image. Once an ImageList is placed on the form, the image editor is accessed by double-clicking the ImageList component. Use the Add button to add images to the list.
Figure 15-12: The ImageList editor
Notice that the images each contain an image index. Once the ImageList is connected to the menu, each menu item can select an ImageIndex to determine which picture is used with that menu item. To associate a menu with an image list, set the Images property of the main menu (not the individual menu items).
348
Chapter 15: CLX
Figure 15-13: Menu images
Menu Templates Because menus commonly follow standards, Kylix allows menu templates to be created with predetermined menu items. To use a menu template, right-click the menu editor and select Insert from Template. A list of menu templates is shown. When a template is selected, the appropriate menu items are automatically created by Kylix. Figure 15-14 shows a menu that was implemented by simply dropping a menu onto the form and selecting a pre-created template. This is a great time-saver for developers and helps to promote cohesive menu standards.
Figure 15-14: Using a menu template
Templates of existing menus can be created by right-clicking the menu editor and selecting Save as Template.
Chapter 15: CLX
349
Toolbars A good application is easily navigated and provides functionality in a convenient and easily accessible way. Toolbars play a big role in providing functionality to the user at the click of a button. The Kylix toolbar component maintains a collection of ToolButtons as well and can also contain other controls. A toolbar, for instance, may include a drop-down list of valid fonts or colors for use within the application. Tip: To create more complex toolbars with varied controls, check out the CoolBar component. Information on the CoolBar can be found in the Kylix help files. Drop a toolbar onto the form from the Common Controls page of the component palette. To add buttons and separators, right-click the toolbar and select the appropriate menu item.
Figure 15-15: Creating a toolbar
Each toolbutton on the toolbar has its own set of properties that are accessible through the designer. The toolbar itself includes a number of properties to create the basic look and feel of the toolbar. Following is a description of some of the toolbar’s most notable attributes. AutoSize — Determines if the toolbar automatically resizes its height to accommodate its currently held controls. Bitmap — Sets a background bitmap for the entire toolbar. The bitmap is not shown on the toolbar’s child controls, only on the toolbar background.
350
Chapter 15: CLX
ButtonHeight/ButtonWidth — Sets the uniform height and width of buttons on the toolbar. This property is set both through the toolbar and through the toolbuttons. Setting the Width of any button automatically adjusts the ButtonWidth of the toolbar. Flat — Flattens the toolbuttons. When the cursor is passed over a button, it temporarily “pops up” and then goes flat again when the cursor moves away. HotImages, Images, DisabledImages — These properties can be used to connect the toolbar to an ImageList component. A toolbar may be connected to more than one ImageList at the same time. When the cursor is not over the toolbar, the toolbuttons display the images used by the ImageList in the Images property. As the cursor passes over each button in the toolbar, the button changes its image to the one used by the ImageList referenced by the HotImages property. This can be a great way to provide a more exciting interface. ShowCaptions — Determines whether the captions of toolbuttons are shown. This does not affect the display of toolbutton images. Captions are shown beneath the toolbutton’s image. The ButtonWidth property is influenced by the captions. It is set to the width necessary to display the longest caption. Wrapable — Remember that toolbars hold more than just buttons. They can hold any vCLX control. The Wrapable property forces components to line wrap when they do not fit horizontally within the toolbar.
A Quick Note about ToolButtons Toolbuttons can be set to more than just click like regular buttons. TToolButton includes the Style property that can be set to a number of values. If the toolbutton’s Style property is set to tbsDropDown, the toolbutton changes to display a small arrow next to the main body of the button. The toolbutton has a DropDownDisplay property that refers to a PopupMenu component. When the application is run, users can either click the button normally, or they can click the arrow, which exposes a drop-down menu for a more detailed selection. For instance, the Open button (which has a submenu in the main menu system) should also be able to display a sublist from the toolbar. The figure below is created by following the steps mentioned in this paragraph.
Figure 15-16: Creating drop-down toolbar options
Chapter 15: CLX
351
vCLX Tricks The sophistication of vCLX makes a number of “little goodies” available to the developer. From adding small user interface elements to creating entire architectures, vCLX offers a massive collection of functionality. This section describes only a few of those elements. For more information on what can be done with vCLX — PRACTICE, PRACTICE, PRACTICE.
Implementing Drag and Drop Part of creating a usable application lies in making tasks convenient for the user. Rather than “click here, select there, click here” to transfer information between different parts of the application GUI, it is much easier for users to be able to drag and drop elements between controls, forms, etc. It turns out that implementing drag and drop in Kylix applications is quite simple. There are three things to consider when programming a drag and drop operation: n
When the drag operation should begin (usually when the user holds down the left button of the mouse).
n
As the object is being dragged over other controls, each control must decide whether or not it will “accept” the dragged control if the user should attempt to drop it there.
n
What action to take when a control is dropped.
Let’s say an employee application contains an administration section where, among other things, new employees can be added. Each new employee must be assigned a valid manager, team lead, and other information. Rather than the user typing in all of the information, the application allows the user to drag names from a valid list onto the form. This is a great time-saver and eliminates data entry mistakes. To add a manager to the employee’s file, the user must drag the correct manager from the manager list to the content pane of the form.
Beginning the Drag Operation Drag operations typically begin when a user presses down the left mouse button over an item that the application has determined can be dragged. As such, it is common to begin a drag operation in the OnMouseDown event of whatever control needs to be dragged. However, while this is convention, it is by no means a rule. An application might begin a drag operation automatically when an item is selected and not require the user to hold down the mouse button continuously. procedure TfrmNewEmployee.lstbxManagerMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
352
Chapter 15: CLX begin if(Sender is TListBox) and (Button = mbLeft) then (Sender as TListBox).BeginDrag(false); end;
To begin a drag operation, use the BeginDrag method. BeginDrag is declared in the TControl class and takes two parameters: Immediate and Threshold. Immediate determines if the drag operation begins immediately. In this case, the method is called in the OnMouseDown event. But what if the user simply selects an item and doesn’t want to drag it? Setting Immediate to false delays the begin of the drag operation until the mouse is dragged a certain number of pixels, determined by the Threshold property. Threshold defaults to –1, in which case the distance is set by the global mouse’s DragThreshold property. Tip: Notice that instead of using lstbxManager.BeginDrag(false), the method uses the more generic (Sender as TListBox).BeginDrag(false). Keeping in mind that there will be several list boxes in this application from which the user can drag items onto the screen, this event handler is written to be used by all of them.
Accepting/Rejecting Dragged Controls When the selected item is dragged over other controls on the form, each control must decide whether it will “accept” the dragged object if it is dropped within the control’s boundaries. There are many reasons why a control might accept or not accept another control. This can be determined by the name, type, location, or parent of a control and by countless other things. In the accepting control’s OnDragOver event, a decision must be made as in the following code. procedure TfrmNewEmployee.edtManagerDragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); begin if (Source is TListBox) then Accept := ((Source as TListBox).Parent = pnlDraggableInfo); end;
OnDragOver takes a number of parameters describing the circumstances under which the event was fired. Three new parameters are worthy of pointing out. Source determines which control is being dragged (the source of the operation). State is of type TDragState. State gives a more precise view of the operation. It can be one of three values.
Chapter 15: CLX
353
Table 15-5 Value
Description
dsEnter dsMove dsLeave
The mouse just entered the boundaries of the control The mouse is moving over the control The mouse just left the boundaries of the control.
Finally, the Accept parameter is used to specify the control’s decision. Notice that it is passed by reference. That means that whatever value it holds at the end of this procedure is permanently applied to it. In this case, Accept is set to true if the Source is a ListBox and is sitting on the panel with all the other draggable ListBoxes. Tip: The Sender parameter often confuses developers when implementing drag and drop. Keep in mind that Sender specifies what component caused the event to occur and is not specific to a drag operation. It does not determine “who is sending the drag.” That duty is reserved for the Source parameter, i.e., “the source of the dragged object.”
Taking Action When a Control is Dropped Finally, when the object is dropped, the receiving control must decide what to do with it. This occurs in the OnDragDrop event. procedure TfrmEmployee.edtManagerDragDrop(Sender, Source: TObject; X, Y: Integer); begin edtManager.Text := (Source as TListBox).Items[(Source as TListBox).ItemIndex]; end;
Of course, this is the area where business logic can take the application anywhere. In the case of the new employee, the information is transferred from the list box to the appropriate control.
Using Application Hints Hints are a great way for applications to provide quick help to users by displaying a message whenever the mouse is placed over a control. To provide a hint for a control, set the Hint property. Also, be sure to set the ShowHint property to true. Once a hint has been set, the hint will be displayed each time the mouse moves over the control. In addition to controls, the application object also has a Hint property. When a hint is displayed, it generates the Application.OnHint event. Many applications provide a status bar that shows the application’s current hint text at the bottom of a form. A common technique is to write a procedure that assigns the application’s current hint to the StatusBar’s SimpleText property during the Application.OnHint
354
Chapter 15: CLX
event. This is actually unnecessary. The StatusBar component includes an AutoHint property that automatically assigns the application’s current hint to the SimpleText property of the StatusBar. Note that the SimplePanel property must be set to true. Control hints and application hints do not always have to be identical. The same control can specify a single hint, part of which is intended as the control’s hint and part of which becomes the application’s hint when the control hint is displayed. To do this, add the text for both hints to the control’s Hint property with a pipe symbol ( | ) separating the two. Text to the left of the pipe is used as the control’s hint and text to the right of the pipe is used as the application’s hint whenever the control’s hint is displayed. The hint shown in Figure 15-17 was created by typing the following text into the Hint property of the copy toolbutton: Copy|Copy the selected text to the clipboard.
Figure 15-17: Mixing application hints and control hints
Actions and Action Lists Applications commonly include a number of controls that perform similar tasks. For instance, menu items often have corresponding toolbar buttons to make the operation easier for the user. When multiple controls connect to the same event, the common changes in state for each control must be set individually. Changes in state can include images, captions, etc. Instead of each control manually setting its own state and the state of other controls, they can use actions. Actions encapsulate a user command and the settings that should be applied when the command is executed. In Kylix, actions are grouped in action lists. The ActionList component contains a collection of actions that can be assigned to menu items and toolbar buttons.
Chapter 15: CLX
355
Figure 15-18: Using the ActionList component
The Cut, Copy, and Paste items appear both in the menu and on the toolbar. Using an ActionList allows us to control the images, enabled/disabled states, hints, and more for both of the controls. To use an action list, set the ActionList property of the menu and the toolbar. Then set the Action property of the individual menu items and toolbuttons to the corresponding actions. The advantage to this is that the settings of an action can be specified at one location, discouraging differences in the various controls that invoke the action. Once the menu item or the toolbutton has been associated with a particular action, the appropriate properties are populated automatically by the action. ActionLists include predefined categories of actions that make using actions much easier. For instance, navigating data requires the use of a collection of data set methods (first, prior, next, last, insert, etc.). ActionLists already define actions to deal with each one of those methods. All the developer has to do is associate them with a data set and the rest is taken care of.
Summary vCLX is a powerful component architecture, not only because of the functionality that it provides, but also because of the relative simplicity with which it is presented and handled in Kylix applications. vCLX abstracts issues related to the underlying operating system and to the Qt C++ library. While developers have the flexibility to augment the layers of functionality provided by the vCLX wrapper, they are insulated from having to do so in most situations by the translation provided by the DisplayCLX layer. This allows cross-platform applications to be developed faster and better by all levels of Kylix developers.
TE AM FL Y
Writing Custom Chapter 16 Writing Custom Components Components
Introduction Creating custom components can be a key to decreasing development time in an application. Once a piece of functionality has been internalized by a component, the developer does not have to recode that code every time the component is used. Creating objects that internally manage their own behavior is a great way to employ a large degree of code reuse as well as to thin out the non-business rule code in your applications. No tool makes it easier to create, install, and deploy components than Kylix. In this chapter, we look at the component building process and how it is used to create powerful components.
Why Create Components? Because components employ similar coding techniques used in other application code, the question is often asked, why build components? Creating custom components is a tremendous aid in providing clean code efficiently and consistently across an application or a set of applications.
Development Productivity Creating custom components increases development productivity dramatically. Say, for instance, that an application’s GUI requires a certain look and feel. Buttons and labels must all have a consistent font and all panels must use a particular border style. Although those properties are easily set in the Kylix IDE, think of the number of times they have to be set over the course of an application. Now think about how much faster it would be if components were dropped onto the form already having those values, and you’ll see why creating custom components provides a huge boost in productivity.
357
358
Chapter 16: Writing Custom Components
Consistency A good program or set of programs is one that works consistently, both in the look and feel of its GUI and the internal processes of its components. Creating custom components allows code to be executed consistently by the component and does not leave consistency to the agreement and adherence of a collection of developers to a standard. The same component will behave the same way every time.
A Whole New Audience It’s important to understand that component users and component writers look at components from two very different perspectives. Component users are typically application developers who place components onto a form or data module and set the component’s properties, methods, and events either programmatically or through the Kylix IDE (the Object Inspector). They cannot directly affect the internal behavior of the component but do so through a specified interface of properties and methods. Component users are generally concerned with how a component behaves at run time. Their target audience is the end user of an application. Component writers, on the other hand, have to manage a component’s interaction with both the end user and the component user. They must be concerned with a component’s run-time behavior but also must account for the component’s design-time behavior, that is, how it interacts with the IDE and the developer. Although Kylix takes care of many of the details of the interaction between a component and the IDE, the component writer must still be concerned with things like registering and installing the component, as well as creating any design-time editors. They are responsible for defining the component’s interface, implementing the internal behaviors of the component, and determining how and when it responds to user or system events.
Component Properties Although properties seem trivial to the component user, they are a little more complex to the component writer. Properties are used like any other variable but are actually composed of three different parts: storage identifiers, property declarations, and accessor methods. As an example, suppose that you have developed the IDLabel component, a label that has an additional property called LabelID that holds an integer value.
Chapter 16: Writing Custom Components
359
Tip: A property like the LabelID property already exists in every component. It is called the Tag property and is an integer that does nothing. It was placed into the hierarchy as an additional hook by which component users can distinguish a particular component. Because it is a 4-byte value, it can be used to hold any 4-byte variable, such as an integer, a character, or a pointer to an object.
Storage Identifiers Storage identifiers are private instance variables that hide the actual data from the outside world. Because they are declared in the private section, they can only be accessed from within the component class itself and not by users of the component. type TkdIDLabel = class(TLabel) private FLabelID : Integer; { … }
Property Declarations At the other end of the component property architecture is the property declaration. Property declarations have the following format property : [read clause] [write clause];
In the case of the LabelID property, the declaration would look like this: property LabelID : Integer;
Tip: This is a great place to use the class completion feature discussed earlier in the book. After declaring the property, simply right-click anywhere in the class and select “Complete class at cursor.” In fact, the storage identifier doesn’t even have to be declared. The Kylix IDE can determine from the property declaration all of the necessary parts of the property.
Accessor Methods To access the component’s property values, methods called accessor methods are used to set and retrieve the value of a storage identifier. Accessor methods may have any valid method name, but are commonly named and referred to as a get/set pair or an accessor and a mutator. For example, the TEdit component’s Text property actually refers to the private field FText whose value is retrieved through its accessor methods. Kylix takes care of calling these methods when a property value is assigned or requested and abstracts their use from the developer.
360
Chapter 16: Writing Custom Components
But why all the trouble? Why use private storage identifiers for a property that may eventually be declared as public? Information hiding is an important concept in creating objects. When we define properties in this manner, we are able to employ the concepts of the “Black Box” theory. You’ll remember that the “Black Box” theory says that as long as the inputs and outputs of an object remain constant, the requesting object does not care how the inputs and outputs are managed (or stored). Originally, the LabelID property was stored internally as an integer. Although the use of the LabelID property should be restricted to integers, subsequent versions of the component include functionality that requires the LabelID value to be used internally as a string. This functionality, then, requires frequent casts of the value from an integer to a string. Eventually, you decide that it would save a lot of code to change LabelID’s type to a string. If you are using a public variable instead of a formally declared property, changing its type from integer to string will break all the code that uses the property. Because programmers were assigning integers to the property (which is now a string) in their code, the applications will no longer compile. type TkdIDLabel = class(TLabel) private FLabelID:Integer; function GetLabelID: Integer; procedure SetLabelID(const Value: Integer); { Private declarations } protected { Protected declarations } public { Public declarations } published { Published declarations } property LabelID:Integer read GetLabelID write SetLabelID; end; procedure Register; implementation procedure Register; begin RegisterComponents('Samples', [TkdLabelID]); end; { TkdLabelID } function TkdIDLabel.GetLabelID:Integer;
Chapter 16: Writing Custom Components
361
begin Result := FlabelID; end; procedure TkdIDLabel.SetLabelID(const Value: Integer); begin FLabelID := Value; end;
On the other hand, let’s say that you’ve used fully implemented properties. Listed below is the new and improved TkdIDLabel class. Notice that the storage of the FLabelID field has changed from integer to string; however, the declarations of the accessor methods have not changed nor has the actual type of the property. From the component user’s perspective, this component is exactly the same as when it was first created. Clients of the component still pass in and receive back integers for the LabelID field. type TkdIDLabel = class(TLabel) private FLabelID: String; // This variable was formerly an Integer { Private declarations } protected { Protected declarations } // accessor method declarations haven’t changed. function GetLabelID:Integer; procedure SetLabelID(const Value: Integer); public { Public declarations } published { Published declarations } // Property declaration hasn’t changed. property LabelID:Integer read GetLabelID write SetLabelID; end;
Notice the implementation of the accessor methods. They are performing an internal cast on the inputs and outputs to transform values to and from strings to integers and back. This allows the component to receive and deliver the user input in whatever format is consistent, yet store the value in a way that is advantageous to us (in this case, to avoid unnecessary casts). function TkdIDLabel.GetLabelID: Integer; begin Result := StrToInt(FLabelID); end;
362
Chapter 16: Writing Custom Components procedure TkdIDLabel.SetLabelID(const Value: Integer); begin FLabelID := IntToStr(Value); end;
Tip: In addition to information hiding, accessors and mutators are a great place to perform validations and invoke side effects (when setting one property affects another or has some additional influence on the component). Note: The class completion feature of Kylix declares accessor methods in the private section of the component class. Many people feel that the protected section of the class is a better place to declare these methods. That way, component writers who descend from the component can override or overwrite the methods for their own uses.
Component Events The last chapter described the Qt event model and how it interacts with Kylix. This chapter focuses on the much more practical use and creation of events, as they apply to custom components.
Event Architecture from 10,000 Feet Although events are thought of as a totally different beast than properties, it turns out that they are more similar than most people realize. Just like properties, events have storage identifiers, declarations, and accessor methods (though, with events they are called dispatch methods). An event is basically anything that happens within the system. The event can be generated by the operating system, by the application, or from within the component itself. Customizing the use of events is a great way to implement any needed behavior or side effects within a component.
Storage Identifiers Event handlers are methods that are used to respond to an event. The Kylix IDE generates event handlers for developers when a particular event is double-clicked in the Object Inspector. But how does the component store and fire the event handler? How do parameters to the event handler get populated? To properly understand events, let’s look under the component covers to understand how events are seen from inside a CLX component. It turns out that event handlers are actually properties, but they are a different kind of property than we are used to. Properties typically hold a reference to a particular piece of data or a particular object. They can, however, also refer to a method (that is, the first instruction of a method). Such an identifier is called a method pointer and is declared in the following way:
Chapter 16: Writing Custom Components
363
type TNotifyEvent = procedure(Sender:TObject) of object;
Notice that a method pointer is a type that explicitly declares the signature (the type and parameters) of a method. There is no name because this is not a procedure, simply a declaration of a procedural type. The TNotifyEvent is a method pointer that is widely used in CLX. Any event handler that accepts one parameter of type TObject is a TNotifyEvent. An event handler, then, is actually an internal component variable that holds a reference to a method that fits a given signature. In the case of the TkdIDLabel component, the OnClick event is implemented in the following way (additional component code not related to events has been omitted for this section): type TkdIDLabel = class(TLabel) private { Private declarations } FOnClick:TNotifyEvent; protected { Protected declarations } procedure Click;dynamic; public { Public declarations } published { Published declarations } property OnClick:TNotifyEvent read FOnClick write FOnClick; end;
Like the property, the OnClick event is made up of a storage identifier, a dispatch method, and a declaration. The storage identifier is a declared method pointer, TNotifyEvent. When the developer double-clicks the event in the Object Inspector, a method is added to the class and a reference to that method is stored in this identifier within the component.
Dispatch Methods TkdIDLabel also includes a protected dispatch method called Click. The job of the Click method is to execute the assigned event handler (if one has been assigned by the developer). It is the mechanism by which the component “fires” the event. Dispatch methods are not strictly necessary, as events can be fired in methods unrelated to the event itself, but a good component writer will provide a dispatch method for each event so that inheritors of the component in question can easily fire the event when needed. Remember to declare these methods with at least a protected scoping level if the method is intended to be overridden in component descendants. Notice that the dispatch method is declared as protected and with the dynamic modifier (remember that the dynamic modifier is functionally equivalent to the
364
Chapter 16: Writing Custom Components
virtual modifier). This is done because dispatch methods are often overridden in components’ descendants. Dispatch methods are a great place to introduce validations and side effects that may arise upon the execution of a certain event. Although the Click method has some subtle differences, a basic dispatch method for the OnClick event might look like this: procedure TkdIDLabel.Click; begin inherited; if Assigned(FOnClick) then FOnClick(Self); end;
In this case, the dispatch method simply checks to see if anything has been assigned to the event’s storage identifier. In other words, if the developer has written an event handler for the component, execute it. For a procedure pointer type, simply referring to the identifier executes the associated method. Notice that Self is passed to the FOnClick method. Did you ever wonder how the Sender parameter of the OnClick method gets populated? Well, here’s your answer! The dispatch method must pass the correct parameters to the event handler just as any other caller would have to do. Tip: A couple of things must be noted about the preceding example. First, the OnClick event for Labels is not declared in the TLabel class. It is actually declared much further up the hierarchy in the TControl class. This, of course, means that the TLabel class inherits it along with the other functionality that was declared along the way. In addition, TControl’s OnClick property is declared in the protected section of the class. Its scope is raised to the published level in the TLabel class. The actual implementation of the Click method does a few other things in addition to what is shown, but these things were left out for brevity.
Property Declarations The final piece of a component’s event structure is the property declaration. Remember that in many ways, events and properties are the same thing. The declaration of an event is the same as a property declaration. An event is declared by using the word property followed by the event name (which is commonly prefaced by the word “On”) and type, and finally by the read and write clauses and any necessary modifiers, similar to the following: property OnClick: TNotifyEvent read FOnClick write FOnClick stored IsOnClickStored;
Chapter 16: Writing Custom Components
365
Tip: stored is an optional directive that dictates whether a property’s value is streamed out to the .xfm file.
Creating Custom Events An event could be defined as anything that could happen to a component. Events are not always generated by the operating system or by the application but could also be generated by the component itself. Component writers can add custom events to a component in order to provide hooks to developers that allow them to insert reactionary code to the component. Suppose, for instance, that a new event must be created to notify the user (the developer) when the new LabelID property is changed.
Defining the Event The first step is to declare the new event. As stated previously, event declarations are written this way. Notice that both the read and the write clause of the event refer directly to the storage identifier, instead of using an accessor method. Unlike properties, this is a common structure for event-type properties. property OnLabelIDChange:TNotifyEvent read FOnClick write FOnClick;
The storage identifier itself is declared the same as any other variable. It is declared in the private section and usually is named the same as the event but is prefaced by the letter “F”. private FOnClick: TNotifyEvent;
The storage identifier for an event is of the same type as the declared event. The type can be any valid method pointer type. The type determines what event handlers can be assigned to that event as well as the parameter list that must be sent when the event is fired.
Firing the Event (Capturing Events) Once the new event has been declared, the component writer must determine when the event is fired. The dispatch method is used to fire the event handler at a time specified by the component developer. In this case, the dispatch method is called ChangeLabelID and is implemented this way. The method checks to see if an event handler is defined for the event and, if so, fires the event handler. procedure TkdIDLabel.ChangeLabelID; begin if Assigned(FOnLabelIDChange) then FOnLabelIDChange(Self); end;
366
Chapter 16: Writing Custom Components
Note: The dispatch method can be used for a variety of other tasks in addition to calling the event handler. Any side effects or validations can be called here either before or after the execution of the event handler. It is a good idea to declare the dispatch method in the protected section of the class and to declare it with the dynamic or virtual directive. This allows for maximum flexibility with regard to the augmentation of component behavior by descendants. Warning: There are places in CLX where the dispatch method of an object is declared in the private section. Many developers consider this a big mistake on the part of the CLX architects because it limits the extensibility of the components!
AM FL Y
Once the event has been added, component users (developers) can use the TkdIDLabel component as they would any other. The new event (OnChange) is displayed in the Object Inspector with the component’s other events. The completed TkdIDLabel component can be found on the companion CD. Of course, there are many variations that can be used when implementing events, but the purpose of this section was to explain and demonstrate basic event architecture.
Events in Kylix (vCLX)
TE
As Chapter 15 described, the event model of vCLX abstracts the program’s interaction with the operating system and, in effect, makes Qt the operating system. Kylix does allow developers to interact with hooks, signals, and slots to affect the behavior of a component. In addition, developers may choose to wrapper Qt objects directly in their components. As Chapter 15 mentioned, this requires Qt licenses to be purchased from TrollTech.
Using Custom Classes The CLX hierarchy includes a number of custom classes that allow for easy creation of “close, but not quite” components. Suppose that you want to create a standard panel component that will be used across forms and/or applications. Because you want a consistent look and feel, you set out to create a component that does not give developers the ability to customize the panel’s edges (BevelInner, BevelOuter, BevelWidth, BorderWidth, etc.). The trouble is that the new component is “close, but not quite” a panel. It would be nice to reuse most of the functionality already implemented by TPanel; however, the Kylix object model does not allow a component to selectively determine what it inherits from its ancestor. If the component is a descendant of TPanel, it will inherit all of TPanel’s bevel and border related properties. On the other hand, if the
Chapter 16: Writing Custom Components
367
component is descended from TControl or TWidgetControl, the majority of the TPanel class’s functionality will have to be reimplemented. To deal with this problem, the CLX architecture uses a number of custom classes. A closer look at the TPanel class reveals that it actually descends from TCustomPanel. TCustomPanel contains all of the properties that distinguish a panel; however, they are declared as protected properties. Recall that protected properties are inherited by subclasses and are available to the component developer but are not available to the component user (the developer). Custom classes allow component developers to expose whatever functionality is required by descendant classes.
Exposing Hidden Properties and Events Custom classes allow us to pick and choose the functionality that we want exposed in components. CLX allows a component member’s scope to be raised in a descendant class (scope can be raised but not lowered). Looking at the following TMyPanel declaration, we see that the scope of the listed properties has been raised to the published level. The component is not required to formally redeclare the property (including its modifiers), only to declare the property itself in its new scoping designation. type TMyPanel = class(TCustomPanel) private { Private declarations } protected { Protected declarations } public { Public declarations } published { Published declarations } { Notice that the BevelInner, BevelOuter, BevelWidth, and BorderWidth properties are not exposed. } property property property property property property property end;
Alignment; BorderStyle; BorderWidth; Caption; Color; Font; OnClick;
368
Chapter 16: Writing Custom Components
Tip: Another good example of a custom class is the TCustomForm class. Suppose a new type of form must be created but should not change its BorderStyle or BorderIcons properties. If the new class descends from TForm, it automatically inherits those properties. On the other hand, if it extends the TCustomForm class, you can simply pick and choose which properties and events you want to expose in the new form.
Components that Own Other Components Kylix gives you the power to build more complex, “super components” that are actually comprised of several smaller components. Super components can be as simple as a label and an edit box or can cover an entire section of a user interface (though, of course, components don’t have to be visual in nature). To build a super component, the developer must consider a number of issues including: n
What are the container relationships between the visual controls?
n
What properties, methods, and events of the internal components should be exposed to the super component’s users?
Setting the Owner/Parent All components have a constructor that takes the owner (another component) as a parameter. The owner is the component that is responsible for making sure that an object (namely, the component being instantiated) gets destroyed. If an internal component is being created within a super component, its owner can be set to be the same as the new component by passing through the AOwner parameter value in the constructor. More commonly, the new component owns any components that are created internally. To set the new component as the owner of any internals, simply pass Self (which refers to the outer component) to whatever is being created internally. constructor TkdDialogPanel.Create(AOwner: TComponent); begin { … } // TkdDialogPanel is the owner of the dialog buttons. FButton1 := TBitBtn.Create(Self); FButton2 := TBitBtn.Create(Self); { … } end;
When you are creating a component that owns controls (visual components), it is important to remember that controls must have a parent, that is, a visual container. When controls are dropped onto the form designer in the Kylix IDE, the parent property is set for you to the container in which the control is placed. As a component writer, remember to explicitly set the parent of any internal controls.
Chapter 16: Writing Custom Components
369
constructor TkdDialogPanel.Create(AOwner: TComponent); begin { … } FButton1 := TBitBtn.Create(Self); FButton1.Parent := Self; FButton2 := TBitBtn.Create(Self); FButton2.Parent := Self; { … } end;
The Loaded Method Components that refer to other components or to the owning component may execute poorly or not at all if their creation and initialization code is written in the constructor. This is because during the execution of the owner’s constructor, the owner has not yet finished being created, so internal components that make assumptions about its state may be “disappointed.” If an internal component must access its owner or another internal component when the owner is created, it should override the Loaded method. The Loaded method is declared in the TComponent class and is called after a component has completed the instantiation process (after the component has been completely read in from the XFM and the constructor has finished executing). The loaded method will not execute until the new component’s constructor has finished executing (the constructors of the internal components are normally called in the new component’s constructor). constructor TkdDlgPanel.Create(AOwner: TComponent); begin inherited; FButton1 := TBitBtn.Create(Self); FButton1.Parent := Self; FButton1.Name := 'kdButton1'; FButton2 := TBitBtn.Create(Self); FButton2.Parent := Self; FButton2.Name := 'kdButton2'; FButton1.Kind := bkCustom; FButton2.Kind := bkCustom; // This code should be in the Loaded method! if ((FButton1.Kind bkCustom) or (FButton2.Kind bkCustom)) then UpdateButtonPosition else begin FButton1.Left := 10; FButton2.Left := 20 + FButton1.Width;
370
Chapter 16: Writing Custom Components end; end;
In the preceding code, the UpdateButtonPosition method is called only if the Kind property of the internally owned buttons is set to something other than the default bkCustom. However, the default value of the Kind property has just been set to bkCustom. So in this case, the UpdateButtonPosition method will never be called! The problem here is that the value set by the developer in the Kylix IDE (through the Object Inspector) is stored in the .xfm file. The values from that file are streamed in after the constructor is finished executing. Moving the code to the Loaded method will ensure that the buttons have been created and initialized fully when their state is accessed. procedure TkdDlgPanel.Loaded; begin if ((FButton1.Kind bkCustom) or (FButton2.Kind bkCustom)) then UpdateButtonPosition else begin Fbutton1.Left := 10; Fbutton2.Left := 20 + Fbutton1.Width; end; end;
Exposing Internal Component Members Now that the components have been assembled, the next step is to decide what members of the internal components should be exposed through the new component. In the DialogPanel class, it makes sense to expose the Kind property of each of the internal components (the BitButtons) to the user. Start by creating two virtual properties. By virtual, we mean properties that are not stored explicitly by the owning component, but rather pass the values through to the internal components as in the following example. Tip: Super components are a great way to control a user’s access to functionality. If the component should have a set of states that is more restrictive than the individual components normally provide, create a wrapper component and expose whatever functionality is desired. type TkdDialogPanel = class(TPanel) private { Private declarations } FButton1 : TBitBtn; FButton2 : TBitBtn; procedure SetButton1Kind(const Value: TBitBtnKind);
Chapter 16: Writing Custom Components
371
procedure SetButton2Kind(const Value: TBitBtnKind); function GetButton1Kind:TBitBtnKind; function GetButton2Kind:TBitBtnKind; procedure SetButtonPosition; protected { Protected declarations } procedure Loaded;override; public { Public declarations } constructor Create(AOwner:TComponent);override; published { Published declarations } property Button1Kind:TBitBtnKind read GetButton1Kind write SetButton1Kind; property Button2Kind:TBitBtnKind read GetButton2Kind write SetButton2Kind; end; procedure Register; implementation procedure Register; begin RegisterComponents('Samples', [TDialogPanel]); end; { TkdDialogPanel } constructor TkdDialogPanel.Create(AOwner: TComponent); begin inherited; Width := 220; FButton1 := TBitBtn.Create(Self); FButton1.Parent := Self; FButton2 := TBitBtn.Create(Self); FButton2.Parent := Self; Button1Kind := bkCustom; Button2Kind := bkCustom; end; procedure TkdDialogPanel.Loaded; begin inherited; if ((FButton1.Kind bkCustom) or (FButton2.Kind bkCustom)) then UpdateButtonPosition; end;
372
Chapter 16: Writing Custom Components function TkdDialogPanel.GetButton1Kind: TBitBtnKind; begin Result := FButton1.Kind; end; function TkdDialogPanel.GetButton2Kind: TBitBtnKind; begin Result := FButton2.Kind; end; procedure TkdDialogPanel.SetButton1Kind(const Value: TBitBtnKind); begin FButton1.Kind := Value; end; procedure TkdDialogPanel.SetButton2Kind(const Value: TBitBtnKind); begin FButton2.Kind := Value; end; end;
Notice that there is not an FButton1Kind or FButton2Kind variable storage identifier. Also notice that the properties have both a get and a set method. The storage of the Kind values is already implemented for us by the TBitButton class. All that remains for us to do is to set and retrieve the Kind value from the internal buttons.
Components that Refer to Other Components In addition to components owning other components, they are often required to refer to another existing component. A MainMenu component, for instance, can include an image for each menu item. Rather than embedding each image into the menu, the component refers to a TImageList component (This allows images to be loaded into the application once and be used by multiple components like a menu and a toolbar.) A component property that refers to another component is no different from any other property. It follows the format of all properties, complete with private storage, accessor methods, and a formal property declaration. However, since the property itself is a pointer to another component, the programmer should code “defensively” and never assume that the pointer is valid. He should always check the validity of the pointer before using it.
Chapter 16: Writing Custom Components
373
The Notification Method When a component refers to another component, it is important that the referring component be aware of design-time events such as the removal of the referred to component. The TComponent class declares a protected, virtual method called Notification for just this purpose. Notification is commonly overridden in descendant component classes and takes two parameters: the component that is being affected and the operation that is being performed (whether it is being added or removed). This allows the referring component to add or clean up any references to the component and avoid access violations. A good example of the Notification method comes from data-aware controls. Every data-aware control has a DataSource property that refers to a datasource used to connect the control to a data set. Each control contains a Notification method that alerts it if the datasource to which it refers is removed in the designer. This allows the control to internally clean up the reference to the referred component. TDBEdit, for instance, contains the following notification method implementation: procedure TDBEdit.Notification(AComponent: TComponent; Operation:TOperation); begin inherited Notification(AComponent,Operation); if (Operation = opRemove) and (AComponent = DataSource) then DataSource := nil; end;
Another example of the notification method comes from TForm. When a main menu component is dropped onto a form, it is automatically assigned to the menu property of the form (as long as the form does not already have a main menu). The Notification method performs this validation and assignment. procedure TCustomForm.Notification(AComponent: TComponent; Operation: TOperation); begin inherited Notification(AComponent, Operation); case Operation of opInsert: begin { … } else if not (csLoading in ComponentState) and (Menu = nil) and (AComponent.Owner = Self) and (AComponent is TMainMenu) then Menu := TMainMenu(AComponent); end; opRemove: begin { … }
374
Chapter 16: Writing Custom Components end; end;
Building Data-Aware Components The first time they set out to create a data-aware component, most developers are surprised at how easy it is to connect components to data. A simple way to approach such a seemingly daunting task is to separate the data access from the presentation and other behavior. Just about everything we’ve learned to this point about components applies to data-aware components. The new task is to connect to and manage the data that flows through the component.
The TDataLink Class Fortunately, most of the data management is handled for us via the TDataLink class. TDataLink and its descendants encapsulate almost all of the functionality for connecting to a data set and for reading and updating data. Data-aware components fall into two categories: those that manipulate/display a data set and those that manipulate/display a particular field from a data set. The first type of component typically includes a published datasource property, and the second includes both a datasource and a datafield property. Let’s look at an example of the second type, which uses a descendant of TDataLink called TFieldDataLink. The TFieldDataLink class contains a collection of information which helps to bring data to the component including a TField object, a reference to the component that owns it, and a group of methods that are used for retrieving and updating data. To illustrate the use of the TFieldDataLink class, we will construct a TDBStatusBar component that displays the value of a single field in a data set. Begin by creating a new component, subclassed from the TStatusBar component, as shown in Figure 16-1. Add a private field (usually called FDataLink) of type TFieldDataLink.
Figure 16-1: Creating the TDBStatusBar component
Chapter 16: Writing Custom Components
375
type TkdDBStatusBar = class(TStatusBar) private FDataLink: TFieldDataLink; { … } end;
Adding a DataSource Adding a datasource to a data-aware component is a simple process. To begin, declare a published DataSource property, which is of course of type TDataSource (you will need to use the DB and QDBCtrls units). The DataSource property is a virtual property (similar to the BitButton’s kind property discussed in the “Exposing Internal Component Members” section ealier in this chapter), that is, its value is stored for you by the fielddatalink object declared previously. Now create a get/set pair of methods for the property. You do not need to declare an FDataSource variable to store the datasource. The DataSource property does not need to be stored within the class because it is already stored for you by the TFieldDataLink class. The accessor and mutator methods simply pass through the value from FDataLink. function TkdDBStatusBar.GetDataSource: TDataSource; begin Result := FDataLink.DataSource; end; procedure TkdDBStatusBar.SetDataSource(const Value: TDataSource); begin FDataLink.DataSource := Value; end;
Adding a DataField The next step is to add a published DataField property. This property is actually a simple string property that again passes its value through to the fielddatalink. Adding a DataField property is a simple matter of creating a new string property with accessors that update and retrieve the field name from the fielddatalink. When the FieldName property is set, the FieldDataLink object uses the field name to request and construct an appropriate TFieldObject that helps to govern the use of that field. function TkdDBStatusBar.GetDataField: String; begin Result := FDataLink.FieldName; end;
376
Chapter 16: Writing Custom Components procedure TkdDBStatusBar.SetDataField(const Value: String); begin FDataLink.FieldName := Value; end;
Controlling Behavior Now that the properties have been added, the particular behaviors of this component must be implemented with regard to changes in the data set. When the current data in the data set changes, the component must update itself accordingly.
OnDataChange
TE
AM FL Y
TFieldDataLink contains a number of events that may be dynamically assigned to component. The OnDataChange event occurs anytime the current data in the data set changes. This can occur for a number of reasons including scrolling to a new record, changing the data set that the component’s datasource refers to, or changing the state of the data set. To provide the correct changes to the component, write a method (typically a protected method named DataChange) that updates the component. Then, assign that method to the FDataLink’s OnDataChange event (normally this is done in the constructor). In order to be able to assign the DataChange method to the OnDataChange event, it must have the signature of a TNotifyEvent ( procedure (Sender:TObject) of object; ). In other words, it must be a procedure that takes a single parameter of type TObject. Typically, the DataChange event will display FDataLink’s value in the control. procedure TkdDBStatusBar.DataChange(Sender: TObject); begin SimpleText := (Sender as TFieldDataLink).Field.Text; end; constructor TkdDBStatusBar.Create(AOwner: TComponent); begin inherited; FDataLink := TFieldDataLink.Create; FDataLink.OnDataChange := DataChange; end;
Warning: Be careful of writing components that assume a particular state of a component. In the above example, the SimpleText property is set to the value of the data field. However, the value will only be displayed if the panel’s SimplePanel property has been set to true!
Chapter 16: Writing Custom Components
377
OnUpdateData The data-aware component must not only be notified of and respond to changes in data, but also be able to update the data set field with its new value. The OnUpdateData event allows the component to set the field object contained in the FieldDataLinkObject with its current value. In a similar manner to OnDataChange, the component defines an UpdateData method (also with a TNotifyEvent signature) and assigns it to the OnUpdateData event in the constructor.
Writing Platform-Independent Components The job of writing platform-agnostic components is at first a seemingly daunting task, but with CLX, writing such components has been made remarkably easy. For the large majority of tasks, issues of operating system interoperability have been hidden from the developer in the fundamental pieces of CLX. A more difficult task is to port existing Windows components to a cross-platform environment. Although the VCL of Delphi is similar to CLX, there are some danger areas to be careful of when converting components. n
System-level API calls — Any direct calls that are made by the component to the Windows API will of course not work on Linux. The component can maintain its existing code and add Linux-specific code by using conditional defines.
n
File references — Windows components that refer directly to filenames do not typically take into account issues of case sensitivity. On Linux, filenames are case sensitive and must be referred to as such from CLX components. This includes references to other CLX files. This means that even the case of units in the uses clause are case sensitive.
n
Path references — Dealing with files in a component can cause issues for existing components. Any component that explicitly refers to a file path must keep in mind that the default file separator character on Linux is the forward slash (/). On Windows, it is the backslash (\). Components that deal with path information must use the proper separator character.
Tip: CLX includes a file separator constant in the SysUtils unit. const PathDelim
= {$IFDEF MSWINDOWS} '\'; {$ELSE} '/'; {$ENDIF}
Referring to separators via this constant takes away some of the complexity involved in referring to file paths in cross-platform components.
Probably the most important tool in dealing with cross-platform components is the use of special compiler directives, called conditional defines. (See Chapter 9 for more information.) Conditional defines allow the developer to tell the compiler to include different code bits into the application, depending on the platform on which the component is being compiled. Take, for instance, the use of system-level calls.
378
Chapter 16: Writing Custom Components
Obviously, the methods that are available on Windows are different from those that are available on Linux. To create a component that is able to be installed and used on both, wrap each call in a conditional define as shown below. {$IFDEF WIN32 } // Conditional compilation for Windows // Windows-specific calls here. {$ENDIF } {$IFDEF LINUX} // Conditional compilation for Linux // Windows-specific calls here. {$ENDIF }
Tip: The use of conditional defines is not limited to parallel calls to different operating systems. Entire property and event declarations can be included or excluded from a component, depending on the OS on which it was compiled. However, changing the public interface of a component can be dangerous because it fundamentally changes a developer’s ability to use the component in the same way on both Linux and Windows.
Building Custom Property Editors An important part of building usable components is defining their design-time behavior. When a component is dropped onto a form or a datamodule, it is instantiated by and manipulated through the IDE. But the component user (the application developer) must be able to manipulate the component’s properties at design time. The Object Inspector can display four different types of properties: simple, enumerated, set, and property editor types. Kylix includes a number of predefined property editors that deal with Object Inspector types. Property editor type values, on the other hand, are typically set by interacting with a special, design-time GUI (usually a modal dialog box), designed by the component writer to assist the component user in setting a complex property value. The following example demonstrates the creation of a custom property editor. The TkdColorEdit class includes a property called FocusColor, which is of type TColor. The code for both the component and the property type and editor are listed below.
Chapter 16: Writing Custom Components
379
Figure 16-2: The custom editor hierarchy
Creating the Property Editor Dialog Creating the dialog that assists the developer in setting the property value is the easiest part of creating the editor. The Focus Color dialog is shown below and is used to set the value of the FocusColor property. Property editor dialogs can be as simple or as complex as the component developer wants. In this case, the editor simply allows the user to enter a color value.
Figure 16-3: The Focus Color editor
380
Chapter 16: Writing Custom Components
Creating the Property Editor Class TFocusColorProperty = class(TClassProperty) procedure Edit;override; function GetAttributes: TPropertyAttributes; override; end;
The property editor’s Edit method determines exactly what happens when the IDE invokes the property editor. For dialog editors, this commonly involves launching the editor form, retrieving the current property value, and then setting the new value of the property. procedure TFocusColorProperty.Edit; var PropValue:String; NewColor:Integer; begin inherited; with TfrmFocusColorDlg.Create(nil) do try ColorToIdent(GetOrdValue,PropValue); cmbxFocusColor.ItemIndex := cmbxFocusColor.Items.IndexOf(PropValue); shpColor.Color := StringToColor(cmbxFocusColor.Text); if ShowModal = mrOk then begin IdentToColor(cmbxFocusColor.Text,NewColor); SetOrdValue(NewColor); end; finally Free; end; // try..finally end;
The GetAttributes function determines what type of property is being edited so that the IDE can react accordingly. Attributes is a set property that can include multiple values. Not all of these values are applicable to dialog editors but help the IDE to create the appropriate type of editor for the property type. (Note that the following list does not include every option available.) Table 16-1 Attribute
Description
paValueList
The editor returns a list of valid property values. If this option is set, the GetValues method should be overridden to specify the list of values. This option causes the IDE to embed a drop-down editor in the Object Inspector. Lets the Object Inspector sort the list of values returned from GetValues.
paSortList
Chapter 16: Writing Custom Components
381
Attribute
Description
paSubProperties
Designates that the property has subproperties. If this option is selected, the GetProperties method should be overridden to provide the valid subproperties. Launches a dialog in the editor. Causes the Object Inspector to display an ellipsis next to the property. Allows the property to be edited when a collection of components is simultaneously selected. The property value cannot be changed. The property editor comes from the VCL and not CLX (generally, for portability’s sake, editors should be created as CLX entities). This property is not shown when it is being viewed as a subproperty of a nested component.
paDialog paMultiSelect paReadOnly paVCL paNotNestable
function TFocusColorProperty.GetAttributes:TPropertyAttributes; begin Result := [paDialog, paMultiSelect]; end;
Registering the Property Editor The Register procedure tells the IDE what component and what property within that component the editor should be associated with. The first statement is a call to RegisterComponents. This method installs the component on the desired page of the Component Palette. The RegisterPropertyEditor method associates the custom property editor with the comoponent. The first parameter dictates the type of property that will use this editor. The second component denotes the component that the editor should be associated with. The third parameter tells IDE what property of the component will use the associated editor. Finally, the last parameter is a reference to the class type of the property editor. procedure Register; begin RegisterComponents('Samples',[TkdColorEdit]); RegisterPropertyEditor(TypeInfo(TColor), TkdColorEdit, 'FocusColor', TFocusColorProperty); end;
Warning: If you do not specify the component class, the editor will be registered for all components. Furthermore, if you do not specify a property name, the editor will be registered for every property in that class!
382
Chapter 16: Writing Custom Components
Deploying Your Components with Packages Once a component is complete, it must be deployed. Kylix components are commonly deployed in packages. A package is an SO library that contains code necessary for the component’s use either in the designer or in the run-time environment. Thus, packages come in two flavors, design-time packages and run-time packages. Design-time packages are used to install the component in the Kylix IDE. In addition to component code, design-time packages typically include any custom property or component editors. Package source code files have a .dpk extension and are easily installed into the Kylix IDE. To create a new package, select Package from the Object Repository. By default, Kylix creates a package that is both a run-time and a design-time package. Use the Add and Remove buttons to specify component files that are included in the package. The Kylix package editor is shown below.
Figure 16-4: The Kylix package editor
Once the package has been compiled, it can be installed by clicking the Install button. This registers the components and any editors with the Kylix IDE. Components should then appear on the component palette. A notification message shows what changes have occurred.
Figure 16-5: Component installation
Chapter 16: Writing Custom Components
383
Note in Figure 16-4 that the Kylix2Development package includes both source code files and .dcp files. A .dcp is a binary image that contains package header information and all of the combined code from the .dpu files in the package. Because the Kylix2Development package is a design-time as well as a run-time package, it requires the designide.dcp file. This increases the overall size of the package, which makes for a more difficult deployment. Design-time packages are an important consideration during deployment. Because they are only used by Kylix, design-time packages should not be deployed with applications that are developed with these components. For this reason, it is a better architecture to place component editors, property editors, and registration routines in a source code file that is separate from the file holding the run-time component code. This makes the deployments smaller and avoids the unneccessary inclusion of design-time code. It is also important to note that design-time packages run inside the IDE, and as a result, can cause problems in the IDE if they are not carefully debugged. Many IDE errors can be traced back to bugs in packages installed into the IDE. Tip: By default, new packages are created as both run-time and design-time packages. Make sure that the package settings are specified according to the usage of the package. To provide this separation of design-time and run-time functionality, a new package called Kylix2DevelopmentReg is created. This unit includes only the property and component editors for the Kylix2Development components and the procedures used to register the components with the Kylix IDE. Since this package is marked as design-time only, it does not need to be deployed with the run-time component package. Notice that because the component and property editors reference the component classes themselves, the Kylix2DevelopmentReg package requires the Kylix2Development package. Thus, the design-time package gets component information from the run-time package, and only contains the code needed to integrate the components into the IDE. Thus, the design-time code and run-time code are separated. The key file in the Kylix2DevelopmentReg package is the kdCompReg file. This file contains the registration code for all the components as well as the registration of component and property editors. Its code is listed below. unit kdCompReg; interface uses Classes, QGraphics, DesignIntf, QkdColorEdit, QkdFocusColorDlg, QkdDlgPanel; procedure Register
384
Chapter 16: Writing Custom Components implementation procedure Register; begin RegisterComponents('KylixDevelopment',[TkdColorEdit]); RegisterComponentEditor(TkdColorEdit,TFocusColorEditor); RegisterPropertyEditor(TypeInfo(TColor),TkdColorEdit, 'FocusColor',TFocusColorProperty); RegisterComponents('KylixDevelopment',[TkdDlgPanel]); end; end.
Figure 16-06: The new component deployment strategy
Deploying components intelligently includes making decisions on exactly what pieces of the components must be deployed. Although we have seen a simple example with only a couple of components, it is just as likely that a package could contain dozens of components and many component and property editors. Size is commonly
Chapter 16: Writing Custom Components
385
a vital area for application deployments and reducing the size of deployed packages makes application deployment much smaller and much faster.
Summary Creating custom components is a great way to internalize reusable code into a component that can be used in any application. This gives the advantage of eliminating redundant coding and ensuring consistency in the way a certain piece of the application behaves. This chapter demonstrated practical methods for creating components from the simplest property addition to the most complex internal implementation. Once the component architecture is understood, creating custom components becomes as simple as any other code and as complex as the developer’s mind can create.
TE AM FL Y
Processes and Chapter 17 Processes and Threads Threads Introduction Processes and threads are the building blocks of writing applications in every modern operating system, including Linux. In order to build applications, these concepts must be clearly understood. This chapter will cover the basic concepts of processes and threads, how to create and use them, and caveats when working with them.
Multitasking Operating systems like Linux, Solaris, Windows NT, and many others are called multitasking. Multitasking is the ability of the operating system to rapidly switch between tasks, giving each a slice of time in order to perform their work. In a computer with only one processor, this rapid switching gives the illusion that it is running different tasks simultaneously. This concept is known as task switching. Since the computer has only one processor, there is only one task executing at a time. However, on a computer with two or more processors, the maximum number of tasks that can run concurrently is the number of processors in the machine. Even though a computer with more than one processor can run multiple tasks simultaneously, that does not mean that all processors are always busy. A task is a context of execution — a set of CPU instructions, stack, and memory that is scheduled by the kernel. There is no distinction, at the kernel level, between a process and a thread.
Processes Processes have existed from the beginning of the programming era. A process is an executable program that is currently loaded. It can either be running, stopped, or blocked. Stopped processes are those that are currently being debugged or have received a SIGSTOP, SIGTSTP, SIGTTIN, or SIGTTOU signal. Blocked processes are those that are waiting to be executed. Each process has its own instruction
389
390
Chapter 17: Processes and Threads
pointer, address space, memory space, and other various CPU registers. They run independent of each other and are scheduled by the kernel. Table 17-1 explains the signals and how they are triggered. Notice that the last three signals are generated by the user. One way a SIGSTOP signal is generated is by using the kill –9 [pid] command. Table 17-1 Signal
Meaning
SIGSTOP SIGTSTP SIGTTIN SIGTTOU
Stops a process. Unable to ignore signal. Suspends a process when Ctrl+Z is pressed in a terminal. Suspends a background process that is waiting for input. Suspends a background process from writing to a terminal.
The instruction pointer is used to keep track of the current instruction the program is running. Address space (also known as the text segment) refers to the memory location of where the process’s program code is loaded. Memory space refers to the heap (where memory is dynamically allocated), the stack (where local variables are located), and the global process data. Without using interprocess communication (IPC) of some kind, a process cannot communicate with another process. The good news is a process cannot directly read or write another process’s memory. This is good news because a process cannot inadvertently corrupt another process. Imagine if a program could write data into another process. The results could be disastrous. At the same time, the bad news is a process cannot directly read or write another process’s memory. When processes need to communicate, they need to use an IPC like pipes or shared memory. Coordinating and using any IPC mechanism requires diligent design and adds additional overhead. For more information on IPCs, see Chapters 18, 19, and 20.
Creating a New Process Write a simple console program in Kylix that looks like the following: program ProcessTest; begin writeln('I am alive!'); end.
Compile and run the program, using dcc ProcessTest.dpr. When the program is executed (e.g., ./ProcessTest), Linux, or more accurately the shell, creates a new process and executes the code to print the text “I am alive!” on the screen. In Linux and other versions of UNIX, creating processes are a normal way of life. Programmers frequently create processes to divide up tasks when it makes sense. Examples of processes that use this method include Apache (httpd), Minimal getty for consoles (mingetty), and TCP/IP IDENT protocol server (identd).
Chapter 17: Processes and Threads
391
Creating a process is accomplished by using the fork API. Fork does not take any arguments and returns an integer. Its definition from Libc.pas looks like this: function fork: Integer; cdecl; {$EXTERNALSYM fork}
Fork operates in an unusual way: it is called once but returns twice! When called, fork creates another process called the child. This child process is identical to its parent at the moment it is created. It contains copies of the same variables, open files, and signal handlers as the parent. However, changes in either the child or parent are independent of each other. After fork is called, it returns an integer to both the parent and child processes. A value of –1 indicates an error, with no child process created. A value of 0 indicates that the child process code is being executed. Positive integers represent the child’s process identifier (PID), and the code being executed is in the parent’s process. Once the child process is created, a decision needs to be made. The code for the child process can be in the same executable or it can load another program in its place, replacing the existing child process. For server applications like Apache (on UNIX platforms), keeping the child code in the same executable is the typical approach. Replacing a process is accomplished by using one of the following exec APIs: execl, execlp, execle, execv, and execvp. The definition of these APIs can be found in Libc.pas. These functions attempt to load the program and if successful, never return. In the case of a shell program, it loads the requested executable into the child process.
Simpleshell — an Example What exactly does a shell program do in order to execute the ProcessTest executable? It creates another process using the fork API and loads the ProcessTest executable into the new, created process and runs it. The following example demonstrates a simple shell program. program simpleshell; uses Libc; type TArgArray = array of AnsiString; var i,j,status : integer; cmdline,cmd : string; args : TargArray; begin while true do begin
392
Chapter 17: Processes and Threads write('[ss]$ '); readln(cmdline); if (cmdline = '') then break; // search for arguments in the string ParseArgs(cmdline,cmd,args); i := fork; if (i = -1) then begin // an error occurred; report it and exit the loop perror(PChar('Error attempting to fork..')); break; end else if (i = 0) then begin // in the new, child process so execute the command.. j := execv(PChar(cmd), PPChar(@args[0])); if (j = -1) then begin // an error occurred; report it and exit the loop writeln('Error executing ',cmd,' cmdline ', cmdline,' the error was ',GetLastError); perror(PChar('Error: ')); break; end; end else begin // in the parent process wait for my child to exit wait(status); if (status 0) then begin // an error occurred; report it and exit the loop perror(PChar('Error waiting for child process')); break; end; end; end; end.
This simpleshell program does the following: 1. Reads a command. 2. Parses the command line using the ParseArgs procedure to populate the args array.
Chapter 17: Processes and Threads
393
3. Forks a new process. 4a. In the parent process, it waits for the child to exit. 4b. In the child process, it executes the command using the execcv API. 5. Once the child is finished, the parent then starts over at the first step. Note: The ParseArgs procedure was left out for brevity. It can be found on the companion CD.
An Object-Oriented Simple Shell Example The previous example shows an older style of creating a shell program. By encapsulating the logic used by the fork and exec APIs, developers can create powerful applications without needing to know how it works. TExecutor and TForker are two components that makes creating multiprocess applications quick and easy. For brevity, the source for these components are not in this section, but can be found on the companion CD. TExecutor wraps the exec family of functions. The ProcessName property specifies which process to run and must be given a value. Use the Environment property when a specific set of environment variables is needed. TForker wraps the fork function. When the child code is in the same process, assign the child procedure to the OnChild event. For processes that need to be executed, set the Exec property to the appropriate values. Finally, the WaitForChild process will wait for the child process to finish. Using these two components, the simple shell program becomes even easier to write. Shown below is the revised version, using these components: program oosimpleshell; uses Classes,Process; var cmdline : string; cmd : string; f : TForker; {$IFDEF DEBUG} ret : TForkerWhichProc; {$ENDIF} begin // create the forker object f := TForker.Create; try f.Exec := TExecuter.Create; try {$IFDEF DEBUG}
394
Chapter 17: Processes and Threads f.Debug := true; f.Exec.Debug := true; {$ENDIF} f.WaitForChild := true; while true do begin write('[ooss]$ '); readln(cmdline); if (cmdline = '') then break; // search for arguments in the string ParseArgs(cmdline, cmd, f.Exec.Parameters); f.Exec.ProcessName := cmd; {$IFDEF DEBUG} ret := {$ENDIF} f.DoFork; {$IFDEF DEBUG} writeln('Done. I am back from ',ord(ret),' 0 parent 1 child'); {$ENDIF} end; finally // we should only hit this code if "exec" errors out... f.Exec.Free; end; finally f.Free; end; end.
Note: The ParseArgs procedure and the Process unit were left out for brevity. They can be found on the companion CD.
Daemon Processes Most UNIX variants including Linux have special processes called daemons. These processes are the Linux equivalent of Windows NT services, as they run in the background. Traditionally they are started when the system is booted up and run until the system is shut down. They do not have an associated console for displaying output or errors. Most daemon process names usually end with the letter “d”, like lpd (printer daemon) or syslogd (system logging daemon). Furthermore, they run with the superuser privilege, with a user ID of 0. So what is the difference between a process and a daemon? A daemon is a process that follows a specific set of rules:
Chapter 17: Processes and Threads
395
n
Immediately forks a process, allowing the parent to terminate. All daemon code is placed in the child process.
n
Calls the setsid function. This function creates a new session.
Note: An additional option is to fork another child process and put the daemon code in the second child process. This is common under UNIX SVR4. The purpose is to guarantee that the daemon is not a session leader. Instead of forking another child, the O_NOCCTY option can be specified when opening a terminal device. These concepts are beyond the scope of this book. More information can be found on this topic in W. Richard Stevens’ book, Advanced Programming in the UNIX Environment (Addison Wesley, 1992, ISBN 0201563177). n
Changes the current directory to the root directory or to a specific daemon directory.
n
Clears the file mode creation mask to 0.
n
Closes all file descriptors that are not needed.
Using these rules results in the following skeleton code for a daemon: program daemon; {$APPTYPE CONSOLE} uses Libc; var i,maxfds : integer; begin // 1. Fork a child process i := fork; if (i < 0) then begin ExitCode := -1; Exit; end; if (i = 0) then begin // This is the child code // 2. Call setsid setsid;
396
Chapter 17: Processes and Threads // 2a. Optional: Call fork again, daemon code in second child // 3. Change to the root directory chdir('/'); // 4. Clear the file creation mask umask(0); // 5. Close unneeded file descriptors // retrieve the maximum open file descriptors from OS maxfds := sysconf( _SC_OPEN_MAX); for i:=0 to maxfds do begin __close(i); end;
AM FL Y
// do daemon code here end; end.
Daemons are not mysterious creatures. They simply need to obey the rules. Used properly, daemons are powerful tools that are available when needed.
Threads
TE
Note: An easier way to write a daemon is to use the daemon function found in Libc.pas.
A thread is a path of execution in a process. In a normal process, there is only one path of execution — a single threaded program. Each thread has its own instruction pointer and various CPU registers. It shares the same memory space with the process that created the thread. All of the threads in the process share the same global memory, but each thread is allocated a chunk of memory for its stack space. Threads are sometimes referred to as “lightweight processes.” The scheduling of threads also occurs in the kernel. In fact, at the kernel level, threads are viewed as processes that share the same global memory. This makes it much easier for the kernel to schedule tasks. However, the kernel does not know that a process is a collection of threads. Linux does not address multithreaded applications in the same way as other environments. It does not recognize a particular application boundary for a collection of related threads; rather, it treats threads and processes similarly. For most applications this myopia is fine. When dealing with multiprocessor computers, however, the scheduling module that is currently implemented does not allow for
Chapter 17: Processes and Threads
397
applications to take advantage of the additional processors. The reason for this is that the kernel has no association between two tasks of the same process.
Thread-Safe Code When writing programs that use multiple threads, care must be taken to ensure that the work of one thread does not interfere with another thread. A function that is safe for multiple threads to execute is referred to as thread-safe code. Avoid global variables, functions, or procedures that are not thread-safe. Thread-safe code refers to the implementation of the function, not the interface that it provides. Functions that are not thread-safe can become thread-safe by serializing access to the function. This is accomplished by using a mechanism like mutexes or semaphores. These are topics are covered in Chapter 18. Thread-safe code can be accomplished several ways: n
Avoid all global variables and objects or protect all accesses to them.
n
Use only local variables, that is, those variables that are declared on the stack or within the var declaration block of a routine.
n
Make stand-alone functions and procedures stateless. Pass in all parameters that the routine needs to do its work. Parameters must not refer to other global data or objects that are not protected in some manner.
n
Make all methods stateless, by performing their work on the internal data of the object. Pass in any additional data to the method. The exception to this rule is that the object and parameters must not refer to other global data or objects that are not protected in some manner.
n
Use thread local storage, also known as threadvars, which is explained a little later in this chapter.
Reentrant Functions Another commonly used term describing functions, especially in Linux or UNIX, is reentrant. A reentrant function is one that does not keep the state of a previous call. In other words, no data is stored within the function between calls. Each call to the function is independent of previous or subsequent calls. Therefore, any data (or state) that is needed between calls must be passed to the function. It is important to note that thread-safe code and reentrant functions are different concepts. It is possible that a function is reentrant, thread-safe, both reentrant and thread-safe, or neither. A reentrant function that is not thread-safe can be made thread-safe by serializing calls to the reentrant function. Of course, any advantage of using multiple threads is no longer a benefit. In the first example that follows, the FindNextCharacter function is used to search a string for a given charater. It returns the location of the character in the string or 0 if the character is not found. The first time it is called, the string and character that are searched for are specified. This first example is not reentrant or thread-safe.
398
Chapter 17: Processes and Threads // // this is not the way this function should be written // it is only an example to illustrate reentrancy and thread-safety // // global variables should be avoided at all costs when used in // multi-threaded applications // // var PreviousStr : string; PreviousLoc : integer; // // Find the next character, ch in the string str. A return value // of 0 indicates that the character was not found // function FindNextCharacter(str : string; ch : char) : integer; begin if str '' then begin PreviousStr := str; PrevLoc := 1; end; Result := 0; while (PrevLoc 0 then begin // first allocate some memory SetLength(SemBufArray, FSemOps.Count); try // now loop through the FSemOps and set the fields for i:=0 to FSemOps.Count-1 do begin SemBufArray[i].sem_num := FSemOps[i].SemNumber; SemBufArray[i].sem_op := FSemOps[i].SemValue; SemBufArray[i].sem_flg := FSemOps[i].SemFlags; end; // assume a positive outcome Result := true; ret := semop(FSemID, @SemBufArray[0], FSemOps.Count ); if (ret = LIBC_FAILURE) and (errno = EAGAIN) then Result := false else if (ret = LIBC_FAILURE) then ErrorMessage('semop failed'); finally // release the memory SetLength(SemBufArray,0); end;
Chapter 18: Synchronization IPCs
445
end; end;
Deleting
When a System V semaphore is no longer needed, use the Delete method to remove it from the system. procedure TSysVSemaphore.Delete; var ret : integer; begin if FOpened then begin ret := semctl(FSemID, 0, IPC_RMID); if (ret = LIBC_FAILURE) then ErrorMessage('semctl failed for IPC_RMID command'); end; end;
Included on the CD are several examples of using the various methods and properties of the TSysVSemaphore class.
Read-Write Locks Read-write locks are another locking synchronization object. They distinguish between a reading lock and writing lock. While a read lock is placed, only reading can take place, allowing for multiple readers. A write lock can only be obtained when there are no readers. Read-write locks are memory-based. The following table lists the read-write functions. Table 18-7 Read-Write Functions
Description
pthread_rwlock_init pthread_rwlock_destroy pthread_rwlock_rdlock pthread_rwlock_tryrdlock
Initializes a read-write lock. Destroys a read-write lock. Obtains a read lock. Tries to obtain a read lock if it is available (does not block). Tries to obtain a read lock before the specified time. Obtains a write lock. Tries to obtain a write lock if it is available (does not block). Tries to obtain a write lock before the specified time. Releases the lock on the read-write lock.
pthread_rwlock_timedrdlock pthread_rwlock_wrlock pthread_rwlock_trywrlock pthread_rwlock_timedwrlock pthread_rwlock_unlock
446
Chapter 18: Synchronization IPCs
Read-Write Lock Attributes Read-write lock attributes are similar to condition variable and mutex attributes. They specify if the read-write lock can be shared between processes. For instance, when a read-write lock needs to be shared between multiple processes, set the PTHREAD_PROCESS_SHARED flag. The default behavior is specified with the PTHREAD_PROCESS_PRIVATE flag, which does not allow the read-write lock to be shared between processes. The following table shows the functions that manipulate read-write lock attributes: Table 18-8 Read-Write Lock Attribute Functions
Description
pthread_rwlockattr_init pthread_rwlockattr_destroy pthread_rwlockattr_getpshared
Initializes a read-write lock attribute. Destroys a read-write lock attribute. Retrieves the current value of the processed shared flag. Sets the processed shared flag.
AM FL Y
pthread_rwlockattr_setpshared
Tip: The TMultiReadExclusiveWriteSynchronizer class as currently implemented for Kylix does not allow multiple readers, since it only uses a mutex to protect both read and write accesses.
TE
Creating a Read-Write Locking Class
A read-write locking class is a wrapper for the read-write functions previously shown.
Class Definition TPosixRWLock = class(TIPCBase) private FRWLock : TPthreadRWlock; FProcShared : boolean; public constructor Create(bProcessShared : boolean); destructor Destroy; override; procedure procedure procedure
GrabReadLock; GrabWriteLock; Unlock;
function function function function
TryReadLock : boolean; TryWriteLock : boolean; TimedReadLock(absTime : TTimeSpec) : boolean; TimedWriteLock(absTime : TTimeSpec) : boolean;
Chapter 18: Synchronization IPCs
property end;
447
ProcessShared : boolean read FProcShared;
Constructor and Destructor
The constructor initializes the attribute and read-write lock, determines if the read-write lock will be shared between processes, and creates the lock. A read-write lock is destroyed when the destructor is called. constructor TPosixRWLock.Create(bProcessShared: boolean); var LockAttr : TPthreadRWlockAttribute; PShared : integer; ret : integer; begin inherited Create; // initialize the attribute ret := pthread_rwlockattr_init(LockAttr); if (ret LIBC_SUCCESS) then ErrorMessage('phtread_rwlockattr_init'); // do we need a shared rw lock? FProcShared := bProcessShared; if FProcShared then PShared := PTHREAD_PROCESS_SHARED else PShared := PTHREAD_PROCESS_PRIVATE; ret := pthread_rwlockattr_setpshared(LockAttr,PShared); if (ret LIBC_SUCCESS) then ErrorMessage('pthread_rwlockattr_setpshared'); // finally, create the rwlock ret := pthread_rwlock_init(FRWlock, @LockAttr); if (ret LIBC_SUCCESS) then ErrorMessage('pthread_rwlock_init'); end; destructor TPosixRWLock.Destroy; var ret : integer; begin ret := pthread_rwlock_destroy(FRWLock);
448
Chapter 18: Synchronization IPCs if (ret LIBC_SUCCESS) then ErrorMessage('pthread_rwlock_destroy'); inherited; end;
Acquiring a Read Lock
When a read lock needs to be acquired, call one of the three methods shown below. GrabReadLock blocks until the read lock is obtained. TryReadLock does not block, but checks the availability at the time of the call. TimedReadLock allows an absolute time to be specified to indicate when the method should no longer attempt to obtain the lock. procedure TPosixRWLock.GrabReadLock; var ret : integer; begin ret := pthread_rwlock_rdlock(FRWlock); if (ret LIBC_SUCCESS) then ErrorMessage('pthread_rwlock_rdlock'); end; function TPosixRWLock.TryReadLock: boolean; var ret : integer; begin ret := pthread_rwlock_tryrdlock(FRWLock); if (ret = LIBC_SUCCESS) then Result := true else Result := false; end; function TPosixRWLock.TimedReadLock(absTime: TTimeSpec): boolean; var ret : integer; begin ret := pthread_rwlock_timedrdlock(FRWlock,absTime); if ret = LIBC_SUCCESS then Result := true else Result := false; end;
Chapter 18: Synchronization IPCs
449
Acquiring a Write Lock
Similarly, when a write lock needs to be acquired, call one of the three methods shown below. GrabWriteLock blocks until the write lock is obtained. TryWriteLock does not block, but checks the availability at the time of the call. TimedWriteLock allows an absolute time to be specified to indicate when the method should no longer attempt to obtain the lock. procedure TPosixRWLock.GrabWriteLock; var ret : integer; begin ret := pthread_rwlock_wrlock(FRWlock); if (ret LIBC_SUCCESS) then ErrorMessage('pthread_rwlock_rdlock'); end;
function TPosixRWLock.TimedWriteLock(absTime: TTimeSpec): boolean; var ret : integer; begin ret := pthread_rwlock_timedrdlock(FRWlock,absTime); if ret = LIBC_SUCCESS then Result := true else Result := false; end;
function TPosixRWLock.TryWriteLock: boolean; var ret : integer; begin ret := pthread_rwlock_trywrlock(FRWLock); if (ret = LIBC_SUCCESS) then Result := true else Result := false; end;
450
Chapter 18: Synchronization IPCs Unlocking
After a lock has been acquired, it is released using the Unlock method. procedure TPosixRWLock.Unlock; var ret : integer; begin ret := pthread_rwlock_unlock(FRWLock); if (ret LIBC_SUCCESS) then ErrorMessage('pthread_rwlock_trywrlock'); end;
An example of how to use the TPosixRWLock class can be found on the CD.
Record Locking Record locking is used to share reading and writing of a file. It is used for locking between multiple processes, usually unrelated, and should not be used with multithreaded programs, as the process ID is used to identify the owner of the lock. Most UNIX-based systems use record locking for implementing a print spooler. All print operations are sent to the print queue and are issued a unique job ID. This unique job ID is generated from a simple file that contains the current job ID. Since it is possible for multiple requests to be submitted simultaneously, a lock is required to assure that only one process has access to the file at the same time.
Advisory Locking When processes use the same locking functions in a consistent way, they are cooperative and should use advisory locking. For example, suppose a database application uses a common library to access the data files. As long as the only access to the data files is through the library, advisory locking will work. Like read-write locks, two types of locks can be obtained, a read lock and a write lock. The kernel maintains the state of each file that has been locked and which process owns the lock. However, the kernel does not prevent writing to a file when it is read-locked, or reading a file when it is write-locked.
Mandatory Locking Mandatory locking is enforced at the kernel level. Every operation that modifies the contents of a protected file is checked before allowing the operation. Functions like __read, _write, readv, writev, open, creat, mmap, __truncate, and ftruncate, all declared in Libc.pas, modify a file. Setting the group-id bit for the protected file and removing the group-execute bit specifies a mandatory lock. This is accomplished by using the command chmod g+s,g-x .
Chapter 18: Synchronization IPCs
451
Tip: It is important to understand that no user, including the root user, can override a mandatory lock after a lock has been obtained. Sometimes it is possible to remove the group-id bit before attempting to read or write to the file. Be careful not to set this combination for crucial system files.
Lock Granularity Granularity refers to the size of a lock that is on a file. A lock that is placed on the entire file has a large granularity. Record locks have a smaller granularity that depends on the size of the lock. Files having smaller granularity allow for more simultaneous users, increasing concurrency. The following table lists the record locking functions that are declared in Libc.pas. Table 18-9 Function Lock Type
Lock Granularity Description
flock
Advisory Only File Only
fcntl
Both
Both
lockf
Both
Both
Creates or removes advisory lock on an open file. Allows for setting and removing locks on an open file. Allows for setting and removing locks on an open file (a wrapper for fcntl).
Creating a Record Locking Class For the record locking class, only the fcntl function will be used as it offers all of the options available.
Class Definition A type definition of TRecLockRelOffset has been defined to indicate how the offset parameter is calculated, relative to the beginning, current position, or end of the file. TRecLockRelOffset = (roBegin, roCurrent, roEnd); TRecordLock = class(TIPCBase) private FFd : integer; // file handle that the lock is on FLock : TFlock; // the lock structure function RecLockToSeek( relOff : TRecLockRelOffset ) : integer; public constructor Create(fd : integer); procedure GrabReadLock(relOff : TRecLockRelOffset; offset, RecordLen : off_t);
452
Chapter 18: Synchronization IPCs procedure GrabWriteLock(relOff : TRecLockRelOffset; offset, RecordLen : off_t); procedure Unlock; function
TryReadLock(relOff : TRecLockRelOffset; offset, RecordLen : off_t) : boolean;
function
TryWriteLock(relOff : TRecLockRelOffset; offset, RecordLen : off_t) : boolean;
function
IsReadLockable(relOff : TRecLockRelOffset; offset, RecordLen : off_t; var PidOfHolder : pid_t) : boolean;
function
IsWriteLockable(relOff : TRecLockRelOffset; offset, RecordLen : off_t; var PidOfHolder : pid_t) : boolean;
end;
Constructor
The constructor saves the file descriptor that the locking will occur on. constructor TRecordLock.Create(fd: integer); begin inherited Create; FFd := fd; end;
Acquiring Read Locks
A read lock can be acquired by using either the GrabReadLock or TryReadLock methods. GrabReadLock will block until the lock is obtained, while TryReadLock will not block. An additional method, IsReadLockable, is used to determine if a read lock could be obtained at the time of the call. There is no guarantee that an immediate call to GrabReadLock or TryReadLock after calling IsReadLockable will succeed, as another process could acquire the lock before either of the read lock methods has a chance to obtain it. procedure TRecordLock.GrabReadLock(relOff: TRecLockRelOffset; offset, RecordLen: off_t); var ret : integer; begin FLock.l_type := F_RDLCK; FLock.l_start := offset; FLock.l_whence := RecLockToSeek(relOff);
Chapter 18: Synchronization IPCs FLock.l_len := RecordLen; ret := fcntl(FFd, F_SETLKW, FLock); if (ret = LIBC_FAILURE) then ErrorMessage('GrabReadLock: fcntl failed'); end; function TRecordLock.TryReadLock(relOff: TRecLockRelOffset; offset, RecordLen: off_t): boolean; var ret : integer; begin FLock.l_type := F_RDLCK; FLock.l_start := offset; FLock.l_whence := RecLockToSeek(relOff); FLock.l_len := RecordLen; ret := fcntl(FFd, F_SETLK, FLock); if (ret = LIBC_FAILURE) then begin Result := false; if (errno EACCES) and (errno EAGAIN) then ErrorMessage('TryReadLock: fcntl failed'); end else Result := true; end; function TRecordLock.IsReadLockable(relOff: TRecLockRelOffset; offset, RecordLen: off_t; var PidOfHolder : pid_t): boolean; var ret : integer; begin FLock.l_type := F_RDLCK; FLock.l_start := offset; FLock.l_whence := RecLockToSeek(relOff); FLock.l_len := RecordLen; ret := fcntl(FFd, F_GETLK, FLock); if ret = LIBC_FAILURE then ErrorMessage('IsReadLockable: fcntl failed'); if (FLock.l_type = F_UNLCK) then Result := true else begin
453
454
Chapter 18: Synchronization IPCs Result := false; PidOfHolder := FLock.l_pid; end; end;
Acquiring Write Locks
Similarly, a write lock is obtained by using either the GrabWriteLock or TryWriteLock methods. GrabWriteLock will block until the lock is obtained, while TryWriteLock will not block. An additional method, IsWriteLockable, is used to determine if a read lock could be obtained at the time of the call. There is no guarantee that an immediate call to GrabWriteLock or TryWriteLock after calling IsWriteLockable will succeed, as another process could acquire the lock before either of the read lock methods has a chance to obtain it. procedure TRecordLock.GrabWriteLock(relOff: TRecLockRelOffset; offset, RecordLen: off_t); var ret : integer; begin FLock.l_type := F_WRLCK; FLock.l_start := offset; FLock.l_whence := RecLockToSeek(relOff); FLock.l_len := RecordLen; ret := fcntl(FFd, F_SETLKW, FLock); if (ret = LIBC_FAILURE) then ErrorMessage('GrabWriteLock: fcntl failed'); end; function TRecordLock.TryWriteLock(relOff: TRecLockRelOffset; offset, RecordLen: off_t): boolean; var ret : integer; begin FLock.l_type := F_WRLCK; FLock.l_start := offset; FLock.l_whence := RecLockToSeek(relOff); FLock.l_len := RecordLen; ret := fcntl(FFd, F_SETLK, FLock); if (ret = LIBC_FAILURE) then begin Result := false; if (errno EACCES) and (errno EAGAIN) then ErrorMessage('TryWriteLock: fcntl failed'); end
Chapter 18: Synchronization IPCs else Result := true; end; function TRecordLock.IsWriteLockable(relOff: TRecLockRelOffset; offset, RecordLen: off_t; var PidOfHolder : pid_t): boolean; var ret : integer; begin FLock.l_type := F_WRLCK; FLock.l_start := offset; FLock.l_whence := RecLockToSeek(relOff); FLock.l_len := RecordLen; ret := fcntl(FFd, F_GETLK, FLock); if ret = LIBC_FAILURE then ErrorMessage('IsWriteLockable: fcntl failed'); if (FLock.l_type = F_UNLCK) then Result := true else begin Result := false; PidOfHolder := FLock.l_pid; end; end;
Unlocking
Use the Unlock method to release a lock. procedure TRecordLock.Unlock; var ret : integer; begin // just overwrite the last lock type FLock.l_type := F_UNLCK; ret := fcntl(FFd, F_SETLKW, FLock); if (ret = LIBC_FAILURE) then ErrorMessage('GrabReadLock: fcntl failed'); end;
An example of how to use the TRecordLock class can be found on the CD.
455
456
Chapter 18: Synchronization IPCs
Summary
TE
AM FL Y
Shared resources are coordinated by using synchronization IPC objects. Linux provides mutexes, condition variables, POSIX semaphores, System V semaphores, read-write locks, and record locking to coordinate the usage of shared resources. Mutexes, POSIX semaphores, System V semaphores, read-write locks, and record locking can all be used to lock shared resources. Condition variables, POSIX semaphores, and System V semaphores can be used to wait for an event or condition to be satisfied. Choose the method that is the most appropriate for the application being designed and developed. Synchronization IPC objects are the musical conductors of a symphony of various processes and threads. They lead and direct these processes and threads in a way that, when done properly, produces applications that perform like a world-class orchestra.
Message Passing Chapter 19 Message Passing IPCs IPCs Introduction Applications that consist of multiple processes communicate by passing messages back and forth between them. The contents of the messages are application specific and range from simple strings to complex record structures. This chapter discusses the ways of exchanging messages, shows how to build classes for easier usage, and demonstrates their usage. Message passing is sending information, or messages, between multiple processes or threads. Messages are sent in one of three ways: by using pipes, FIFOs (a UNIX term for named pipes), or System V message queues. OS Note: Other versions of UNIX also have POSIX message queues; however, they are not available in Linux at the time of this writing.
Pipes A pipe is a one-way communication channel from a specific source to a specific destination. For bidirectional communication, an additional pipe is needed. Since pipes are not named, nor do they have another method of identification, they can only be used by related processes.
Figure 19-1: Using pipes
Pipes are frequently used in processes that create child processes. (A child process is one that is created from another process; see Chapter 17 for more information.) Shell programs like bash or csh create a pipe when one is specified on the command
457
458
Chapter 19: Message Passing IPCs
line with the vertical bar symbol (|). The shell creates a pipe and forks a child process. In the parent process, it redirects the standard output to the write side of the pipe, and in the child, it redirects standard input to the read side of the pipe. OS Note: Bidirectional pipes do exist in other versions of UNIX like SVR4. Blocking, when reading and writing pipes, is the default. When a pipe is read and it is empty, the read will block. Similarly, when writing to a full pipe, the write will block. Tip: When a process (or thread) blocks, it is waiting for something to occur so it can continue to run. Blocking usually happens in the kernel, where it is very efficient and does not waste CPU cycles. When processes exchange messages, they must agree upon the structure of a message. This is especially true when using variable length messages. Common solutions to variable length messages include using a termination sequence (like a carriage return, ASCII value 13, or linefeed character, ASCII value 10), placing the size of the data before the message, and sending only one message, forcing a separate connection for each communication. The following table lists the functions available for manipulating pipes: Table 19-1 Pipe Functions
Description
pipe
Creates a one-way pipe. Returns two file descriptors, or handles, for each end of the pipe. Reads from a pipe or any file descriptor. Writes to a pipe or any file descriptor. Closes a pipe or any file descriptor. Duplicates a file descriptor, closing the new file descriptor if necessary. Creates a pipe and starts another process that can read from or write to the pipe. Reads from a pipe or any file stream. Writes to a pipe or any file stream. Closes a file stream and waits for the process spawned by popen to terminate. Manipulates open file descriptors. Used to change the blocking mode of __read and __write operations. Synchronizes, or flushes, the contents of a file in memory to disk.
__read __write __close dup2 popen fgets (and more) fputs (and more) pclose fcntl fsync
Chapter 19: Message Passing IPCs
459
Creating a Pipe Class The basic operations needed for creating a Pipe class are creating, reading, writing, and closing. Two additional methods have been added: PipeWritesToStdOut redirects the write side of the pipe to standard output and PipeReadsFromStdIn redirects standard input to the read side of the pipe. The use of these additional methods is shown in the example below. Tip: All of the classes used here are descendants of a class called TIPCBase. This class contains debugging and helper methods for reporting errors.
Class Definition TPipeBuffer = array of char; TPipeSide = (psRead, psWrite); TPipe = class(TIPCBase) private FHandles : array[0..1] of Integer; public constructor Create; destructor Destroy; override; function Read(var buffer : TPipeBuffer; bufSize : Integer) : boolean; function Write(var buffer : TPipeBuffer; bufSize : Integer) : boolean; function Close(what : TPipeSide) : boolean; function WriteToStdOut : boolean; function ReadFromStdIn : boolean; end;
Constructor
In the constructor, the pipe is created, and the handles that the pipe function returns are saved in an array. The read side of the pipe is position zero and the write side is position one. constructor TPipe.Create; var pptr : PInteger; ret : integer; begin inherited; pptr := @FHandles; ret := pipe(pptr); if ret LIBC_SUCCESS then
460
Chapter 19: Message Passing IPCs ErrorMessage('pipe'); end;
Destructor
The destructor closes both handles of the pipe. destructor TPipe.Destroy; begin __close(FHandles[IDX_READ_PIPE]); __close(FHandles[IDX_WRITE_PIPE]); inherited; end;
Reading and Writing
Reading from and writing to pipes is accomplished by using the Libc functions __read and __write. These functions retrieve information from and send information to the pipe. After writing the data, the __write method flushes the buffer so that the data is sent out over the pipe. function TPipe.Read(var buffer : TPipeBuffer; bufSize : Integer) : boolean; var ret : integer; begin // IDX_READ_PIPE = 0 ret := __read(FHandles[IDX_READ_PIPE],buffer[0],bufSize); Result := (ret = LIBC_SUCCESS); end; function TPipe.Write(var buffer : TPipeBuffer; bufSize : Integer) : boolean; var ret : integer; begin // IDX_WRITE_PIPE = 1 ret := __write(FHandles[IDX_WRITE_PIPE],buffer[0],bufSize); // flush the write down the pipe.. fsync(FHandles[IDX_WRITE_PIPE]); Result := (ret = LIBC_SUCCESS); end;
Chapter 19: Message Passing IPCs
461
Redirecting Standard Input
When developing command-line tools, it is very common to use a pipeline. A pipeline takes the output of one program and sends it to the input of another program, creating a “pipeline” between the two programs.
Figure 19-2: Redirecting standard input
ReadFromStdIn first closes standard input, then points standard input to the read side of the pipe. After ReadFromStdIn is called, any function that reads from standard input, like readln (without a file handle), will read from the pipe and not from the console. This allows for command-line utilities, like more, to be written without and knowledge of pipes. An example of how to use this method is shown in the Pipe example later in this chapter. function TPipe.ReadFromStdIn : boolean; var ret : integer; begin // IDX_READ_PIPE = 0 ret := dup2(FHandles[IDX_READ_PIPE], STDIN_FILENO); if (ret = LIBC_FAILURE) then ErrorMessage('ReadFromStdIn:dup2'); Result := (ret = LIBC_SUCCESS); end;
Redirecting Standard Output
Similarly, WriteToStdOut first closes standard output, then makes a copy of the write side of the pipe point to standard output, thereby redirecting the output of the pipe to standard output. After this method is called, any output that normally goes to standard output, like writeln (without a file handle), will be writing directly to the pipe and onto the screen.
462
Chapter 19: Message Passing IPCs
Figure 19-3: Redirecting standard output
function TPipe.WriteToStdOut : boolean; var ret : integer; begin // IDX_WRITE_PIPE = 1 ret := dup2(FHandles[IDX_WRITE_PIPE], STDOUT_FILENO); if (ret = LIBC_FAILURE) then ErrorMessage('WriteToStdOut:dup2'); Result := (ret = LIBC_SUCCESS); end;
Closing the Pipe
When a specific side of the pipe needs to be closed, use the Close method. function TPipe.Close(what : TPipeSide) : boolean; begin __close(FHandles[ord(what)]); Result := true; end;
Pipe Example Demonstrating the usage of pipes is the PipeRedir program. It shows how to use a pipe to send and receive messages by creating a parent and child process using the TForker class. The parent process sends messages and the child process receives them. Furthermore, PipeRedir also shows how to use the ReadFromStdIn method to allow the child to read from the pipe by using the readln procedure. Tip: The TForker class is discussed in detail in Chapter 17. It is a wrapper for the Libc Fork API call that creates a child process.
Chapter 19: Message Passing IPCs program PipeRedir; {$APPTYPE CONSOLE} uses SysUtils, Libc, Process, Pipes; const MAX_MSG_SIZE = 100; // arbitrary value var f ThePipe WhichProc tmpStr RecvBuf RecvLen SideToClose bRet
: : : : : : : :
TForker; // described in Chapter 17 TPipe; TForkerWhichProc; string; TPipeBuffer; integer; TPipeSide; boolean;
procedure SendMessage(msg : string; p : TPipe); var SendBuf : TPipeBuffer; MsgLen : integer; bRet : boolean; begin // the length of the message MsgLen := length(msg) + 1; SetLength(SendBuf,MsgLen); try StrPCopy(PChar(SendBuf),msg); writeln(GetPid,' Sending the message ',StrPas(PChar(SendBuf))); bRet := ThePipe.Write(SendBuf,MsgLen); writeln(GetPid,' The message was written. Return value is ', Integer(bRet)); finally // release the memory SetLength(SendBuf,0); end; end; begin f := TForker.Create; try ThePipe := TPipe.Create; try // turn on the debugging information..
463
464
Chapter 19: Message Passing IPCs f.Debug := true; ThePipe.Debug := true; // Before forking, set the pipe so it reads from standard input ThePipe.ReadFromStdIn; // Don't wait for the child.. f.WaitForChild := false; // Do the fork WhichProc := f.DoFork; if (WhichProc = wpParent) then begin writeln(GetPid,' In the parent'); // in the parent, we will close the read side of the pipe ThePipe.Close(psRead); // and write a message on the pipe SendMessage('Hello from Parent!', ThePipe); // write another message // since we are demonstrating the usage of redirecting // standard input to the pipe, and using "readln" to read // the message, we need to tack on a Carriage Return character SendMessage('What are you doing?' + #13 , ThePipe); tmpStr := 'Hello from Parent!'; // since we are in the parent, close the write side of the pipe SideToClose := psWrite; end else begin writeln(GetPid,' In the child..'); // in the child, we will close the write side of the pipe ThePipe.Close(psWrite); // and read a message from the pipe, first normally SetLength(RecvBuf,MAX_MSG_SIZE); RecvLen := MAX_MSG_SIZE; writeln(GetPid,' Reading from the pipe..'); bRet := ThePipe.Read(RecvBuf,RecvLen); if bRet then writeln(GetPid,' Received from the pipe!') else
Chapter 19: Message Passing IPCs
465
writeln(GetPid,' Error reading the pipe!'); // Since Standard input is redirected to the pipe, // we now we can read a message from the pipe by using readln SetLength(tmpStr,MAX_MSG_SIZE); // Note: Using readln implies that messages are terminated // by Carriage Returns readln(tmpStr); writeln(GetPid,' The pipe sent !!'); // since we are the child, close the read side of the pipe SideToClose := psRead; end; // now close writeln(GetPid,' Closing the pipe..'); ThePipe.Close(SideToClose); writeln(GetPid,' Done..'); finally ThePipe.Free; end; finally f.Free; end; end.
After running the PipeRedir program, the output will look similar to the following: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
$ ./PipeRedir The read size handle is 3 the write side is 4 1590 In the parent 1591 In the child.. 1591 Closing 1 1591 Reading from the pipe.. 1590 Closing 0 1590 Sending the message Hello from Parent! 1590 The message was written. Return value is 1 1591 Received from the pipe! 1590 Sending the message What are you doing? 1590 The message was written. Return value is 1 1590 Closing the pipe.. 1590 Closing 1 1590 Done.. 1591 The pipe sent !! 1591 Closing the pipe.. 1591 Closing 0 1591 Done..
466
Chapter 19: Message Passing IPCs
The numbers that are shown before the output are the process identifiers (pids). 1590 is the pid for the parent process and 1591 is the pid for the child. Notice how the child reads from the pipe (line number 6) before the parent writes a message to the pipe (line number 8).
FIFOS (Named Pipes) Recall that pipes need to be used by related processes. FIFOs (first in, first out) add a name to a pipe, allowing for unrelated processes to exchange messages. The name given to a FIFO must be a valid Linux filename and every FIFO has file permissions associated with it. Like pipes, FIFOs are a one-way communication device. FIFOs have the same rules for blocking and exchanging messages as the pipe. The following table lists the functions available for creating, opening, reading, writing, and deleting FIFOs:
AM FL Y
Table 19-2 Description
mkfifo open __read __write __close unlink fcntl
Creates a FIFO. Opens a FIFO as well as normal files. Reads from a pipe or any file descriptor. Writes to a pipe or any file descriptor. Closes a pipe or any file descriptor. Deletes a FIFO. Manipulates open file descriptors. Used to change the blocking mode of __read and __write operations.
TE
FIFO Functions
Creating a FIFO Class A FIFO has the same basic operations as the Pipe class, namely reading and writing. The differences are the name of the FIFO and the file permissions that are associated with it.
Class Definition TPipeSide
= (psRead, psWrite);
TNamedPipe = class(TIPCBase) private FOpen : boolean; FFileDesc : integer; FPipeName : string; FPipeSide : TPipeSide;
Chapter 19: Message Passing IPCs
467
function PipeSideToBits(pipeSide : TPipeSide) : integer; public constructor Create(name : string; pipeSide : TPipeSide; perm : TIPCFilePermissions); destructor Destroy; override; function Read(var buffer : TPipeBuffer; bufSize : Integer) : boolean; function Write(var buffer : TPipeBuffer; bufSize : Integer) : boolean; function Close : boolean; end;
Constructor
The constructor creates the FIFO with the appropriate permissions, which are specified on the side of the pipe that is being opened. The read side needs read-only permissions, while the write side needs write permissions. After creating the FIFO, it must be opened before it is used. Errors that occur during the opening of a FIFO raise application exceptions. constructor TNamedPipe.Create(name : string; pipeSide : TPipeSide; perm : TIPCFilePermissions); var ret : integer; begin inherited Create; FOpen := false; // create the pipe ret := mkfifo(PChar(name), FilePermissionsToBits(perm)); // It's okay if the pipe already exists.. if (ret < 0) and (GetLastError EEXIST) then ErrorMessage('mkfifo'); FPipeName := name; FPipeSide := pipeSide; // open the pipe FFileDesc := open(PChar(name), PipeSideToBits( FPipeSide ), 0); if FFileDesc LIBC_SUCCESS then ErrorMessage('NamedPipe.__open'); FOpen := true; end;
468
Chapter 19: Message Passing IPCs Destructor
A FIFO’s destructor is responsible for closing the pipe and releasing its allocated resources. However, this does not release resources that are being pushed through the pipe at the time. destructor TNamedPipe.Destroy; begin Close; inherited; end;
Permissions
In this example, PipeSideToBits is a private method used by the constructor. Its purpose is to map the pipe with the proper permissions, depending on whether it is the read or write side of the pipe. Reading requires read-only permissions; writing requires write-only permissions. function TNamedPipe.PipeSideToBits(pipeSide : TPipeSide) : integer; begin Result := 0; case pipeSide of psRead : Result := O_RDONLY; psWrite : Result := O_WRONLY; else ErrorMessage('PipeSideToBits: unknown argument'); end; end;
Reading and Writing
Reading information from the pipe is accomplished by using the Read method, while writing data to the pipe is accomplished by using the Write method. Both methods take a buffer and the size of the buffer. They return a Boolean value to indicate success or failure. function TNamedPipe.Read(var buffer : TPipeBuffer; bufSize : Integer) : boolean; var ret : integer; begin ret := __read(FFileDesc,buffer[0],bufSize); Result := (ret = LIBC_SUCCESS); end;
Chapter 19: Message Passing IPCs
469
function TNamedPipe.Write(var buffer : TPipeBuffer; bufSize : Integer) : boolean; var ret : integer; begin ret := __write(FFileDesc,buffer[0],bufSize); // flush the write down the pipe.. fsync(FFileDesc); Result := (ret = LIBC_SUCCESS); end;
Closing the FIFO
When a FIFO is no longer needed, close it with the Close method. It closes the file descriptor, and then it attempts to delete the pipe with the name used when it was created. Tip: It is important to note that the last call to the Close method will actually delete the pipe. function TNamedPipe.Close : boolean; begin if FOpen then begin __close(FFileDesc); FOpen := false; // we skip error checking because it will fail if another // process still has the file open. When the last process // ends, the pipe will be deleted. DeleteFile(FPipeName); end; Result := true; end;
FIFO Example For the FIFO example, there is a server program and a client program. The server creates a named pipe and blocks while waiting for the client to send a message. In the client, the pipe is opened and the message is sent to the server. Then, the server retrieves the message and displays whatever the client sent to it. // The Fifo Server program program fifoserver;
470
Chapter 19: Message Passing IPCs uses SysUtils, Pipes; const PIPE_NAME = '/tmp/fifotest'; var fifo : TNamedPipe; recvBuf : TPipeBuffer; begin writeln('Starting fifo server...'); fifo := TNamedPipe.Create( PIPE_NAME, psRead, [fpOwnerRead, fpOwnerWrite, fpGroupRead, fpGroupWrite]); try SetLength(recvBuf,1024); if not fifo.Read(recvBuf, length(recvBuf)) then writeln('Server: Error reading from fifo!') else writeln('Received ->',StrPas(PChar(recvBuf)),' 0 then begin Session.Values['EmailAddress'] := Value.Values[0]; end;
Chapter 22: Introduction to WebSnap
553
SL := TStringList.Create; try Value := EmailTypesWantedField.ActionValue; for i := 0 to Value.ValueCount - 1 do begin SL.Add(Value.Values[i]); end; Session.Values['EmailTypesWanted'] := SL.Text; finally SL.Free; end; Value := WantsEmailField.ActionValue; if Value nil then begin if Value.ValueCount > 0 then begin Session.Values['WantsEmail'] := Value.Values[0]; end; end else begin Session.Values['WantsEmail'] := 'false'; end; end;
The code above is fairly straightforward. The actual “work” is done by the Value variable, which is of type IActionFieldValue. It is an interface, and is declared as follows: IActionFieldValue = interface ['{C5D4E556-A474-11D4-A4FA-00C04F6BB853}'] function GetFieldName: string; function GetValueCount: Integer; function GetValue(I: Integer): Variant; function GetFileCount: Integer; function GetFile(I: Integer): TAbstractWebRequestFile; property ValueCount: Integer read GetValueCount; property Values[I: Integer]: Variant read GetValue; property FileCount: Integer read GetFileCount; property Files[I: Integer]: TAbstractWebRequestFile read GetFile; property FieldName: string read GetFieldName; end;
The ActionValue of each field contains the information added to it by the user and returns an IActionFieldValue interface to access that data. Because fields can be of many different types, this interface is flexible and can return any type of data, as well as information about the nature of that data. In the case of a simple edit field,
554
Chapter 22: Introduction to WebSnap
that data can be retrieved via the Values property, which is an array of variants. (In the next chapter, we’ll see how easy it is to upload files from a client using a wrapper around the IActionFieldValue.Files property.) In the case of an edit control, there will only be one item in the array, but in the case of multi-value control sets, there could be from zero to many values. The information is grabbed from the Value variable and then stored in the very powerful Session object. Session also has a property that is an array of variants, like the IActionFieldValue. However, this array is indexed on string values, and you can set them to any variable value that will fit into a variant, and index it on any string value. The Session object will store these values in memory for later retrieval at anytime or from anywhere in the application. The Session object will hold the information unique to each separate session, so there is nothing for you to do other than use the data normally. The Session variable is stored on the client browser as a cookie. Therefore, values stored in the Session variable are always available in your application. Sessions expire, and you can set how long, in minutes, a session will remain in memory without being accessed before being terminated. You can also terminate sessions with the Terminate method. Note that the application uses a TStringList to hold the multiple values possible in the EmailTypesWantedField, and then stores the values in the Session variable as a single string. Later, you’ll pull that string out again, and put it back into a TStringList.
Displaying Adapter Data Back to the User Of course, gathering up that data isn’t any good if you can’t display it back or use it in your application. To do that, you need to get the data from the fields of the TAdapter. And here’s the powerful part — once you make the information available from the TAdapter fields, those values are usable in server-side script, making the values very easy to put onto your Web page. Getting the value of a TAdapter field seems a little backwards. You set the value in the OnGetValue event handler for the field. In that handler, you assign a value to the field. In your case, you’ll simply return the value of the Session.Values variable. For each of the three fields in UserInfoAdapter, add an OnGetValue handler. For the EmailTypesWantedField, you’ll also need to add an event handler for the OnGetValueCount event so that the app can know how many items are in the multi-valued field. Make them look like this: // Get Field Values procedure TUserInformation.WantsEmailFieldGetValue(Sender: TObject; var Value: Boolean); begin Value := Session.Values[sWantsEmail]; end;
Chapter 22: Introduction to WebSnap
555
procedure TUserInformation.EmailAddressFieldGetValue(Sender: TObject; var Value: Variant); begin Value := Session.Values[sEmailAddress]; end; procedure TUserInformation.EmailTypesWantedFieldGetValueCount( Sender: TObject; var Count: Integer); var SL: TStringList; begin SL := TStringList.Create; try SL.Text := Session.Values[sEMailTypesWanted]; Count := SL.Count; finally SL.Free; end; end; procedure TUserInformation.EmailTypesWantedFieldGetValues(Sender: TObject; Index: Integer; var Value: Variant); var SL: TStringList; begin SL := TStringList.Create; try SL.Text := Session.Values[sEMailTypesWanted]; Value := SL[Index]; finally SL.Free; end; end;
Note that the code uses a TStringList as a temporary storage device for the multi-value field. Once you add this code, the Adapter will remember your settings. Compile, remember to move the HTML, restart Apache, and if you log in, make entries, and then go back to the UserInformation page. Note that the settings you entered are still there, as the application remembers the values put into the controls. Now that the data is properly saved in the TAdapter fields, and the user can set and change his settings as needed, you’ll want to actually use these settings in your web site. The real power of adapters comes when they are used to present data to the user in server-side script. So dust off your JavaScript manual, because instead of writing Kylix code, you are going to do a little scripting.
Chapter 22: Introduction to WebSnap
You’ve already created the display page, so this is the page where we’ll provide some feedback to the user based on the entries made in the UserInformation page. Go to that page, and open the associated HTML file. At the bottom, just above the tag, add the following JavaScript code:
AM FL Y
Your Email Address:
These are the types of email you have chosen:
This code will display the information in the UserInfoAdapter fields on the page. Note that it uses references to your Kylix objects in the JavaScript code, enabling you to display the results of Kylix code in the HTML of your Web applications.
Managing Your HTML You probably have already gotten tired of moving all your HTML files to the DSO directory. You also will very likely want to manage that HTML separately from the Kylix application itself. Perhaps your Web team wants to make changes to the templates or pages themselves, and you don’t want to give them access to the directory on the server. Or perhaps you want to manage your HTML in a database. This is where the TLocateFileService component comes in. It can be used to access HTML from almost any location. There are two main ways to get HTML — from an actual file or from a stream. You can retrieve a file using the TLocateFileService.OnFindTemplateFile event, or you can get it from a stream using the TLocateFileServices.OnFindStream event. These two will show up as follows in your Code Editor when you double-click on them in the Object Inspector: procedure THome.LocateFileService1FindStream(ASender: TObject; AComponent: TComponent; const AFileName: String; var AFoundStream: TStream; var AOwned, AHandled: Boolean); begin end; procedure THome.LocateFileService1FindTemplateFile(ASender: TObject; AComponent: TComponent; const AFileName: String; var AFoundFile: String; var AHandled: Boolean); begin end;
Both are similar in that they pass in the same basic information and expect you to return HTML in the AFoundXXXX parameter. In both, the AFilename parameter is the name of the file that is being sought by WebSnap. For your home page in the demo app, this value will be wmHome.html. You can use this value to do special searching for specific files if need be.
558
Chapter 22: Introduction to WebSnap
For the OnFindStream event, the AFoundStream parameter is a pointer to a stream that the event is expecting will hold the HTML when you are all done. The AOwned Boolean parameter determines whether the stream will be owned by you or WebSnap. If you set this to true, WebSnap will free the stream, but if you leave this false, you are responsible for freeing the stream in question. Set AHandled to True if you do return an HTML file in the stream. The OnFindTemplate event handler works the same way, except that you return a fully qualified filename in the AFoundFile parameter. Here are two examples of how this might work: procedure THome.LocateFileServiceFindTemplateFile(ASender: TObject; AComponent: TComponent; const AFileName: String; var AFoundFile: String; var AHandled: Boolean); begin // This will look in the same directory for all the HTML files AFoundFile := '/home/nick/KylixDemo' + AFilename; end;
This code will look for all the files in the directory where the sample application is currently stored. This is the easiest thing to do during development, as you can make changes to the HTML files in the IDE, and those changes will be reflected immediately in the DSO without even compiling or redeploying. Once you deploy, you could change this to point to the location of your deployed HTML files. procedure THome.LocateFileServiceFindStream(ASender: TObject; AComponent: TComponent; const AFileName: String; var AFoundStream: TStream; var AOwned, AHandled: Boolean); begin AFoundStream := TFileStream.Create('/home/nick/KylixDemo/' + AFilename, fmOpenRead); AOwned := True; AHandled := True; end;
This example does basically the same thing as the first, except it opens up the file in a TFileStream object and returns that. You could just as easily open the HTML in a TBlobStream or a TResourceStream. Thus, the TLocateFileService gives you complete control over where your WebSnap application’s HTML resides. You can easily manage your HTML as you please, and set it up such that changes to the HTML files for your site are instantly seen by your users.
Chapter 22: Introduction to WebSnap
559
Displaying Data in a WebSnap Application So far, your application has not done anything more than display the inner workings of WebSnap. This “plumbing” is important, of course, but displaying data is really what a web site does, and what you will likely be most concerned about once you have set up the inner workings of the site to handle users, logins, etc. WebSnap, as you might expect, provides a set of powerful tools and controls to display data in your web pages. The next step in your demo application will build a simple data-handling application based on the now world-famous BioLife Fishfacts table, which is located in the kylix2/demos/db/fishfact/directory.
Displaying Data in a Grid Go to the IDE toolbar, and create a new TWebData module. You can do that by selecting the third button on the toolbar, the one with the globe and small window. Accept the default settings from the wizard, and save the unit as wdmData.pas. Then follow these steps: 1. Drop a ClientDataset on the module. 2. Put the biolife.xml file in the DSO directory. To the OnCreate event of the webdatamodule, add ClientDataset1.Filename := QualifyPathname(‘biolife.xml’);
That will keep the XML from being embedded in the DFM file, and will make sure that the pictures are displayed properly. 3. In the module’s OnActivate event add: i. ClientDataset1.Open;
4. And in the OnDeactivate event, put: i. ClientDataset1.Close;
5. For the time being, point the ClientDataset1.Filename to the biolife.xml file. You need to connect up the ClientDataset to the file at design time to get at the needed field data. You need to do it at design time for the fields to be accessed. 6. Add all the fields to the ClientDataset. Select the Species No field and add the pfInKey flag to its ProviderFlags property. 7. Drop a TDataSetAdapter on the module from the WebSnap tab on the component palette. 8. Set the DatasetAdapter1.Dataset property to the ClientDataset. 9. Right-click on the DatasetAdapter, choose the Fields Editor, and add all fields. 10. Right-click on the DatasetAdapter, choose the Actions Editor, and add all the actions.
560
Chapter 22: Introduction to WebSnap
Next, add another page to your application, call it GridDisplay, give it a TAdapterPageProducer, and save it as wmGridDisplay.pas. Make it require a logged-in user in order to be viewed, as later we’ll be doing some user-based things with this page. Once you have the page ready to go, perform the following steps: 1. Add wdmData to the unit’s uses clause. 2. Double-click on the AdapterPageProducer and add an AdapterForm to the page. 3. Add an AdapterGrid to the AdapterForm. 4. Set the AdapterGrid.Adapter property to the DatasetAdapter. 5. Right-click on the AdapterGrid, and select Add All Columns. 6. Remove the ColNotes column. It contains text, and makes each row in the table too tall. 7. Go back to the WebData module and clear the ClientDataset1.Filename property. Once you have done all these steps, recompile the application, restart the server, and you should see something like this:
Figure 22-12: The KylixDemo app displaying the BioLife table
Of course, merely displaying data like this isn’t always enough. Sometimes, as you browse data, you may want to look more closely at a single record. In order to do that, you can create a Command column for each row of the table that will take you to another page to look at that particular record.
Chapter 22: Introduction to WebSnap
561
Do that by first creating a new page, giving it a TAdapterPageProducer, naming it RecordDisplay, making it a login required page, and saving it as wmRecordDisplay. Then, once again, follow this set of steps (These steps are getting a little more terse, as you should be more familiar with how the process works): 1. Add wdmData to the uses clause of the new unit. 2. Double-click on the AdapterPageProducer and add an AdapterForm, an AdapterFieldGroup, and an AdapterCommandGroup. 3. Attach the AdapterFieldGroup to the DatasetAdapter, and the AdapterCommandGroup to the AdapterFieldGroup. 4. Add all the fields to the AdapterFieldGroup. 5. Add the following commands to the AdapterCommandGroup: n n n n n
FirstRow PrevRow NextRow LastRow RefreshRow
Now, the page should display records, and you can use the buttons along the bottom to move between rows. Next, go back to the GridDisplay page, and select the AdapterPageProducer. You are now going to add a CommandColumn to the grid that will hold a button to go to the RecordDisplay page for the given row in the grid. Do that as follows: 1. Go to the AdapterPageProducer on the GridDisplay page and double-click it. 2. Right-click the AdapterGrid, select New Component, and add a new AdapterCommandColumn. 3. Right-click the AdapterCommandColumn, and select Add Command… 4. Add the BrowseRow command. 5. Select CmdBrowseRow, and set its PageName property to RecordDisplay. Now if you deploy and run the application, you can select the button in a grid row, and you will be taken to the RecordDisplay page for that row. In the next chapter, when we cover access rights, we’ll see how to edit that record using the same page.
Summary In this chapter, we covered the basics of WebSnap. You saw how to create a basic WebSnap application that displays pages and runs as a CGI. You then built a more complex application that managed user logins, tracked user information, and displayed data from a database. In the next chapter, you cover some more advanced topics, such as persistent sessions, access rights, and file uploading.
Chapter 23
Advanced WebSnap
Introduction In the last chapter, you covered the basics of WebSnap, and saw how to build pages, manage users and HTML, and display basic data in grids and fields. This chapter will cover more advanced topics such as access rights, file uploading, component building, and other topics that will really bring out the power of WebSnap for building full-featured dynamic Web sites.
Granting Rights on Actions Often, merely logging in a user is not enough. Sometimes, you want to further restrict the access given to logged-in users, and other times you may want to give some users rights to perform actions that other users don’t have. WebSnap provides a system for managing all this that is built into the TAdapter architecture. You can easily limit the view of pages, fields, and actions to specific users by using the AccessRights property of the TWebUserList component. To illustrate this, open up the KylixDemo application from the last chapter, and go to the home page. Add another user named “poweruser/poweruser” to the WebUserList component, and this time, put “modify” in the AccessRights. Then take the following steps: 1. Go to the GridDisplay page and delete the BrowseRow action. 2. Add the the EditRow action. Change its caption to “Details…”. Set its PageName to RecordDisplay. 3. Go back to the wdmData unit and set the ExecuteAccess property of both DatasetAdapter1.ActionDeleteRow and ActionApply to “modify” — that is, the same value you set for the AccessRights property of your new user. 4. Change the ModifyAccess property of all the Adapter fields to “modify” using the Fields Editor. 5. Go to the RecordsDisplay page, and add ActionApply and ActionDeleteRow to the AdapterCommandGroup in that page’s AdapterPageProducer.
563
564
Chapter 23: Advanced WebSnap
6. For these two new commands, add the HideOnNoExecute flag to their HideOptions properties. This will hide the buttons for users who don’t have the proper access rights in the ExecuteAccess property (which you set in Step 3). 7. Select all the fields in the AdapterFieldGroup and set their ViewMode property to vmToggleOnAccess. This will make the fields either labels or edit boxes, depending on the user’s access rights. Now, when you compile and run the new application, log in as poweruser, and click on the Details button, you are taken to the RecordDisplay page, but this time you are allowed to edit and alter the data rather than just browse it. The fields and commands are smart enough to adjust their visibility and type to the access rights of the current user. Thus, you can grant modify rights to any new users simply by adding the appropriate value to their AccessRights property. Some other things to note before you move on: when the page goes into edit mode, notice that the Graphic field provides controls to look up and then upload a new graphic for the current record. You’ll look at this a little more closely later on. Also, the text box that is used to edit the Notes field is quite small. You can fix that by adjusting the DisplayRows and DisplayWidth properties of the FldNotes in the AdapterFieldGroups component.
Granting Access to Specific Pages Sometimes, you may want to limit access to certain pages on your web site even to users who are already logged in. Access rights can be used to do this as well. Here’s how. First, add two pages to the project. Call one AccessRightsPage and call the other SorryPage. On the AccessRightsPage, add some HTML in the body that states that only authorized users are allowed to access the page. On the SorryPage, add text stating that the user is not authorized to view the page. You can take a look at the application on the companion CD-ROM to see how to do this if you aren’t sure. Save the units as wmAccessRights.pas and wmSorry.pas. Now give the AccessRightsPage unit a value that shows that the page can be viewed and seen only by users with the proper string in their AccessRights property. This is done in a rather subtle way. Earlier, you looked at the anatomy of a WebSnap page, and you examined the TWebPageInfo class. It was noted that there were some default parameters not included in the declaration that the WebSnap wizard writes for you. The default parameter of interest here is the last one, the AViewAccess parameter. Go to your AccessRightsPage and make the initialization section look this: initialization if WebRequestHandler nil then // Alter the TWebPageInfo constructor to hold the AccessRights value, // the last parameter. This isn't how the wizard does it -// it actually doesn't even give you a chance to fill in
Chapter 23: Advanced WebSnap
565
// the blank parameters below. WebRequestHandler.AddWebModuleFactory(TWebPageModuleFactory.Create( TAccessRightsPage, TWebPageInfo.Create([wpPublished, wpLoginRequired], '.html', '', '', '', 'ViewAccessRightsPage'), crOnDemand, caCache));
Notice that the code leaves three parameters blank and fills in the fourth with a value: ViewAccessRightsPage. When using default values, you have to fill in all of the parameters that are before the last parameter that you provide. In this case, since the one we want is the last one, we have to fill them all in. Filling in the AViewAccess parameter gives you a value that you can check against. If the user doesn’t have that value as part of her AccessRights, then she does not get to see the page. And as you’ll see in a little bit, we can make it so she won’t even know the page exists. Next we have to authorize a user to view the page. We do that by going to the home page and opening the WebUserList.UserItems property editor. Select “user” and set his AccessRights property to ViewAccessRightsPage. Note that this is the same value passed to the page in the code above. Leave poweruser’s AccessRights alone, and add another user, nobody/nobody, leaving his AccessRights property empty. This will allow us to test for people with the correct AccessRights, those with no AccessRights, and those with the wrong AccessRights. Next, of course, we have to check for all of these cases, and that involves a little code. Interestingly, all of it is written as event handlers for the components on the home page. The first thing we need to be able to do is to quickly get at the current user’s AccessRights property. The easiest way to do that is to make the value a field on the EndUserSessionAdapter. Go to that component on the home page and right-click on it. Select Fields Editor... and add a simple AdapterField. Call it UserRightsField. Then go to the Object Inspector and make the field’s OnGetValue event handler look like this: procedure THome.UserRightsFieldGetValue(Sender: TObject; var Value: Variant); var WebUserItem: TWebUserItem; begin Value := ''; // Assume there is no value if not VarIsEmpty(EndUserSessionAdapter.UserID) then // Don't try to access an empty variant begin WebUserItem := WebUserList.UserItems.FindUserID(EndUserSessionAdapter.UserId); // Get the data for the given user if WebUserItem nil then Value := WebUserItem.AccessRights; // Grab that user's AccessRights end; end;
Chapter 23: Advanced WebSnap
This code simply finds the current user in the WebUserList component and grabs the value for the user’s AccessRights property. It does this in a “safe” way by making sure that the pertinent values actually exist. Warning: There is a bug in the way WebSnap handles adapter fields on the EndUserSessionAdapter. If you add a field like you did above, the component will never let you log in. The way to work around this is to add all the default fields and actions to the component. Go ahead and do that now. Once you know you can always get the values, you need to compare the user’s rights with the rights required by the page. Pages are requested and dispatched via the PageDispatcher component, and it has an OnCanViewPage event. Go to that page, and make the event handler look like this:
AM FL Y
procedure THome.PageDispatcherCanViewPage(Sender: TObject; const PageName: String; var CanView, AHandled: Boolean); var aPageInfo: TAbstractWebPageInfo; UserRights, PageRights: String; begin CanView := False; // Assume that you can't view the page UserRights := UserRightsField.Value; // Get the user's AccessRights string from the EndUserSessionAdapter // Returns True if the page is found, False otherwise if WebContext.FindPageInfo(PageName, [fpLoginRequired], aPageinfo) then begin PageRights := aPageInfo.ViewAccess; // Get the value as set in the constructor end else begin PageRights := ''; end; // You can view the page if your PageRights string is part or all of your // AccessRights, or if they both are empty strings CanView := (Pos(PageRights, UserRights) >= 0); aHandled := not CanView; end;
TE
566
This code first sets the value for the local variable UserRights by calling the Adapter field that you created. Then it looks up PageInfo for the page being dispatched, and if it finds that information, grabs the ViewAccess value for that page. This is the string value passed in the TWebPageInfo constructor that you modified in the initialization section. If PageInfo is not found, the string is set to an empty value. Then it is a simple matter of checking to see if the UserAccess variable contains the value held in PageRights.
Chapter 23: Advanced WebSnap
567
Note: The user’s rights string can be in any format that you like. The fact that all the AccessRights values are strings means that you can pretty much use any scheme you like to manage them. WebSnap expects that the value be a string or a series of strings separated by a semicolon, a comma, or a space, and will parse these values into a TStrings value for you at different times during a page request. The procedure then sets CanView to True if the user’s rights match those of the page. That’s all well and good, but if you run the application, log in without rights, and try to go to the AccessRightsPage, you’ll get an ugly error message that is quite unappealing. Instead, let’s steer unauthorized users to the SorryPage, where they can be informed that they aren’t authorized to view the page. That is done in the PageDispatcher’s OnPageAccessDenied event. Make the event handler look like this: procedure THome.PageDispatcherPageAccessDenied(Sender: TObject; const PageName: String; Reason: TPageAccessDenied; var Handled: Boolean); begin if Reason = adCantView then // only do something if we are here because the user is denied access begin DispatchPageName('SorryPage', Response, []); // Show them the sorry page Handled := True; // Quit dealing with this event after this end; end;
This code should be pretty self-explanatory. It merely sends users to the SorryPage when they are denied access to a page. That works nicely, but if users aren’t allowed to view a page, they shouldn’t even be given access to a link for that page, right? Doing this is a bit of a hassle, as you’ll have to change the HTML for all the pages in the application. Thus, for each of the HTML pages associated with each of the web modules, change line 32 from this: if ((e.item().Published)
to this: if ((e.item().Published) && (e.item().CanView))
Notice that this script will call the code that you wrote in the PageDispatcher — a perfect example of the power of WebSnap. You can write code and objects in Kylix that can be used seamlessly in your server-side script. In addition, you don’t want users to be able to see the SorryPage on the menu, so go to the initialization section of the wmSorry.pas unit and remove the wpPublished flag from the TWebPageInfo constructor. It then should look something like this:
568
Chapter 23: Advanced WebSnap initialization if WebRequestHandler nil then WebRequestHandler.AddWebModuleFactory( TWebPageModuleFactory.Create(TSorryPage, TWebPageInfo.Create([{wpPublished,} wpLoginRequired], '.html'), crOnDemand, caCache));
Now, when you run the application, the AccessRightsPage link in the standard menu will not appear unless you are logged in and you have the right to view the page. You can easily limit access to any pages that you add to this application by setting the page’s AccessRights property and the user’s access rights. Note that the solution given here is not page specific — it doesn’t limit access to the AccessRightsPage only, but to any page in the application that has a value set in its ViewAccess property. Thus, if you have other pages that you want to limit, merely set the parameter in the TWebPageInfo constructor for that page, and the code above will take care of the rest.
Persistent Sessions You may have noticed that while your demo application is able to remember your user’s information between HTTP requests, it is not able to remember the user information between sessions. If your user logs out or his session expires, his information is lost for good. This probably is not the behavior you want, as your users will not want to enter this information in every time they come back to your site. Instead, you can save your user’s session information in a database for later retrieval. For this application, you’ll use an Interbase table and dbExpress to store the data. The Interbase table will be quite simple, as the TCookieSessionsService component provides a means for persisting session information to any stream. Therefore, we’ll need a table with only two fields — a varchar field to hold the user name and a Blob field to hold the session name. That way, we can use the TBlobStream class to write all the session information out to the table. The advantage here is that you can change and add to the session fields all you want as your site expands, and this scheme will work no matter how much session information is stored. If new fields are added to the session and you retrieve an old session from the database, those new fields will simply contain their default values, usually 0 or empty strings. Session values are variants, and therefore they can be written to and read from streams. Right now you are maintaining only three pieces of information about each user, but if you decide later to store more information than that, you won’t need to change anything in the database since it is all stored as a Blob. The Interbase table is defined as follows: /* Table: USERINFO, Owner: SYSDBA */ CREATE TABLE "USERINFO"
Chapter 23: Advanced WebSnap
569
( "USERNAME" VARCHAR(20) NOT NULL, "SESSIONINFO" BLOB SUB_TYPE 0 SEGMENT SIZE 80, CONSTRAINT "USERINFOPRIMARYKEY1" PRIMARY KEY ("USERNAME") );
A sample database with this schema is included along with the code for this chapter and can be found on the CD-ROM. Next, you’ll need to set up the data access controls so you can read and write session information from the database. To do that, take the following steps: 1. Create a new web data module for the project, name it SessionDatamodule, and save the file as wdmSession.pas. 2. Drop a TSQLConnection component on the form and set its LoginPrompt property to False. 3. Double-click on the component. 4. Create a new Connection called SessionPersist of type Interbase. 5. Set the connection’s Database property to a fully qualified path that points to the PersistSession.gdb file. 6. Set the username and password to values appropriate for your system. 7. Test the connection. Once it tests successfully, close the Connections dialog. 8. Set Connected to False, and use the OnActivate and OnDeactivate event handlers to open and close the connection. 9. Drop a TSQLQuery onto the webdatamodule and name it UserInfoQuery. 10. Set its SQLConnection property to SQLConnection1. 11. Set its SQL property to "SELECT * FROM USERINFO WHERE USERNAME = :aUserName". 12. Right-click on it, bring up the fields editor, and add all the fields. 13. As with the SQLConnection, use the webdatamodule’s OnActive and OnDeactivate events to open and close the data set. Next, you’ll need two helper functions that save and read session information to a stream. They can be found in the KylixWebSnapUtils.pas unit in the code for this chapter. Put this file in with your code and add it to the uses clause of the wdmSession unit. The KylixWebSnapUtils unit is declared as follows: unit KylixWebSnapUtils; interface uses WebSess, Classes, SiteComp, SessColn, WebContnrs; procedure SaveSession(AID: TSessionID; aStream: TStream); procedure RestoreSession(AID: TSessionID; aStream: TStream);
570
Chapter 23: Advanced WebSnap implementation uses WebCntxt, HTTPApp; // Special thanks goes to Corbin Dunn of Borland's QA Department for fixing these up for me // so that they work with the TCookieSessionsService component; // Write a session to a stream procedure SaveSession(AID: TSessionID; aStream: TStream); var I: Integer; S: Tstringlist; Str: string; const cCookieSessId = 'WebSnapCookie'; {Do not localize} begin S := TStringlist.Create; // add tr try with WebContext.Response.Cookies do begin for I := 0 to Count - 1 do begin if Pos(cCookieSessId, Items[I].Name) > 0 then s.values[Copy(Items[I].Name, Length(cCookieSessId)+1, maxint)] := HttpEncode(Items[I].Value); end; end; Str := S.text; aStream.Write(Str[1], Length(Str)); finally S.Free; end; end; // Update or Add all name/value pairs from the saved session to a new or existing session procedure RestoreSession(AID: TSessionID; aStream: TStream); var I: Integer; Str: string; S: TStringList; begin if aStream.Size 0 then // if there is no data there, don't do anything begin
Chapter 23: Advanced WebSnap
571
SetLength(Str, aStream.Size); aStream.Read(Str[1], aStream.Size); // Restore each session item. S := TStringlist.Create; try S.Text := Str; for I := 0 to S.count - 1 do WebContext.Session.Values[S.Names[I]] := HttpDecode (S.values[S.Names[I]]); finally S.Free; end; end; end;
end.
Notice that the SaveSession and RestoreSession functions do exactly as their names suggest. They write out the values stored by the TCookieSessionsService component to the Blob field of USERINFO table in the PERSISTESESSION.GDB database. Thus, at any time you can save your session information, confident that it will be available the next time the user logs in. Note that the code doesn’t care how many session variables you have, what types they are, or how big they are. Any and all session information will be stored and loaded by these procedures. This means that your session information will get stored, and no matter how much you change or add to the amount of information you track for each user, all of this code will still work. To actually get these helper functions to do their job, you need to add some methods to the SessionDatamodule. Add these two methods to the unit: procedure TSessionDatamodule.SaveSessionInformation(aUserName: string); var BlobStream: TStream; TD: TTransactionDesc; begin if aUserName '' then begin with SessionDatamodule do begin if not SQLConnection1.InTransaction then begin TD.TransactionID := 1;
572
Chapter 23: Advanced WebSnap TD.IsolationLevel := xilREADCOMMITTED; SQLConnection1.StartTransaction(TD); try UserInfoQuery.Close; UserInfoQuery.Params.ParamByName('aUserName').AsString := aUserName; UserInfoQuery.Open; if not UserInfoQuery.IsEmpty then begin UserInfoQuery.Edit; end else begin // There's no entry yet for this user, so create one UserInfoQuery.Insert; UserInfoQueryUSERNAME.AsString := aUserName; end; BlobStream := UserInfoQuery.CreateBlobStream( UserInfoQuerySESSIONINFO, bmWrite); try SaveSession(Session.SessionID, BlobStream); finally BlobStream.Free; end; UserInfoQuery.Post; UserInfoQuery.ApplyUpdates(-1); SQLConnection1.Commit(TD); except SQLConnection1.Rollback(TD); end; end end; end; end; procedure TSessionDatamodule.GetSessionInformation(aUserName: string); var BlobStream: TStream; begin if aUserName '' then begin SessionDatamodule.UserInfoQuery.Close; SessionDatamodule.UserInfoQuery.Params.ParamByName('aUserName').AsString := aUserName;
Chapter 23: Advanced WebSnap
573
SessionDatamodule.UserInfoQuery.Open; if not SessionDatamodule.UserInfoQuery.IsEmpty then begin BlobStream := UserInfoQuery.CreateBlobStream( UserInfoQuerySESSIONINFO, bmRead); try RestoreSession(Session.SessionID, BlobStream); finally BlobStream.Free; end; end; end; end;
Of course, the key thing is to save and load this information at the right time. This would, naturally, be when the user logs in, when he enters new or updated session information, and when his session ends. If you cover all three of these events, your user should always see current session information between logins. The first thing that you want to do is to load information about a user when the user logs in. Naturally, WebSnap has an event that occurs at that very important moment — the TLoginFormAdapter.OnLogin event. Go to the TLoginFormAdapter on the Login page, and create an event handler for the OnLogin event. Put wdmSession in the uses clause and add code so it looks like this: procedure TLogin.LoginFormAdapter1Login(Sender: TObject; UserID: Variant); begin // Get session information for the UserID SessionDatamodule.GetSessionInformation(UserID); end;
As you can see, the event passes you the UserID of the person who just logged in. You can then use this information to do a lookup on the record in the database with that username and get the session information out of the Blob field for that record. That’s what the call to SessionDataModule.GetSessionInformation does. Note: The SessionDatamodule and associated database file turn out to be pretty useful. You can use them in any WebSnap application. Just create a new, empty database, and add the wdmSession unit to the project. You’ll be able to save session information for your new application just like this one. The unit is not specific to any session implementation or set of session values.
574
Chapter 23: Advanced WebSnap
But what about when the data changes? You can easily save the data when the user enters it. Go to the UserInformation page in the wmUserInfo unit, put wdmSession in the uses clause, and add the following to the very end of the SubmitPrefsActionExecute procedure. The procedure should look like this: procedure TUserInformation.SubmitPrefsActionExecute(Sender: TObject; Params: TStrings); var Value: IActionFieldValue; i: integer; SL: TStringList; begin ... // Save this new session information to the database. SessionDatamodule.SaveSessionInformation( WebContext.EndUser.DisplayName); end;
There’s one other place that we need to add some code. The code we’ve written so far saves user preference data right after a user updates it, but your application may change that data as the user runs it, so you’ll want to save the data one other time — when the session ends, either as a result of the user logging out or when the session expires. The SessionServices.OnEndSession event fires on both occasions, so it is perfect. Go to the home page of the application, create an event handler for the event, and make it look like this: procedure THome.SessionsServiceEndSession(ASender: TObject; ASession: TAbstractWebSession; AReason: TEndSessionReason); begin SessionDatamodule.SaveSessionInformation( EndUserSessionAdapter.UserID); end;
Note that you’ll have to add the wdmSession unit to the uses clause of the wmHome unit as well. Now when you run the application, log in and set the user information, log out, and log back in, your data is saved. You can log in as any user, and the data will be saved automatically. If you add new users, their information will be handled seamlessly as well.
Chapter 23: Advanced WebSnap
575
Image Handling Practically every Web application displays graphics. Graphics can enhance your applications’ appeal and functionality. Naturally, WebSnap makes including images and graphics in your applications as easy as, well, everything else WebSnap does. As you might expect, WebSnap will allow you to use graphics and images from any source you like — files, resources, database streams, etc. If your image data can be put into a stream, it can be used in a WebSnap application. Use the Internet toolbar to add another page to your application. Use a TAdapterPageProducer, publish the page, and make users log in to gain access to it. Then name the page Images, and save the resulting unit as wmImages. Go to the Images web module and add a TAdapter to the module, and give it the name ImageAdapter. Then, double-click on ImageAdapter, and add two fields of type TAdapterImageField. Each of these will show a different way to display images. First, you can display an image based on a URL. Highlight the first AdaperImageField, and set the HREF property to a URL that points to an image on your system or anywhere on the Internet. For instance, if you want to look at the one-year history of the stock price of Krispie Kreme Donuts, set the HREF property to: http://chart/yahoo.com/c/1y/k/kkd.gif Double-click on the TAdapterPageProducer in the Images web module, add an AdapterForm, and then add an AdapterFieldGroup. Set its adapter property to the ImageAdapter. Then right-click again on the AdapterFieldGroup and select Add All Fields. Set the ReadOnly field of the AdapterImageField to true. If this property is true, it will display the image on your page. If it is set to false, it will give you an edit box and a button to look up a filename. Obviously, to see images, you should set this property to true. When you first look at the image, you will notice that the image has a pesky little caption. Most often you won’t want that, so to get rid of it, set the Caption property to a single space. (Note that it won’t accept a blank caption.) You can now see the chart appear in the Code Editor like so:
Figure 23-1: The IDE HTML Preview tab showing a graphic in a TAdapterPageProducer based on a URL
576
Chapter 23: Advanced WebSnap
Now you can display images based on a URL. Other times, however, you may want to get an image from a stream. The AdapterImageField component provides support for that as well. Select the second AdapterImageField from your ImageAdapter and open the Object Inspector. Go to the events page, and double-click on OnGetImage. Put a JPG image in the DSO directory (the demo on the CD-ROM uses athena.jpg), and make your event handler look this: procedure TImages.AdapterImageField2GetImage(Sender: TObject; Params: TStrings; var MimeType: String; var Image: TStream; var Owned: Boolean); begin MimeType := 'image\jpg'; Image := TFileStream.Create('athena.jpg', fmOpenRead); end;
TE
File Uploading
AM FL Y
This code is quite simple; Image is a stream variable that you create and fill with an image. Of course, the application needs to know what type of image it is getting, so you can return that information in the MimeType parameter. A TFileStream is a simple solution, but you could get the image from any source, such as BlobStream from a database, or build the image on the fly and return it in a memory stream. Now when you run the application, you should see the JPG you chose right below the stock graphic.
In the past, one of the more challenging tasks for a web application developer has been uploading files from the client to the server. It often involved dealing with the very arcane features of the HTTP specification, and counting very carefully every byte passed. As you would expect, WebSnap makes this previously difficult task easy. WebSnap provides all the functionality for uploading a file inside a TAdapter, and your part is not much more difficult than placing the incoming file into a stream. To illustrate this, you can create a web page that will upload a JPEG file from the client to the server. Create another page in your application that will upload files to the server from the client. Name the page Upload, and give it a TAdapterPageProducer. Then save the file as wmUpload. Then, drop a TAdapter on the form and name it UploadAdapter. Give the TAdapter a new AdapterFileField. This field will manage all the uploading of the files selected on the client. In addition, give the Adapter a single action and call it UploadAction. Next, give the AdapterPageProducer an AdapterForm with an AdapterErrorList, an AdapterFieldGroup, and an AdapterCommandGroup. Connect the first two to the UploadAdapter, and the AdapterCommandGroup to the AdapterFieldGroup. Then add all the fields to the AdapterFieldGroup and all the actions to the AdapterCommandGroup. Change the caption on the button to “Upload File”.
Chapter 23: Advanced WebSnap
577
There are two places to add code. The first is to the UploadAdapter.AdapterFileField OnFileUpload event handler. The code there should look like this: procedure TUpload.AdapterFileField1UploadFiles(Sender: TObject; Files: TUpdateFileList); var i: integer; CurrentDir: string; Filename: string; FS: TFileStream; begin // Upload file here if Files.Count